How to make a reusuable Futurebuilder - flutter

I have to build multiple future builders for different types of categories for a screen, like: Weekly deal, All, Newly Arrived, etc. My codes right now is pretty barebone, but here it is.
+Home Screen
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:vgo_customer_side/models/Product.dart';
import 'package:vgo_customer_side/repos/ProductRepo.dart';
import 'package:vgo_customer_side/widgets/Boxes.dart';
import 'package:vgo_customer_side/widgets/MyFunction.dart';
class GeneralScreen extends StatefulWidget {
const GeneralScreen({Key? key}) : super(key: key);
#override
_GeneralScreenState createState() => _GeneralScreenState();
}
class _GeneralScreenState extends State<GeneralScreen> with AutomaticKeepAliveClientMixin<GeneralScreen> {
List list = ["Weekly Deal", "Relevant", "Freshly"];
late Future<Product> futureProduct;
#override
void initState(){
futureProduct = readAllProducts();
super.initState();
}
String? rise;
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
constraints: BoxConstraints.tightForFinite(),
child: SingleChildScrollView(
child: Column(
children: [
deliverField(),
SizedBox(height: 29,),
FutureBuilder<Product>(
future: readAllProducts(),
builder: (context, AsyncSnapshot<Product> snapshot){
if(snapshot.hasData){
return RowBoxes(categoryName: "Weekly Deal", icon: Icon(Icons.arrow_forward, color: Colors.orange,));
}
return Text("waiting");
}),
SizedBox(height: 10,),
ElevatedButton(onPressed: (){
setState(() {
futureProduct = readAllProducts();
});
}, child: Text("press me")),
Center(child: Text("All")),
Center(child: Text("Just for you")),
_justForYou(),
],),
),
);
}
_justForYou(){
return Container();
}
_bottomGrid(){
return Container();
}
}
+RepoProduct
import 'package:vgo_customer_side/models/ApiRespone.dart';
import 'package:vgo_customer_side/models/Product.dart';
import 'package:http/http.dart' as http;
Future<Product> readAllProducts() async{
final response = await http.get(Uri.parse('https://vgo-buyer.herokuapp.com/api/v1/shopping/products/'));
if(response.statusCode == 200){
for(dynamic data in getAllProductsResponseFromJson(response.body).payload) {
return Product.fromJson(data);
}
throw Exception("Failed to load Products");
}
else{
throw Exception("Failed to load Products");
}
}
Now, having to add each "Weekly deal, All, Newly Arrived" to a futurebuilder is a lot of boilerplates, and I would like to simplify it into a reusable widget instead. Like I do with RowBoxes().
I know how to make normal reusable widgets but when it comes to a futurebuilder, it requires generic type(for the futurebuilder widget) and specific class for its "future:" function. Can anyone show me how to do it?

You can write a generic widget similar to this one:
class DealsWidget<T> extends StatelessWidget {
final Future<T> future;
final String category;
final IconData iconData;
final Color color;
final String loadingText;
DealsWidget({
this.future,
this.category,
this.iconData,
this.color,
this.loadingText,
});
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: future,
builder: (context, AsyncSnapshot<T> snapshot) {
if (snapshot.hasData) {
return RowBoxes(
categoryName: category,
icon: Icon(
iconData,
color: color,
),
);
}
return Text(loadingText);
},
);
}
}
And use it like:
DealsWidget<Product>(
future: readAllProducts(),
category: 'Weekly Deals',
iconData: Icons.arrow_forward,
color: Colors.orange,
loadingText: 'Please Wait...'
)

Related

Flutter async methods for widget initialize

Let's say I create a new screen team_screen which is the first parent of the tree.
Now for my team screen there are many widgets, some of theme have their own request, I want to show loader until every widget/request finished and ready.
I thought on 2 approaches.
All the requests are executed in team_screen with future builder and I pass the props to my widgets by demand.
Every widget with request get function that get executed in the async function in the initState function, then in my parent I make to every widget state parameter that is equal to true by the function I passed and when all is don't I stop the loader.
To sum up my problem is how to maintain a widget with many children and requests and showing one loader for entire page, making all the request on same widget? Pass isInitialize function to every widget?.
Which approach is better and if there are more approaches, I would like to hear.
Thank you for your help
Example for the second approach:
import 'package:flutter/material.dart';
import 'package:info_striker/locator.dart';
import 'package:info_striker/models/fixture/fixture.dart';
import 'package:info_striker/models/odds/bookmaker.dart';
import 'package:info_striker/models/synced-team/synced_team.dart';
import 'package:info_striker/services/fixture_service.dart';
import 'package:info_striker/utils/date_utilities.dart';
class TeamNextMatch extends StatefulWidget {
Function isInitialized;
SyncedTeam team;
TeamNextMatch({
Key? key,
required this.isInitialized,
required this.team,
}) : super(key: key);
#override
State<TeamNextMatch> createState() => _TeamNextMatchState();
}
class _TeamNextMatchState extends State<TeamNextMatch> {
Fixture? _fixture;
Bookmaker? _matchResult;
bool _isInitialized = false;
#override
void initState() {
super.initState();
init();
}
init() async {
final response = await locator<FixturesService>().getData(widget.team.id);
if (response != null) {
setState(() {
_fixture = Fixture.fromMap(response["fixture"]);
_matchResult = Bookmaker.fromMap(response["matchResultOdds"]);
});
}
widget.isInitialized(true);
}
#override
Widget build(BuildContext context) {
String? _date;
bool show = _fixture != null && _matchResult != null;
_fixture != null ? "${DateUtilities.getShortDateString(_fixture!.date)}, ${DateUtilities.getTimeString(_fixture!.date)}" : null;
return show
? Column(
children: [
Text(_fixture?.league?["name"]),
if (_date != null) Text(_date),
],
)
: const SizedBox();
}
}
You can show loader as described below -
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/data_model.dart';
import 'package:http/http.dart' as http;
class APiTest extends StatefulWidget {
const APiTest({Key? key}) : super(key: key);
#override
_APiTestState createState() => _APiTestState();
}
class _APiTestState extends State<APiTest> {
final String _url = "https://jsonplaceholder.typicode.com/todos/";
bool _isLoading = true;
final List<DataModel> _allData = [];
#override
void initState() {
super.initState();
_initData().then((value) {
setState(() {
_isLoading = false;
});
});
}
Future<void> _initData() async {
final response = await http.get(Uri.parse(_url));
final List res = jsonDecode(response.body);
res.forEach((element) {
_allData.add(DataModel.fromJson(element));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Loading Demo"),
),
body: Stack(
children: [
ListView.separated(
itemCount: _allData.length,
controller: ScrollController(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: ((context, index) {
return ListTile(
tileColor: Colors.grey[200],
title: Text(_allData[index].title!),
subtitle: Text(_allData[index].id.toString()),
);
}),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
)
],
),
);
}
}

Flutter Slidable Widget does not get removed when onDismissed

I am having trouble where a Slidable does not get removed when I pressed delete action as shown in an image below.
The problem is in class NotificationInputPage where onDismissed got triggered, but notificationList.removedAt(index); does not seems to work.
If you spot something isn't right, please let me know. Thank you so much.
return SlidableWidget(child: notification, onDismissed: (context) {
setState(() {
notificationList.removeAt(index);
print('deleted..');
});
});
Here is my code
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class SlidableWidget extends StatelessWidget {
final Widget child;
final void Function(BuildContext context) onDismissed;
const SlidableWidget({required this.child, required this.onDismissed , Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => Slidable(
child: child,
endActionPane: ActionPane(motion: ScrollMotion(), children: [
SlidableAction(
flex: 1,
onPressed: onDismissed,
backgroundColor: Colors.red,
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'delete',
),
],
extentRatio: 0.2,),
);
}
import 'package:carwashqueue/constants/constants.dart';
import 'package:carwashqueue/constants/enumerations.dart';
import 'package:carwashqueue/models/carwash_notification.dart';
import 'package:carwashqueue/widget/slidable_widget.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class NotificationInputPage extends StatefulWidget {
const NotificationInputPage({Key? key}) : super(key: key);
#override
_NotificationInputPageState createState() => _NotificationInputPageState();
}
class _NotificationInputPageState extends State<NotificationInputPage> {
DateTime thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30));
final _auth = FirebaseAuth.instance;
final _fireStore = FirebaseFirestore.instance;
#override
Widget build(BuildContext context) {
Stream<List<CarWashNotification>> readNotification() => _fireStore
.collection('notifications')
.orderBy('notification_date', descending: false)
.where('notification_date',
isGreaterThanOrEqualTo: thirtyDaysAgo)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => CarWashNotification.fromJson(doc.data(), doc)).toList());
return Scaffold(
body: StreamBuilder<List<CarWashNotification>>(
stream: readNotification(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('There is an error. Please try again.');
} else if (snapshot.hasData) {
final notifications = snapshot.data!;
List<Widget> notificationList = notifications.map((e) => buildListTile(e)).toList();
// return ListView(children: notificationList,);
return ListView.separated(
itemBuilder: (context, index) {
final notification = notificationList[index];
return SlidableWidget(child: notification, onDismissed: (context) {
setState(() {
notificationList.removeAt(index);
print('deleted..');
});
});
},
separatorBuilder: (context, index) => Divider(),
itemCount: notificationList.length);
} else {
return Center(
child: Text('There is no notification at the moment.'));
}
}),
);
}
Widget buildListTile(CarWashNotification item) {
return ListTile(
leading: FaIcon(
FontAwesomeIcons.bullhorn,
size: 30.0,
color: mainColour1,
),
title: Text(item.title),
subtitle: Text(item.message.length > textLimit
? item.message.substring(0, textLimit) + '...'
: item.message),
onTap: () {
print('onTap');
},
);
}
You should use dissmissible widget for this.
Dissmissible Widget
Dissmissible Widget Swiping
Now I understand why item didn't get removed from the list.
Actually it did get removed but when setState function runs, it reruns the build method so all the items are retrieved from firebase and reassigned to the list. That's why the item looks like it's never been removed from the list.
So What I will need to do is to really delete selected item from firebase or I will need to use a different mechanism.

RepaintBoundary with a StreamBuilder

I thought I understood RepaintBoundary but now I don't.
Background
I wrote this answer describing how you can add a RepaintBoundary around a widget that has to draw a lot to prevent other parts of the widget tree from redrawing. That worked as expected.
Problem now
I'm trying to make a real life example now where the widget is being rebuilt inside a StreamBuilder based on an audio player stream. I tried wrapping the whole StreamBuilder in a RepaintBoundary like this:
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
But the rest of the UI is still repainting (except the play/pause button which I also wrapped in a RepaintBoundary).
The build method of that ProgressBarWidget looks like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
}
But if I remove the StreamBuilder like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return ProgressBar(
progress: Duration.zero,
total: Duration(minutes: 5),
onSeek: (duration) {
_player.seek(duration);
},
);
}
Then the repaint boundary works again when I manually move the thumb.
What is it about the StreamBuilder that makes the RepaintBoundary not work?
Full code
The full code for the widget layout is here:
import 'package:flutter/material.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/rendering.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
void main() {
debugRepaintTextRainbowEnabled = true;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: HomeWidget(),
);
}
}
class HomeWidget extends StatefulWidget {
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
AudioPlayer _player;
final url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3';
Stream<DurationState> _durationState;
#override
void initState() {
super.initState();
_player = AudioPlayer();
_durationState = Rx.combineLatest2<Duration, PlaybackEvent, DurationState>(
_player.positionStream,
_player.playbackEventStream,
(position, playbackEvent) => DurationState(
progress: position,
buffered: playbackEvent.bufferedPosition,
total: playbackEvent.duration,
));
_init();
}
Future<void> _init() async {
try {
await _player.setUrl(url);
} catch (e) {
print("An error occured $e");
}
}
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
}
class ProgressBarWidget extends StatelessWidget {
const ProgressBarWidget({
Key key,
#required Stream<DurationState> durationState,
#required AudioPlayer player,
}) : _durationState = durationState,
_player = player,
super(key: key);
final Stream<DurationState> _durationState;
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
// ProgressBar(
// progress: Duration.zero,
// total: Duration(minutes: 5),
// onSeek: (duration) {
// _player.seek(duration);
// },
// );
}
}
class PlayPauseButton extends StatelessWidget {
const PlayPauseButton({
Key key,
#required AudioPlayer player,
}) : _player = player,
super(key: key);
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building play/pause button');
return StreamBuilder<PlayerState>(
stream: _player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: _player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: _player.pause,
);
} else {
return IconButton(
icon: Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => _player.seek(Duration.zero),
);
}
},
);
}
}
class DurationState {
const DurationState({this.progress, this.buffered, this.total});
final Duration progress;
final Duration buffered;
final Duration total;
}
The whole project is on GitHub.
When you don't have the StreamBuilder and drag in the ProgressBar, it will probably just repaint itself and not require a relayout.
When the StreamBuilder gets a new event from the stream, it rebuilds ProgressBar. Depending on the details of ProgressBar, when it gets rebuild it will also require a relayout (perhaps it contains a layout builder). Since it is in a Column and the Column uses the size of it children during layout (to determine the position of the next child), then Column has to do it layout again as well, which might cause its children to need a repaint.
Play around with this: You'll notice that marking Foo to repaint (horizontal drag) only causes Foo to repaint (when it is wrapped with a RepaintBoundary). Marking Foo for relayout (a tap) will also cause the Column to relayout and repaint. When the LayoutBuilder is present (which causes a relayout when it is rebuild), you'll see that a rebuild of Foo (by vertical drag) also causes the Column to repaint.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => Column(
children: [
Container(
height: 400,
color: Color(0x11ff0000),
),
RepaintBoundary(
child: Foo(),
),
],
);
}
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
#override
Widget build(BuildContext context) => GestureDetector(
onHorizontalDragUpdate: (_) => context.findRenderObject().markNeedsPaint(),
onTap: () => context.findRenderObject().markNeedsLayout(),
onVerticalDragUpdate: (_) => setState(() {}),
child: LayoutBuilder(
builder: (context, _) => Container(
height: 100,
width: 100.0,
color: Color(0xff002200),
),
),
);
}
This is a supplemental answer to tell how specifically I solved the problem after getting #spkersten's help.
The ProgressBar widget was rebuilding internally whenever the text labels would change. My first attempt at solving the problem was to wrap the widget in a SizedBox with a fixed height and width. This did work in that it prevented the rest of the screen from needing relayout or repainting. However, it was difficult to know what the height of the progress bar was going to be before laying it out.
So my second solution was to paint the text manually rather than use Text widgets. That way I could refrain from calling markNeedsLayout when the text changed. This solved the problem.
My current implementation of the progress bar is here.

Flutter Send Data To Other Screen Without Navigator

I have question how to pass data between pages/screen in flutter without navigator and only using onChanged and streambuilder.
All I want is whenever user write in textfield on first widget, the second widget automatically refresh with the new data from first widget.
Here's my code for first.dart
import 'package:flutter/material.dart';
import 'second.dart';
class First extends StatefulWidget {
First({Key key}) : super(key: key);
#override
_FirstState createState() => _FirstState();
}
class _FirstState extends State<First> {
final TextEditingController _myTextController =
new TextEditingController(text: "");
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: Text("Passing Data"),
),
body: Column(
children: <Widget>[
Container(
height: 120.0,
child: Column(
children: <Widget>[
TextField(
controller: _myTextController,
onChanged: (String value) {
// refresh second with new data
},
)
]
)
),
Container(
height: 120.0,
child: Second(
myText: _myTextController.text,
),
)
],
),
);
}
}
and here's my second.dart as second widget to receive data from first widget.
import 'dart:async';
import 'package:flutter/material.dart';
import 'api_services.dart';
class Second extends StatefulWidget {
Second({Key key, #required this.myText}) : super(key: key);
final String myText;
#override
_SecondState createState() => _SecondState();
}
class _SecondState extends State<Second> {
StreamController _dataController;
loadPosts() async {
ApiServices.getDetailData(widget.myText).then((res) async {
_dataController.add(res);
return res;
});
}
#override
void initState() {
_dataController = new StreamController();
loadPosts();
super.initState();
print(widget.myText);
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _dataController.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.hasData) {
return Container();
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Row(
children: <Widget>[
Text("Please Write A Text"),
],
);
} else if (snapshot.connectionState != ConnectionState.active) {
return CircularProgressIndicator();
}
if (!snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) {
return Text('No Data');
} else if(snapshot.hasData && snapshot.connectionState == ConnectionState.done) {
return Text(widget.myText);
}
return null;
});
}
}
You have a couple of options. The two simplest are - passing the text editing controller itself through to the second widget, then listening to it and calling setState to change the text in the second widget.
Example
class Second extends StatefulWidget {
Second({Key key, #required this.textController}) : super(key: key);
final TextEditingController textController;
#override
_SecondState createState() => _SecondState();
}
class _SecondState extends State<Second> {
// made this private
String _myText;
#override
void initState() {
_myText = widget.textController.text
widget.textController.addListener(() {
setState((){_myText = widget.textController.text});
);
});
super.initState();
}
...
// then in your build method, put this in place of return Text(widget.myText);
return Text(_myText);
Option 2 is listening to the controller in your first widget and call setState in there. This will rebuild both the first and second widget though, and I think is not as performant as the first option.
Hope that helps

How to show errors from ChangeNotifier using Provider in Flutter

I'm trying to find the best way to show errors from a Change Notifier Model with Provider through a Snackbar.
Is there any built-in way or any advice you could help me with?
I found this way that is working but I don't know if it's correct.
Suppose I have a simple Page where I want to display a list of objects and a Model where I retrieve those objects from api. In case of error I notify an error String and I would like to display that error with a SnackBar.
page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Page extends StatefulWidget {
Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State< Page > {
#override
void initState(){
super.initState();
Provider.of<Model>(context, listen: false).load();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
Provider.of< Model >(context, listen: false).addListener(_listenForErrors);
}
#override
Widget build(BuildContext context){
super.build(context);
return Scaffold(
appBar: AppBar(),
body: Consumer<Model>(
builder: (context, model, child){
if(model.elements != null){
...list
}
else return LoadingWidget();
}
)
)
);
}
void _listenForErrors(){
final error = Provider.of<Model>(context, listen: false).error;
if (error != null) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(error) )),
],
),
),
);
}
}
#override
void dispose() {
Provider.of<PushNotificationModel>(context, listen: false).removeListener(_listenForErrors);
super.dispose();
}
}
page_model.dart
import 'package:flutter/foundation.dart';
class BrickModel extends ChangeNotifier {
List<String> _elements;
List<String> get elements => _elements;
String _error;
String get error => _error;
Future<void> load() async {
try{
final elements = await someApiCall();
_elements = [..._elements, ...elements];
}
catch(e) {
_error = e.toString();
}
finally {
notifyListeners();
}
}
}
Thank you
Edit 2022
I ported (and reworked) this package also for river pod if anyone is interested
https://pub.dev/packages/riverpod_messages/versions/1.0.0
EDIT 2020-06-05
I developed a slightly better approach to afford this kink of situations.
It can be found at This repo on github so you can see the implementation there, or use this package putting in your pubspec.yaml
provider_utilities:
git:
url: https://github.com/quantosapplications/flutter_provider_utilities.git
So when you need to present messages to the view you can:
extend your ChangeNotifier with MessageNotifierMixin that gives your ChangeNotifier two properties, error and info, and two methods, notifyError() and notifyInfo().
Wrap your Scaffold with a MessageListener that will present a Snackbar when it gets called notifyError() or NotifyInfo()
I'll give you an example:
ChangeNotifier
import 'package:flutter/material.dart';
import 'package:provider_utilities/provider_utilities.dart';
class MyNotifier extends ChangeNotifier with MessageNotifierMixin {
List<String> _properties = [];
List<String> get properties => _properties;
Future<void> load() async {
try {
/// Do some network calls or something else
await Future.delayed(Duration(seconds: 1), (){
_properties = ["Item 1", "Item 2", "Item 3"];
notifyInfo('Successfully called load() method');
});
}
catch(e) {
notifyError('Error calling load() method');
}
}
}
View
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_utilities/provider_utilities.dart';
import 'notifier.dart';
class View extends StatefulWidget {
View({Key key}) : super(key: key);
#override
_ViewState createState() => _ViewState();
}
class _ViewState extends State<View> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: MessageListener<MyNotifier>(
child: Selector<MyNotifier, List<String>>(
selector: (ctx, model) => model.properties,
builder: (ctx, properties, child) => ListView.builder(
itemCount: properties.length,
itemBuilder: (ctx, index) => ListTile(
title: Text(properties[index])
),
),
)
)
);
}
}
OLD ANSWER
thank you.
Maybe I found a simpler way to handle this, using the powerful property "child" of Consumer.
With a custom stateless widget (I called it ErrorListener but it can be changed :))
class ErrorListener<T extends ErrorNotifierMixin> extends StatelessWidget {
final Widget child;
const ErrorListener({Key key, #required this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<T>(
builder: (context, model, child){
//here we listen for errors
if (model.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleError(context, model); });
}
// here we return child!
return child;
},
child: child
);
}
// this method will be called anytime an error occurs
// it shows a snackbar but it could do anything you want
void _handleError(BuildContext context, T model) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(model.error) )),
],
),
),
);
// this will clear the error on model because it has been handled
model.clearError();
}
}
This widget must be put under a scaffold if you want to use a snackbar.
I use a mixin here to be sure that model has a error property and a clarError() method.
mixin ErrorNotifierMixin on ChangeNotifier {
String _error;
String get error => _error;
void notifyError(dynamic error) {
_error = error.toString();
notifyListeners();
}
void clearError() {
_error = null;
}
}
So for example we can use this way
class _PageState extends State<Page> {
// ...
#override
Widget build(BuildContext context) =>
ChangeNotifierProvider(
builder: (context) => MyModel(),
child: Scaffold(
body: ErrorListener<MyModel>(
child: MyBody()
)
)
);
}
You can create a custom StatelessWidget to launch the snackbar when the view model changes. For example:
class SnackBarLauncher extends StatelessWidget {
final String error;
const SnackBarLauncher(
{Key key, #required this.error})
: super(key: key);
#override
Widget build(BuildContext context) {
if (error != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _displaySnackBar(context, error: error));
}
// Placeholder container widget
return Container();
}
void _displaySnackBar(BuildContext context, {#required String error}) {
final snackBar = SnackBar(content: Text(error));
Scaffold.of(context).hideCurrentSnackBar();
Scaffold.of(context).showSnackBar(snackBar);
}
}
We can only display the snackbar once all widgets are built, that's why we have the WidgetsBinding.instance.addPostFrameCallback() call above.
Now we can add SnackBarLauncher to our screen:
class SomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Title',
),
),
body: Stack(
children: [
// Other widgets here...
Consumer<EmailLoginScreenModel>(
builder: (context, model, child) =>
SnackBarLauncher(error: model.error),
),
],
),
);
}
}