Skip to content

Commit e2f4915

Browse files
committed
0.0.2
1 parent 8923f36 commit e2f4915

File tree

7 files changed

+233
-35
lines changed

7 files changed

+233
-35
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.0.2
2+
3+
- **BREAKING CHANGE**: Add `state` to the routes builder
4+
- Public version
5+
- Basic "how to" and documentation
6+
17
## 0.0.1-pre.2
28

39
- Dialogs and pop buttons logic

README.md

Lines changed: 212 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Octopus
1+
# [Octopus: A Declarative Router for Flutter](https://github.com/PlugFox/octopus/wiki)
22

33
[![Pub](https://img.shields.io/pub/v/octopus.svg)](https://pub.dev/packages/octopus)
44
[![Actions Status](https://github.com/PlugFox/octopus/actions/workflows/checkout.yml/badge.svg?branch=master)](https://github.com/PlugFox/octopus/actions/workflows/checkout.yml)
@@ -8,9 +8,33 @@
88
[![Linter](https://img.shields.io/badge/style-linter-40c4ff.svg)](https://pub.dev/packages/linter)
99
[![GitHub stars](https://img.shields.io/github/stars/plugfox/octopus?style=social)](https://github.com/plugfox/octopus/)
1010

11+
Octopus is a declarative router for Flutter. Its main concept and distinction from other solutions is dynamic navigation through state mutations. It is a TRULY DECLARATIVE router, where you don’t change the state imperatively using push and pop commands. Instead, you (or the user) specify the desired outcome through state mutations or the address bar, and the router delivers a predictably expected result.
12+
13+
Most solutions use templating for navigation, pre-describing all possible router states with hardcoding (and code generation). While this is an expected and predictable approach in traditional BE SSR (where the page is assembled server-side), it has several serious drawbacks on the client side:
14+
15+
1. You cannot predict all possible states in advance.
16+
2. There is no ability to implement routes of arbitrary depth (e.g., `/shop/category~id=1/category~id=12/category~id=123/product~id=1234`).
17+
3. Understanding what happens in cases with nested routes can be quite complex.
18+
4. Loss of state in nested routes, as such routers typically display only the active route, even though the nested state continues to exist.
19+
20+
What does the current solution offer?
21+
22+
1. Changing state through mutation.
23+
2. Nested navigation, both through the out-of-the-box solution and your custom implementation.
24+
3. A router state machine and case implementation based on state changes. For example, it’s very easy to implement breadcrumbs or integrate a tab/sidebar with router state arguments.
25+
4. A history of states, allowing you to implement a time machine or, after reauthentication, return the user to where they started. Or simply log this for analytics purposes.
26+
5. A user-friendly API with a "foolproof" design, where mutable states are clear and methods for changing them are provided, and immutable states are also clearly indicated.
27+
6. A strong focus on Guards. Since the user can now obtain any desired configuration, you might want to "clip their wings." Ensure that the "Home" page is always at the root except for logged-out users, change states during navigation or upon reaching certain conditions, for example, showing login for unauthorized users. Recheck all navigation states on a specific event, just pass the subscription to your guard.
28+
7. Implementation of dialogs through declarative navigation. No more showing dialogs from the navigator and mixing anonymous imperative dialogs with declarative navigation.
29+
8. Preserve and display the entire navigation tree in the URL and deep links, not just the active route!
30+
9. Clear debugging and the representation of the state as a string will simplify development.
31+
10. Concurrency support a out of the box.
32+
33+
With a declarative approach, the only limit is your imagination!
34+
1135
---
1236

13-
## Installation
37+
## [Installation](https://github.com/PlugFox/octopus/wiki/Installation)
1438

1539
Add the following dependency to your `pubspec.yaml` file:
1640

@@ -19,6 +43,192 @@ dependencies:
1943
octopus: <version>
2044
```
2145
46+
## [Get Started](https://github.com/PlugFox/octopus/wiki/Get-Started)
47+
48+
Set up your routes.
49+
You can use an enum, make a few sealed classes, or both.
50+
This doesn't matter. A recommended and simple way is to get started with enums.
51+
Override a `builder` function to link your nodes and widgets.
52+
Optionally, set up a "title" field for any route.
53+
54+
```dart
55+
enum Routes with OctopusRoute {
56+
home('home', title: 'Home'),
57+
gallery('gallery', title: 'Gallery'),
58+
picture('picture', title: 'Picture'),
59+
settings('settings', title: 'Settings');
60+
61+
const Routes(this.name, {this.title});
62+
63+
@override
64+
final String name;
65+
66+
@override
67+
Widget builder(BuildContext context, OctopusState state, OctopusNode node) =>
68+
switch (this) {
69+
Routes.home => const HomeScreen(),
70+
Routes.gallery => const GalleryScreen(),
71+
Routes.picture => PictureScreen(id: node.arguments['id']),
72+
Routes.settingsDialog => const SettingsDialog(),
73+
};
74+
}
75+
```
76+
77+
[Example](https://github.com/PlugFox/octopus/blob/master/example/lib/src/common/router/routes.dart)
78+
79+
Create an Octopus router instance.
80+
During `main` initialization or state of the root `App` widget.
81+
To do so, pass a list of all possible routes.
82+
Optionally, set a `defaultRoute` as a route by default.
83+
84+
```dart
85+
router = Octopus(
86+
routes: Routes.values,
87+
defaultRoute: Routes.home,
88+
);
89+
```
90+
91+
[Example](https://github.com/PlugFox/octopus/blob/master/example/lib/src/common/router/router_state_mixin.dart)
92+
93+
Add configuration from `Octopus.config` to the `WidgetApp.router` constructor.
94+
95+
```dart
96+
MaterialApp.router(
97+
routerConfig: router.config,
98+
)
99+
```
100+
101+
[Example](https://github.com/PlugFox/octopus/blob/master/example/lib/src/common/widget/app.dart)
102+
103+
## [How to navigate](https://github.com/PlugFox/octopus/wiki/How-to-navigate)
104+
105+
Use the `context.octopus.setState((state) => ...)` method as a basic navigation method.
106+
107+
And realize any navigation logic inside the callback as you please.
108+
109+
```dart
110+
context.octopus.setState((state) =>
111+
state
112+
..findByName('catalog-tab')?.add(Routes.category.node(
113+
arguments: <String, String>{'id': category.id},
114+
)));
115+
```
116+
117+
Of course, there are other ways to navigate, primarily shortcuts for the most common cases.
118+
119+
```dart
120+
context.octopus.push(Routes.shop)
121+
```
122+
123+
But you can truly do anything you want.
124+
Just change the state, children, nodes, and arguments as you please.
125+
Everything is in your hands and just works fine, that's a declarative approach as it should be.
126+
127+
## [Guards](https://github.com/PlugFox/octopus/wiki/Guards)
128+
129+
Guards are a powerful tool for controlling navigation.
130+
They allow you to check the state of the router and mutate/cancel navigation if necessary.
131+
For example, you can check the user's authorization and redirect them to the login page if they are not authorized.
132+
133+
Examples:
134+
135+
1. [How to make an authentification guard and restore the previous state after login](https://github.com/PlugFox/octopus/blob/master/example/lib/src/common/router/authentication_guard.dart)
136+
2. [How to place a Home route at the root of the navigation stack](https://github.com/PlugFox/octopus/blob/master/example/lib/src/common/router/home_guard.dart)
137+
138+
## [Glossary](https://github.com/PlugFox/octopus/wiki/Glossary)
139+
140+
1. State - the overall state of the router can be mutable (while the user mutates the new desired state and in guards) or immutable (all other times). The state can include a hash table of arguments, which are global arguments of the current state. These can be used at your discretion.
141+
142+
2. Node - the components that constitute the state form a tree structure in the case of nested navigation. Each node has a name and arguments (usually parameters passed to a screen, like an identifier). At each level, within each list of nodes, the combination of name and arguments must be unique, as this forms the unique key of the node.
143+
144+
3. Route - router has a list of possible routes that can be used in the project. The router matches nodes and routes by their names. Routes contain information on how to construct a page for the navigator.
145+
146+
## [State structure](https://github.com/PlugFox/octopus/wiki/State-structure)
147+
148+
Let's take a look at the next nested tree which we want to get:
149+
150+
```
151+
Home
152+
Shop
153+
├─Catalog-Tab
154+
│ ├─Catalog
155+
│ ├─Category {id: electronics}
156+
│ ├─Category {id: smartphones}
157+
│ └─Product {id: 3}
158+
└─Basket-Tab
159+
├─Basket
160+
└─Checkout
161+
```
162+
163+
Also, we want the global argument `shop` with the value `catalog` to refer to a tab bar state.
164+
165+
Let's create the following state to represent our expectations:
166+
167+
```dart
168+
final state = OctopusState(
169+
intention: OctopusStateIntention.auto,
170+
arguments: <String, String>{'shop': 'catalog'},
171+
children: <OctopusNode>[
172+
OctopusNode(
173+
name: 'shop',
174+
arguments: <String, String>{},
175+
children: <OctopusNode>[
176+
OctopusNode(
177+
name: 'catalog-tab',
178+
arguments: <String, String>{},
179+
children: <OctopusNode>[
180+
OctopusNode(
181+
name: 'catalog',
182+
arguments: <String, String>{},
183+
children: <OctopusNode>[],
184+
),
185+
OctopusNode(
186+
name: 'category',
187+
arguments: <String, String>{'id': 'electronics'},
188+
children: <OctopusNode>[],
189+
),
190+
OctopusNode(
191+
name: 'category',
192+
arguments: <String, String>{'id': 'smartphones'},
193+
children: <OctopusNode>[],
194+
),
195+
OctopusNode(
196+
name: 'product',
197+
arguments: <String, String>{'id': '3'},
198+
children: <OctopusNode>[],
199+
),
200+
],
201+
),
202+
OctopusNode(
203+
name: 'basket-tab',
204+
arguments: <String, String>{},
205+
children: <OctopusNode>[
206+
OctopusNode(
207+
name: 'basket',
208+
arguments: <String, String>{},
209+
children: <OctopusNode>[],
210+
),
211+
OctopusNode(
212+
name: 'checkout',
213+
arguments: <String, String>{},
214+
children: <OctopusNode>[],
215+
),
216+
],
217+
),
218+
],
219+
),
220+
],
221+
);
222+
```
223+
224+
Take a look closer. That's a tree structure.
225+
Each component of that tree has a `List<OctopusNode> children` for children nodes and arguments for the current node.
226+
States have arguments, too; it's your global arguments.
227+
Each node also has a name; by this name, you can identify this node and link it with your routes table.
228+
229+
If we try to represent this state as a location string, we get something like that:
230+
`/home/shop/.catalog-tab/..catalog/..category~id=electronics/..category~id=smartphones/..product~id=3/.basket-tab/..basket/..checkout?shop=catalog`
231+
22232
## Changelog
23233

24234
Refer to the [Changelog](https://github.com/PlugFox/octopus/blob/master/CHANGELOG.md) to get all release notes.

example/lib/src/common/router/routes.dart

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ enum Routes with OctopusRoute {
4242
final String? title;
4343

4444
@override
45-
Widget builder(BuildContext context, OctopusNode node) => switch (this) {
45+
Widget builder(BuildContext context, OctopusState state, OctopusNode node) =>
46+
switch (this) {
4647
Routes.signin => const SignInScreen(),
4748
Routes.signup => const SignUpScreen(),
4849
Routes.home => const HomeScreen(),
@@ -92,25 +93,4 @@ enum Routes with OctopusRoute {
9293
node.children.add(route.node(arguments: {'id': id}));
9394
return state..arguments['shop'] = 'catalog';
9495
});
95-
96-
/// Pops the last [route] from the catalog tab.
97-
static void popFromCatalog(BuildContext context) =>
98-
context.octopus.setState((state) {
99-
final node = state.find((n) => n.name == 'catalog-tab');
100-
if (node == null || node.children.length < 2) {
101-
return state
102-
..removeByName(Routes.shop.name)
103-
..add(Routes.shop.node(
104-
children: <OctopusNode>[
105-
OctopusNode.mutable(
106-
'catalog-tab',
107-
children: <OctopusNode>[Routes.catalog.node()],
108-
),
109-
],
110-
))
111-
..arguments['shop'] = 'catalog';
112-
}
113-
node.children.removeLast();
114-
return state;
115-
});
11696
}

lib/src/controller/navigator/delegate.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ final class OctopusDelegate$NavigatorImpl extends OctopusDelegate
131131
pages.add(
132132
_defaultRoute.pageBuilder(
133133
context,
134+
currentConfiguration,
134135
_defaultRoute.node(),
135136
),
136137
);
@@ -219,7 +220,7 @@ final class OctopusDelegate$NavigatorImpl extends OctopusDelegate
219220
continue;
220221
}
221222
} else {
222-
page = route.pageBuilder(context, node);
223+
page = route.pageBuilder(context, currentConfiguration, node);
223224
}
224225
pages.add(page);
225226
} on Object catch (error, stackTrace) {

lib/src/state/state.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ final class OctopusNode$Immutable extends OctopusNode
719719
typedef DefaultOctopusPageBuilder = Page<Object?> Function(
720720
BuildContext context,
721721
OctopusRoute route,
722+
OctopusState state,
722723
OctopusNode node,
723724
);
724725

@@ -729,11 +730,11 @@ mixin OctopusRoute {
729730
static set defaultPageBuilder(DefaultOctopusPageBuilder fn) =>
730731
_defaultPageBuilder = fn;
731732
static DefaultOctopusPageBuilder _defaultPageBuilder =
732-
(context, route, node) => MaterialPage<Object?>(
733+
(context, route, state, node) => MaterialPage<Object?>(
733734
key: route.createKey(node),
734735
child: InheritedOctopusRoute(
735736
node: node,
736-
child: route.builder(context, node),
737+
child: route.builder(context, state, node),
737738
),
738739
name: node.name,
739740
arguments: node.arguments,
@@ -760,7 +761,7 @@ mixin OctopusRoute {
760761
/// ```dart
761762
/// final OctopusNode(:name, :arguments, :children) = node;
762763
/// ```
763-
Widget builder(BuildContext context, OctopusNode node);
764+
Widget builder(BuildContext context, OctopusState state, OctopusNode node);
764765

765766
/// Create [LocalKey] for [Page] of this route using [OctopusNode].
766767
LocalKey createKey(OctopusNode node) => ValueKey<String>(node.key);
@@ -771,11 +772,12 @@ mixin OctopusRoute {
771772
///
772773
/// If you want to override this method, do not forget to add
773774
/// [InheritedOctopusRoute] to the element tree.
774-
Page<Object?> pageBuilder(BuildContext context, OctopusNode node) =>
775+
Page<Object?> pageBuilder(
776+
BuildContext context, OctopusState state, OctopusNode node) =>
775777
node.name.endsWith('-dialog')
776778
? OctopusDialogPage(
777779
key: createKey(node),
778-
builder: (context) => builder(context, node),
780+
builder: (context) => builder(context, state, node),
779781
name: node.name,
780782
arguments: node.arguments,
781783
)
@@ -784,13 +786,13 @@ mixin OctopusRoute {
784786
key: createKey(node),
785787
child: InheritedOctopusRoute(
786788
node: node,
787-
child: builder(context, node),
789+
child: builder(context, state, node),
788790
),
789791
name: node.name,
790792
arguments: node.arguments,
791793
fullscreenDialog: node.name.endsWith('-dialog'),
792794
)
793-
: _defaultPageBuilder.call(context, this, node);
795+
: _defaultPageBuilder.call(context, this, state, node);
794796

795797
/// Construct [OctopusNode] for this route.
796798
OctopusNode$Mutable node({

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: octopus
22
description: "A cross-platform declarative router for Flutter with a focus on state and nested navigation. Made with ❤️ by PlugFox."
33

4-
version: 0.0.1-pre.2
4+
version: 0.0.2
55

66
homepage: https://github.com/PlugFox/octopus
77

test/src/state_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ void main() => group('state', () {
2727
});
2828

2929
test('empty_url', () {
30-
const location = '';
30+
const location = '/';
3131
final state = StateUtil.decodeLocation(location);
3232
expect(state.location, equals(location));
3333
expect(state.uri, equals(Uri.parse(location)));
34-
expect(state.uri, equals(Uri()));
3534
expect(state.arguments, isEmpty);
3635
expect(state, equals(OctopusState.empty()));
3736
});

0 commit comments

Comments
 (0)