Closed oravecz closed 1 year ago
You can use VNester.stackedRoutes
. Would that work for your use case?
I took your example and added a stackedRoutes property to the Settings route.
My intent is to learn how I can get the SlideOverWidget
to fill the viewport, even though it is a stacked route inside the Settings route which, in turn, is a nested route inside MyScaffold
.
import 'package:flutter/material.dart';
import 'package:vrouter/vrouter.dart';
void main() {
runApp(
VRouter(
debugShowCheckedModeBanner: false,
// VRouter acts as a MaterialApp
mode: VRouterMode.history,
// Remove the '#' from the url
// logs: [VLogLevel.info], // Defines which logs to show, info is the default
routes: [
VWidget(
path: '/login',
widget: LoginWidget(),
stackedRoutes: [
ConnectedRoutes(), // Custom VRouteElement
],
),
// This redirect every unknown routes to /login
VRouteRedirector(
redirectTo: '/login',
path: r'*',
),
],
),
);
}
// Extend VRouteElementBuilder to create your own VRouteElement
class ConnectedRoutes extends VRouteElementBuilder {
static final String profile = 'profile';
static void toProfile(BuildContext context, String username) =>
context.vRouter.to('/$username/$profile');
static final String settings = 'settings';
static final String slideOver = 'slideover';
static void toSettings(BuildContext context, String username) =>
context.vRouter.to('/$username/$settings');
@override
List<VRouteElement> buildRoutes() {
return [
VNester.builder(
// .builder constructor gives you easy access to VRouter data
path:
'/:username', // :username is a path parameter and can be any value
widgetBuilder: (_, state, child) => MyScaffold(
child,
currentIndex: state.names.contains(profile) ? 0 : 1,
),
nestedRoutes: [
VWidget(
path: profile,
name: profile,
widget: ProfileWidget(),
),
VWidget(
path: settings,
name: settings,
widget: SettingsWidget(),
// Custom transition
buildTransition: (animation, ___, child) {
return ScaleTransition(
scale: animation,
child: child,
);
},
stackedRoutes: [
VWidget(
name: slideOver,
path: slideOver,
widget: SlideOverWidget(),
)
],
),
],
),
];
}
}
class LoginWidget extends StatefulWidget {
@override
_LoginWidgetState createState() => _LoginWidgetState();
}
class _LoginWidgetState extends State<LoginWidget> {
String name = 'bob';
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Enter your name to connect: '),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: Form(
key: _formKey,
child: TextFormField(
textAlign: TextAlign.center,
onChanged: (value) => name = value,
initialValue: 'bob',
),
),
),
),
],
),
// This FAB is shared and shows hero animations working with no issues
FloatingActionButton(
heroTag: 'FAB',
onPressed: () {
setState(() => (_formKey.currentState!.validate())
? ConnectedRoutes.toProfile(context, name)
: null);
},
child: Icon(Icons.login),
)
],
),
),
);
}
}
class MyScaffold extends StatelessWidget {
final Widget child;
final int currentIndex;
const MyScaffold(this.child, {required this.currentIndex});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('You are connected'),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.person_outline), label: 'Profile'),
BottomNavigationBarItem(
icon: Icon(Icons.info_outline), label: 'Info'),
],
onTap: (int index) {
// We can access this username via the path parameters
final username = VRouter.of(context).pathParameters['username']!;
if (index == 0) {
ConnectedRoutes.toProfile(context, username);
} else {
ConnectedRoutes.toSettings(context, username);
}
},
),
body: child,
// This FAB is shared with login and shows hero animations working with no issues
floatingActionButton: FloatingActionButton(
heroTag: 'FAB',
onPressed: () => VRouter.of(context).to('/login'),
child: Icon(Icons.logout),
),
);
}
}
class ProfileWidget extends StatefulWidget {
@override
_ProfileWidgetState createState() => _ProfileWidgetState();
}
class _ProfileWidgetState extends State<ProfileWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
// VNavigationGuard allows you to react to navigation events locally
return VWidgetGuard(
// When entering or updating the route, we try to get the count from the local history state
// This history state will be NOT null if the user presses the back button for example
afterEnter: (context, __, ___) => getCountFromState(context),
afterUpdate: (context, __, ___) => getCountFromState(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
VRouter.of(context).to(
context.vRouter.url,
isReplacement: true,
historyState: {'count': '${count + 1}'},
);
setState(() => count++);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.blueAccent,
),
padding:
EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0),
child: Text(
'Your pressed this button $count times',
style: buttonTextStyle,
),
),
),
SizedBox(height: 20),
Text(
'This number is saved in the history state so if you are on the web leave this page and hit the back button to see this number restored!',
style: textStyle,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
void getCountFromState(BuildContext context) {
setState(() {
count = (VRouter.of(context).historyState['count'] == null)
? 0
: int.tryParse(VRouter.of(context).historyState['count'] ?? '') ?? 0;
});
}
}
class SettingsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Did you see the custom animation when coming here?',
style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
final username =
VRouter.of(context).pathParameters['username']!;
context.vRouter.toNamed(
ConnectedRoutes.slideOver,
pathParameters: {
'username': username,
},
);
},
child: Text('Push Route', style: buttonTextStyle),
),
],
),
),
);
}
}
final textStyle = TextStyle(color: Colors.black, fontSize: 16);
final buttonTextStyle = textStyle.copyWith(color: Colors.white);
class SlideOverWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Slide In Widget'),
backgroundColor: Colors.white,
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'This is a screen with a back button, and I would like it to '
'fill the whole viewport (expand outside the nested ',
style: textStyle.copyWith(fontSize: textStyle.fontSize! + 2),
),
],
),
),
);
}
}
As I was saying you have to use VNester.stackedRoutes
.
The only issue with this API is that is does not know how to redirect pop
properly so you have to wire it manually using a VGuard
for example.
Thanks for the working example. This wasn't intuitive for me, so please confirm these generalizations I am drawing from your code.
nestedRoutes
property hierarchy.pop()
behavior by manually inspecting the url path to determine the destinationsettings
is in the url paths? It seems to prevent me from navigating anywhere else than to a url under the settings pages.Concerning your assertions:
Answer to your follow-ups:
/bob/profile
and /alice/profile
are 2 possible path of VNester
(because VNester
path is :username
) which represent 2 differents places. Therefore you do not add a key to VNester
. The effect is that the state of VNester
and everything bellow would be reset if your switched from /bob/profile
to /alice/profile
(which is what you want)/bob/settings
and /bob/settings/slideover
represent the same settings, therefore you use a key. The concrete effect is that you won't loose your Settings
state when you go to slideover
/bob/settings/slideover
, then the settings should be shown inside VNester
. Basically, every time you use nestedRoutes
and stackedRoute
you have to make sure that there is always a matching route in nestedRoutes
so that VNester
knows what to display therepathOfNested + '/popover'
and where you hard code the pop to nativate to the currentPath - '/popover'
b. Using VGuard
was a bad idea because it impacted every natigation. The good think to do is to use WillPopScope
to only influence the pop. Warning: I think you would want to use BackButtonListener
as well to override android back button behavior to do the same as pop.3.b
Closing since I answered every questions. Feel free to reopen if some doubts persist.
I have a deep level of nested routes, including wrapping widgets that provide headers and nav bar, but also sometimes BlocProviders.
I am currently declaring my stacked routes as children of the nested routes, and those stacked pages are displaying within the child areas of the nested routes.
I want the stacked routes to visually cover the headers and nav bars, not be displayed within the child areas of the nested route.
Do I need to declare these routes at the root (peer with my nested routes) for them to cover the nested routes?
Or, do I achieve this using specific Keys?