| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| / | Open search |
| ? | Show this help |
| Esc | Close search / help |
| n / N | Next / previous section |
| h | Go to landing |
| d | Go to docs |
| : | Command mode — type section name |
Press ? or Esc to close
ApiClient wraps Dio with three interceptors. BaseStore handles all execution, error catching, and cache orchestration. ApiResponse is the universal return type.
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);
} 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');
}
}
} 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: true | localFirst: false |
|---|---|
| Check local → hit → return immediately | Go to network first |
| Miss → fetch → persist → return | Fail → fallback to local |
| Best for: profile, settings, config | Best for: feeds, listings |
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;
}
}