Should flutter root widget always be StatelessWidget? - flutter

When I read the doc in flutter, I have a question that should flutter root widget always be StatelessWidget?
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Code Sample for Navigator',
// MaterialApp contains our top-level Navigator
initialRoute: '/',
routes: {
'/': (BuildContext context) => HomePage(),
'/signup': (BuildContext context) => SignUpPage(),
},
);
}
}
Because I think there's sometime need init function to call, and maybe not want the code of that write in HomePage. For example: check token expire or not, and decide go to HomePage or LoginPage.
Then the best option: should I change the root Widget to StatefulWidget, and just include the logic above in its initState function ?

Making root widget a StatefulWidget is useful when listen AppLifecycleState
such as do resume job like resume WebSocket connection
code snippet
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;
#override
void initState() {
super.initState();
initPlatformState();
WidgetsBinding.instance.addObserver(this);
}
initPlatformState() async {
Screen.keepOn(true);
}
Future<void> resumeCallBack() {
if (sl<WebSocketService>().webSocketState == 'lost') {
sl<WebSocketService>().initWebSocket();
}
if (mounted) {
setState(() {});
}
print("resumeCallBack");
}
Future<void> suspendingCallBack() {
if (mounted) {
setState(() {});
}
print("suspendingCallBack");
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
print("AppLifecycleState Current state = $state");
setState(() {
_lastLifecycleState = state;
});
switch (state) {
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
/*case AppLifecycleState.suspending:
await suspendingCallBack();
break;*/
case AppLifecycleState.resumed:
await resumeCallBack();
break;
}
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Template',
debugShowCheckedModeBanner: false,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => LoginPage(),

Yes, you can use StatefulWidget as your root parent. But, you should only use it when it makes sense.
Like, If you are initialising and observing some animations, firebase messaging, services, applifecycle states etc. which might require sometimes.
Otherwise Stateless widget are better to use.

It can be StatefulWidget or StatelessWidget. But placing a StatefulWidget in the root will impact your app performance because a simple state change in the StatefulWidget will cause the entire widget tree to rebuild and reduce your app performance.
always try to place StatefulWidget or InheritedWidget deep inside the widget tree.
better solution for your scenario is to use Providers or InheritedWidgets and listen to the token changes rather than changing the root widget to Stateful

Related

Flutter - How to safely change theme after awaiting theme data from an API in a Widget?

Using a normal setup for handling Theme with a ChangeNotifier that notifies the whole app / everything below it in the three - that something should be redrawn.
This approach seems general and there's multiple "guides" doing it this way. And this works works well when clicking a Button to change it. However, if the data for a Theme is coming from an API - where can we safely update the same value before rendering a Widget?
This is an example code where the ThemeData is somehow "downloaded" and supposed to be updated before rendering the view once the StreamBuilder is done. This, of course, causes the same Widget that's downloading something being redrawn while building so I'm getting a warning for that.
How can this be solved? The Theme can just be a single color that is downloaded and changed dynamically. And so far I haven't seen themes being changed inside one single widget while the "main one" is unchanged. Not sure what's the best approach to this (or similiar) issue - since it can't be uncommon in an mostly online based world.
Edit #1: Just to clarify - the Theme might change depending on the Widget / Page / Screen being loaded and it's not a "one time thing" where you initialize it at the beginning but with each screen being loaded - to customize that particular page based on online API data.
Example code:
void main() {
runApp(ChangeNotifierProvider(
create: (context) => ThemeConfig(),
child: MyApp()
));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child)
{
return MaterialApp(
theme: state.getTheme()
)
});
}
}
class _MyScreen extends State<MyScreen>
{
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Api.downloadTheme(),
builder: (context, snapshot)
{
// If OK render screen - But where to safely set the "Theme" from API?
return MyWidget(context.data)
});
)
}
}
class _MyWidget extends State<MyWidget>
{
#override
void initState() {
super.initState();
// This will cause the Widget tree to be redrawn while it's drawing and not work at all
// So when I've downloaded the data - where can this safely be changed?
Provider.of<ThemeConfig>(context).setTheme(widget.data.theme);
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I'm not sure if I understood you correctly, but if you wander how to update the theme by fetching if from an api, here is the example of simulating an api call which updates the theme:
void main() {
runApp(
ChangeNotifierProvider(
lazy: false, // triggering ThemeConfig constructor
create: (context) => ThemeConfig(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child) {
return MaterialApp(
theme: state.theme,
home: MyScreen(),
);
});
}
}
class ThemeConfig with ChangeNotifier {
ThemeConfig() {
// trigger theme fetch
getTheme();
}
ThemeData theme = ThemeData(primarySwatch: Colors.blue); // initial theme
Future<void> getTheme() async {
// TODO: fetch your theme data here and then update it like below
await Future.delayed(Duration(seconds: 3)); // simulating waiting for response
theme = ThemeData(primarySwatch: Colors.red);
notifyListeners();
}
}
class MyScreen extends StatelessWidget {
const MyScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(), // notice how the colors change
body: Center(
child: Container(
height: 200,
width: 200,
color: Theme.of(context).primaryColor,
),
),
);
}
}

Flutter Bloc: log out from any place when using Navigator + Bloc

I am new to Fluter and trying to tackle the navigation part using Bloc.
I would like to add the possibility to log out from any place in the application.
Details:
I am working on the Notes application (just for learning), and have wrapped MaterialApp with BlocProvider, so I can access AuthBloc from any place. MaterialApp 'home' widget is HomePage, which returns BlocConsumer, thus it can return a state depending on the event.
So, if the user has logged in - I emit AuthStateLoggedIn and return a ViewNotePage.
From the ViewNotePage (by clicking on a concrete Note) I go to NotePage, using MaterialPageRoute. And if I emit AuthEventLogOut (by press on the LogOut button) from the NotesListPage - nothing happens, until I invoke Navigator.of(context).popUntil(ModalRoute.withName('/')).
Here is the code
void main() {
runApp(const MyApp());
}
// App
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider<AuthBloc>(
create: (context) => AuthBloc(FirebaseAuthProvider()),
child: MaterialApp(
title: 'Notes',
theme: Colors.blue,
home: const HomePage(),
routes: {
viewNotePage: (context) => const ViewNotePage(),
}
),
);
}
}
// HomePage
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
context
.read<AuthBloc>()
.add(const AuthEventInitialize());
return BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state.isLoading) {
// loading is handled here
}
},
builder: (context, state) {
if (state is AuthStateLoggedOut) {
return const LoginView();
} else if (state is AuthStateRegistering) {
return const RegisterView();
} else if (state is AuthStateNeedsVerification) {
return const VerifyEmailView();
} else if (state is AuthStateLoggedIn) {
return const NotesPage();
} else if (state is AuthStateForgotPassword) {
return const ForgotPasswordView();
}
return const Scaffold(body: CircularProgressIndicator());
},
);
}
}
// NotesPage
To shorten the code, basically, I do here:
await Navigator.of(context).pushNamed(viewNotePage, arguments: someArgs);
// ViewNotePage
context.read<AuthBloc>().add(const AuthEventLogOut());
Navigator.of(context).popUntil(ModalRoute.withName('/')); // without popUntil - I stay on the ViewNotePage
So from what I understand, when I use MaterialPageRoute - it creates a new tree under the MaterialApp and now it can not access BlocConsumer.
Can someone please point out, what is the correct way to handle this case?
Is it a good idea to mix Bloc + Navigator features?
you can listen for the logout event using bloc listener and then call the navigator function
Yes, you can embed navigation into the flutter bloc (which handles login/logout navigation for you within the bloc). A solution for this already exists on flutter_bloc's official website! https://bloclibrary.dev/#/flutterlogintutorial

onDestroy or dispose in flutter

I am developing a note app, and I want to save my note when I closed the app (when removing the app from recent apps).
I tried dispose() method but it did not work.
I tried :
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
final isDetached = state == AppLifecycleState.detached;}
but it did not work too. it does not print detached status, just it prints AppLifecycleState.paused AppLifecycleState.inactive AppLifecycleState.resume
so what should I do here!!
Your code should look something like this if you're observing the app lifecycle state:
class AppLifecycleReactor extends StatefulWidget {
const AppLifecycleReactor({ Key? key }) : super(key: key);
#override
State<AppLifecycleReactor> createState() => _AppLifecycleReactorState();
}
class _AppLifecycleReactorState extends State<AppLifecycleReactor> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
}
#override
Widget build(BuildContext context) {
return Widgets;
}
}
to summarise:
with WidgetsBindingObserver after extends State
WidgetsBinding.instance!.addObserver(this); in initstate
WidgetsBinding.instance!.removeObserver(this); in dispose
For more info check: https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html
dispose() function is only visible in StatefulWidget
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final AppRouter _appRouter = AppRouter();
#override
Widget build(BuildContext context) {
return BlocProvider<CounterCubit>(
create: (context) => CounterCubit(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: _appRouter.onGenerateRoute,
),
);
}
#override
void dispose() {
super.dispose();
// dispose your code here
}
}
Please consider using Firebase and save for example after every edit, or after 500ms after an edit using rx.debounce from rxdart.
You cannot rely on the app really being able to execute dispose or even save something to a database when it is closed. Removing an app from the recent list ends the app very quickly (kills the app), that's why it works even for hung/crashed apps.
You can always use shared preferences in that case. Share preferences are used to save data in your local storage and the implementation is also very simple. and don't worry it won't make your app laggy or slow. It works very smoothly
https://pub.dev/packages/shared_preferences
do upvote if helpful

flutter-web - Avoid initialRoute from initiating when the app launched with a different route via the browser's address bar?

New to Flutter.
I'm making an app that has a splash screen that initially shows up when the user opens the app. After 3 seconds, the app will show the login or the dashboard screen, depending on the authentication state.
Here's my code.
main.dart
void main() {
runApp(myApp);
}
MaterialApp myApp = MaterialApp(
initialRoute: "/",
routes: {
"/": (context) => SplashScreen(),
"/signin": (context) => SignInScreen(),
"/notes": (context) => NotesScreen(),
},
);
splash_screen.dart
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_goToNextScreen();
}
void _goToNextScreen() {
Future.delayed(
Duration(seconds:3),
() async {
AuthState authState = await Auth.getAuthState();
String route = authState == AuthState.SIGNED_IN ? "/notes" : "/signin";
Navigator.pushReplacementNamed(context, route);
}
);
}
// build() override goes here...
}
I've been debugging the app with a web-server. When the app launches with the url localhost:8000/, everything seems fine. However, if the app started with the url localhost:8000/notes, the splash screen, I think, still gets initiated. What happens is the app will show the notes screen, then after 3 seconds, the app will open another notes screen.
Any ideas?
Because first render always started at root '/', it's preferable to use your own path for splash screen, like
initialRoute: '/splash'.
To hide this path in the address bar, replace routes map with route generator:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: (RouteSettings settings) {
// print current route for clarity.
print('>>> ${settings.name} <<<');
switch (settings.name) {
case '/splash':
return MaterialPageRoute(
builder: (context) => SplashScreen(),
// settings omitted to hide route name
);
case '/signin':
return MaterialPageRoute(
builder: (context) => SignInScreen(),
settings: settings,
);
case '/notes':
return MaterialPageRoute(
builder: (context) => NotesScreen(),
settings: settings,
);
case '/':
// don't generate route on start-up
return null;
default:
return MaterialPageRoute(
builder: (context) => FallbackScreen(),
);
}
},
initialRoute: '/splash',
);
}
}
See since the main logic is we cannot have await in the init state so the page will build irrespective of the any logic you provide. I have a solution to this, there may be some advance or other good solutions too, so this is what I would use.
I would use a concept of future builder. What it will do is wait for my server and then build the whole app.
So process is
In your main.dart
use
Future<void> main() async {
try {
WidgetsFlutterBinding.ensureInitialized();
//await for my server code and according to the variable I get I will take action
//I would have a global parameter lets say int InternetOff
await checkServer();
runApp(MyApp());
} catch (error) {
print(error);
print('Locator setup has failed');
//I can handle the error here
}
}
Now MyApp stateless Widget that will help us choose our path
class MyApp extends Stateless Widget{
Widget build(BuildContext context) {
//Using this FutureBuilder
return FutureBuilder<String>(
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// AsyncSnapshot<Your object type>
// Now if InternetOff is equal to one I would make it go to home
if(InternetOff==1) return MaterialApp(
theme: ThemeData.light(),
home: CheckInternet(),
debugShowCheckedModeBanner: false,
);
//else go to Home similarly with these if and else you can add more conditions
else {
return MaterialApp(
theme: ThemeData.dark(),
home: UserHome(),
debugShowCheckedModeBanner: false,
);
}
}
}
},
);
}
}
First of all, flutter-web like any other Single Page Application supports hash based routing. As a result if you want to access
localhost:8000/notes
you have to access it as
localhost:8000/#/notes
Cleaner way to handle auth state
Call getAuthState function before runApp() to make sure that the auth state is set before app is initialized. And pass authState to SplashScreen widget as parameter.
void main() {
WidgetsFlutterBinding.ensureInitialized();
AuthState authState = await Auth.getAuthState();
runApp(MaterialApp myApp = MaterialApp(
initialRoute: "/",
routes: {
"/": (context) => SplashScreen(authState: authState),
"/signin": (context) => SignInScreen(),
"/notes": (context) => NotesScreen(),
},
));
}
splash_screen.dart
class SplashScreen extends StatefulWidget {
final AuthState authState;
SplashScreen({Key key, this.authState}) : super(key: key);
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_goToNextScreen();
}
void _goToNextScreen() {
Future.delayed(
Duration(seconds:3),
() async {
String route = widget.authState == AuthState.SIGNED_IN ? "/notes" : "/signin";
Navigator.pushReplacementNamed(context, route);
}
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
And if you want even more cleaner way to handle auth state, you have to use state management solution like Provider.

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 ...
}
});
}
}