Ich habe angefangen, Flutter zu lernen und habe kürzlich den ganzen Tag damit verbracht, die Model-View-ViewModel-Architektur in meine Flutter-Anwendung zu integrieren. Normalerweise schreibe ich für Android in Java und implementiere MVVM mit AndroidViewModel und LiveData / MutableLiveData. Das heißt, es gibt Erfahrung in der Programmierung und Anwendung des Musters, die Anwendung ist ein einfacher Timer. Nichts ahnte also, wie viel Zeit für eine einfache Aufgabe aufgewendet wurde.
Die Suche nach Artikeln und Anweisungen zu MVVM in Flutter (ohne Verwendung von RxDart) ergab ein Beispiel ohne Bezugnahme auf die vollständige Quelle. Ich möchte es daher für diejenigen, die dieses Muster in Flutter studieren möchten, etwas einfacher machen.
Projekt
Ein Projekt ohne MVVM ist ein einzelner Bildschirm mit einem Countdown-Timer. Durch Drücken der Taste startet oder pausiert der Timer je nach Status. Wenn die Zeit abgelaufen ist, wird eine Benachrichtigung ausgegeben oder ein Ton abgespielt.
Modellbeschreibung
Beginnen wir mit der Implementierung von MVVM. Zuerst habe ich die Schnittstelle beschrieben, die ich für die Interaktion zwischen dem Widget und dem Modell benötige (die Datei timer_view_model.dart wurde erstellt):
abstract class TimerViewModel { Stream<bool> get timerIsActive; Stream<String> get timeTillEndReadable; Stream<bool> get timeIsOver; void changeTimerState(); }
Das heißt, ich möchte Ereignisse empfangen, die den Status der Schaltfläche ändern (Timer stoppen - fortfahren), um zu wissen, wann der Timer abgelaufen ist, um die Zeit zu erhalten, die auf dem Bildschirm angezeigt werden muss. Ich möchte auch den Timer stoppen / starten. Genau genommen ist eine Beschreibung dieser Schnittstelle optional. Hier möchte ich nur zeigen, was für das Modell erforderlich ist.
ViewModel-Implementierung
Eine weitere Implementierung des Modells ist die Datei timer_view_model_impl.dart
Der Timer arbeitet tatsächlich als StreamController mit einem einzelnen Teilnehmer. Die Basis für den Code ist diesem Artikel entnommen. Es gibt nur eine Beschreibung des Controllers, der mit einem Timer arbeitet und angehalten und erneut gestartet werden kann. Im Allgemeinen eine fast perfekte Übereinstimmung. Der Code hat sich für meine Aufgabe geändert:
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; }
Wie funktioniert nun das Starten und Stoppen des Timers durch das Modell:
@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); } } }
Damit der Timer funktioniert, müssen Sie ihn abonnieren.
_timeSubscription = _timer.listen(_onTimeChange);
. Stop /
_timeSubscription.pause();
werden durch Pause /
_timeSubscription.pause();
Abonnements (
_timeSubscription.pause();
/
_timeSubscription.resume();
)
_timeSubscription.resume();
. Hier gibt es einen Datensatz im Aktivitätsstatusstrom des Timers _timerStateActive und einen Informationsfluss darüber, ob der Timer aktiviert war oder nicht _timerIsEnded.
Alle Durchflussregler müssen initialisiert werden. Es lohnt sich auch, Anfangswerte hinzuzufügen.
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)); }
Empfangen von Streams wie in der Benutzeroberfläche beschrieben:
@override Stream<bool> get timeIsOver => _timerIsEnded.stream; @override Stream<bool> get timerIsActive { return _timerStateActive.stream; } @override Stream<String> get timeTillEndReadable => _timeFormatted.stream;
Das heißt, um etwas in den Stream zu schreiben, benötigen Sie einen Controller. Es ist einfach unmöglich, irgendetwas dort abzulegen (eine Ausnahme ist, wenn der Stream in einer Funktion generiert wird). Und schon nimmt das Widget die fertigen Flows auf, die von den Modellcontrollern gesteuert werden.
Widget und Status
Nun zum Widget. ViewModel im Statuskonstruktor initialisiert
_MyHomePageState() { viewModel = new TimerViewModelImpl(); }
Bei der Initialisierung werden dann Listener für Threads hinzugefügt:
viewModel.timerIsActive.listen(_setIconForButton); viewModel.timeIsOver.listen(informTimerFinished); viewModel.timeTillEndReadable.listen(secondChanger);
Listener sind fast die gleichen Funktionen wie zuvor, nur die Nullprüfung wurde hinzugefügt und _setIconForButton wurde ein wenig geändert:
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; } }); } }
Der Rest der Änderungen in main.dart ist das Entfernen aller Timer-Logik - jetzt lebt es im ViewModel.
Fazit
In meiner MVVM-Implementierung werden keine zusätzlichen Widgets (z. B. StreamBuilder) verwendet. Die Zusammensetzung der Widgets bleibt unverändert. Die Situation ähnelt der Verwendung von ViewModel und LiveData durch Android. Das heißt, das Modell wird initialisiert, und dann werden Listener hinzugefügt, die bereits auf Modelländerungen reagieren.
Projekt-Repository mit allen Änderungen