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
Related
First, 'main.dart' calls 'homepage.dart'.
And 'homepage.dart' calls several pages in the body(the code'_pages[_index]') using bottomnavigationbar and index.
Initially, index is 0 and 'Ppage1' appears by default.
In this 'Ppage1', I brought the collection 'exhibition' from firestore.
And then called the '_buildBody'(if data is not arrived,LinearProgressIndicator will be displayed) ,
and in there i made the list 'exhibitions' using 'Exhibition'. (Previously, I had made a data model called 'Exhibition' in 'model_exhibitions.dart'.)
And in the '_buildBody', I brought the class 'BoxSlider' that is using the data from firebase.
I think the data is arrived wel since the LinearProgressIndicator is not displayed.
But only the UI using the data from firestore is not displayed.
What is problem? i can't find it
homepage.dart
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _index = 0;
final _pages = [
Ppage1(),
const Page2(),
const Page3(),
const Page4(),
const Page5(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( ...
Ppage1.dart
class Ppage1 extends StatefulWidget {
#override
State<Ppage1> createState() => _Ppage1State();
}
class _Ppage1State extends State<Ppage1> {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
late Stream<QuerySnapshot> streamData;
#override
void initState() {
super.initState();
streamData = firebaseFirestore.collection('exhibition').snapshots();
}
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildBody(context, snapshot.data!.docs);
},
);
}
Widget _buildBody(BuildContext context, List<DocumentSnapshot> snapshot) {
List<Exhibition> exhibitions = snapshot.map((d) => Exhibition.fromSnapshot(d)).toList();
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
BoxSlider(exhibitions: exhibitions), ...
box_slider.dart
class BoxSlider extends StatefulWidget {
late final List<Exhibition> exhibitions;
BoxSlider({required this.exhibitions});
#override
State<BoxSlider> createState() => _BoxSliderState();
}
class _BoxSliderState extends State<BoxSlider> {
late List<Exhibition> exhibitions;
late List<Widget> posters;
...
#override
void initState() {
super.initState();
exhibitions = widget.exhibitions;
posters = exhibitions.map((m) => Image.asset(m.poster)).toList();
...
}
#override
Widget build(BuildContext context) {
return Container(
height: 440,
child: ListView(
scrollDirection: Axis.horizontal,
children: makeBoxImages(context, widget.exhibitions),
),
);
}
}
List<Widget> makeBoxImages(BuildContext context, List<Exhibition> exhibitions) {
List<Widget> results = [];
for (var i = 0; i < exhibitions.length; i++) {
results.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () {Navigator.push(
context,
MaterialPageRoute(builder: (context)=>DetailScreen(exhibition: exhibitions[i])),
);
},
child: SizedBox(
height: 350,
child: Image.network(exhibitions[i].poster),
), ...
You should change the _fetchdata method to know if the data is fetched correctly:
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
//To know if the snapshot contains errors.
if (snapshot.hasError) {
return Center(
child: Text('Some error occured: ${snapshot.error.toString()}'),
);
}
//To know if the snapshot contains valid data.
if (snapshot.hasData) {
return _buildBody(context, snapshot.data!.docs);
}
//Returns a progress indicator in case that the two conditions above are not satisfied.
return LinearProgressIndicator();
},
);
}
I have the dropdownbutton widget below, with a futurebuilder that reads a list from a google spreadsheet. The widget works good as intended.
I want to reuse this widget within the app, and simply pass a different Future
As example, I want to call the same widget, but pass different lists
mydropdownbutton ( futureList1 ),
mydropdownbutton ( futureList2 ),
mydropdownbutton ( futureList3 ),
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidget extends StatefulWidget {
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
// --------------------------------------------------------
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
#override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(future: futureList(),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
String defaultValue = dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
//=============================================
//FUTURE LIST FOR THE DROPDOWN MENU
//=============================================
Future<List> futureList() async {
var items = await ScheduleInfo.mx_schedule_WEEKDAYS_as_List(debugONOFF: 1);
return items;}
//=============================================
How can I make this widget modular and reusable?
you can send the future to this widget's constructor. first you declare a variable and set it to constructor. then you can reference that variable in the state of that class by widget.variable keyword. something like this
class dropDownButtonWidget extends StatefulWidget {
final variable;
dropDownButtonWidget(this.variable);
const dropDownButtonWidget({ Key? key,}) : super(key: key);
#override
State<dropDownButtonWidget> createState() => _dropDownButtonWidgetState();
}
class _dropDownButtonWidgetState extends State<dropDownButtonWidget> {
widget.variable //whatever you want to do with it
}
Community... after few hours of reading and testing, I found the solution to my own question. I am posting the solution for anyone else needing it.
Probably, my code can be improved, I welcome suggestions.
My question above has a working code for a dropdown button widget (fully working as today), using a Future
Below, my own answer, the same widget transformed into a reusable modular widget.
( This works only with future lists (async) )
Simple SCREEN WIDGET (with nested dropdownbutton widgets):
class Screen01 extends StatefulWidget {
const Screen01({
Key? key,
}) : super(key: key);
#override
State<Screen01> createState() => _Screen01State();
}
class _Screen01State extends State<Screen01> {
#override
Widget build(BuildContext context) {
return Center(child:
Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children:
[ // each time you call the widget, you can provide a different future list
dropDownButtonWidgetModular(futureList: dropdownFutureList()),
SizedBox(width: 50,),
dropDownButtonWidgetModular(futureList: anotherFutureList())
]
),
);
}
}
DROPDOWNBUTTON WIDGET:
//========================================================
// WIDGET: FUTURE DROPDOWN MENU + FUTURE LIST
//--------------------------------------------------------
class dropDownButtonWidgetModular extends StatefulWidget {
final Future<List> futureList; // ===> ADDED FUTURE LIST
const dropDownButtonWidgetModular({ Key? key, required this.futureList}) : super(key: key); // ===> ADDED REQUIRED FUTURE LIST
#override
State<dropDownButtonWidgetModular> createState() => _dropDownButtonWidgetModularState(chosenFutureList: futureList);
}
// --------------------------------------------------------
class _dropDownButtonWidgetModularState extends State<dropDownButtonWidgetModular> {
Future<List> chosenFutureList; // ===> ADDED FUTURE LIST
String? defaultValue;
_dropDownButtonWidgetModularState({required this.chosenFutureList}); // ===> ADDED REQUIRED FUTURE LIST
#override
Widget build(BuildContext context) {
return Center(child:
FutureBuilder(future: chosenFutureList, // ===> ADDED FUTURE LIST
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if(snapshot.hasData){ List? futureDataList = snapshot.data;
futureDataList ??= ['Loading']; //if snapshot is null
return buildDropdownButton(dropdownList: futureDataList );
}else if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}else {
return Center(child: CircularProgressIndicator());
}
}
)
);
}
//----------------------------------
// DROPDOWNBUTTON EXTRACTED METHOD
DropdownButton<Object> buildDropdownButton({required List dropdownList}) {
defaultValue ??= dropdownList.first; //DEFAULT SELECTED ITEM
return DropdownButton(
value: defaultValue,
onChanged: (value) => setState(() => defaultValue = value.toString()),
items: dropdownList.map((items) {
return DropdownMenuItem(value: items, child: Text(items));
}).toList(),
);
}
//----------------------------------
}
//=============================================
I commented some of the changes made from my original question
Flutter
Dart
I am a beginner in flutter and i am trying to add controller to streamBuilderWidget so i can dispose it but i have no idea where should i put the controller.. i tried this
the stream below as a widget not function
StreamController<QuerySnapshot> controller;
void dispose() {
super.dispose();
controller.close();
}
void initState() {
super.initState();
controller = StreamController<QuerySnapshot>();
}
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection("users").doc(widget.documentUid).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(
child: circulearProgress(),
);
}
in this code it never disposed or closed the stream:(
Anyone who edits my code in the right way will be very grateful to him , thanks friends
StreamController is like a pipeline. In your case, that pipeline went from water supplier to your house, there is no need to worried about what goes in there.
But if you want to set up a pipeline from your washing machine to the draining hole, that is where you need to use StreamController.
Example:
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final NumberController controller = NumberController();
#override
void dispose() {
controller.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
InputWidget(controller: controller,),
OutputWidget(controller: controller,)
],
),
),
),
);
}
}
class NumberController {
//This is the pipeline of "number"
final StreamController<int> controller = StreamController<int>.broadcast();
//This is where your "number" go in
Sink<int> get inputNumber => controller.sink;
//This is where your "number" go out
Stream<int> get outputNumber => controller.stream;
//Dispose
void dispose() {
controller.close();
}
}
class InputWidget extends StatelessWidget {
final NumberController controller;
const InputWidget({Key key, this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
controller.inputNumber.add(Random().nextInt(10));
},
child: Text(
'Random Number'
),);
}
}
class OutputWidget extends StatelessWidget {
final NumberController controller;
const OutputWidget({Key key, this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: controller.outputNumber,
builder: (context, snapshot) {
return Text(snapshot.hasData ? snapshot.data.toString() : 'No data');
},
);
}
}
You don't have to use StreamController. StreamBuilder you are using closes the stream internally.
From your comments, you seem to want to close the listener in the method below:
void handleDelete() {
FirebaseFirestore.instance.collection("handleCountM").doc(currentUser.uid + widget.documentUid).collection("handleCountM2").limit(1).snapshots()
.listen((value) {
value.docs.forEach((element) {
element.reference.delete();
});
});
}
You can do that by getting a reference to the stream subscription and calling .cancel on the subscription.
Calling .listen on a stream returns a stream subscription object like this:
StreamSubscription handleDeleteStreamSubscription = FirebaseFirestore.instance.collection("handleCountM").doc(currentUser.uid + widget.documentUid).collection("handleCountM2").limit(1).snapshots()
.listen((value) {
value.docs.forEach((element) {
element.reference.delete();
});
});
Cancelling the subscription is done like this:
handleDeleteStreamSubscription.cancel();
I am new to the BLoC pattern on flutter and i'm trying to rebuild a messy flutter app using it. Currently, I intend to get a list of user's apps and display them with a ListView.builder(). The problem is that whenever the state of my AppsBloc changes, my StatelessWidget doesn't update to show the new state. I have tried:
Using MultiBlocProvider() from the main.dart instead of nesting this appsBloc inside a themeBloc that contains the whole app
Returning a list instead of a Map, even if my aux method returns a correct map
Using a StatefulWidget, using the BlocProvider() only on the ListView...
I have been reading about this problem on similar projects and the problem might be with the Equatable. However, I haven't been able to identify any error on that since I'm also new using Equatable. I have been debugging the project on VScode with a breakpoint on the yield* line, and it seems to be okay. In spite of that the widget doesn't get rebuilt: it keeps displaying the textcorresponding to the InitialState.
Moreover, the BLoC doesn't print anything on console even though all the states have an overwritten toString()
These are my 3 BLoC files:
apps_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:device_apps/device_apps.dart';
import 'package:equatable/equatable.dart';
part 'apps_event.dart';
part 'apps_state.dart';
class AppsBloc extends Bloc<AppsEvent, AppsState> {
#override
AppsState get initialState => AppsInitial();
#override
Stream<AppsState> mapEventToState(AppsEvent event) async* {
yield AppsLoadInProgress();
if (event is AppsLoadRequest) {
yield* _mapAppsLoadSuccessToState();
}
}
Stream<AppsState> _mapAppsLoadSuccessToState() async* {
try {
final allApps = await DeviceApps.getInstalledApplications(
onlyAppsWithLaunchIntent: true, includeSystemApps: true);
final listaApps = allApps
..sort((a, b) =>
a.appName.toLowerCase().compareTo(b.appName.toLowerCase()));
final Map<Application, bool> res =
Map.fromIterable(listaApps, value: (e) => false);
yield AppsLoadSuccess(res);
} catch (_) {
yield AppsLoadFailure();
}
}
}
apps_event.dart
part of 'apps_bloc.dart';
abstract class AppsEvent extends Equatable {
const AppsEvent();
#override
List<Object> get props => [];
}
class AppsLoadRequest extends AppsEvent {}
apps_state.dart
part of 'apps_bloc.dart';
abstract class AppsState extends Equatable {
const AppsState();
#override
List<Object> get props => [];
}
class AppsInitial extends AppsState {
#override
String toString() => "State: AppInitial";
}
class AppsLoadInProgress extends AppsState {
#override
String toString() => "State: AppLoadInProgress";
}
class AppsLoadSuccess extends AppsState {
final Map<Application, bool> allApps;
const AppsLoadSuccess(this.allApps);
#override
List<Object> get props => [allApps];
#override
String toString() => "State: AppLoadSuccess, ${allApps.length} entries";
}
class AppsLoadFailure extends AppsState {
#override
String toString() => "State: AppLoadFailure";
}
main_screen.dart
class MainScreen extends StatelessWidget {
const MainScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return TabBarView(
children: <Widget>[
HomeScreen(),
BlocProvider(
create: (BuildContext context) => AppsBloc(),
child: AppsScreen(),
)
,
],
);
}
}
apps_screen.dart
class AppsScreen extends StatelessWidget {
const AppsScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.fromLTRB(30, 5, 10, 0),
child: Column(children: <Widget>[
Row(
children: <Widget>[
Text("Apps"),
],
),
Row(children: <Widget>[
Container(
width: MediaQuery.of(context).size.width - 50,
height: MediaQuery.of(context).size.height - 150,
child: BlocBuilder<AppsBloc, AppsState>(
builder: (BuildContext context, AppsState state) {
if (state is AppsLoadSuccess)
return Text("LOADED");
else if (state is AppsInitial)
return GestureDetector(
onTap: () => AppsBloc().add(AppsLoadRequest()),
child: Text("INITIAL"));
else if (state is AppsLoadInProgress)
return Text("LOADING...");
else if (state is AppsLoadFailure)
return Text("LOADING FAILED");
},
),
),
])
])),
);
}
}
In GestureDetector.onTap() you create a new AppsBloc(), this is wrong. So, you need:
apps_screen.dart:
AppsBloc _appsBloc;
#override
void initState() {
super.initState();
_appsBloc = BlocProvider.of<AppsBloc>(context);
}
//...
#override
Widget build(BuildContext context) {
//...
return GestureDetector(
onTap: () => _appsBloc.add(AppsLoadRequest()),
child: Text("INITIAL")
);
//...
}
Or you can do the same even without the _appsBloc field:
BlocProvider.of<AppsBloc>(context).add(AppsLoadRequest())
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),
),
],
),
);
}
}