There is a variable pinEnable which tells the app whether the user has set up a pin for the app. This is stored in SharedPreferences. My first page that comes in my app depends on it. Since the fetching operation is async, it just returns null.
the relevant code I used is given:-
PinData is just a class containing functions to set and get pin and pinEnable
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool pinEnable;
PinData pinData = PinData();
updatePinEnable() async {
pinEnable = await pinData.getPinEnable();
print(pinEnable);
}
#override
void initState() {
super.initState();
updatePinEnable();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(...),
home: pinEnable == false ? MyTabbedHome() : PinCodePage());
}
}
In the last code statement pinEnable is not false but it's null, therefore it returns PinCodePage()
Is there any way to fix this, or any ideas to get around this. Thanks!!
You don't need stateful widget
,and this is a better solution using a FutureBuilder to return the correct widget only when the async process is completed:
Edit: edited the code to address fact that you are not setting initial value in shared prefs
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
PinData pinData = PinData();
Future<bool> isPinEnabled() async => pinData.getPinEnable();
#override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: isPinEnabled(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
else if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return snapshot.data ?
PinScreen() //if true returned from shared prefs go to pin screen
:
HomeScreen(); //if false returned from shared prefs go to home screen
}
else {
return HomeScreen(); //if null returned from shared prefs go to home screen
}
}
}
);
}
}
Related
I want to navigate to a specific page via beamer from a notification click.
In my main.dart I initialze my app and fcm. The class 'PushNotificationReceiver' should handle the notification logic.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PushNotificationReceiver.instance.initialize();
runApp(MultiProvider(providers: [
// Some of my providers
], builder: (context, _) => MyApp()));
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
PushNotificationReceiver.instance.registerNotifications((route) => {
context.beamToNamed(route)
});
}
#override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(builder: (context, themeProvider, child) {
return MaterialApp.router(
routeInformationParser: BeamerParser(),
routerDelegate: _beamerDelegate,
backButtonDispatcher: BeamerBackButtonDispatcher(delegate: _beamerDelegate),
);
}
}
}
I implemented the functions to receive and show local notifications but to simplify it I only paste the code for the click (removed null checks as well).
class PushNotificationReceiver {
static PushNotificationReceiver _instance;
void Function(String route) navigateFunction;
static PushNotificationReceiver get instance {
if (_instance == null) {
_instance = new PushNotificationReceiver();
}
return _instance;
}
Future<void> initialize() async {
await Firebase.initializeApp();
}
void registerNotifications(void Function(String route) navigateFunction) {
this.navigateFunction = navigateFunction;
// Called the other functions to receive notifications, but excluded them for simplicity.
FirebaseMessaging.onMessageOpenedApp.listen((message) {
this.navigateFunction("/MyPage/${message.data["id"]}");
});
}
}
When I click on the notification I get the following error:
[ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: 'package:beamer/src/beamer.dart': Failed assertion: line 40 pos 14: 'BeamerProvider.of(context) != null': There was no Router nor BeamerProvider in current context. If using MaterialApp.builder, wrap the MaterialApp.router in BeamerProvider to which you pass the same routerDelegate as to MaterialApp.router.
I tried it first without a function that I pass in and a GlobalKey in the main.dart with the same result.
Any suggestions?
Found the solution.
My first approach of a global key works if I wrap my MaterialApp.router in a Beamerprovider (like the error message suggested).
final GlobalKey myGlobalKey = GlobalKey();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PushNotificationReceiver.instance.initialize();
runApp(MultiProvider(providers: [
// Some of my providers
], builder: (context, _) => MyApp()));
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
PushNotificationReceiver.instance.registerNotifications();
}
#override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(builder: (context, themeProvider, child) {
return BeamerProvider(
key: myGlobalKey,
routerDelegate: _beamerDelegate,
child: MaterialApp.router(
routeInformationParser: BeamerParser(),
routerDelegate: _beamerDelegate,
backButtonDispatcher: BeamerBackButtonDispatcher(
delegate: _beamerDelegate
)
)
);
}
}
}
That leads to my push notification receiver:
class PushNotificationReceiver {
static PushNotificationReceiver _instance;
static PushNotificationReceiver get instance {
if (_instance == null) {
_instance = new PushNotificationReceiver();
}
return _instance;
}
Future<void> initialize() async {
await Firebase.initializeApp();
}
void registerNotifications(void Function() {
// Called the other functions to receive notifications, but excluded them for simplicity.
FirebaseMessaging.onMessageOpenedApp.listen((message) {
myGlobalKey.currentContext.beamToNamed("/MyPage/${message.data["id"]}");
});
}
}
I hope this will help some others too.
Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});
1.this is the main entry
void main() {
WidgetsFlutterBinding.ensureInitialized();
StorageUtil.getInstance();
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Wrapper(),
);
}
}
This is the Wrapper. The log-in form or the home page do not show unless I manually hot-reload the app.
I've tried everything but i am stuck. Please help.
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
User _user = User();
#override
Widget build(BuildContext context) {
_user.uId = StorageUtil.getString('access_token');
if(_user.uId != null && _user.uId != ""){
print('filled ${_user.uId}');
return Home();
}else{
print('empty ${_user.uId}');
return Authenticate();
}
}
}
I think your StorageUtil is giving you promise for get data back to you but you are not waiting for it when app loads at first time.You can try await StorageUtil.getInstance(); in main block.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageUtil.getInstance();
runApp(MaterialApp(home: MyApp()));
}
You need to watch the instance. Right now you are grabbing the instance to get the value but you are not subscribing to the value itself, which means that when the value changes nothing will happen until you refresh the page. I recommend subscribing to the value (access_token) that is determining the login screen vs the home screen.
Flutter has some built in features that makes this a bit easier such as streams and or quicker widgets like the ValueListenerBuilder. Let's see if we can do that with StorageUtil.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageUtil.getInstance();
runApp(MaterialApp(home: MyApp()));
}
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
User _user = User();
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: StorageUtil.getString('access_token');,
builder: (BuildContext context, String value, _) {
if(value != null && value != ""){
print('filled ${_user.uId}');
return Home();
} else {
print('empty ${_user.uId}');
return Authenticate();
}
},
),
}
}
It is rough but it should get the job done! I recommend probably finding a more streamlined way to store your state than just the StorageUtil that'll better scale as your application grows.
I want to show onboarding screen only for the first time user opens the application, so at the final page of Onboarding screen I put OnBoardingStatus value to be "Done" and move to the main screen. But when user opens the application for the next time this code flash the Onboarding screen for few milliseconds and then opens the mainScreen.
Here is my code
class App2 extends StatefulWidget {
App2({Key key}) : super(key: key);
#override
_App2State createState() => _App2State();
}
class _App2State extends State<App2> {
String onBoardingStatus;
#override
void initState() {
// TODO: implement initState
getOnBoardingStatus();
super.initState();
}
Future<void> getOnBoardingStatus() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences prefs = await SharedPreferences.getInstance();
var onboardingstatus = prefs.getString('OnBoardingStatus');
setState(() {
onBoardingStatus = onboardingstatus;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: onBoardingStatus != null
? MainScreen()
: OnboardingScreen());
}
}
Currently you have no way to know if onBoardingStatus is null because the SharedPreferences instance hasn't been retrieved yet, or because the OnBoardingStatus really is empty. You can work around this with a FutureBuilder:
class App2 extends StatelessWidget {
App2({Key key}) : super(key: key);
Future<String> getOnBoardingStatus() async =>
(await SharedPreferences.getInstance()).getString('OnBoardingStatus');
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getOnBoardingStatus(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
//TODO: Return a widget that indicates loading
}
return Scaffold(
body: snapshot.data != null
? MainScreen()
: OnboardingScreen());
},
);
}
}
However I don't think it's the best solution. For starters, App2 should get the status from an outer source - this way if you ever decide to change your storage solution you wouldn't need to touch App2.
I have problems with FutureBuilder starting twice.
First it fetch the data correctly, returning my StartScreen, then after few seconds, the StartScreen rebuilds and I noticed that the FutureBuilder fires again.
Here is my code and it's pretty simple, so I wonder what may the problem be?!?
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
FirebaseUser user;
#override
void initState() {
// TODO: implement initState
super.initState();
getNewestlocation();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'APP',
theme: buildTheme(),
home: FutureBuilder<FirebaseUser>(
future: Provider.of<AuthService>(context).getUser(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print('error');
return Text(snapshot.error.toString());
}
user = snapshot.data;
print('user here $user');
return snapshot.hasData ? StartScreen(user) : LoginScreen();
} else {
return LoadingCircle();
}
},
),
);
}
}
Can anyone help me with this, please?
The future is firing again because you're creating it in the build method at the same time as the FutureBuilder.
From the FutureBuilder docs:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
So to prevent it from firing you'd have to do something like this:
class _MyAppState extends State<MyApp> {
Future<String> _myString;
#override
void initState() {
super.initState();
_myString = _fetchString();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _myString,
builder: (context, snapshot) {
// build page stuff...
},
),
);
}
}
Future<String> _fetchString() async {
print('running future function');
await Future.delayed(Duration(seconds: 3));
return 'potatoes';
}
Note, to access a provider in initState() you have to set listen to false, as detailed in this answer.
I think you have some things bad in your code, maybe that's not the problem but is good to correct that:
first: It is not recommendable to do that job in your main file, you should have something like a Splash page to handle that.
second: You should use blocs and not write your logic code on the same place at the view(UI)
If you're using android studio, try if running from the terminal fix the issue. The run button attached the debug service, which then force the entire app to be rebuilt