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