Flutter App Architecture Implementation
Goal
Implements a scalable, maintainable Flutter application architecture using the MVVM pattern, unidirectional data flow, and strict separation of concerns across UI, Domain, and Data layers. Assumes a standard Flutter environment utilizing provider for dependency injection and ListenableBuilder for reactive UI updates.
Decision Logic
Before implementing a feature, evaluate the architectural requirements using the following logic:
- Data Source:
- If interacting with an external API -> Create a Remote Service.
- If interacting with local storage (SQL/Key-Value) -> Create a Local Service.
- Business Logic Complexity:
- If the feature requires merging data from multiple repositories or contains highly complex, reusable logic -> Implement a Domain Layer (UseCases).
- If the feature is standard CRUD or simple data presentation -> Skip the Domain Layer; the ViewModel communicates directly with the Repository.
Instructions
Analyze Feature Requirements
Evaluate the requested feature to determine the necessary data models, services, and UI state.
STOP AND ASK THE USER: "Please provide the specific data models, API endpoints, or local storage requirements for this feature, and confirm if complex business logic requires a dedicated Domain (UseCase) layer."
Implement the Data Layer: Services
Create a stateless service class to wrap the external API or local storage. This class must not contain business logic or state.
class SharedPreferencesService {
static const String _kDarkMode = 'darkMode';
Future<void> setDarkMode(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_kDarkMode, value);
}
Future<bool> isDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_kDarkMode) ?? false;
}
}
Implement the Data Layer: Repositories
Create a repository to act as the single source of truth. The repository consumes the service, handles errors using Result objects, and exposes domain models or streams.
class ThemeRepository {
ThemeRepository(this._service);
final _darkModeController = StreamController<bool>.broadcast();
final SharedPreferencesService _service;
Future<Result<bool>> isDarkMode() async {
try {
final value = await _service.isDarkMode();
return Result.ok(value);
} on Exception catch (e) {
return Result.error(e);
}
}
Future<Result<void>> setDarkMode(bool value) async {
try {
await _service.setDarkMode(value);
_darkModeController.add(value);
return Result.ok(null);
} on Exception catch (e) {
return Result.error(e);
}
}
Stream<bool> observeDarkMode() => _darkModeController.stream;
}
Implement the UI Layer: ViewModels
Create a ChangeNotifier to manage UI state. Use the Command pattern to handle user interactions and asynchronous repository calls.
class ThemeSwitchViewModel extends ChangeNotifier {
ThemeSwitchViewModel(this._themeRepository) {
load = Command0(_load)..execute();
toggle = Command0(_toggle);
}
final ThemeRepository _themeRepository;
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
late final Command0<void> load;
late final Command0<void> toggle;
Future<Result<void>> _load() async {
final result = await _themeRepository.isDarkMode();
if (result is Ok<bool>) {
_isDarkMode = result.value;
}
notifyListeners();
return result;
}
Future<Result<void>> _toggle() async {
_isDarkMode = !_isDarkMode;
final result = await _themeRepository.setDarkMode(_isDarkMode);
notifyListeners();
return result;
}
}
Implement the UI Layer: Views
Create a StatelessWidget that observes the ViewModel using ListenableBuilder. The View must contain zero business logic.
class ThemeSwitch extends StatelessWidget {
const ThemeSwitch({super.key, required this.viewmodel});
final ThemeSwitchViewModel viewmodel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
const Text('Dark Mode'),
ListenableBuilder(
listenable: viewmodel,
builder: (context, _) {
return Switch(
value: viewmodel.isDarkMode,
onChanged: (_) {
viewmodel.toggle.execute();
},
);
},
),
],
),
);
}
}
Wire Dependencies
Inject the dependencies at the application or route level using constructor injection or a dependency injection framework like provider.
void main() {
runApp(
MainApp(
themeRepository: ThemeRepository(SharedPreferencesService()),
),
);
}
Validate and Fix
Review the generated implementation against the constraints. Ensure that data flows strictly downwards (Repository -> ViewModel -> View) and events flow strictly upwards (View -> ViewModel -> Repository). If a View contains data mutation logic, extract it to the ViewModel. If a ViewModel directly accesses an API, extract it to a Service and route it through a Repository.
Constraints
- No Logic in Views: Views must only contain layout logic, simple conditional rendering based on ViewModel state, and routing.
- Unidirectional Data Flow: Data must only flow from the Data Layer to the UI Layer. UI events must trigger ViewModel commands.
- Single Source of Truth: Repositories are the only classes permitted to mutate application data.
- Service Isolation: ViewModels must never interact directly with Services. They must communicate exclusively through Repositories (or UseCases).
- Stateless Services: Service classes must not hold any state. Their sole responsibility is wrapping external APIs or local storage mechanisms.
- Immutable Models: Domain models passed from Repositories to ViewModels must be immutable.
- Error Handling: Repositories must catch exceptions from Services and return explicit
Result (Ok/Error) objects to the ViewModels.
1---2name: flutter-architecture3description: Use the Flutter team's recommended app architecture4---5# Flutter App Architecture Implementation67## Goal8Implements a scalable, maintainable Flutter application architecture using the MVVM pattern, unidirectional data flow, and strict separation of concerns across UI, Domain, and Data layers. Assumes a standard Flutter environment utilizing `provider` for dependency injection and `ListenableBuilder` for reactive UI updates.910## Decision Logic11Before implementing a feature, evaluate the architectural requirements using the following logic:121. **Data Source:** 13 * If interacting with an external API -> Create a Remote Service.14 * If interacting with local storage (SQL/Key-Value) -> Create a Local Service.152. **Business Logic Complexity:**16 * If the feature requires merging data from multiple repositories or contains highly complex, reusable logic -> Implement a **Domain Layer** (UseCases).17 * If the feature is standard CRUD or simple data presentation -> Skip the Domain Layer; the ViewModel communicates directly with the Repository.1819## Instructions20211. **Analyze Feature Requirements**22 Evaluate the requested feature to determine the necessary data models, services, and UI state. 23 **STOP AND ASK THE USER:** "Please provide the specific data models, API endpoints, or local storage requirements for this feature, and confirm if complex business logic requires a dedicated Domain (UseCase) layer."24252. **Implement the Data Layer: Services**26 Create a stateless service class to wrap the external API or local storage. This class must not contain business logic or state.27 ```dart28 class SharedPreferencesService {29 static const String _kDarkMode = 'darkMode';3031 Future<void> setDarkMode(bool value) async {32 final prefs = await SharedPreferences.getInstance();33 await prefs.setBool(_kDarkMode, value);34 }3536 Future<bool> isDarkMode() async {37 final prefs = await SharedPreferences.getInstance();38 return prefs.getBool(_kDarkMode) ?? false;39 }40 }41 ```42433. **Implement the Data Layer: Repositories**44 Create a repository to act as the single source of truth. The repository consumes the service, handles errors using `Result` objects, and exposes domain models or streams.45 ```dart46 class ThemeRepository {47 ThemeRepository(this._service);4849 final _darkModeController = StreamController<bool>.broadcast();50 final SharedPreferencesService _service;5152 Future<Result<bool>> isDarkMode() async {53 try {54 final value = await _service.isDarkMode();55 return Result.ok(value);56 } on Exception catch (e) {57 return Result.error(e);58 }59 }6061 Future<Result<void>> setDarkMode(bool value) async {62 try {63 await _service.setDarkMode(value);64 _darkModeController.add(value);65 return Result.ok(null);66 } on Exception catch (e) {67 return Result.error(e);68 }69 }7071 Stream<bool> observeDarkMode() => _darkModeController.stream;72 }73 ```74754. **Implement the UI Layer: ViewModels**76 Create a `ChangeNotifier` to manage UI state. Use the Command pattern to handle user interactions and asynchronous repository calls.77 ```dart78 class ThemeSwitchViewModel extends ChangeNotifier {79 ThemeSwitchViewModel(this._themeRepository) {80 load = Command0(_load)..execute();81 toggle = Command0(_toggle);82 }8384 final ThemeRepository _themeRepository;85 bool _isDarkMode = false;8687 bool get isDarkMode => _isDarkMode;8889 late final Command0<void> load;90 late final Command0<void> toggle;9192 Future<Result<void>> _load() async {93 final result = await _themeRepository.isDarkMode();94 if (result is Ok<bool>) {95 _isDarkMode = result.value;96 }97 notifyListeners();98 return result;99 }100101 Future<Result<void>> _toggle() async {102 _isDarkMode = !_isDarkMode;103 final result = await _themeRepository.setDarkMode(_isDarkMode);104 notifyListeners();105 return result;106 }107 }108 ```1091105. **Implement the UI Layer: Views**111 Create a `StatelessWidget` that observes the ViewModel using `ListenableBuilder`. The View must contain zero business logic.112 ```dart113 class ThemeSwitch extends StatelessWidget {114 const ThemeSwitch({super.key, required this.viewmodel});115116 final ThemeSwitchViewModel viewmodel;117118 @override119 Widget build(BuildContext context) {120 return Padding(121 padding: const EdgeInsets.symmetric(horizontal: 16.0),122 child: Row(123 children: [124 const Text('Dark Mode'),125 ListenableBuilder(126 listenable: viewmodel,127 builder: (context, _) {128 return Switch(129 value: viewmodel.isDarkMode,130 onChanged: (_) {131 viewmodel.toggle.execute();132 },133 );134 },135 ),136 ],137 ),138 );139 }140 }141 ```1421436. **Wire Dependencies**144 Inject the dependencies at the application or route level using constructor injection or a dependency injection framework like `provider`.145 ```dart146 void main() {147 runApp(148 MainApp(149 themeRepository: ThemeRepository(SharedPreferencesService()),150 ),151 );152 }153 ```1541557. **Validate and Fix**156 Review the generated implementation against the constraints. Ensure that data flows strictly downwards (Repository -> ViewModel -> View) and events flow strictly upwards (View -> ViewModel -> Repository). If a View contains data mutation logic, extract it to the ViewModel. If a ViewModel directly accesses an API, extract it to a Service and route it through a Repository.157158## Constraints159* **No Logic in Views:** Views must only contain layout logic, simple conditional rendering based on ViewModel state, and routing.160* **Unidirectional Data Flow:** Data must only flow from the Data Layer to the UI Layer. UI events must trigger ViewModel commands.161* **Single Source of Truth:** Repositories are the only classes permitted to mutate application data.162* **Service Isolation:** ViewModels must never interact directly with Services. They must communicate exclusively through Repositories (or UseCases).163* **Stateless Services:** Service classes must not hold any state. Their sole responsibility is wrapping external APIs or local storage mechanisms.164* **Immutable Models:** Domain models passed from Repositories to ViewModels must be immutable.165* **Error Handling:** Repositories must catch exceptions from Services and return explicit `Result` (Ok/Error) objects to the ViewModels.