I want to get my state from database upon startup. I use provider with ChangeNotivierProvider at my first widget. Code included for clarification.
This is my main method, it has the widget that provides the state i plan to use in the app:
void main() => runApp(
ChangeNotifierProvider(
child: MyApp(),
builder: (context) => StateDao.get(),
),
}
My DAO just returns the state from database (i use sembast, but could just as easily be sqflite or Firebase)
Future<State> get() async {
// Database logic
}
State is just an object extending ChangeNotifier
class State extends ChangeNotifier {
// Getters, setters, changeNotifiers etc.
}
This will not work as i cannot call async methods in the builder-method of ChangeNotifierProvider. How, when and where should this be initialized? As i understand it, async calls should not be done in any build methods. I tried overriding didChangeDependencies that provided context-access but i could not get past the async-call in builder method limitation.
You can initialize the value in initState. For example, you can store Future object and use FutureBuilder to build widget when a value is resolved:
Future<State> futureState;
#override
void initState() {
// Calling async method and storing Future object
futureState = StateDao.get();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<State>(
future: futureState,
builder: (context, snapshot) {
// No data yet, showing loader
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return ChangeNotifierProvider(
child: MyApp(),
builder: (context) => snapshot.data,
);
},
);
}
Another similar approach is to use StreamController and StreamBuilder:
final StreamController<State> stateStream = StreamController<State>();
#override
void initState() {
// Calling async method and setting callback to add data to the stream
StateDao.get().then((v) => stateStream.add(v));
super.initState();
}
#override
void dispose() {
// Closing the sink
stateStream.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<State>(
stream: stateStream.stream,
builder: (context, snapshot) {
// No data yet, showing loader
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return ChangeNotifierProvider(
child: MyApp(),
builder: (context) => snapshot.data,
);
},
);
}
If you need more complex architecture, you might want to look at BLoC, which is very popular architecture in Flutter community.
Related
I'm using StreamProvider from the provider package for auth functionality in my flutter-firebase app, just like it is explained in this tutorial https://www.youtube.com/watch?v=j_SJ7XmT2MM&list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC&index=9.
When trying to run my app, I get an error message, with a suggestion how to do it correctly, but my code IS written in the way that is suggested.
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App());
}
class FirebaseWrapper extends StatelessWidget {
// Create the initialization Future outside of build():
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
// final Future<void> _initSharedPrefs = SharedPrefsHelper().initSharedPrefsInstance();
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
return FutureBuilder(
// from: https://firebase.flutter.dev/docs/overview/#initializing-flutterfire
future: _initialization,
// future: Future.wait([_initialization, _initSharedPrefs]),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorPage(); //TODO better error pages
if (snapshot.connectionState == ConnectionState.done) return FirebaseAuthWrapper();
return Loading(); //waiting
},
);
}
}
class FirebaseAuthWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: Auth().userStream,
initialData: null,
child: App(),
);
}
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print('yeet');
return MaterialApp(
key: UniqueKey(),
title: 'Wanderapp',
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: (user == null) ? '/signIn' : '/',
routes: (user == null)
? {
'/signIn': (context) => SignIn(),
'/register': (context) => Register(),
// '/forgotPassword': (context) => ForgotPassword(),
}
: {
'/': (context) => Home(),
//...
},
);
}
}
the error message:
Error: Could not find the correct Provider<User> above this App Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that App is under your MultiProvider/Provider<User>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
```
consider using `builder` like so:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
```
I'm user the same "User" class from Firebase for StreamProvider and Provider.of, the hierarchy/scope also seems to be correct in my code, but it doesn't work.
Does anyone know what my mistake is? Thank you very much.
In this link about runApp it says:
Calling runApp again will detach the previous root widget from the
screen and attach the given widget in its place.
So, you just need to remove the second runApp, as App is being called anyway from the StreamProvider: child: App(),.
Solution:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(FirebaseWrapper());
runApp(App()); //*** Remove this line ***///
}
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.
I have a Listview.builder inside a FutureBuilder which taking some data from an http request.i have a bool closed i want to prevent some items from refreshing if status bool is true
how can I do that
You can achieve this by placing your call in initState. In this way you can make sure that it will get the data only once.
example:
class FutureSample extends StatefulWidget {
// Create instance variable
#override
_FutureSampleState createState() => _FutureSampleState();
}
class _FutureSampleState extends State<FutureSample> {
Future myFuture;
Future<String> _fetchData() async {
await Future.delayed(Duration(seconds: 10));
return 'DATA';
}
#override
void initState() {
// assign this variable your Future
myFuture = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FutureBuilder(
future: myFuture,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
}
return CircularProgressIndicator();
},
),
),
);
}
}
In that way you don't need a bool value. There are also different ways to achieve or extend your request. You can check this article for more informations: https://medium.com/flutterworld/why-future-builder-called-multiple-times-9efeeaf38ba2
I'm relatively new to Flutter and am currently struggling with FutureBuilders.
I've read Remi's answer on this thread, which basically states Flutter's philosophy that widget builds should be idempotent. I get the principle in theory but how does that work practically though?
Consider the following snippet:
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Consumer<DataPresenter>(
builder: (context, presenter, _) {
return DefaultFutureBuilder<List<Data>>(
future: presenter.data(),
builder: (context, data) {
// bla bla
}
);
}
),
// More widgets
],
),
),
);
}
Where this is my DefaultFutureBuilder:
class DefaultFutureBuilder<T> extends StatelessWidget {
final Future<T> future;
final Widget Function(BuildContext, T data) builder;
final Widget Function(BuildContext) errorBuilder;
DefaultFutureBuilder({
#required this.future,
#required this.builder,
this.errorBuilder,
});
#override
Widget build(BuildContext context) {
var errBuilder = errorBuilder ?? _buildDefaultErrorScreen;
return FutureBuilder<T>(
future: future,
builder: (context, snapshot) {
var indicator = Center(child: CircularProgressIndicator());
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData)
return builder(context, snapshot.data);
else if (snapshot.hasError)
return errBuilder(context);
}
return indicator;
},
);
}
Widget _buildDefaultErrorScreen(BuildContext context) {
// default
}
}
Now I know that the call to presenter.data() (fetches some async data) is not kosher. But I do want to re-fetch the data when the presenter notifies the consumer while at the same time I do not want to fetch it when Flutter rebuilds my widget tree because of framework shenanigans.
My initial idea was to build the presenter so that it only fetches the data when it does not have any data at the moment. I could then set the data to null from other methods and notify the listeners to rebuild the affected widget subtrees.
Like that:
class DataPresenter extends ChangeNotifier {
// fields, services, bla bla
List<Data> _data;
Future<List<Data>> data() async {
if (_data == null) {
var response = await _service.fetchMyStuff(params: params);
_data = response.data;
}
return _data;
}
}
The problem with this solution is that any rebuilds that happen while I fetch the data for the first time will cause another request.
I'd be grateful if someone could point out which part of the framework / architecture I didn't get.
I think this might solve your problem I had this problem too very annoying anything you do and your app rebuilds. So they way I solved it was by memorizing my future. Now this may or may not work for you. If it treats the Consumer rebuild as a new future then you will be good because then the FutureBuilder will rebuild when the Consumer does which is what you want if I understand correctly. Here's what you do.
//Initialize at top of class
final AsyncMemoizer _memoizer = AsyncMemoizer();
Future<void> _someFuture() {
return this._memoizer.runOnce(() async {
//Do your async work
});
}
Then _someFuture() would be what you pass to FutureBuilder. Here is a good article on this issue. Flutter: My FutureBuilder Keeps Firing!
If you want to rebuild a provider then you can use
context.refresh(providerThatYouWantToRebuild);
This method is useful for features like "pull to refresh" or "retry on error", to restart a specific provider at any time.
Documentation is available at:
https://riverpod.dev/docs/concepts/combining_providers#faq
https://pub.dev/documentation/riverpod/latest/all/ProviderContainer/refresh.html
https://pub.dev/documentation/flutter_riverpod/latest/all/BuildContextX/refresh.html
I would suggest a solution, without a futureBuilder
You fetch data after your first Build of your Stateless Widget
WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<DataPresenter>(context, listen: false).fetchdata());
you have a difference in your _data List with value null or lenght==0
class DataPresenter extends ChangeNotifier {
// fields, services, bla bla
List<Data> _data;
// getter for the Consumer
List<Data> get data => _data;
Future<List<Data>> fetchdata() async {
_data = await _service.fetchMyStuff(params: params);
if (_data == null)
_data = new List(); //difference between null and length==0
//rebuild Consumer
notifyListeners();
}
}
The Page without FutureBilder
class ProvPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
// calls after the widget ist build
WidgetsBinding.instance.addPostFrameCallback((_) => Provider.of<TodoListModel>(context, listen: false).refreshTasks());
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => DataPresenter()),
],
child: Scaffold(
body: SafeArea(
child: Stack(
children: [
Consumer<DataPresenter>(
builder: (context, presenter, _) {
//get current Values vom the DataPresenter._data
List<Data> currData = presenter.data;
// firstime fetch ??
if(currData == null)
return CircularProgressIndicator();
// DataPresenter.getData is finished
return new RefreshIndicator(
onRefresh: () => presenter.fetchdata(),
child: new Text("length " + currData.length.toString())
);
},
),
// More widgets
],
),
),
),
);
}
}
From this answer:
The build method is designed in such a way that it should be pure/without side effects.
and
This means that the build method should not trigger an http call or modify any state.
But this contradicts with firestore plugin usage example(condensed for briefness):
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
// do something with books
},
);
}
}
Anytime when build method called, builder function from StreamBuilder called as well.
What I tried:
...
stream: Firestore.instance.collection('books').snapshots().distinct(),
...
Neither advice from previously mentioned answer works for this case.
The solution is the same actually: Make a StatefulWidget
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
Stream<QuerySnapshot> stream;
#override
void initState() {
super.initState();
stream = Firestore.instance.collection('books').snapshots();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: stream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
// do something with books
},
);
}
}