Flutter: How to make a sequence of http requests on a widget before build method - flutter

I have 3 classes: Users, Posts and Comments. User has many Posts and
Posts has many Comments.
I want that all data to be fetched before the widget's build method is called.
I tryed to use initState() to do this:
class FetchDataExample extends StatefulWidget {
final User _user;
FetchDataExample(this._user);
#override
_State createState() => _State(_user);
}
class _State extends State<FetchDataExample> {
final User _user;
_State(this._user);
#override
void initState() {
_user.setPosts();
super.initState();
}
#override
Widget build(BuildContext context) {
print(this._user.posts[0]);
return Container(
);
}
}
In User class I have:
void setPosts() async {
String url = 'https://jsonplaceholder.typicode.com/posts?userId=' + this.id.toString();
var request = Requester.get(url); // Returns a Future<Response>
await request.then((value) => this.posts = Post.jsonToPosts(json.decode(value.body)));
this.posts.forEach((post) => post.setComments());
print(this.posts[0]);
}
The 'setComments()' has the same logic.
I have two prints:
Inside build that returns null;
Inside setPosts the returns Instance of 'Post';
So, by the time that Build method is called in the widget, the initState has not finished yet.
I need it be finished, does anyone know how can I do that?

You can use a FutureBuilder to build a widget by using latest result from a future.
And also you can combile multiple futures into a single one using Future.wait method.
Here is a sample code:
_getPageData() async {
var _combinedFutures = await Future.wait([setPosts, setComments]);
//do stuff with data
}
...
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:_getPageData(),
builder: (context, snapshot) {
return Container();
}),
);
});

Related

How can i call my provider model into initState method

i have several widgets use my provider as a condition , and i need one call to access my provider to whole widget from init state instead of wrapping every widget into my provider and it's consumer
this is my provider
class ProviderForFiltter extends ChangeNotifier {
bool isFiltterrr = true ;
bool get isFiltter => isFiltterrr;
void changeStatus(bool status){
isFiltterrr = status;
notifyListeners();
}
}
this is my main.dart
class Myproject extends StatefulWidget {
const Myproject ({Key? key}) : super(key: key);
#override
_Myproject State createState() => _Myproject State();
}
class _Myproject State extends State<Myproject > {
#override
Widget build(BuildContext context) {
return
Provider(
create: (BuildContext context) {
return ProviderForFiltter();
},
child: const MaterialApp(
debugShowCheckedModeBanner: false,
home: WelcomeScreen()
),
),
);
}
}
this is my Stful Widget
ProviderForFiltter? isF ;
#override
void initState() {
super.initState();
// i tried this but it always give me errors that is isF null value
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
isF = context.read<ProviderForFiltter>();
});
// also itried this but it don't work
isF = Provider.of<ProviderForFiltter>(context, listen: false);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Text('change'),
)
}
}
in the fact i need to use it's bool value as condition into Consumer and change it
i hope any help guys
is better don't do use Provider in initState, but you can use Future.delayed
because you need context
#override
void initState() {
super.initState();
// i tried this but it always give me errors that is isF null value
Future.delayed(Duration(seconds: 1), () {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
isF = context.read<ProviderForFiltter>();
});
// also itried this but it don't work
isF = Provider.of<ProviderForFiltter>(context, listen: false);
});
}
providers need context, in order to access it for one time you should override didChangeDependencies
#override
void didChangeDependencies() {
super.didChangeDependencies();
///access provider here and update your state if needed,
///this will be called one time just before the build method
**isF = Provider.of<ProviderForFiltter>(context, listen: false);**
}
There are multiple ways to deal with this.
The first option which I use is to add a Post Frame Callback like so:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
aProvider = Provider.of< aProvider >(context, listen: false);
});
Alternatively, you could override the didChangeDependencies method to get the provider value once initState has been called - remembering to set the listen value to false.
I was facing the same issue and regarding the documentation of provider this should be the answer.
"This likely happens because you are modifying the ChangeNotifier from
one of its descendants while the widget tree is building."
In my case i am calling an http api async where the future is stored inside the notifier. So i have to update like this and it is working.
initState() {
super.initState();
Future.microtask(() =>
context.read<MyNotifier>().fetchSomething(someValue);
);
}
The best way is to use like this (when there's no "external parameter".
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {}
}
source : https://pub.dev/packages/provider
You can use a different method called didChangeDependencies to get the value from the provider after the initState method is called. Also, make sure to set the listen value to false.
#override
void didChangeDependencies() {
super.didChangeDependencies();
final filtterData = Provider.of<ProviderForFiltter>(context, listen: false);
}

Async Data Initialization in initState

I'm calling an async method getMyLocation() to get my current location in my initState(). The method can take a while...
I wanted to understand the behavior of initState() in these cases. Does the method still execute in the background as build() renders or does initState() timeout since it needs to complete before build() renders?
In my build() I have a statement checking if my latitude is null, in which case I return a Loading() widget. Sometimes Screen() renders and sometimes Loading() goes on indefinitely. I am assuming sometimes the getMyLocation() successfully executes during initState() and sometimes it timesout?
#override
void initState() {
super.initState();
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = userData.getUser();
userData.getMyLocation();
}
getMyLocation() async {
_myUser.longitude = await getCurrentLongitude();
_myUser.latitute = await getCurrentLatitude();
notifyListeners();
}
Widget build(BuildContext context) {
final userData = Provider.of<MyUser>(context);
final myUser = userData.getUser();
myUser.latitude == null?
return Loading()
: return Screen()
Great question. First of all, initState() runs synchronously, it prepares various things needed for build() method to run properly. If you are executing some async function here, it will just return a Future because you can't await it in the initState(). In your case you probably need a FutureBuilder. The "proper way" of dealing with futures would be something like:
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<void> getMyLocation() async {
final userData = Provider.of<MyUser>(context, listen: false);
final myUser = await userData.getUser();
// if getUser() is async then we have to await
myUser.longitude = await getCurrentLongitude();
myUser.latitute = await getCurrentLatitude();
// notifyListeners();
// You probably do not need this, should be done in provider methods instead
}
Widget build(BuildContext context) {
return FutureBuilder(
future: getMyLocation(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return SomeErrorWidget();
}
if (snapshot.hasData) {
return Screen(snapshot.data);
}
return SomeLoadingWidget();
});
}

Flutter: Stateful Widget does not update

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

Widget continuously being reloaded

Please check out this 36 seconds video for more clarity, cause it was getting too verbose explaning things : https://www.youtube.com/watch?v=W6WdQuLjrCs
My best guess
It's due to the provider.
App structure ->
Outer Page -> NoteList Page
The Outer Page code :
class OuterPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
var noteList;
final _pageOptions = [
NoteList(),
AnotherPageScreen(),
];
#override
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
var customFabButton;
if (_selectedTab == 0) {
customFabButton = FloatingActionButton(
// Password section
onPressed: () {
navigateToDetail(context, Note('', '', 2), 'Add Note');
},
child: Icon(Icons.add),
);
~~~ SNIP ~~~
The Notes Tab aka NoteList page code :
class NoteList extends StatefulWidget {
NoteList();
#override
NoteListState createState() => NoteListState();
}
class NoteListState extends State<NoteList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context).getNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context).count > 0
? NoteListScreen(_scaffoldKey)
: CircularProgressIndicator());
}
}
For full code : check here : https://github.com/LuD1161/notes_app/tree/reusable_components
Update 1 - Possible solution is FutureBuilder
I know that there's a possible solution with FutureBuilder but I think even Provider is apt for this use case.
Moreover is it an anti-pattern here ?
Also, please don't suggest another package for the same thing, if possible try limiting the solution to Provider or base libraries.
Update 2 - Not possible with FutureBuilder
FutureBuilder can't be used here because there's a delete button in the list tile and hence when the note gets deleted the note list won't get updated.
The issue is coming because of the getNotes function you are calling from build method. You are calling notifyListeners from that function. It again re-builds the widget and calls the build method again and this cycle continues.
You either need to set false to the listen property of provider, but it will break your functionality. To fix this, you have to move the getNotes call from build function to initState like following:
#override
void initState() {
super.initState();
postInit(() {
Provider.of<NotesProvider>(context).getNotes();
});
}
Implement postInit (Reference: Flutter Issue 29515):
extension StateExtension<T extends StatefulWidget> on State<T> {
Stream waitForStateLoading() async* {
while (!mounted) {
yield false;
}
yield true;
}
Future<void> postInit(VoidCallback action) async {
await for (var isLoaded in waitForStateLoading()) {}
action();
}
}
Note: Instead of writing the postInit code, you can also use after_init package for same purpose.
Several other posts discussing similar kind of issues:
How to correctly fetch APIs using Provider in Flutter
Using provider in fetching data onLoad

Canonical way to use FutureBuilder with NetworkImage

I would like to be able to get to a network image within a single microtask if the image is already loaded. However, with the current API available in NetworkImage and FutureBuilder, this does not seem to be possible.
This is how we typically wire the two:
NetworkImage imageProvider = getSomeNetworkImage(id);
Completer<ui.Image> completer = Completer<ui.Image>();
imageProvider.resolve(ImageConfiguration()).addListener(
(ImageInfo info, _) => completer.complete(info.image));
return FutureBuilder<ui.Image>(
future: completer.future,
builder: (BuildContext futureBuilderContext, AsyncSnapshot<ui.Image> snapshot) {
if (!snapshot.hasData) {
return _buildPlaceholder();
} else {
return _buildActual(context, snapshot.data, imageProvider);
}
},
);
addListener() immediately calls completer.complete() if the image is already there. However, FutureBuilder is based off of completer.future which does not complete until the next microtask. So even when the image is available, placeholder is displayed momentarily.
What is the best way to avoid this? Perhaps, imageProvider should expose a Future that prevents us from piping this through a completer?
Instead of using a FutureBuilder, I would take advantage of the syncCall argument passed to the listener of the ImageStream. This will tell you if the image resolved immediately, meaning it is already cached. Otherwise you can call setState and trigger a rebuild when it does complete.
class Example extends StatefulWidget {
const Example({Key key, this.image, this.child}): super(key: key);
final ImageProvider image;
final Widget child;
#override
State createState() => new ExampleState();
}
class ExampleState extends State<Example> {
bool _isImageLoaded = false;
#override
void initState() {
widget.image
.resolve(const ImageConfiguration)
.addListener(_handleResolve);
super.initState();
}
#override
Widget build(BuildContext context) {
// if syncCall = true, then _handleResolve will have already been called.
if (_isImageLoaded)
return new Image(widget.image);
return widget.child;
}
void _handleResolve(ImageInfo info, bool syncCall) {
_isImageLoaded = true;
if (!syncCall) {
// we didn't finished loading immediately, call setState to trigger frame
setState(() { });
}
}
}