Considere las características de este enfoque y nuestra implementación utilizando widgets, su concepto, ventajas y diferencias con respecto a otras vistas en Android.
Interfaz de usuario basada en backend: un enfoque que le permite crear componentes de interfaz de usuario basados en la respuesta del servidor. La descripción de la API debe contener los tipos de componentes y sus propiedades, y la aplicación debe mostrar los componentes necesarios según los tipos y propiedades. En general, se puede incorporar la lógica de los componentes, y para una aplicación móvil, son una caja negra, ya que cada componente puede tener una lógica independiente del resto de la aplicación y el servidor puede configurarlo arbitrariamente, dependiendo de la lógica empresarial requerida. Es por eso que este enfoque se usa a menudo en aplicaciones bancarias: por ejemplo, cuando necesita mostrar un formulario de traducción con una gran cantidad de campos definidos dinámicamente. La aplicación no conoce de antemano la composición del formulario y el orden de los campos, por lo tanto,Este enfoque es la única forma de escribir código sin muletas. Además, agrega flexibilidad: desde el lado del servidor, puede cambiar el formulario en cualquier momento, y la aplicación móvil estará lista para esto.Caso de uso
Los siguientes tipos de componentes se presentan arriba:- Lista de cuentas disponibles para transferir;
- El nombre del tipo de traducción;
- Campo para ingresar un número de teléfono (tiene una máscara para ingresar y contiene un icono para seleccionar contactos desde el dispositivo);
- Campo para ingresar el monto de la transferencia.
También en el formulario, es posible cualquier cantidad de otros componentes integrados en la lógica de negocios y determinados en la etapa de diseño. La información sobre cada componente que viene en la respuesta del servidor debe cumplir con los requisitos, y la aplicación móvil debe esperar que cada componente lo procese correctamente.
Los diferentes campos de entrada tienen máscaras y reglas de validación diferentes; el botón puede tener una animación brillante en el momento del arranque; un widget para seleccionar una cuenta de pago puede tener animación al desplazarse, etc.Todos los componentes de la interfaz de usuario son independientes entre sí, y la lógica se puede tomar en vistas separadas con diferentes áreas de responsabilidad, llamémoslas widgets. Cada widget recibe su configuración en la respuesta del servidor y encapsula la lógica de visualización y procesamiento de datos.Al implementar la pantalla, RecyclerView es el más adecuado, cuyos elementos contendrán widgets. El ViewHolder de cada elemento de lista único inicializará el widget y le dará los datos que necesita para mostrar.Concepto de widget
Consideremos los widgets con más detalle. En esencia, un widget es una vista personalizada "a la máxima velocidad". Una vista personalizada ordinaria también puede contener datos y la lógica de su visualización, pero el widget implica algo más: tiene un presentador, un modelo de pantalla y tiene su propio alcance DI.Antes de sumergirnos en los detalles de la implementación de widgets, discutimos sus ventajas:- , , «» , — UI- , .
- , , .
- , — : , , .
Para implementar widgets escalables, utilizamos las clases base para los ViewGroups más utilizados, que tienen sus propios ámbitos DI. Todas las clases base, a su vez, se heredan de la interfaz común, que contiene todo lo necesario para inicializar widgets.El caso más fácil para usar widgets son las vistas estáticas, especificadas directamente en el diseño. Después de implementar las clases de widgets, puede agregarlo de manera segura al diseño XML, sin olvidar especificar su identificación en el diseño (en función de la identificación, se formará el alcance DI de un widget).En este artículo, consideramos los widgets dinámicos con más detalle, ya que el caso del formulario de traducción descrito anteriormente con un conjunto arbitrario de campos se resuelve convenientemente con su ayuda.Cualquier widget, tanto estático como dinámico, en nuestra implementación casi no es diferente de la vista ordinaria en términos de MVP. Por lo general, se necesitan 4 clases para implementar un widget:- Ver clase, donde se produce el diseño del diseño y la visualización del contenido;
class TextInputFieldWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : CoreFrameLayoutView(context, attrs) {
@Inject
lateinit var presenter: TextInputFieldPresenter
…
init {
inflate(context, R.layout.view_field_text_input, this)
}
}
- Una clase para el presentador, donde se describe la lógica básica del widget, por ejemplo:
- cargar datos y transmitirlos para un render;
- Suscribirse a varios eventos y emitir eventos de cambios de entrada de widget;
@PerScreen
class TextInputFieldPresenter @Inject constructor(
basePresenterDependency: BasePresenterDependency,
rxBus: RxBus
) : BaseInputFieldPresenter<TextInputFieldWidget>(
basePresenterDependency, rxBus
) {
private val sm = TextInputFieldScreenModel()
...
}
En nuestra implementación, la clase RxBus es un bus basado en PublishSubject para enviar eventos y suscribirse a ellos.
- Una clase para el modelo de pantalla, con la ayuda de la cual el presentador recibe datos y los transfiere para representarlos en una vista (en términos del patrón del Modelo de presentación);
class TextInputFieldScreenModel : ScreenModel() {
val value = String = “”
val hint = String = “”
val errorText = String = “”
}
- Una clase de configurador para implementar DI, con la ayuda de qué dependencias para el widget se entregan que tienen el alcance deseado, y el presentador se inyecta en su vista.
class TextInputFieldWidgetConfigurator : WidgetScreenConfigurator() {
}
La única diferencia entre los widgets y nuestra implementación de pantallas completas (Actividad, Fragmento) es que el widget no tiene muchos métodos de ciclo de vida (onStart, onResume, onPause). Solo tiene el método onCreate, que muestra que el widget ha creado actualmente su alcance, y la primicia se destruye en el método onDetachedFromWindow. Pero por conveniencia y consistencia, el presentador del widget obtiene los mismos métodos de ciclo de vida que el resto de las pantallas. Estos eventos se le transmiten automáticamente desde el padre. Cabe señalar que la clase base del presentador de widgets es la misma clase base de presentadores de otras pantallas.Usando widgets dinámicos
Pasemos a la implementación del caso descrito al comienzo del artículo.- En el presentador de pantalla, se cargan los datos para el formulario de traducción, los datos se transmiten a la vista para su representación. En esta etapa, no nos importa si la vista de la pantalla de actividad es un fragmento o un widget. Solo estamos interesados en tener RecyclerView y renderizar un formulario dinámico con él.
private val sm = TransferFormScreenModel()
…
private fun loadData() {
loadDataDisposable.dispose()
loadDataDisposable = subscribe(
observerDataForTransfer().io(),
{ data ->
sm.data = data
view.render(sm)
},
{ error -> }
)
}
- Los datos del formulario se transfieren al adaptador de lista y se procesan utilizando widgets que se encuentran en ViewHolder para cada elemento de formulario único. El ViewHolder deseado para representar el componente se determina en función de los tipos predefinidos de componentes de formulario.
fun render(sm: TransferFormScreenModel) {
val list = ItemList.create()
sm.data
.filter { transferField -> transferField.visible }
.forEach { transferField ->
when (transferField.type) {
TransferFieldType.PHONE_INPUT -> {
list.add(
PhoneInputFieldData(transferField),
phoneInputController
)
}
TransferFieldType.MONEY -> {
list.add(
MoneyInputFieldData(transferField),
moneyInputController
)
}
TransferFieldType.BUTTON -> {
list.add(
ButtonInputFieldData(transferField),
buttonController
)
}
else -> {
list.add(
TextInputFieldData(transferField),
textInputController
)
}
}
}
adapter.setItems(list)
}
- El widget se inicializa en el método de enlace de ViewHolder. Además de transmitir datos para el render, también es importante establecer una identificación única para el widget, sobre la base de la cual se formará su alcance DI. En nuestro caso, cada elemento del formulario tenía una identificación única, que era responsable de la designación de entrada y respondía además del tipo de elemento (los tipos se pueden repetir en el formulario).
override fun bind(data: TransferFieldUi) {
itemView.findViewById(R.id.field_tif).initialize(...)
}
- El método initialize inicializa los datos de la vista del widget, que luego se transmiten al presentador utilizando el método del ciclo de vida onCreate, donde los valores de campo se establecen para el modelo del widget y su representación.
fun initialize(
id: String = this.id,
value: String = this.value,
hint: String = this.hint,
errorText: String = this.errorText
) {
this.id = id
this.value = value
this.hint = hint
this.errorText = errorText
}
override fun onCreate() {
presenter.onCreate(value, hint, errorText)
}
fun onCreate(value: String, hint: String, errorText: String) {
sm.value = value
sm.hint = hint
sm.errorText = errorText
view.render(sm)
}
Rocas bajo el agua
Como se puede ver en la descripción, la implementación es muy simple e intuitiva. Sin embargo, hay matices que deben considerarse.Considere el ciclo de vida de los widgets
Dado que las clases base de widgets son herederos de los ViewGroups de uso común, también conocemos el ciclo de vida de los widgets. Por lo general, los widgets se inicializan en ViewHolder llamando a un método especial donde se transfieren los datos, como se muestra en el párrafo anterior. Otra inicialización tiene lugar en onCreate (por ejemplo, configurando escuchas de clics): este método se llama después de onAttachedToWindow utilizando un delegado especial que controla las entidades clave de la lógica del widget.
public abstract class CoreFrameLayoutView
extends FrameLayout implements CoreWidgetViewInterface {
…
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isManualInitEnabled) {
widgetViewDelegate = createWidgetViewDelegate();
widgetViewDelegate.onCreate();
}
}
public void onCreate() {
}
public class WidgetViewDelegate {
…
public void onCreate() {
coreWidgetView.onCreate();
}
Siempre limpia a los oyentes
Si hay campos dependientes en el formulario, es posible que necesitemos onDetachedFromWindow. Considere el siguiente caso: el formulario de traducción tiene muchos campos, entre los cuales hay una lista desplegable. Dependiendo del valor seleccionado en la lista, un campo de entrada de formulario adicional puede hacerse visible o uno existente puede desaparecer.En el caso descrito anteriormente, es muy importante borrar todos los oyentes de widgets en el método onDetachedFromWindow, ya que si agrega el widget nuevamente a la lista, todos los oyentes se reiniciarán.
override fun onCreate() {
initListeners()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
clearListeners()
}
Manejar suscripciones de eventos de widget correctamente
La presentación de la pantalla que contiene los widgets debe ser notificada de los cambios en la entrada de cada widget. La implementación más obvia de cada widget usando eventos de emisión y suscribiéndose a todos los eventos con un presentador de pantalla. El evento debe contener la identificación del widget y sus datos. Es mejor implementar esta lógica para que los valores de entrada actuales se guarden en el modelo de pantalla y cuando haga clic en el botón, los datos terminados se envíen en la solicitud. Con este enfoque, es más fácil implementar la validación del formulario: ocurre cuando se hace clic en un botón y, si no hubo errores, la solicitud se envía con los datos del formulario guardados de antemano.
private val defaultTextChangedListener = object : OnMaskedValueChangedListener {
override fun onValueChanged(value: String) {
presenter.onTextChange(value, id)
}
}
sealed class InputValueType(val id: String)
class TextValue(id: String, val value: String) : InputValueType(id)
class DataEvent(val data: InputValueType)
fun onTextChange(value: String, id: String) {
rxBus.emitEvent(DataEvent(data = TextValue(id = id, value = value)))
}
private fun subscribeToEvents() {
subscribe(rxBus.observeEvents(DataEvent::class.java))
{
handleValue(it.data) // handle data
}
}
private fun handleValue(value: InputValueType) {
val id = value.id
when (value) {
// handle event using its type, saving event value using its id
is TextValue -> {
sm.fieldValuesMap[id] = value.value
}
else -> {
// handle other events
}
}
}
// TransferScreenModel
class TransferScreenModel : ScreenModel() {
// map for form values: key = input id
val fieldValuesMap: MutableMap<String, String> = mutableMapOf()
}
Existe una segunda opción de implementación, en la que los eventos de los widgets con sus datos solo se producen después de hacer clic en el botón, y no mientras escribe, es decir, recopilamos todos los datos inmediatamente antes de enviar la solicitud. Con esta opción, habrá muchos menos eventos, pero vale la pena señalar que esta implementación puede no ser trivial en la práctica, y se necesitará una lógica adicional para verificar si se han recibido todos los eventos.Unificar todos los requisitos
Una vez más, me gustaría señalar que el caso descrito solo es posible después de coordinar los requisitos con el backend.Qué requisitos deben ser unificados:- Tipos de campo La aplicación móvil debe esperar cada campo para su visualización y procesamiento correctos.
- — , , , .
- , .
- . , : , , — , -, .
Esto es necesario para que el componente recibido en la respuesta sea conocido por la aplicación móvil para la correcta visualización y procesamiento de la lógica.El segundo matiz es que los componentes de la forma en sí son generalmente independientes entre sí, sin embargo, algunos escenarios son posibles cuando, por ejemplo, la visibilidad de un elemento depende del estado de otro, como se describió anteriormente. Para implementar esta lógica, es necesario que los elementos dependientes siempre se unan, y la respuesta debe contener una descripción de la lógica, qué componentes dependen unos de otros y cómo. Y, por supuesto, todo esto debe acordarse con el equipo del servidor antes de comenzar el desarrollo.Conclusión
Por lo tanto, al implementar incluso un caso estándar como completar una lista dinámica, no puede detenerse en soluciones ya existentes. Para nosotros, este era un nuevo concepto, que nos permitió seleccionar piezas atómicas de lógica y representación de pantallas gigantes y logramos obtener una solución extensible que funciona y que es fácil de mantener debido a la similitud de widgets con otras vistas. En nuestra implementación, los widgets se desarrollaron en términos del patrón RxPM: después de agregar carpetas, se hizo aún más conveniente usar widgets, pero esta es una historia completamente diferente.Enlaces útiles
- Marco de desarrollo de aplicaciones de Android Surf
- Módulo de widgets
- Nuestra implementación de render simple de listas complejas
- Presentación Patrón de modelo