From 90a8692e9bf92fe874c8f2d17b2bd18b2eda9dc8 Mon Sep 17 00:00:00 2001 From: guilherme Date: Wed, 28 Apr 2021 19:13:56 -0300 Subject: [PATCH 1/9] doc: Improve README --- README.md | 134 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d47d22f..395e402 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,40 @@ -# APS Navigator +# APS Navigator - App Pagination System +[![build](https://github.com/guilherme-v/aps_navigator/workflows/ci.yml/badge.svg)](https://github.com/guilherme-v/aps_navigator/actions) [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) +[![codecov](https://codecov.io/gh/guilherme-v/aps_navigator/branch/develop/graph/badge.svg)](https://codecov.io/gh/guilherme-v/aps_navigator) -This library is just a wrapper around Navigator 2.0 and Router/Pages API to make their use a little easier. +This library is just a wrapper around Navigator 2.0 and Router/Pages API that tries to make their use easier: -## Basic feature set +## :wrench: Basic feature set -- Web support - back/forward buttons, URL updates, recover app state from web history. -- Gives you control of Route Stack - add/remove Pages at the Middle, add multiples Pages at once, remove a range of pages at once. -- Handles Operational System events. -- Internal Navigators. - -## Overview +:rowboat: What we've tried to achieve: + +- Simple API +- Easy setup +- Minimal amount of "new classes types" to learn: + - No need to extend(or implement) anything +- Web support (check the images in the following sections): + - Back/Forward buttons + - Dynamic URLs + - Static URLs + - Recover app state from web history +- Control of Route Stack: + - Add/remove Pages at a specific position + - Add multiples Pages at once + - Remove a range of pages at once +- Handles Operational System events +- Internal(Nested) Navigators + +:warning: What we didn't try to achieve: + +- To use code generation + - Don't get me wrong. Code generation is a fantastic technique that makes code clear and coding faster - we have great libraries that are reference in the community and use it + - The thing is: It doesn't seems natural to me have to use this kind of procedure for something "basic" as navigation +- To use Strongly-typed arguments passing + +## :eyes: Overview ### 1 - Create the Navigator and define the routes: @@ -25,7 +47,7 @@ final navigator = APSNavigator.from( ); ``` -### 2 - Configure MaterialApp to use it: +### 2 - Configure MaterialApp to use it: ```dart class MyApp extends StatelessWidget { @@ -41,7 +63,7 @@ class MyApp extends StatelessWidget { } ``` -### 3 - Create the widget Page: +### 3 - Create the widget Page (route): ```dart class DynamicURLPage extends StatefulWidget { @@ -51,8 +73,7 @@ class DynamicURLPage extends StatefulWidget { @override _DynamicURLPageState createState() => _DynamicURLPageState(); - // You don't need to use a static function as Builder, - // but it seems to be a good way to organize things + // Builder function static Page route(RouteData data) { final tab = data.values['tab'] == 'books' ? 0 : 1; return MaterialPage( @@ -63,7 +84,11 @@ class DynamicURLPage extends StatefulWidget { } ``` -### 4 - Navigate to the page: +- You don't need to use a static function as PageBuilder, but it seems to be a good way to organize things. +- Important: **AVOID** using '**const**' keyword at `MaterialPage` or `DynamicURLPage` levels, or Pop may not work correctly with Web History. +- Important: **Always** include a Key. + +### 4 - Navigate to it: ```dart APSNavigator.of(context).push( @@ -73,10 +98,11 @@ class DynamicURLPage extends StatefulWidget { ``` - The browser's address bar will display: `/dynamic_url_example?tab=books`. -- The navigator will create the Page and put it at the top of the Route Stack. -- The following sections describe better the above steps. +- The `Page` will be created and put at the top of the Route Stack. + +The following sections describe better the above steps. -## Usage +## :massage: Usage ### 1 - Creating the Navigator and defining the Routes: @@ -94,7 +120,8 @@ final navigator = APSNavigator.from( '/static_url_example': PageBuilder.., // Defines the location (and queries): '/dynamic_url_example?tab=(tab_value)&other=(other_value)' - '/dynamic_url_example{?tab,other}': PageBuilder.., // Important: Notice that the '?' is only included once + // Important: Notice that the '?' is only + '/dynamic_url_example{?tab,other}': PageBuilder.., // Defines the location (and path variables): '/posts' and '/posts/(post_id_value)' '/posts': PageBuilder.., @@ -109,19 +136,19 @@ final navigator = APSNavigator.from( ); ``` -`Routes` is just a map between `Templates` and `Page Builders`: +`routes` is just a map between `Templates` and `Page Builders`: -- `Templates` are simple strings with predefined markers to Path ({a}) and Query({?a,b,c..}) values. -- `Page Builders` are functions that return a Page and receive a `RouteData`. Check the section 3 bellow. +- :postbox: `Templates` are simple strings with predefined markers to Path (`{a}`) and Query(`{?a,b,c..}`) values. +- :house: `Page Builders` are plain functions that return a `Page` and receive a `RouteData`. Check the section 3 bellow. Given the configuration above, the app will open at: `/dynamic_url_example?tab=1`. ### 2 - Configure MaterialApp: -After creating a Navigator: +After creating a Navigator, we need to set it up to be used: -- Set it as `MaterialApp.router.routeDelegate`. -- Remember to also add the `MaterialApp.router.routeInformationParser`: +- :one: Set it as `MaterialApp.router.routeDelegate`. +- :two: Remember to also add the `MaterialApp.router.routeInformationParser`: ```dart class MyApp extends StatelessWidget { @@ -137,17 +164,17 @@ class MyApp extends StatelessWidget { } ``` -### 3 - Creating the widget Page: +### 3 - Creating the widget Page(route): -When building a route: +When building a `Page`: -- The library matches address `templates` with the current address. E.g.: - - Template: `/dynamic_url_example/{id}{?tab,other}'` - - Address: `/dynamic_url_example/10?tab=1&other=abc` -- All *paths* and *queries* values are extracted and included in a `RouteData.data` instance. E.g.: +- :one: The library tries to match the address `templates` with the current address. E.g.: + - :postbox: Template: `/dynamic_url_example/{id}{?tab,other}'` + - :house: Address: `/dynamic_url_example/10?tab=1&other=abc` +- :two: All *paths* and *queries* values are extracted and included in a `RouteData.data` instance. E.g.: - `{'id': '10', 'tab': '1', 'other': 'abc'}` -- This istance is passed as param to the `PageBuilder` function - `static Page route(RouteData data)`... -- A new Page instance is created and included at the Route Stack - you check that easily using the dev tools. +- :three: This istance is passed as param to the `PageBuilder` function - `static Page route(RouteData data)`... +- :four: A new Page instance is created and included at the Route Stack - you check that easily using the dev tools. ```dart class DynamicURLPage extends StatefulWidget { @@ -169,14 +196,14 @@ class DynamicURLPage extends StatefulWidget { } ``` -### 4 - Navigating to the pages: +### 4 - Navigating to Pages: Example Link: [All Navigating Examples](https://github.com/guilherme-v/aps_navigator/blob/develop/example/lib/pages/home_page.dart) -4.1 - To navigate to a route with queries variables: +4.1 - To navigate to a route with **queries variables**: -- Template: `/dynamic_url_example{?tab,other}` -- Address: `/dynamic_url_example?tab=books&other=abc` +- :postbox: Template: `/dynamic_url_example{?tab,other}` +- :house: Address: `/dynamic_url_example?tab=books&other=abc` ```dart APSNavigator.of(context).push( @@ -185,10 +212,10 @@ Example Link: [All Navigating Examples](https://github.com/guilherme-v/aps_navig ); ``` -4.2 - To navigate to a route with path variables: +4.2 - To navigate to a route with **path variables**: -- Template: `/posts/{post_id}` -- Address: `/posts/10` +- :postbox: Template: `/posts/{post_id}` +- :house: Address: `/posts/10` ```dart APSNavigator.of(context).push( @@ -196,10 +223,10 @@ Example Link: [All Navigating Examples](https://github.com/guilherme-v/aps_navig ); ``` -You can also include params that aren't used as queries variables: +4.3 - You can also include params that **aren't** used as queries variables: -- Template: `/static_url_example` -- Address: `/static_url_example` +- :postbox: Template: `/static_url_example` +- :house: Address: `/static_url_example` ```dart APSNavigator.of(context).push( @@ -210,7 +237,7 @@ You can also include params that aren't used as queries variables: --- -## Details +## :wine_glass: Details ### 1. Dynamic URLs Example @@ -244,7 +271,7 @@ When using dynamic URLs, changing the app's state also changes the browser's URL } ``` -What is important to know: +:sleepy: What is important to know: - Current limitation: Any value used at URL must be saved as `string`. - Don't forget to include a `Key` on the `Page` created by the `PageBuilder` to everything works properly. @@ -280,7 +307,7 @@ When using static URLs, changing the app's state doesn't change the browser's UR } ``` -What is important to know: +:sleepy: What is important to know: - Don't forget to include a `Key` on the `Page` created by the `PageBuilder` to everything works properly. @@ -306,7 +333,7 @@ Pop returning the data: APSNavigator.of(context).pop('Do!'); ``` -What is important to know: +:sleepy: What is important to know: - Data will only be returned once. - In case of user navigate your app and back again using the browser's history, the result will be returned at `didUpdateWidget` method as `result,` instead of `await` call. @@ -345,7 +372,7 @@ Push a list of the Pages at once: In the example above `ApsPushParam(path: '/multi_push', params: {'number': 4}),` will be the new top. -What is important to know: +:sleepy: What is important to know: - You don't necessarily have to add at the top; you can use the `position` param to add the routes at the middle of Route Stack. - Don't forget to include a `Key` on the `Page` created by the `PageBuilder` to everything works properly. @@ -364,7 +391,7 @@ Remove all the Pages you want given a range: APSNavigator.of(context).removeRange(start: 2, end: 5); ``` -### 6. Internal Navigator +### 6. Internal (Nested) Navigators Example Link: [Internal Navigator Example](https://github.com/guilherme-v/aps_navigator/blob/develop/example/lib/pages/examples/internal_navigator/internal_navigator.dart) @@ -411,16 +438,17 @@ class _InternalNavigatorState extends State { } ``` -What is important to know: +:sleepy: What is important to know: - Current limitation: Browser's URL won't update based on internal navigator state ## Warning & Suggestions -- Although this package is already useful, it's still in the Dev stage. -- I'm not sure if creating yet another navigating library is something good - we already have a lot of confusion around it today. -- This lib is not back-compatible with the old official Navigation API - at least for now (Is it worth it?). -- Do you have any ideas or found a bug? Fell free to open an issue :) +- :construction: Although this package is already useful, it's still in the **Dev stage**. +- :stuck_out_tongue: I'm not sure if creating yet another navigating library is something good - we already have a lot of confusion around it today. +- :hankey: This lib is not back-compatible with the old official Navigation API - at least for now (Is it worth it?). +- :bug: Do you have any ideas or found a bug? Fell free to open an issue! :) +- :information_desk_person: Do you want to know the current development stage? Check the [Project's Roadmap](https://github.com/guilherme-v/aps_navigator/projects/1). ## Maintainers From 9c81ec0c910d7ecacb766a38c7a14fc68aa690c5 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 13:52:50 -0300 Subject: [PATCH 2/9] ref: Apply refafctor, Improve Docs and minor fixes - Improve Lib DOCs - Fix issue when popping page restored from web history - Fix issue when popping page at invalid position - Apply general refactoring --- lib/aps_navigator.dart | 2 +- .../aps_route/aps_route_build_function.dart | 48 ----- lib/src/aps_route/aps_route_descriptor.dart | 18 +- lib/src/aps_route/aps_route_matcher.dart | 2 +- lib/src/aps_route/route_data.dart | 39 ++++ lib/src/aps_snapshot.dart | 46 ++++- lib/src/controller/aps_controller.dart | 171 +++++++++++------- lib/src/navigator/aps_navigator.dart | 35 ++-- lib/src/parser/aps_parser.dart | 20 +- lib/src/parser/aps_parser_data.dart | 34 +++- 10 files changed, 260 insertions(+), 155 deletions(-) delete mode 100644 lib/src/aps_route/aps_route_build_function.dart create mode 100644 lib/src/aps_route/route_data.dart diff --git a/lib/aps_navigator.dart b/lib/aps_navigator.dart index ebec31d..78a401d 100644 --- a/lib/aps_navigator.dart +++ b/lib/aps_navigator.dart @@ -1,7 +1,7 @@ library aps_navigator; -export 'src/aps_route/aps_route_build_function.dart'; export 'src/aps_route/aps_route_matcher.dart'; +export 'src/aps_route/route_data.dart'; export 'src/controller/aps_controller.dart'; export 'src/controller/aps_push_param.dart'; export 'src/navigator/aps_navigator.dart'; diff --git a/lib/src/aps_route/aps_route_build_function.dart b/lib/src/aps_route/aps_route_build_function.dart deleted file mode 100644 index cbd5674..0000000 --- a/lib/src/aps_route/aps_route_build_function.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Function called when creating a new [Page] instance needs to be made and included in the Navigation Stack. -/// -/// [RouteData] contains data about the current location. -/// -/// Usually we use this type to configure [ApsNavigator.from] route param. E.g.: -/// -/// ```dart -/// // 1 - Create the navigation and the route -/// final navigator = APSNavigator.from( -/// routes: { -/// '/static_url_example': StaticURLPage.route, // .. -/// } -/// ) -/// -/// class StaticURLPage extends StatefulWidget { -/// // 2 - Define a ApsRouteBuilderFunction -/// static Page route(RouteData data) { -/// return MaterialPage( -/// key: ValueKey('StaticURLPage'), // 3 - Important! Always include a key -/// child: StaticURLPage(tabIndex: data.values['tab']), -/// ); -/// } -/// } -/// ``` -typedef ApsRouteBuilderFunction = Page Function( - RouteData data, -); - -/// Data used by [ApsRouteBuilderFunction] when creating a new route. -/// -/// [location] is the current location being build (path + queries). E.g.: '/path/abc?tab=0' -/// -/// [values] is a map containing all values extracted from [location] variables. E.g.: -/// * Given the configured route: `/path/{var1}/abc/?{?tab}` -/// * And the following location: `/path/post_a/abc/?tab=0` -/// * [values] will contain: `{'var1': 'post_a', 'tab': 0}` -/// -class RouteData { - final Map values; - final String location; - - const RouteData({ - required this.location, - this.values = const {}, - }); -} diff --git a/lib/src/aps_route/aps_route_descriptor.dart b/lib/src/aps_route/aps_route_descriptor.dart index 322ae30..da38509 100644 --- a/lib/src/aps_route/aps_route_descriptor.dart +++ b/lib/src/aps_route/aps_route_descriptor.dart @@ -3,21 +3,23 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -/// [ApsRouteDescriptor] includes all the information [APSNavigator] needs to create a new route ([Page]). +/// It contains all the data required to (re)create a route. +/// +/// Each [ApsRouteDescriptor] instance will be later converted to an [Page] added to the route stack. +/// class ApsRouteDescriptor { - /// Represents the template that [location] will match. E.g.: `/path/{var1}/abc/?{?tab}`. - final String template; - - /// The current location that this descritor will build. E.g: `/path/post_a/abc/?tab=0`. + /// The location this descriptor represents. E.g: `/path/post_a/abc/?tab=0`. final String location; + /// The template that [location] will match. E.g.: `/path/{var1}/abc/?{?tab}`. + final String template; + /// Values extracted from [location], based on [template]. It'll contain both path and query values. final Map values; /// Completer returned to this 'child' page. - /// - /// This won't be serialized in browser's history, so this is recreated when loading pages from history. - Completer popCompleter; + Completer + popCompleter; // * This won't be serialized in browser's history, so this is recreated when loading pages from history. ApsRouteDescriptor({ required this.template, diff --git a/lib/src/aps_route/aps_route_matcher.dart b/lib/src/aps_route/aps_route_matcher.dart index 57e5d07..81c9166 100644 --- a/lib/src/aps_route/aps_route_matcher.dart +++ b/lib/src/aps_route/aps_route_matcher.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:uri/uri.dart'; -import 'aps_route_build_function.dart'; +import 'route_data.dart'; class ApsRouteMatcher { /// Map template URL -> builder diff --git a/lib/src/aps_route/route_data.dart b/lib/src/aps_route/route_data.dart new file mode 100644 index 0000000..a3c0fc6 --- /dev/null +++ b/lib/src/aps_route/route_data.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +/// Defines a Route Builder function. +/// +/// [RouteData] contains data about the current location that can be used the create the [Page]. +/// +/// This type is used usually to configure [ApsNavigator.from.route]. E.g.: +/// +/// ```dart +/// // 1 - Create the navigation and the route +/// final navigator = APSNavigator.from( +/// routes: { +/// '/static_url_example': // [ApsRouteBuilderFunction]... +/// } +/// ) +/// ``` +/// +typedef ApsRouteBuilderFunction = Page Function( + RouteData data, +); + +/// It contains information about the current route being created. +/// +/// [location] represents the current location being build (path + queries). E.g.: `/path/abc?tab=0` +/// +/// [values] is a map containing all values extracted from [location] variables. E.g.: +/// * Given the route template: `/path/{var1}/abc/?{?tab}` +/// * And the following location: `/path/post_a/abc/?tab=0` +/// * [values] will contains: `{'var1': 'post_a', 'tab': 0}` +/// +class RouteData { + final Map values; + final String location; + + const RouteData({ + required this.location, + this.values = const {}, + }); +} diff --git a/lib/src/aps_snapshot.dart b/lib/src/aps_snapshot.dart index a57f660..3495dd6 100644 --- a/lib/src/aps_snapshot.dart +++ b/lib/src/aps_snapshot.dart @@ -1,19 +1,42 @@ +import 'package:flutter/foundation.dart'; + import 'aps_route/aps_route_descriptor.dart'; import 'parser/aps_parser_data.dart'; +/// An [ApsSnapshot] can be used to (re)create the entire route stack at any moment. +/// +/// A route stack is any stack of [Page]s that can be set [ApsController] and passed to +/// [ApsNavigator] to create its internal [Navigator] page list. +/// class ApsSnapshot { + /// List of Descriptors that the [APSController] will use to recreate the [Navigator]'s [Page] stack. final List routesDescriptors; - final bool descriptorsWereLoadedFromBrowserHistory; + /// Signals if the [APSNavigator] had to restore the [popCompleter]. + /// + /// It will be `true` if the User uses the web history to go back to a page that pops a result. + /// + /// And if this happens, the result will be returned in `current.config.values['result']`. E.g.: + /// + /// ```dart + /// final params = APSNavigator.of(context).currentConfig.values; + /// result = params['result'] as String?; + /// ``` + bool popWasRestored; + + /// Configuration is used to create the current top Page that the user sees. ApsRouteDescriptor get topConfiguration => routesDescriptors.last; + /// Configuration is used to create the root Page. ApsRouteDescriptor get rootConfiguration => routesDescriptors.first; ApsSnapshot({ required this.routesDescriptors, - this.descriptorsWereLoadedFromBrowserHistory = false, + this.popWasRestored = false, }); + /// Transform this [ApsSnapshot] instance in an instance of [ApsParserData] that can be serialized in the browser's + /// web history. ApsParserData toApsParserData() { return ApsParserData( location: topConfiguration.location, @@ -24,8 +47,23 @@ class ApsSnapshot { ApsSnapshot clone() { return ApsSnapshot( routesDescriptors: routesDescriptors.map((d) => d.copyWith()).toList(), - descriptorsWereLoadedFromBrowserHistory: - descriptorsWereLoadedFromBrowserHistory, + popWasRestored: popWasRestored, ); } + + @override + String toString() => + 'ApsSnapshot(routesDescriptors: $routesDescriptors, popWasRestored: $popWasRestored)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ApsSnapshot && + listEquals(other.routesDescriptors, routesDescriptors) && + other.popWasRestored == popWasRestored; + } + + @override + int get hashCode => routesDescriptors.hashCode ^ popWasRestored.hashCode; } diff --git a/lib/src/controller/aps_controller.dart b/lib/src/controller/aps_controller.dart index 1052cc3..2409936 100644 --- a/lib/src/controller/aps_controller.dart +++ b/lib/src/controller/aps_controller.dart @@ -1,30 +1,35 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import '../aps_route/aps_route_build_function.dart'; import '../aps_route/aps_route_descriptor.dart'; import '../aps_route/aps_route_matcher.dart'; +import '../aps_route/route_data.dart'; import '../aps_snapshot.dart'; import '../helpers.dart'; import '../parser/aps_parser_data.dart'; import 'aps_push_param.dart'; +/// Class that allows navigation control. +/// class APSController extends ChangeNotifier { - /// Global key used by the [Navigator] instance created internally by [APSNavigator]. + /// Global key set to the [Navigator] instance created internally by [APSNavigator]. final navigatorKey = GlobalKey(); /// Router matcher. final ApsRouteMatcher routerMatcher; + /// First snapshot created by this controller. ApsSnapshot initialSnapshot; - /// Configuration currently used to recreate the [APSNavigator] page's list. + /// Snapshot used to create the current route/page stack. ApsSnapshot currentSnapshot; - /// List of pages used by [APSNavigator] to populate its [Navigator] instance. - List get pages => List.unmodifiable(_buildPagesUsingCurrentConfig()); - + /// Shortcut to `currentConfig.topConfiguration`. ApsRouteDescriptor get currentConfig => currentSnapshot.topConfiguration; + /// List of pages used by [APSNavigator] to populate its internal [Navigator]'s page list. + List get pages => List.unmodifiable(_buildPagesUsingCurrentConfig()); + /// BuildContext was used to build this APSController instance. late BuildContext buildContext; @@ -41,10 +46,9 @@ class APSController extends ChangeNotifier { return APSController._(initialConfiguration, routerMatcher); } - /// /// Pushes a new page to the top. /// - /// The [path] should match one of those configured when creating the [APSNavigator]. + /// [path] should match one of those templates configured when creating the [APSNavigator] instance. /// /// Example: /// @@ -61,7 +65,7 @@ class APSController extends ChangeNotifier { /// ); ///``` /// - /// The following results in **"/static_url_example"**. + /// The following navigates to: **"/static_url_example"**. /// /// ```dart /// APSNavigator.of(context).push( @@ -71,7 +75,7 @@ class APSController extends ChangeNotifier { /// ``` /// /// - /// The following results in only **"/dynamic_url_example?tab=authors"**. + /// The following navigates to: **"/dynamic_url_example?tab=authors"**. /// /// ```dart /// APSNavigator.of(context).push( @@ -81,12 +85,11 @@ class APSController extends ChangeNotifier { /// ``` /// /// - /// The following results in only **"/posts/10"**. + /// The following navigates to: **"/posts/10"**. /// /// ```dart /// APSNavigator.of(context).push( /// path: '/posts/10', - /// params: {'post_id': 10}, /// ); /// ``` /// @@ -102,13 +105,69 @@ class APSController extends ChangeNotifier { return descriptorToAdd.popCompleter.future as Future; } + /// Pop top [Page] + /// + /// The result returned by the page is returned as expected: + /// ```dart + /// final result = await APSNavigator.of(context).push( + /// path: '...', + /// ); + /// ``` + /// + /// But if the User uses the web history to go back to a page that pops a result, + /// the result is returned at `didUpdateWidget`. E.g.: + /// ```dart + /// @override + /// void didUpdateWidget(HomePage oldWidget) { + /// super.didUpdateWidget(oldWidget); + /// final params = APSNavigator.of(context).currentConfig.values; + /// result = params['result'] as String?; + /// if (result != null) _showSnackBar(result!); + /// } + /// ``` + /// + bool pop([T? result]) { + final noPagesToPop = currentSnapshot.routesDescriptors.length <= 1; + if (noPagesToPop) return false; + + final d = currentSnapshot.routesDescriptors.removeLast(); + + if (currentSnapshot.popWasRestored) { + final newTop = currentSnapshot.topConfiguration; + newTop.values['result'] = result; + } else { + d.popCompleter.complete(result); + } + + notifyListeners(); + + return true; + } + + /// Pushes a list of Pages at the specified position. + /// + /// [position] should be in the range: [0 <= position <= [apsController.pages.length]]. + /// if no [position] is given, the list will be added at the top of the current Page Stack. + /// + /// ```dart + /// APSNavigator.of(context).pushAll( + /// position: .., + /// list: [ + /// ApsPushParam(path: '/a', params: {'p1': 1}), + /// ApsPushParam(path: '/b'), + /// ApsPushParam(path: '/c', params: {'number': 3}), + /// ApsPushParam(path: '/d', params: {'any_other': 'asdf'}), + /// ], + /// ); + /// ``` + /// void pushAll({int? position, required List list}) { final desc = currentSnapshot.routesDescriptors; // Check for valid a position if (position != null) { - final isPositionValid = position >= 0 || position <= desc.length - 1; - final msg = 'Trying to push List at invalid position: $position'; + final isPositionValid = position >= 0 && position <= desc.length - 1; + final msg = 'Trying to push List of Pages at invalid position: $position'; if (!isPositionValid) throw msg; } @@ -121,8 +180,9 @@ class APSController extends ChangeNotifier { notifyListeners(); } + /// It navigates back to the Root (the route provided to [APSNavigator.from.initialRoute]). /// - /// Returns until the root page. + /// It won't call the [PopCompleter] of the pages above it. /// void backToRoot() { final curLocation = currentSnapshot.topConfiguration.location; @@ -134,28 +194,7 @@ class APSController extends ChangeNotifier { notifyListeners(); } - /// - /// Pop top - /// - bool pop([T? result]) { - final noPagesToPop = currentSnapshot.routesDescriptors.length <= 1; - if (noPagesToPop) return false; - - final d = currentSnapshot.routesDescriptors.removeLast(); - - if (currentSnapshot.descriptorsWereLoadedFromBrowserHistory) { - final nextTop = currentSnapshot.topConfiguration; - nextTop.values['result'] = result; - } else { - d.popCompleter.complete(result); - } - - notifyListeners(); - - return true; - } - - /// Removes a range of Pages from stack history. + /// Removes a range of Pages from the Page Stack. /// /// Removes the elements with positions greater than or equal to [start] /// and less than [end], from the list. @@ -168,16 +207,18 @@ class APSController extends ChangeNotifier { void removeRange({required int start, required int end}) { final desc = currentSnapshot.routesDescriptors; - if (start < 0 || end >= desc.length - 1) throw 'Invalid range'; + if (start < 0 || end >= desc.length) { + throw 'Trying to use an invalid range when removing Pages'; + } desc.removeRange(start, end); notifyListeners(); } + /// It requests the Browser to update its address bar based on the given params. /// - /// Updates the current route params. - /// - /// Example, the follows generates: **"baseURL?tab=books"** or **"baseURL?tab=authors"**. + /// Example, given a route templated configured as `'/dynamic_url_example{?tab}'`, + /// the following generates: **"baseURL?tab=books"** or **"baseURL?tab=authors"**. /// /// ```dart /// final aps = APSNavigator.of(context); @@ -200,18 +241,32 @@ class APSController extends ChangeNotifier { currentSnapshot.routesDescriptors.removeLast(); currentSnapshot.routesDescriptors.add(descriptorToAdd); - // notifyListeners(); + _forceBrowserUpdateURL(); } + /// Configures this [ApsController] instance to use an [ApsSnapshot] created based on the given [configuration]. + /// + /// [configuration] is usually an [ApsParserData] that was created by the browser or recovered from its history. void browserSetNewConfiguration(ApsParserData configuration) { if (configuration.location == '/') { backToRoot(); return; } - if (configuration.isANewConfigCreatedByBrowser) { - // build and push a new descriptor + if (configuration.hasPageDescriptorsAvailableFromWebHistory) { + // load all descriptors and create a new Snapshot from them + final descriptors = configuration.descriptorsJsons + .map((j) => ApsRouteDescriptor.fromJson(j)) + .toList(); + + currentSnapshot = ApsSnapshot( + routesDescriptors: descriptors, + popWasRestored: true, + ); + notifyListeners(); + } else { + // build a new descriptor and upate the current Snapshot final location = configuration.location; final template = routerMatcher.getTemplateForRoute(location)!; final params = routerMatcher.getValuesFromRoute(location); @@ -222,14 +277,11 @@ class APSController extends ChangeNotifier { values: params, ); - _browserPushDescriptor(descriptorToAdd); - } else { - // load all the previous descriptors available - final descriptors = configuration.descriptorsJsons - .map((j) => ApsRouteDescriptor.fromJson(j)) - .toList(); + currentSnapshot.routesDescriptors.add(descriptorToAdd); + currentSnapshot.popWasRestored = + configuration.isUserOpeningAppForTheFirstTime; - _browserLoadDescriptors(descriptors); + notifyListeners(); } } @@ -254,21 +306,8 @@ class APSController extends ChangeNotifier { return List.unmodifiable(pages); } - void _forceBrowserUpdateURL() { - Router.navigate(buildContext, () {}); - } - - void _browserPushDescriptor(ApsRouteDescriptor descriptor) { - currentSnapshot.routesDescriptors.add(descriptor); - notifyListeners(); - } - - void _browserLoadDescriptors(List descriptors) { - currentSnapshot = ApsSnapshot( - routesDescriptors: descriptors, - descriptorsWereLoadedFromBrowserHistory: true, - ); - notifyListeners(); + void _forceBrowserUpdateURL({VoidCallback? callback}) { + Router.navigate(buildContext, callback ?? () {}); } ApsRouteDescriptor _createDescriptorFrom( diff --git a/lib/src/navigator/aps_navigator.dart b/lib/src/navigator/aps_navigator.dart index 82f819c..96cf28d 100644 --- a/lib/src/navigator/aps_navigator.dart +++ b/lib/src/navigator/aps_navigator.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import '../aps_route/aps_route_build_function.dart'; import '../aps_route/aps_route_matcher.dart'; +import '../aps_route/route_data.dart'; import '../aps_snapshot.dart'; import '../controller/aps_controller.dart'; import '../controller/aps_inherited_controller.dart'; @@ -11,8 +11,7 @@ import '../helpers.dart'; import '../parser/aps_parser.dart'; import '../parser/aps_parser_data.dart'; -/// -/// APS implementation of [RouterDelegate]. +/// The APS lib implementation of a [RouterDelegate]. /// /// It: /// - Creates an internal [Navigator] Widget and handles it's [Pages] updates. @@ -41,9 +40,6 @@ import '../parser/aps_parser_data.dart'; /// } /// /// // 3 - Prepare your Widget to be used as a Route. -/// // -/// // It should return any Widget that extends a Page. -/// // Recommendation is to use a Key to avoid problems. /// class HomePage extends StatefulWidget { /// HomePage({Key? key}) : super(key: key); /// @@ -52,8 +48,10 @@ import '../parser/aps_parser_data.dart'; /// /// // You don't need to use a static method here, but it's a nice way of organizing things. /// static Page route({Map? params}) { +/// // * Important: AVOID using 'const' keyword at "MaterialPage" or "HomePage" levels, +/// // * or Pop may not work properly with Web History /// return MaterialPage( -/// key: ValueKey('Home'), // Always include a key +/// key: const ValueKey('Home'), //* Important: Always include a key here! /// child: HomePage(), /// ); /// } @@ -99,7 +97,7 @@ class APSNavigator extends RouterDelegate { /// } BackButtonDispatcher? backButtonDispatcher; - /// Returns the [context] closest [APSController] instance + /// Returns the [APSController] instance closest [context]. static APSController of(BuildContext context) => context .dependOnInheritedWidgetOfExactType()! .controller; @@ -147,15 +145,14 @@ class APSNavigator extends RouterDelegate { /// Example of addresses that can be used in [routeBuilders]: /// /// ```dart - /// // no path or query variable + /// // route template with no path or query variable /// '/posts': PostListPage.route, /// - /// // path variable + /// // route template with path variable /// '/posts/{post_id}': PostListItemPage.route, /// - /// // queries variable + /// // route template with queries variable /// '/bottom_nav{?var1,var2}': BottomNavPage.route, - /// /// ``` /// factory APSNavigator.from({ @@ -189,7 +186,7 @@ class APSNavigator extends RouterDelegate { @override ApsParserData? get currentConfiguration { - // Child navigators won't report back to browser history + // Child navigators won't report back to browser's history final isAChildNavigator = parentNavigator != null; if (isAChildNavigator) return null; @@ -202,8 +199,16 @@ class APSNavigator extends RouterDelegate { } @override - Future setInitialRoutePath(ApsParserData _) { - return setNewRoutePath(controller.initialSnapshot.toApsParserData()); + Future setInitialRoutePath(ApsParserData configuration) async { + // 'setInitialRoutePath' is tricky, + // Initial Route is any kind of route the triggered app opening. + // - user opens the app normally + // - user opens the app using an browser's history entry + // - ... + // + return setNewRoutePath( + configuration.copyWith(isUserOpeningAppForTheFirstTime: true), + ); } @override diff --git a/lib/src/parser/aps_parser.dart b/lib/src/parser/aps_parser.dart index e0e1e2d..14b761d 100644 --- a/lib/src/parser/aps_parser.dart +++ b/lib/src/parser/aps_parser.dart @@ -8,22 +8,24 @@ import 'aps_parser_data.dart'; /// [APSNavigator] instance of [RouteInformationParser] class APSParser extends RouteInformationParser { const APSParser(); - static const _descriptorsKey = 'descriptors'; + static const descriptorsKey = '_aps_pages_descriptors'; @override Future parseRouteInformation( RouteInformation routeInformation, ) { - final ApsParserData data = ApsParserData( + var data = ApsParserData( location: routeInformation.location ?? '/', ); - final isANewConfigCreatedByBrowser = routeInformation.state == null; - if (!isANewConfigCreatedByBrowser) { - final loadedState = routeInformation.state! as Map; - data.descriptorsJsons = (loadedState[_descriptorsKey] as List) - .map((e) => e as String) - .toList(); + final loadedState = routeInformation.state as Map? ?? {}; + final hasApsData = loadedState.containsKey(descriptorsKey); + if (hasApsData) { + data = data.copyWith( + descriptorsJsons: (loadedState[descriptorsKey] as List) + .map((e) => e as String) + .toList(), + ); } return SynchronousFuture(data); @@ -34,7 +36,7 @@ class APSParser extends RouteInformationParser { ApsParserData configuration, ) { final Map stateToSave = { - _descriptorsKey: configuration.descriptorsJsons + descriptorsKey: configuration.descriptorsJsons }; final routeInfo = RouteInformation( diff --git a/lib/src/parser/aps_parser_data.dart b/lib/src/parser/aps_parser_data.dart index f31b42f..d8b8b62 100644 --- a/lib/src/parser/aps_parser_data.dart +++ b/lib/src/parser/aps_parser_data.dart @@ -1,11 +1,39 @@ class ApsParserData { - String location; - List descriptorsJsons; + final String location; + final List descriptorsJsons; + final bool isUserOpeningAppForTheFirstTime; + + final bool + isUserRestoringAppStateFromWebHistory; // User DIDN't leave the app and is recoverying from history ApsParserData({ required this.location, this.descriptorsJsons = const [], + this.isUserOpeningAppForTheFirstTime = false, + this.isUserRestoringAppStateFromWebHistory = false, }); - bool get isANewConfigCreatedByBrowser => descriptorsJsons.isEmpty; + bool get hasPageDescriptorsAvailableFromWebHistory => + descriptorsJsons.isNotEmpty; + + ApsParserData copyWith({ + String? location, + List? descriptorsJsons, + bool? isUserOpeningAppForTheFirstTime, + bool? isUserRestoringAppStateFromWebHistory, + }) { + return ApsParserData( + location: location ?? this.location, + descriptorsJsons: descriptorsJsons ?? this.descriptorsJsons, + isUserOpeningAppForTheFirstTime: isUserOpeningAppForTheFirstTime ?? + this.isUserOpeningAppForTheFirstTime, + isUserRestoringAppStateFromWebHistory: + isUserRestoringAppStateFromWebHistory ?? + this.isUserRestoringAppStateFromWebHistory, + ); + } + + @override + String toString() => + 'ApsParserData(location: $location, descriptorsJsons: $descriptorsJsons, isUserOpeningAppForTheFirstTime: $isUserOpeningAppForTheFirstTime, isUserRestoringAppStateFromWebHistory: $isUserRestoringAppStateFromWebHistory)'; } From 61b6c089db3cfc4678bca16265a98a6a57ef1ed5 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 13:55:21 -0300 Subject: [PATCH 3/9] test: Increase test coverage --- test/aps_route/aps_route_matcher_test.dart | 2 +- test/aps_route/route_data_test.dart | 2 +- test/aps_snapshot_test.dart | 6 +- test/controller/aps_controller_test.dart | 479 +++++++++++++++++++++ test/parser/aps_parser_test.dart | 27 +- test/test_utils/test_utils.dart | 25 ++ 6 files changed, 514 insertions(+), 27 deletions(-) create mode 100644 test/controller/aps_controller_test.dart create mode 100644 test/test_utils/test_utils.dart diff --git a/test/aps_route/aps_route_matcher_test.dart b/test/aps_route/aps_route_matcher_test.dart index 5687197..60edb3c 100644 --- a/test/aps_route/aps_route_matcher_test.dart +++ b/test/aps_route/aps_route_matcher_test.dart @@ -1,4 +1,4 @@ -import 'package:aps_navigator/src/aps_route/aps_route_build_function.dart'; +import 'package:aps_navigator/src/aps_route/route_data.dart'; import 'package:aps_navigator/src/aps_route/aps_route_matcher.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/aps_route/route_data_test.dart b/test/aps_route/route_data_test.dart index 6cb24e6..a8b71ee 100644 --- a/test/aps_route/route_data_test.dart +++ b/test/aps_route/route_data_test.dart @@ -1,4 +1,4 @@ -import 'package:aps_navigator/src/aps_route/aps_route_build_function.dart'; +import 'package:aps_navigator/src/aps_route/route_data.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/aps_snapshot_test.dart b/test/aps_snapshot_test.dart index 11babd1..90f8259 100644 --- a/test/aps_snapshot_test.dart +++ b/test/aps_snapshot_test.dart @@ -15,7 +15,7 @@ void main() { // asserts expect(data.routesDescriptors, routesDescriptors); - expect(data.descriptorsWereLoadedFromBrowserHistory, isFalse); + expect(data.popWasRestored, isFalse); expect(data.topConfiguration, routesDescriptors.last); expect(data.rootConfiguration, routesDescriptors.first); @@ -29,8 +29,8 @@ void main() { final clone = data.clone(); expect(clone.routesDescriptors, data.routesDescriptors); expect( - clone.descriptorsWereLoadedFromBrowserHistory, - data.descriptorsWereLoadedFromBrowserHistory, + clone.popWasRestored, + data.popWasRestored, ); }); } diff --git a/test/controller/aps_controller_test.dart b/test/controller/aps_controller_test.dart new file mode 100644 index 0000000..40a77ea --- /dev/null +++ b/test/controller/aps_controller_test.dart @@ -0,0 +1,479 @@ +import 'package:aps_navigator/aps_navigator.dart'; +import 'package:aps_navigator/src/aps_route/aps_route_descriptor.dart'; +import 'package:aps_navigator/src/aps_snapshot.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_utils/test_utils.dart'; + +void main() { + MaterialPage createBuilder(RouteData data) => + MaterialPage(child: Container()); + + final routes = { + '/static_url_example': createBuilder, + '/dynamic_url_example{?tab}': createBuilder, + '/other_dynamic_url_example{?tab,other}': createBuilder, + '/return_data_example': createBuilder, + '/posts': createBuilder, + '/posts/{post_id}': createBuilder, + '/internal_navs': createBuilder, + '/multi_push': createBuilder, + '/multi_remove': createBuilder, + '/': createBuilder, + }; + + test('it should be able to Push new Pages', () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + // act + controller.push(path: '/posts'); + controller.push(path: '/posts/10'); + controller.push(path: '/static_url_example'); + controller.push( + path: '/dynamic_url_example', + params: {'tab': 'books'}, + ); + + // asserts + expect(controller.currentConfig.location, '/dynamic_url_example?tab=books'); + expect(controller.pages.length, 5); + expect(notifyCounter, 4); + }); + + test('it should be able to Pop Pages', () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + controller.push(path: '/posts'); + controller.push(path: '/posts/10'); + controller.push(path: '/static_url_example'); + controller.push( + path: '/dynamic_url_example', + params: {'tab': 'books'}, + ); + + // act + asserts + expect(controller.currentConfig.location, '/dynamic_url_example?tab=books'); + expect(controller.pages.length, 5); + expect(notifyCounter, 4); + + var res = controller.pop(); + expect(res, true); + expect(controller.currentConfig.location, '/static_url_example'); + expect(controller.pages.length, 4); + expect(notifyCounter, 5); + + res = controller.pop(); + expect(res, true); + expect(controller.currentConfig.location, '/posts/10'); + expect(controller.pages.length, 3); + expect(notifyCounter, 6); + + res = controller.pop(); + expect(res, true); + expect(controller.currentConfig.location, '/posts'); + expect(controller.pages.length, 2); + expect(notifyCounter, 7); + + res = controller.pop(); + expect(res, true); + expect(controller.currentConfig.location, '/'); + expect(controller.pages.length, 1); + expect(notifyCounter, 8); + expect( + controller.currentConfig, + controller.initialSnapshot.topConfiguration, + ); + + res = controller.pop(); + expect(res, false); + }); + + test('it should return data to previous Pages when poping', () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + final f1 = controller.push(path: '/posts'); + final f2 = controller.push(path: '/posts/10'); + final f3 = controller.push(path: '/static_url_example'); + final f4 = controller.push( + path: '/dynamic_url_example', + params: {'tab': 'books'}, + ); + + // act + asserts + controller.pop('dummyRes'); + expect(await f4, 'dummyRes'); + + controller.pop(123); + expect(await f3, 123); + + controller.pop(123.00); + expect(await f2, 123.00); + + controller.pop({'a': 1}); + expect(await f1, {'a': 1}); + }); + + test( + "it should return data in value['result'] restoring Pages from web history", + () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + const loc = 'path/to/something?tab=1&other=2'; + final descriptors = TestUtils.createDescriptorsJson(top: loc); + final browserConfig = ApsParserData( + location: loc, + descriptorsJsons: descriptors, + ); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + // act + assert + controller.browserSetNewConfiguration(browserConfig); + + controller.pop('dummyRes'); + expect(controller.currentConfig.values['result'], 'dummyRes'); + + controller.pop(123); + expect(controller.currentConfig.values['result'], 123); + + controller.pop(123.00); + expect(controller.currentConfig.values['result'], 123.00); + + controller.pop({'a': 1}); + expect(controller.currentConfig.values['result'], {'a': 1}); + }); + + test("it should be able to push a list of Pages at once on Top of Page Stack", + () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + // act + controller.pushAll( + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/dynamic_url_example', params: {'tab': 'authors'}), + ApsPushParam(path: '/posts/10'), + ApsPushParam(path: '/multi_push', params: {'number': 4}), + ], + ); + + // asserts + expect(notifyCounter, 1); + expect(controller.pages.length, 5); + expect( + controller.currentConfig, + controller.currentSnapshot.topConfiguration, + ); + + expect( + controller.currentSnapshot.routesDescriptors[4].location, + '/multi_push', + ); + expect( + controller.currentSnapshot.routesDescriptors[3].location, + '/posts/10', + ); + expect( + controller.currentSnapshot.routesDescriptors[2].location, + '/dynamic_url_example?tab=authors', + ); + expect( + controller.currentSnapshot.routesDescriptors[1].location, + '/static_url_example', + ); + }); + + test( + "it should be able to push a list of Pages at specific Position of Page Stack", + () async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + controller.pushAll( + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), // <- we'll push here + ApsPushParam(path: '/static_url_example'), + ], + ); + + // act + controller.pushAll( + position: 3, + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/dynamic_url_example', params: {'tab': 'authors'}), + ApsPushParam(path: '/posts/10'), + ApsPushParam(path: '/multi_push', params: {'number': 4}), + ], + ); + + // asserts + expect(notifyCounter, 2); + expect(controller.pages.length, 9); + expect( + controller.currentConfig, + controller.currentSnapshot.topConfiguration, + ); + + expect( + controller.currentSnapshot.routesDescriptors[6].location, + '/multi_push', + ); + expect( + controller.currentSnapshot.routesDescriptors[5].location, + '/posts/10', + ); + expect( + controller.currentSnapshot.routesDescriptors[4].location, + '/dynamic_url_example?tab=authors', + ); + expect( + controller.currentSnapshot.routesDescriptors[3].location, + '/static_url_example', + ); + expect( + controller.currentSnapshot.routesDescriptors[2].location, + '/static_url_example', + ); + expect( + controller.currentSnapshot.routesDescriptors[1].location, + '/static_url_example', + ); + }); + + test("it should not allow to push at Page Stack invalid position", () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + // act + expect( + () => controller.pushAll( + position: 50, + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/posts/10'), + ApsPushParam(path: '/multi_push', params: {'number': 4}), + ], + ), + throwsA('Trying to push List of Pages at invalid position: 50'), + ); + + // asserts + expect(notifyCounter, 0); + expect(controller.pages.length, 1); + }); + + test('It should be able to navigate back to the Root', () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + controller.push(path: '/posts'); + controller.push(path: '/posts/10'); + notifyCounter = 0; // reset again + + // act + controller.backToRoot(); + + // asserts + expect(notifyCounter, 1); + expect( + controller.currentConfig, + controller.initialSnapshot.topConfiguration, + ); + expect(controller.pages.length, 1); + }); + + test('It should be able to remove a Range of Pages from Page Stack', () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + controller.pushAll( + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), // will be removed + ApsPushParam(path: '/static_url_example'), // will be removed + ApsPushParam(path: '/static_url_example'), + ], + ); + notifyCounter = 0; // reset + + // act + controller.removeRange(start: 2, end: 4); + + // asserts + expect(notifyCounter, 1); + expect(controller.pages.length, 3); + }); + + test('It should NOT be able to remove Pages from PageStack invalid ranges', + () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + controller.pushAll( + list: [ + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), + ApsPushParam(path: '/static_url_example'), + ], + ); + notifyCounter = 0; // reset + + // act + asserts + expect( + () => controller.removeRange(start: -1, end: 4), + throwsA('Trying to use an invalid range when removing Pages'), + ); + expect( + () => controller.removeRange(start: 1, end: 5), + throwsA('Trying to use an invalid range when removing Pages'), + ); + expect(notifyCounter, 0); + expect(controller.pages.length, 5); + }); + + testWidgets('It should update Browsers Address properly when requested', + (WidgetTester tester) async { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + final myApp = MaterialApp.router( + routerDelegate: navigator, + routeInformationParser: navigator.parser, + ); + await tester.pumpWidget(myApp); + + // act + asserts + + // - If template contains Query params, updated it + controller.push( + path: '/other_dynamic_url_example', + params: {'tab': 'authors', 'other': 123}, + ); + expect(notifyCounter, 1); + controller.updateParams( + params: {'tab': 'books', 'other': 321}, + ); + expect(notifyCounter, 1); // updateParams do not rebuild the UI + expect( + controller.currentConfig.location, + '/other_dynamic_url_example?tab=books&other=321', + ); + + // - If template doesn't contains Query params, doen't update the URL + controller.push(path: '/static_url_example'); + expect(notifyCounter, 2); + controller.updateParams(params: {'tab': 'books', 'other': 321}); + expect(notifyCounter, 2); // updateParams do not rebuild the UI + expect(controller.currentConfig.location, '/static_url_example'); + }); + + test('It should redirect to Root when Browser goes back to /', () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + + // act + controller.browserSetNewConfiguration(ApsParserData(location: '/')); + + // assert + expect( + controller.currentConfig, + controller.initialSnapshot.topConfiguration, + ); + }); + + test( + 'It should create a Snapshot based on PageDescriptors if user uses we history', + () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + final descList = [ + ApsRouteDescriptor(location: '/a', template: '/a', values: const {}), + ApsRouteDescriptor(location: '/a/b', template: '/a/b', values: const {}), + ApsRouteDescriptor( + location: '/a/b/c', + template: '/a/b/{var2}', + values: const {'var2': 'c'}, + ), + ApsRouteDescriptor( + location: '/a/b/c/d?x=1&z=2', + template: '/a/b/{var2}/d{?x,y}', + values: const {'var2': 'c'}, + ), + ApsRouteDescriptor( + location: '/dynamic_url_example?tab=books', + template: '/dynamic_url_example{?tab}', + values: const {'tab': 'books'}, + ) + ].map((d) => d.toJson()).toList(); + + // act + final browserConfig = ApsParserData( + location: '/dynamic_url_example?tab=books', + descriptorsJsons: descList, + ); + controller.browserSetNewConfiguration(browserConfig); + + // assert + expect(notifyCounter, 1); + expect(controller.currentConfig.location, '/dynamic_url_example?tab=books'); + expect( + controller.currentSnapshot, + ApsSnapshot( + popWasRestored: true, + routesDescriptors: browserConfig.descriptorsJsons + .map((j) => ApsRouteDescriptor.fromJson(j)) + .toList(), + ), + ); + }); + + test( + 'It should be able to create a new PageDescriptor and add to current Snapshot', + () { + // arrange + final navigator = APSNavigator.from(routes: routes); + var notifyCounter = 0; + final controller = navigator.controller..addListener(() => notifyCounter++); + const loc = '/dynamic_url_example?tab=books'; + + // act + controller.browserSetNewConfiguration(ApsParserData( + location: loc, + isUserOpeningAppForTheFirstTime: true, + )); + + // assert + expect(notifyCounter, 1); + expect(controller.currentConfig.location, '/dynamic_url_example?tab=books'); + expect(controller.currentConfig.template, '/dynamic_url_example{?tab}'); + expect(controller.currentConfig.values, const {'tab': 'books'}); + expect(controller.currentSnapshot.popWasRestored, true); + }); +} diff --git a/test/parser/aps_parser_test.dart b/test/parser/aps_parser_test.dart index 7df1322..4bc385c 100644 --- a/test/parser/aps_parser_test.dart +++ b/test/parser/aps_parser_test.dart @@ -1,29 +1,11 @@ import 'package:aps_navigator/aps_navigator.dart'; -import 'package:aps_navigator/src/aps_route/aps_route_descriptor.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../test_utils/test_utils.dart'; + void main() { const loc = 'path/to/something?tab=1&other=2'; - final descriptors = [ - ApsRouteDescriptor(location: '/a', template: '/a', values: const {}), - ApsRouteDescriptor(location: '/a/b', template: '/a/b', values: const {}), - ApsRouteDescriptor( - location: '/a/b/c', - template: '/a/b/{var2}', - values: const {'var2': 'c'}, - ), - ApsRouteDescriptor( - location: '/a/b/c/d?x=1&z=2', - template: '/a/b/{var2}/d{?x,y}', - values: const {'var2': 'c'}, - ), - ApsRouteDescriptor( - location: loc, - template: '/path/to/something{?tab,other}', - values: const {'tab': '1', 'other': '2'}, - ) - ].map((d) => d.toJson()).toList(); - + final descriptors = TestUtils.createDescriptorsJson(top: loc); final configuration = ApsParserData( location: loc, descriptorsJsons: descriptors, @@ -36,7 +18,8 @@ void main() { // asserts final routeInfo = parser.restoreRouteInformation(configuration); expect(routeInfo.location, configuration.location); - expect(routeInfo.state, {'descriptors': configuration.descriptorsJsons}); + expect(routeInfo.state, + {APSParser.descriptorsKey: configuration.descriptorsJsons}); }); test('it should be able to parse RouteInformation properly', () async { diff --git a/test/test_utils/test_utils.dart b/test/test_utils/test_utils.dart new file mode 100644 index 0000000..6b2bf34 --- /dev/null +++ b/test/test_utils/test_utils.dart @@ -0,0 +1,25 @@ +import 'package:aps_navigator/src/aps_route/aps_route_descriptor.dart'; + +abstract class TestUtils { + static List createDescriptorsJson({required String top}) { + return [ + ApsRouteDescriptor(location: '/a', template: '/a', values: const {}), + ApsRouteDescriptor(location: '/a/b', template: '/a/b', values: const {}), + ApsRouteDescriptor( + location: '/a/b/c', + template: '/a/b/{var2}', + values: const {'var2': 'c'}, + ), + ApsRouteDescriptor( + location: '/a/b/c/d?x=1&z=2', + template: '/a/b/{var2}/d{?x,y}', + values: const {'var2': 'c'}, + ), + ApsRouteDescriptor( + location: top, + template: '/path/to/something{?tab,other}', + values: const {'tab': '1', 'other': '2'}, + ) + ].map((d) => d.toJson()).toList(); + } +} From 12226b649ae6015a4aef1d64c9d887c6b9381c14 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 13:56:04 -0300 Subject: [PATCH 4/9] chore: Apply updates in exemple dir --- example/lib/pages/examples/dynamic_url_page.dart | 11 +++++++++-- example/lib/pages/home_page.dart | 13 +++++++++---- example/pubspec.lock | 7 ------- example/pubspec.yaml | 9 ++------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/example/lib/pages/examples/dynamic_url_page.dart b/example/lib/pages/examples/dynamic_url_page.dart index 3c66bd5..2c84b58 100644 --- a/example/lib/pages/examples/dynamic_url_page.dart +++ b/example/lib/pages/examples/dynamic_url_page.dart @@ -37,12 +37,19 @@ class _DynamicURLPageState extends State { } @override - void didUpdateWidget(DynamicURLPage oldWidget) { - super.didUpdateWidget(oldWidget); + void didChangeDependencies() { + super.didChangeDependencies(); final previous = APSNavigator.of(context).currentConfig.values; tabIndex = (previous['tab'] == 'books') ? 0 : 1; } + @override + void didUpdateWidget(DynamicURLPage oldWidget) { + super.didUpdateWidget(oldWidget); + // final previous = APSNavigator.of(context).currentConfig.values; + // tabIndex = (previous['tab'] == 'books') ? 0 : 1; + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 27cafb0..1d7ddae 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -8,9 +8,13 @@ class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); - static Page route(RouteData _) { - return const MaterialPage( - key: ValueKey('Home'), // Important to include a key + static Page route(RouteData a) { + // * Important: AVOID using 'const' keyword at this level, or Pop may not work properly with Web History + // ignore: prefer_const_constructors + return MaterialPage( + // * Important: include a key, you can use 'const' here + key: const ValueKey('Home'), + // ignore: prefer_const_constructors child: HomePage(), ); } @@ -23,7 +27,7 @@ class _HomePageState extends State { void didUpdateWidget(HomePage oldWidget) { super.didUpdateWidget(oldWidget); final params = APSNavigator.of(context).currentConfig.values; - result = params['result'] as String; + result = params['result'] as String?; if (result != null) _showSnackBar(result!); } @@ -170,6 +174,7 @@ class _HomePageState extends State { } void _showSnackBar(String message) { + // print('HERE!: $message'); WidgetsBinding.instance!.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), diff --git a/example/pubspec.lock b/example/pubspec.lock index a00d02a..7d27bd0 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -50,13 +50,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" fake_async: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c285df4..66e3ffb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,15 +21,10 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - flutter: - sdk: flutter aps_navigator: path: ../ - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + flutter: + sdk: flutter dev_dependencies: flutter_test: From d58515f4e62c8ca30ba9a32f6006c55d476905c2 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 13:56:27 -0300 Subject: [PATCH 5/9] fix: Minor lint issue in pubspec --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index da93ec9..9ebc550 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,11 +8,11 @@ environment: flutter: ">=1.17.0" dependencies: + collection: ^1.15.0 flutter: sdk: flutter - uri: ^1.0.0 - collection: ^1.15.0 lint: ^1.0.0 + uri: ^1.0.0 dev_dependencies: flutter_test: From 2a36e4f5a2b3277d040f40509cee8149bb6a1c63 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 13:56:47 -0300 Subject: [PATCH 6/9] feat: Config C.I --- .github/workflows/ci.yaml | 27 +++++++++++++++++++++++++++ README.md | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..92c1e0a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,27 @@ +name: Build +on: [push, pull_request] +jobs: + flutter_test_and_analyze: + name: Run flutter test and analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1.4.0 + with: + channel: "stable" + - name: Install Dependencies + run: flutter packages get + - name: Format + run: flutter format --set-exit-if-changed lib test example + - name: Analyze + run: flutter analyze lib test example + - name: Run tests + run: flutter test --no-pub --coverage --test-randomize-ordering-seed random + - name: Check Code Coverage + uses: ChicagoFlutter/lcov-cop@v1.0.0 + with: + path: packages/flutter_bloc/coverage/lcov.info + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 395e402..f69d259 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # APS Navigator - App Pagination System -[![build](https://github.com/guilherme-v/aps_navigator/workflows/ci.yml/badge.svg)](https://github.com/guilherme-v/aps_navigator/actions) +[![build](https://github.com/guilherme-v/aps_navigator/actions/workflows/ci.yaml/badge.svg)](https://github.com/guilherme-v/aps_navigator/actions) +[![codecov](https://codecov.io/gh/guilherme-v/aps_navigator/branch/develop/graph/badge.svg)](https://codecov.io/gh/guilherme-v/aps_navigator) [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) -[![codecov](https://codecov.io/gh/guilherme-v/aps_navigator/branch/develop/graph/badge.svg)](https://codecov.io/gh/guilherme-v/aps_navigator) This library is just a wrapper around Navigator 2.0 and Router/Pages API that tries to make their use easier: From 5c98738839df33eb8cd2dda59d1d818de64aab53 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 16:23:07 -0300 Subject: [PATCH 7/9] release: Increase release to 0.0.4-dev.8 --- .github/workflows/ci.yaml | 2 +- CHANGELOG.md | 10 ++++++++++ README.md | 3 +++ example/pubspec.lock | 2 +- pubspec.yaml | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 92c1e0a..fc4909b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,5 @@ name: Build -on: [push, pull_request] +on: [pull_request] jobs: flutter_test_and_analyze: name: Run flutter test and analyze diff --git a/CHANGELOG.md b/CHANGELOG.md index b8051e0..90aa220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 0.0.4-dev.8 + +- docs: Improve README.md +- docs: Improve API documentation +- docs: Improve `example` comments +- fix: Issue when poping pages restored from web history +- fix: Issue when popping pagem from invalid positions +- test: Improve test coverage +- chore: Set up C.I (test, code analyze) + # 0.0.4-dev.7 - feat: initial release 🎉 \ No newline at end of file diff --git a/README.md b/README.md index f69d259..08e4d6d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ [![codecov](https://codecov.io/gh/guilherme-v/aps_navigator/branch/develop/graph/badge.svg)](https://codecov.io/gh/guilherme-v/aps_navigator) [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) +[![Pub](https://img.shields.io/pub/v/mocktail.svg)](https://pub.dev/packages/aps_navigator) +[![pub points](https://badges.bar/aps_navigator/pub%20points)](https://pub.dev/packages/aps_navigator) +[![pub package](https://img.shields.io/pub/v/aps_navigator.svg?color=success)](https://pub.dartlang.org/packages/aps_navigator) This library is just a wrapper around Navigator 2.0 and Router/Pages API that tries to make their use easier: diff --git a/example/pubspec.lock b/example/pubspec.lock index 7d27bd0..b2659d6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "0.0.4-dev.7" + version: "0.0.4-dev.8" async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9ebc550..d29f1e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: aps_navigator description: A wrapper around Navigator 2.0 and Router/Pages API to make their use a little easier. It offers Web support and complete control of the Route Stack. -version: 0.0.4-dev.7 +version: 0.0.4-dev.8 homepage: https://github.com/guilherme-v/aps_navigator environment: From 143dcd17343f51d26ad38f4a3a6232ab050b7de5 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 16:29:17 -0300 Subject: [PATCH 8/9] chore: Remove wrong badged from README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 08e4d6d..b823b8f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![codecov](https://codecov.io/gh/guilherme-v/aps_navigator/branch/develop/graph/badge.svg)](https://codecov.io/gh/guilherme-v/aps_navigator) [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) -[![Pub](https://img.shields.io/pub/v/mocktail.svg)](https://pub.dev/packages/aps_navigator) [![pub points](https://badges.bar/aps_navigator/pub%20points)](https://pub.dev/packages/aps_navigator) [![pub package](https://img.shields.io/pub/v/aps_navigator.svg?color=success)](https://pub.dartlang.org/packages/aps_navigator) From 1269d476d56cb75117bc75b63a76d09b9134efa9 Mon Sep 17 00:00:00 2001 From: guilherme Date: Sun, 2 May 2021 16:49:31 -0300 Subject: [PATCH 9/9] chore: Update CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fc4909b..92c1e0a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,5 @@ name: Build -on: [pull_request] +on: [push, pull_request] jobs: flutter_test_and_analyze: name: Run flutter test and analyze