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

Architecture

Layers

Five layers. Strict responsibilities between them. Each layer has exactly one job — nothing leaks across boundaries.

01Presentationfeatures/*/presentation/

Stateless widgets only. Reads Cubit state via BlocBuilder, calls cubit methods — nothing else. No business logic, no store calls, no API calls.

Screens are StatefulWidget only to call cubit methods in initState. All visual decisions come from design tokens — never raw colors or hardcoded sizes.

lib/features/profile/presentation/screens/profile_screen.dart
class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});
  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  @override
  void initState() {
    super.initState();
    context.read<ProfileCubit>().fetchProfile();
  }

  @override
  Widget build(BuildContext context) =>
      BlocBuilder<ProfileCubit, ProfileState>(
        builder: (context, state) {
          if (state.response.isLoading) return const AppShimmer();
          return ProfileContent(data: state.response.data);
        },
      );
}
StatelessWidgetBlocBuildercontext.colors.*AppSpacingAppTap
02Logic — Cubitfeatures/*/logic/

Cubit is the only state primitive — simple, lightweight, no boilerplate. There is no Bloc anywhere in the codebase. Multiple Cubits per feature are allowed when concerns are clearly separate (e.g. form state vs data state).

State always has an initial() factory and copyWith(). Always emit loading before an async operation — never jump directly to success.

lib/features/profile/logic/profile_cubit.dart
class ProfileCubit extends Cubit<ProfileState> {
  ProfileCubit(this._store) : super(ProfileState.initial());

  Future<void> fetchProfile() async {
    emit(state.copyWith(response: state.response.loading));
    emit(state.copyWith(response: await _store.getProfile()));
  }
}
Cubit onlyApiResponse<T>initial()copyWith()
03Storefeatures/*/data/store/

Extends BaseStore (in config/network/). Orchestrates the cache strategy: checks local first, fetches remote if stale, returns ApiResponse<T>.

All try/catch happens inside BaseStore — feature stores never write their own try/catch. The Store decides what to call and when. BaseStore decides how errors are wrapped.

lib/features/profile/data/store/profile_store.dart
class ProfileStore extends BaseStore {
  ProfileStore(this._source);

  Future<SimpleApiResponse<ProfileModel>> getProfile() =>
      execute(
        request: _source.getProfile,
        parser: ProfileModel.fromJson,
        empty: ProfileModel.empty(),
      );
}
BaseStoreexecuteWithCacheApiResponsecache orchestration
04DataSource + Localfeatures/*/data/source/ · local/

DataSource — raw HTTP only. Returns raw Response. No base class needed. No business logic, no parsing, no try/catch. Just HTTP calls.

Local (opt-in) — sqflite CRUD only. Handles freshness via cached_at. Only added when offline is a confirmed requirement.

lib/features/profile/data/source/profile_data_source.dart
class ProfileDataSource {
  ProfileDataSource(this._client);

  Future<Response> getProfile() => _client.get('/profile');
  Future<Response> updateAvatar(String url) => _client.patch('/profile/avatar', data: {'url': url});
}
dio Responsesqflite (opt-in)cached_atfromMap / toMap
05Core / Configapp/ · config/

app/ is composition only — no business logic. Only global/core services registered at bootstrap: ApiClient, AppDatabase, GlobalErrorCubit, ThemeCubit, LocalizationCubit.

config/ is stateless, globally available, zero feature imports. Interceptors: AuthInterceptor (token + 401 refresh lock), LoggingInterceptor (debug only), ErrorInterceptor (maps errors → GlobalErrorCubit).

lib/app/app_bootstrap.dart
// Composition only — wire global services, no business logic
sl.registerLazySingleton(() => ApiClient());
sl.registerLazySingleton(() => ThemeCubit(sl<SharedPreferences>()));
sl.registerLazySingleton(() => GlobalErrorCubit());
get_itgo_routerGlobalErrorCubitThemeCubitAuthInterceptor
sam's arch — A Pragmatic Flutter Architecture