Closed Luckey-Elijah closed 1 week ago
after some experiment with the hack
extension CurrentLocation on DuckRouter {
Location? get currentLocation {
final state = routeInformationProvider.value.state as dynamic;
try {
final location = state.location; // ignore: avoid_dynamic_calls
if (location is Location) return location;
return null;
} catch (_) {
return null;
}
}
}
It only provides the initial location. Which is not useful :/ So this maybe would be a feature request?
As a direct answer: you can do this:
final stack =
DuckRouter.of(context).routerDelegate.currentConfiguration;
final currentLocation = stack.locations.last;
However, you will need to keep in mind that this will quickly get complicated if you use stateful locations, because then you might need to dig deeper into that location.
Could you describe what problem you're facing that you would find this useful?
I am using StatefulLocation
to build a "shell" widget and want to style a button that is a part of the shell widget if it's the current location.
@override
StatefulLocationBuilder get childBuilder {
return (BuildContext context, DuckShell shell) {
final Location location = /* get location here */;
Color? color<T extends Location>() {
return (location is T)
? Theme.of(context).colorScheme.secondaryContainer
: null;
}
return Row(
children: [
SizedBox(
width: 120,
child: Column(
children: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: color<RouteOneLocation>(),
),
onPressed: () {},
child: const Text('Route One'),
),
],
),
),
Expanded(child: shell),
],
);
};
So I do wan to be aware of the StatefulLocation that is mounted..
As I inspect the DuckShellState, I can imagine that it would not be too tricky to expose a currentIndex
getter and then build retrieve the from there
Location getLocation(DuckRouter router) {
final last = router.routerDelegate.currentConfiguration.locations.last;
if (last is StatefulLocation) {
// this value is not yet available
return last.children[last.state.currentIndex];
}
return last;
}
What I am currently doing as a work around is using a LocationInterceptor
and setting a value in my state management through that interceptor but it of course doesn't work for redirect from other interceptor
class CurrentLocationInterceptor extends LocationInterceptor {
CurrentLocationInterceptor({required this.add, super.pushesOnTop});
final ValueSetter<Location> add;
@override
Location? execute(Location to, Location? from) {
add(to);
return null;
}
}
Thanks for the use case.
Have you tried using onNavigate
on DuckRouter? It's a callback that reports about every navigational change.
Alternatively, I think your function should work like this:
Location getLocation(DuckRouter router) {
final last = router.routerDelegate.currentConfiguration.locations.last;
if (last is StatefulLocation) {
return last.state.currentRouterDelegate.currentConfigurations.locations.last;
}
return last;
}
the last.state.currentRouterDelegate.currentConfiguration
seems like it would work. I did not try it out because currentRouterDelegate
was not "correctly" typed to anything except RouterDelegate<T?>
/RouterDelegate<dynamic>
. Could you expose some interface to that last.state.currentRouterDelegate
in order to promote discoverability?
More specifically this is how i used since the Router delegate doesn't have the type i expect RouterDelegate<LocationStack>
if (last is StatefulLocation) {
// ignore: avoid_dynamic_calls
return last.state.currentRouterDelegate.currentConfiguration.locations.last as Location;
}
Hmm. I feel conflicted about that. I don't really want to promote this kind of usage, it goes against a lot of the philosophy behind DuckRouter (straightforward, no magic). Does onNavigate work for you?
Does onNavigate work for you?
Note really.. When I set a new value in the ValueNotifer
notifyListeners()
is called, but this notification happens while the navigation is happening so I get setState() or markNeedsBuild called during build
expections.
I don't really want to promote this kind of usage
If you are talking about this https://github.com/collectiveuk/packages/issues/30#issuecomment-2353990047 "solution", then yes I agree. I don't want to do this. Could you expose an API to safely access this value?
Sorry, we're getting deeper into this problem so I just have a few more questions. I am starting to feel like this is an actual problem (and a legit use case), I just want to make sure there is no existing way to solve it.
In your code example here: https://github.com/collectiveuk/packages/issues/30#issuecomment-2351042794. How do you switch children in the duck shell? In theory, when you change child you could take that index and use it to update your color, right? For example:
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
destinations: ...
selectedIndex: _currentIndex,
onDestinationSelected: (int i) {
widget.shell.switchChild(i);
setState(() {
_currentIndex = i;
});
},
backgroundColor: colors[_currentIndex], // <----
),
body: widget.shell,
);
}
Because I think my core question is: is this a problem for DuckRouter to solve or for app developers (in their own code)?
No worries! I'm more than happy to work this out. I believe that the router should be the "source of truth" of the current location. Which is why I'm hesitant to hook into the the side effects of the router (onNavigate
and interceptors) and user interactions (like onTap: router.navigate(...)
) to assume the current location.
In the example with StatefulLocation
, it's definitely doable with using the same value as what is passed to shell.switchChild
. But there are other factors that impact what is actually set in the router such as an Interceptor or widgets lower in the widget tree will may navigate to other locations - not just what's called with switchChild
.
Take this following example
```dart
import 'package:duck_router/duck_router.dart';
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: router);
}
}
final router = DuckRouter(initialLocation: RootLocation());
class RootLocation extends StatefulLocation {
@override
String get path => 'root';
@override
List
Each of the child screens are able to navigate.The _ScaffoldShellState
does not know that a navigation event happened and thus does not update its index
.
class Page1Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Page1Screen'),
TextButton(
// navigating "inside" the shell
onPressed: () => DuckRouter.of(context).navigate(
to: Child2Location(),
replace: true,
),
child: Text('Go to Page2Screen'),
)
],
);
}
}
I want to know how to get the current location in instances like this ^. Does this example help what I'm trying to achieve?
I believe that the router should be the "source of truth" of the current location.
This is key to our conversation I think! My general stance in designing DuckRouter has been that this is not the responsibility of a router. I observe that a lot of other routers do consider this their problem. Because of that, you have to make concessions to the API that I think complicate things, and often even compromise on features. I consider this part of state management, and I do not necessarily see why DuckRouter would have to save this state for the developer. Yes, of course, we track routing internally. But so does the developer. They tell DuckRouter to go somewhere. In our app at Onsi, we do similar things. But we save the state for that in external services from DuckRouter. We use Riverpod but a developer could also use an InheritedWidget, or Blocs, etc;
If I think about how to support this feature to get the current location for example, things quickly become complicated. What exactly is the current location when dealing with nested state? If I have RootLocation containing two locations, which have both navigated somewhere, what exactly is the current location? Is it RootLocation? Is it one of the children? etc. What if one of the child pages shows a dialog on top of the root? I think an app developer implementing DuckRouter is much more able to conclusively answer that question than the router.
So does the mean the answer is to the original question (How to get the current Location
of DuckRouter
?) "you can not know unless you track it outside of the router"? Also could you share an example of how it can be achieved using riverpod
, ChangeNotifer
, or Bloc
? I don't want to leave future developers without some type of "canonical" solution to this use case
The answer to the original question is that you can find this from the delegate. However, for more advanced use cases the recommendation is to keep track of this yourself. For example, if we want to specify the color of the bottom bar in our stateful location based on what child is selected (and what's happening inside those), we could do something like this when using riverpod:
@riverpod
class BottomBarColorService extends _$BottomBarColorService {
@override
Color build() {
return Colors.red;
}
void changeColor(Color color) {
state = color;
}
}
Then just watch that value in the bottom bar, and change it whenever you navigate, or change index.
@JaspervanRiet thanks for all your help. I think all my questions are answered (even though it's not exactly what I want), I can achieve what I need to with this info :)
Given the following code that would exist in a
Widget.build
method how could I read the currentLocation
or top-most in the stack?I tried using
router.routeInformationProvider.value.state.location
But this is obviously not an ideal way to access.
Could you expose a type-safe API on DuckRouter that let's us get it?