3-qism · Amaliy Flutter·09-bob
Networking, JSON serializatsiya va offline-first
http va dio orasidagi farqdan tortib, JSON serializatsiya strategiyalari, xatolarni modellash va offline-first arxitekturagacha — Flutter ilovalarida ma'lumot qatlamini qurish.
Deyarli har qanday real Flutter ilovasi tarmoq bilan ishlaydi. Intervyuda "API bilan qanday ishlaysiz?" degan savol aslida ko'p narsani sinaydi: HTTP klientni tanlash, xatolarni to'g'ri modellash, va tarmoq yo'qligida ilova qanday xatti-harakat qilishi. Bu bobda ma'lumot qatlamini boshidan oxirigacha ko'rib chiqamiz.
1. http va dio — qachon qaysi birini tanlash#
Dart'ning rasmiy http paketi — minimal, past darajali klient:
final response = await http.get(Uri.parse('https://api.example.com/users/1'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
}dio esa production ilovalar uchun zarur bo'lgan ko'p imkoniyatni
o'z ichiga oladi: interceptor'lar, so'rovni bekor qilish, avtomatik retry,
FormData bilan fayl yuklash va boshqalar:
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer ${AuthStore.token}';
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
AuthStore.logout();
}
handler.next(error);
},
));
final response = await dio.get('/users/1');Interceptor'lar — dio'ning eng qimmatli xususiyati: token qo'shish, xatoларни markazlashgan qayta ishlash, loglash kabi "cross-cutting" mantiqni har bir so'rov chaqiruvida takrorlash o'rniga bir joyga jamlaydi.
CancelToken — foydalanuvchi ekrandan chiqqanda yoki qidiruv so'zi tez-tez o'zgarganda (masalan, har harf kiritilganda yangi so'rov ketayotgan bo'lsa) eskirgan so'rovlarni bekor qilish uchun kerak:
final cancelToken = CancelToken();
dio.get('/search', queryParameters: {'q': query}, cancelToken: cancelToken);
// Ekrandan chiqishda yoki yangi so'rov boshlanganda:
cancelToken.cancel('eskirgan so\'rov');| Kriteriy | http | dio |
|---|---|---|
| Og'irligi | Minimal, kam bog'liqlik | Kattaroq, lekin ko'proq imkoniyat |
| Interceptor'lar | Yo'q (o'zingiz client wrap qilishingiz kerak) | Ichki qo'llab-quvvatlanadi |
| So'rovni bekor qilish | Qo'lda http.Client().close() bilan cheklangan | CancelToken bilan aniq boshqariladi |
| Fayl yuklash progress | Qo'lda stream orqali | onSendProgress/onReceiveProgress tayyor |
| Qachon tanlanadi | Kichik skript, minimal bog'liqlik kerak bo'lganda | Deyarli har qanday real production ilova |
2. JSON serializatsiya — qo'lda yozishdan kod generatsiyagacha#
Eng oddiy holat — qo'lda fromJson/toJson:
class User {
User({required this.id, required this.name, this.email});
final int id;
final String name;
final String? email;
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String?,
);
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
}Kichik loyihalarda bu yetarli, lekin model o'sishi bilan xatoga moyil bo'ladi (maydon nomini xato yozish, tur mos kelmasligi kompilyatsiya vaqtida ushlanmaydi). Shuning uchun katta loyihalarda kod generatsiya standart:
// json_serializable
@JsonSerializable()
class User {
User({required this.id, required this.name, this.email});
final int id;
final String name;
final String? email;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}freezed esa bundan ham uzoqroqqa boradi — immutable class, copyWith,
==/hashCode, va union typelarni (masalan, natija holatlarini
modellash uchun) avtomatik generatsiya qiladi:
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
String? email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}build_runner orqali kod generatsiya qilinadi:
dart run build_runner build --delete-conflicting-outputs3. Xatolarni modellash — exception'dan Result turigacha#
Eng oddiy yondashuv — try/catch bilan Exception:
Future<User> fetchUser(int id) async {
final response = await dio.get('/users/$id');
if (response.statusCode != 200) {
throw ApiException('Foydalanuvchi topilmadi', response.statusCode);
}
return User.fromJson(response.data);
}Muammo: chaqiruvchi kod xato bo'lishi mumkinligini kompilyator darajasida
bilmaydi — try/catch yozishni unutish oson va bu runtime'da qo'lga
tushadi. Shuning uchun ko'p senior jamoalar Result/Either turini afzal
ko'radi — Dart 3'ning sealed class'lari bilan bu tabiiy modellashadi:
sealed class Result<T> {}
class Success<T> extends Result<T> {
Success(this.value);
final T value;
}
class Failure<T> extends Result<T> {
Failure(this.error);
final AppError error;
}
Future<Result<User>> fetchUser(int id) async {
try {
final response = await dio.get('/users/$id');
return Success(User.fromJson(response.data));
} on DioException catch (e) {
return Failure(AppError.fromDioException(e));
}
}final result = await fetchUser(1);
switch (result) {
case Success(:final value):
print('Foydalanuvchi: ${value.name}');
case Failure(:final error):
print('Xato: ${error.message}');
}switch — sealed class bilan exhaustive (barcha holatlarni qamrab
olishni kompilyator tekshiradi) — yangi holat qo'shilsa, uni qayta ishlamagan
joylar compile vaqtida ko'rinadi. Bu xatoni chaqiruvchi kodning "tur
tizimi darajasida" e'tiborsiz qoldirib bo'lmasligini ta'minlaydi.
4. Offline-first arxitektura#
"Offline-first" — ilova tarmoqni standart holat emas, istisno deb hisoblashi kerak degan tamoyil. Amaliy naqsh — local-first ma'lumot oqimi: UI har doim mahalliy bazadan o'qiydi, tarmoq faqat fonda sinxronlaydi.
Mahalliy baza tanlanadi
sqflite(xom SQL),drift(type-safe SQL wrapper, reaktiv so'rovlar) yokihive/isar(NoSQL, kalit-qiymat/obyekt bazasi) — struktura murakkabligi va so'rov talablariga qarab tanlanadi.UI mahalliy bazadan o'qiydi
StreamBuilderyoki reaktiv query (masalan,drift'ningwatch()) orqali — ma'lumot mahalliy bazada o'zgarganda UI avtomatik yangilanadi, tarmoq holatidan qat'i nazar.Fon sinxronizatsiyasi
Tarmoq mavjud bo'lganda serverdan yangi ma'lumot olinadi va mahalliy bazaga yoziladi — bu yozish UI'ni avtomatik yangilaydi, chunki UI bazani "tomosha qilmoqda".
Offline yozuvlar navbatga qo'yiladi
Foydalanuvchi offline holatda o'zgartirish kiritsa (masalan, forma yuboradi), bu amal mahalliy "pending actions" navbatiga yoziladi va tarmoq qaytganda avtomatik yuboriladi.
Keshlash strategiyalari#
| Strategiya | Tavsif | Qachon mos |
|---|---|---|
| Cache-first | Avval keshdan ko'rsatiladi, keyin fonda yangilanadi | Tez-tez o'zgarmaydigan, "eskirgan ma'lumot ham foydali" holatlar |
| Network-first | Avval tarmoqqa urinadi, muvaffaqiyatsiz bo'lsa keshga tushadi | Real-vaqt muhim (masalan, narxlar, qoldiq) |
| Stale-while-revalidate | Keshdan darhol ko'rsatib, fonda yangilab, natija kelganda UI'ni yangilaydi | Ko'p zamonaviy ilovalar uchun eng yaxshi murosa |
Konflikt hal qilish#
Agar foydalanuvchi offline holatda o'zgartirgan ma'lumot serverda ham o'zgargan bo'lsa (masalan, boshqa qurilmadan), konfliktni hal qilish strategiyasi kerak: last-write-wins (oddiy, lekin ma'lumot yo'qotilishi mumkin), server-wins (mahalliy o'zgarish e'tiborsiz qoldiriladi), yoki merge/foydalanuvchiga ko'rsatish (murakkab, lekin eng to'g'ri). Tanlov domenga bog'liq — masalan, chat ilovasida xabarlar odatda konflikt qilmaydi (append-only), lekin umumiy hujjat tahrirlashda murakkab merge kerak bo'lishi mumkin.
5. Pagination#
Katta ro'yxatlarni bir martada yuklash o'rniga, sahifalab (paginated) yuklash standart amaliyot:
Future<List<Item>> fetchItems({required int page, int limit = 20}) async {
final response = await dio.get('/items', queryParameters: {
'page': page,
'limit': limit,
});
return (response.data['items'] as List)
.map((json) => Item.fromJson(json))
.toList();
}ListView.builder bilan ScrollController yordamida oxiriga yaqinlashganda
keyingi sahifa avtomatik yuklanadi ("infinite scroll"):
_scrollController.addListener(() {
if (_scrollController.position.pixels >
_scrollController.position.maxScrollExtent - 200) {
_loadNextPage();
}
});6. Tarmoq holatini UI'da to'g'ri ko'rsatish#
Sekin yoki uzilgan tarmoqda ilova "muzlab qolgandek" ko'rinmasligi kerak. Amaliy qoidalar:
- Har bir tarmoq chaqiruvi uchta holatga ega bo'lishi kerak: yuklanmoqda,
muvaffaqiyatli, xato — va UI har uchalasini aniq ko'rsatishi kerak
(
AsyncValueyoki shunga o'xshash tur bilan, 7-bobda ko'rilgan). - Xato holatida foydalanuvchiga qayta urinish tugmasi berish, faqat xom xato matnini ko'rsatmaslik.
- Timeout belgilash — cheksiz kutish o'rniga (
dio'daconnectTimeout,receiveTimeout), belgilangan vaqtdan keyin foydalanuvchiga signal berish. - Optimistic UI — foydalanuvchi amali (masalan, "layk bosish") darhol UI'da aks etadi, server javobini kutmasdan; agar server xato qaytarsa, holat orqaga qaytariladi va foydalanuvchiga xabar beriladi.
Ko'p uchraydigan xato javoblar#
| Savol/holat | Junior darajadagi javob | Senior darajadagi javob |
|---|---|---|
| "Nega dio, http emas?" | "dio zamonaviyroq" | Interceptor, CancelToken, progress kuzatuvi kabi aniq imkoniyatlarni va production ehtiyojlarini sanaydi |
| Tarmoq xatosi UI'da xom stack trace sifatida chiqadi | E'tibor bermaydi | Xatolarni domain darajasida (AppError) modellashtirib, foydalanuvchiga tushunarli xabar ko'rsatishni tushuntiradi |
| Offline holatda ilova bo'sh ekran ko'rsatadi | "Internet yo'q, tabiiy" deydi | Local-first arxitektura bilan mahalliy keshdan ma'lumot ko'rsatish kerakligini tushuntiradi |
| Tez skroll qilganda bir xil sahifa bir necha marta so'raladi | Muammoni sezmaydi | isLoading flag va debounce/guard mexanizmini tushuntiradi |
Tez-tez so'raladigan intervyu savollari#
Savol: dio'da interceptor'lar qanday muammoni hal qiladi?
Javob: Interceptor'lar so'rov yuborilishidan oldin yoki javob kelganidan keyin markazlashgan tarzda ishlaydigan kod yozish imkonini beradi — masalan, har bir so'rovga avtomatik autentifikatsiya tokenini qo'shish, 401 xatosida avtomatik logout qilish, yoki barcha so'rov/javoblarni loglash. Bu mantiqni har bir API chaqiruvida qo'lda takrorlash o'rniga bir joyda saqlaydi, bu esa kodni DRY va xatoga kamroq moyil qiladi.
Savol: Nega ba'zi jamoalar oddiy try/catch o'rniga Result/Either
turini afzal ko'radi?
Javob: try/catch bilan xato chaqiruvchi tomonda "unutilishi" mumkin —
kompilyator buni majburlamaydi. Result<T> (yoki Either) sealed class
sifatida xatoni funksiya imzosining bir qismiga aylantiradi — chaqiruvchi
switch/pattern matching orqali muvaffaqiyat va xato holatlarini alohida
qayta ishlashga majbur bo'ladi, va Dart 3'ning exhaustiveness
tekshiruvi tufayli yangi xato turi qo'shilganda buni unutgan joylar
compile vaqtida ko'rinadi.
Savol: CancelToken'ni qachon va nega ishlatish kerak?
Javob: Foydalanuvchi hali javob kelmagan so'rov yuborilgan ekrandan chiqib ketsa yoki qidiruv maydoniga tez-tez yozayotganda har bir harf uchun yangi so'rov ketsa, eski, endi keraksiz bo'lgan so'rovlarni bekor qilish kerak. Aks holda: (1) resurs behuda sarflanadi, (2) eskirgan javob keyinroq kelib, yangi holatni noto'g'ri qiymat bilan ustidan yozib qo'yishi mumkin (race condition) — masalan, "flutter" so'zi bo'yicha natija "flut" so'zi natijasidan keyin kelib, ekranni noto'g'ri ma'lumot bilan qoldirishi mumkin.
Savol: Offline-first arxitekturada UI qayerdan ma'lumot o'qishi kerak — to'g'ridan-to'g'ri tarmoqdan yoki mahalliy bazadan? Nega?
Javob: Mahalliy bazadan. UI mahalliy bazani "tomosha qiladi" (reaktiv query orqali), tarmoq esa faqat fonda mahalliy bazani yangilaydigan bitta manba sifatida ishlaydi. Bu yondashuvning foydasi: tarmoq bo'lmasa ham UI ishlashda davom etadi (oxirgi ma'lum ma'lumot bilan), va tarmoq qaytganda ma'lumot avtomatik yangilanadi — UI kodida "tarmoq bormi-yo'qmi" degan shartlarni alohida yozish shart bo'lmaydi.
Savol: json_serializable yoki freezed ishlatishning qanday
narxi bor, va qachon oddiy qo'lda fromJson yetarli bo'ladi?
Javob: Kod generatsiya build_runner orqali qo'shimcha build vaqtini
talab qiladi, va loyihaga yana bir bog'liqlik qatlamini qo'shadi — kichik
skript yoki bir martalik prototipda bu ortiqcha bo'lishi mumkin. Model
soni o'sib, ular ko'p maydonga ega bo'lganda va copyWith/immutability/
union type kabi imkoniyatlar kerak bo'lganda, codegen'ning xatoni erta
(compile vaqtida) ushlash foydasi qo'lda yozishning qo'shimcha
tezligidan ustun keladi.
Savol: Pagination'da "bir vaqtning o'zida bir nechta so'rov ketishi" muammosini qanday oldini olasiz?
Javob: Yuklash holatini kuzatuvchi flag (masalan, bool _isLoadingMore)
saqlanadi — scroll listener chaqirilganda, agar _isLoadingMore == true
bo'lsa, yangi so'rov yuborilmaydi. So'rov tugagach flag false'ga
qaytariladi. Bundan tashqari, agar oxirgi sahifa allaqachon bo'sh natija
qaytargan bo'lsa (hasMore == false), keyingi urinishlar butunlay
o'tkazib yuboriladi.
Savol: "Optimistic UI" nima va uni qachon ishlatish xavfli bo'lishi mumkin?
Javob: Optimistic UI — foydalanuvchi amalini (masalan, layk bosish) server javobini kutmasdan darhol UI'da aks ettirish, so'ng fonda serverga so'rov yuborish. Bu ilovani sezilarli tezroq his qildiradi. Xavfi — agar server keyinroq xato qaytarsa, UI'ni avvalgi holatga qaytarish (rollback) va foydalanuvchiga nima uchun o'zgarish bekor qilinganini tushuntirish kerak; bu logikani to'g'ri yozmaslik "UI bir narsa ko'rsatadi, server boshqa narsa saqlaydi" degan nomuvofiqlikka olib keladi. Shuning uchun pulni yoki muhim tranzaksiyani o'z ichiga olgan amallarda optimistic UI odatda ishlatilmaydi yoki juda ehtiyotkorlik bilan qo'llaniladi.
Savol: REST API'dan tashqari, qanday holatlarda GraphQL yoki WebSocket'ni tanlagan bo'lardingiz?
Javob: GraphQL — client turli ekranlar uchun turli, ba'zan ichma-ich bog'liq ma'lumotni bitta so'rovda, aynan kerakli maydonlarni olishi kerak bo'lganda (over-fetching/under-fetching muammosini kamaytiradi) foydali — ayniqsa backend jamoasi turli mobil/veb klientlarga xizmat qilayotganda. WebSocket (yoki server-sent events) esa real-vaqt, davomiy oqim kerak bo'lgan holatlarda (chat, jonli narxlar, bildirishnomalar) — REST'ning so'rov-javob modeli bunday doimiy yangilanishlar uchun mos emas, chunki har safar poll qilish kerak bo'lardi.
Result turi bilan xatoni to'liq oqimda modellashtir
Amaliy topshiriq: tarmoq chaqiruvini o'rab turgan fetchUser(int id) -> Future<Result<User>> funksiyasini yozing. DioException'ning turli
holatlarini (timeout, 404, 500, tarmoq yo'qligi) alohida AppError
turlariga aylantiring. Keyin bu natijani iste'mol qiluvchi widget'da
switch orqali barcha holatlarni (shu jumladan har bir xato turi uchun
alohida xabar) qayta ishlang.
Maslahatni ko'rsatish
fetchUser funksiyasini Result<User> qaytaradigan qilib yozing, DioException'ni tutib domenga xos AppError'ga aylantiring, va UI qatlamida switch orqali uchta holatni (loading avvaldan, Success, Failure) to'liq qamrab oling.