Why ChangeNotifierProvider disappears from Widget Tree when using routes? - flutter

I'm having trouble understanding this: When the ChangeNotifierProvider widget is below the MaterialApp one in the widget tree, it disappear from the widget tree when I use the Navigator.of(context).pushReplacement method to go to the next screen. However, when the ChangeNotifierProvider widget is above the MaterialApp one in the widget tree, it stays in the widget tree.
Here is a simple example below.
ChangeNotifierProvider widget that disappear example:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp( <= DIFFERENCE HERE
title: 'Flutter Demo',
home: ChangeNotifierProvider(
create: (context) => ToiletsListModel(), child: SplashWidget()));
}
}
class SplashWidget extends StatefulWidget {
SplashWidget();
#override
State<SplashWidget> createState() => SplashWidgetState();
}
class SplashWidgetState extends State<SplashWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: Text("press"),
onPressed: goNext,
),
);
}
goNext() async {
await Future.delayed(Duration(seconds: 3));
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => HomeScreen()));
}
}
#override
void initState() {
super.initState();
[1, 2, 3, 4, 5].forEach((t) {
Provider.of<MyModel>(context, listen: false).mylist.add(t);
});
print(Provider.of<MyModel>(context, listen: false).mylist);
}
}
The widget tree:
ChangeNotifierProvider widget that stays example:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider( <= DIFFERENCE HERE
create: (context) => ToiletsListModel(),
child: MaterialApp(title: 'Flutter Demo', home: SplashWidget()));
}
}
class SplashWidget extends StatefulWidget {
SplashWidget();
#override
State<SplashWidget> createState() => SplashWidgetState();
}
class SplashWidgetState extends State<SplashWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: Text("press"),
onPressed: goNext,
),
);
}
goNext() async {
await Future.delayed(Duration(seconds: 3));
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (BuildContext context) => HomeScreen()));
}
}
#override
void initState() {
super.initState();
[1, 2, 3, 4, 5].forEach((t) {
Provider.of<MyModel>(context, listen: false).mylist.add(t);
});
print(Provider.of<MyModel>(context, listen: false).mylist);
}
}
The widget tree:
It makes me crazy and I can't find any answer. I suppose that pushReplacement() does something to the widget tree or that ChangeNotifierProvider is not persistent (altough I did not see that in the documentation, i'm not even sure it's possible tbh). Also, I read that using the ChangeNotifierProvider above the MaterialApp wasn't a good idea but I don't know why.
Thanks for your help.

Related

Flutter splash screen error - Navigator operation requested with a context that does not include a Navigator. How can I solve this error

Edit: (main.dart)
Added Sentry which actually starts the app
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = _sentryDSN;
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = _sentryTracesSampleRate;
options.attachStacktrace = true;
options.enableAppLifecycleBreadcrumbs = true;
},
appRunner: () => runApp(const SplashScreen()),
);
// or define SENTRY_DSN via Dart environment variable (--dart-define)
}
New to flutter, creating a splash screen to an app that was built with MaterialApp but getting an error. HOw can I solve this without a onPress function
Error:
Exception has occurred.
FlutterError (Navigator operation requested with a context that does not include a Navigator.
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.)
import 'package:flutter/material.dart';
import 'package:loopcycle/screens/loopcycle_main.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_navigateToMainApp();
}
void _navigateToMainApp() async {
await Future.delayed(const Duration(milliseconds: 2000), () {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const LoopcycleMainApp()));
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => const Center(
child: Text("test"),
)),
);
}
}
Thank you in advance.
EDIT: I changed the solution after you provided more information about the code.
This error is happening because you are using a context that does not have a Navigator in it, this is happening probrably because the widget that you are getting the context is parent of the MaterialApp() widget, to solve it you should create another widget that is a child of the MaterialApp() instead of using the parent widget, let me give you an example instead:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SomeWidget(),
),
),
child: Container(
height: 300,
width: 300,
color: Colors.red,
),
),
);
}
}
This may give an error because you are using the context of a widget that is the parent of the MaterialApp() widget, to solve it just create another widget that is a child of MaterialApp().
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: AnotherWidget(),
);
}
}
class AnotherWidget extends StatelessWidget {
const AnotherWidget({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SomeWidget(),
),
),
child: Container(
height: 300,
width: 300,
color: Colors.red,
),
),
);
}
}
I was playing with your code, and fixed it for you, and there are basically two ways to solve it, you can create a MaterialApp() before calling the SplashScreen() in the runApp() function like so:
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:loopcycle/screens/loopcycle_main.dart';
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = _sentryDSN;
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = _sentryTracesSampleRate;
options.attachStacktrace = true;
options.enableAppLifecycleBreadcrumbs = true;
},
appRunner: () => runApp(
const MaterialApp(
home: SplashScreen(),
),
),
);
// or define SENTRY_DSN via Dart environment variable (--dart-define)
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_navigateToMainApp();
}
void _navigateToMainApp() async {
await Future.delayed(const Duration(milliseconds: 2000), () {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const LoopcycleMainApp()));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (context) => const Center(
child: Text("test"),
)),
);
}
}
Or you can create an intermediate widget to hold the MaterialApp() and then inside this widget you can call SplashScreen(), like so:
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:loopcycle/screens/loopcycle_main.dart';
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = _sentryDSN;
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = _sentryTracesSampleRate;
options.attachStacktrace = true;
options.enableAppLifecycleBreadcrumbs = true;
},
appRunner: () => runApp(const MyApp()),
);
// or define SENTRY_DSN via Dart environment variable (--dart-define)
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
_navigateToMainApp();
}
void _navigateToMainApp() async {
await Future.delayed(const Duration(milliseconds: 2000), () {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const LoopcycleMainApp()));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (context) => const Center(
child: Text("test"),
)),
);
}
}
In this second solution, the intermediate widget is the MyApp() widget, and in my opinion, I consider this solution as being the best one for your problem, because if you ever wanted to load a different screen based on the different states, for example:
If a user is signed in you load a home page, and if a user is not signed in you load a sign up page.
Doing this, or anything similar is much easier when you have this intermediate widget that holds the MaterialApp(), and you can even create some logic to integrate the Splash Screen too, but I don't know what you are trying to achieve, so pick the solution you find the best for your problem.

Flutter Context Error in Navigator pushReplacement

I am creating a Splash Screen and I am getting Context error in Navigator push replacement
Following is the code for splash screen in main.dart file
import 'dart:async';
import 'package:number_trivia/pages/home.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: splash_screen(),
));
}
class splash_screen extends StatefulWidget {
#override
_splash_screenState createState() => _splash_screenState();
}
class _splash_screenState extends State<splash_screen> {
#override
void initState() {
super.initState();
Timer(Duration(seconds: 3),
()=>Navigator.pushReplacement(context,
MaterialPageRoute(builder:
(context) =>home()
)
)
);
}
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: FlutterLogo(size: MediaQuery.of(context).size.height,),
);
}
}
The error says - The argument type 'JsObject' can't be assigned to the parameter type 'BuildContext'.
How do I correct it?
Any help will be much appreciated:)
When widget build completed, you can call Timer function.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Timer(Duration(seconds: 3), () {
Navigator.pushReplacement(context,
MaterialPageRoute(builder:
(context) =>home()
)
);
});
});
}
to that pushReplacement method, you passed a context which wasn't specified upper in the widget tree.
try wrapping the screen with a widget that has a build method so that it creates a BuildContext that you can use.
like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: splash_screen(context),
);
}
}
class splash_screen extends StatefulWidget {
BuildContext context;
splash_screen(this.context);
#override
_splash_screenState createState() => _splash_screenState();
}
class _splash_screenState extends State<splash_screen> {
#override
void initState() {
super.initState();
Timer(
Duration(seconds: 3),
() => Navigator.pushReplacement(
widget.context, MaterialPageRoute(builder: (context) => home())));
}
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: FlutterLogo(
size: MediaQuery.of(context).size.height,
),
);
}
}
does that help?

Is there anyway that I can use MultiBlocProvider to clean this nest of widgets up?

I'm trying to clean this mess of widgets up but I have found no way to do so. My NavigationBloc depends on the stream provided by AuthenticationBloc and to prevent memory leaks I have to close the stream.
The Builder widget is required so that I can get the latest BuildContext provided by BlocProvider but I know that MultiBlocProvider would clean this up tremendously. I'd like to avoid wrapping this widget in the runApp function but it's an option I guess.
class _MyAppState extends State<MyApp> {
final authRepo = AuthRepo();
AuthenticationBloc authBloc;
#override
void dispose() {
authBloc?.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
create: (_) =>
AuthenticationBloc(authRepo: authRepo)..add(InitializeAuth()),
child: Builder(builder: (context) {
authBloc = context.bloc<AuthenticationBloc>();
return BlocProvider<NavigationBloc>(
create: (_) => NavigationBloc(authBloc),
child: MaterialApp(
title: 'Arrow Manager',
debugShowCheckedModeBanner: false,
theme: appTheme(),
builder:
ExtendedNavigator<Router>(router: Router(), initialRoute: '/'),
),
);
}),
);
}
}
As you say, you can use the MultiProvider to avoid having nested providers
You have to create your AuthenticationBloc in the initState() method
class _MyAppState extends State<MyApp> {
final authRepo = AuthRepo();
AuthenticationBloc authBloc;
#override
void initState() {
super.initState();
authBloc = AuthenticationBloc(authRepo: authRepo);
}
#override
void dispose() {
authBloc?.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => authBloc..add(InitializeAuth()),
),
BlocProvider(
create: (context) => NavigationBloc(authBloc),
),
],
child: Builder(
builder: (context) {
authBloc = context.bloc<AuthenticationBloc>();
return MaterialApp(
title: 'Arrow Manager',
debugShowCheckedModeBanner: false,
theme: appTheme(),
builder: ExtendedNavigator<Router>(router: Router(), initialRoute: '/'),
);
},
),
);
}
}

This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets

I have got follow app:
class MyAppState extends State<MyApp>
{
TenderApiProvider _tenderApiProvider = TenderApiProvider();
Future init() async {
await _tenderApiProvider.getToken();
}
MyAppState()
{
init();
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => _tenderApiProvider),
],
child: MaterialApp(
title: "My App",
routes: {
'/': (context) => HomePage(),
'/splash-screen': (context) => SplashScreen(),
'/result_table': (context) => ResultDataTable(),
}
),
);
}
}
I need to draw firstly SplashScreen current code show at start HomePage.
In splash-screen I need to switch to HomePage after all data loaded. Here it's code:
Widget build(BuildContext context) {
TenderApiProvider apiProv = Provider.of<TenderApiProvider>(context);
return StreamBuilder(
stream: apiProv.resultController,
builder: (BuildContext context, AsyncSnapshot snapshot) {
//...
if(apiProv.apiKeyLoadingState == ApiKeyLoadingState.Done && apiProv.regionsLoadingState == RegionsLoadingState.Done)
{
Navigator.of(context).pushNamed("/"); // Should it be placed in Build??
}
});
}
Could you help me and show to to draw at app start SplashScreen and then switch from it to HomePage?
You will need to wrap your SplashScreen() inside a StatefulWidget so you can fetch your data in initState(). It is important to wrap fetch() logic inside a SchedulerBinding.instance.addPostFrameCallback() to access the BuildContext inside initState(). Also, that way, you avoid conflicts with RenderObjects that get destoryed while they are actually build.
Following a complete minimal example.
EDIT: You cant use await in initState({}).
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Wrapper(),
);
}
}
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
_loadDataAndNavigate()
});
}
_loadDataAndNavigate() async {
// fetch data | await this.service.fetch(x,y)
Navigator.of(context).pushNamed('/');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SplashScreen(),
);
}
}
i use splashScreen
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SplashHome(),
routes: <String, WidgetBuilder>{
'/HomeScreen': (BuildContext context) => new ImageHome()
},
);
}
}
class SplashHome extends StatefulWidget{
#override
State<StatefulWidget> createState() {
return _SplashHome();
}
}
const timeout = const Duration(seconds: 2);
class _SplashHome extends State<SplashHome>{
startTimeout() {
return new Timer(timeout, handleTimeout);
}
void handleTimeout() {
Navigator.of(context).pushReplacementNamed('/HomeScreen');
}
#override
void initState() {
super.initState();
startTimeout();
}
#override
Widget build(BuildContext context) {
return new Container(
color: Colors.lightBlue,
);
}
}

Flutter : Navigator operation requested with a context that does not include a Navigator

I have a scenario wherein I check the value of SharePreferences based on the value it will redirect the user to HomePage or LandingPage. I am not sure where did I got wrong? but I am getting this error below: I guess its not getting the context right any idea how do I get it?.
Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
E/flutter (11533): 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.
Here is my code:
import 'package:credit/src/pages/landing.dart';
import 'package:flutter/material.dart';
import 'package:credit/src/pages/credit/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
_LoadingPageState createState() => _LoadingPageState();
}
class _LoadingPageState extends State<MyApp> {
#override
void initState() {
super.initState();
getUserStatus().then((userStatus) {
if (userStatus == null) {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return LandingPage();
}));
} else {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return HomePage();
}));
}
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
));
}
}
Future<String> getUserStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String userStatus = prefs.getString('userstatus');
print("==On Load Check ==");
print(userStatus);
return userStatus;
}
When you call Navigator.of(context) framework goes up in widget tree attached to provided context and tries to find the closest Navigator.
The widget tree you showed does not have one, so you need to include Navigator in the widget tree.
Easiest option is to use MaterialApp with your widget passed as home. MaterialApp is creating navigator inside itself. (CupertinoApp does it too)
Updated code from original example:
import 'package:credit/src/pages/landing.dart';
import 'package:flutter/material.dart';
import 'package:credit/src/pages/credit/home.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: LoadingPage(),
);
}
}
class LoadingPage extends StatefulWidget {
LoadingPage({Key key}) : super(key: key);
_LoadingPageState createState() => _LoadingPageState();
}
class _LoadingPageState extends State<LoadingPage> { // note type update
#override
void initState() {
super.initState();
getUserStatus().then((userStatus) {
if (userStatus == null) {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return LandingPage();
}));
} else {
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return HomePage();
}));
}
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: CircularProgressIndicator(),
));
}
}
Future<String> getUserStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String userStatus = prefs.getString('userstatus');
print("==On Load Check ==");
print(userStatus);
return userStatus;
}
I have changed my code from
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo App',
theme: ThemeData(
primarySwatch: white,
scaffoldBackgroundColor: Colors.white,
),
home: Scaffold(
appBar: AppBar(
title: Text('Demo App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
HomeScreen(title: 'Demo Home')));
},
child: Text('Open Home Screen'))
],
),
),
),
);
}
To
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo App',
theme: ThemeData(
primarySwatch: white,
scaffoldBackgroundColor: Colors.white,
),
home: InitScreen());
}
}
class InitScreen extends StatelessWidget {
const InitScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Demo App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
HomeScreen(title: 'Demo Home')));
},
child: Text('Open Home Screen'))
],
),
),
);
}
What changed?
Create a separate widget for home code in MyApp with InitScreen
What was the issue?
When we try to push Route by using Navigator.of(context), flutter will
try to find Navigator in the widget tree of the given context. In the
initial code, there was no widget that has Navigator. So, create a
separate widget for home code. And the MaterialApp widget in MyApp
will have Navigator.