leafittome/lib/core/router/app_router.dart
cschlaefke 53ee30c598 Firestore und Auth: echte Daten statt Demo, Login mit E-Mail+Passwort, Security Rules
- Datenmodell: users/{uid}, households/{id} mit memberUids, plants/locations als Subcollections
- StreamProvider liefern Live-Daten, Repositories kapseln Schreibzugriffe
- Login-Screen mit Registrierung (legt Haushalt automatisch an), Logout in Einstellungen
- Router-Redirect bei Login/Logout, firestore.rules mit Haushalts-Prinzip
- Tests auf firebase_auth_mocks + fake_cloud_firestore umgestellt
- Verifiziert: analyze/test grün, Android-Debug-Build erfolgreich

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:53:09 +02:00

105 lines
3.4 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/presentation/login_screen.dart';
import '../../features/household/presentation/household_screen.dart';
import '../../features/locations/presentation/locations_screen.dart';
import '../../features/plants/domain/plant.dart';
import '../../features/plants/presentation/plant_detail_screen.dart';
import '../../features/plants/presentation/plant_form_screen.dart';
import '../../features/plants/presentation/plants_screen.dart';
import '../../features/settings/presentation/settings_screen.dart';
import '../../features/today/presentation/today_screen.dart';
import '../firebase/firebase_providers.dart';
/// Zentrale Routen-Definition. Die Pfade werden auch vom Drawer genutzt,
/// deshalb hier als Konstanten.
abstract final class AppRoutes {
static const today = '/';
static const login = '/login';
static const plants = '/plants';
static const plantAdd = '/plants/add';
static const plantEdit = '/plants/edit';
static const locations = '/locations';
static const household = '/household';
static const settings = '/settings';
}
/// Übersetzt einen Stream in ein Listenable, damit der Router bei
/// Login/Logout sofort neu auswertet, wohin umgeleitet werden muss.
class _StreamListenable extends ChangeNotifier {
_StreamListenable(Stream<dynamic> stream) {
_subscription = stream.listen((_) => notifyListeners());
}
late final StreamSubscription<dynamic> _subscription;
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
final appRouterProvider = Provider<GoRouter>((ref) {
final auth = ref.watch(firebaseAuthProvider);
final refreshListenable = _StreamListenable(auth.authStateChanges());
ref.onDispose(refreshListenable.dispose);
final router = GoRouter(
initialLocation: AppRoutes.today,
refreshListenable: refreshListenable,
redirect: (context, state) {
final loggedIn = auth.currentUser != null;
final onLoginScreen = state.matchedLocation == AppRoutes.login;
if (!loggedIn) return onLoginScreen ? null : AppRoutes.login;
if (onLoginScreen) return AppRoutes.today;
return null;
},
routes: [
GoRoute(
path: AppRoutes.login,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: AppRoutes.today,
builder: (context, state) => const TodayScreen(),
),
GoRoute(
path: AppRoutes.plants,
builder: (context, state) => const PlantsScreen(),
),
GoRoute(
path: AppRoutes.plantAdd,
builder: (context, state) => const PlantFormScreen(),
),
GoRoute(
path: AppRoutes.plantEdit,
builder: (context, state) =>
PlantFormScreen(existing: state.extra as Plant?),
),
GoRoute(
path: '/plants/:id',
builder: (context, state) =>
PlantDetailScreen(plantId: state.pathParameters['id']!),
),
GoRoute(
path: AppRoutes.locations,
builder: (context, state) => const LocationsScreen(),
),
GoRoute(
path: AppRoutes.household,
builder: (context, state) => const HouseholdScreen(),
),
GoRoute(
path: AppRoutes.settings,
builder: (context, state) => const SettingsScreen(),
),
],
);
ref.onDispose(router.dispose);
return router;
});