why data from firebase is not displayed? - flutter

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

Related

how to display the value that came from valueNotifier?

I have a valueNotifier that generates a list of events and takes a random string every 5 seconds and sends it to the screen. It lies in inheritedWidget. How can I display in the ListView the event that came with the valueNotifier? What is the correct way to print the answer?
My code:
class EventList extends StatelessWidget {
const EventList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return EventInherited(
child: EventListScreen(),
);
}
}
class EventListScreen extends StatefulWidget {
const EventListScreen({Key? key}) : super(key: key);
#override
State<EventListScreen> createState() => _EventListScreenState();
}
class _EventListScreenState extends State<EventListScreen> {
#override
Widget build(BuildContext context) {
final eventNotifier = EventInherited.of(context).eventNotifier;
return Scaffold(
appBar: AppBar(
title: const Text('Event List'),
centerTitle: true,
),
body: Container(
padding: const EdgeInsets.all(30),
child: ValueListenableBuilder(
valueListenable: eventNotifier,
builder: (BuildContext context, List<String> value, Widget? child) {
return ListView(
children: [
],
);
},
),
),
);
}
}
class EventNotifier extends ValueNotifier<List<String>> {
EventNotifier(List<String> value) : super(value);
final List<String> events = ['add', 'delete', 'edit'];
final stream = Stream.periodic(const Duration(seconds: 5));
late final streamSub = stream.listen((event) {
value.add(
events[Random().nextInt(4)],
);
});
}
class EventInherited extends InheritedWidget {
final EventNotifier eventNotifier = EventNotifier([]);
EventInherited({required Widget child}) : super(child: child);
static EventInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
#override
bool updateShouldNotify(EventInherited oldWidget) {
return oldWidget.eventNotifier.streamSub != eventNotifier.streamSub;
}
}
If you have correct value, you can return listview like this:
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return Text(value[index]);
},
);
After having a quick look at ValueNotifier,
It says the following:
When the value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
In your case, the value is an array. By adding items to the array, it wont recognise a change.
Also see other Stacko post.
Try something like:
value = [...value].add(...)

Flutter provider profile picture not updating

I am building a method that the user can select a prefered profile picture to show arround the app, using provider package. I used shared_preferences to save the profile picture preferences on locally as a int value. And it worked, means I can save the profile picture to local system. But the problem is, the provider package completely became useless in this case, because I have to convert the widget to statefull and call the setState method when ever I insert a profilePicture widget inside the widget tree. And even the profilePicture widget in the HomeScreen not updating this way. I want to know how can I use the provider package for this issue instead of using statefulWidgets.
watch the Gif or video
This is the Provider class I created:
class ProfilePicProvider with ChangeNotifier {
ProfilePicPref profilePicPreferences = ProfilePicPref();
int _svgNumber = 1;
int get svgNumber => _svgNumber;
set svgNumber(int value) {
_svgNumber = value;
profilePicPreferences.setProfilePic(value);
notifyListeners();
}
void changePic(int val) {
_svgNumber = val;
profilePicPreferences.setProfilePic(val);
notifyListeners();
}
}
This is the sharedPreferences class
class ProfilePicPref {
static const PRO_PIC_STS = 'PROFILESTATUS';
setProfilePic(int svgNo) async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
profilePref.setInt(PRO_PIC_STS, svgNo);
}
Future<int> getProfilePicture() async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
return profilePref.getInt(PRO_PIC_STS) ?? 1;
}
}
This is the image selection screen and save that data to sharedPreferences class
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
This is the HomeScreen which is not updating the profile image whether it is statefull or stateless
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final proPicProvider = Provider.of<ProfilePicProvider>(context);
return Scaffold(
body:
Column(
children: [
Row(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proPicProvider.svgNumber}.svg'),
],
),
],
),
);
}
}
example:
I have to convert the widget to statefull and call setState method to get the current profile picture from sharedPreferences. You may find this screen from the GIF I provided.
class Progress extends StatefulWidget {
const Progress({Key? key}) : super(key: key);
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SizedBox(
height: 130.0,
width: 130.0,
child: SvgPicture.asset(
'assets/svg/${proProvider.svgNumber}.svg'),
),
),
);
}
}
The problem is in _SelectProfilePictureState when you create new instance of your ChangeNotifier:
ProfilePicProvider proProvider = ProfilePicProvider();. It means you are not using the provider available across the context but creating new one every time. So when the value of your provider changed it has effect only inside _SelectProfilePictureState. Instead of creating new instance you must call it always using the context:
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
// [removed] ProfilePicProvider proProvider = ProfilePicProvider();
//removed
/*void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}*/
#override
Widget build(BuildContext context) {
//use provider from the context
final proProvider = Provider.of<ProfilePicProvider>(context,listen:true);
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
If you enter the application you may want send initially selected image to your provider:
Add parameter to the constructor of ProfilePicProvider:
ProfilePicProvider(SharedPreferences prefs): _svgNumber = prefs.getInt(ProfilePicPref.PRO_PIC_STS) ?? 1;
In main.dart:
Future<void> main()async{
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers:[
ChangeNotifierProvider( create:(_) => ProfilePicProvider(prefs))
],
child: yourtopWidget
)
);
}

Flutter FutureProvider Execute build twice

I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.

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),
),
],
),
);
}
}