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.
Related
I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}
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.
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));
});
}
I am working on a flutter application using several dialogs for several purposes.
In our code, there are some cases where the user can open a Dialog. Inside this dialog, there are some buttons that will also open another dialog. It results with 2 dialogs on top of each other and with a very dark background screen.
What we would like to do is to only display one dialog at the time. How can we achieve that ?
Here is a simple code to illustrate our issue:
class MyScreen extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog1>(
context: context,
builder: (BuildContext context) => MyFirstDialog(),
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MyFirstDialog extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button in first dialog'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog2>(
context: context,
builder: (BuildContext context) => MySecondDialog(), // <- This will appear on top of the first dialog
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MySecondDialog extends StatelessWidget {
#override
build(BuildContext context) {
return Text('Second Dialog');
}
}
let me give you a widget for that
class MultiDialog extends StatefulWidget {
final Widget child;
const MultiDialog({Key key, this.child}) : super(key: key);
#override
_MultiDialogState createState() => _MultiDialogState();
static void addDialog(
{#required BuildContext context, #required Widget dialog}) {
assert(context != null, "the context cannot be null");
assert(dialog != null, "the dialog cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._addDialog(dialog);
}
static void remove({#required BuildContext context}) {
assert(context != null, "the context cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._remove();
}
}
class _MultiDialogState extends State<MultiDialog> {
final _allDialogs = <Widget>[];
void _addDialog<T>(Widget dialog) {
assert(dialog != null, "The dialog cannot be null");
setState(() {
_allDialogs.add(dialog);
});
}
void _remove() {
if (_allDialogs.isEmpty) {
print("No dialogs to remove");
return;
}
setState(() {
_allDialogs.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_allDialogs.isNotEmpty) _allDialogs.last,
],
);
}
}
and when you want to add a dialog just call
MultiDialog.addDialog(
context: context,
dialog: AlertDialog(),
);
call Navigator.pop to remove the dialog, if there is another dialog which you pushed exist it will be shown, you can further pop them all with results, PS:this code isn't tested, let me know in the comments if this works for you
call MultiDialog.remove(context:context) to pop the visible dialog and bring back the previous dialog,
and if you receive a error that the addDialog is called on null, its because how flutter works, after MultiDialog use a Builder to introduce a new context use it call showDialog,
PS:ABOVE CODE IS TESTED
i made a stream out of the events that cause the dialog to pop up and used rx darts exhaust map to wait for the result (i was already using rxdart)
dialogEventStream
.exhaustMap((_) => maybeShowDialog().asStream())
.listen((_) {});
Future<bool> maybeShowOfflineModeDialog() async {
final isOfflineModeEnabled = await _sharedPreferencesService.isOfflineModeEnabled();
if (!isOfflineModeEnabled) {
final isLoginOffline = await _navigationService.showDialog(NoConnectionDialog());
if (isLoginOffline == true) {
await _sharedPreferencesService.setIsOfflineModeEnabled(isOfflineModeEnabled: true);
return await _navigationService.pushReplacement(AppShellOffline.routeName) ?? true;
} else {
return false;
}
}
return false;
}
something like this
I have been learning flutter for 2-3 months now and I feel I have a reached a fundamental roadblock with understanding state management. This post will be long unfortunately so please bare with me and I hope I put the right detail.
Problem Definition
I have a list of widgets in a shopping cart,im at the point where I click minus and it only has 1 left the widget must be removed.No matter what I try I cant get that widget to be removed. If I click back button and go back into cart the Item will not appear anymore.
I have considered other methods, like disposing the widget(that didn't seem to work) and I was busy implementing Visibility Show/hide widgets in Flutter programmatically
but that doesn't feel like the right way.If my understanding of providers,changeNotifiers,async and future builders,is correct the below method should work and I think its fundamental to my flutter journey to understand why it doesn't work.
Overview:The idea was to use the minus button on CartItemWidget to call a method that updates Json stored on the local device, then repopulate the List cartProdList in ProductProvider which calls
notifyListeners() and then should propagate everywhere the provider is used. Now I have used this pattern successfully 5 times now, the only different this time is it will be removing a widget which I haven't done before. But this should work dynamically if the future is based of the same provider right ?
function call order
CartItemWidget.onPressed:()
calls >>>
ProductProvider.cartMinusOne(String id)
calls >>>
ProductProvider.Future<List<Product>> cartProducts()
well here goes the code.I also wouldn't mind comments on things I could be doing better in all areas.
CartWidget
class CartWidget extends StatefulWidget {
#override
_CartWidgetState createState() => _CartWidgetState();
}
class _CartWidgetState extends State<CartWidget> {
var providerOfProd;
ProductProvider cartProdProvider = new ProductProvider();
#override
void initState() {
_productsList = new ProductsList();
super.initState();
providerOfProd = Provider.of<ProductProvider>(context, listen: false).cartProducts();
}
#override
Widget build(BuildContext context) {
........
Column(children: <Widget>[
FutureBuilder(
future: providerOfProd,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Container(
width: 0,
height: 0,
);
case ConnectionState.done:
return ListView.separated(
..............
},
itemBuilder: (context, index) {
return CartItemWidget(
product: cartProdProvider.cartProdList.elementAt(index),
heroTag: 'cart',
quantity: cartProdProvider.cartProdList.elementAt(index).cartqty,
key: UniqueKey(),
);
},
);
.........
CartItemWidget
class CartItemWidget extends StatefulWidget {
CartItemWidget({Key key, this.product, this.heroTag, this.quantity = 1}) : super(key: key);
// ProductProvider cartProd = new ProductProvider();
String heroTag;
Product product;
int quantity;
#override
_CartItemWidgetState createState() => _CartItemWidgetState();
}
class _CartItemWidgetState extends State<CartItemWidget> {
#override
Widget build(BuildContext context) {
return Consumer<ProductProvider>(
builder: (context, productProv, _) => InkWell(
child: Container(
.............
child: Row(
children: <Widget>[
.............
IconButton(
onPressed: () {
setState(() {
productProv.cartMinusOne(widget.product.id);
widget.quantity = this.decrementQuantity(widget.quantity);
});
}
.............
ProductProvider
class ProductProvider with ChangeNotifier {
ProductProvider() {
cartProducts();
}
List<Product> cartProdList;
cartMinusOne(String id) async {
//Code to minus item,then return as a string to save as local jason
var test = jsonEncode(cartList);
saveLocalJson(test, 'cart.json');
cartProducts();
notifyListeners();
}
Future<List<Product>> cartProducts() async {
String jsonString = await JsonProvider().getProductJson();
String cartString = await getCartJson();
var filterProdList = (json.decode(jsonString) as List).map((i) => Product.fromJson(i)).toList();
//code to get match cart list to product list
cartProdList = filterProdList.where((element) => element.cartqty > 0).toList();
notifyListeners();
return cartProdList;
}
........................