如何处理跨平台应用程序中基于 UI 的导航?

How to handle UI based Navigation in Cross Platform Apps?

假设您有一个跨平台应用程序。该应用程序在 Android 和 iOS 上运行。您在两个平台上的共同语言是 Java。通常,您会在 Java 中编写业务逻辑,在 Java(对于 Android)和 Objective-C(对于 iOS)中编写所有 UI 特定部分).

通常,当您在跨平台、跨语言应用程序中实现 MVP pattern 时,您会在 Java 中拥有模型和演示者,并为您的视图提供 Java 界面为您的演示者所知。通过这种方式,您共享的 Java 演示者可以与您在平台特定部分使用的任何视图实现进行通信。

假设我们要编写一个 iOS 应用程序,其中包含一个 Java 部分,稍后可以与相同的 Android 应用程序共享。这是设计的图形表示:

左边是Java部分。在 Java 中,您编写模型、控制器以及视图界面。您使用依赖注入进行所有连接。然后可以使用 J2objc 将 Java 代码转换为 Objective-C。

右侧是 Objective-C 部分。在这里,您的 UIViewController 可以实现 Java 接口,这些接口被转换为 ObjectiveC 协议。

问题:

我苦恼的是视图之间的导航是如何发生的。假设您在 UIViewControllerA 上,然后点击一个按钮,该按钮会将您带到 UIViewControllerB。你会怎么做?

案例一:

您将按钮点击报告给 UIViewControllerA 的 Java ControllerA (1),并且 Java ControllerA 调用链接到的 Java ControllerB (2) UIViewControllerB (3)。然后你有一个问题,你不知道 Java 控制器端如何在 Objective-C 视图层次结构中插入 UIViewControllerB。您无法从 Java 端处理该问题,因为您只能访问 View 界面。

案例二:

您可以过渡到 UIViewControllerB,无论它是模态的还是使用 UINavigationController 或其他 (1)。然后,首先您需要绑定到 Java ControllerB (2) 的 UIViewControllerB 的正确实例。否则 UIViewControllerB 无法与 Java ControllerB (2,3) 交互。当您拥有正确的实例时,您需要告诉 Java ControllerB 视图 (UIViewControllerB) 已显示。

我还在为如何处理不同控制器之间的导航问题而苦恼。

如何对不同控制器之间的导航进行建模并适当地处理跨平台视图更改?

简答:

我们是这样做的:

  • 对于简单的 "normal" 内容(例如打开设备摄像头或打开另一个 Activity/UIViewController 的按钮,操作背后没有任何逻辑)- ActivityA 直接打开 ActivityBActivityB 现在负责在需要时与应用程序共享逻辑层进行通信。
  • 对于任何更复杂或逻辑相关的事情,我们使用 2 个选项:
    1. ActivityA 调用一些 UseCase 的方法,其中 returns 一个 enumpublic static final int 并相应地采取一些行动 -OR-
    2. UseCase 可以调用我们之前注册的 ScreenHandler 的方法,它知道如何使用一些提供的参数从应用程序的任何地方打开公共 Activities

长答案:

我是一家公司的首席开发人员,该公司使用 java 库来处理两个移动平台(Android 和 iOS)实现的应用程序模型、逻辑和业务规则使用 j2objc.

我的设计原则直接来自 Uncle Bob 和 SOLID,我真的不喜欢在设计带有 inter-component 通信的整个应用程序时使用 MVP 或 MVC,因为那样你就开始将每个 Activity 与 1并且只有 1 Controller,这有时是可以的,但大多数时候你最终会得到一个控制器的上帝 Object,它往往会像 View 一样变化。这会导致严重的代码异味。

我最喜欢的处理方式(也是我认为最干净的一种)是将所有内容分解为 UseCases,每个处理应用程序中的 1 "situation"。当然你可以有一个 Controller 来处理其中的几个 UseCases 但它所知道的只是如何委托给那些 UseCases 而已。

另外,如果这个动作是一个简单的 "take me to the map screen" 或者诸如此类的任何事情。 Activity 的作用应该是处理它持有的 Views,并且作为应用程序生命周期中唯一存在的 "smart" 事物,我认为它没有理由不能调用下一个 activity 本身的开始。

此外,Activity/UIViewController 生命周期过于复杂且相互之间差异太大,无法由通用 java 库处理。我认为这是 "detail" 而不是真正的 "business rules",每个平台都需要实施和担心,从而使 java 库中的代码更加可靠且不易更改。

同样,我的目标是让应用程序的每个组件尽可能符合 SRP(单一职责原则),这意味着将尽可能少的东西链接在一起。

所以一个简单的例子 "normal" 东西:

(所有例子纯属虚构)

ActivityAllUsers 显示型号 object 项目的列表。这些项目来自调用 AllUsersInteractor - 后台线程中的 UseCase controller(这反过来也由 java 库处理,并在请求完成时分派到主线程)。用户单击此列表中的一项。在此示例中,ActivityAllUsers 已经具有模型 object,因此打开 ActivityUserDetail 是使用此数据模型 object 的捆绑包(或其他机制)的直接调用。如果需要进一步的操作,新的 activity、ActivityUserDetail 负责创建和使用正确的 UseCases

复杂逻辑调用示例:

ActivityUserDetail 有一个标题为 "Add as a friend" 的按钮,单击该按钮会调用 ActivityUserDetail:

中的回调方法 onAddFriendClicked
public void onAddFriendClicked() {  
  AddUserFriendInteractor addUserFriend = new AddUserFriendInteractor();
  int result = addUserFriend.add(this.user);
  switch(result){
    case AddUserFriendInteractor.ADDED:
      start some animation or whatever
      break;
    case AddUserFriendInteractor.REMOVED:
      start some animation2 or whatever
      break;
    case AddUserFriendInteractor.ERROR:
      show a toast to the user
      break;
    case AddUserFriendInteractor.LOGIN_REQUIRED:
      start the log in screen with callback to here again
      break;

  }
}

更复杂的调用示例[​​=108=]

Android 上的 BroadcastReceiver 或 iOS 上的 AppDelegate 收到推送通知。这被发送到 java 库逻辑层中的 NotificationHandler。在 App.onCreate() 构造一次的 NotificationHandler 构造函数中,它需要一个 ScreenHandler interface ,你在两个平台上都实现了它。此推送通知被解析并在 ScreenHandler 中调用正确的方法打开正确的 Activity.

底线是:让 View 尽可能地愚蠢,让 Activity 足够聪明来处理自己的生命周期和处理自己的观点,并与自己的人交流controllers(复数!),其他所有内容都应写入(希望 test-first ;))在 java 库中。

使用这些方法,我们的应用程序目前在 java 库中运行大约 60-70% 的代码,希望下一次更新能达到 70-80%。

在跨平台开发共享中,我称之为 "core"(用 Java 编写的应用程序的 ),我倾向于给UI 下一个要显示的视图的所有权。这使您的应用程序更加灵活,根据需要适应环境(在 iOS 上使用 UINavigationController,在 Android 上使用 Fragments 并在 Web 界面上使用包含动态内容的单个页面)。

您的 controllers 不应绑定到视图,而是履行特定角色([=62= 的 accountController,显示和编辑食谱的 recipeController 等)。

您的 controllers 应该是 interfaces 而不是 views。然后,您可以使用 Factory design pattern 端(您的 Java 代码)实例化您的 controllers,并在 views UI 一侧。 view factory 引用了您的 controller factory,并使用它为请求的视图提供一些实现特定接口的控制器。

示例: 点击 "login" 按钮后,homeViewControllerViewControllerFactory 请求 loginViewController。该工厂反过来向 ControllerFactory 请求实现 accountHandling 接口的控制器。然后它实例化一个新的 loginViewController,给它刚收到的控制器,并且 returns 将新实例化的视图控制器提供给 homeViewControllerhomeViewController 然后将新的视图控制器呈现给用户。

由于您的 "core" 与环境无关并且只包含您的 和业务逻辑,它应该保持稳定并且不易被编辑。

您可以查看 this simplified demo project I made,它说明了此设置(减去接口)。

我建议您使用某种插槽机制。与其他 MVP 框架使用的类似。

定义:插槽是视图的一部分,可以插入其他视图。

在您的演示器中,您可以根据需要定义任意数量的插槽:

GenericSlot slot1 = new GenericSlot();
GenericSlot slot2 = new GenericSlot();
GenericSlot slot3 = new GenericSlot();

这些插槽必须在演示者的视图中具有引用。您可以实施

setInSlot(Object slot, View v);

方法。如果您在视图中实现 setInSlot,则视图可以决定如何包含它。

看看槽是如何实现的here