Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
/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

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 get(String path, {Map? params}) =>
      _dio.get(path, queryParameters: params);

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

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

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

BaseStore

abstract class BaseStore {

  // Simple: request → parse → ApiResponse
  Future> execute({
    required Future Function() request,
    required T Function(dynamic) parser,
    required T empty,
  }) async {
    try {
      final response = await request();
      final parsed = parser(response.data);
      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> executeWithCache({
    required Future Function() request,
    required T Function(dynamic) parser,
    required Future Function() fromLocal,
    required Future Function(T) toLocal,
    required T empty,
    bool localFirst = true,
  }) 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 = parser(response.data);
      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

enum ActionStatus { pure, isLoading, isSuccess, isFailure }

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

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

typedef SimpleApiResponse    = ApiResponse;
typedef PaginatedApiResponse = ApiResponse, 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

class ProfileStore extends BaseStore {
  final ProfileDataSource _source;
  final ProfileLocal _local;

  ProfileStore(this._source, this._local);

  // offline-aware — local first
  Future> 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> updateAvatar(String url) async {
    final result = await execute(
      request: () => _source.updateAvatar(url),
      parser: (_) {},
      empty: null,
    );
    if (result.status == ActionStatus.isSuccess) await _local.clear();
    return result;
  }
}
sam's arch — A Pragmatic Flutter Architecture