Using a FutureBuilder in a Flutter stateful widget with RefreshIndicator - flutter

I have a Flutter widget which gets data from a server and renders a List. After getting the data, I parse the data and convert it to an internal object in my application, so the function is something like this:
Future<List<Data>> getData(Thing thing) async {
var response = await http.get(Uri.parse(MY_URL));
// do some processing
return data;
}
After that, I've defined a stateful widget which calls this function and takes the future to render a List.
class DataList extends StatefulWidget {
const DataList({Key key}) : super(key: key);
#override
_DataListState createState() => _DataListState();
}
class _DataListState extends State<DataList> {
Widget createListView(BuildContext context, AsyncSnapshot snapshot) {
List<Data> values = snapshot.data;
if (values.isEmpty) {
return NoResultsWidget('No results.');
}
return ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return values[index];
},
);
}
#override
Widget build(BuildContext context) {
var data = getSomething().then((thing) => getData(thing));
return FutureBuilder(
future: data,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return CustomErrorWidget('Error');
case ConnectionState.waiting:
return LoadingWidget();
default:
if (snapshot.hasError) {
return CustomErrorWidget('Error.');
} else {
return createListView(context, snapshot);
}
}
},
);
}
}
Now, the code works just fine in this manner. But, when I try to move my data to be a class variable (of type Future<List>) that I update through the initState method, the variable just never updates. Example code below:
class _DataListState extends State<DataList> {
Future<List<Data>> data;
....
#override
void initState() {
super.initState();
updateData();
}
void updateData() {
data = getSomething().then((thing) => getData(thing));
}
....
}
I want to add a refresh indicator to update the data on refresh, and to do that I need to make my data a class variable to update it on refresh, but I can't seem to figure out how to make my data part of the state of the stateful widget and have it work. any help or guides to a github code example would be appreciated.

You need to wrap the assignment of the data variable in setState so that Flutter knows the variable changed and rebuilds your widget.
For example:
void updateData() {
setState(() {
data = getSomething().then((thing) => getData(thing));
});
}

Related

Flutter set state not updating my UI with new data

I have a ListView.builder widget wrapped inside a RefreshIndicator and then a FutureBuilder. Refreshing does not update my list, I have to close the app and open it again but the refresh code does the same as my FutureBuilder.
Please see my code below, when I read it I expect the widget tree to definitely update.
#override
void initState() {
super.initState();
taskListFuture= TaskService().getTasks();
}
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(builder: (context, taskData, child) {
return FutureBuilder(
future: taskListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
taskData.tasks = (snapshot.data as ApiResponseModel).responseBody;
return RefreshIndicator(
onRefresh: () async {
var responseModel = await TaskService().getTasks();
setState(() {
taskData.tasks = responseModel.responseBody;
});
},
child: ListView.builder(
...
...
Let me know if more code is required, thanks in advance!
Points
I am using a StatefulWidget
Task data is a class that extends ChangeNotifier
When I debug the refresh I can see the new data in the list, but the UI does not update
getTasks()
Future<ApiResponseModel> getTasks() async {
try {
var _sharedPreferences = await SharedPreferences.getInstance();
var userId = _sharedPreferences.getString(PreferencesModel.userId);
var response = await http.get(
Uri.parse("$apiBaseUrl/$_controllerRoute?userId=$userId"),
headers: await authorizeHttpRequest(),
);
var jsonTaskDtos = jsonDecode(response.body);
var taskDtos= List<TaskDto>.from(
jsonTaskDtos.map((jsonTaskDto) => TaskDto.fromJson(jsonTaskDto)));
return ApiResponseModel(
responseBody: taskDtos,
isSuccessStatusCode: isSuccessStatusCode(response.statusCode));
} catch (e) {
return null;
}
}
The issue here seems to be that you are updating a property that is not part of your StatefulWidget state.
setState(() {
taskData.tasks = responseModel.responseBody;
});
That sets a property part of TaskData.
My suggestion is to only use the Consumer and refactor TaskService so it controls a list of TaskData or similar. Something like:
Provider
class TaskService extends ChangeNotifier {
List<TaskData> _data;
load() async {
this.data = await _fetchData();
}
List<TaskData> get data => _data;
set data(List<TaskData> data) {
_data = data;
notifyListeners();
}
}
Widget
class MyTaskList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<TaskService>(builder: (context, service, child) {
return RefreshIndicator(
onRefresh: () {
service.getTasks();
},
child: ListView.builder(
itemCount: service.data.length,
itemBuilder: (BuildContext context, int index) {
return MyTaskItem(data:service.data[index]);
},
),
);
});
}
}
and make sure to call notifyListeners() in the service.getTasks() method to make the Consumer rebuild
I think (someone will correct me if I'm wrong) the problem is that you are using the FutureBuilder, once it's built, you need to refresh to whole widget for the FutureBuilder to listen to changes. I can suggest a StreamBuilder that listens to any changes provided from the data model/api/any kind of stream of data. Or better yet, you can use some sort of state management like Provider and use Consumer from the Provider package that notifies the widget of any changes that may occurred.

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

Reload widget in flutter

I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.

How to pass setstate as a parameter to a class method

in flutter I use a class to load values for switch widgets from a database and then update that database when the switch is toggled. Somehow I need to have that class call setstate on the calling function of the instance but it doesn't seem to work.
See the code below for an example.
The first switch is how I'd write it without the database class. That is working fine, when tapping the switch it both moves and the print shows that the value changed.
In the second switch widget however, I used the database class to build it but it doesn't seem to call the callback function correctly. The print always prints false.
I thought I tried all combinations of => and (){} but something is still amiss. I'm pretty sure the problem is how the callback is handled in the line: callBackFunctionForSetState();
maybe that should be called with callBackFunctionForSetState((){}); but that also doesn't work.
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
bool myBool = true;
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Title',
home: ScreenUpgrades(),
);
}
}
class ScreenUpgrades extends StatefulWidget {
#override
_ScreenUpgradesState createState() => _ScreenUpgradesState();
}
class _ScreenUpgradesState extends State<ScreenUpgrades> {
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Upgrades"),
),
body: FutureBuilder(
future: buildSwitchList(),
builder: (BuildContext ctxt, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView(children: snapshot.data);
} else {
return Center(child: CircularProgressIndicator());
}
}));
}
Future<List> buildSwitchList() async {
List<Widget> widgetList = [];
//This one below for a working example only
widgetList.add(Switch(value: myBool,onChanged: (bb)=>nonDBSetState()));
//Normally I'll create a bunch of widgets by loading their data from the DB as below
widgetList.add(DataBaseSwitchBuilder(1,()=>setState((){})).listViewWidget);
return widgetList;
}
nonDBSetState()
{
myBool = !myBool;
print('New value of first switch: ' + myBool.toString());
setState((){});
}
}
class DataBaseSwitchBuilder {
Widget listViewWidget;
int dbID;
bool onOff;
Function callBackFunctionForSetState;
DataBaseSwitchBuilder (int paramID, Function callBack)
{
dbID=paramID; //used to query the parameter from the DB
onOff = true;
callBackFunctionForSetState=callBack;
listViewWidget=(Switch(value: onOff,onChanged: (bb)=> updateDBAndState()));
}
updateDBAndState()
{
//update the switch
onOff = !onOff;
print('DB Swtich value now: ' + onOff.toString());
//first we save the record in the DB
//todo: code for updating DB
//Then call the passed function which should be a setstate from the calling function
//Below doesn't seem to work.
callBackFunctionForSetState();
}
}
I'm just expecting that the updateDBAndState will allow me to save the new value of the switch to the database and then call the setstate callback.
Just to respond to "How to pass setstate as a parameter to a class method"
widget controler
class Controller {
Controller._privateConstructor();
factory Controller() => _singleton;
static final Controller _singleton =
Controller._privateConstructor();
late Function setStateHandler;
void initSetState(Function setState) => setStateHandler = setState;
void triggerSetState() => setStateHandler();
}
widget
#override
void initState() {
super.initState();
controller.initSetState(() => setState(() {
widgetVariable = true;
}));
}

StreamBuilder receives only last item from stream

My ApplicationBloc is the root of the widget tree. In the bloc's constructor I'm listening to a stream from a repository that contains models decoded from JSON and forwarding them to another stream which is listened to by StreamBuilder.
I expected that StreamBuilder would receive models one by one and add them to AnimatedList. But there's the problem: StreamBuilder's builder fires only once with the last item in the stream.
For example, several models lay in the local storage with ids 0, 1, 2 and 3. All of these are emitted from repository, all of these are successfully put in the stream controller, but only the last model (with id == 3) appears in the AnimatedList.
Repository:
class Repository {
static Stream<Model> load() async* {
//...
for (var model in models) {
yield Model.fromJson(model);
}
}
}
Bloc:
class ApplicationBloc {
ReplaySubject<Model> _outModelsController = ReplaySubject<Model>();
Stream<Model> get outModels => _outModelsController.stream;
ApplicationBloc() {
TimersRepository.load().listen((model) => _outModelsController.add(model));
}
}
main.dart:
void main() {
runApp(
BlocProvider<ApplicationBloc>(
bloc: ApplicationBloc(),
child: MyApp(),
),
);
}
//...
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
final ApplicationBloc appBloc = //...
return MaterialApp(
//...
body: StreamBuilder(
stream: appBloc.outModels,
builder: (context, snapshot) {
if (snapshot.hasData) {
var model = snapshot.data;
/* inserting model to the AnimatedList */
}
return AnimatedList(/* ... */);
},
),
);
}
}
Interesting notice: in the StreamBuilder's _subscribe() method onData() callback triggers required number of times but build() method fires only once.
You need a Stream that outputs a List<Model instead of a single element. Also, listening to a stream to add it to another ReplaySubject will delay the output stream by 2 (!!!) frames, so it would be better to have a single chain.
class TimersRepository {
// maybe use a Future if you only perform a single http request!
static Stream<List<Model>> load() async* {
//...
yield models.map((json) => Model.fromJson(json)).toList();
}
}
class ApplicationBloc {
Stream<List<Model>> get outModels => _outModels;
ValueConnectableObservable<List<Model>> _outModels;
StreamSubscription _outModelsSubscription;
ApplicationBloc() {
// publishValue is similar to a BehaviorSubject, it always provides the latest value,
// but without the extra delay of listening and adding to another subject
_outModels = Observable(TimersRepository.load()).publishValue();
// do no reload until the BLoC is disposed
_outModelsSubscription = _outModels.connect();
}
void dispose() {
// unsubcribe repo stream on dispose
_outModelsSubscription.cancel();
}
}
class _MyAppState extends State<MyApp> {
ApplicationBloc _bloc;
#override
Widget build(BuildContext context) {
return StreamBuilder<List<Model>>(
stream: _bloc.outModels,
builder: (context, snapshot) {
final models = snapshot.data ?? <Model>[];
return ListView.builder(
itemCount: models.length,
itemBuilder: (context, index) => Item(model: models[index]),
);
},
);
}
}