Mövcud qarışıq ekranı qatlara köçürmək
Real layihələrin çoxu sıfırdan başlamır. Mövcud, qatları olmayan kod bazasını bir dəfəyə yenidən yazmaq praktik olaraq mümkün deyil: iş dayanır, review edilə bilməyən nəhəng PR yaranır və risk idarə olunmaz olur.
İşləyən yanaşma tədricidir və iki qaydaya söykənir:
1. Yeni kod düzgün qaydalarla yazılır. Bu, ən vacib addımdır: problem böyüməyi dayandırır. Yeni feature-lər service + repository + notifier ilə gəlir, köhnə ekranlar isə yerində qalır. 2. Köhnə kod toxunulduqda köçürülür. Bir ekranda baq düzəldirsənsə ya da funksiya əlavə edirsənsə, əvvəlcə onu qatlara ayır, sonra dəyişikliyi et. Bu, "boy scout" qaydasının praktik formasıdır.
Heç vaxt toxunulmayan ekranlar isə köçürülməyə bilər — və bu, normal qərardır. 5 ildir dəyişmədən işləyən ekranı refaktor etmək heç bir qazanc vermir, yalnız risk gətirir.
Ən vacib texniki qayda: struktur dəyişikliyi və davranış dəyişikliyi heç vaxt bir commit-də olmur. Bu, review-u mümkün edir və problem çıxdıqda hansı commit-in səbəb olduğunu tapmağa imkan verir.
| Addım | Nə edilir | Bitmə meyarı |
|---|---|---|
| 0. Təhlükəsizlik şəbəkəsi | Mövcud davranış üçün 1-2 widget testi yaz (nə göstərilir, əsas hərəkət işləyir) | Testlər yaşıldır və refaktor zamanı sınmasına güvənə bilirsən |
| 1. Service ayır | HTTP çağırışlarını widget-dən `...ApiClient` sinfinə köçür; `http.Client` konstruktordan | Widget-də `http`, `Uri`, `jsonDecode` yoxdur |
| 2. Model ayır | `Map<String, dynamic>` yerinə tipli model; JSON açarları bir faylda qalır | `raw['key']` yalnız DTO/mapper faylındadır |
| 3. Repository ayır | Service üzərində müqavilə + implementasiya; cache və xəta çevrilməsi buraya köçür | Widget yalnız müqaviləni tanıyır |
| 4. Notifier ayır | `setState` və `bool _loading` sahələri notifier-ə və `AsyncValue`-a köçür | Widget `ConsumerWidget`-dir, `setState` yoxdur |
| 5. Biznes qaydalarını köçür | Hesablamalar (`* 1.18`, `/ 100`) domain modelinin getter-lərinə | Qayda üçün unit test var və `build`-də hesablama yoxdur |
| 6. Xəta axını | `catch (e)` → tipli `Failure`; `e.toString()` ekrandan silinir | Dörd hal (loading, boş, məlumat, xəta) emal olunur |
| 7. Testləri tamamla | Model, mapper, repository, notifier üçün unit testlər | 0-cı addımdaki widget testi hələ də yaşıldır |
Sıra niyə belədir. Hər addım özündən əvvəlkinə söykənir və hər biri tək başına commit edilə bilər:
- Service birinci gəlir, çünki o, ən aydın sərhəddir (HTTP çağırışları göz qabağındadır) və dərhal test imkanı verir.
- Model ikincidir, çünki repository müqaviləsi tipli model tələb edir.
- Notifier repository-dən sonra gəlir: əks halda notifier
Mapilə işləməyə başlayır və sonra yenidən dəyişməli olur. - Biznes qaydaları sonda köçürülür, çünki bu, ən çox diqqət tələb edən addımdır — burada davranış təsadüfən dəyişə bilər.
Sıfırıncı addımın vacibliyi. Refaktor davranışı dəyişməməlidir, lakin bunu necə bilirsən? Mövcud kod üçün testlər yoxdursa, refaktordan sonra nəyin sındığını görmək mümkün olmur. Ona görə 1-2 widget testi yazmaq ("ekran məlumatı göstərir", "düymə işləyir") refaktorun ön şərtidir. Bu testlər "characterization test" adlanır: onlar kodun mövcud davranışını qeyd edir, ideal davranışını deyil.
Nə vaxt dayanmaq. Miqrasiyanı tam bitirmək məcburi deyil. Praktik hədəf: yeni kod düzgün, toxunulan köhnə kod köçürülmüş. Heç vaxt açılmayan ekranlar köhnə üslubda qala bilər — onları refaktor etmək xalis risk və sıfır qazancdır.
// ══════ ƏVVƏL: hər şey widget-də ══════
class _ProductsPageState extends State<ProductsPage> {
List<dynamic> _items = [];
bool _loading = true;
String? _error;
Future<void> _load() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/v1/products?active=true'),
headers: {'Authorization': 'Bearer $kToken'},
);
if (response.statusCode != 200) {
setState(() { _error = 'Xəta: ${response.statusCode}'; });
return;
}
setState(() {
_items = jsonDecode(response.body)['data'] as List<dynamic>;
_loading = false;
});
} catch (e) {
setState(() { _error = e.toString(); _loading = false; });
}
}
// ... build: raw['price_cents'] / 100 və s.
}
// ══════ ADDIM 1-dən SONRA ══════
// Yeni fayl: lib/data/services/product_api_client.dart
class ProductApiClient {
ProductApiClient({required http.Client client, required this.tokenProvider})
: _client = client;
final http.Client _client;
final Future<String?> Function() tokenProvider;
/// Hələ də `List<dynamic>` qaytarır — model ADDIM 2-dədir.
/// Vacib olan: HTTP və status kodu widget-dən ÇIXDI.
Future<List<dynamic>> getActiveProducts() async {
final token = await tokenProvider();
final response = await _client.get(
Uri.parse('https://api.example.com/v1/products?active=true'),
headers: {if (token != null) 'Authorization': 'Bearer $token'},
);
return switch (response.statusCode) {
200 => jsonDecode(response.body)['data'] as List<dynamic>,
401 => throw const UnauthorizedException(),
>= 500 => throw ServerException(response.statusCode),
_ => throw ApiException(response.statusCode, response.body),
};
}
}
// Widget: dəyişiklik minimaldır — bir çağırış.
Future<void> _load() async {
try {
final items = await widget.apiClient.getActiveProducts();
setState(() { _items = items; _loading = false; });
} on UnauthorizedException {
setState(() { _error = 'Sessiya bitdi'; _loading = false; });
} catch (e) {
setState(() { _error = 'Yüklənmə alınmadı'; _loading = false; });
}
}
// ── Bu addımın DƏRHAL qazancı ──
// 1) Service üçün `MockClient` ilə test yazmaq mümkündür:
// status kodları, yanlış JSON, timeout — hamısı yoxlanıla bilər.
// 2) `e.toString()` ekrandan çıxdı.
// 3) İkinci ekran gəldikdə sorğu kopyalanmır.
//
// ── Bu addımda EDİLMƏYƏNLƏR (qəsdən) ──
// • model ayrılmadı (ADDIM 2)
// • repository yoxdur (ADDIM 3)
// • setState qaldı (ADDIM 4)
// Hər addım ayrı commit — review edilə bilən ölçüdə.Addım 1: service-in ayrılması. Ən kiçik, ən təhlükəsiz və dərhal fayda verən addım.
Ən çox edilən səhv: hamısını bir dəfəyə. "Bu ekranı düzgün yazım" niyyəti ilə başlanan iş 40 fayllıq PR-a çevrilir; review edilə bilmir, iki həftə mərge olunmur, bu müddətdə konfliktlər yığılır və nəticədə ya rədd edilir, ya diqqətsiz mərge olunur.
Hər addımın ayrı commit olması texniki tələb deyil, sosial tələbdir: komanda yoldaşının 200 sətirlik diff-i oxumağa vaxtı var, 4000 sətirlik diff-i isə yox.
Praktika. Layihəndən ən çox toxunulan ekranı seç (git tarixinə baxaraq: git log --format=format: --name-only | sort | uniq -c | sort -rn | head) və 0-4 addımlarını icra et. Hər addım ayrı commit olsun.
Hazır sayılır: 5 commit var, hər birindən sonra flutter analyze && flutter test yaşıldır, və 0-cı addımdaki widget testi heç vaxt sınmayıb.
📚 Mənbələr və sənədlər
- Arxitektura tövsiyələrirəsmidocs.flutter.dev
Miqrasiyanın hədəf vəziyyəti: hansı bəndlər mütləq, hansılar şərtlidir.
- Case study: data qatırəsmidocs.flutter.dev
Service və repository-nin hədəf forması — miqrasiyanın 1-3 addımları üçün nümunə.
- Flutter: widget testlərirəsmidocs.flutter.dev
0-cı addım üçün: mövcud davranışı qeyd edən widget testinin yazılması.