added explicit procedure steps with input/output contracts, formalized decision points for common scenarios (auth, real-time data, errors, scaling), documented dependency injection requirements, provided concrete directory structure, and clarified the single source of truth principle with repository ownership.
intent
use this skill when structuring a new flutter project or refactoring an existing one for scalability. the goal is to enforce strict separation of concerns across three layers (ui, logic, data) with unidirectional data flow and a single source of truth. this architecture prevents monolithic code, makes testing easier, and keeps features maintainable as the app grows.
inputs
- flutter sdk 3.0 or higher (flutter --version to verify)
- a code editor (vs code, android studio, or equivalent)
- familiarity with dart async patterns (futures, streams)
- understanding of immutable state objects and reactive programming
- optional: state management package (provider, riverpod, or bloc)
- optional: dependency injection package (get_it, service_locator)
procedure
define domain models
- create
lib/domain/models/ directory
- write pure dart classes representing your core business entities (e.g., user, post, product)
- keep domain models free of any framework dependencies or ui concerns
- input: feature requirements
- output: immutable domain model classes (freezed or equatable recommended)
implement the data layer services
- create
lib/data/services/ directory
- write stateless service classes that handle raw api calls, database queries, or file i/o
- each service is responsible for one data source (e.g., api_service, local_db_service, cache_service)
- services throw typed exceptions on failure, never catch errors silently
- input: domain models, external connections (http client, db driver)
- output: service classes with typed method signatures
implement repositories
- create
lib/data/repositories/ directory
- write repository classes that coordinate multiple services and transform responses into domain models
- repositories are the single source of truth for their domain; they handle caching, retry logic, and data transformation
- repositories expose stream or future interfaces, not raw service responses
- input: services, domain models
- output: repository classes implementing typed interfaces
create viewmodels (logic layer)
- create
lib/logic/viewmodels/ directory
- write viewmodel classes that hold feature state (as immutable state objects) and expose streams or listenable notifiers
- viewmodels contain business logic like filtering, sorting, validation, or orchestration across multiple repositories
- viewmodels never directly reference ui widgets; they only know about domain models and state
- input: repositories, user events
- output: viewmodel classes emitting state objects
build ui layer views
- create
lib/ui/screens/ and lib/ui/widgets/ directories
- write stateless widget classes that subscribe to viewmodel state streams
- ui rebuilds only when the underlying state changes; keep widgets thin and declarative
- route user interactions (taps, form submissions) as events sent upward to the viewmodel
- input: viewmodels, domain state objects
- output: reactive widget tree
wire dependency injection
- at app startup (main.dart or a setup function), instantiate all services, repositories, and viewmodels
- register dependencies in a service locator (e.g., get_it.registerSingleton) or pass them down via constructor injection
- ensure each layer only knows about the layer below it (ui knows logic, logic knows data, data knows domain)
- input: all layer classes
- output: configured dependency graph
validate with unit and widget tests
- write unit tests for domain models, services, repositories, and viewmodels
- write widget tests for ui components using mock viewmodels
- test each layer independently; mock dependencies below the layer under test
- input: test framework (flutter test, mockito, mocktail)
- output: green test suite with >80% coverage on business logic
decision points
- if building a simple feature with no complex state: skip the logic/viewmodel layer; repositories can emit state directly to the ui.
- if the api requires authentication: store tokens in a secure local storage service (flutter_secure_storage); refresh tokens before api calls or on 401 errors.
- if you need real-time data: use websocket services or firestore streams in the data layer; expose them as repository streams.
- if handling large lists: implement pagination at the repository level; emit partial state updates as new pages load.
- if a service fails (network timeout, 5xx error): repository catches the typed exception, applies retry logic, and emits error state to viewmodel; viewmodel surfaces the error to the ui.
- if the viewmodel state grows large: split into multiple feature-specific viewmodels or use a state management package (bloc, riverpod) to compose smaller state atoms.
output contract
the artifact is a flutter project tree following this structure:
lib/
domain/
models/
<feature>_model.dart
data/
services/
<source>_service.dart
repositories/
<feature>_repository.dart
logic/
viewmodels/
<feature>_viewmodel.dart
ui/
screens/
<feature>_screen.dart
widgets/
<custom>_widget.dart
main.dart
test/
domain/
data/
logic/
ui/
each feature module (e.g., posts, users) spans all three layers. dependencies point downward only (ui -> logic -> data -> domain).
outcome signal
you know the architecture is working when:
- adding a new feature requires changes in all three layers, with minimal modification to existing code
- unit tests for repositories and viewmodels run in <1s and don't depend on ui or framework setup
- widget tests mock the viewmodel and run without spinning up real services
- the ui tree is small and declarative; all conditional logic lives in the viewmodel
- swapping one data source (e.g., rest api for graphql) requires changes only in services and repositories, not in views
- code review feedback focuses on business logic, not architectural violations