¿Cómo manejar navegación basada en interfaz de usuario en aplicaciones de plataforma cruzada?

Suponga que tiene una aplicación de plataforma cruzada. La aplicación se ejecuta en Android y en iOS. Su idioma compartido en ambas plataformas es Java. Normalmente escribiría su lógica de negocio en Java y todo lo que UI parte específica en Java (para Android) y Objective-C (para iOS).

Normalmente, cuando se implementa el patrón MVP en una plataforma cruzada, la aplicación de idioma cruzado que tendría el modelo y el presentador en Java y proporcionar una interfaz Java para sus vistas que es conocido por sus presentadores. De esta forma, sus presentadores Java compartidos pueden comunicarse con cualquier implementación de vista que utilice en la parte específica de la plataforma.

Supongamos que queremos escribir una aplicación de iOS con una parte de Java que podría compartirse posteriormente con la misma aplicación de Android. Aquí está una representación gráfica del diseño:

Introduzca aquí la descripción de la imagen

En el lado izquierdo está la parte Java. En Java escribes tus modelos, controladores así como tus interfaces de vista. Haces todo el cableado usando la inyección de dependencia. Entonces el código Java puede ser traducido a Objective-C usando J2objc .

En el lado derecho tiene la parte Objetivo-C. Aquí su UIViewController 's puede implementar las interfaces de Java que se traducen en los protocolos de ObjectiveC.

Problema:

Lo que estoy luchando es cómo la navegación entre las vistas tiene lugar. Suponga que está en UIViewControllerA y pulsa en un botón que debe llevarlo a UIViewControllerB. ¿Qué harías?

Caso 1:

Introduzca aquí la descripción de la imagen

Reporta el botón tocando en Java ControllerA (1) de UIViewControllerA y Java ControllerA llama a Java ControllerB (2) que está vinculado a UIViewControllerB (3). A continuación, tiene el problema de que no sabe desde el lado del controlador de Java cómo insertar el UIViewControllerB en la jerarquía de la vista de Objective-C. No puedes manejar eso desde el lado de Java porque solo tienes acceso a las interfaces de vista.

Caso 2:

Introduzca aquí la descripción de la imagen

Puede realizar la transición a UIViewControllerB si es modal o con un UINavigationController o lo que sea (1). Entonces, primero necesita la instancia correcta de UIViewControllerB que se enlaza con el Java ControllerB (2). De lo contrario el UIViewControllerB no podría interactuar que el Java ControllerB (2,3). Cuando tenga la instancia correcta, deberá informar a Java ControllerB que se ha revelado la Vista (UIViewControllerB).

Todavía estoy luchando con este problema de cómo manejar la navegación entre los diferentes controladores.

¿Cómo puedo modelar la navegación entre diferentes controladores y manejar la plataforma cruzada Ver cambios adecuadamente?

Respuesta corta:

Aquí es cómo lo hacemos:

  • Para cosas simples "normales" (como un botón que abre la cámara del dispositivo o abre otra Activity / UIViewController sin ninguna lógica detrás de la acción) – ActivityA abre directamente ActivityB . ActivityB ahora es responsable de comunicarse con la capa lógica de la aplicación compartida si es necesario.
  • Para cualquier cosa más compleja o dependiente de la lógica estamos usando 2 opciones:
    1. ActivityA llama a un método de algún UseCase que devuelve un enum o public static final int y toma alguna acción en consecuencia -OR-
    2. Dicho UseCase puede llamar a un método de un ScreenHandler que registramos anteriormente y que sabe cómo abrir Activities comunes desde cualquier lugar de la aplicación con algunos parámetros suministrados.

Respuesta larga:

Soy el desarrollador principal de una empresa que utiliza una biblioteca java para los modelos de aplicaciones, la lógica y las reglas de negocio que ambas plataformas móviles (Android e iOS) implementan con j2objc.

Mis principios de diseño vienen directamente de Tío Bob y SOLID, realmente no me gusta el uso de MVP o MVC al diseñar aplicaciones completas con comunicaciones entre componentes, porque entonces empiezas a vincular cada Activity con 1 y sólo 1 Controller que a veces está bien, pero la mayoría de Las veces que terminas con un Objeto Divino de un controlador que tiende a cambiar tanto como una View . Esto puede conducir a graves olores de código.

Mi manera favorita (y la que encuentro más limpia) de manejar esto es romper todo en UseCases cada uno de los cuales maneja 1 "situación" en la aplicación. Seguro que usted puede tener un Controller que maneja varios de esos UseCases pero entonces todo lo que sabe es cómo delegar a esos UseCases y nada más.

Además, no veo una razón de vincular cada acción de una Activity a un Controller sentado en la capa lógica, si esta acción es un simple "llevarme a la pantalla del mapa" o cualquier cosa de este tipo. El papel de la Activity debe manejar las Views que posee y como la única cosa "inteligente" que vive en el ciclo de vida de la aplicación, no veo ninguna razón por la cual no pueda llamar al inicio de la siguiente actividad en sí.

Además, el ciclo de vida de Activity/UIViewController es demasiado complejo y demasiado diferente para ser manejado por el java lib común. Es algo que veo como un "detalle" y no realmente "reglas de negocio", cada plataforma necesita implementar y preocuparse, haciendo así que el código en la lib de java sea más sólido y no propenso a cambiar.

Una vez más, mi objetivo es que cada componente de la aplicación sea como SRP (Single Responsability Principle) como puede ser, y esto significa enlazar como pocas cosas juntos como sea posible.

Así que un ejemplo de cosas simples "normales":

(Todos los ejemplos son totalmente imaginarios)

ActivityAllUsers muestra una lista de elementos de objetos de modelo. Esos elementos procedían de llamar a AllUsersInteractor – un UseCase controller en un subproceso (que a su vez también manejado por el java lib con un despacho al hilo principal cuando se completa la solicitud). El usuario hace clic en uno de los elementos de esta lista. En este ejemplo, ActivityAllUsers ya tiene el objeto de modelo, de modo que abrir ActivityUserDetail es una llamada directa con un paquete (u otro mecanismo) de este objeto de modelo de datos. La nueva actividad, ActivityUserDetail , es responsable de crear y usar las UseCases correctas si se necesitan otras acciones.

Ejemplo de llamada lógica compleja:

ActivityUserDetail tiene un botón titulado "Añadir como amigo" que cuando se hace clic llama al método de devolución de llamada onAddFriendClicked en ActivityUserDetail :

 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; } } 

Ejemplo de llamada aún más compleja

Un BroadcastReceiver en Android o AppDelegate en iOS recibirá una notificación push. Esto se envía a NotificationHandler que está en la capa lógica java lib. En el constructor NotificationHandler que se construye una vez en App.onCreate() toma una interface ScreenHandler que implementó en ambas plataformas. Esta notificación push se analiza y se llama al método correcto en ScreenHandler para abrir la Activity correcta.

La conclusión es: mantener la View tan tonta como puedas, mantener la Activity lo suficientemente inteligente como para manejar su propio ciclo de vida y manejar sus propias vistas, y comunicarse con sus propios controllers (plural!), Y todo lo demás debería escribirse ( Espero que pruebe primero;)) en el java lib.

Usando estos métodos de nuestra aplicación en la actualidad se ejecuta alrededor del 60-70% de su código en la biblioteca java, con la siguiente actualización debe llevar a la 70-80% esperamos.

En el desarrollo de plataforma cruzada compartiendo lo que yo llamo un "núcleo" (el dominio de su aplicación escrita en Java), tienden a dar a la UI la propiedad de qué vista para mostrar a continuación. Esto hace que su aplicación sea más flexible, adaptándose al entorno según sea necesario (utilizando un UINavigationController en iOS, Fragments en Android y una sola página con contenido dinámico en la interfaz web).

Sus controllers no deben estar vinculados a una vista, sino que cumplen una función específica (un controlador de cuenta para logins / logouts, un recipeController para mostrar y editar una receta, etc.).

Tendrías interfaces para tus controllers lugar de tus views . A continuación, puede utilizar el patrón de diseño de fábrica para instanciar sus controllers en el lado del dominio (su código Java) y las views en el lado de la interfaz de usuario . La view factory tiene una referencia a la controller factory su dominio y la utiliza para dar a la vista solicitada algunos controladores que implementan interfaces específicas.

Introduzca aquí la descripción de la imagen

Ejemplo: Siguiendo un toque en un botón de "inicio de sesión", un homeViewController pregunta al ViewControllerFactory por un loginViewController . A su vez, la fábrica solicita a ControllerFactory un controlador que implemente la interfaz accountHandling . A continuación, instancia un nuevo loginViewController , le da el controlador que acaba de recibir y devuelve el controlador de vista recién instanciado al homeViewController . El homeViewController presenta entonces el nuevo controlador de vista al usuario.

Dado que su "núcleo" es agnóstico del entorno y sólo contiene su dominio y lógica de negocio, debe permanecer estable y menos propenso a las ediciones.

Podría echar un vistazo a este proyecto de demostración simplificado que he hecho que ilustra esta configuración (menos las interfaces).

Yo recomendaría que utilice algún tipo de mecanismo de ranura. Similar a lo que otros marcos MVP utilizan.

Definición: Una ranura es una parte de una vista donde se pueden insertar otras vistas.

En su presentador puede definir tantas ranuras como desee:

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

Estas ranuras deben tener una referencia en la vista del presentador. Puede implementar un

 setInSlot(Object slot, View v); 

método. Si implementa setInSlot en una vista, la vista puede decidir cómo debe incluirse.

Echa un vistazo a cómo se implementan las franjas horarias aquí .

FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.