-
-
Notifications
You must be signed in to change notification settings - Fork 149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scope race conditions (continued) #326
Comments
Hmm, interesting. that problem is slightly different than the other race conditions that dropScope solves. |
Not sure this is an issue that GetIt needs to solve. The problem is go-router shows a new route before calling dispose, so the OPs solution is not a good choice. GetIt is working as expected, if you call register before calling popScope, of course it ends up in the current scope. Either GR should have a With that said the |
I just mad a small fix to the get_it_mixin's pushscope, that ensures that the pushed scope has a unique name and it uses that to drop the scope when the widget is destroyed. |
@esDotDev I think an isClosed would be possible without adding too much complexity, but first I'd like to know if the above proposal solves that problem. |
but calling pushScope in build() is not what I wanted.
I'll create an issue in a GR repo when I find the time, but it's hard to predict whether they'll fix that, and when (their development is veeery slow). It would be nice to have a possibility to solve it on the get_it side. |
@subzero911 I just added an |
oh, I was a bit too quick, I forgot to run the the existing test, my bad |
ok, now all current tests run. |
Yup, I don't mind helping. |
Cool, waiting for your PR, |
yep but not in the readme and if you want to improve the docs, always welcome |
Working on tests. How to check if the object is in a certain scope? |
I think you can only test it indirekt by first pushing a closed scope, then registering and popping the scope and testing if it is still there |
Also I found a little bug. |
No, it won't solve, if I registered a new singleton right before popScope() was called. I added yet another PR #330 |
I do wonder if a warning should be printed out if you attempt to register an object with a final scope. In this case it would be expected, as the OP wants to essentially call methods in the wrong order, but still have things work. (Should be But in other cases, where this is not desired, it might be nice to at least indicate to the user that they might be doing something unexpected? I would guess that the expected/intuitive use case for Just a thought. I also wonder why OP doesn't just register the details singleton when the app starts up, as it is never disposed of. Seems like a lazy singleton would be perfect here, and sidestep this whole issue of trying to register a singleton in the base scope, while another scope exists during transition. Then you end up with the easier to follow: |
Also, just thinking out loud, but if we could access the list of scopes, we could probably also solve the problem that way, a bit more directly.
or maybe
|
Why have you decided it's never disposed? I will dispose it in the onDispose callback as well. |
You mean if the top level scope is final and someone tries to register something instead of silently register it in the next open scope?
Am 26. Mai 2023, 22:09 +0200 schrieb Shawn ***@***.***>:
… I do wonder if a warning should be printed out if you attempt to register an object with a final scope.
In this case it would be expected, as the OP wants to essentially calls methods in the wrong order, but still have things work. But in other cases, where this is not desired, it might be nice to at least indicate to the user that they might be doing something unexpected?
Just a thought.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
What would be easy is to add an optional scopename when registering. That way you could make sure it gets in the scope you want. Might not often be needed but maybe in some edge cases it could be helpful
Am 26. Mai 2023, 22:14 +0200 schrieb Shawn ***@***.***>:
… Also, just thinking out loud, but if we could access the list of scopes, we could probably also solve the problem that way, a bit more directly.
GoRoute(
path: '/details',
builder: (context, state) => const DetailsScreen(),
onInit: () => getIt.registerSingleton(Bar(), scope: getIt.scopes.first);
}
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Just based on the example. If it's just local controller, why use scopes at all? Couldn't you just register the singleton in If the boilerplate bothers you, I think you could probably make a generic mixin that handles this easily enough too... psuedocode: class LocalControllerMixin<T> on State {
T get controller; // each view must implement this
void initState(){
getIt.registerSingleton<T>(getController);
}
void dispose(){
getIt.unregister<T>();
}
} Then a view would just need something like: MyViewState extends State with LocalControllerMixin<Bar> {
Bar get controller => Bar();
}
Ya exactly, this sort of magic behavior of registering in the next available scope seems like it could be confusing / problematic, hard to debug race conditions etc. |
Say, in 1st route it's multiple controllers, and I decided to put them into scope. |
Sure, but only because you want to register a new singleton before the old one is popped, which is because your callback is firing later than it should be, which is an implementation detail of GR. Maybe it's fine? Just trying to play devils advocate here, really this is not GetIt's problem, and I'd just be careful about adding a specific feature for this one issue, unless the feature actually has value on its own. I think having |
I'd actually leaning to make all scopes final by default if an initFunction is given and throwing an exception if you try to register something outside the init scope unless you enable a static flag 'automaticRegisterInNextopenScope because I agree if a scope was declared final or closed (still not sure what's the better name) a registration outside of init it probably an error
Am 26. Mai 2023, 23:19 +0200 schrieb Shawn ***@***.***>:
… Sure, but only because you want to register a new singleton before the old one is popped, which is because your callback is firing later than it should be, which is an implementation detail of GR.
Maybe it's fine? Just trying to play devils advocate here, really this is not GetIt's problem, and I'd just be careful about adding a specific feature for this one issue, unless the feature actually has value on its own.
I think having isFinal but showing a warning when scopes are pushed to it might be a nice middle ground. Then the feature exists primarily as you would expect it to, which is to indicate that no new singletons should be registered while it exists, but then also opening the door to this somewhat hacky approach where you can register something, and rely on it implicitly being assigned to the latest non-final scope.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
It does strike me that the optional scope name is the way to go, as it is the most clear and does not do any implicit behavior, and it solves for all cases where order-of-operations is causing some issues. But I'm not sure how we would make that work in the case where you want to use the first non-named scope... as passing But maybe we don't? Maybe it's just a requirement to use named scopes for this sort of use case. ProposalThe main downside to forcing scopes here is the extra boilerplate. What if calling Then the example above could be written as: GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
onInit: () => getIt.registerSingleton(Foo(), scope: 'profile');
onDispose: () => getIt.dropScope('profile'),
),
GoRoute(
path: '/details',
builder: (context, state) => const DetailsScreen(),
onInit: () => getIt.registerSingleton(Bar(), scope: 'details');
onDispose: () => getIt.dropScope('details'),
} Now there is no concern about order of operations, and no magic behavior. Optionally a developer could manually call |
I'm with you that the implicit behaviour isn't a good solution and that the optional name is a good idea. Actually the first named scope has a name 'baseScope' (maybe we could rename it to get_it) So either we rethink how disposal is implemented in future get_it or I'm not really pro automatic creation of scopes when registering. Definitely removing the automatic registration in the base scope if the top scope is final. Questions:
if someone tries to register in a closed scope we throw an exception/assertion |
That's basically what I proposed in the first post #316 (comment) |
final or sealed
Please don't do, |
Ah I didn't realize these names already existed. I think we'd want to make them |
Would you make it static const members or just independent constants?
Am 29. Mai 2023, 17:14 +0200 schrieb Shawn ***@***.***>:
… Ah I didn't realize these names already existed. I think we'd want to make them const values so that users of the lib can reference them, and then the named scopes feature seems pretty solid and should fully address this use case.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Personally I like static consts, because naming imports is a pain, and without named imports (or a class name) it hurts readability imo. But I think this is rather subjective. |
We could also potentially have a Might be a good idea to prefix these as to avoid any potential conflicsts with user strings:
|
Yeah, that could be a nice option. |
Nice to see this topic, trying to implement same logic. @subzero911 Just wondering which version of |
It's my own wrapper. |
I'm using get_it and go_router. Would you mind if you shared it ? How did you make the GoRoute lifecycle aware ? |
import 'package:flutter/widgets.dart';
class GoRouteWrapper extends StatefulWidget {
const GoRouteWrapper({
required this.child,
this.onInit,
this.onChangeDependencies,
this.onAfterLayout,
this.onDispose,
super.key,
});
final Widget child;
final void Function()? onInit;
final void Function(BuildContext context)? onChangeDependencies;
final void Function(BuildContext context)? onAfterLayout;
final void Function(BuildContext context)? onDispose;
@override
State<GoRouteWrapper> createState() => _GoRouteWrapperState();
}
class _GoRouteWrapperState extends State<GoRouteWrapper> {
@override
void initState() {
super.initState();
widget.onInit?.call();
if (widget.onAfterLayout == null) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onAfterLayout?.call(context);
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onChangeDependencies?.call(context);
}
@override
void dispose() {
widget.onDispose?.call(context);
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
} Usage: GoRoute(
name: RouteNames.newTask,
path: 'new-task',
builder: (context, state) => GoRouteWrapper(
onInit: () => GetIt.I.registerSingleton<TaskCreationController>(TaskCreationController()),
onDispose: (context) => GetIt.I.unregister<TaskCreationController>(),
child: NewTaskScreen(editableTask: state.extra as Task?),
),
), |
@esDotDev what are your thoughts on this here #326 (comment) |
I guess I would expect that if a scope if in the process of disposing we can consider it disposed, and act as if it is gone already. So no need to throw an assertion, just use the next scope in the stack and go from there? |
But what if the next one is final?
Am 1. Juni 2023, 17:01 +0200 schrieb Shawn ***@***.***>:
… > Yeah, that could be a nice option. I'm just pondering what we should do if a new registration occurs while the popScope isn't finished and was not awaited. If we don't automatically register in the next nonfinal scope. Would it be then better to introduce another internal scopestate 'disposing' and throw an assertion if a registration occurs in that phase a la 'Please await 'pop/dropScope' before registering another object?
I guess I would expect that if a scope if in the process of disposing we can consider it disposed, and act as if it is gone already. So no need to an assertion, just use the next scope in the stack and go from there?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Then it would throw an error, and they need to specify which non-final scope they are targeting? I would defer to you here though, these are just my initial thoughts on things, and you're much more experienced with various use cases. I only ever use scopes for testing, and don't really use the dispose functionality much, so not sure how much my feedback is valid. |
No worries, I value your opinion very much. It really helps to have a sparing partner in this decisions
BTW can you live with the watch_it naming now?
Am 1. Juni 2023, 17:33 +0200 schrieb Shawn ***@***.***>:
… Then it would throw an error, and they need to specific which non-final scope they are targeting?
I would defer to you here though, these are just my initial thoughts on things, and you're much more experienced with various use cases. I only ever use scopes for testing, and don't really use the dispose functionality much, so not sure how much my feedback is valid.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
I'm keeping continue on this thread because i'm having same problem(race condition). @subzero911 I just implemented For example when i navigate to As a result it gives me error: PreferencesViewModel is already registered GoRoute(
path: RoutePaths.preferences,
parentNavigatorKey: parentNavigatorKey,
builder: (context, state) => GoRouteWrapper(
onInit: () => GetIt.I.registerSingleton<PreferencesViewModel>(PreferencesViewModel()..init()),
onDispose: (context) => GetIt.I.unregister<PreferencesViewModel>(disposingFunction: (vm) => vm.onDispose()),
child: PreferencesScreen(),
),
),
GoRoute(
path: RoutePaths.languages,
parentNavigatorKey: parentNavigatorKey,
builder: (context, state) => GoRouteWrapper(
onInit: () => GetIt.I.registerSingleton<LanguagesViewModel>(LanguagesViewModel()..init()),
onDispose: (context) => GetIt.I.unregister<LanguagesViewModel>(disposingFunction: (vm) => vm.onDispose()),
child: LanguagesScreen(),
),
), |
if your disposing function returns a future the unregister call has to be awaited to not get into trouble |
if you need an object only for one page I recommend using the get_it_mixin (soon to be replaced by the watch_it package) and use the pushScope function |
In my scenario, I have multiple screens, each with its own corresponding view model. For example, I have a screen called "PreferencesScreen" and its associated view model called "PreferencesViewModel". Similarly, I have a screen called "LanguagesScreen" and its associated view model called "LanguagesViewModel". The view models are created and destroyed when their respective screens are opened and closed. In other words, when I navigate from one screen to another, the view model for the current screen is created, and when I navigate away from that screen, the view model is destroyed. Here's an example sequence of screen navigations:
|
In addition to the previous information, I'd like to mention a class called "GoRouteWrapper." This class is a StatefulWidget that acts as a wrapper for my screens. It takes the screen as a child and allows me to control the lifecycle of the screen, effectively connecting the screen and its view model. It's important to note that the "onDispose" methods of PreferencesViewModel and LanguagesViewModel, which are associated with the PreferencesScreen and LanguagesScreen, respectively, are empty. This means that the navigation process does not wait for their completion before proceeding to the next screen. In other words, the navigation moves forward without waiting for any cleanup or finalization tasks in these view models to finish. |
why don't you just register your viewmodel in the initState and remove it in the dispose function of the widget instead of doing it in the routing logic? |
We can close this issue? It's solved on 7.6.1. |
@escamoteur I checked out this new dropScope() method in v7.5.0.
But it does not protect me in any way from getting other singletons into this scope, unless added yet another scope before them.
On navigating from
/profile
screen to the/details
screen:Expected behaviour: Bar() is registered in the base scope.
Actual behaviour: Bar() is registered in the 'scope1', then screen transitioning ends and 'scope1' gots disposed.
That fix does nothing about it.
Proposal:
Add
protected
flag to the pushNewScope, so I can register singletons in theinit
callback only.The text was updated successfully, but these errors were encountered: