Azimjon Abdurasulov

Flutter 10: Professional REST API — Dio, Interceptors, Token Refresh va Clean Repository

💙 Flutter — Mobile (iOS, Android) & Desktop (macOS, Windows)

Flutter 10: Professional REST API — Dio, Interceptors, Token Refresh va Clean Repository

Holat: 2026-yil. Zamonaviy Flutter ilovalarida eng muhim ko'nikmalardan biri — bu masofaviy server (REST API) bilan barqaror, xavfsiz va uzluksiz ishlay olishdir. Tarmoq yo'qolishi, sekin internet, 401 Unauthorized holatlari va JSON parslashdagi xatolar ilovani buzmasligi kerak. Ushbu darsda Clean Architecture asosida Dio mijozi, avtomatik JWT Token Refresh interceptorlari, tiplangan modellar va oflayn keshlash mexanizmini bosqichma-bosqich quramiz.


Mundarija

  1. Tarmoq arxitekturasi: Clean Architecture qatlamlari
  2. Dio vs http: Nega Dio standart hisoblanadi?
  3. Result Pattern: Xatoliklarni xavfsiz boshqarish
  4. Dio instansiyasi va BaseOptions sozlamalari
  5. JWT Auth & Refresh Token Interceptor (Eng muhim mexanizm)
  6. Type-safe Model va JSON Serialization
  7. Repository va DataSource qatlami
  8. Riverpod 3 bilan UI ga ulash va holatni boshqarish
  9. Oflayn kesh: SharedPreferences va Hive bilan uzluksizlik
  10. Xulosa va Xavfsizlik qoidalari
  11. Rasmiy Manbalar

1. Tarmoq arxitekturasi: Clean Architecture qatlamlari {#clean-architecture}

To'g'ridan-to'g'ri Widget ichida http.get() chaqirish — bu loyihani o'limga olib boruvchi yo'ldir. Clean Architecture tizimida tarmoq oqimi quyidagicha tashkil etiladi:

┌────────────────────────────────────────────────────────────────────────┐
│                          PRESENTATION LAYER                            │
│  UI Widgets (Screens)  ◄───►  State Notifiers (Riverpod / BLoC)        │
└───────────────────────────────────┬────────────────────────────────────┘
                                    │ Chaqiradi (Uses)
┌───────────────────────────────────▼────────────────────────────────────┐
│                             DOMAIN LAYER                               │
│  Entities (Pok Dart obyektlari)  ◄──►  Repository Interface (Shartnoma)│
│  Failures / Results (Xatoliklar klassi)                                │
└───────────────────────────────────▲────────────────────────────────────┘
                                    │ Amalga oshiradi (Implements)
┌───────────────────────────────────┴────────────────────────────────────┐
│                              DATA LAYER                                │
│  Repositories Impl  ──►  RemoteDataSource (Dio API Client)             │
│                     ──►  LocalDataSource (Kesh / Hive / SQLite)        │
│  DTOs / Models (fromJson / toJson)                                     │
└────────────────────────────────────────────────────────────────────────┘

Asosiy qoida: Domain qatlami hech qanday tashqi paketga (hatto Flutter'ga ham) bog'liq bo'lmasligi kerak — sof Dart kodidan iborat bo'ladi.


2. Dio vs http: Nega Dio standart hisoblanadi? {#dio-vs-http}

Standart http paketi juda yengil, ammo korporativ ilovalar uchun yetarli emas:

Xususiyathttp paketidio paketi (^5.8+)
Interceptors (Filtrlar)Yo'q (qo'lda o'rab chiqish kerak)O'rnatilgan (Request, Response, Error)
Global Base URL & HeadersHar bir so'rovda qo'lda uzatiladiBaseOptions orqali markaziy o'rnatiladi
Avtomatik JSON parslashjsonDecode(response.body) shartDio response.data da avtomatik Map qaytaradi
Fayl yuklash / yuklab olishMurakkab MultipartRequestProgress tinglovchi (onSendProgress) bilan oson
So'rovni bekor qilishMurakkabCancelToken orqali bir lahzada bekor qilinadi
Timeout nazoratiQiyinconnectTimeout, receiveTimeout mavjud

Kerakli paketlarni pubspec.yaml ga qo'shamiz:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.8.0+1
  flutter_riverpod: ^2.6.1
  shared_preferences: ^2.5.2

3. Result Pattern: Xatoliklarni xavfsiz boshqarish {#result-pattern}

Dart'da doimiy try-catch yozish o'rniga, natijani **Result<T>** muhrlangan klassi (Sealed Class) orqali ifodalash xavfsizdir:

sealed class Result<T> {
  const Result();
}

final class Success<T> extends Result<T> {
  final T data;
  const Success(this.data);
}

final class Failure<T> extends Result<T> {
  final String message;
  final int? statusCode;
  const Failure(this.message, {this.statusCode});
}

Bu pattern yordamida UI qismida switch orqali barcha holatlarni compile-time xavfsizlik bilan tekshirish mumkin:

switch (result) {
  case Success(:final data):
    showData(data);
  case Failure(:final message):
    showErrorSnackbar(message);
}

4. Dio instansiyasi va BaseOptions sozlamalari {#dio-setup}

Barcha API so'rovlari uchun yagona ApiClient sinfini yaratamiz:

import 'package:dio/dio.dart';

class ApiClient {
  static const String baseUrl = 'https://api.azimjondev.uz/v1';

  late final Dio dio;

  ApiClient({required String? initialToken}) {
    dio = Dio(
      BaseOptions(
        baseUrl: baseUrl,
        connectTimeout: const Duration(seconds: 15),
        receiveTimeout: const Duration(seconds: 15),
        sendTimeout: const Duration(seconds: 15),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          if (initialToken != null) 'Authorization': 'Bearer $initialToken',
        },
      ),
    );

    // Logging interceptor (Faqat debug paytida)
    dio.interceptors.add(
      LogInterceptor(
        requestHeader: false,
        requestBody: true,
        responseBody: true,
        responseHeader: false,
        error: true,
      ),
    );
  }
}

5. JWT Auth & Refresh Token Interceptor (Eng muhim mexanizm) {#refresh-token}

Foydalanuvchi ilovadan foydalanayotganda 15 daqiqalik Access Token muddati tugaydi va server 401 Unauthorized xatosini qaytaradi. To'g'ri yechim: Foydalanuvchini login sahifasiga chiqarib yubormasdan, orqa fonda Refresh Token orqali yangi Access Token olish va to'xtab qolgan so'rovni avtomatik qayta yuborish (Retry):

import 'package:dio/dio.dart';

class AuthInterceptor extends QueuedInterceptor {
  final Dio dio;
  final Future<String?> Function() getAccessToken;
  final Future<String?> Function() getRefreshToken;
  final Future<void> Function(String newAccess, String newRefresh) onTokensRefreshed;
  final VoidCallback onSessionExpired;

  AuthInterceptor({
    required this.dio,
    required this.getAccessToken,
    required this.getRefreshToken,
    required this.onTokensRefreshed,
    required this.onSessionExpired,
  });

  // 1. Har bir so'rovga oxirgi yangi tokenni qo'shamiz
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await getAccessToken();
    if (token != null && !options.headers.containsKey('Authorization')) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  // 2. 401 xatosi yuz berganda avtomatik refresh qilamiz
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      final refreshToken = await getRefreshToken();

      if (refreshToken == null) {
        onSessionExpired();
        return handler.reject(err);
      }

      try {
        // Asosiy dio emas, alohida toza dio orqali so'raymiz (tsiklni oldini olish uchun)
        final refreshDio = Dio(BaseOptions(baseUrl: dio.options.baseUrl));
        final response = await refreshDio.post('/auth/refresh', data: {
          'refresh_token': refreshToken,
        });

        if (response.statusCode == 200) {
          final newAccessToken = response.data['access_token'] as String;
          final newRefreshToken = response.data['refresh_token'] as String;

          // Yangi tokenlarni mahalliy xotiraga saqlaymiz
          await onTokensRefreshed(newAccessToken, newRefreshToken);

          // Xatoga uchragan so'rovning headerini yangilab, qayta yuboramiz
          final requestOptions = err.requestOptions;
          requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';

          final retryResponse = await dio.fetch(requestOptions);
          return handler.resolve(retryResponse);
        }
      } catch (refreshError) {
        // Refresh token ham eskirgan bo'lsa, foydalanuvchini logindan chiqaramiz
        onSessionExpired();
        return handler.reject(err);
      }
    }

    // Boshqa turdagi xatoliklarni davom ettiramiz
    handler.next(err);
  }
}

E'tibor bering: QueuedInterceptor dan foydalanildi. Bu bir vaqtning o'zida bir nechta so'rov 401 olganda serverga bir nechta refresh so'rov ketishining oldini oladi va navbat bilan hal qiladi!


6. Type-safe Model va JSON Serialization {#json-models}

Serverdan keladigan ma'lumotlar uchun mustahkam model tuzamiz:

class ProductModel {
  final int id;
  final String title;
  final double price;
  final String description;
  final String imageUrl;

  const ProductModel({
    required this.id,
    required this.title,
    required this.price,
    required this.description,
    required this.imageUrl,
  });

  factory ProductModel.fromJson(Map<String, dynamic> json) {
    return ProductModel(
      id: json['id'] as int,
      title: json['title'] as String? ?? 'Nomsiz mahsulot',
      price: (json['price'] as num?)?.toDouble() ?? 0.0,
      description: json['description'] as String? ?? '',
      imageUrl: json['image_url'] as String? ?? 'https://via.placeholder.com/150',
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'price': price,
    'description': description,
    'image_url': imageUrl,
  };
}

7. Repository va DataSource qatlami {#repository-pattern}

Clean Architecture qoidasiga binoan, avval interfeys yoziladi:

// Domain Layer: Interface
abstract interface class ProductRepository {
  Future<Result<List<ProductModel>>> getProducts();
  Future<Result<ProductModel>> getProductById(int id);
}

// Data Layer: Implementation
class ProductRepositoryImpl implements ProductRepository {
  final Dio dio;

  const ProductRepositoryImpl(this.dio);

  @override
  Future<Result<List<ProductModel>>> getProducts() async {
    try {
      final response = await dio.get('/products');

      if (response.statusCode == 200) {
        final list = (response.data as List)
            .map((e) => ProductModel.fromJson(e as Map<String, dynamic>))
            .toList();
        return Success(list);
      }

      return Failure('Server xatosi: ${response.statusCode}', statusCode: response.statusCode);
    } on DioException catch (e) {
      return Failure(_handleDioError(e), statusCode: e.response?.statusCode);
    } catch (e) {
      return Failure('Kutilmagan xatolik: $e');
    }
  }

  @override
  Future<Result<ProductModel>> getProductById(int id) async {
    try {
      final response = await dio.get('/products/$id');
      return Success(ProductModel.fromJson(response.data));
    } on DioException catch (e) {
      return Failure(_handleDioError(e));
    }
  }

  String _handleDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return 'Server bilan aloqa vaqti tugadi (Timeout). Internetni tekshiring.';
      case DioExceptionType.badResponse:
        final code = error.response?.statusCode;
        if (code == 404) return 'Ma\'lumot topilmadi (404).';
        if (code == 500) return 'Serverda ichki xatolik (500).';
        return 'Xato javob: $code';
      case DioExceptionType.connectionError:
        return 'Internet aloqasi mavjud emas.';
      default:
        return 'Tarmoq xatoligi yuz berdi.';
    }
  }
}

8. Riverpod 3 bilan UI ga ulash va holatni boshqarish {#ui-integration}

Repository'ni Riverpod provayderiga joylaymiz va UI uchun AsyncNotifier yaratamiz:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. Dio va Repository provayderlari
final dioProvider = Provider<Dio>((ref) {
  return ApiClient(initialToken: null).dio;
});

final productRepositoryProvider = Provider<ProductRepository>((ref) {
  return ProductRepositoryImpl(ref.watch(dioProvider));
});

// 2. Mahsulotlar holatini boshqaruvchi FutureProvider
final productsProvider = FutureProvider<List<ProductModel>>((ref) async {
  final repo = ref.watch(productRepositoryProvider);
  final result = await repo.getProducts();

  return switch (result) {
    Success(:final data) => data,
    Failure(:final message) => throw Exception(message),
  };
});

UI qismida ekranga chiqarish:

class ProductListScreen extends ConsumerWidget {
  const ProductListScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productsProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Mahsulotlar katalogi'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => ref.refresh(productsProvider),
          ),
        ],
      ),
      body: productsAsync.when(
        loading: () => const Center(child: CircularProgressIndicator.adaptive()),
        error: (err, stack) => Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.wifi_off_rounded, size: 64, color: Colors.redAccent),
                const SizedBox(height: 16),
                Text(err.toString().replaceAll('Exception: ', ''), textAlign: TextAlign.center),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () => ref.refresh(productsProvider),
                  child: const Text('Qayta urinish'),
                ),
              ],
            ),
          ),
        ),
        data: (products) => ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final item = products[index];
            return ListTile(
              leading: ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(item.imageUrl, width: 50, height: 50, fit: BoxFit.cover),
              ),
              title: Text(item.title),
              subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
              trailing: const Icon(Icons.chevron_right),
            );
          },
        ),
      ),
    );
  }
}

9. Oflayn kesh: SharedPreferences va Hive bilan uzluksizlik {#offline-cache}

Tarmoq bo'lmaganda ilova bo'sh ekran ko'rsatmasligi uchun kesh qatlami (LocalDataSource) qo'shiladi:

  1. So'rov yuboriladi.
  2. Agar tarmoq bor bo'lsa: ma'lumot serverdan olinadi, mahalliy xotiraga saqlanadi va ekranga beriladi.
  3. Agar tarmoq yo'q bo'lsa: mahalliy xotiradagi oxirgi ma'lumot olinadi va Snackbar orqali "Oflayn rejim" xabarnomasi bilan taqdim etiladi.

10. Xulosa va Xavfsizlik qoidalari {#xulosa}

  1. Tokenlarni hech qachon ochiq kodda saqlamang: API sirlari va kalitlar uchun flutter_dotenv yoki compile-time --dart-define dan foydalaning.
  2. Katta JSON larni Isolate orqali parslash: 1 MB dan katta bo'lgan JSON ro'yxatlarini asosiy oqimda parslash kadr tushishiga olib keladi. Buning uchun compute(parseJsonFunction, responseBody) dan foydalaning.
  3. Cancellation Token: Agar foydalanuvchi sahifadan chiqib ketsa, cancelToken.cancel() orqali behuda yuklanayotgan so'rovni to'xtating.

11. Rasmiy Manbalar {#manbalar}

Markdown manba

iOS Lessons