FlutterFlutter · Chapterبەش 12 · Lectureوانە 12.1
MVVM in Flutter with ProviderMVVM لە Flutterـدا بە Provider
The MVVM layers in Flutter code: views are widgets, view models are ChangeNotifiers. The lecture's to-do app refactored in two steps (view models into the service locator, then a provider and consumer for each view), the login and to-do views as View + ViewModel, the same view without the provider package (ListenableBuilder), and Flutter's official architecture guide with repositories, commands and tests with fakes.
لە کۆدی Flutterـدا: ، ChangeNotifierن. ئەرکەکانی وانەکە بە دوو هەنگاو ڕێک دەخرێتەوە ( بۆ ناو ، پاشان provider و consumerـێک بۆ هەر )، و ئەرکەکان وەک View + ViewModel، هەمان بەبێ provider (ListenableBuilder) و ڕێنماییە فەرمییەکەی Flutter بە ، و تاقیکردنەوە بە .
What you will learnچی فێر دەبیت
- Name the Flutter class for each MVVM part: widgets, ChangeNotifier view models, model and service classes.
- Refactor a provider app: move view models into a service locator, then give each view its own provider and consumer.
- Write the login and to-do screens as View + ViewModel, keeping logic out of the widgets.
- Use a view model without the provider package (ChangeNotifier + ListenableBuilder).
- Relate the lecture's design to Flutter's guide (repositories, services, commands) and test a view model with fakes.
- Flutter بۆ هەر بەشێکی ناو ببەیت: ، ChangeNotifier، و .
- provider ڕێک بخەیتەوە: بگوازیتەوە بۆ ، پاشان provider و consumerـی خۆی بدەیت بە هەر .
- شاشەکانی و ئەرکەکان وەک View + ViewModel بنووسیت و لۆژیک لە دوور بخەیتەوە.
- بەبێ provider بەکار بهێنیت (ChangeNotifier + ListenableBuilder).
- دیزاینەکەی وانەکە ببەستیتەوە بە ڕێنماییەکەی Flutter (، ، ) و بە تاقی بکەیتەوە.
MVVM, the Flutter wayMVVM، بە شێوازی Flutter
The shared lesson App Architecture: the MVVM Pattern explains the idea without any code: a view shows the screen, a view model holds the screen's state and prepares data, a service and a model do the real work with the data, the view knows its view model, and the view model never knows the view. This lesson shows what each of those parts is in Flutter, then rebuilds the to-do app in MVVM the way the lecture does.
وانە هاوبەشەکەی تەلارسازیی نەرمەواڵە: شێوازی MVVM بیرۆکەکە بەبێ هیچ کۆدێک ڕوون دەکاتەوە: شاشەکە پیشان دەدات، شاشەکە هەڵدەگرێت و دراوە ئامادە دەکات، و کاری ڕاستەقینە لەگەڵ دراوەکەدا دەکەن، دەناسێت و هەرگیز ناناسێت. ئەم وانەیە پیشان دەدات هەریەکێک لەو بەشانە لە Flutterـدا چییە، پاشان ئەپی ئەرکەکان بە دووبارە دروست دەکاتەوە بەو شێوەیەی وانەکە دەیکات.
| MVVM partبەشی | In Flutterلە Flutterـدا | In our to-do appلە ئەرکەکانماندا |
|---|---|---|
| View | Widgets. A whole screen is a view, and so are its big parts: an app bar, a list, one row.. شاشەیەکی تەواو و بەشە گەورەکانیشی: ، لیستێک، یەک دێڕ. | LoginView, TodoListView, AppBarView, TodoViewLoginView، TodoListView، AppBarView، TodoView |
| View model | A ChangeNotifier. It holds the screen's state and calls notifyListeners() when it changes.ChangeNotifier. شاشەکە هەڵدەگرێت و کاتێک دەگۆڕێت notifyListeners() بانگ دەکات. | LoginViewModel, TodoListViewModelLoginViewModel، TodoListViewModel |
| Model | Plain Dart classes, held by the view models. ئاسایی Dart، کە هەڵیان دەگرن. | User, TodoUser، Todo |
| Service | Plain Dart classes that talk to a data source. ئاسایی Dart کە قسە لەگەڵ دەکەن. | UserService, TodoServiceUserService، TodoService |
Two short sentences from the lecture carry the whole idea: in Flutter, views are widgets, and ChangeNotifiers are view models; the models are held by the ChangeNotifiers. Everything else in this lesson is about connecting the two.
دوو ڕستەی کورتی وانەکە هەموو بیرۆکەکە هەڵدەگرن: لە Flutterـدا، و ChangeNotifierـەکان ؛ لەلایەن ChangeNotifierـەکانەوە هەڵدەگیرێن. هەموو ئەوەی تری ئەم وانەیە دەربارەی بەستنەوەی ئەم دووانەیە.
Providers revisited: state above the whole appدیسان providerـەکان: دۆخ لە سەرووی هەموو ئەپەکەوە
The lecture starts by drawing the provider apps of the previous chapter in a new way. One MultiProvider sits above MaterialApp and holds several providers, each with a piece of state. Screens and their widgets anywhere below read the state they need. Next to the tree stands a service locator with the services and other dependencies that the providers use.
وانەکە بەوە دەست پێ دەکات کە providerـی بەشی پێشوو بە شێوەیەکی نوێ دەکێشێت. یەک MultiProvider لە سەرووی MaterialAppـەوە دادەنرێت و چەند providerـێک هەڵدەگرێت، هەریەکەیان پارچەیەک . شاشەکان و لە هەر شوێنێکی خوارەوە ئەو دەخوێننەوە کە پێویستیانە. لە تەنیشت دارەکەوە هەیە کە ئەو و ترانەی تێدایە کە providerـەکان بەکاریان دەهێنن.
To turn this into MVVM, the lecture makes two choices. Each provider is a ChangeNotifierProvider, because the UI must be able to update the state, and that is what state management needs. And each ChangeNotifier it holds is a view model with its models inside, written as Viewmodel<Model> on the slide.
بۆ گۆڕینی ئەمە بۆ ، وانەکە دوو هەڵبژاردن دەکات. هەر providerـێک ChangeNotifierProviderـە، چونکە ڕووکارەکە دەبێت بتوانێت نوێ بکاتەوە و ئەوەش ئەوەیە کە پێویستی پێیەتی. هەر ChangeNotifierـێکیش کە هەڵیدەگرێت و لەناودایە، کە لەسەر سلایدەکە بە Viewmodel<Model> نووسراوە.
The demo in MVVM: two views, two view modelsنموونەکە بە MVVM: دوو دیمەن، دوو مۆدێلی دیمەن
The app stays the same: a login screen and a to-do screen. Only its inside changes. The widget tree becomes small (MaterialApp with LoginView and TodoListView), and a service locator holds a REST service (which talks to the database on the server) and two view models: one for the users and the current user, one for the to-do list. Each view, and even each part of a view, gets its own ChangeNotifierProvider and Consumer:
وەک خۆی دەمێنێتەوە: شاشەیەکی و شاشەیەکی ئەرکەکان. تەنها ناوەوەکەی دەگۆڕێت. بچووک دەبێتەوە (MaterialApp لەگەڵ LoginView و TodoListView) و REST هەڵدەگرێت (کە قسە لەگەڵ لەسەر دەکات) لەگەڵ دوو : یەکێک بۆ و ئێستا، یەکێک بۆ لیستی ئەرکەکان. هەر ، تەنانەت هەر بەشێکی ، ChangeNotifierProvider و Consumerـی خۆی وەردەگرێت:
- LoginView: its provider and consumer get the user list from the login view model to build the ListView; a tap on a ListTile tells the view model to change the current user.
- TodoListView: (a) the app bar has its own provider and consumer that get the current user (avatar and name); (b) the list has its own provider and consumer that get the to-do list; each row sends “delete to-do” and “toggle status” to the to-do view model.
- The view models use the services, and the services talk to the database.
- LoginView: provider و consumerـەکەی لیستی لە وەردەگرن بۆ دروستکردنی ListView؛ دەستلێدان لە ListTileـێک بە دەڵێت ئێستا بگۆڕێت.
- TodoListView: (a) provider و consumerـی خۆی هەیە کە ئێستا وەردەگرن (وێنۆچکە و ناو)؛ (b) لیستەکە provider و consumerـی خۆی هەیە کە لیستی ئەرکەکان وەردەگرن؛ هەر دێڕێک «سڕینەوەی ئەرک» و «گۆڕینی دۆخ» دەنێرێت بۆ ئەرکەکان.
- بەکار دەهێنن و قسە لەگەڵ دەکەن.
Step 0: write the two view modelsهەنگاوی ٠: دوو مۆدێلی دیمەنەکە بنووسە
Before moving anything, write the view models. Each one extends ViewModel, a small base class of our project: a ChangeNotifier with a busy flag that is true while data loads (its file is shown in the next lesson). The services are private fields, so a view cannot skip the view model and call a service itself. The view sees only getters and methods:
پێش گواستنەوەی هەر شتێک، بنووسە. هەریەکەیان ViewModel درێژ دەکاتەوە، بنەڕەتیی بچووکی پڕۆژەکەمان: ChangeNotifierـێک بە ئاڵایەکی busy کە لە کاتی بارکردنی دراوەدا trueـە ( لە وانەی داهاتووـدا پیشان دەدرێت). خانەی تایبەتن، بۆیە ناتوانێت بازبدات و خۆی بانگ بکات. تەنها و دەبینێت:
import '../../models/user.dart';
import '../../services/user/user_service.dart';
import '../viewmodel.dart';
class LoginViewModel extends ViewModel {
LoginViewModel(this._userService) {
_loadUsers();
}
final UserService _userService; // private: the view cannot reach it
List<User> _users = const [];
User? _currentUser;
List<User> get users => _users;
User? get currentUser => _currentUser;
Future<void> _loadUsers() async {
setBusy(true);
_users = await _userService.getUserList();
setBusy(false); // also notifies the view
}
void logInAs(User user) {
_currentUser = user;
notifyListeners();
}
void logOut() {
_currentUser = null;
notifyListeners();
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 6
The service is handed in (dependency injection); loading starts at once.
دەدرێتێ ()؛ بارکردن یەکسەر دەست پێ دەکات.
- Lineدێڕی 10
Private: the view cannot reach the data layer directly.
تایبەت: ناتوانێت ڕاستەوخۆ بگاتە دراوە.
- Lineدێڕی 23
An event from the view becomes a change of state plus a notification.
لە دەبێت بە گۆڕانی لەگەڵ ئاگادارکردنەوەیەک.
import '../../models/todo.dart';
import '../../models/user.dart';
import '../../services/todo/todo_service.dart';
import '../login/login_viewmodel.dart';
import '../viewmodel.dart';
class TodoListViewModel extends ViewModel {
TodoListViewModel(this._todoService, this._login) {
_login.addListener(_userChanged); // follow the current user
_userChanged();
}
final TodoService _todoService;
final LoginViewModel _login;
int? _userId;
List<Todo> _todos = const [];
User? get user => _login.currentUser;
List<Todo> get todos => _todos;
void _userChanged() {
final id = _login.currentUser?.id;
if (id == _userId) return;
_userId = id;
_todos = const [];
if (id != null) _load(id);
notifyListeners();
}
Future<void> _load(int userId) async {
setBusy(true);
final list = await _todoService.getTodoList(userId);
if (userId != _userId) return; // another user logged in meanwhile
_todos = list;
setBusy(false);
}
Future<void> toggle(Todo todo) async {
final changed = todo.copyWith(completed: !todo.completed);
await _todoService.updateTodo(changed);
_todos = [for (final t in _todos) t.id == todo.id ? changed : t];
notifyListeners();
}
Future<void> delete(Todo todo) async {
await _todoService.deleteTodo(todo.id);
_todos = _todos.where((t) => t.id != todo.id).toList();
notifyListeners();
}
Future<void> add(String title) async {
final userId = _userId;
if (userId == null) return;
_todos = [..._todos, await _todoService.addTodo(userId, title)];
notifyListeners();
}
@override
void dispose() {
_login.removeListener(_userChanged);
super.dispose();
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 9
It follows the current user (see the next screen).
شوێنی ئێستا دەکەوێت (بڕوانە شاشەی داهاتوو).
- Lineدێڕی 33
A late answer for an old user is ignored.
وەڵامێکی دواکەوتوو بۆ کۆن پشتگوێ دەخرێت.
- Lineدێڕی 38
toggle, delete and add: the events the view can send.
toggle، delete و add: ئەو دەتوانێت بینێرێت.
Compare with the TodoListNotifier of the provider version: the list logic is the same. What changed is who creates it and how it learns about the current user.
بەراوردی بکە لەگەڵ TodoListNotifierـی وەشانی provider: لۆژیکی لیستەکە هەمانە. ئەوەی گۆڕاوە ئەوەیە کێ دروستی دەکات و چۆن لە ئێستا ئاگادار دەبێت.
Step 0: write the two view modelsهەنگاوی ٠: دوو مۆدێلی دیمەنەکە بنووسە
One view model follows anotherمۆدێلێکی دیمەن شوێنی یەکێکی تر دەکەوێت
In the provider version, a ChangeNotifierProxyProvider told the to-do list about each new user. In MVVM there is no proxy in the tree, so the to-do view model does it itself: it receives the login view model in its constructor and listens to it with addListener. When the current user changes, _userChanged loads that user's to-dos. In dispose it removes the listener again.
لە وەشانی providerـدا، ChangeNotifierProxyProviderـێک بە لیستی ئەرکەکانی دەگوت کە نوێ هاتووە. لە هیچ proxyـێک لە دارەکەدا نییە، بۆیە ئەرکەکان خۆی ئەمە دەکات: لە وەردەگرێت و بە addListener گوێی لێ دەگرێت. کاتێک ئێستا دەگۆڕێت، _userChanged ئەرکەکانی ئەو بار دەکات. لە disposeـدا لادەباتەوە.
Refactoring the provider app into MVVMڕێکخستنەوەی کۆدی ئەپی provider بۆ MVVM
Changing the structure of existing code (where things are created, named and kept) without changing what the program does, so that it is easier to read, test and extend.
گۆڕینی پێکهاتەی کۆدێکی هەبوو (ئەوەی شتەکان لە کوێ دروست دەکرێن، ناویان چییە و لە کوێ هەڵدەگیرێن) بەبێ گۆڕینی ئەوەی دەیکات، تا خوێندنەوە، تاقیکردنەوە و فراوانکردنی ئاسانتر بێت.
The lecture changes the to-do app in two refactoring steps. The app looks and behaves exactly the same after each step; only the place where things are created and found changes.
وانەکە ئەرکەکان بە دوو هەنگاوی دەگۆڕێت. دوای هەر هەنگاوێک ڕێک وەک پێشوو دەردەکەوێت و ڕەفتار دەکات؛ تەنها ئەو شوێنە دەگۆڕێت کە شتەکانی تێدا دروست دەکرێن و دەدۆزرێنەوە.
Refactoring the provider app into MVVMڕێکخستنەوەی کۆدی ئەپی provider بۆ MVVM
Step 1: move the view models to the service locatorهەنگاوی ١: مۆدێلەکانی دیمەن بگوازەرەوە بۆ دۆزەرەوەی خزمەتگوزاری
In the provider version, create: built each notifier inside its provider, so the notifier lived and died with the tree. The lecture's first refactoring moves the view models into the service locator, next to the services. The locator now creates and owns them; the providers in the MultiProvider only hand them on with .value, which is the right constructor for objects that already exist (create or .value?). With the get_it package the registration looks like this (the next lesson explains get_it fully):
لە وەشانی providerـدا، create: هەر ChangeNotifierـێکی لەناو providerـەکەیدا دروست دەکرد، بۆیە ChangeNotifierـەکە لەگەڵ دارەکەدا دەژیا و دەمرد. یەکەم وانەکە دەگوازێتەوە بۆ ناو ، لە تەنیشت . ئێستا دروستیان دەکات و خاوەنیانە؛ providerـەکانی ناو MultiProvider تەنها بە .value دەیانگوازنەوە، کە دروستە بۆ کە پێشتر هەن (create یان .value؟). لەگەڵ get_it تۆمارکردنەکە بەم شێوەیەیە (وانەی داهاتوو get_it بە تەواوی ڕوون دەکاتەوە):
void init() {
// … the services, as before (the next lesson shows the whole file) …
// View models
dependency.registerLazySingleton(
() => LoginViewModel(dependency<UserService>()),
);
dependency.registerLazySingleton(
() => TodoListViewModel(
dependency<TodoService>(),
dependency<LoginViewModel>(),
),
);
}import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app/dependencies.dart' as di;
import 'screens/login/login_view.dart';
import 'screens/login/login_viewmodel.dart';
import 'screens/todolist/todolist_viewmodel.dart';
// Refactoring step 1: the view models live in the service locator.
// The providers still sit together in one MultiProvider above MaterialApp.
void main() {
di.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<LoginViewModel>.value(
value: di.dependency<LoginViewModel>(),
),
ChangeNotifierProvider<TodoListViewModel>.value(
value: di.dependency<TodoListViewModel>(),
),
],
child: const MaterialApp(home: LoginView()),
),
);
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 12
Fill the service locator first.
یەکەم پڕ بکەرەوە.
- Lineدێڕی 18
The provider only passes on the view model the locator owns.
providerـەکە تەنها ئەو دەگوازێتەوە کە هی .
Refactoring the provider app into MVVMڕێکخستنەوەی کۆدی ئەپی provider بۆ MVVM
Step 2: each view gets its own providerهەنگاوی ٢: هەر دیمەنێک providerـی خۆی وەردەگرێت
Once the locator owns the view models, the MultiProvider above MaterialApp has no job left. The second refactoring removes it: each view is wrapped in its own ChangeNotifierProvider, which takes its view model from the service locator, and a Consumer right below it rebuilds only that view. A provider now sits as close as possible to the widgets that use it, and a part of a view (an app bar, a single row) can have its own pair too.
کاتێک خاوەنی بێت، MultiProviderـەکەی سەرووی MaterialApp هیچ کارێکی نامێنێت. دووەم لای دەبات: هەر لەناو ChangeNotifierProviderـی خۆیدا دەپێچرێت، کە لە وەردەگرێت و Consumerـێک ڕاستەوخۆ لە خوارەوەیدا تەنها ئەو دووبارە دروست دەکاتەوە. ئێستا provider تا دەکرێت نزیک لەو دادەنرێت کە بەکاری دەهێنن و بەشێکی (، یەک دێڕ) دەتوانێت جووتێکی خۆی هەبێت.


What do you gain? A view's provider and its consumer are now next to each other, so a reader sees at once which view model a view uses. When a view model notifies, only the consumers below its own provider rebuild. And there is no list of every provider of the app to keep in order above MaterialApp.
چی بەدەست دەهێنیت؟ ئێستا provider و consumerـی لە تەنیشت یەکترن، بۆیە خوێنەر یەکسەر دەبینێت کام بەکار دەهێنێت. کاتێک ئاگادار دەکاتەوە، تەنها ئەو consumerـانە دووبارە دروست دەکرێنەوە کە لە خوارەوەی providerـی خۆیدان. هەروەها هیچ لیستێکی هەموو providerـەکانی نییە کە لە سەرووی MaterialAppـەوە بە ڕیزی ڕابگریت.
Tasks for the login screenئەرکەکانی شاشەی چوونەژوورەوە
The lecture lists four tasks for turning the login screen into an MVVM view:
وانەکە چوار ئەرک ڕیز دەکات بۆ گۆڕینی شاشەی بۆ :
- Define LoginViewModelLoginViewModel پێناسە بکە
Done above: it holds the users and the current user.
لە سەرەوە کرا: و ئێستا هەڵدەگرێت.
- Move LoginViewModel to the service locatorLoginViewModel بگوازەرەوە بۆ دۆزەرەوەی خزمەتگوزاری
Register it with get_it, so the locator creates and owns it.
بە get_it تۆماری بکە، تا دروستی بکات و خاوەنی بێت.
- The view has its own provider and consumerدیمەنەکە provider و consumerـی خۆی هەیە
Wrap the view in
ChangeNotifierProvider.valueand aConsumer<LoginViewModel>.لەناو
ChangeNotifierProvider.valueوConsumer<LoginViewModel>ـدا بپێچە. - Refactor DI from Provider.of() to the service locatorتێخستنی وابەستەیی لە Provider.of()ـەوە بگۆڕە بۆ دۆزەرەوەی خزمەتگوزاری
The view no longer finds the users with
Provider.of<List<User>>from a provider above the app; it gets its view model from the locator.چیتر بە
Provider.of<List<User>>لە providerـێکی سەرووی نادۆزێتەوە؛ لە وەردەگرێت.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app/dependencies.dart';
import '../todolist/todolist_view.dart';
import 'login_viewmodel.dart';
// Refactoring step 2: the view has its own provider and consumer.
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LoginViewModel>.value(
value: dependency<LoginViewModel>(), // from the service locator
child: Consumer<LoginViewModel>(
builder: (context, viewModel, child) => Scaffold(
appBar: AppBar(title: const Text('Choose a user to log in as')),
body: viewModel.busy
? const Center(child: CircularProgressIndicator())
: ListView(
children: [
for (final user in viewModel.users)
ListTile(
leading: CircleAvatar(child: Text(user.name[0])),
title: Text(user.name),
subtitle: Text('user id: ${user.id}'),
onTap: () {
viewModel.logInAs(user); // change the current user
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const TodoListView(),
),
);
},
),
],
),
),
),
);
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 14
The view's own provider…
providerـی خودی …
- Lineدێڕی 15
…takes the view model from the service locator…
… لە وەردەگرێت…
- Lineدێڕی 16
…and its own consumer rebuilds the screen when the view model notifies.
…و consumerـی خۆی کاتێک ئاگادار دەکاتەوە شاشەکە دووبارە دروست دەکاتەوە.
- Lineدێڕی 29
The view only sends an event; the view model decides what changes.
تەنها دەنێرێت؛ بڕیار دەدات چی دەگۆڕێت.
Tasks for the to-do list screenئەرکەکانی شاشەی لیستی ئەرکەکان
The to-do screen follows the same tasks with two view models. The app bar has its own provider and consumer of the login view model (“get user”); the list has its own provider and consumer of the to-do view model (“get the to-do list, to build the ListView”), and each row sends “delete to-do” and “toggle status”:
شاشەی ئەرکەکان هەمان ئەرکەکان پەیڕەو دەکات بە دوو . provider و consumerـی خۆی هەیە بۆ (« وەربگرە»)؛ لیستەکە provider و consumerـی خۆی هەیە بۆ ئەرکەکان («لیستی ئەرکەکان وەربگرە بۆ دروستکردنی ListView») و هەر دێڕێک «سڕینەوەی ئەرک» و «گۆڕینی دۆخ» دەنێرێت:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../app/dependencies.dart';
import '../login/login_viewmodel.dart';
import 'todolist_viewmodel.dart';
class TodoListView extends StatelessWidget {
const TodoListView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
// (a) the app bar: its own provider + consumer of LoginViewModel
child: ChangeNotifierProvider<LoginViewModel>.value(
value: dependency<LoginViewModel>(),
child: Consumer<LoginViewModel>(
builder: (context, login, child) => AppBar(
title: Text(login.currentUser?.name ?? ''),
actions: [
IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: () {
login.logOut();
Navigator.of(context).pop();
},
),
],
),
),
),
),
// (b) the list: its own provider + consumer of TodoListViewModel
body: ChangeNotifierProvider<TodoListViewModel>.value(
value: dependency<TodoListViewModel>(),
child: Consumer<TodoListViewModel>(
builder: (context, todos, child) => ListView(
children: [
for (final todo in todos.todos)
ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration:
todo.completed ? TextDecoration.lineThrough : null,
),
),
onTap: () => todos.toggle(todo), // toggle status
onLongPress: () => todos.delete(todo), // delete
),
],
),
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Add a to-do',
onPressed: () => dependency<TodoListViewModel>().add('New task'),
child: const Icon(Icons.add),
),
);
}
}login view: Aram | Shilan | Karwan to-do view: Prepare proposal for the new project | Replace light bulb | Buy Flutter eBook | Subscribe to fibre-optic internet | Set up online meeting room (done) tap, long-press, +: Prepare proposal for the new project | Replace light bulb (done) | Subscribe to fibre-optic internet | Set up online meeting room (done) | New task log out: current user = null
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 16
(a) The app bar listens only to the login view model.
(a) تەنها گوێ لە دەگرێت.
- Lineدێڕی 36
(b) The list listens only to the to-do view model.
(b) لیستەکە تەنها گوێ لە ئەرکەکان دەگرێت.
- Lineدێڕی 51
Row events go to the view model.
دێڕ دەچن بۆ .
Can we use a view model without Provider?دەتوانین مۆدێلی دیمەن بەبێ Provider بەکار بهێنین؟
The lecture ends with a question and two pictures. Our implementation: a ChangeNotifierProvider sits above the View, the View holds the widgets, and the view model lives in the service locator. Implementation without Provider: the View and its widgets talk directly to the view model in the service locator. The slide does not answer. The answer is yes, and it is the way Flutter's own documentation teaches MVVM today.
وانەکە بە پرسیارێک و دوو وێنە کۆتایی دێت. جێبەجێکردنی ئێمە: ChangeNotifierProviderـێک لە سەرووی Viewـەوەیە، View لەخۆ دەگرێت و لە دەژی. جێبەجێکردن بەبێ Provider: و ڕاستەوخۆ قسە لەگەڵ لە دەکەن. سلایدەکە وەڵام نادات. وەڵامەکە بەڵێیە و ئەوە ئەو ڕێگایەیە کە بەڵگەنامەکانی خودی Flutter ئەمڕۆ پێ فێر دەکەن.
Two built-in classes are enough: the view model is a ChangeNotifier, and the view rebuilds with a ListenableBuilder. The view receives its view model through its constructor (from the router, main, or the service locator), so no package is involved at all:
دوو ناوخۆیی بەسن: ChangeNotifierـە و بە ListenableBuilder دووبارە دروست دەبێتەوە. لە ڕێگەی وەردەگرێت (لە router، main یان )، بۆیە هیچ بەکار نایەت:
import 'package:flutter/material.dart';
import 'todolist_viewmodel.dart';
// The same view without the provider package:
// it receives its view model and listens with ListenableBuilder.
class TodoListView extends StatelessWidget {
const TodoListView({super.key, required this.viewModel});
final TodoListViewModel viewModel;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(viewModel.user?.name ?? '')),
body: ListenableBuilder(
listenable: viewModel, // rebuilds when the view model notifies
builder: (context, child) => ListView(
children: [
for (final todo in viewModel.todos)
ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration:
todo.completed ? TextDecoration.lineThrough : null,
),
),
onTap: () => viewModel.toggle(todo),
onLongPress: () => viewModel.delete(todo),
),
],
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Add a to-do',
onPressed: () => viewModel.add('New task'),
child: const Icon(Icons.add),
),
);
}
}
// Somewhere above it (a route, or main) hands the view model in:
// TodoListView(viewModel: dependency<TodoListViewModel>())no provider: Prepare proposal for the new project | Replace light bulb | Buy Flutter eBook | Subscribe to fibre-optic internet | Set up online meeting room (done) after tap: Prepare proposal for the new project (done)
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 8
The view model comes in through the constructor.
لە ڕێگەی دێتە ژوورەوە.
- Lineدێڕی 17
ListenableBuilder plays the role of the Consumer.
ListenableBuilder ڕۆڵی Consumer دەگێڕێت.
- Lineدێڕی 29
Events go straight to the view model.
ڕاستەوخۆ دەچن بۆ .
| Partبەش | With provider (the lecture)بە provider (وانەکە) | Without provider (Flutter docs)بەبێ provider (بەڵگەنامەکانی Flutter) |
|---|---|---|
| View model | ChangeNotifier | ChangeNotifier |
| How the view gets it چۆن وەری دەگرێت | ChangeNotifierProvider.value + Consumer (or context.watch)ChangeNotifierProvider.value + Consumer (یان context.watch) | A constructor parameter |
| What rebuilds the viewچی دووبارە دروست دەکاتەوە | Consumer | ListenableBuilder |
| Package needed پێویست | provider | none (built into Flutter)هیچ (لەناو خودی Flutterـدایە) |
Flutter's official architecture guideڕێنماییە فەرمییەکەی تەلارسازیی نەرمەواڵەی Flutter
The lecture's slides were made before Flutter published its own architecture guide. The guide recommends MVVM too, with a few more names. Here is how its parts line up with this lesson:
سلایدەکانی وانەکە پێش ئەوە دروست کران کە Flutter ڕێنمایی خۆی بڵاو بکاتەوە. ڕێنماییەکە پێشنیار دەکات، بە چەند ناوێکی زیاترەوە. ئەمە ئەوەیە کە بەشەکانی چۆن لەگەڵ ئەم وانەیەدا یەک دەگرنەوە:
| Guideڕێنمایی | Its jobکارەکەی | In this lessonلەم وانەیەدا |
|---|---|---|
| View (UI layer) ( ڕووکار) | A composition of widgets for one feature; no business logic, only simple display logic (show/hide, animation, layout, simple routing).کۆمەڵێک بۆ یەک تایبەتمەندی؛ بەبێ لۆژیکی بازرگانی، تەنها لۆژیکی سادەی پیشاندان (پیشاندان/شاردنەوە، ، ڕیزبەندی، سادە). | LoginView, TodoListViewLoginView، TodoListView |
| View model (UI layer) ( ڕووکار) | Turns app data into UI state, keeps that state, and exposes commands to the view.دراوەی دەگۆڕێت بۆ ڕووکار، ئەو هەڵدەگرێت و دەدات بە . | LoginViewModel, TodoListViewModelLoginViewModel، TodoListViewModel |
| Repository (data layer) ( دراوە) | The source of truth for one type of data: caching, errors, retries, turning raw data into models.سەرچاوەی ڕاستی بۆ یەک جۆر لە دراوە: ، ، هەوڵدانەوە، گۆڕینی دراوەی خاو بۆ . | Not in the lecture; the view models talk to services directlyلە وانەکەدا نییە؛ ڕاستەوخۆ قسە لەگەڵ دەکەن |
| Service (data layer) ( دراوە) | Wraps one data source (a REST API, local files, a platform API); holds no state.یەک دەپێچێتەوە (REST API، فایلە ناوخۆییەکان، APIـی )؛ هیچ هەڵناگرێت. | UserService, TodoServiceUserService، TodoService |
Flutter's official architecture guideڕێنماییە فەرمییەکەی تەلارسازیی نەرمەواڵەی Flutter
One view, one view model, and commandsیەک دیمەن، یەک مۆدێلی دیمەن و فەرمانەکان
- One view, one view model per feature (usually a screen). A view model may use several repositories, and repositories may be shared by several view models.
- A command is a method of the view model that the view calls from a gesture, without knowing how it works. The guide's example wraps each action in a small
Commandobject that also says whether it is running, finished with an error, or completed, so the view can show a spinner or an error without extra flags. - The guide's case study shares services and repositories with the provider package (
Provider,ChangeNotifierProvider,context.read) and builds each screen's view model when the route is created; views listen withListenableBuilder. Provider and the built-in classes work together.
- یەک ، یەک بۆ هەر تایبەتمەندییەک (زۆربەی جار شاشەیەک). دەتوانێت چەند بەکار بهێنێت و چەند دەتوانن بەش بکەن.
- کە لە بانگی دەکات، بەبێ ئەوەی بزانێت چۆن کار دەکات. نموونەکەی ڕێنماییەکە هەر کارێک لەناو بچووکی
Commandـدا دەپێچێت کە دەشڵێت ئایا جێبەجێ دەبێت، بە کۆتایی هات یان تەواو بوو، بۆیە دەتوانێت بەبێ ئاڵای زیادە بازنەی چاوەڕوانی یان پیشان بدات. - نموونەی لێکۆڵینەوەی ڕێنماییەکە و بە provider بەش دەکات (
Provider،ChangeNotifierProvider،context.read) و هەر شاشەیەک لە کاتی دروستکردنی دروست دەکات؛ بەListenableBuilderگوێ دەگرن. Provider و ناوخۆییەکان پێکەوە کار دەکەن.
Flutter's official architecture guideڕێنماییە فەرمییەکەی تەلارسازیی نەرمەواڵەی Flutter
Testing a view model with fakesتاقیکردنەوەی مۆدێلێکی دیمەن بە ئۆبجێکتە ساختەکان
Because the view models receive their services in the constructor, a test can hand them fake services and check the logic with no widget, no server and no service locator — a unit test, the first of the four testing levels. This is the guide's advice, and this test really passed:
چونکە لە وەردەگرن، تاقیکردنەوەیەک دەتوانێت ساختەیان بداتێ و لۆژیکەکە بپشکنێت بەبێ هیچ ، یان — ئەمە ـە، یەکەمی چوار ئاستەکەی تاقیکردنەوە. ئەمە ئامۆژگاریی ڕێنماییەکەیە و ئەم تاقیکردنەوەیە بە ڕاستی سەرکەوتوو بوو:
import 'package:architecture_mvvm/screens/login/login_viewmodel.dart';
import 'package:architecture_mvvm/screens/todolist/todolist_viewmodel.dart';
import 'package:architecture_mvvm/services/todo/todo_service_mock.dart';
import 'package:architecture_mvvm/services/user/user_service_mock.dart';
import 'package:flutter_test/flutter_test.dart';
Future<void> answers() => Future<void>.delayed(Duration.zero);
void main() {
test('TodoListViewModel follows the logged-in user', () async {
// Fake services: no server, no widgets, no get_it.
final login = LoginViewModel(UserServiceMock());
final todos = TodoListViewModel(TodoServiceMock(), login);
await answers();
expect(login.users.map((u) => u.name), ['Aram', 'Shilan', 'Karwan']);
login.logInAs(login.users[1]); // Shilan
await answers();
expect(todos.todos.length, 2);
await todos.add('Revise chapter 9');
expect(todos.todos.last.id, 9);
});
}00:00 +1: All tests passed!
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 13
Fake services in, real view models under test.
ساختە دەچنە ژوورەوە، ڕاستەقینە تاقی دەکرێنەوە.
Common mistakesهەڵە باوەکان
| Mistakeهەڵە | What goes wrongچی هەڵە دەبێت | Fixچارەسەر |
|---|---|---|
A view model that imports material.dart and holds a BuildContext or widgets کە material.dart import دەکات و BuildContext یان هەڵدەگرێت | It now knows the view; it cannot be unit tested or reusedئێستا دەناسێت؛ ناتوانرێت بکرێت یان دووبارە بەکار بهێنرێتەوە | Import only foundation.dart; expose state, let the view decide how it looksتەنها foundation.dart import بکە؛ بدە و با بڕیار بدات چۆن دەردەکەوێت |
ChangeNotifierProvider(create: (_) => dependency<LoginViewModel>())ChangeNotifierProvider(create: (_) => dependency<LoginViewModel>()) | The provider disposes the locator's view model when the view leaves; the next screen gets a dead objectproviderـەکە کاتێک دەڕوات dispose دەکات؛ شاشەی داهاتوو مردوو وەردەگرێت | Use ChangeNotifierProvider.value(value: …) for objects the locator ownsبۆ ئەو هی ChangeNotifierProvider.value(value: …) بەکار بهێنە |
| Business logic in the view (filtering, formatting, calling services)لۆژیکی بازرگانی لە (پاڵاوتن، شێوەپێدان، بانگکردنی ) | Hard to test; the same rule gets copied into several screensتاقیکردنەوەی قورسە؛ هەمان یاسا لە چەند شاشەیەکدا کۆپی دەکرێت | Move it into the view model; the view only shows state and sends eventsبیگوازەرەوە بۆ ؛ تەنها پیشان دەدات و دەنێرێت |
| Public service fields in the view modelخانەی گشتیی لە | A view calls the service directly and the view model's state goes out of date ڕاستەوخۆ بانگی دەکات و کۆن دەبێت | Keep injected services private (_todoService) تێخراوەکان تایبەت ڕابگرە (_todoService) |
addListener on another notifier without removeListener in disposeaddListener لەسەر ChangeNotifierـێکی تر بەبێ removeListener لە disposeـدا | The other notifier keeps calling a disposed objectChangeNotifierـەکەی تر بەردەوام بانگی disposeکراو دەکات | Always pair themهەمیشە جووتیان بکە |
Exercisesڕاهێنانەکان
For each piece of the to-do app, say whether it belongs in a view, a view model, a model or a service: (a) cross out completed to-dos; (b) remember which user is logged in; (c) GET /todos?userId=1; (d) the Todo class with id, title and completed; (e) open the to-do screen after a tap; (f) ignore a late answer for a user who logged out.
بۆ هەر پارچەیەکی ئەرکەکان، بڵێ هی ، ، یان : (أ) هێڵ بەسەر ئەرکە تەواوکراوەکاندا بهێنە؛ (ب) لەبیرت بێت کام چووەتە ژوورەوە؛ (ج) GET /todos?userId=1؛ (د) Todo بە id، title و completed؛ (هـ) دوای دەستلێدانێک شاشەی ئەرکەکان بکەرەوە؛ (و) وەڵامێکی دواکەوتوو بۆ کە چووەتە دەرەوە پشتگوێ بخە.
Show answerوەڵامەکە پیشان بدە
- (a) view (how it looks)
- (b) view model (state)
- (c) service (talks to the data source)
- (d) model
- (e) view (simple routing)
- (f) view model (a rule about the state)
- (أ) (چۆن دەردەکەوێت)
- (ب) ()
- (ج) (قسە لەگەڵ دەکات)
- (د)
- (هـ) ( سادە)
- (و) (یاسایەک دەربارەی )
A ProfileScreen reads context.watch<ProfileNotifier>() from a ChangeNotifierProvider above MaterialApp. Write the two refactoring steps of this lesson for it: what changes in the service locator, and what does the screen's build return in the end?
ProfileScreenـێک context.watch<ProfileNotifier>() لە ChangeNotifierProviderـێکی سەرووی MaterialAppـەوە دەخوێنێتەوە. دوو هەنگاوەکەی ئەم وانەیە بۆی بنووسە: چی لە دەگۆڕێت و لە کۆتاییدا buildـی شاشەکە چی دەگەڕێنێتەوە؟
Show answerوەڵامەکە پیشان بدە
Step 1: rename it
ProfileViewModel, register it withdependency.registerLazySingleton(() => ProfileViewModel(dependency<ProfileService>())), and change the provider above MaterialApp toChangeNotifierProvider.value(value: dependency<ProfileViewModel>()).هەنگاوی ١: ناوی بگۆڕە بۆ
ProfileViewModel، بەdependency.registerLazySingleton(() => ProfileViewModel(dependency<ProfileService>()))تۆماری بکە و providerـەکەی سەرووی MaterialApp بگۆڕە بۆChangeNotifierProvider.value(value: dependency<ProfileViewModel>()).Step 2: remove that provider from above MaterialApp.
ProfileScreen.buildreturnsChangeNotifierProvider<ProfileViewModel>.value(value: dependency<ProfileViewModel>(), child: Consumer<ProfileViewModel>(builder: (context, vm, child) => Scaffold(...))).هەنگاوی ٢: ئەو providerـە لە سەرووی MaterialApp لابە.
ProfileScreen.buildئەمە دەگەڕێنێتەوە:ChangeNotifierProvider<ProfileViewModel>.value(value: dependency<ProfileViewModel>(), child: Consumer<ProfileViewModel>(builder: (context, vm, child) => Scaffold(...))).
Rewrite the login view of this lesson without the provider package. What replaces ChangeNotifierProvider.value and Consumer, and how does the view get its view model?
ئەم وانەیە بەبێ provider بنووسەرەوە. چی جێگەی ChangeNotifierProvider.value و Consumer دەگرێتەوە و چۆن وەردەگرێت؟
Show answerوەڵامەکە پیشان بدە
class LoginView extends StatelessWidget {
const LoginView({super.key, required this.viewModel});
final LoginViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) => Scaffold(
appBar: AppBar(title: const Text('Choose a user to log in as')),
body: ListView(
children: [
for (final user in viewModel.users)
ListTile(
title: Text(user.name),
onTap: () => viewModel.logInAs(user),
),
],
),
),
);
}
}
// in the routes: LoginView(viewModel: dependency<LoginViewModel>())ListenableBuilder replaces the Consumer, and the view model arrives as a constructor parameter instead of through a provider.
ListenableBuilder جێگەی Consumer دەگرێتەوە و وەک دێت لەبری ئەوەی لە ڕێگەی providerـێکەوە بێت.
Write a unit test (no widgets) that logs in as Aram with the mock services, toggles “Replace light bulb” and checks that it is now completed.
بنووسە (بەبێ ) کە بە ساختەکان وەک ئارام دەچێتە ژوورەوە، «Replace light bulb» دەگۆڕێت و دەپشکنێت کە ئێستا تەواوکراوە.
Show answerوەڵامەکە پیشان بدە
test('toggle marks a to-do as completed', () async {
final login = LoginViewModel(UserServiceMock());
final todos = TodoListViewModel(TodoServiceMock(), login);
await Future<void>.delayed(Duration.zero);
login.logInAs(login.users.first); // Aram
await Future<void>.delayed(Duration.zero);
final bulb = todos.todos.firstWhere((t) => t.title == 'Replace light bulb');
await todos.toggle(bulb);
expect(todos.todos.firstWhere((t) => t.id == bulb.id).completed, isTrue);
});Summaryپوختە
- In Flutter, views are widgets and view models are ChangeNotifiers that hold the models; services are plain classes behind the view models.
- The lecture refactors in two steps: (1) move the view models into the service locator (providers use
.value); (2) remove the MultiProvider and give each view, or part of a view, its own ChangeNotifierProvider and Consumer. - Views send events and show state; view models keep the rules; injected services stay private.
- A view model works without provider too: a ChangeNotifier plus
ListenableBuilder, the way Flutter's docs teach it. - Flutter's guide adds repositories (source of truth) between view models and services, and commands for actions; view models are tested with fake services.
- لە Flutterـدا، و ChangeNotifierن کە هەڵدەگرن؛ ئاسایین لە پشت .
- وانەکە بە دوو هەنگاو ڕێک دەخاتەوە: (1) بگوازەرەوە بۆ ناو (providerـەکان
.valueبەکار دەهێنن)؛ (2) MultiProvider لابە و بە هەر ، یان بەشێکی ، ChangeNotifierProvider و Consumerـی خۆی بدە. - دەنێرن و پیشان دەدەن؛ یاساکان دەپارێزن؛ تێخراوەکان تایبەت دەمێننەوە.
- بەبێ providerـیش کار دەکات: ChangeNotifier لەگەڵ
ListenableBuilder، بەو شێوەیەی بەڵگەنامەکانی Flutter فێری دەکەن. - ڕێنماییەکەی Flutter (سەرچاوەی ڕاستی) لە نێوان و زیاد دەکات و بۆ کارەکان؛ بە ساختە تاقی دەکرێنەوە.
Lesson complete!وانەکە تەواو بوو!
Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.
Key termsزاراوە سەرەکییەکان
Organising an MVVM Project: Folders, Services and get_itڕێکخستنی پڕۆژەیەکی MVVM: فۆڵدەرەکان، خزمەتگوزارییەکان و get_it
Related lecturesوانە پەیوەندیدارەکان
- Organising an MVVM Project: Folders, Services and get_itڕێکخستنی پڕۆژەیەکی MVVM: فۆڵدەرەکان، خزمەتگوزارییەکان و get_it
- State Management with Providerبەڕێوەبردنی دۆخ بە Provider
- ChangeNotifier, ChangeNotifierProvider and MultiProviderChangeNotifier، ChangeNotifierProvider و MultiProvider
- The Consumer Widgetویجێتی Consumer
- The Selector Widget: Fewer Rebuildsویجێتی Selector: دووبارە دروستکردنەوەی کەمتر
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- Flutter docs: Guide to app architecture
- Flutter docs: Architecture case study, UI layer (view models, ListenableBuilder, commands)
- Flutter docs: Architecture case study, dependency injection (provider)
- Flutter docs: Architecture case study, testing each layer (fakes)
- Flutter docs: Learning pathway (ChangeNotifier and ListenableBuilder)
- pub.dev: provider 6.1.5+1
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.