| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| ⌘K / / | 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
Five layers. Strict responsibilities between them. Each layer has exactly one job — nothing leaks across boundaries.
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.
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);
},
);
} 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.
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()));
}
} 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.
class ProfileStore extends BaseStore {
ProfileStore(this._source);
Future<SimpleApiResponse<ProfileModel>> getProfile() =>
execute(
request: _source.getProfile,
parser: ProfileModel.fromJson,
empty: ProfileModel.empty(),
);
} 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.
class ProfileDataSource {
ProfileDataSource(this._client);
Future<Response> getProfile() => _client.get('/profile');
Future<Response> updateAvatar(String url) => _client.patch('/profile/avatar', data: {'url': url});
} 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).
// Composition only — wire global services, no business logic
sl.registerLazySingleton(() => ApiClient());
sl.registerLazySingleton(() => ThemeCubit(sl<SharedPreferences>()));
sl.registerLazySingleton(() => GlobalErrorCubit());