Flutter initState not working when pushing navigator - flutter

This is the code where the initState is not working
class _MyAppState extends State<MyApp> {
late bool _nightMode;
Future<void> _loadNightMode() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_nightMode = (prefs.getBool('nightMode') ?? false);
});
}
#override
void initState() {
super.initState();
_loadNightMode();
print("HELLO");
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: _nightMode ? ThemeMode.dark : ThemeMode.light,
debugShowCheckedModeBanner: false,
home: const LandingPage(),
);
}
}
I tried printing HELLO as a testing but it resulted in nothing actually.
This is how I call it.
logout() {
FirebaseAuth.instance.signOut();
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const MyApp();
},
),
);
}
As you can see, I am trying to pass the sharedpreferences to get a bool but with the initState not working it's actually not getting passed.
There is also no error popping out.

Navigator use context and when initstate called context not ready yet so don't use navigator or any context related work in initstate

Try to add _loadNightMode() method call in callback inside initState method because of context not available yet:
WidgetsBinding.instance.addPostFrameCallback((_) async {
_loadNightMode();
});
Second options is move _loadNightMode() into didChangeDependencies() widget lifecycle callback function.

Related

FutureBuilder doesn't wait until future completes

In my app I want to initialize something before my widgets will be created. I need to do it exactly in App class and trying to use FutureBuilder for this purpose. But _AppBlocProvider's build method is called before initInjectionContainer(), for example. My repository is not initialised yet in injectionContainer, but Blocs in provider are trying to access it's instance. What's wrong with this code?
I've also tried this:
void main() {
runApp(App());
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
Future<bool>? _myFuture;
Future<bool> _init() async {
...
await initInjectionContainer();
await sl<AudioManager>().preloadFiles();
return false;
}
...
#override
void initState() {
super.initState();
_myFuture = _init();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _myFuture,
builder: (context, _) {
return _BlocProvider(
child: Builder(
builder: (context) => MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MainMenu(),
),
),
);
},
);
}
}
doesn't work.
FutureBuilder doesn't just automatically block or show a loading screen or whatever. It builds once on initialization, and then again once the future completes. That second parameter in the builder that you anonymized is crucial to properly handling the state of the future and building accordingly.
FutureBuilder(
future: _someFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// Future not done, return a temporary loading widget
return CircularProgressIndicator();
}
// Future is done, handle it properly
return ...
},
),
That being said, if there is stuff that your entire app needs that you need to initialize, you can call it from main before you call runApp so that they become a part of the runtime loading process rather than forcing a widget to deal with it:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initInjectionContainer();
await sl<AudioManager>().preloadFiles();
runApp(App());
}
Now with that being said, if these processes can take a while, it's better to handle them with a widget so that you can display a loading state to the user so they know the app didn't just freeze on start-up.

Flutter login control in splash screen

I want to make a small login application. When entering the application, I want to inquire whether the user has a token code or not on the splash screen. How can do this? thank you for help.
main.dart file
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashScreen(),
);
}
}
My splash screen.
I want to know if the user has a token or not
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
loginControl();
}
// ignore: missing_return
Future<bool> loginControl() async {
bool status = AuthController.isLoginUser() as bool;
print(status);
if (status) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => HomeScreen()));
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => LoginScreen()));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('welcome my app'),
),
);
}
}
my auth controller like this;
class AuthController {
static Future<bool> isLoginUser() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
String token = sharedPreferences.getString("token");
if (token == null) {
return false;
} else {
return true;
}
}
}
Your isLoginUser is actually returning a Future<bool> means that it returns a Future that will later resolve to a bool value.
So, when you use it like this in your loginControl,
bool status = AuthController.isLoginUser() as bool;
AuthController.isLoginUser() return Future<bool> and it can't be directly converted to a bool using as bool.
Instead you should await that Future to resolve, like this.
bool status = await AuthController.isLoginUser(); // This will work.
Now, your code will pause at this line, until it gets a return value from isLoginUser and then resume to next line with status being an actual bool value. i.e., true or false.

Using FutureBuilder in main.dart

Below code always show OnboardingScreen a little time (maybe miliseconds), after that display MyHomePage. I am sure that you all understand what i try to do. I am using FutureBuilder to check getString method has data. Whats my fault ? Or any other best way for this ?
saveString() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('firstOpen', '1');
}
getString() method always return string.
getString() async {
final prefs = await SharedPreferences.getInstance();
String txt = prefs.getString('firstOpen');
return txt;
}
main.dart
home: new FutureBuilder(
future: getString(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
})
Usually I'm using another route, rather than FutureBuilder. Because futurebuilder every hot reload will reset the futureBuilder.
There always will be some delay before the data loads, so you need to show something before the data will load.
Snapshot.hasData is showing only the return data of the resolved future.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
const isOnboardingFinished = 'isOnboardingFinished';
class _SplashScreenState extends State<SplashScreen> {
Timer timer;
bool isLoading = true;
#override
void initState() {
_checkIfFirstOpen();
super.initState();
}
Future<void> _checkIfFirstOpen() async {
final prefs = await SharedPreferences.getInstance();
var hasOpened = prefs.getBool(isOnboardingFinished) ?? false;
if (hasOpened) {
_changePage();
} else {
setState(() {
isLoading = false;
});
}
}
_changePage() {
Navigator.of(context).pushReplacement(
// this is route builder without any animation
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading ? Container() : OnBoarding();
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(child: Text('homePage'));
}
}
class OnBoarding extends StatelessWidget {
Future<void> handleClose(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(isOnboardingFinished, true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
onPressed: () => handleClose(context),
child: Text('finish on bording and never show again'),
),
),
);
}
}
From the FutureBuilder class documentation:
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 you need to create a new Stateful widget to store this Future's as a State. With this state you can check which page to show. As suggested, you can start the future in the initState method:
class FirstPage extends StatefulWidget {
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final Future<String> storedFuture;
#override
void initState() {
super.initState();
storedFuture = getString();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: storedFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
});
}
}
So in your home property you can call it FirstPage:
home: FirstPage(),
Your mistake was calling getString() from within the build method, which would restart the async call everytime the screen gets rebuilt.

Calling Navigator outside Widget build() gives context error

So I'm basically trying to check if users have seen an Intro page in my flutter app. If they already have seen it, I want them to be directed to the Login() page. Else, I want them to be directed to the IntroScreen() page.
However, I am getting the following error: Unhandled Exception: Navigator operation requested with a context that does not include a Navigator. E/flutter (13982): The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
This is my code:
void main() => runApp(CheckSplash());
//check if the intro screen has already been seen
class CheckSplash extends StatefulWidget {
#override
_CheckSplashState createState() => _CheckSplashState();
}
class _CheckSplashState extends State<CheckSplash> {
bool _introseen=true;
Future checkIntroSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _introseen = (prefs.getBool('seen') ?? false);
if (_introseen) {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (BuildContext context) => Login()));
} else {
//await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (BuildContext context) => new IntroScreen()));
}
}
#override
void initState() {
super.initState();
new Timer(new Duration(milliseconds: 200), () {
checkIntroSeen();
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: appTheme,
debugShowCheckedModeBanner: false,
home: Builder(
builder: (context) => Scaffold(
resizeToAvoidBottomPadding: false,
body: Center(child: CircularProgressIndicator(valueColor: new AlwaysStoppedAnimation<Color>(Colors.red[300])))
)
)
);
}
}
So I solved the problem by using the flutter GET library. Link: flutter get library
Using this library, one doesn't need the context to navigate between pages and thus it is a full proof method to navigate from anywhere in flutter.
I Have tried the easiest way. Create a function for your code after your contextBuilder. Then, Just put the context
as a function argument:
Widget build(BuildContext context) {
return ....(your code)
}
yourFunction(context) {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen()));
}
It is worked as expected for me. Thanks!
context inside initState is available inside SchedulerBinding.instance.addPostFrameCallback. This function fired after widget is built.
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
new Timer(new Duration(milliseconds: 200), () {
checkIntroSeen();
});
});
}

flutter restart app or redirect to first widget

I'm developing an app with flutter.
All goes fine, but I need something.
I need check if user's token has expired when app comes to foreground.
I get the app state with
didChangeAppLifecycleState(AppLifecycleState state).
An example of my code is this (I can't write the real code because is in my job's computer, and now I'm in home):
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void didChangeAppLifecycleState(AppLifecycleState state) {
var isLogged = boolFunctionToCheckLogin();
if (state == AppLifecycleState.resume && isLogged) {
// THIS IS THE PLACE WHEN I TRY TO CODE THE MAGIC
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tutorial Lifecycle'),
),
body: Center(),
);
}
But, inside of didChangeAppLifecycleState function:
I have tried to make a showDialog with a Navigator.push to login
I have tried to make directly a Navigator.push
I tried to restart the app
All of these have the same problem: context is null or similar.
If I do any of these options directly with button or whatever, works.
But when app comes to foreground, the system says me that context is null or Material
Anybody can help to know how to do it??
Thank you all!!
I've solve it.
Into the conditional mark with "magic", I've call "showDialog" inside of setState:
void didChangeAppLifecycleState(AppLifecycleState state) {
var isLogged = boolFunctionToCheckLogin();
if (state == AppLifecycleState.resume && isLogged) {
setState(() {
showDialog(
context: context,
builder: (BuildContext context) {
... blablabla ...
}
});
}
}