diff --git a/coffee_maker_navigator_2/lib/app/app.dart b/coffee_maker_navigator_2/lib/app/app.dart index 33953937..415ee196 100644 --- a/coffee_maker_navigator_2/lib/app/app.dart +++ b/coffee_maker_navigator_2/lib/app/app.dart @@ -9,6 +9,12 @@ class CoffeeMakerApp extends StatelessWidget { @override Widget build(BuildContext context) { + /// STEP #3: Provide the DependencyInjector to the widget tree. + /// + /// Here, we wrap the entire widget tree with the `DependencyInjector` widget. + /// This makes the DI system available to all widgets in the tree, allowing them + /// to access the active dependency containers. By doing this, any widget can + /// easily retrieve the necessary dependencies it needs. return DependencyInjector( child: Builder(builder: (context) { final appLevelDependencyContainer = DependencyInjector.container< diff --git a/coffee_maker_navigator_2/lib/app/router/entities/app_route_configuration.dart b/coffee_maker_navigator_2/lib/app/router/entities/app_route_configuration.dart index bd0dc6c8..f4f66052 100644 --- a/coffee_maker_navigator_2/lib/app/router/entities/app_route_configuration.dart +++ b/coffee_maker_navigator_2/lib/app/router/entities/app_route_configuration.dart @@ -31,6 +31,11 @@ class AppRouteConfiguration { final AppRouteUriTemplate appRouteUriTemplate; final QueryParams queryParams; + /// STEP #16: Define the AppRouteConfiguration class. + /// + /// This class represents the navigation state by combining a static route template + /// from [AppRouteUriTemplate] with dynamic query parameters. It is used in handling + /// deep linking, dynamic navigation, and keeping the app's state in sync with the browser's URL. const AppRouteConfiguration({ required this.appRouteUriTemplate, this.queryParams = const {}, diff --git a/coffee_maker_navigator_2/lib/app/router/entities/app_route_page.dart b/coffee_maker_navigator_2/lib/app/router/entities/app_route_page.dart index 100f76e7..93991d21 100644 --- a/coffee_maker_navigator_2/lib/app/router/entities/app_route_page.dart +++ b/coffee_maker_navigator_2/lib/app/router/entities/app_route_page.dart @@ -19,22 +19,16 @@ import 'package:flutter/material.dart'; import 'package:wolt_di/wolt_di.dart'; import 'package:wolt_modal_sheet/wolt_modal_sheet.dart'; -/// `AppRoutePage` is a sealed class that extends Flutter's [Page] class. It serves as a base -/// class for all route pages within the application, defining a consistent interface and behavior -/// for routes. +/// STEP #7: Define the route pages. /// -/// One of the main benefits of a sealed class is that it guarantees exhaustiveness. -/// Since all possible subclasses of `AppRoutePage` are known at compile time, the application can -/// ensure that every route is explicitly handled. This helps prevent errors that can arise from -/// unhandled routes or navigation scenarios, leading to more robust and predictable routing behavior. -/// When working with pattern matching or switch cases on instances of `AppRoutePage`, the compiler -/// can enforce that all cases are covered, reducing the risk of runtime errors. +/// The `AppRoutePage` class is a base class for all route pages in the application. +/// It extends Flutter's [Page] class, providing a consistent way to define the pages +/// and their behavior for navigation. /// -/// Each subclass of `AppRoutePage` must specify an [AppRouteUriTemplate] that represents the -/// static route template associated with the page. The `queryParams` getter allows each page -/// to define any dynamic query parameters it might need. This is crucial for passing state or -/// configuration data through the URL, supporting more sophisticated navigation scenarios and -/// state management.x +/// Using a sealed class provides exhaustiveness that helps to ensure that all possible route types +/// are defined and handled explicitly. This makes the navigation system more robust and +/// predictable by preventing errors from unhandled routes. It allows for clear and +/// exhaustive pattern matching when managing navigation, reducing the chance of runtime errors. sealed class AppRoutePage extends Page { const AppRoutePage({LocalKey? key}) : super(key: key); diff --git a/coffee_maker_navigator_2/lib/app/router/view/app_route_information_parser.dart b/coffee_maker_navigator_2/lib/app/router/view/app_route_information_parser.dart index 0b13a5c8..ef45b534 100644 --- a/coffee_maker_navigator_2/lib/app/router/view/app_route_information_parser.dart +++ b/coffee_maker_navigator_2/lib/app/router/view/app_route_information_parser.dart @@ -34,7 +34,7 @@ class AppRouteInformationParser extends RouteInformationParser { const AppRouteInformationParser(); - /// Parses the given [RouteInformation] into an [AppRouteConfiguration]. + /// Step #13: Parse the given [RouteInformation] into an [AppRouteConfiguration]. /// /// This method extracts the URI from the [RouteInformation], identifies the route path using /// [AppRouteUriTemplate.findFromUri], and captures any query parameters. The resulting @@ -51,7 +51,8 @@ class AppRouteInformationParser ); } - /// Restores the [RouteInformation] from a given [AppRouteConfiguration]. + /// Step #19: Restores the [RouteInformation] from a given + /// [AppRouteConfiguration]. /// /// This method converts the application's current navigation state back into a URI, /// which can be used to update the browser's address bar, ensuring consistency between diff --git a/coffee_maker_navigator_2/lib/app/router/view/app_route_observer.dart b/coffee_maker_navigator_2/lib/app/router/view/app_route_observer.dart index 4235d7a1..29b95523 100644 --- a/coffee_maker_navigator_2/lib/app/router/view/app_route_observer.dart +++ b/coffee_maker_navigator_2/lib/app/router/view/app_route_observer.dart @@ -35,6 +35,13 @@ class AppRouteObserver extends RouteObserver> { } } + /// STEP #12: Update the System UI Overlay Style based on the current route. + /// + /// This method updates the system UI overlay style, such as the status bar and navigation bar + /// colors, based on the active route. It checks whether the current route has a bottom + /// navigation bar (like the Orders page) and adjusts the UI elements accordingly to ensure + /// a consistent look and feel throughout the app. This is done using the provided color scheme + /// and helps maintain visual consistency as users navigate between different parts of the app. void _updateSystemUIOverlayStyle(Route route) { SystemUIAnnotationWrapper.setSystemUIOverlayStyle( colorScheme, diff --git a/coffee_maker_navigator_2/lib/app/router/view/app_router_delegate.dart b/coffee_maker_navigator_2/lib/app/router/view/app_router_delegate.dart index 6d4031b9..de93d768 100644 --- a/coffee_maker_navigator_2/lib/app/router/view/app_router_delegate.dart +++ b/coffee_maker_navigator_2/lib/app/router/view/app_router_delegate.dart @@ -45,13 +45,29 @@ class AppRouterDelegate extends RouterDelegate ]).addListener(notifyListeners); } + /// STEP #6: Build the navigation stack. + /// + /// This method is responsible for building the `Navigator` widget, which manages + /// the app's navigation stack. It uses the list of pages provided by the `RouterViewModel` to + /// define what pages are currently displayed. The `Navigator` widget is the View part of MVVM, + /// and it updates automatically whenever the ViewModel (RouterViewModel) changes, ensuring the + /// UI reflects the current navigation state. This connection allows for a reactive and dynamic + /// navigation experience in the app. @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, - pages: routerViewModel.pages - .value, // The list of pages defining the current navigation stack. + pages: routerViewModel.pages.value, onPopPage: (route, result) { + /// STEP #9: Handle the pop page event. + /// + /// This callback is invoked when a [Route] created from a [Page] in the [pages] list is + /// popped. In other words, it handles the scenario where a declaratively added route is + /// removed using an imperative pop call to the [Navigator] widget. For example, the back + /// button in the [AddWaterScreen] can trigger a pop event that removes the screen from the + /// + /// The `RouterViewModel` is notified about the page pop event, allowing it to update and sync + /// the [pages] list accordingly, ensuring that the navigation state remains consistent. routerViewModel.onPagePoppedImperatively(); return route.didPop(result); }, @@ -59,25 +75,37 @@ class AppRouterDelegate extends RouterDelegate ); } - /// Handles pop actions initiated by the operating system (e.g., back gestures or hardware - /// buttons). This method ensures that such interactions are managed consistently with the - /// app's navigation logic. + /// STEP #10: Handle the pop route event. + /// + /// This method manages pop actions initiated by the operating system, such as back gestures or + /// hardware back button presses on devices like Android. It ensures these interactions are + /// handled consistently with the app's navigation logic as defined in the `RouterViewModel`, + /// which tracks and updates the list of pages in the navigation stack. @override Future popRoute() { return routerViewModel.onPagePoppedWithOperatingSystemIntent(); } + /// STEP #17: Get the current route configuration. + /// + /// This method retrieves the current route configuration, which reflects the app's current + /// navigation state. The router widget calls this method whenever it needs to update the + /// browser's URL or sync the app state with the URL. This ensures that the displayed URL + /// accurately represents the current visible screen in the app. @override AppRouteConfiguration get currentConfiguration { - // Returns the current route configuration, used to update the browser's URL - // and keep it in sync with the application's state. return routerViewModel.onUriRestoration(); } + /// STEP #14: Set a new navigation stack for the app configuration. + /// + /// This method updates the navigation stack based on a new route configuration. It is used to handle + /// changes such as URL updates or deep links, ensuring the app responds appropriately to these changes. + /// By parsing the new route configuration and updating the state in the `RouterViewModel`, the app + /// can dynamically adjust its navigation stack to reflect the user's intent, whether it's through + /// direct URL input, deep links, or other routing events. @override Future setNewRoutePath(AppRouteConfiguration configuration) async { - // Updates the navigation stack based on a new route configuration, allowing - // the application to respond to changes such as URL updates or deep links. routerViewModel.onNewUriParsed(configuration); } } diff --git a/coffee_maker_navigator_2/lib/app/router/view_model/router_view_model.dart b/coffee_maker_navigator_2/lib/app/router/view_model/router_view_model.dart index 0d086e6c..02e3bff6 100644 --- a/coffee_maker_navigator_2/lib/app/router/view_model/router_view_model.dart +++ b/coffee_maker_navigator_2/lib/app/router/view_model/router_view_model.dart @@ -66,10 +66,24 @@ class RouterViewModel { ValueListenable get visibleOrderScreenNavBarTab => _visibleOrderScreenNavBarTab; + /// STEP #4: Inject dependencies into the ViewModel's constructor. + /// + /// In this step, the AuthService and OnboardingService are injected into the + /// RouterViewModel through its constructor. These services together represent the Model + /// in the MVVM pattern, providing the data and business logic related to authentication + /// and onboarding. By injecting these services, the ViewModel can interact with the + /// underlying data and logic, allowing it to manage the state of the View. RouterViewModel({ required this.authService, required this.onboardingService, }) { + /// STEP #5: Subscribe to authentication state changes. + /// + /// Here, we listen to changes in the user's authentication state by subscribing + /// to the `authStateListenable` from AuthService. This allows the ViewModel to + /// respond to changes in the Model (AuthService) and update the UI (the View) accordingly. + /// For instance, when the user logs in or out, the ViewModel updates the list of pages to be + /// displayed in the app, ensuring the UI always reflects the current authentication state. authService.authStateListenable.addListener(_authStateChangeSubscription); } @@ -101,6 +115,11 @@ class RouterViewModel { void onDrawerDestinationSelected( AppNavigationDrawerDestination destination, ) { + /// STEP #8: Implement the navigation logic for drawer destinations in the ViewModel. + /// + /// This step defines how the app should respond when a user selects an item from the + /// navigation drawer. Based on the selected destination, the ViewModel updates the + /// page stack to display the appropriate screen(s). switch (destination) { case AppNavigationDrawerDestination.ordersScreen: _pages.value = [OrdersRoutePage(_visibleOrderScreenNavBarTab)]; @@ -152,8 +171,9 @@ class RouterViewModel { } } - /// Handles the update of the routing URL (visible on the Browser address bar) when the app - /// navigation state changes. + /// Step #18: Handles the update of the routing URL (visible on the Browser + /// address bar) when the + /// app navigation state changes. /// /// This method is triggered by changes to either the [pages] list or the /// [visibleOrderScreenNavBarTab]. It constructs a new [AppRouteConfiguration] @@ -171,6 +191,8 @@ class RouterViewModel { ); } + /// Step #15: Respond to the parsing of a new URL route. + /// /// Responds to the parsing of a new URL route by the [RouteInformationParser] and returns sets /// the new navigation stack defined by the [pages] list based on the provided /// [AppRouteConfiguration]. diff --git a/coffee_maker_navigator_2/lib/app/ui/widgets/app_navigation_drawer.dart b/coffee_maker_navigator_2/lib/app/ui/widgets/app_navigation_drawer.dart index 9d6de14f..d344b23a 100644 --- a/coffee_maker_navigator_2/lib/app/ui/widgets/app_navigation_drawer.dart +++ b/coffee_maker_navigator_2/lib/app/ui/widgets/app_navigation_drawer.dart @@ -5,8 +5,7 @@ enum AppNavigationDrawerDestination { ordersScreen(label: Text("Orders"), icon: Icon(Icons.coffee)), tutorialsScreen( label: Text("Tutorials"), icon: Icon(Icons.menu_book_outlined)), - logOut(label: Text("Log out"), icon: Icon(Icons.logout)), - ; + logOut(label: Text("Log out"), icon: Icon(Icons.logout)); const AppNavigationDrawerDestination({ required this.icon, @@ -37,6 +36,13 @@ class AppNavigationDrawer extends StatelessWidget { @override Widget build(BuildContext context) { + /// STEP #11: Handle the back button press event. + /// + /// This callback manages back button presses from the operating system (e.g., gestures or hardware + /// buttons on Android) in this particular case, when the navigation drawer is open. Instead + /// of leaving the screen, it closes the drawer and returns `true` to indicate the event is + /// handled locally. If the drawer is not open, it returns `false`, allowing + /// the Router widget's `RootBackButtonDispatcher` content to handle the event. return BackButtonListener( onBackButtonPressed: () async { final scaffold = Scaffold.maybeOf(context); @@ -47,7 +53,6 @@ class AppNavigationDrawer extends StatelessWidget { return true; } - // If view is not open, return false to indicate that // the router should handle this. return false; diff --git a/coffee_maker_navigator_2/lib/main.dart b/coffee_maker_navigator_2/lib/main.dart index b6ba529d..17d2862e 100644 --- a/coffee_maker_navigator_2/lib/main.dart +++ b/coffee_maker_navigator_2/lib/main.dart @@ -6,7 +6,8 @@ import 'package:coffee_maker_navigator_2/app/di/coffee_maker_app_level_dependenc import 'package:coffee_maker_navigator_2/features/orders/di/orders_dependency_container.dart'; import 'package:flutter/material.dart'; -void _registerDependencyContainerFactories(DependencyContainerManager manager) { +void _registerFeatureLevelDependencyContainers( + DependencyContainerManager manager) { manager ..registerContainerFactory( () => OrdersDependencyContainer()) @@ -25,13 +26,13 @@ void _registerDependencyContainerFactories(DependencyContainerManager manager) { | | | AuthService | | | | | DependencyInjector | | | | +-------------------------+ | | | | Widget | | | | | AuthRepository | | | | | +-----------------------+ | | -| | +-------------------------+ | | | | | FeatureLevelDependency| | | -| | | AuthRemoteDataSource | | | | | | Container | | | -| | +-------------------------+ | | | | | +-------------------+ | | | -| | | RouterViewModel | | | | | | | Feature | | | | -| | +-------------------------+ | | | | | | Screen Widget | | | | -| +-----------------------------+ | | | | | | | | | -| | | | | +-------------------+ | | | +| | +-------------------------+ | | | | | | | | +| | | AuthRemoteDataSource | | | | | | | | | +| | +-------------------------+ | | | | | | | | +| | | RouterViewModel | | | | | | Feature | | | +| | +-------------------------+ | | | | | Screen Widget | | | +| +-----------------------------+ | | | | | | | +| | | | | | | | | +-----------------------------+ | | | | | | | | | OrdersDependencyContainer | | | | +-----------------------+ | | | | +-------------------------+ | | | +-----------------------------+ | @@ -57,9 +58,26 @@ void _registerDependencyContainerFactories(DependencyContainerManager manager) { */ Future main() async { WidgetsFlutterBinding.ensureInitialized(); + + /// STEP #1: Initialize the dependency container manager with the App-level dependency container. + /// + /// The app-level dependency container is responsible for managing the dependencies used + /// as long as app is alive. These dependencies can be shared across multiple feature level + /// dependency containers. It is the only container that is initialized asynchronously. final dependencyContainerManager = DependencyContainerManager.instance; await dependencyContainerManager .init(CoffeeMakerAppLevelDependencyContainer()); - _registerDependencyContainerFactories(dependencyContainerManager); + + /// STEP #2: Register feature-level dependency containers. + /// + /// Here, we register dependency containers for specific features, like Orders, AddWater, and + /// LoginScreen with the `DependencyContainerManager`. Each feature has its own container to + /// manage its group of dependencies. + /// + /// This uses a Service Locator pattern, where dependencies are registered and retrieved as needed. + /// Additionally, the `DependencyContainerManager` automatically disposes of containers that + /// are no longer needed and have no active subscribers, helping manage resources efficiently. + _registerFeatureLevelDependencyContainers(dependencyContainerManager); + runApp(const CoffeeMakerApp()); }