Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
K / /Open search
?Show this help
EscClose search / help
n / NNext / previous section
hGo to landing
dGo to docs
:Command mode — type section name

Press ? or Esc to close

Deep Dives

Network Layer

ApiClient wraps Dio with three interceptors. BaseStore handles all execution, error catching, and cache orchestration. ApiResponse is the universal return type.

ApiClient

lib/config/network/api_client.dart
class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(BaseOptions(
      baseUrl: AppConstants.baseUrl,
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ));

    _dio.interceptors.addAll([
      AuthInterceptor(),    // Bearer token + 401 refresh lock
      LoggingInterceptor(), // Debug mode only
      ErrorInterceptor(),   // Maps errors → GlobalErrorCubit
    ]);
  }

  Future<Response> get(String path, {Map<String, dynamic>? params}) =>
      _dio.get(path, queryParameters: params);

  Future<Response> post(String path, {dynamic data}) =>
      _dio.post(path, data: data);

  Future<Response> patch(String path, {dynamic data}) =>
      _dio.patch(path, data: data);

  Future<Response> delete(String path) =>
      _dio.delete(path);
}

BaseStore

lib/config/network/base_store.dart
abstract class BaseStore {

  // Simple: request → parse → ApiResponse
  Future<ApiResponse<T, void>> execute<T>({
    required Future<Response> Function() request,
    required T Function(dynamic) parser,
    required T empty,
    bool shouldIsolate = false,
  }) async {
    try {
      final response = await request();
      final parsed = await _runParser(parser, response.data, shouldIsolate);
      return ApiResponse.initial(data: parsed).success(data: parsed);
    } on DioException catch (e) {
      return ApiResponse.initial(data: empty).failure(message: mapDioError(e));
    } catch (_) {
      return ApiResponse.initial(data: empty).failure(message: 'Unexpected error');
    }
  }

  // With cache: local → network → ApiResponse
  Future<ApiResponse<T, void>> executeWithCache<T>({
    required Future<Response> Function() request,
    required T Function(dynamic) parser,
    required Future<T?> Function() fromLocal,
    required Future<void> Function(T) toLocal,
    required T empty,
    bool localFirst = true,
    bool shouldIsolate = false,
  }) async {
    if (localFirst) {
      final local = await fromLocal();
      if (local != null) return ApiResponse.initial(data: local).success(data: local);
    }
    try {
      final response = await request();
      final parsed = await _runParser(parser, response.data, shouldIsolate);
      await toLocal(parsed);
      return ApiResponse.initial(data: parsed).success(data: parsed);
    } on DioException catch (e) {
      final local = await fromLocal();
      if (local != null) return ApiResponse.initial(data: local).success(data: local);
      return ApiResponse.initial(data: empty).failure(message: mapDioError(e));
    } catch (_) {
      final local = await fromLocal();
      if (local != null) return ApiResponse.initial(data: local).success(data: local);
      return ApiResponse.initial(data: empty).failure(message: 'Unexpected error');
    }
  }
}

ApiResponse

lib/config/network/api_response.dart
enum ActionStatus { pure, loading, success, failure }

class ApiResponse<T, P> {
  final ActionStatus status;
  final T data;
  final P? extra;       // PaginationMeta, filter params, POST params
  final String? message;

  ApiResponse<T, P> get loading => copyWith(status: ActionStatus.loading);
  ApiResponse<T, P> success({T? data, P? extra}) => ...;
  ApiResponse<T, P> failure({String? message}) => ...;
}

typedef SimpleApiResponse<T>    = ApiResponse<T, void>;
typedef PaginatedApiResponse<T> = ApiResponse<List<T>, PaginationMeta>;

localFirst behavior

localFirst: truelocalFirst: false
Check local → hit → return immediatelyGo to network first
Miss → fetch → persist → returnFail → fallback to local
Best for: profile, settings, configBest for: feeds, listings

Feature Store pattern

lib/features/profile/data/store/profile_store.dart
class ProfileStore extends BaseStore {
  final ProfileDataSource _source;

  ProfileStore(this._source);

  // offline-aware — local first
  Future<SimpleApiResponse<ProfileModel>> getProfile() => executeWithCache(
    request: () => _source.getProfile(),
    parser: ProfileModel.fromJson,
    fromLocal: () => _local.getIfFresh(),
    toLocal: _local.upsert,
    empty: ProfileModel.empty(),
    localFirst: true,
  );

  // pure network — invalidate cache on success
  Future<SimpleApiResponse<void>> updateAvatar(String url) async {
    final result = await execute(
      request: () => _source.updateAvatar(url),
      parser: (_) {},
      empty: null,
    );
    if (result.isSuccess) await _local.clear();
    return result;
  }
}
sam's arch — A Pragmatic Flutter Architecture