Created
March 24, 2021 08:56
-
-
Save by90/3151505f82539251dae7a4248a162821 to your computer and use it in GitHub Desktop.
[Basic implementation of declarative routes] #Flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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