I am using the game template of Flutter casual game.
Flutter casual game template: Link
They are using a provider actually. They have this:
ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>,
AudioController>(
// Ensures that the AudioController is created on startup,
// and not "only when it's needed", as is default behavior.
// This way, music starts immediately.
lazy: false,
create: (context) => AudioController()..initialize(),
update: (context, settings, lifecycleNotifier, audio) {
if (audio == null) throw ArgumentError.notNull();
audio.attachSettings(settings);
audio.attachLifecycleNotifier(lifecycleNotifier);
return audio;
},
dispose: (context, audio) => audio.dispose(),
),
As you can see in the comment of code, it says
`Ensures that the AudioController is Created on startup',
and the ProxyProvider has the dispose parameter and it already pass the audio.dispose()
Therefore, I actually convert this into riverpod:
/// The AudioControllerProvider is used to control the audio of the game.
final audioControllerProvider = Provider.autoDispose<AudioController>((ref) {
// final appLifeCycle = ref.watch(appLifeCycleStateProvider);
final audioController = AudioController();
audioController.initialize();
final settingsController = ref.watch(settingsControllerProvider);
final appLifeCycle = ref.watch(appLifecycleStateProvider);
audioController.attachSettings(settingsController);
audioController.attachLifecycleNotifier(appLifeCycle);
ref.onDispose(audioController.dispose);
return audioController;
});
If we are trying to observe, as you what can see, the provider is auto dispose, and I called the
ref.onDispose()
since the old one has the dispose parameter.
Then since this audio controller must be created on start up. This is what I did, by creating a ProviderContainer before the runApp() function
// Run Some Riverpod Providers during startup
final container = ProviderContainer();
container.read(audioControllerProvider);
runApp(
UncontrolledProviderScope(
container: container,
child: MyApp(
adsController: adsController,
gamesServicesController: gamesServicesController,
),
),
);
So what actually the problems there?
It actually working fine at all, the problem there is the dispose. As you can see the audiocontroller was called on startup. But when I go to the new screen by calling this function
GoRouter.of(context).push(
'/game/${questGame[index].gameCategory.gameCategoryKey}');
}),
Then the audio controller it will triggered the dispose. I think the audio controller auto dispose must be not triggered since the provider was called in the main method, right?
Related
I have several sub-screens which give the user the option to save some data. When that screen is closed, I want the parent screen, which pushed the sub-screen, to know whether data was saved or not. The sub-screens maintain a didSave flag and are set to true when data is saved.
There are several ways for the sub-screens to be closed:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
I can handle the 3rd case using Navigator.pop(context, didSave) and in the parent that didSave flag is captured using final didSave = await Navigator.push<bool>(context, myRoute).
However, for the first 2 cases, the result will obviously be null.
I have looked into WillPopScope but that only is used to determine whether the screen should be closed or not. It doesn't seem that I can set any data to be returned for the push call.
I have also looked into wrapping the parent screen in a Provider where it can listen to didSave states but that will trigger immediately when emitted which is not desirable for my use case. I only want to act when the sub-screen is closed not when data is saved.
I can potentially use WillPopScope and emit an event there if a save operation has occurred but I would like a simpler solution if available.
Is there a way for this that I am missing?
Thanks!
as you said the Provider will listen to didSave and this doesn't fit in your case you can use just a simple inheritedWidget:
wrapping the parent like this:
InheritedExampleWidget(
didSave: false,
child: Parent(),
),
you need to set a setter to didSave
then on the ascendant widgets on the widget tree, you can:
InheritedExampleWidget.of(context).didSave = true;
this will not trigger it immediately, which the Provider package solves.
then you can manage the state however you want
Did you try to create the variable with state management? And there is method with the push that do task after user come from the child screen. So, when they come you can checkout the use have saved data or not.
For EG:
saved = false; //State management variable
//We are pushing on another screen.
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
new ScreenName(),
),
).then((val) {
//This part with work when user popped the screen on which we have pushed
if (!saved) {//Do Something when not saved}
});
Try above code and let me know if you get any error or you're facing any issue.
when you push a new route, using StatefulWidget, it will have a lifecycle starting from an createState, when the widget isn't there on the widget tree ( when we pop ), the dispose method will be called.
those cases of popping the route:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
will trigger the dispose method to execute.
so you can put inside it your logic that you want.
exmaple :
class classTest {
bool didSave = false;
}
then when on the property where you want to push the screen set it to that classTest's didSave, as an example:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const YourRouteWidget(didSave: classTest.didSave,
),
),
);
on that point it's false, then when the user will complete using the screen, going back with any king of popping the route (with Navigator.pop(context), or with back button...), the dispose method will be called, so you can :
void dispose() {
if(/* your conditions*/ ) {
classTest.didSave = true;
}
super.dispose();
}
it will be back with a true value to the parent page.
The problem I want to solve:
My app which uses GoRouter needs to be able to route to a named route from within main(). Since most routing is of the form 'context.go' I cannot do so within main.
Background
My app uses GoRouter. The ease with which GetX had let me define named routes and pass parameters from main() was perfect.
However, GetX and GoRouter eventually causes problems for me. GoRouter would eventually have no context in other parts of the app.
If there were a way to have them co-exist simply, I'd be open to it.
I had used the service locator pattern with the GetIt package to associate with a navigatorKey. It would work when I tested it -- but this involved creating two MaterialApps.
However, this app uses GoRouter which doesn't seem to use the navigatorKey.
I would like to go to a specific route from within main (). It seems like the service locator pattern could work for GoRouter as it did with Navigator 2.0 for MaterialApp -- but I can't find an example of how to do so.
More detailed context:
Here is what I have currently in main().
You can see the key challenge I have is that the listener for the data parameters being passed in lives in main (I got this from the third-party SDK -- I don't need it to be in main but it needs to listen regardless of the state of the app).
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
setupLocator();
runApp(MyApp());
locator<LandingPageData>().referralID = "defaultReferralID";
registerListeners();
}
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print("listener - socialdata");
String passedReferralID =
globalReferralData.linkParams['referralID'].toString();
String passedCreatorID =
globalReferralData.linkParams['creatorID'].toString();
String passedCampaignID =
globalReferralData.linkParams['\$campaign_id'].toString();
print(passedReferralID);
print(passedCreatorID);
print(passedCampaignID);
// How can I route to a named Route?
locator<LandingPageData>().referralID = passedReferralID;
locator<LandingPageData>().creatorID = passedCreatorID;
locator<LandingPageData>().campaignID = passedCampaignID;
});
}
Here is what the locator.service.dart looks like:
final locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// final GlobalKey<ScaffoldMessengerState> navigatorKey = GlobalKey<ScaffoldMessengerState>();
}
The above worked when I could attach to a navigatorKey and then navigate from within the listener. But that doesn't seem to work since the rest of the application uses GoRouter.
static BuildContext? get ctx => myGoRouter.routerDelegate.navigatorKey.currentContext;
you can get context in your NavigationService in this way and use it like
NavigationService.ctx?.go(...)
the problem you may face is that ctx will be null on app state till your first page starts to be built. In the case your listener has a data while ctx is still null, routing won't work. but you can handle this situation like:
define a global tempPageToGo in main func or a service and
var _ctx = NavigationService.ctx;
if(_ctx == null) {
tempPageToGo = anyPageDataYouWant;
while((await Future.delayed(Duration(seconds: 1))) == null) {
if(_ctx != null) {
_ctx!.go(...);
break;
}
}
} else _ctx!.go(...);
Unluckily, if I were you, I'd either drop the usage of GetX or of GoRouter.
Actually, I'd just drop GetX.
The reason is that GetX performs magic under the hood that lifts the developer the responsibility and usage of BuildContext, but that's clearly an anti-pattern, as the built-in navigation from Flutter clearly uses context: think of Navigator.of, for example.
GoRouter is built around context, and simplifies a lot of the implementations needed to perform "Navigator 2.0" actions.
If you're trying to implement deep linking, your MaterialApp should look like this in your root widget:
return MaterialApp.router( // Flutter's Router 2.0 usage
title: 'MyApp',
routeInformationProvider: myGoRouter.routeInformationProvider,
routeInformationParser: myGoRouter.routeInformationParser,
routerDelegate: myGoRouter.routerDelegate,
);
If GetX enables you to put myGoRouter there, then you should be good to go. But as I said before, everytime you need explicit navigation, you need context.
I'm in researching to adopt go_router in my project, and i was also stuckled for this usecase ( in my case i tried to prove that i can navigate from deferred link that callback from appsflyer SDK ).
For solution, like that go_router allows us to either navigate from context that is below the router declaration or from the redirect state. So we can wrap up all the state that effect the navigation on that.
This is how i redirect from appRouterState
redirect: (GoRouterState state) {
String? redirection(GoRouterState state) {
final appRouterState = ref.read(appRouterStateNotifierProvider);
final isAuthed = appRouterState.email != null;
if (appRouterState.deferredLink != state.location && appRouterState.deferredLink != null) {
return appRouterState.deferredLink;
}
if (state.location != '/login' && !isAuthed) return '/login';
if (state.location == '/login' && isAuthed) return '/';
return null;
}
final result = redirection(state);
return result;
},
In your case, you may implement setOnReferralDataReceivedListener in the appRouterStateProvider or something. And use it for refreshListenable param in the GoRouter constructor.
Hope this helps.
I implemented a third-party "listener" in my Flutter ios app. I have two goals:
Whenever the listener receives an event, route to LandingPage;
Pass to LandingPage the values captured by the listener -- globalReferralData.
Values should also be written to local variables on disk. The screen/page that opens should be able read that variable.
Because the listener is in main(), there is no context.
I also cannot use Get for GetPage, because it seems to collide with using GoRouter as it is used and defined throughout the rest of the app in the nav.dart file.
ReferralData globalReferralData;
List<BuildContext> buildContextList = [];
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
runApp(MaterialApp(
home: MyApp(),
routes: {
'/landingPage': (context) => const LandingPageWidget(),
},
navigatorKey: locator<NavigationService>().navigatorKey));
registerListeners();
}
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print(globalReferralData.linkParams['referralID']);
// pass value and open route -- this line failed to do anything
Navigator.pushNamed(context, '/landingPage');
// showAlert(buildContextList.last, 'Referral Data Received', '$received');
});
}
Based on your comment
... generate state from outside build or State and then access it if I can only route that way. Ideally, I can route from within main().....
Yes it's possible. That's with reference to routing without BuildContext. The following is the mechanism behind Navigation in Stacked Architecture.
In summary, you provide MaterialApp with your own navigatorKey and use that key to navigate anywhere in the Flutter code. A clean way to do this is to use dependency injection. Have a service provided by GetIt package that will make the navigatorKey accessible to the entire Flutter app.
In details, we can use the following 4 steps:
1. NavigationService and key
Have a navigation.service.dart file with a navigatorKey. This key will have the type and value of GlobalKey<NavigatorState>
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
}
This NavigationService class can do other things but let's leave it like this for now.
2. GetIt package
The package provides some mechanism to make services available app-wide. You actually initialise it before the runApp() call just as you do with Firebase for example.
Install the package. Run the following command.
flutter pub add get_it
Then, have a file, let's say app.locator.dart with the following.
import 'package:get_it/get_it.dart';
import 'navigation.service.dart';
final locator = GetIt.instance;
void setupLocator {
locator.registerLazySingleton(() => NavigationService());
}
Then in main.dart, call setupLocator() before runApp(). Based on the main method you provided:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FFAppState(); // Initialize FFAppState
GetSocial.addOnInitializedListener(() => {
// GetSocial SDK is ready to use
});
setupLocator(); //add this line
runApp(MyApp());
registerListeners();
}
3. Key and MaterialApp
Attach the navigatorKey from the NavigationService (obtained from locator) to the navigatorKey on the topmost MaterialApp.
So in whatever file where you defined MaterialApp, have something like this
// ...
return MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
// ...
);
4. Routing without BuildContext
In the registerListeners() function (called from main and not having access to BuildContext), you navigate with the navigatorKey. You can also use the context attached to the navigatorKey to showAlert too. Change your registerListeners() function to the following
void registerListeners() {
Invites.setOnReferralDataReceivedListener((received) {
globalReferralData = received;
print(globalReferralData);
print(globalReferralData.linkParams);
print(globalReferralData.linkParams['referralID']);
// instead of navigating with Navigator,
// Navigator.pushNamed(context, '/landingPage');
// navigate with the navigatorKey
locator<NavigationService>().navigatorKey.currentState!.pushNamed('/landingPage');
// You can as well use the key's context to showAlert.
// so instead of the following
// showAlert(buildContextList.last, 'Referral Data Received', '$received');
// You can have
showAlert(
locator<NavigationService>().navigatorKey.currentContext!,
'Referral Data Received',
'$received',
);
});
}
This way you have access to routing without the BuildContext and can use it in any other Dart file in the same project.
And hence also you've ideally routed from within main.
The above doesn't invalidate any routing with Navigator.of(context).navigate... in other parts of the app as it'll still be the same NavigationState that is been used by Flutter.
Update
I've updated the snippet of step 4 above.
From your comment
I'm unclear how from within main() which is where I put the listener class to capture the globalReferralData how I can pass that based on the example above to then route....
If you carry step 4 well above (and all the other steps) the problem should be solved. It should be solved because your main method calls this registerListeners() function, so that should do the trick.
Do you now understand?
It's also good that you called registerListeners after runApp, that way you're sure the navigatorKey must have been attached to MaterialApp.
...
That should solve one part of the problem, that's navigation. The above is just minimalist setup to serve the purpose you want. You can go further to use the entire StackedServices and or StackedApp if you wish.
The other issue about setState not working in main, it's really not possible. It's just natural. main is an entry point for compiled code and not a StatefulWidget. I'm supposing that if the above routing works for you, you might not have the issue of setState again.
But if you still need something, I propose you notifyListeners. Other state management architectures, not only Stacked, use notifyListeners (a method from ChangeNotifier) to update UI from outside StatefulWidget.
I guess you would have your way around this. And you could still go ahead with the View and ViewModel classes of Stacked and call notifyListeners in a viewModel (maybe that of a splash screen) when the globalReferredData is received.
I have problems with integration of a photography function in my app.
I get asked if I permit the access to the camera, but after that nothing happens exept this errror:
W/Camera (26849): The selected imageFormatGroup is not supported by
Android. Defaulting to yuv420
I/CameraManagerGlobal(26849): Camera 0 facing CAMERA_FACING_BACK state
now CAMERA_STATE_OPEN for client...
This is my code:
class FaultReporting extends StatefulWidget {
#override
_FaultReportingState createState()=> _FaultReportingState();
}
class _FaultReportingState extends State<FaultReporting>{
bool isReady=false;
List<CameraDescription> cameras;
CameraController camController;
#override
void initState() {
super.initState();
setupCameras();
}
Future<void> setupCameras() async {
try {
cameras = await availableCameras();
camController = new CameraController(cameras[0], ResolutionPreset.medium);
await camController.initialize();
} on CameraException catch (_) {
setState(() {
isReady = false;
});
}
setState(() {
isReady = true;
});
}
...
child: ElevatedButton(
onPressed: (){
if(!isReady && !camController.value.isInitialized)
{
return Container();
}
return AspectRatio(
aspectRatio: camController.value.aspectRatio,
child: CameraPreview(camController),
);
},
...
I had the exact same error when I used the camera on flutter. The message is just informing you that the imageFormatGroup parameter must be ImageFormatGroup.yuv420.
So try this:
camController = new CameraController(
cameras[0],
ResolutionPreset.medium,
imageFormatGroup: ImageFormatGroup.yuv420,
);
I had the same issue. However, the abovementioned solution (adding imageFormatGroup: ImageFormatGroup.yuv420) did not solve the problem.
I found out what was the real problem. Turns out Flutter Navigation requires pushed routes to be popped before another one pushed into navigator.
Otherwise, it will cause big issues with all camera using packages (CameraController from camera, QRViewController? controller from qr_code_scanner, MobileScannerController cameraController from mobile_scanner all affected) using packages (Both Android and iOS), specially if you use your own custom internal navigation without using Flutter Navigator.
Because, that way you use Navigator partially and in some situations you push several different routes without popping them or even pushing same route several times.
The camera will come either black or keep loading forever. Only thing will solve the issue is to kill/force stop the app and open again as it clears Flutter Navigator.
In Android you will see this error: "MessageQueue: java.lang.IllegalStateException: Handler sending message to a Handler on a dead thread"
In iOS you will see this: "The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888"
Another related error from mobile_scanner package: flutter: MobileScanner: Called start() while already started!
Realted error from qr_code_scanner package: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Scan rect cannot be set when not (yet) scanning. You may want to set it within didStartScanningBlock.
Here is the solution which may help you to prevent it. What we need to do is to make sure that when we push new route to the Navigator we are clearing other previously pushed routes from it:
For undefined routes:
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => YourPage()),
(Route<dynamic> route) => false);
For defined routes:
Navigator.of(context)
.pushNamedAndRemoveUntil('/yourdefinedroute', (Route<dynamic> route) => false);
have a problem that I'm sitting on couple of days now.
have an app where:
depending of AUTH state, 'LoginScreen' or 'MainScreen' is Shown.
in MainScreen I setUp bottomNavigation with screens (HomeScreen, ShoppingScreen,MyFavorites)
I set up there as well my StreamProviders(those depend on Auth) by using MultiProvider
on HomeScreen when I User Provider.of(context) it works like it should
but when I use :
`Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProfileScreen(),
),
);
` and use Provider.of(context) there I get "Could not find correct Provider....above this...widget"
I read some issues on that and solution there was to decler providers above MaterailApp which in my case I can not do because I can set up thoese only after Auth is successfull.
Tryed passing context(from HomeScreen) to ProfileScreen(through constructor) and that work but when value changed of UserData it did not update the screen (guessing beacause of diffrent 'contexts')
What am I doing wrong in here,any Ideas?:S
Providers are "scoped".
This means that if they are placed inside a screen, they aren't accessible outside that screen.
Which means that if a provider is scoped but needs to be accessed outside of the route it was created in, we have two solutions:
un-scope the provider. This involves moving the provider to a common ancestor of both widgets that needs to obtain the value.
If those two widgets are on two different Routes, then it basically mean "move the provider above MaterialApp/CupertinoApp.
manually pass the provider to the new screen (needed when using Navigator.push)
The idea is, instead of having one provider, we have two of them, both using the same value as explained here See How to scope a ChangeNotifier to some routes using Provider? for a practical example.
For Navigator.push, this can look like:
final myModel = Provider.of<MyModel>(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ChangeNotifierProvider.value(
value: myModel,
child: MyScreen(),
),
),
);
Please make sure that you application's root widget is Provider Widget, it should event be the parent of MaterialWidget. If this is already the case I will need your code to look into. Something like this
class AppState {
User loggedInUser;
bool get isLoggedIn {
return loggedInUser != null;
}
// Other states as per the requirements
// ...
}