Arquitectura MVVM en aplicaciones móviles Flutter

Comencé a aprender Flutter y recientemente pasé todo el día tratando de incorporar la arquitectura Model-View-ViewModel en mi aplicación Flutter. Generalmente escribo para Android en Java, implemento MVVM usando AndroidViewModel y LiveData / MutableLiveData. Es decir, hay experiencia en la programación y aplicación del patrón, la aplicación es un temporizador simple. Así que nada presagiaba tanto tiempo dedicado a una tarea simple.


La búsqueda de artículos e instrucciones sobre MVVM en Flutter (sin usar RxDart) dio un ejemplo, sin referencia a la fuente completa, por lo que quiero que sea un poco más fácil para aquellos que estén interesados ​​en estudiar este patrón en Flutter.


Proyecto


Un proyecto sin MVVM es una sola pantalla con un temporizador de cuenta regresiva. Al presionar el botón, el temporizador comienza o se detiene según el estado. Cuando se agota el tiempo, se emite una notificación o se reproduce un sonido.


Descripción del modelo


Comencemos la implementación de MVVM, primero describí la interfaz que necesito para interactuar entre el widget y el modelo (se creó el archivo timer_view_model.dart):

abstract class TimerViewModel { Stream<bool> get timerIsActive; Stream<String> get timeTillEndReadable; Stream<bool> get timeIsOver; void changeTimerState(); } 

Es decir, quiero recibir eventos que cambien el estado del botón (detener el temporizador - continuar), para saber cuándo ha finalizado el temporizador, para obtener el tiempo que debe mostrarse en la pantalla. También quiero detener / iniciar el temporizador. Estrictamente hablando, una descripción de esta interfaz es opcional, aquí solo quiero mostrar lo que se requiere del modelo.

Implementación de ViewModel


La implementación adicional del modelo es el archivo timer_view_model_impl.dart


El temporizador funciona de hecho como un StreamController con un solo suscriptor. La base del código se toma de este artículo . Solo hay una descripción del controlador, que funciona en un temporizador y puede pausarse y reiniciarse. En general, una combinación casi perfecta. El código cambió para mi tarea:

 static Stream<DateTime> timedCounter(Duration interval, Duration maxCount) { StreamController<DateTime> controller; Timer timer; DateTime counter = new DateTime.fromMicrosecondsSinceEpoch(maxCount.inMicroseconds); void tick(_) { counter = counter.subtract(oneSec); controller.add(counter); // Ask stream to send counter values as event. if (counter.millisecondsSinceEpoch == 0) { timer.cancel(); controller.close(); // Ask stream to shut down and tell listeners. } } void startTimer() { timer = Timer.periodic(interval, tick); } void stopTimer() { if (timer != null) { timer.cancel(); timer = null; } } controller = StreamController<DateTime>( onListen: startTimer, onPause: stopTimer, onResume: startTimer, onCancel: stopTimer); return controller.stream; } 

Ahora, ¿cómo funciona el inicio y la parada del temporizador a través del modelo?

 @override void changeTimerState() { if (_timeSubscription == null) { print("subscribe"); _timer = timedCounter(oneSec, pomodoroSize); _timerIsEnded.add(false); _timerStateActive.add(true); _timeSubscription = _timer.listen(_onTimeChange); _timeSubscription.onDone(_handleTimerEnd); } else { if (_timeSubscription.isPaused) { _timeSubscription.resume(); _timerStateActive.add(true); } else { _timeSubscription.pause(); _timerStateActive.add(false); } } } 

Para que el temporizador comience a funcionar, debe suscribirse a él _timeSubscription = _timer.listen(_onTimeChange); . Detener / continuar se implementan mediante suscripciones de pausa / reanudar ( _timeSubscription.pause(); / _timeSubscription.resume(); ). Aquí, hay un registro en la secuencia de estado de actividad del temporizador _timerStateActive y una secuencia de información sobre si el temporizador estaba activado o no _timerIsEnded.

Todos los controladores de flujo requieren inicialización. También vale la pena agregar valores iniciales.

 TimerViewModelImpl() { _timerStateActive = new StreamController(); _timerStateActive.add(false); _timerIsEnded = new StreamController(); _timeFormatted = new StreamController(); DateTime pomodoroTime = new DateTime.fromMicrosecondsSinceEpoch(pomodoroSize.inMicroseconds); _timeFormatted.add(DateFormat.ms().format(pomodoroTime)); } 

Recepción de transmisiones como se describe en la interfaz:

 @override Stream<bool> get timeIsOver => _timerIsEnded.stream; @override Stream<bool> get timerIsActive { return _timerStateActive.stream; } @override Stream<String> get timeTillEndReadable => _timeFormatted.stream; 

Es decir, para escribir algo en la transmisión, necesita un controlador. Es simplemente imposible tomar y poner algo allí (una excepción es cuando el flujo se genera en una función). Y ya el widget recoge los flujos terminados, que son controlados por los controladores del modelo.


Widget y estado


Ahora al widget. ViewModel inicializado en constructor de estado

 _MyHomePageState() { viewModel = new TimerViewModelImpl(); } 

Luego, en la inicialización, se agregan escuchas para hilos:

  viewModel.timerIsActive.listen(_setIconForButton); viewModel.timeIsOver.listen(informTimerFinished); viewModel.timeTillEndReadable.listen(secondChanger); 

Los oyentes son casi las mismas funciones que antes, solo se agregó la comprobación nula y _setIconForButton cambió un poco:

 Icon iconTimerStart = new Icon(iconStart); Icon iconTimerPause = new Icon(iconCancel); void _setIconForButton(bool started) { if (started != null) { setState(() { if (started) { iconTimer = iconTimerPause; } else { iconTimer = iconTimerStart; } }); } } 

El resto de los cambios en main.dart es la eliminación de toda la lógica del temporizador; ahora vive en ViewModel.

Conclusión


Mi implementación MVVM no usa widgets adicionales (como StreamBuilder), la composición de los widgets sigue siendo la misma. La situación es similar a cómo Android usa ViewModel y LiveData. Es decir, el modelo se inicializa, luego se agregan los oyentes que ya reaccionan a los cambios del modelo.


Repositorio de proyectos con todos los cambios.

Source: https://habr.com/ru/post/es427327/


All Articles