diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f46add..d686f57 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,6 @@ jobs:
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.3.8'
channel: 'stable'
- name: Install dependencies
diff --git a/.github/workflows/fix-typos.yml b/.github/workflows/fix-typos.yml
new file mode 100644
index 0000000..84590bc
--- /dev/null
+++ b/.github/workflows/fix-typos.yml
@@ -0,0 +1,19 @@
+name: Automatically fix typos
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: main
+ - uses: sobolevn/misspell-fixer-action@master
+ - uses: peter-evans/create-pull-request@v4.2.0
+ env:
+ ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5f0411d..66027d2 100644
--- a/README.md
+++ b/README.md
@@ -25,14 +25,36 @@ building a navigation menu in `Flutter`.
- [3. Creating pages to navigate to](#3-creating-pages-to-navigate-to)
- [4. Adding the navigation menu](#4-adding-the-navigation-menu)
- [4.1 Using the `drawer` attribute in `Scaffold`](#41-using-the-drawer-attribute-in-scaffold)
- - [4.1.1 Creating menu](#411-creating-menu)
- - [4.1.2 Run it!](#412-run-it)
+ - [4.1.1 Creating menu](#411-creating-menu)
+ - [4.1.2 Run it!](#412-run-it)
- [4.2 Slider menu with animation](#42-slider-menu-with-animation)
- [4.2.1 Simplify `HomePage` and `App` class](#421-simplify-homepage-and-app-class)
- [4.2.2 Creating `AnimationController`](#422-creating-animationcontroller)
- [4.2.3 Making our animations *work* with `AnimatedBuilder`](#423-making-our-animations-work-with-animatedbuilder)
- [4.2.4 Creating `SlidingMenu`](#424-creating-slidingmenu)
- [4.2.5 Run the app!](#425-run-the-app)
+ - [5. Adding a dynamic section to the menu](#5-adding-a-dynamic-section-to-the-menu)
+ - [5.1 How the `JSON` will look like](#51-how-the-json-will-look-like)
+ - [5.2 Dealing with information from the `JSON` file](#52-dealing-with-information-from-the-json-file)
+ - [5.2.1 `MenuItemInfo` class](#521-menuiteminfo-class)
+ - [5.2.2 Loading menu items from `JSON` file/local storage](#522-loading-menu-items-from-json-filelocal-storage)
+ - [5.2.3 Updating menu items](#523-updating-menu-items)
+ - [5.2.4 Updating menu items](#524-updating-menu-items)
+ - [5.3 Using loaded `JSON` data in menu](#53-using-loaded-json-data-in-menu)
+ - [5.4 Displaying menu items](#54-displaying-menu-items)
+ - [5.5 Reordering items](#55-reordering-items)
+ - [5.6 Adding text customization](#56-adding-text-customization)
+ - [5.7 Adding icons to menu items](#57-adding-icons-to-menu-items)
+ - [6. Adding `i18n` to our app](#6-adding-i18n-to-our-app)
+ - [6.1 Adding supported languages and delegates](#61-adding-supported-languages-and-delegates)
+ - [6.2 Creating our custom delegate for localizing our own labels](#62-creating-our-custom-delegate-for-localizing-our-own-labels)
+ - [6.3 Changing labels according to locale](#63-changing-labels-according-to-locale)
+ - [6.3.1 A note on `i18n` on dynamic menus](#631-a-note-on-i18n-on-dynamic-menus)
+ - [6.3.1.1 Translations in `assets/menu_items.json`](#6311-translations-in-assetsmenu_itemsjson)
+ - [6.3.1.2 Lookup automatic translations of dynamic menu items](#6312-lookup-automatic-translations-of-dynamic-menu-items)
+ - [6.4 Using `Riverpod` to toggle between languages](#64-using-riverpod-to-toggle-between-languages)
+ - [7. Fixing decoration when expanding titles](#7-fixing-decoration-when-expanding-titles)
+ - [8. Adding basic navigation for each menu item](#8-adding-basic-navigation-for-each-menu-item)
- [Star the repo! ⭐️](#star-the-repo-️)
@@ -139,7 +161,7 @@ checking a `Todo` item as *complete*.
The design we're doing should
look like the following.
-
+
@@ -152,7 +174,7 @@ Regardless, the following constraints ought to be considered:
- we will adopt a
[**Progressive UI**](https://github.com/dwyl/product-roadmap/issues/43)
approach,
-where the user will only
+where the person will only
be shown the option to open the menu
*after doing a specific action*
(in this case, completing a simple
@@ -418,7 +440,7 @@ After this,
we are going to be adding
a `showMenu` variable in `_HomePageState`,
a flag that will let us know if we should show
-the option for the user to open the menu.
+the option for the person to open the menu.
```dart
class _HomePageState extends State {
@@ -611,7 +633,7 @@ Let's do it!
In the wireframe,
the menu currently has three items
-that the user can click
+that the person can click
to navigate into the referring page:
- the **Todo List**
- the **Feature Tour** page
@@ -702,7 +724,7 @@ class SettingsPage extends StatelessWidget {
Both pages are very similar.
They have some text
-and a button that will allow the user
+and a button that will allow the person
to **navigate back**.
These pages will be later used
@@ -761,7 +783,7 @@ add this key to the
We've also disabled `drawerEnableOpenDragGesture`,
so the `Drawer` isn't opened with the right-to-left gesture,
-so the user *has* to click the button to open the menu.
+so the person *has* to click the button to open the menu.
In the `IconButton`,
in the `actions` attribute of the `Scaffold`,
@@ -798,7 +820,7 @@ go from **right-to-left**,
not the other way around,
which is what `drawer` does.
-## 4.1.1 Creating menu
+### 4.1.1 Creating menu
In the previous section
we've used `DrawerMenu()`,
@@ -912,7 +934,7 @@ The menu is essentially consisted of an
`AppBar` and `ListView`,
with many `ListTile` children,
each one pertaining to the different page
-the user can navigate into.
+the person can navigate into.
The `AppBar` is similar to the one
found in the `_HomePageState` class.
@@ -925,7 +947,7 @@ Each item uses a `Navigator.push()` function
to navigate to the pages
defined in `lib/pages.dart`.
-## 4.1.2 Run it!
+### 4.1.2 Run it!
And that's it!
Wasn't it easy?
@@ -1356,6 +1378,2159 @@ and you should see the following result!
![final_sliding](https://user-images.githubusercontent.com/17494745/219359834-a4f7962b-9300-4b91-9f62-60d9d51952ab.gif)
+
+## 5. Adding a dynamic section to the menu
+
+You now have working menu!
+But what if we want to make it **dynamic**
+by reading contents from a `JSON` file
+and persisting it on local storage?
+
+This is what we are going to be focusing on
+for the next section.
+
+Before this,
+let's make some preparations:
+
+- let's move the `sliding_main.dart` and `sliding_menu.dart`
+files to a folder called `alt`.
+This folder is localed in `lib`, making it `lib/alt`.
+We're doing this because we are going to be using
+the `Drawer` menu, so we'll just tidy up our workspace.
+
+- make sure that in your `main.dart`,
+you're calling the app like so.
+
+```dart
+void main() {
+ runApp(const App());
+}
+```
+
+- install the
+[`shared_preferences`](https://pub.dev/packages/shared_preferences) package.
+This will make it easy for us to save
+stuff in the device's local storage!
+
+- open `pubspec.yaml`
+and add `- assets/` to the `assets:` section.
+
+```yml
+ assets:
+ - assets/images/
+ - assets/
+```
+
+And now you're ready!
+
+### 5.1 How the `JSON` will look like
+
+Let's start with an initial view
+of how the `JSON` file will look like.
+We are assuming we are going to have
+**nested menus up to 3 levels deep**.
+For each `Menu Item` we will need:
+- a `title`.
+- an `id`.
+- a field `index_in_level`
+referring to the index of the menu item within the level.
+- a `tiles` field,
+pertaining to the child `menu items`/`tiles` of it.
+
+If you want to see how the file should look like,
+do check [`assets/menu_items.json`](https://github.com/dwyl/flutter-navigation-menu-demo/blob/3cb94b701377889cc7243a0aba9b4facf67314ab/assets/menu_items.json).
+
+
+### 5.2 Dealing with information from the `JSON` file
+
+Let's create a file called `settings.dart` inside `lib`.
+In this file we will create functions
+that will load the information from the `JSON` file,
+save it in the device's local storage
+and update it accordingly.
+
+#### 5.2.1 `MenuItemInfo` class
+
+In this file we will create a class called `MenuItemInfo`.
+This is the class that will represent
+each menu item that is loaded from the `JSON` file.
+
+Open `lib/settings.dart`
+and add the following code to it.
+
+```dart
+/// Class holding the information of the tile
+class MenuItemInfo {
+ late int id;
+ late int indexInLevel;
+ late String title;
+ late List tiles;
+
+ MenuItemInfo({required this.id, required this.title, this.tiles = const []});
+
+ /// Converts `json` text to BasicTile
+ MenuItemInfo.fromJson(Map json) {
+ id = json['id'];
+ indexInLevel = json['index_in_level'];
+ title = json['title'];
+ if (json['tiles'] != null) {
+ tiles = [];
+ json['tiles'].forEach((v) {
+ tiles.add(MenuItemInfo.fromJson(v));
+ });
+ }
+ }
+
+ Map toJson() {
+ final Map data = {};
+ data['id'] = id;
+ data['index_in_level'] = indexInLevel;
+ data['title'] = title;
+ if (tiles.isNotEmpty) {
+ data['tiles'] = tiles.map((v) => v.toJson()).toList();
+ } else {
+ data['tiles'] = [];
+ }
+ return data;
+ }
+}
+```
+
+We are creating a class field
+for each key of the object within the `JSON` file.
+The functions `fromJson` and `toJson`
+convert the information from the `JSON` file
+into a `MenuItemInfo` and decode into a `json` string,
+respectively.
+
+Awesome! 🎉
+
+
+#### 5.2.2 Loading menu items from `JSON` file/local storage
+
+Now that we have our own class,
+let's create a function to load these menu items
+from the file!
+
+In the same `settings.dart` file,
+create the following function.
+
+```dart
+const jsonFilePath = 'assets/menu_items.json';
+const storageKey = 'menuItems';
+
+Future> loadMenuItems() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+
+ final String? jsonStringFromLocalStorage = prefs.getString(storageKey);
+
+ String jsonString;
+ // If local storage has content, return it.
+ if (jsonStringFromLocalStorage != null) {
+ jsonString = jsonStringFromLocalStorage;
+ }
+
+ // If not, we initialize it
+ else {
+ // Setting local storage key with json string from file
+ final String jsonStringFromFile = await rootBundle.loadString(jsonFilePath);
+ prefs.setString(storageKey, jsonStringFromFile);
+
+ jsonString = jsonStringFromFile;
+ }
+
+ // Converting json to list of MenuItemInfo objects
+ List data = await json.decode(jsonString);
+ final List menuItems = data.map((obj) => MenuItemInfo.fromJson(obj)).toList();
+
+ // Return the MenuItemInfo list
+ return menuItems;
+}
+```
+
+In our application,
+we will persist the `JSON` file string
+into the device's local storage
+and update it accordingly.
+With this in mind,
+in the beginning of this function
+we check if there is any
+`JSON` string in the device's local storage.
+
+If **the `JSON` string is saved into our local storage**,
+we simply use it to later decode it
+into a list of `MenuItemInfo` (class we've created previously).
+If **the `JSON` string is not saved into our local storage**,
+we fetch it from the `assets/menu_items.json` file
+and then later decode it in a similar fashion.
+
+This function returns
+a list of `MenuItemInfo`.
+
+
+#### 5.2.3 Updating menu items
+
+If the person wants to reorder the menu items,
+we need to update these changes into our local storage
+so it's always up-to-date and reflects the true state
+of the list.
+
+When a person reorders a menu item in any level except the root,
+we update the `tiles` list field (which pertains to the children menu items)
+of the parent.
+
+> **Note**
+>
+> Reordering *root menu items* is much easier
+> because we don't need to traverse the tree of menu items.
+> We just need to update the indexes of each men item on root level.
+
+![update_example](https://user-images.githubusercontent.com/17494745/236282208-2160b55f-947f-4e03-80b4-3cbba4c02a14.png)
+
+Since we are importing information from the `JSON` file,
+we don't know upfront how many levels the nested menu has.
+Therefore,
+we need a way to **traverse it**.
+
+According to the image,
+we traverse the tree of menu items
+until we find the menu item with the given `id`.
+After the menu item is found,
+we update its children with the reordered list.
+
+Let's create a
+[recursive](https://en.wikipedia.org/wiki/Recursion_(computer_science))
+function that will traverse the tree
+of menu items and update
+a menu item with a given `id`.
+
+Inside the same file `settings.dart`,
+add the following function.
+
+```dart
+MenuItemInfo? _findAndUpdateMenuItem(MenuItemInfo item, int id, List updatedChildren) {
+ // Breaking case
+ if (item.id == id) {
+ item.tiles = updatedChildren;
+ return item;
+ }
+
+ // Continue searching
+ else {
+ final children = item.tiles;
+ MenuItemInfo? ret;
+ for (MenuItemInfo child in children) {
+ ret = _findAndUpdateMenuItem(child, id, updatedChildren);
+ if (ret != null) {
+ break;
+ }
+ }
+ return ret;
+ }
+}
+```
+
+This function `_findAndUpdateMenuItem` receives
+the `id` of the menu item we want to update the children of
+and the `updatedChildren` list of menu items.
+The function recursively traverses the tree
+until it finds the menu item with the `id`.
+When it does,
+it updates it and stops traversing.
+
+After execution,
+this function returns the updated menu item.
+
+This function will be *extremely* useful
+to update menu item list at any level.
+
+Let's use it!
+
+
+#### 5.2.4 Updating menu items
+
+We are going to have widgets
+that will render each menu item.
+
+We are going to have two "types" of menu items:
+- **root menu items**, self-explanatory.
+- **_n-th_ level menu item**,
+which is nested from the second level upwards.
+
+*Because* we can have multiple root menu items,
+we need to create two functions to update menu items:
+
+- for *root items*,
+we simply receive the reordered root menu item list
+and update our local storage.
+- for *nested menu items*,
+we iterate over each root menu item
+and recursively try to find the `id` of the menu item
+to update its children.
+We then save the updated list to local storage.
+
+Let's implement these functions!
+
+In `settings.dart`,
+add the next two functions:
+
+```dart
+/// Updates the root menu item list [menuItems] in shared preferences.
+updateRootObjectsInPreferences(List menuItems) async {
+ final jsonString = json.encode(menuItems);
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ prefs.setString(storageKey, jsonString);
+}
+
+/// Update deeply nested menu item [item] with a new [updatedChildren] in shared preferences.
+updateDeeplyNestedObjectInPreferences(MenuItemInfo itemToUpdate, List updatedChildren) async {
+ // Fetch the menu items from `.json` file
+ List menuItems = await loadMenuItems();
+
+ // Go over the root items list and find & update the object with new children
+ MenuItemInfo? updatedItem;
+ for (var item in menuItems) {
+ updatedItem = _findAndUpdateMenuItem(item, itemToUpdate.id, updatedChildren);
+ if (updatedItem != null) {
+ break;
+ }
+ }
+
+ // Saving updated menu items encoded to json string.
+ final jsonString = json.encode(menuItems);
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ prefs.setString(storageKey, jsonString);
+}
+```
+
+`updateRootObjectsInPreferences` receives the reordered menu item list.
+It simply saves the updated list to the local storage.
+On the other hand, `updateDeeplyNestedObjectInPreferences`
+receives the item to update and the reordered children list.
+Inside this latter function,
+we go over each root menu item and traverse down the tree
+to update the menu item's children.
+After this, similarly to the previous function,
+the updated menu item list is saved to local storage.
+
+We are going to be using these handful functions *later*
+when we are rendering these menu items!
+
+> e.g. [`lib/settings.dart`](https://github.com/dwyl/flutter-navigation-menu-demo/blob/0630989f1d6f849c7f78dab0d1e8eb632299898c/lib/settings.dart)
+
+> **Note**
+>
+> We didn't create an utils class like you can do in other languages.
+> For this class to be statically accessed,
+> it would *only have static members*.
+>
+> In Flutter, [we should avoid having classes with only static members.](https://dart-lang.github.io/linter/lints/avoid_classes_with_only_static_members.html).
+> Luckily, Dart allows functions to exist outside of classes
+> for this very reason.
+
+
+### 5.3 Using loaded `JSON` data in menu
+
+Let's call the `loadMenuItems()` function
+we've defined in `settings.dart`
+in `menu.dart`.
+Everytime the menu is opened,
+we are going to load the menu items
+and list them accordingly
+in the drawer menu.
+
+Open `menu.dart`.
+We are going to convert the
+`DrawerMenu` class into a *stateful widget*.
+
+```dart
+class DrawerMenu extends StatefulWidget {
+ const DrawerMenu({super.key});
+
+ @override
+ State createState() => _DrawerMenuState();
+}
+
+class _DrawerMenuState extends State {
+ late Future> menuItems;
+
+ @override
+ void initState() {
+ super.initState();
+ menuItems = loadMenuItems();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // ...
+ }
+}
+```
+
+By converting this widget into a stateful
+widget.
+The `loadMenuitems()` function
+is used in the `initState()` overridden function
+to fetch the menu items from the `json` or local storage
+whenever the menu is mounted.
+
+These `menuItems` are going to be used in the `build()` function.
+Speaking of which, we are going to change this method now!
+
+First, let's wrap the widgets
+inside the `body:` paramater
+with a `Column` and `Expanded` widget,
+making it like so:
+
+```dart
+body: Column(
+ children: [
+ Expanded(
+ child: Container(
+ color: Colors.black,
+ child: ListView(key: todoTileKey, padding: const EdgeInsets.only(top: 32), children: [
+ //...
+ ]
+ )
+ )
+ )
+ ]
+)
+```
+
+This is needed because
+we don't know how *much height* the dynamic menu will
+have within the drawer menu.
+Hence why we use `Expanded` to expand
+the contents as necessary.
+
+Inside the `ListView`,
+we have an array of children where some menu items
+were created (Feature Tour, Settings).
+We are going to add the dynamic menu *below* these.
+Add the following piece of code at the end of the array.
+
+```dart
+Container(
+color: Colors.black,
+child: FutureBuilder>(
+ future: menuItems,
+ builder: (BuildContext context, AsyncSnapshot> snapshot) {
+ // If the data is correctly loaded,
+ // we render a `ReorderableListView` whose children are `MenuItem` tiles.
+ if (snapshot.hasData) {
+ List menuItemInfoList = snapshot.data!;
+
+ return DrawerMenuTilesList(key: dynamicMenuItemListKey, menuItemInfoList: menuItemInfoList);
+ }
+
+ // While it's not loaded (error or waiting)
+ else {
+ return const SizedBox.shrink();
+ }
+ }))
+```
+
+> e.g. [`lib/main.dart`](https://github.com/dwyl/flutter-navigation-menu-demo/blob/0630989f1d6f849c7f78dab0d1e8eb632299898c/lib/main.dart)
+
+
+Because `loadItems()` is an *asynchronous operations*,
+we have to wait for it to conclude to properly display the menu items.
+For this, we are using the
+[`FutureBuilder`](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html)
+class to handle the possible states
+of the `Future` class variable that `loadItems()` returns.
+
+We can generally display a loading button
+when fetching the menu items
+(for example, it fetches the menu items from an API).
+However, to keep it simple here,
+we will only render the menu items
+if they are correctly fetched (`snapshot.hasData`).
+If not, we don't render anything.
+
+Here, we are rendering a class
+called `DynamicMenuTilesList`.
+We haven't created it yet.
+Let's do that!
+
+Inside `lib`,
+create a file called `dynamic_menu.dart`
+and create a simple class.
+
+```dart
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+
+import 'settings.dart';
+
+
+// Widget with the list of Menu Item tiles
+class DynamicMenuTilesList extends StatefulWidget {
+ final List menuItemInfoList;
+
+ const DynamicMenuTilesList({super.key, required this.menuItemInfoList});
+
+ @override
+ State createState() => _DynamicMenuTilesListState();
+}
+
+class _DynamicMenuTilesListState extends State {
+ late List menuItemInfoList;
+
+ @override
+ void initState() {
+ super.initState();
+ menuItemInfoList = widget.menuItemInfoList;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+```
+
+This class simply receives
+the list of menu items that were loaded
+from the local storage
+and uses it in its state.
+For now, let's just render a
+simple `Container` so we know the project *builds*.
+
+If we run our app,
+we should see everything still looks the same.
+After all, we're not rendering our dynamic menu items *yet*.
+
+
+
+
+
+> **Note**
+>
+> We've removed the
+> `margin: const EdgeInsets.only(top: 100),`
+> in the second container
+> of the `ListView` children array
+> just so we can see the dynamic menu better
+> without having to scroll.
+
+
+### 5.4 Displaying menu items
+
+Now let's get to the bread and butter
+of this whole section:
+*dispaying our menu items*.
+
+In `lib/dynamic_menu.dart`,
+locate the `build()` function
+in the `_DynamicMenuTilesListState` class
+and change it like so:
+
+```dart
+ Widget build(BuildContext context) {
+ return ReorderableListView(
+ padding: const EdgeInsets.only(top: 32),
+ onReorder: (oldIndex, newIndex) {},
+ children: menuItemInfoList
+ .map(
+ (tile) => MenuItem(key: ValueKey(tile.id), info: tile),
+ )
+ .toList()
+ ..sort((a, b) => a.info.indexInLevel.compareTo(b.info.indexInLevel)));
+ }
+```
+
+We are rendering a
+[`ReorderableListView`](https://api.flutter.dev/flutter/material/ReorderableListView-class.html)
+which, in turn,
+renders a list of `MenuItem`s
+(don't worry, we'll create this class right away).
+
+Since `DynamicMenuTilesList` receives a list
+of `MenuItemInfo`,
+we use `indexInLevel` to sort it by the index
+that is defined in the `JSON` file/local storage.
+
+Essentially,
+`DynamicMenuTilesList`
+**is rendering the root menu items**.
+Nested menu items will be rendered
+in the `MenuItem` class.
+
+This `MenuItem` class
+receives a `key`
+and the `MenuItemInfo` object.
+
+Let's create `MenuItem` right now!
+
+In the same file,
+create the stateful widget `MenuItem`.
+
+```dart
+/// Widget that expands if there are child tiles or not.
+class MenuItem extends StatefulWidget {
+ final Key key;
+ final MenuItemInfo info;
+ final double leftPadding;
+
+ const MenuItem({required this.key, required this.info, this.leftPadding = 16}) : super(key: key);
+
+ @override
+ State