FlutterFlutter · Chapterبەش 12 · Lectureوانە 12.2
Organising an MVVM Project: Folders, Services and get_itڕێکخستنی پڕۆژەیەکی MVVM: فۆڵدەرەکان، خزمەتگوزارییەکان و get_it
The lecture's MVVM project structure: lib/app, models, screens (view, view model and widgets per screen), shared widgets and services (an abstract TodoService with REST, mock and Firebase implementations); Dart's file-name style; registering services and view models with get_it (registerLazySingleton) and calling init() in main; swapping the real service for a mock in one line.
پێکهاتەی پڕۆژەی وانەکە: lib/app، models، screens (، و بۆ هەر شاشەیەک)، هاوبەشەکان و services (TodoServiceـێکی ڕووت بە جێبەجێکردنەکانی REST، ساختە و Firebase)؛ شێوازی ناوی Dart؛ تۆمارکردنی و بە get_it (registerLazySingleton) و بانگکردنی init() لە mainـدا؛ گۆڕینەوەی ڕاستەقینەکە بە ساختەیەک لە یەک دێڕدا.
What you will learnچی فێر دەبیت
- Lay out an MVVM Flutter project in app, models, screens, widgets and services folders.
- Put a screen's view, view model and private widgets together, and shared widgets apart.
- Write an abstract service with several implementations and name the files in Dart style.
- Register services and view models with get_it and explain singleton, lazy singleton and factory.
- Start the locator in main() and swap a real service for a mock.
- پڕۆژەیەکی Flutterـی لە app، models، screens، widgets و services ڕێک بخەیت.
- ، و تایبەتەکانی شاشەیەک پێکەوە دابنێیت و هاوبەشەکان جیا.
- ڕووت بە چەند جێبەجێکردنێکەوە بنووسیت و ناوی بە شێوازی Dart دابنێیت.
- و بە get_it تۆمار بکەیت و ، دواخراو و factory ڕوون بکەیتەوە.
- لە
main()ـدا دەست پێ بکەیت و ڕاستەقینە بە ساختەیەک بگۆڕیتەوە.
Where does each file go?هەر فایلێک دەچێتە کوێ؟
In MVVM in Flutter with Provider the to-do app became views, view models, models and services. A real app has dozens of them. If they all sit in one folder, nobody finds anything, and two developers keep changing the same file. The lecture's last part answers three practical questions: which folders an MVVM project has, how services are organised so they can be swapped, and how the service locator (the get_it package) is set up and started.
لە MVVM لە Flutterـدا بە Provider ئەرکەکان بوو بە ، ، و . ڕاستەقینە دەیان دانەی لەمانە هەیە. ئەگەر هەموویان لە یەک بن، کەس هیچ شتێک نادۆزێتەوە و دوو بەردەوام هەمان دەگۆڕن. دوایین بەشی وانەکە وەڵامی سێ پرسیاری پراکتیکی دەداتەوە: پڕۆژەیەکی کام هەیە، چۆن ڕێک دەخرێن تا بگۆڕدرێنەوە و ( get_it) چۆن ئامادە دەکرێت و دەست پێ دەکات.
In a good pharmacy, every medicine has a shelf: painkillers here, syrups there, each brand in its own box. The pharmacist finds anything in seconds, and a new assistant learns the system in a day. A project's folders are the shelves: a fixed place for each kind of file makes the code easy to find, even for a new team member.
لە دەرمانخانەیەکی باشدا، هەر دەرمانێک ڕەفەیەکی هەیە: ئازارشکێنەکان لێرە، شەربەتەکان لەوێ، هەر مارکەیەک لە سندووقی خۆیدا. دەرمانسازەکە لە چەند چرکەیەکدا هەر شتێک دەدۆزێتەوە و یاریدەدەرێکی نوێ لە ڕۆژێکدا سیستەمەکە فێر دەبێت. پڕۆژەیەک ڕەفەکانن: شوێنێکی جێگیر بۆ هەر جۆرە دۆزینەوەی کۆدەکە ئاسان دەکات، تەنانەت بۆ ئەندامێکی نوێی تیمەکە.
Where the analogy stops workingسنووری ئەم نموونەیە
Shelves do not choose the medicine; folders do not make bad code good. They only make it easy to find what is there.
ڕەفەکان دەرمانەکە هەڵنابژێرن؛ کۆدی خراپ باش ناکەن. تەنها دۆزینەوەی ئەوەی هەیە ئاسان دەکەن.
The top folders of lib/فۆڵدەرەکانی سەرەوەی lib/
All Dart code of a Flutter app lives in lib/. The lecture's MVVM project (architecture_mvvm) puts main.dart there and five folders next to it:
هەموو کۆدی Dartـی Flutter لە lib/ـدا دەژی. پڕۆژەی وانەکە (architecture_mvvm) main.dart و پێنج لە تەنیشتیەوە دادەنێت:
architecture_mvvm/
└── lib/
├── main.dart
├── app/
│ ├── dependencies.dart (service registry)
│ ├── router.dart
│ └── other_app_wide_stuff.dart
├── models/
│ ├── todo.dart
│ └── user.dart
├── screens/
├── widgets/
└── services/| Folder | What goes in itچی تێدا دادەنرێت |
|---|---|
app/ | Things for the whole app: dependencies.dart, the service registry (the get_it setup); router.dart, the routes; and any other app-wide code such as the theme.شتی هەموو : dependencies.dart، تۆماری (ئامادەکردنی get_it)؛ router.dart، ؛ و هەر کۆدێکی تری هەموو وەک . |
models/ | Plain data classes, one file each: todo.dart, user.dart. دراوەی سادە، هەریەکەیان : todo.dart، user.dart. |
screens/ | One folder per screen, with its view and view model. بۆ هەر شاشەیەک، لەگەڵ و . |
widgets/ | Custom widgets shared by several screens. تایبەت کە چەند شاشەیەک بەشیان دەکەن. |
services/ | Code that talks to data sources: REST, Firebase, mocks.ئەو کۆدەی قسە لەگەڵ دەکات: REST، Firebase، ساختە. |
Two ways to organise the lib folder: by layer or by feature, plus a shared app/core folder, and when each one fits a project.
دوو شێواز بۆ دابەشکردنی lib: بەپێی یان بەپێی feature، لەگەڵ هاوبەشی app/core و کاتی گونجاوی هەریەکەیان بۆ پرۆژەیەک.
The folder names differ from the lesson's (data, domain, presentation; controller, repository). Watch for the two ideas: by layer and by feature. The lesson's project mixes them: screens by feature, models and services by type.
ناوی لەگەڵ وانەکە جیاوازن (data، domain، presentation؛ controller، repository). سەرنج بدە دوو بیرۆکەکە: بەپێی و بەپێی feature. پرۆژەی وانەکە هەردووکیان تێکەڵ دەکات: شاشەکان بەپێی feature، و بەپێی جۆر.
The screens folder: a folder per screenفۆڵدەری screens: فۆڵدەرێک بۆ هەر شاشەیەک
screens/ و widgets/ـی وانەکەTEXTscreens/
├── view.dart (generic view class)
├── viewmodel.dart (generic view model class)
├── login/
│ ├── login_view.dart
│ ├── login_viewmodel.dart
│ └── widgets/
├── todolist/
│ ├── todolist_view.dart
│ ├── todolist_viewmodel.dart
│ └── widgets/
│ ├── appbar_view.dart
│ └── todo_view.dart
└── other_screen/
├── other_view.dart
├── other_viewmodel.dart
└── widgets/
widgets/ (shared widgets)- Each screen has its own folder (
login/,todolist/). - A screen's view and view model sit in the same folder, so you open one folder to work on one screen.
- The
widgets/folder inside a screen folder is for widgets refactored out of that screen, when a widget's code is too large to stay in the screen's file (ourappbar_view.dartandtodo_view.dart). view.dartandviewmodel.darthold generic classes used to build every screen.- The
widgets/folder outside (inlib/) holds custom widgets shared between screens, such as ourUserAvatar.
- هەر شاشەیەک خۆی هەیە (
login/،todolist/). - و شاشەیەک لە هەمان ، بۆیە بۆ کارکردن لەسەر یەک شاشە یەک دەکەیتەوە.
-
widgets/لەناو شاشەیەکدا بۆ ئەو کە لە شاشەکە دەرهێنراون، کاتێک کۆدی زۆر گەورەیە بۆ مانەوە لە شاشەکەدا (appbar_view.dartوtodo_view.dartـی ئێمە). view.dartوviewmodel.dartگشتی هەڵدەگرن کە بۆ دروستکردنی هەموو شاشەیەک بەکار دێن.-
widgets/ـی دەرەوە (لەlib/ـدا) تایبەت هەڵدەگرێت کە لە نێوان شاشەکاندا هاوبەشن، وەکUserAvatarـی ئێمە.
Two small examples. TodoView, one row of the to-do list, was moved out of the to-do screen into that screen's own widgets/ folder. UserAvatar is used by both screens, so it lives in the shared lib/widgets/ folder:
دوو نموونەی بچووک. TodoView، یەک دێڕی لیستی ئەرکەکان، لە شاشەی ئەرکەکانەوە گوازرایەوە بۆ widgets/ـی خودی ئەو شاشەیە. UserAvatar هەردوو شاشەکە بەکاری دەهێنن، بۆیە لە هاوبەشی lib/widgets/ـدا دەژی:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../models/todo.dart';
import '../todolist_viewmodel.dart';
// One to-do row. Taps become events for the view model.
class TodoView extends StatelessWidget {
const TodoView(this.todo, {super.key});
final Todo todo;
@override
Widget build(BuildContext context) {
final viewModel = context.read<TodoListViewModel>();
return ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: Text('id: ${todo.id}'),
onTap: () => viewModel.toggle(todo),
onLongPress: () => viewModel.delete(todo),
);
}
}import 'package:flutter/material.dart';
import '../models/user.dart';
// Shared by the login screen and the to-do screen.
class UserAvatar extends StatelessWidget {
const UserAvatar(this.user, {super.key});
final User user;
@override
Widget build(BuildContext context) {
return CircleAvatar(child: Text(user.name[0]));
}
}The screens folder: a folder per screenفۆڵدەری screens: فۆڵدەرێک بۆ هەر شاشەیەک
The generic classes: ViewModel and BaseViewکلاسە گشتییەکان: ViewModel و BaseView
Here are the two generic classes. ViewModel is the base of every view model: a ChangeNotifier with a busy flag. BaseView gives any view its own ChangeNotifierProvider and Consumer in one line, which is exactly the pattern of step 2 written once:
ئەمە دوو گشتییەکەن. ViewModel بنەمای هەموو : ChangeNotifierـێک بە ئاڵای busy. BaseView بە یەک دێڕ ChangeNotifierProvider و Consumerـی خۆی دەدات بە هەر ، کە ڕێک شێوازی هەنگاوی ٢ـیە کە یەک جار نووسراوە:
import 'package:flutter/foundation.dart';
// The generic view model: every screen's view model extends it.
abstract class ViewModel extends ChangeNotifier {
bool _busy = false;
bool get busy => _busy; // true while data is loading
@protected
void setBusy(bool value) {
_busy = value;
notifyListeners();
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 9
@protected: only subclasses (the real view models) may set it.@protected: تەنها ( ڕاستەقینەکان) دەتوانن دایبنێن.
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'viewmodel.dart';
// The generic view: gives one view its own provider and consumer.
// (Not named View: Flutter already has a widget called View.)
class BaseView<T extends ViewModel> extends StatelessWidget {
const BaseView({super.key, required this.viewModel, required this.builder});
final T viewModel; // taken from the service locator
final Widget Function(BuildContext context, T viewModel, Widget? child)
builder;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>.value(
value: viewModel, // .value: the locator owns it, not this widget
child: Consumer<T>(builder: builder),
);
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 8
Generic:
Tis the view model type of the screen that uses it.گشتی:
Tجۆری ئەو شاشەیەیە کە بەکاری دەهێنێت.- Lineدێڕی 18
.value, because the service locator owns the view model..value، چونکە خاوەنی .
With BaseView, a screen's view is short. It asks the service locator for its view model and builds itself from it:
بە BaseView، شاشەیەک کورتە. داوای لە دەکات و لێیەوە خۆی دروست دەکات:
import 'package:flutter/material.dart';
import '../../app/dependencies.dart';
import '../../app/router.dart' as router;
import '../../widgets/user_avatar.dart';
import '../view.dart';
import 'login_viewmodel.dart';
class LoginView extends StatelessWidget {
const LoginView({super.key});
@override
Widget build(BuildContext context) {
return BaseView<LoginViewModel>(
viewModel: dependency<LoginViewModel>(),
builder: (context, viewModel, _) => 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: UserAvatar(user),
title: Text(user.name),
subtitle: Text('user id: ${user.id}'),
onTap: () {
viewModel.logInAs(user); // an event for the view model
Navigator.of(context).pushNamed(router.todoList);
},
),
],
),
),
);
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 15
The view model comes from the service locator.
لە دێت.
- Lineدێڕی 24
A shared widget from
lib/widgets/.هاوبەش لە
lib/widgets/ـەوە.- Lineدێڕی 29
Routes come from
app/router.dart.لە
app/router.dartـەوە دێن.
The services folder: one abstract class, several implementationsفۆڵدەری services: کلاسێکی ڕووت، چەند جێبەجێکردنێک
services/ـی وانەکە (ناوی فایلەکان بە شێوازی Dart)TEXTservices/
├── rest.dart
├── firebase.dart
├── todo/
│ ├── todo_service.dart (abstract class)
│ ├── todo_service_rest.dart (implementation class)
│ ├── todo_service_mock.dart (implementation class)
│ └── todo_service_firebase.dart (implementation class)
└── user/
├── user_service.dart (abstract class)
├── user_service_rest.dart (implementation class)
├── user_service_mock.dart (implementation class)
└── user_service_firebase.dart (implementation class)- Shared services sit at the root of
services/:rest.dart(talking to the REST server) andfirebase.dart(talking to Firebase) are used by every service. - A service may have different implementations, such as the actual one and a mock one.
- Thus, each service gets its own folder (
todo/,user/), holding the abstract class and all its implementations. - Services are registered in the service locator, in
app/dependencies.dart.
- هاوبەشەکان لە ڕەگی
services/ـدان:rest.dart(قسەکردن لەگەڵ REST) وfirebase.dart(قسەکردن لەگەڵ Firebase) هەموو بەکاریان دەهێنێت. - دەکرێت چەند جێبەجێکردنێکی جیاوازی هەبێت، وەک ڕاستەقینەکە و ساختەیەک.
- بۆیە هەر خۆی وەردەگرێت (
todo/،user/)، کە و هەموو جێبەجێکردنەکانی هەڵدەگرێت. - لە تۆمار دەکرێن، لە
app/dependencies.dartـدا.
The abstract class is a promise: it lists what every to-do service can do, without any code. The view models depend only on it (abstract classes):
بەڵێنێکە: ئەوە ڕیز دەکات کە هەموو ئەرکەکان دەتوانێت بیکات، بەبێ هیچ کۆدێک. تەنها پشت بەو دەبەستن (کلاسە ڕووتەکان):
import '../../models/todo.dart';
// What every to-do service can do. No code here: only the promises.
abstract class TodoService {
Future<List<Todo>> getTodoList(int userId);
Future<Todo> addTodo(int userId, String title);
Future<void> updateTodo(Todo todo);
Future<void> deleteTodo(int id);
}The services folder: one abstract class, several implementationsفۆڵدەری services: کلاسێکی ڕووت، چەند جێبەجێکردنێک
Three implementations of one promiseسێ جێبەجێکردنی یەک بەڵێن
Each implementation keeps the same promises in its own way. The REST one asks the server through the shared RestService:
هەر جێبەجێکردنێک هەمان بەڵێنەکان بە ڕێگای خۆی دەپارێزێت. ئەوەی REST لە ڕێگەی RestServiceـی هاوبەشەوە داوا لە دەکات:
import '../../models/todo.dart';
import '../rest.dart';
import 'todo_service.dart';
// The real one: talks to the REST server.
class TodoServiceRest implements TodoService {
TodoServiceRest(this._rest);
final RestService _rest;
@override
Future<List<Todo>> getTodoList(int userId) async {
final json = await _rest.get('todos?userId=$userId') as List<dynamic>;
return [for (final item in json) Todo.fromJson(item as Map<String, dynamic>)];
}
@override
Future<Todo> addTodo(int userId, String title) async {
final json = await _rest.post(
'todos',
{'userId': userId, 'title': title, 'completed': false},
);
return Todo.fromJson(json as Map<String, dynamic>);
}
@override
Future<void> updateTodo(Todo todo) =>
_rest.patch('todos/${todo.id}', todo.toJson());
@override
Future<void> deleteTodo(int id) => _rest.delete('todos/$id');
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 6
implements: it must provide every method of TodoService.implements: دەبێت هەموو TodoService دابین بکات.- Lineدێڕی 13
The server answers with JSON;
Todo.fromJsonturns it into models.بە وەڵام دەداتەوە؛
Todo.fromJsonدەیگۆڕێت بۆ .
import '../../models/todo.dart';
import 'todo_service.dart';
// A fake one: keeps the lecture's sample to-dos in memory.
class TodoServiceMock implements TodoService {
final List<Todo> _todos = [
const Todo(id: 1, userId: 1, title: 'Prepare proposal for the new project'),
const Todo(id: 2, userId: 1, title: 'Replace light bulb'),
const Todo(id: 3, userId: 1, title: 'Buy Flutter eBook'),
const Todo(id: 4, userId: 1, title: 'Subscribe to fibre-optic internet'),
const Todo(id: 5, userId: 1, title: 'Set up online meeting room', completed: true),
const Todo(id: 6, userId: 2, title: 'New task'),
const Todo(id: 8, userId: 2, title: 'New task', completed: true),
];
@override
Future<List<Todo>> getTodoList(int userId) async =>
_todos.where((t) => t.userId == userId).toList();
@override
Future<Todo> addTodo(int userId, String title) async {
final id = _todos.fold(0, (max, t) => t.id > max ? t.id : max) + 1;
final todo = Todo(id: id, userId: userId, title: title);
_todos.add(todo);
return todo;
}
@override
Future<void> updateTodo(Todo todo) async {
final i = _todos.indexWhere((t) => t.id == todo.id);
if (i != -1) _todos[i] = todo;
}
@override
Future<void> deleteTodo(int id) async => _todos.removeWhere((t) => t.id == id);
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 5
Same promises, no network: perfect for tests and for working without a server.
هەمان بەڵێنەکان، بەبێ : باشترینە بۆ تاقیکردنەوە و بۆ کارکردن بەبێ .
The Firebase one keeps the same promises with Cloud Firestore (set up as in Firebase and Cloud Firestore):
ئەوەی Firebase هەمان بەڵێنەکان بە Cloud Firestore دەپارێزێت (ئامادەکراو وەک لە Firebase و Cloud Firestore):
import '../../models/todo.dart';
import '../firebase.dart';
import 'todo_service.dart';
// Another real one: the same promises, kept with Cloud Firestore.
class TodoServiceFirebase implements TodoService {
TodoServiceFirebase(this._firebase);
final FirebaseService _firebase;
@override
Future<List<Todo>> getTodoList(int userId) async {
final snapshot = await _firebase.db
.collection('todos')
.where('userId', isEqualTo: userId)
.get();
return [for (final doc in snapshot.docs) Todo.fromJson(doc.data())];
}
@override
Future<Todo> addTodo(int userId, String title) async {
final id = DateTime.now().millisecondsSinceEpoch;
final todo = Todo(id: id, userId: userId, title: title);
await _firebase.db.collection('todos').doc('$id').set(todo.toJson());
return todo;
}
@override
Future<void> updateTodo(Todo todo) =>
_firebase.db.collection('todos').doc('${todo.id}').update(todo.toJson());
@override
Future<void> deleteTodo(int id) =>
_firebase.db.collection('todos').doc('$id').delete();
}The services folder: one abstract class, several implementationsفۆڵدەری services: کلاسێکی ڕووت، چەند جێبەجێکردنێک
File names and class namesناوی فایلەکان و ناوی کلاسەکان
The lecture's services tree writes file names like the classes inside them: TodoService.dart, TodoServiceRest.dart. Dart's official style guide, Effective Dart, has a different rule for each kind of name, and the analyzer's lints follow it:
وانەکە ناوی وەک ئەو ناویان دەنووسێت: TodoService.dart، TodoServiceRest.dart. ڕێنمایی شێوازی فەرمیی Dart، Effective Dart، یاسایەکی جیاوازی بۆ هەر جۆرە ناوێک هەیە و lintـەکانی analyzer پەیڕەوی دەکەن:
| Kind of nameجۆری ناو | Styleشێواز | Goodباش | Badخراپ |
|---|---|---|---|
| Files, folders, packages، ، پاکێجەکان | : lowercase with underscores: پیتی بچووک لەگەڵ هێڵی ژێرەوە | todo_service_rest.dart | TodoServiceRest.dart, todo-service.dart |
| Classes, enums, typedefs، enumـەکان، typedefـەکان | UpperCamelCase | TodoServiceRest | todoServiceRest |
| Variables, functions, parameters، ، | (lowerCamelCase) (lowerCamelCase) | dependency, getTodoList | Dependency, get_todo_list |
| Import prefixesپێشگرەکانی import | lowercase with underscoresپیتی بچووک لەگەڵ هێڵی ژێرەوە | as di, as router | as DI, as Router |
get_it: the service locatorget_it: دۆزەرەوەی خزمەتگوزارییەکە
A service locator is one central place where the app registers its services (and, in MVVM, its view models) and where any code can find them by type. get_it is the package the lecture uses for it. Add it with flutter pub add get_it. Unlike provider, it does not live in the widget tree and needs no BuildContext: you can ask it from anywhere, even from a view model.
یەک شوێنی ناوەندییە کە تێدا تۆمار دەکات (و لە ) و هەر کۆدێک دەتوانێت بەپێی جۆر بیاندۆزێتەوە. get_it ئەو کە وانەکە بۆی بەکار دەهێنێت. بە flutter pub add get_it زیادی بکە. بە پێچەوانەی provider، لە ناژی و پێویستی بە BuildContext نییە: دەتوانیت لە هەر شوێنێکەوە داوای لێ بکەیت، تەنانەت لە .
Here is the to-do app's app/dependencies.dart, the lecture's “register dependencies to the service locator” slide written out in full:
ئەمە app/dependencies.dartـی ئەرکەکانە، سلایدی « لە تۆمار بکە»ـی وانەکە بە تەواوی نووسراوە:
import 'package:get_it/get_it.dart';
import '../screens/login/login_viewmodel.dart';
import '../screens/todolist/todolist_viewmodel.dart';
import '../services/rest.dart';
import '../services/todo/todo_service.dart';
import '../services/todo/todo_service_rest.dart';
import '../services/user/user_service.dart';
import '../services/user/user_service_rest.dart';
final dependency = GetIt.instance; // the service locator
void init() {
// Services
dependency.registerLazySingleton(() => RestService());
dependency.registerLazySingleton<TodoService>(
() => TodoServiceRest(dependency<RestService>()),
);
dependency.registerLazySingleton<UserService>(
() => UserServiceRest(dependency<RestService>()),
);
// View models
dependency.registerLazySingleton(
() => LoginViewModel(dependency<UserService>()),
);
dependency.registerLazySingleton(
() => TodoListViewModel(
dependency<TodoService>(),
dependency<LoginViewModel>(),
),
);
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 11
The one global GetIt object (the slide writes
GetIt dependency = …;finalkeeps anyone from replacing it).تاکە گشتیی GetIt (سلایدەکە
GetIt dependency = …دەنووسێت؛finalڕێگە نادات کەس بیگۆڕێتەوە).- Lineدێڕی 15
A lazy singleton: created on first use, then shared.
دواخراو: لە یەکەم بەکارهێناندا دروست دەکرێت، پاشان بەش دەکرێت.
- Lineدێڕی 16
Registered under the abstract type: code asks for
TodoService…بە جۆرە ڕووتەکە تۆمار کراوە: کۆد داوای
TodoServiceدەکات…- Lineدێڕی 17
…and gets this implementation, built with the shared RestService.
…و ئەم جێبەجێکردنە وەردەگرێت، کە بە RestServiceـی هاوبەش دروست کراوە.
- Lineدێڕی 25
View models get their services from the same locator.
لە هەمان وەردەگرن.
registerLazySingleton<TodoService>(() => TodoServiceRest(…)): the type in angle brackets is what code asks for; the function says what it gets.dependency<TodoService>()(ordependency.get<TodoService>()) returns the registered object. Asking for a type that was never registered throws an error.- Registering the same type twice is an error too (see below), which protects you from two different services fighting.
registerLazySingleton<TodoService>(() => TodoServiceRest(…)): جۆرەکەی ناو کەوانە گۆشەییەکان ئەوەیە کە کۆد داوای دەکات؛ دەڵێت چی وەردەگرێت.dependency<TodoService>()(یانdependency.get<TodoService>()) تۆمارکراوەکە دەگەڕێنێتەوە. داواکردنی جۆرێک کە هەرگیز تۆمار نەکراوە دروست دەکات.- تۆمارکردنی هەمان جۆر دوو جار (لە خوارەوە بڕوانە)، کە دەتپارێزێت لە ململانێی دوو جیاواز.
Flutter's official 80-second tour of get_it: register an object once, get it from anywhere, create it only when needed, and swap objects for tests.
ناساندنی فەرمیی get_it لە 80 چرکەدا: یەک جار تێیدا دابنێ، لە هەر شوێنێکەوە وەریبگرە، تەنها کاتی پێویست دروستی بکە و بۆ بگۆڕەوە.
A very short overview; the lesson explains why a service locator helps. The video's 'lazy initialization' uses registerFactory (a new object on every get); the lesson's table compares it with registerLazySingleton.
پوختەیەکی زۆر کورت؛ وانەکە ڕوون دەکاتەوە بۆچی سوودی هەیە. بەشی lazy initialization لە ڤیدیۆکەدا registerFactory بەکار دەهێنێت (لە هەر getـێکدا نوێ)؛ وانەکە لە بەراوردێکدا لەگەڵ registerLazySingleton دایدەنێت.
get_it: the service locatorget_it: دۆزەرەوەی خزمەتگوزارییەکە
Singleton, lazy singleton or factory?تاکنموونە، تاکنموونەی دواخراو یان factory؟
get_it can create an object at three different moments. The lecture uses registerLazySingleton for everything. This small Dart program shows all three, each class printing a line when it is created:
get_it دەتوانێت لە سێ کاتی جیاوازدا دروست بکات. وانەکە بۆ هەموو شتێک registerLazySingleton بەکار دەهێنێت. ئەم بچووکی Dart هەر سێکیان پیشان دەدات، هەر کاتێک دروست دەکرێت دێڕێک چاپ دەکات:
import 'package:get_it/get_it.dart';
final dependency = GetIt.instance;
class RestService {
RestService() {
print(' RestService created');
}
}
class Clock {
Clock() {
print(' Clock created');
}
}
class Order {
Order() {
print(' Order created');
}
}
void main() {
print('1. register');
dependency.registerSingleton(Clock()); // made now
dependency.registerLazySingleton(() => RestService()); // made on first use
dependency.registerFactory(() => Order()); // made on every use
print('2. first use');
final a = dependency<RestService>();
dependency<Clock>();
final o1 = dependency<Order>();
print('3. second use');
final b = dependency<RestService>();
final o2 = dependency<Order>();
print('same RestService? ${identical(a, b)}');
print('same Order? ${identical(o1, o2)}');
}1. register Clock created 2. first use RestService created Order created 3. second use Order created same RestService? true same Order? false
| Registrationتۆمارکردن | Created when?کەی دروست دەکرێت؟ | How many?چەند دانە؟ | Good forباشە بۆ |
|---|---|---|---|
registerSingleton(Clock()) | At once, during registrationیەکسەر، لە کاتی تۆمارکردندا | One, sharedیەک، هاوبەش | Something needed from the very startشتێک کە لە سەرەتاوە پێویستە |
registerLazySingleton(() => RestService()) | On first useلە یەکەم بەکارهێناندا | One, sharedیەک، هاوبەش | Services and view models (the lecture's choice) و (هەڵبژاردنی وانەکە) |
registerFactory(() => Order()) | On every useلە هەموو بەکارهێنانێکدا | A new one each timeهەر جارێک یەکێکی نوێ | Short-lived objects, such as one form's data کورتخایەنەکان، وەک دراوەی یەک فۆرم |
Calling init() in main()بانگکردنی init() لە main()ـدا
The locator must be filled before the first widget asks it for anything, so main() calls init() before runApp. The lecture imports app/dependencies.dart with the prefix di (dependency injection) and the router with the prefix router. Here is our final main.dart, with every screen getting its own provider:
دەبێت پێش ئەوەی یەکەم داوای هەر شتێکی لێ بکات پڕ بکرێتەوە، بۆیە main() پێش runApp init() بانگ دەکات. وانەکە app/dependencies.dart بە پێشگری di () و router بە پێشگری router import دەکات. ئەمە main.dartـی کۆتایی ئێمەیە، کە هەموو شاشەیەک providerـی خۆی وەردەگرێت:
import 'package:flutter/material.dart';
import 'app/dependencies.dart' as di;
import 'app/router.dart' as router;
void main() {
di.init(); // fill the service locator before the first widget is built
runApp(const TodoApp());
}
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MVVM to-do',
theme: ThemeData(colorSchemeSeed: Colors.green),
initialRoute: router.login,
routes: router.routes,
);
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 3
as di:di.init()anddi.dependencysay clearly where they come from.as di:di.init()وdi.dependencyبە ڕوونی دەڵێن لە کوێوە دێن.- Lineدێڕی 7
First fill the locator…
یەکەم پڕ بکەرەوە…
- Lineدێڕی 9
…then start the app. No providers above MaterialApp any more.
…پاشان دەست پێ بکە. چیتر هیچ providerـێک لە سەرووی MaterialAppـەوە نییە.
import 'package:flutter/widgets.dart';
import '../screens/login/login_view.dart';
import '../screens/todolist/todolist_view.dart';
const login = '/';
const todoList = '/todos';
final Map<String, WidgetBuilder> routes = {
login: (_) => const LoginView(),
todoList: (_) => const TodoListView(),
};The lecture's slide shows an earlier step, cut off after a few lines: services already come from get_it, but the state still sits in providers above MaterialApp, and the DI calls changed from Provider.of() to the locator. Written out and corrected, it looks like this:
سلایدەکەی وانەکە هەنگاوێکی پێشتر پیشان دەدات، کە دوای چەند دێڕێک بڕاوە: پێشتر لە get_itـەوە دێن، بەڵام هێشتا لە providerـەکانی سەرووی MaterialAppـدایە و بانگکردنەکانی لە Provider.of()ـەوە گۆڕاون بۆ . بە تەواوی نووسراو و چاککراو، بەم شێوەیە دەبێت:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app/dependencies.dart' as di;
import 'models/user.dart';
import 'services/user/user_service.dart';
// The lecture's first step: services come from get_it,
// app state still lives in providers above MaterialApp.
void main() {
di.init();
runApp(
MultiProvider(
providers: [
FutureProvider<List<User>>(
create: (_) => di.dependency<UserService>().getUserList(),
initialData: const [], // required since provider 5
),
ChangeNotifierProvider(create: (_) => ValueNotifier<User?>(null)),
],
child: const MaterialApp(home: UserListScreen()),
),
);
}
class UserListScreen extends StatelessWidget {
const UserListScreen({super.key});
@override
Widget build(BuildContext context) {
final users = context.watch<List<User>>();
return Scaffold(
appBar: AppBar(title: const Text('Choose a user to log in as')),
body: ListView(
children: [for (final u in users) ListTile(title: Text(u.name))],
),
);
}
}Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 17
The FutureProvider gets its service from the locator, not from another provider.
FutureProvider لە وەردەگرێت، نەک لە providerـێکی ترەوە.
- Lineدێڕی 18
Missing on the slide: required since provider 5.
لەسەر سلایدەکە نییە: لە provider 5ـەوە پێویستە.
Swapping the real service for a fake oneگۆڕینەوەی خزمەتگوزارییە ڕاستەقینەکە بە ساختەیەک
Now the payoff of the whole structure. The app talks to the server only through TodoService, and only init() says which implementation that is. Change one line in init(), restart the app, and it runs on fake data, with no server at all:
ئێستا بەرهەمی هەموو پێکهاتەکە. تەنها لە ڕێگەی TodoServiceـەوە قسە لەگەڵ دەکات و تەنها init() دەڵێت ئەوە کام جێبەجێکردنە. یەک دێڕ لە init()ـدا بگۆڕە، دووبارە دەست پێ بکەرەوە و لەسەر دراوەی ساختە کار دەکات، بەبێ هیچ :


Widget tests show why this matters. Inside flutter test the real network is blocked (every HTTP request gets status 400), so the REST service cannot work there; the same app with mock services runs normally:
تاقیکردنەوەکانی پیشان دەدەن بۆچی ئەمە گرنگە. لەناو flutter testـدا ڕاستەقینە داخراوە (هەموو HTTP 400 وەردەگرێت)، بۆیە REST لەوێ کار ناکات؛ هەمان بە ساختەکان بە ئاسایی کار دەکات:
TodoService -> TodoServiceRest
UserService -> UserServiceRest
TodoServiceRest: Exception: GET todos?userId=1 failed: HTTP 400
login screen: Aram | Shilan | Karwan
Karwan: 0 to-dos
after +: New taskIn tests you usually replace registrations instead of editing init(). get_it refuses to register the same type twice (unless you set dependency.allowReassignment = true), and reset() clears everything so a test can register mocks. This Dart program shows both:
لە تاقیکردنەوەکاندا زۆربەی جار تۆمارکردنەکان دەگۆڕیتەوە لەبری دەستکاریکردنی init(). get_it ڕەت دەکاتەوە هەمان جۆر دوو جار تۆمار بکات (مەگەر dependency.allowReassignment = true دابنێیت) و reset() هەموو شتێک پاک دەکاتەوە تا تاقیکردنەوەیەک بتوانێت ساختەکان تۆمار بکات. Dartـی خوارەوە هەردووکیان پیشان دەدات:
import 'package:get_it/get_it.dart';
final dependency = GetIt.instance;
abstract class TodoService {
Future<List<String>> getTodoList(int userId);
}
class TodoServiceRest implements TodoService {
@override
Future<List<String>> getTodoList(int userId) async =>
throw Exception('no server in this demo');
}
class TodoServiceMock implements TodoService {
@override
Future<List<String>> getTodoList(int userId) async =>
['Replace light bulb', 'Buy Flutter eBook'];
}
Future<void> main() async {
dependency.registerLazySingleton<TodoService>(() => TodoServiceRest());
print('registered: ${dependency<TodoService>().runtimeType}');
try {
dependency.registerLazySingleton<TodoService>(() => TodoServiceMock());
} catch (e) {
print('second registration: ${e.toString().split('\n').first}');
}
await dependency.reset(); // clears every registration (handy in tests)
dependency.registerLazySingleton<TodoService>(() => TodoServiceMock());
final service = dependency<TodoService>();
print('registered: ${service.runtimeType}');
print(await service.getTodoList(1));
}registered: TodoServiceRest second registration: Invalid argument(s): Type TodoService is already registered inside GetIt. registered: TodoServiceMock [Replace light bulb, Buy Flutter eBook]
get_it and provider: different jobsget_it و provider: کاری جیاواز
The finished app uses both packages, and they do not compete. get_it finds objects; provider (or ListenableBuilder) rebuilds widgets when those objects notify:
تەواوەکە هەردوو بەکار دەهێنێت و ململانێ ناکەن. get_it دەدۆزێتەوە؛ provider (یان ListenableBuilder) دووبارە دروست دەکاتەوە کاتێک ئەو ئاگادار دەکەنەوە:
| Pointخاڵ | get_it | provider |
|---|---|---|
| What it isچییە | A service locator: a global registry of objects by type: تۆمارێکی گشتیی بەپێی جۆر | Dependency injection through the widget tree لە ڕێگەی |
| Needs a BuildContext?پێویستی بە BuildContext هەیە؟ | No: usable anywhere, also in view models and testsنەخێر: لە هەر شوێنێک بەکار دێت، لە و تاقیکردنەوەکانیشدا | Yes: only widgets below the provider can read itبەڵێ: تەنها ئەو لە خوارەوەی providerـەکەن دەتوانن بیخوێننەوە |
| Rebuilds widgets? دووبارە دروست دەکاتەوە؟ | Noنەخێر | Yes, with watch, Consumer, Selectorبەڵێ، بە watch، Consumer، Selector |
| Lifetimeتەمەن | As long as the app runs (until reset or unregister)تا کار دەکات (تا reset یان unregister) | As long as the provider widget is in the treeتا providerـەکە لە دارەکەدایە |
| In the to-do appلە ئەرکەکاندا | Holds services and view models و هەڵدەگرێت | Each view's ChangeNotifierProvider.value + ConsumerChangeNotifierProvider.value + Consumerـی هەر |
Common mistakesهەڵە باوەکان
| Mistakeهەڵە | What happensچی ڕوو دەدات | Fixچارەسەر |
|---|---|---|
Naming files like classes (TodoService.dart)ناونانی وەک (TodoService.dart) | The file_names lint warns; imports become inconsistentlintـی file_names ئاگادار دەکاتەوە؛ importـەکان ناڕێک دەبن | todo_service.dart; class TodoServicetodo_service.dart؛ TodoService |
A generic class called View گشتی بە ناوی View | ambiguous_import with Flutter's own View widgetambiguous_import لەگەڵ Viewـی خودی Flutter | BaseViewBaseView |
Registering the concrete type: registerLazySingleton(() => TodoServiceRest(…))تۆمارکردنی جۆرە دیاریکراوەکە: registerLazySingleton(() => TodoServiceRest(…)) | dependency<TodoService>() fails: nothing is registered as TodoServicedependency<TodoService>() شکست دەهێنێت: هیچ شتێک وەک TodoService تۆمار نەکراوە | registerLazySingleton<TodoService>(() => TodoServiceRest(…))registerLazySingleton<TodoService>(() => TodoServiceRest(…)) |
runApp before init()runApp پێش init() | The first view gets an error from get_itیەکەم لە get_itـەوە وەردەگرێت | Call di.init() first in mainیەکەم di.init() لە mainـدا بانگ بکە |
Calling init() twice (for example in a test)بانگکردنی init() دوو جار (بۆ نموونە لە تاقیکردنەوەیەکدا) | “Type … is already registered inside GetIt” «Type … is already registered inside GetIt» | await dependency.reset() between testsawait dependency.reset() لە نێوان تاقیکردنەوەکاندا |
Expecting hot reload to pick up a changed init()چاوەڕوانی ئەوەی hot reload init()ـی گۆڕاو وەربگرێت | Hot reload does not run main again; the old registration stayshot reload main دووبارە جێبەجێ ناکاتەوە؛ تۆمارکردنە کۆنەکە دەمێنێتەوە | Use hot restart (or stop and run)hot restart بەکار بهێنە (یان بیوەستێنە و دووبارە کاری پێ بکەرەوە) |
Exercisesڕاهێنانەکان
Your app gets a third screen, settings, with a SettingsViewModel, and a SettingsService that saves settings on the phone (with a real and a mock version). Write the new files and folders, with correct names.
شاشەیەکی سێیەم وەردەگرێت، settings، لەگەڵ SettingsViewModel و SettingsServiceـێک کە ڕێکخستنەکان لەسەر مۆبایلەکە پاشەکەوت دەکات (بە وەشانێکی ڕاستەقینە و ساختە). و نوێیەکان بە ناوی دروست بنووسە.
Show answerوەڵامەکە پیشان بدە
lib/
├── screens/
│ └── settings/
│ ├── settings_view.dart
│ ├── settings_viewmodel.dart
│ └── widgets/
└── services/
└── settings/
├── settings_service.dart (abstract class)
├── settings_service_prefs.dart (shared_preferences)
└── settings_service_mock.dartAnd two new lines in app/dependencies.dart: registerLazySingleton<SettingsService>(() => SettingsServicePrefs()) and registerLazySingleton(() => SettingsViewModel(dependency<SettingsService>())).
و دوو دێڕی نوێ لە app/dependencies.dartـدا: registerLazySingleton<SettingsService>(() => SettingsServicePrefs()) و registerLazySingleton(() => SettingsViewModel(dependency<SettingsService>())).
Write UserService (abstract) with getUserList(), and a UserServiceMock that returns Aram, Shilan and Karwan. Then register the mock in init().
UserService (ڕووت) بە getUserList() بنووسە و UserServiceMockـێک کە ئارام، شیلان و کاروان دەگەڕێنێتەوە. پاشان ساختەکە لە init()ـدا تۆمار بکە.
Show answerوەڵامەکە پیشان بدە
import '../../models/user.dart';
abstract class UserService {
Future<List<User>> getUserList();
}
class UserServiceMock implements UserService {
@override
Future<List<User>> getUserList() async => const [
User(id: 1, name: 'Aram', email: 'aram@example.com'),
User(id: 2, name: 'Shilan', email: 'shilan@example.com'),
User(id: 3, name: 'Karwan', email: 'karwan@example.com'),
];
}
// in init():
dependency.registerLazySingleton<UserService>(() => UserServiceMock());With registerLazySingleton(() => A()) and registerFactory(() => B()), where A() and B() print “A made” and “B made”, the program calls dependency<B>(), dependency<A>(), dependency<B>(), dependency<A>(). What is printed?
لەگەڵ registerLazySingleton(() => A()) و registerFactory(() => B())، کە A() و B() «A made» و «B made» چاپ دەکەن، dependency<B>()، dependency<A>()، dependency<B>()، dependency<A>() بانگ دەکات. چی چاپ دەکرێت؟
Show answerوەڵامەکە پیشان بدە
B made
A made
B madeThe factory makes a new B on every call; the lazy singleton makes A once, on the first call, and reuses it.
factory لە هەموو بانگکردنێکدا Bـێکی نوێ دروست دەکات؛ دواخراو A یەک جار، لە یەکەم بانگکردندا، دروست دەکات و دووبارە بەکاری دەهێنێتەوە.
Find three problems in this code and fix them.
سێ کێشە لەم کۆدەدا بدۆزەرەوە و چاکیان بکە.
// lib/services/todo/TodoServiceRest.dart
class TodoServiceRest implements TodoService { /* … */ }
// lib/app/dependencies.dart
void init() {
dependency.registerLazySingleton(() => TodoServiceRest());
}
// lib/main.dart
void main() {
runApp(const TodoApp());
di.init();
}Show answerوەڵامەکە پیشان بدە
- The file name
TodoServiceRest.dartshould betodo_service_rest.dart. runAppruns beforeinit(): calldi.init()first.registerLazySingleton(() => TodoServiceRest())registers the concrete type, sodependency<TodoService>()finds nothing: writeregisterLazySingleton<TodoService>(…).
- ناوی
TodoServiceRest.dartدەبێتtodo_service_rest.dartبێت. runAppپێشinit()جێبەجێ دەبێت: یەکەمdi.init()بانگ بکە.registerLazySingleton(() => TodoServiceRest())جۆرە دیاریکراوەکە تۆمار دەکات، بۆیەdependency<TodoService>()هیچ نادۆزێتەوە:registerLazySingleton<TodoService>(…)بنووسە.
Summaryپوختە
lib/holdsmain.dartand five folders:app/(dependencies.dart, router.dart),models/,screens/,widgets/(shared) andservices/.- Each screen has a folder with its view, view model and a
widgets/folder for parts refactored out of it; genericViewModelandBaseViewclasses build every screen. - Each service has a folder with an abstract class and its implementations (REST, mock, Firebase); shared helpers sit at the root of
services/. - File names are lowercase_with_underscores; classes are UpperCamelCase.
- get_it is the service locator:
registerLazySingleton<Abstract>(() => Implementation())ininit(),dependency<T>()to get it, anddi.init()beforerunApp. - Swapping one registration (
Rest → Mock) changes the whole app's data source without touching views or view models.
lib/main.dartو پێنج هەڵدەگرێت:app/(dependencies.dart، router.dart)،models/،screens/،widgets/(هاوبەش) وservices/.- هەر شاشەیەک هەیە لەگەڵ ، و
widgets/بۆ ئەو بەشانەی لێی دەرهێنراون؛ گشتییViewModelوBaseViewهەموو شاشەیەک دروست دەکەن. - هەر هەیە لەگەڵ و جێبەجێکردنەکانی (REST، ساختە، Firebase)؛ یاریدەدەرە هاوبەشەکان لە ڕەگی
services/ـدان. - ناوی پیتی بچووک و هێڵی ژێرەوەن؛ UpperCamelCaseـن.
- get_it :
registerLazySingleton<Abstract>(() => Implementation())لەinit()ـدا،dependency<T>()بۆ وەرگرتنی وdi.init()پێشrunApp. - گۆڕینەوەی یەک تۆمارکردن (
Rest → Mock) هەموو دەگۆڕێت بەبێ دەستلێدان لە یان .
Lesson complete!وانەکە تەواو بوو!
Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.
Key termsزاراوە سەرەکییەکان
Related lecturesوانە پەیوەندیدارەکان
- Project: A To-Do App with Providersپڕۆژە: ئەپێکی ئەرکەکان بە providerـەکان
- Getting Data from the Internet: HTTP and JSONوەرگرتنی دراوە لە ئینتەرنێت: HTTP و JSON
- Firebase and Cloud FirestoreFirebase و Cloud Firestore
- App Architecture: the MVVM Patternتەلارسازیی نەرمەواڵە: شێوازی MVVM
- FutureProvider and StreamProviderFutureProvider و StreamProvider
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- pub.dev: get_it 9.3.0 (registerSingleton, registerLazySingleton, registerFactory, reset)
- Dart: Effective Dart, Style (naming)
- Flutter docs: Guide to app architecture
- Flutter docs: Architecture case study, data layer (abstract repositories with remote and local versions)
- Flutter API: View class
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
- Flutter docs: Architecture case study, package structure (UI by feature, data by type)
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.