Mobile App
App Architecture
Navigation, state management patterns, and repository layer
App Architecture
Navigation β GoRouter
All routes are declared in lib/src/core/router.dart. The app uses a ShellRoute for the 5-tab bottom navigation, with sub-routes pushed on top.
ShellRoute (5-tab nav)
βββ / β HomeScreen
βββ /meals β MealPlanScreen
βββ /recipes β RecipesScreen
β βββ /recipes/:id β RecipeDetailScreen
βββ /pantry β InventoryScreen
βββ /groceries β ShoppingListScreen
/login β LoginScreen (outside shell)Navigation rules:
context.go('/path')β switch tabs (replaces shell history)context.push('/path')β push on top of current tab (sub-routes)- Login redirect: unauthenticated users are sent to
/loginvia GoRouter redirect
State management β Riverpod
Every screen uses ConsumerWidget or ConsumerStatefulWidget. Data is exposed as FutureProvider / StateProvider / StateNotifierProvider:
// Example: recipe list provider
final recipesProvider = FutureProvider.autoDispose<List<Recipe>>((ref) async {
final repo = ref.read(recipeRepositoryProvider);
return repo.getRecipes();
});
// In screen:
class RecipesScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final recipes = ref.watch(recipesProvider);
return recipes.when(
data: (data) => RecipeList(data),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorWidget(e.toString()),
);
}
}Repository layer
Each feature has a repository class that:
- Makes Dio HTTP requests to the API
- Parses responses using
fromJsonfactory constructors - Handles both
{ "items": [...] }and[...]response shapes
class RecipeRepository {
final Dio _dio;
RecipeRepository(this._dio);
Future<List<Recipe>> getRecipes({String? query, String? category}) async {
final res = await _dio.get('/api/recipes', queryParameters: {
if (query != null) 'q': query,
if (category != null) 'category': category,
});
final raw = res.data;
final list = raw is List ? raw : (raw['items'] as List);
return list.map((j) => Recipe.fromJson(j as Map<String, dynamic>)).toList();
}
}Error handling patterns
All model fromJson methods use null-safe parsing:
- Integer DB IDs are converted to
Stringvia?.toString() ?? '' - Optional fields fall back to defaults:
json['field'] as String? ?? '' - List fields use safe cast:
(json['list'] as List? ?? []).map(...) - Column name aliases:
json['location'] ?? json['storage_location']