Ein umfassendes Flutter-Lernprojekt, das wichtige Software-Engineering-Konzepte demonstriert.
Dieses Projekt zeigt Best Practices fΓΌr:
- MVVM (Model-View-ViewModel) Architektur
- Repository Pattern fΓΌr Datenabstraktion
- Dependency Injection mit Provider
- State Management mit ChangeNotifier
- Testing (Unit Tests & Widget Tests)
- Clean Architecture Prinzipien
lib/
βββ models/ # Datenmodelle (User)
βββ services/ # API-Services (HTTP-Kommunikation)
βββ repositories/ # Repository Pattern (Datenabstraktion + Caching)
βββ viewmodels/ # ViewModels (Business-Logik + State)
βββ views/ # UI-Komponenten (Screens & Widgets)
test/
βββ models/ # Model Tests
βββ repositories/ # Repository Tests (mit Mocks)
βββ viewmodels/ # ViewModel Tests (mit Mocks)
βββ views/ # Widget Tests
1. Model Layer (models/user.dart)
- Datenstrukturen mit
fromJson/toJson - Immutable mit
finalfields copyWithfΓΌr Updates- Equality & HashCode
class User {
final int id;
final String name;
final String email;
factory User.fromJson(Map<String, dynamic> json) { ... }
Map<String, dynamic> toJson() { ... }
User copyWith({...}) { ... }
}2. Service Layer (services/api_service.dart)
- Abstrakte Interfaces fΓΌr Testbarkeit
- HTTP-Kommunikation isoliert
- Error Handling mit Custom Exceptions
- Dependency Injection Ready
abstract class ApiService {
Future<List<User>> fetchUsers();
}
class ApiServiceImpl implements ApiService {
final http.Client client; // Injected!
// ...
}3. Repository Layer (repositories/user_repository.dart)
- Abstrahiert Datenquellen (API, Cache, DB)
- Implementiert Caching-Strategien
- Business-Logik fΓΌr Datenzugriff
- Kombiniert mehrere Services
abstract class UserRepository {
Future<List<User>> getUsers();
void clearCache();
}
class UserRepositoryImpl implements UserRepository {
final ApiService apiService; // Injected!
List<User>? _cachedUsers; // Caching
// ...
}4. ViewModel Layer (viewmodels/user_list_viewmodel.dart)
- Erweitert
ChangeNotifierfΓΌr State Management - Kommuniziert mit Repositories
- UI-unabhΓ€ngige Business-Logik
- Verwaltung von Loading/Error/Success States
class UserListViewModel extends ChangeNotifier {
final UserRepository repository; // Injected!
ViewState _state = ViewState.idle;
List<User> _users = [];
Future<void> loadUsers() async {
_state = ViewState.loading;
notifyListeners(); // UI wird aktualisiert!
_users = await repository.getUsers();
_state = ViewState.success;
notifyListeners();
}
}5. View Layer (views/user_list_screen.dart)
- Stateless/Stateful Widgets
Consumer<T>fΓΌr reactive Updatescontext.read<T>()fΓΌr Methoden-Aufrufe- Keine Business-Logik
Consumer<UserListViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) return CircularProgressIndicator();
if (viewModel.hasError) return ErrorWidget();
return ListView(children: ...);
},
)π§ Dependency Injection Setup (main.dart)
MultiProvider erstellt eine Dependency-Hierarchie:
MultiProvider(
providers: [
// 1. Service Layer
Provider<ApiService>(
create: (_) => ApiServiceImpl(client: http.Client()),
),
// 2. Repository Layer (nutzt ApiService)
ProxyProvider<ApiService, UserRepository>(
update: (_, apiService, _) => UserRepositoryImpl(apiService: apiService),
),
// 3. ViewModel Layer (nutzt Repository)
ChangeNotifierProxyProvider<UserRepository, UserListViewModel>(
create: (ctx) => UserListViewModel(repository: ctx.read<UserRepository>()),
update: (_, repo, vm) => vm ?? UserListViewModel(repository: repo),
),
],
child: MaterialApp(...),
)Model Tests (test/models/user_test.dart):
- JSON Serialisierung/Deserialisierung
- copyWith FunktionalitΓ€t
- Equality & HashCode
Repository Tests (test/repositories/user_repository_test.dart):
@GenerateMocks([ApiService])
void main() {
late MockApiService mockApiService;
late UserRepositoryImpl repository;
setUp(() {
mockApiService = MockApiService();
repository = UserRepositoryImpl(apiService: mockApiService);
});
test('should cache users', () async {
when(mockApiService.fetchUsers()).thenAnswer((_) async => testUsers);
await repository.getUsers(); // 1. API Call
await repository.getUsers(); // Von Cache
verify(mockApiService.fetchUsers()).called(1); // Nur 1x!
});
}ViewModel Tests (test/viewmodels/user_list_viewmodel_test.dart):
- State Transitions (idle β loading β success)
- Error Handling
- Repository Interaktionen
Screen Tests (test/views/user_list_screen_test.dart):
testWidgets('shows loading indicator when loading', (tester) async {
when(mockViewModel.isLoading).thenReturn(true);
await tester.pumpWidget(
ChangeNotifierProvider<UserListViewModel>.value(
value: mockViewModel,
child: UserListScreen(),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});# Mocks generieren
flutter pub run build_runner build --delete-conflicting-outputs
# Alle Tests ausfΓΌhren
flutter test
# Mit Coverage
flutter test --coverage# Dependencies installieren
flutter pub get
# App starten
flutter run
# Tests ausfΓΌhren
flutter testMVVM im klassischen Sinne hat nur 3 Schichten:
βββββββββββββββββββββββββββ
β View (UI) β β UserListScreen
βββββββββββββ¬ββββββββββββββ
β
βββββββββββββΌββββββββββββββ
β ViewModel (UI-Logik) β β UserListViewModel
βββββββββββββ¬ββββββββββββββ
β
βββββββββββββΌββββββββββββββ
β Model (Daten) β β User-Klasse
βββββββββββββββββββββββββββ
Pure MVVM: ViewModel wΓΌrde direkt HTTP-Calls machen
// β Pure MVVM (nicht empfohlen fΓΌr grΓΆΓere Apps)
class UserListViewModel extends ChangeNotifier {
Future<void> loadUsers() async {
final response = await http.get('https://api.com/users'); // Direkt im ViewModel!
_users = jsonDecode(response.body);
notifyListeners();
}
}Dieses Projekt nutzt eine erweiterte MVVM-Architektur mit zusΓ€tzlichen Schichten:
βββββββββββββββββββββββββββββββββββββββ
β View (UI) β β UserListScreen
β - Zeigt Daten an β
β - Reagiert auf User-Input β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββββββββββ
β ViewModel (UI-Logik) β β UserListViewModel
β - Verwaltet UI-State β β
Teil von MVVM
β - Holt Daten vom Repository β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββββββββββ
β Repository (Data-Logik) β β UserRepository
β - Caching β β ZUSΓTZLICHE SCHICHT
β - Daten kombinieren β (Nicht in purem MVVM)
β - Business-Logik fΓΌr Daten β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββββββββββ
β Service (Externe Daten) β β ApiService
β - HTTP-Calls β β ZUSΓTZLICHE SCHICHT
β - Datenbank-Zugriff β (Nicht in purem MVVM)
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββΌβββββββββββββββββββββββ
β Model (Datenstruktur) β β User-Klasse
β - Nur Daten β β
Teil von MVVM
β - fromJson/toJson β
βββββββββββββββββββββββββββββββββββββββ
Nein! In der Praxis nutzen fast alle professionellen Apps erweiterte Architekturen:
| Ansatz | Wann verwendet? | Beispiele |
|---|---|---|
| Pure MVVM | Sehr kleine Apps, Prototypen | Todo-App Demo, einfache Tutorials |
| MVVM + Repository | Kleine bis mittlere Apps | Die meisten Flutter Apps |
| MVVM + Repository + Service | Mittlere bis groΓe Apps | |
| Clean Architecture | Sehr groΓe Enterprise Apps | Banking Apps, E-Commerce |
β Vorteile:
- Separation of Concerns: Jede Schicht hat genau eine Aufgabe
- Testbarkeit: Jede Schicht kann isoliert getestet werden
- Austauschbar: API β lokale DB ohne ViewModel zu Γ€ndern
- Caching: An einem Ort (Repository), nicht in jedem ViewModel
- Skalierbarkeit: Einfach neue Features hinzufΓΌgen
- Team-Arbeit: Verschiedene Entwickler an verschiedenen Schichten
β Nachteile:
- Mehr Code: Mehr Dateien, mehr Boilerplate
- KomplexitΓ€t: Steile Lernkurve fΓΌr AnfΓ€nger
- Overhead: FΓΌr kleine Apps ΓΌbertrieben
- Mehr Abstraktion: Schwieriger zu debuggen
// Kleine App (< 5 Screens):
View β ViewModel β HTTP (direkt)
// Mittlere App (5-20 Screens):
View β ViewModel β Repository β HTTP
β Cache
// GroΓe App (20+ Screens):
View β ViewModel β Repository β Service β HTTP/DB
β Cache
β Offline-SyncVorteile:
- β Klare Trennung von UI und Logik
- β Testbar ohne UI
- β Wiederverwendbare ViewModels
- β Reaktive UI-Updates
Datenfluss:
View β notifyListeners() β ViewModel β Repository β Service β API
View β Aktion β ViewModel β Repository β Service β API
Vorteile:
- β Abstrahiert Datenquellen
- β ErmΓΆglicht Caching
- β Austauschbare Implementierungen
- β Zentrale Datenzugriff-Logik
Vorteile:
- β Loose Coupling
- β Testbarkeit (Mocking)
- β FlexibilitΓ€t
- β Single Responsibility
Vorteile:
- β Built-in in Flutter
- β Reactive State Management
- β Scoped Dependencies
- β Efficient Rebuilds
- Clean Architecture: Schichten-Trennung fΓΌr wartbaren Code
- SOLID Prinzipien: Besonders Dependency Inversion
- Testing: Unit Tests mit Mocks, Widget Tests
- State Management: ChangeNotifier & Provider
- Async Programming: Futures, async/await
- Error Handling: Try-catch, Custom Exceptions
- Caching: In-Memory Caching Strategien
- Erweitere das User-Model: FΓΌge Address, Company hinzu
- Implementiere CRUD: Create, Update, Delete User
- Persistenz: Speichere Daten lokal (SharedPreferences, SQLite)
- Navigation: Implementiere komplexere Navigation
- Themes: Dark Mode mit Provider
- Error States: Besseres Error Handling
- Integration Tests: End-to-End Tests