Skip to content

Instantly share code, notes, and snippets.

@by90
Created March 24, 2021 08:56
Show Gist options
  • Save by90/3151505f82539251dae7a4248a162821 to your computer and use it in GitHub Desktop.
Save by90/3151505f82539251dae7a4248a162821 to your computer and use it in GitHub Desktop.
[Basic implementation of declarative routes] #Flutter
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
enum PossibleAppParams {
phoneId,
}
enum PossibleAppRoutes {
unknown,
home,
phoneSingle,
testRoute,
}
class AppRoutes {
static AppRouteManager all = AppRouteManager(
routes: [
AppRoute(
name: PossibleAppRoutes.home,
path: '/',
paramsList: [],
),
AppRoute(
name: PossibleAppRoutes.phoneSingle,
path: '/phone/_',
paramsList: [PossibleAppParams.phoneId],
),
],
);
}
class AppPath {
final AppRoute route;
Map<PossibleAppParams, String> params;
AppPath(this.route, this.params);
factory AppPath.decode(RouteInformation info) {
final location = info.location ?? '';
final route = AppRoutes.all.getRouteFromLocation(location);
print('AppPath.decode(): ${route.name}');
return route.toPath(location);
}
RouteInformation encode() {
String location = route.getLocation(params);
print('AppPath.encode(): $location');
return RouteInformation(location: location);
}
}
class AppRouteManager {
final List<AppRoute> _routes;
final AppRoute _unknownRoute = AppRoute(
name: PossibleAppRoutes.unknown,
path: '/not-found',
paramsList: [],
);
AppRouteManager({required List<AppRoute> routes}) : _routes = routes;
AppPath currentConfiguration(AppModel model) {
print('currentConfiguration: ${model.currentRoute} ${model.params}');
return AppPath(
_routes.firstWhere(
(element) => element.name == model.currentRoute,
orElse: () => _unknownRoute,
),
model.params,
);
}
AppRoute getRouteFromLocation(String location) {
if (location.contains('_')) return _unknownRoute;
final uri = Uri.parse(location);
for (final route in _routes) {
if (route.path.length == uri.pathSegments.length) {
var found = true;
for (var i = 0; i < uri.pathSegments.length && found; i++) {
if (!route.path[i].contains('_') &&
uri.pathSegments[i] != route.path[i]) found = false;
}
if (found) return route;
}
}
return _unknownRoute;
}
}
class _AppRouteHelper {
static Map<int, int> makeMapping(
String path, List<PossibleAppParams> paramsList) {
RegExp exp = RegExp('_');
if (exp.allMatches(path).length != paramsList.length) {
throw Exception(
'ParamsList length must be equal to the number of _ in path',
);
}
final pathList = Uri.parse(path).pathSegments;
Map<int, int> mapping = {};
for (var pathIndex = 0, paramIndex = 0;
pathIndex < pathList.length && paramIndex < paramsList.length;
pathIndex++) {
if (pathList[pathIndex].contains('_')) {
mapping[pathIndex] = paramIndex++;
}
}
return mapping;
}
}
class AppRoute {
final PossibleAppRoutes name;
final List<PossibleAppParams> paramsList;
List<String> _path;
Map<int, int> _paramsMapping;
List<String> get path => _path;
AppRoute({
required this.name,
required String path,
required this.paramsList,
}) : _path = Uri.parse(path).pathSegments,
_paramsMapping = _AppRouteHelper.makeMapping(path, paramsList);
AppPath toPath(location) {
final pathList = Uri.parse(location).pathSegments;
Map<PossibleAppParams, String> params = {};
for (final mapping in _paramsMapping.entries) {
params[paramsList[mapping.value]] = pathList[mapping.key];
}
return AppPath(this, params);
}
String getLocation(Map<PossibleAppParams, String> params) {
var locationList = List<String>.from(_path);
for (final mapping in _paramsMapping.entries) {
locationList[mapping.key] = params[paramsList[mapping.value]] ?? '';
}
final location = locationList.fold<String>(
'/',
(previus, element) => previus + element + '/',
);
print('getLocation: $location');
return location.substring(0, location.length - 1);
}
}
class AppRouteParser extends RouteInformationParser<AppPath> {
@override
Future<AppPath> parseRouteInformation(RouteInformation info) async =>
AppPath.decode(info);
@override
RouteInformation restoreRouteInformation(AppPath path) => path.encode();
}
class AppRouterDelegate extends RouterDelegate<AppPath> with ChangeNotifier {
final AppModel appModel;
final GlobalKey<NavigatorState> navigatorKey;
AppRouterDelegate({required this.appModel})
: navigatorKey = GlobalKey<NavigatorState>() {
appModel.addListener(notifyListeners);
}
@override
AppPath get currentConfiguration =>
AppRoutes.all.currentConfiguration(appModel);
@override
void dispose() {
appModel.removeListener(notifyListeners);
super.dispose();
}
@override
Widget build(BuildContext context) {
print("RouterDelegate.build()");
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Navigator(
pages: [
MaterialPage(
child: ListView(
children: [
for (final phone in appModel.phones) ...[
ListTile(
title: Text(phone.name),
onTap: () => {appModel.selectedPhone = phone},
)
]
],
),
),
if (appModel.isPhoneSingle)
MaterialPage(
child: Center(
child: Text(appModel.selectedPhone!.name),
),
),
if (appModel.isUnknown)
MaterialPage(
child: Center(
child: Text("Page not found"),
),
),
],
onPopPage: (Route<dynamic> route, dynamic result) {
print("Navigator.onPopPage()");
if (route.didPop(result)) {
return false;
}
appModel.selectedPhone = null;
return true;
},
),
);
}
@override
Future<void> setNewRoutePath(AppPath newLink) async {
print("RouterDelegate.setNewRoutePath(): ${newLink.route.name}");
if (newLink.route.name == PossibleAppRoutes.unknown) {
appModel.isUnknown = true;
} else {
appModel.isUnknown = false;
}
if (newLink.route.name == PossibleAppRoutes.phoneSingle) {
appModel.selectedId =
int.parse(newLink.params[PossibleAppParams.phoneId]!);
}
if (newLink.route.name == PossibleAppRoutes.home) {
appModel.selectedPhone = null;
}
}
@override
Future<bool> popRoute() async {
print("RouterDelegate.popRoute()");
return false;
}
}
class Phone {
final String name;
final String description;
final int year;
const Phone({
required this.name,
required this.description,
required this.year,
});
}
class AppModel with ChangeNotifier {
final List<Phone> _phones = [
Phone(
name: 'Samsung Galaxy',
description: 'Lorem ipsum dolor et.',
year: 2018,
),
Phone(
name: 'IPhone 7',
description: 'Lorem ipsum dolor et.',
year: 2019,
),
Phone(
name: 'RedMi Note 7',
description: 'Lorem ipsum dolor et.',
year: 2020,
),
];
int _selectedId = -1;
bool _isUnknown = false;
Phone? _selectedPhone;
List<Phone> get phones => _phones;
int get selectedId => _selectedId;
Phone? get selectedPhone => _selectedPhone;
bool get isUnknown => _isUnknown;
bool get isHome => _selectedId == -1;
bool get isPhoneSingle => _selectedId != -1;
PossibleAppRoutes get currentRoute {
if (isUnknown) {
return PossibleAppRoutes.unknown;
} else if (isHome) {
return PossibleAppRoutes.home;
} else if (isPhoneSingle) {
return PossibleAppRoutes.phoneSingle;
}
return PossibleAppRoutes.unknown;
}
Map<PossibleAppParams, String> get params {
Map<PossibleAppParams, String> params = {};
if (currentRoute == PossibleAppRoutes.phoneSingle)
params[PossibleAppParams.phoneId] = selectedId.toString();
return params;
}
set isUnknown(bool v) {
_isUnknown = v;
notifyListeners();
}
set selectedId(int id) {
if (id >= 0 && id < _phones.length) {
_selectedPhone = _phones[id];
_selectedId = id;
} else {
_isUnknown = true;
_selectedPhone = null;
_selectedId = -1;
}
notifyListeners();
}
set selectedPhone(Phone? newPhone) {
if (newPhone != null && _phones.contains(newPhone)) {
_selectedPhone = newPhone;
_selectedId = _phones.indexOf(newPhone);
} else {
_selectedPhone = null;
_selectedId = -1;
}
notifyListeners();
}
}
class App extends StatefulWidget {
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
final AppRouteParser _parser = AppRouteParser();
AppRouterDelegate? _delegate;
@override
void initState() {
_delegate = AppRouterDelegate(appModel: context.read<AppModel>());
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: _parser,
routerDelegate: _delegate!,
);
}
}
void main() {
// setPathUrlStrategy();
AppModel appModel = AppModel();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: appModel),
],
child: App(),
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment