Stack Configuration — what FlutterInit generates for this guide
Architecture
Clean Architecture
State Management
MobX
Backend
When to choose this stack
Choose this stack when you like MobX's observable/action model (or you're coming from React/web MobX), want Clean Architecture's feature folders, and Firebase as the backend. Be honest with your team: MobX is less common in Flutter than Riverpod or Bloc, so hiring and Stack Overflow answers are thinner — but the pattern works, and FlutterInit generates the codegen setup for you.
Ready to build?
Generate this project in seconds
FlutterInit scaffolds the entire structure described in this guide — wired up, typed, and ready for flutter run.
MobX + Clean Architecture + Firebase gives you reactive @observable state behind Clean data / domain / presentation layers, with Firebase Auth email-password wired in. It's a valid Flutter stack — just less common than Riverpod or Bloc. FlutterInit generates the stores, build_runner config, and Firebase Auth service so you're not hand-rolling codegen.
What This Stack Generates
Select MobX + Clean Architecture + Firebase and you get:
MobX stores — AuthStore / SessionStore with @observable + @action, plus mobx, flutter_mobx, and build_runner
Firebase — AuthService over signInWithEmailAndPassword, plus Google Services setup docs
go_router — session-aware redirects
Honest caveat up front: most Flutter job posts and tutorials assume Riverpod or Bloc. Pick MobX because you want the reactive model, not because it's the default community pick.
Project Structure
On Clean / Feature-First, MobX stores land under presentation/providers/ (auth_store.dart, session_store.dart). On MVVM, they land under lib/src/ui/auth/stores/ instead.
Observables, Actions, Reactions
MobX's contract is simple:
@observable — state the UI can react to (isLoading, user, form fields)
@action — the only place you mutate observables (login, logout, toggle loading)
You don't manually call notifyListeners(). Codegen wires the tracking.
Screens wrap reactive bits in Observer:
Codegen — build_runner Is Required
MobX stores need generated mixins (_$AuthStore). FlutterInit adds mobx, flutter_mobx, mobx_codegen, and build_runner to pubspec.yaml. After you edit a store:
Skip this step and you'll get missing part / _$AuthStore errors. Re-run after every store change. Same story as Hive adapters or AutoRoute — this stack is codegen-heavy by design.
Domain and Data Still Stay Clean
The store depends on AuthRepository — not Firebase. Domain stays SDK-free:
Data implements it via AuthService → FirebaseAuth.signInWithEmailAndPassword. Drop in google-services.json / GoogleService-Info.plist per SETUP.md. That's the Firebase edge; everything above it stays testable with a fake repository.
When to Choose This vs Alternatives
Pick MobX + Clean + Firebase when:
Your team already thinks in observables/actions (web MobX, or prior Flutter MobX apps)
You want Clean feature folders but prefer MobX ergonomics over Riverpod providers
You're fine running build_runner as part of the daily loop
You want zero codegen for state — Provider or Riverpod without code generation is lighter
You're new to Clean Architecture itself — start with the Clean Architecture generated guide, then layer MobX once the folders make sense
MobX isn't wrong in Flutter. It's just quieter. Generate it when the reactive model is a deliberate choice, not a default.
Ready to Generate?
Open the dashboard, select Clean + MobX + Firebase + go_router, run build_runner once after unzip, and you've got reactive stores sitting on Clean layers with Firebase Auth ready for email-password.
presentation/providers/auth_store.dart
part 'auth_store.g.dart';class AuthStore = AuthStoreBase with _$AuthStore;abstract class AuthStoreBase with Store { final AuthRepository _repository; AuthStoreBase({required AuthRepository repository}) : _repository = repository; @observable bool isLoading = false; @action void login({ required BuildContext context, required String email, required String password, }) async { isLoading = true; final result = await _repository.login(email: email, password: password); isLoading = false; result.fold( (failure) { if (context.mounted) { showToast(context, message: failure.message, status: 'error'); } }, (_) { if (context.mounted) context.go(AppRoutes.home); }, ); }}