> ## Content Index
> Fetch the complete content index at: https://arkar.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Day 28: API Client with Dio
- URL: https://arkar.dev/day-28-api-client-with-dio/
- Published: 2026-11-09T06:06:47.000Z
- Updated: 2026-11-09T06:06:47.000Z
- Description: Call the Pexels API with Dio: BaseOptions, timeouts and the Authorization header, safe logging, turning DioException into readable errors, the Result pattern from Day 9, and a repository that keeps networking out of your UI.
- Author: Arkar Min Tun
- Tags: 100 days of flutter, flutter

မနေ့က Day 27 မှာ Pexels API key ကို `.env` ထဲ ထည့်ပြီး JSON ကို ပြောင်းပေးမယ့် model တွေ ရေးခဲ့ပြီးပါပြီ။ ဒီနေ့မှာတော့ Pexels API ကို တကယ် ခေါ်မယ့် network layer ကို တည်ဆောက်ကြပါမယ်။ ဒီအတွက် Flutter community မှာ အသုံးအများဆုံး HTTP client တွေထဲက တခုဖြစ်တဲ့ **Dio** ကို သုံးသွားမှာ ဖြစ်ပါတယ်။

ဒီနေ့ ပြီးသွားရင် project ထဲမှာ ဒီလို layer ၃ ခု ရှိလာပါမယ်။

```
UI (screens, widgets)
   │  Day 29 မှာ ချိတ်ပါမယ်
   ▼
PhotoRepository   ← "curated photos ပေးပါ" လို့ပဲ ပြောရုံ
   │
   ▼
ApiClient (Dio)   ← URL, header, timeout, error handling
   │
   ▼
Pexels API
```

UI ကနေ Dio ကို တိုက်ရိုက် မခေါ်ဘဲ layer ခွဲထားရတဲ့ အကြောင်းရင်းကတော့ screen တွေက "ပုံ ၂၀ ပေးပါ" လို့ပဲ သိဖို့ လိုပြီး URL ဘယ်လိုဆောက်လဲ၊ header ဘာထည့်လဲ၊ error ကို ဘယ်လို ကိုင်တွယ်လဲဆိုတာ မသိဖို့ လိုလို့ပါ။ ဒါမှ API ပြောင်းရင်ဖြစ်ဖြစ်၊ test လုပ်ချင်ရင်ဖြစ်ဖြစ် တနေရာတည်းမှာ ပြင်ရုံပါပဲ။

## ဘာလို့ Dio လဲ?

Dart team က ထုတ်တဲ့ `http` package နဲ့လဲ API ခေါ်လို့ ရပါတယ်။ Dio ကိုတော့ အောက်က feature တွေ built-in ပါလို့ ရွေးတာ ဖြစ်ပါတယ်။

- **`BaseOptions`** \- base URL, header, timeout တွေကို တခါတည်း သတ်မှတ်ထားလို့ရ
- **Interceptors** \- request/response တိုင်းကို ကြားထဲက ဝင်ကြည့်၊ ပြင်လို့ရ (logging, token ထည့်တာ, retry)
- **JSON decode** \- response ကို JSON အဖြစ် အလိုအလျောက် ပြောင်းပေး
- `**DioException**` \- error အမျိုးအစား (timeout, no internet, 4xx/5xx) ကို `type` နဲ့ ခွဲသိနိုင်

Interceptor တွေ, retry တွေ အပါအဝင် Dio ရဲ့ advanced အပိုင်းတွေကိုတော့ Day 44 မှာ ထပ်ပြီး အသေးစိတ် လေ့လာမှာ ဖြစ်ပါတယ်။ ဒီနေ့တော့ Pixly အတွက် လိုတာလောက်ကိုပဲ ကြည့်ပါမယ်။

```bash
fvm flutter pub add dio
```

## ApiClient

```dart
// lib/core/api/api_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:pixly/config/env.dart';
import 'package:pixly/core/api/api_exception.dart';

/// Thin wrapper around Dio configured for the Pexels API.
class ApiClient {
  ApiClient({Dio? dio, String? apiKey})
    : _dio = dio ?? Dio(_options(apiKey ?? Env.pexelsApiKey)) {
    if (dio == null && kDebugMode) {
      _dio.interceptors.add(
        LogInterceptor(
          requestHeader: false, // never print the API key
          responseHeader: false,
          responseBody: false,
        ),
      );
    }
  }

  static const String baseUrl = 'https://api.pexels.com/v1';

  final Dio _dio;

  static BaseOptions _options(String apiKey) {
    return BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
      headers: {'Authorization': apiKey},
    );
  }

  /// GET [path] and return the decoded JSON object.
  /// Throws [ApiException] when the request fails.
  Future<Map<String, dynamic>> get(
    String path, {
    Map<String, dynamic>? queryParameters,
  }) async {
    try {
      final response = await _dio.get<Map<String, dynamic>>(
        path,
        queryParameters: queryParameters,
      );
      return response.data ?? {};
    } on DioException catch (e) {
      throw ApiException.fromDio(e);
    }
  }
}
```

တပိုင်းချင်း ကြည့်ရအောင်ပါ။

### BaseOptions

- `**baseUrl**` \- request တိုင်းရဲ့ ရှေ့က URL ပါ။ ဒါကြောင့် နောက်ပိုင်း `'/curated'` လို့ပဲ ရေးရင် `https://api.pexels.com/v1/curated` ကို ခေါ်သွားမှာပါ။
- **`connectTimeout`** \- server နဲ့ ချိတ်ဆက်တာ ၁၀ စက္ကန့်ထက် ကြာရင် error အဖြစ် ရပ်လိုက်ပါတယ်။
- **`receiveTimeout`** \- response data ကို စောင့်တဲ့အခါ (data အပိုင်း တခုနဲ့ တခုကြား အပါအဝင်) ၁၀ စက္ကန့်ထက် ဘာမှ မရောက်လာရင် error အဖြစ် ရပ်လိုက်ပါတယ်။ Request တခုလုံး ၁၀ စက္ကန့်အတွင်း ပြီးရမယ်လို့ ဆိုလိုတာ မဟုတ်ပါဘူး။ Data တွေ ဆက်တိုက် ဝင်နေသရွေ့ download က ၁၀ စက္ကန့်ထက် ကြာနိုင်ပါတယ်။

Timeout မထားရင် internet မကောင်းတဲ့အခါ user က loading ကို အကြာကြီး စောင့်နေရနိုင်ပါတယ်။ - `**headers: {'Authorization': apiKey}**` \- Day 27 မှာ ပြောခဲ့သလို Pexels က key ကို `Authorization` header ထဲမှာ တိုက်ရိုက် ထည့်ခိုင်းပါတယ်။ `Bearer` မခံရပါဘူး။ `BaseOptions` မှာ ထည့်ထားတဲ့အတွက် request တိုင်းမှာ အလိုအလျောက် ပါသွားမှာပါ။

### Constructor မှာ Dio ကို လက်ခံထားတာ

`ApiClient({Dio? dio, String? apiKey})` ဆိုပြီး Dio ကို optional parameter အနေနဲ့ လက်ခံထားပါတယ်။ ပုံမှန်သုံးတဲ့အခါ ဘာမှ မပေးရင် အပေါ်က option တွေနဲ့ Dio အသစ် ဆောက်ပေးပါတယ်။ Test လုပ်တဲ့အခါကျတော့ internet မလိုတဲ့ fake Dio ကို ထည့်ပေးလို့ ရသွားပါတယ်။ ဒီလို dependency ကို အပြင်ကနေ ထည့်ပေးတဲ့ ပုံစံကို **dependency injection** လို့ ခေါ်ပါတယ်။

### LogInterceptor - သတိထားစရာ

Development လုပ်တဲ့အခါ request တွေ ဘယ်လို သွားလဲ console မှာ မြင်ရရင် debug လုပ်ရ လွယ်ပါတယ်။ Dio မှာ ပါပြီးသား `LogInterceptor` ကို `kDebugMode` (debug build မှာပဲ `true` ဖြစ်တဲ့ constant) မှာပဲ ထည့်ထားပါတယ်။

သတိထားရမှာက `LogInterceptor` က default အနေနဲ့ **request header တွေကိုပါ print ထုတ်ပါတယ်**။ ဒါဆို `Authorization` header ထဲက API key ပါ console log ထဲ ရောက်သွားမှာပါ။ ဒါကြောင့် `requestHeader: false` လို့ ပိတ်ထားတာ ဖြစ်ပါတယ်။ Log တွေကို screenshot ရိုက်ပြီး group တွေမှာ မေးခွန်းမေးတဲ့အခါ key ပါ မပါသွားအောင် ဒီလို အသေးစိတ်လေးတွေက အရေးကြီးပါတယ်။

### get() method

- `_dio.get<Map<String, dynamic>>` \- response ကို JSON object (`Map`) အဖြစ် ရမယ်လို့ Dio ကို ပြောထားတာပါ။
- `queryParameters` \- URL ရဲ့ `?page=2&per_page=20` အပိုင်းကို `Map` အနေနဲ့ ပေးလို့ ရပါတယ်။ Dio က **URL encoding** ကိုပါ လုပ်ပေးတဲ့အတွက် `coffee & cake` လို space တွေ, `&` တွေ ပါတဲ့ search keyword ကိုလဲ မှန်မှန်ကန်ကန် ပို့ပေးပါတယ်။ URL string ကို `'/search?query=$query'` လို ကိုယ်တိုင် ဆက်ရေးရင်တော့ `&` ပါတဲ့ keyword မှာ URL ပျက်သွားမှာ ဖြစ်ပါတယ်။
- `on DioException catch (e)` \- Dio က error တက်ရင် `DioException` throw လုပ်ပါတယ်။ ဒါကို UI က နားလည်လွယ်တဲ့ `ApiException` အဖြစ် ပြောင်းပြီး ပြန် throw လုပ်ပါတယ်။

## ApiException - Error တွေကို ဘာသာပြန်ခြင်း

`DioException` ထဲမှာ developer တွေအတွက် အသေးစိတ် message တွေ ပါပေမယ့် user ကို တိုက်ရိုက် ပြဖို့ မသင့်ပါဘူး။ Day 14 မှာ လေ့လာခဲ့တဲ့ custom exception ကို သုံးပြီး user ကို ပြလို့ရတဲ့ message အဖြစ် ပြောင်းပါမယ်။

```dart
// lib/core/api/api_exception.dart
import 'package:dio/dio.dart';

/// A readable error the UI can show, created from a [DioException].
class ApiException implements Exception {
  const ApiException(this.message, {this.statusCode});

  final String message;
  final int? statusCode;

  factory ApiException.fromDio(DioException e) {
    final status = e.response?.statusCode;
    return switch (e.type) {
      .connectionTimeout || .sendTimeout || .receiveTimeout =>
        const ApiException('The connection timed out. Please try again.'),
      .connectionError => const ApiException(
        'Unable to connect. Check your network and try again.',
      ),
      .cancel => const ApiException('The request was cancelled.'),
      .badResponse => switch (status) {
        401 => ApiException(
          'Invalid API key. Check PEXELS_API_KEY in your .env file.',
          statusCode: status,
        ),
        429 => ApiException(
          'Too many requests. Please wait a moment and try again.',
          statusCode: status,
        ),
        _ => ApiException('Server error ($status).', statusCode: status),
      },
      _ => const ApiException('Something went wrong. Please try again.'),
    };
  }

  @override
  String toString() => 'ApiException($statusCode): $message';
}
```

ဒီမှာ Day 4 က switch expression နဲ့ Dart 3.10 dot shorthand ကို ပေါင်းသုံးထားပါတယ်။ `e.type` က `DioExceptionType` enum ဖြစ်တဲ့အတွက် `DioExceptionType.connectionTimeout` လို့ အရှည်ကြီး ရေးစရာ မလိုဘဲ `.connectionTimeout` လို့ပဲ ရေးလို့ ရပါတယ်။ `||` နဲ့ pattern တွေကို ပေါင်းပြီး timeout အမျိုးအစား ၃ ခုလုံးကို message တခုတည်းနဲ့ ကိုင်တွယ်ထားပါတယ်။

| DioExceptionType                               | ဘာဖြစ်တာလဲ                                                                                       |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| connectionTimeout, sendTimeout, receiveTimeout | Timeout ထက် ကြာသွား                                                                              |
| connectionError                                | Server ကို ချိတ်လို့ မရ (internet မရှိတာ၊ domain ရှာမတွေ့တာ၊ server က connection ငြင်းတာ စတာတွေ) |
| badResponse                                    | Server က ပြန်ဖြေပေမယ့် status code က error (4xx, 5xx)                                            |
| cancel                                         | Request ကို ကိုယ်တိုင် cancel လုပ်လိုက်                                                          |
| badCertificate, unknown                        | SSL ပြဿနာ နဲ့ အခြား error တွေ (\_ နဲ့ ဖမ်းထား)                                                   |

`badResponse` ထဲမှာ Pexels အတွက် အရေးကြီးတဲ့ status code ၂ ခုကို သီးသန့် ကိုင်တွယ်ထားပါတယ်။

- **401** \- API key မှားနေတာ ဒါမှမဟုတ် မပါတာ
- **429** \- Day 25 မှာ ပြောခဲ့တဲ့ rate limit (default အနေနဲ့ တနာရီ request 200 နဲ့ တလ request 20,000) တခုခု ကျော်သွားတာ

## Result Pattern

Repository က error တက်ရင် exception throw လုပ်မယ့်အစား Day 9 မှာ လေ့လာခဲ့တဲ့ **Result pattern** နဲ့ ပြန်ပေးပါမယ်။

```dart
// lib/core/result.dart
/// Either a value or an error message - the Result pattern from Day 9.
sealed class Result<T> {
  const Result();
}

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

class Failure<T> extends Result<T> {
  const Failure(this.message);
  final String message;
}
```

`sealed` class ဖြစ်တဲ့အတွက် `switch` နဲ့ `Success` case တခုတည်းကိုပဲ ရေးပြီး `Failure` case ကို မေ့ကျန်ခဲ့ရင် compiler က "switch က exhaustive မဖြစ်ဘူး" ဆိုပြီး error ပြပေးပါတယ်။ (`_` wildcard နဲ့ ဖမ်းထားရင်တော့ compiler က မသိနိုင်ပါဘူး။ ဒါကြောင့် case တခုချင်းစီကို သီးသန့် ရေးတာ ပိုကောင်းပါတယ်။) Day 29 မှာ Provider ကနေ ဒီ `Result` ကို ဘယ်လို သုံးလဲ မြင်ရမှာပါ။

## PhotoRepository

```dart
// lib/repositories/photo_repository.dart
import 'package:pixly/core/api/api_client.dart';
import 'package:pixly/core/api/api_exception.dart';
import 'package:pixly/core/result.dart';
import 'package:pixly/models/photos_page.dart';

/// Everything Pixly needs from Pexels, returned as [Result]s.
class PhotoRepository {
  PhotoRepository(this._client);

  final ApiClient _client;

  static const int perPage = 20;

  Future<Result<PhotosPage>> curated({int page = 1}) {
    return _fetch('/curated', {'page': page, 'per_page': perPage});
  }

  Future<Result<PhotosPage>> search(String query, {int page = 1}) {
    return _fetch('/search', {
      'query': query,
      'page': page,
      'per_page': perPage,
    });
  }

  Future<Result<PhotosPage>> _fetch(
    String path,
    Map<String, dynamic> query,
  ) async {
    try {
      final json = await _client.get(path, queryParameters: query);
      return Success(PhotosPage.fromJson(json));
    } on ApiException catch (e) {
      return Failure(e.message);
    } on TypeError {
      // The JSON didn't have the shape our models expect.
      return const Failure('Unexpected response from the server.');
    }
  }
}
```

Repository က Pixly ရဲ့ data လိုအပ်ချက်တွေကို နာမည်ကောင်းကောင်းနဲ့ method တွေအဖြစ် ထုတ်ပေးထားတာ ဖြစ်ပါတယ်။

- `curated(page: 2)` \- Pexels ရဲ့ curated photos page 2
- `search('cat', page: 1)` \- "cat" နဲ့ ရှာထားတဲ့ ရလဒ်

`perPage` ကို 20 ထားပါတယ်။ Pexels ရဲ့ default က page တခုမှာ ၁၅ ပုံ ဖြစ်ပြီး အများဆုံး ၈၀ ထိ တောင်းလို့ ရပါတယ်။ Phone screen တခုစာ grid ကို ပြည့်ပြီး scroll ဆွဲစရာ ကျန်အောင် ၂၀ ယူထားတာ ဖြစ်ပါတယ်။ (Request တခါမှာ ပုံ အများကြီး တောင်းလိုက်ရင် rate limit ကိုလဲ သက်သာစေပါတယ်။)

`_fetch` ထဲမှာတော့ `ApiClient` က ပြန်ပေးတဲ့ JSON ကို Day 27 က `PhotosPage.fromJson` နဲ့ model ပြောင်းပြီး `Success` ထဲ ထည့်ပေးပါတယ်။ Error ၂ မျိုးကို `Failure` အဖြစ် ပြောင်းပေးထားပါတယ်။

- **`ApiException`** \- timeout, connection error, 401, 429 စတဲ့ network error တွေ
- `**TypeError**` \- server က ပြန်ပေးတဲ့ JSON ပုံစံက ကိုယ်မျှော်လင့်ထားတာနဲ့ မတူတဲ့အခါ (ဥပမာ `id` နေရာမှာ number အစား string ပါလာတာ) Day 27 ရဲ့ `as int` လို cast တွေက `TypeError` throw လုပ်ပါတယ်။ ဒါကို မဖမ်းထားရင် app ရဲ့ UI အထိ error ပေါက်သွားမှာ ဖြစ်ပါတယ်။

ဒါကြောင့် Repository ကို သုံးမယ့်သူ (Day 29 က Provider) က ဒီ error တွေအတွက် try/catch ရေးစရာ မလိုတော့ဘဲ `Result` ကို စစ်ရုံပါပဲ။

## Network မလိုဘဲ စမ်းကြည့်ခြင်း

Dio မှာ request တွေကို တကယ် ပို့ပေးတဲ့ အပိုင်းကို **adapter** လို့ ခေါ်ပါတယ်။ Test တွေမှာ adapter ကို internet မသုံးဘဲ Day 27 က sample JSON ကို ပြန်ပေးတဲ့ fake adapter နဲ့ အစားထိုးပြီး ApiClient နဲ့ Repository ကို စမ်းထားပါတယ်။

```dart
// test/api_test.dart (excerpt)
test('search() URL-encodes the query', () async {
  final (repo, adapter) = build(
    (_) => FakeAdapter.json(FakeAdapter.fixture('search_nature')),
  );

  await repo.search('coffee & cake');

  final uri = adapter.requests.single.uri;
  expect(uri.path, '/v1/search');
  expect(uri.queryParameters['query'], 'coffee & cake');
  expect(uri.query, contains('query=coffee+%26+cake'));
});
```

ဒီ test တွေက

- `curated()` က `/v1/curated` ကို `page`, `per_page` နဲ့ ခေါ်လား၊ `Authorization` header မှာ key ကို `Bearer` မပါဘဲ ပို့လား
- search keyword ကို URL encode လုပ်လား
- 401, 429, 500 status တွေ, timeout, connection error တွေကို message မှန်မှန်ကန်ကန် ပြောင်းပေးလား
- JSON ပုံစံ မှားနေရင် crash မဖြစ်ဘဲ `Failure` ပြန်ပေးလား

ဆိုတာတွေကို စစ်ပေးပါတယ်။ `FakeAdapter` နဲ့ `build()` helper ကတော့ project ရဲ့ `test/helpers/fake_adapter.dart` နဲ့ `test/api_test.dart` ထဲမှာ ရှိပါတယ်။ Test ရေးနည်း အသေးစိတ်ကိုတော့ Day 86 မှာ ပြောပါမယ်။

ဒီနေ့ ပြီးသွားတဲ့အခါ project ထဲမှာ

```
lib/
├── core/
│   ├── api/
│   │   ├── api_client.dart
│   │   └── api_exception.dart
│   └── result.dart
├── repositories/
│   └── photo_repository.dart
└── ...
```

ဆိုပြီး network layer တခုလုံး အသင့်ဖြစ်နေပါပြီ။ UI မှာ ဓာတ်ပုံတွေ ပေါ်လာဖို့ကတော့ မနက်ဖြန် Day 29 မှာ Provider နဲ့ ချိတ်ဆက်ပါမယ်။

**Source code:** ဒီနေ့ရဲ့ code အပြည့်အစုံကို [GitHub](https://github.com/arkarmintun1/100-days-of-flutter/tree/main/day28/pixly?ref=arkar.dev) မှာ run ကြည့်လို့ ရပါတယ်။

---

ဒီနေ့ Day 28 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။

- Youtube: [https://www.youtube.com/@arkarmintun](https://www.youtube.com/@arkarmintun?ref=arkar.dev)
- Newsletter: <https://arkar.dev/>
- Discord: [https://discord.gg/3xUJ6k6dkH](https://discord.gg/3xUJ6k6dkH?ref=arkar.dev)