Skip to content

Commit

Permalink
Merge pull request #318 from woltapp/improve-coffee-majer-documentation
Browse files Browse the repository at this point in the history
[Demo app] Improve coffee maker demo documentation for Flutter & Friends workshop
  • Loading branch information
ulusoyca authored Sep 3, 2024
2 parents 961072b + d6d7777 commit 65721b3
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 39 deletions.
6 changes: 6 additions & 0 deletions coffee_maker_navigator_2/lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends Page<T> {
const AppRoutePage({LocalKey? key}) : super(key: key);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class AppRouteInformationParser
extends RouteInformationParser<AppRouteConfiguration> {
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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class AppRouteObserver extends RouteObserver<PageRoute<void>> {
}
}

/// 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<void> route) {
SystemUIAnnotationWrapper.setSystemUIOverlayStyle(
colorScheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,67 @@ class AppRouterDelegate extends RouterDelegate<AppRouteConfiguration>
]).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);
},
observers: [AppRouteObserver(Theme.of(context).colorScheme)],
);
}

/// 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<bool> 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<void> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,24 @@ class RouterViewModel {
ValueListenable<CoffeeMakerStep> 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);
}

Expand Down Expand Up @@ -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)];
Expand Down Expand Up @@ -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]
Expand All @@ -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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
36 changes: 27 additions & 9 deletions coffee_maker_navigator_2/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
() => OrdersDependencyContainer())
Expand All @@ -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 | | | | +-----------------------+ | |
| | +-------------------------+ | | | +-----------------------------+ |
Expand All @@ -57,9 +58,26 @@ void _registerDependencyContainerFactories(DependencyContainerManager manager) {
*/
Future<void> 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());
}

0 comments on commit 65721b3

Please sign in to comment.