¿Cómo manejar la navigation basada en UI en aplicaciones Cross Platform?

Suponga que tiene una aplicación multiplataforma. La aplicación se ejecuta en Android y en iOS. Su idioma compartido en ambas plataforms es Java. Normalmente escribiría su lógica de negocio en Java y en toda su parte específica de la interfaz de usuario en Java (para Android) y Objective-C (para iOS).

Por lo general, cuando implementa el patrón MVP en una plataforma multiplataforma, la aplicación de lenguaje cruzado tendría el Modelo y el presentador en Java y proporcionará una interfaz Java para sus Vistas que sus presentadores conocen. De esta forma, sus presentadores de Java compartidos pueden comunicarse con la implementación de vista que use en la parte específica de la plataforma.

Asummos que queremos escribir una aplicación de iOS con una parte de Java que podría compartirse más adelante con la misma aplicación de Android. Aquí hay una representación gráfica del layout:

introduzca la descripción de la imagen aquí

En el lado izquierdo está la parte de Java. En Java, escribe tus models, controlleres y tus interfaces de vista. Haces todo el cableado usando la dependency injection. Entonces, el código de Java se puede traducir a Objective-C usando J2objc .

En el lado derecho, tienes la parte Objective-C. Aquí, su UIViewController puede implementar las interfaces de Java que se traducen a los protocolos de ObjectiveC.

Problema:

Lo que estoy luchando es cómo se lleva a cabo la navigation entre vistas. Suponga que está en UIViewControllerA y toca un button que lo llevará a UIViewControllerB. ¿Qué harías?

Caso 1:

introduzca la descripción de la imagen aquí

Indica que el button presiona el Controlador Java A (1) de UIViewControllerA y el Controlador Java llama Java ControllerB (2) que está vinculado a UIViewControllerB (3). A continuación, tiene el problema de que no sabe, desde el lado del controller de Java, cómo insert UIViewControllerB en la jerarquía Objective-C View. No puede manejar eso desde el lado de Java porque solo tiene acceso a las interfaces de Vista.

Caso 2:

introduzca la descripción de la imagen aquí

Puede hacer la transición a UIViewControllerB ya sea modal o con un UINavigationController o lo que sea (1). Entonces, primero necesita la instancia correcta de UIViewControllerB que se une al Java ControllerB (2). De lo contrario, UIViewControllerB no podría interactuar con Java ControllerB (2,3). Cuando tenga la instancia correcta, debe decirle a Java ControllerB que se ha revelado la Vista (UIViewControllerB).

Todavía estoy luchando con este problema de cómo manejar la navigation entre diferentes controlleres.

¿Cómo puedo modelar la navigation entre diferentes controlleres y manejar los cambios de la plataforma cruzada de manera adecuada?

Respuesta corta:

Así es como lo hacemos:

  • Para cosas simples "normales" (como un button que abre la camera del dispositivo o abre otro 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 compartida de la aplicación si es necesario.
  • Para cualquier cosa más compleja o dependiente de la lógica, estamos utilizando 2 opciones:
    1. ActivityA llama a un método de alguna UseCase que devuelve una enum o public static final int y toma alguna acción en consecuencia -O-
    2. Said UseCase puede llamar a un método de ScreenHandler que registramos anteriormente, que sabe cómo abrir Activities comunes desde cualquier lugar de la aplicación con algunos parameters suministrados.

Respuesta larga:

Soy el desarrollador principal de una empresa que utiliza una biblioteca de Java para los models, la lógica y las reglas comerciales de la aplicación, que ambas plataforms mobilees (Android e iOS) implementan usando j2objc.

Mis principios de layout provienen directamente de Tío Bob y SOLID, realmente me disgusta el uso de MVP o MVC al diseñar aplicaciones completas completas con comunicaciones entre componentes porque luego comienzas a vincular cada Activity con 1 y solo 1 Controller que a veces está bien pero la mayoría de las veces que terminas con un Objeto de Dios de un controller que tiende a cambiar tanto como una View . Esto puede conducir a olores de código graves.

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

Además, no veo una razón para vincular cada acción de una Activity a un Controller sentado en la capa lógica, si esta acción es un simple "llevame a la pantalla del map" o cualquier cosa de este tipo. El rol de la Activity debería estar manejando 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 que no pueda llamar al inicio de la próxima actividad en sí misma.

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

Una vez más, mi objective es que cada componente de la aplicación sea como SRP (Principio de Responsabilidad Única) como puede ser, y esto significa vincular tan pocas cosas como sea posible.

Entonces, un ejemplo de cosas simples "normales":

(todos los ejemplos son totalmente imaginarios)

ActivityAllUsers muestra una list de elementos del object model. Esos ítems provienen de llamar a AllUsersInteractor , un UseCase controller en un subprocess de retorno (que a su vez también maneja la java lib con un envío al hilo principal cuando se completa la request). El usuario hace clic en uno de los elementos de esta list. En este ejemplo, ActivityAllUsers ya tiene el object model, por lo que abrir ActivityUserDetail es una llamada directa con un package (u otro mecanismo) de este object de model de datos. La nueva actividad, ActivityUserDetail , es responsable de crear y usar los UseCases correctos si se necesitan más acciones.

Ejemplo de llamada lógica compleja:

ActivityUserDetail tiene un button titulado "Agregar como amigo", que cuando se hace clic en llamadas al método de callback 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 recibe una notificación de inserción. Esto se envía a NotificationHandler que se encuentra 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 plataforms. Esta notificación de inserción se analiza y se ScreenHandler el método correcto en ScreenHandler para abrir la Activity correcta.

La conclusión es: mantener la View más estúpida posible, 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 (¡en plural!), Y todo lo demás debería escribirse ( con suerte test-first;)) en java lib.

Usando estos methods, nuestra aplicación actualmente ejecuta aproximadamente el 60-70% de su código en la java lib, con la próxima actualización debería llevarlo al 70-80% con suerte.

En el desarrollo de plataforms cruzadas, lo que yo llamo un "núcleo" (el dominio de su aplicación escrito en Java), tiendo a dar a la propiedad de la interfaz de usuario qué vista mostrar después. Eso hace que su aplicación sea más flexible, adaptándose al entorno según sea necesario (Uso de UINavigationController en iOS, Fragments en Android y una sola página con contenido dynamic en la interfaz web).

Sus controllers no deben estar vinculados a una vista, sino que cumplen una function específica (un accountController para logins / logouts, un recipeController para mostrar y editar una receta, etc.).

Tendrías interfaces para tus controllers lugar de tus views . Entonces podría usar el patrón de layout de fábrica para crear una instancia de 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 reference a la controller factory su dominio , y la usa para proporcionar a la vista solicitada algunos controlleres que implementan interfaces específicas.

introduzca la descripción de la imagen aquí

Ejemplo: después de presionar un button de "inicio de session", un homeViewController le pide a la ViewControllerFactory un loginViewController . Esa fábrica, a su vez, le pregunta a la ControllerFactory si un controller implementa la interfaz accountHandling . A continuación, loginViewController una instancia de un nuevo loginViewController , le proporciona el controller que acaba de recibir y devuelve el controller de vista recién instanciado al homeViewController . El homeViewController presenta el nuevo controller de vista al usuario.

Dado que su "núcleo" es independiente del entorno y solo contiene su dominio y lógica empresarial, debe permanecer estable y less propenso a las ediciones.

Podrías echar un vistazo a este proyecto de demostración simplificado que hice, que ilustra esta configuration (less las interfaces).

Recomendaría que uses algún tipo de mecanismo de ranura. Similar a lo que usan otros frameworks MVP.

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

En su presentador puede definir tantos espacios como desee:

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

Estos espacios deben tener una reference en la vista del presentador. Puedes implementar un

 setInSlot(Object slot, View v); 

método. Si implementa setInSlot en una vista, la vista puede decidir cómo debería includese.

Eche un vistazo a cómo se implementan los espacios aquí .