Arquitetura MVVM em aplicativos móveis Flutter

Comecei a aprender Flutter e recentemente passei o dia inteiro tentando incorporar a arquitetura Model-View-ViewModel no meu aplicativo Flutter. Normalmente, escrevo para Android em Java, implementei o MVVM usando AndroidViewModel e LiveData / MutableLiveData. Ou seja, existe experiência em programação e aplicação do padrão, o aplicativo é um cronômetro simples. Portanto, nada prenunciava tanto tempo gasto em uma tarefa simples.


A pesquisa de artigos e instruções sobre o MVVM no Flutter (sem usar o RxDart) deu um exemplo, sem referência à fonte completa, por isso quero facilitar um pouco para quem está interessado em estudar esse padrão no Flutter.


Projeto


Um projeto sem MVVM é uma única tela com um temporizador de contagem regressiva. Pressionando o botão, o cronômetro inicia ou pausa dependendo do estado. Quando o tempo acaba, uma notificação é emitida ou um som é reproduzido.


Descrição do modelo


Vamos começar a implementação do MVVM, primeiro descrevi a interface que eu preciso para interagir entre o widget e o modelo (o arquivo timer_view_model.dart foi criado):

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

Ou seja, desejo receber eventos que alterem o estado do botão (pare o cronômetro - continue), para saber quando o cronômetro terminou, para obter o tempo que precisa ser exibido na tela. Também quero parar / iniciar o cronômetro. Estritamente falando, uma descrição dessa interface é opcional, aqui só quero mostrar o que é necessário para o modelo.

Implementação do ViewModel


A implementação adicional do modelo é o arquivo timer_view_model_impl.dart


O timer funciona de fato como um StreamController com um único assinante. A base para o código é retirada deste artigo . Há apenas uma descrição do controlador, que funciona em um timer e pode ser pausado e iniciado novamente. Em geral, uma combinação quase perfeita. O código foi alterado para minha tarefa:

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

Agora, como funciona o início e a parada do timer através do 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 o cronômetro comece a funcionar, é necessário assinar _timeSubscription = _timer.listen(_onTimeChange); . Parar / continuar são implementados através da pausa / retomar a assinatura ( _timeSubscription.pause(); / _timeSubscription.resume(); ). Aqui, há um registro no fluxo de status da atividade do cronômetro _timerStateActive e um fluxo de informações sobre se o cronômetro estava ou não _timerIsEnded.

Todos os controladores de fluxo requerem inicialização. Também vale a pena adicionar valores iniciais.

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

Fluxos de recebimento, conforme descrito na interface:

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

Ou seja, para escrever algo no fluxo, você precisa de um controlador. É simplesmente impossível pegar e colocar qualquer coisa lá (uma exceção é quando o fluxo é gerado em uma função). E o widget já seleciona os fluxos concluídos, que são controlados pelos controladores do modelo.


Widget e estado


Agora para o widget. ViewModel inicializado no construtor de estado

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

Em seguida, na inicialização, os ouvintes dos threads são adicionados:

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

Os ouvintes têm quase as mesmas funções de antes, apenas a verificação nula foi adicionada e o _setIconForButton mudou um pouco:

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

O restante das alterações no main.dart é a remoção de toda a lógica do timer - agora ela está no ViewModel.

Conclusão


Minha implementação do MVVM não usa widgets adicionais (como StreamBuilder), a composição dos widgets permanece a mesma. A situação é semelhante à maneira como o Android usa o ViewModel e o LiveData. Ou seja, o modelo é inicializado, os ouvintes que já reagem às alterações do modelo são adicionados.


Repositório do projeto com todas as alterações

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


All Articles