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())
Related
The problem is that I would like to show a loading indicator when the user tries to fetch some data from an api. But when the user presses the button, loading indicator shows once. But I would like to show the loading indicator every time when the user tries to fetch. It works but as I say It works once. Could anyone have any idea what can cause this problem? Here's the minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeCubit()),
],
child: const MaterialApp(
title: 'Flutter Bloc Demo',
home: HomeView(),
),
);
}
}
class HomeView extends BaseView<HomeCubit, HomeState> {
const HomeView({Key? key}) : super(key: key);
#override
Widget builder(HomeCubit cubit, HomeState state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.count.toString()),
ElevatedButton(
onPressed: cubit.increment,
child: const Text('Increase'),
),
],
),
);
}
}
class HomeState extends BaseState {
final int count;
HomeState({required this.count});
HomeState copyWith({
int? count,
}) {
return HomeState(
count: count ?? this.count,
);
}
}
class HomeCubit extends BaseCubit<HomeState> {
HomeCubit() : super(HomeState(count: 0));
void increment() {
flow(() async {
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
});
}
}
#immutable
abstract class BaseView<C extends StateStreamable<S>, S extends BaseState>
extends StatelessWidget {
const BaseView({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return BaseCubit(context.read<S>());
},
child: Scaffold(
body: BlocBuilder<C, S>(
builder: (context, state) {
final cubit = context.read<C>();
if (state.loadingState == LoadingState.loading) {
return loadingWidget;
}
return builder.call(cubit, state);
},
),
),
);
}
Widget builder(C cubit, S state);
Widget get loadingWidget => const Center(
child: CircularProgressIndicator(),
);
}
enum LoadingState { initial, loading, loaded }
class BaseState {
LoadingState loadingState;
BaseState({
this.loadingState = LoadingState.initial,
});
}
class BaseCubit<S extends BaseState> extends Cubit<S> {
BaseCubit(S state) : super(state);
Future<void> flow(Future<void> Function() function) async {
state.loadingState = LoadingState.loading;
emit(state);
await function();
state.loadingState = LoadingState.loaded;
emit(state);
}
}
Is it overengineering? I don't think you are duplicating much code if you just use BlocBuilder instead of some base class.
If bloc already exist you should provide it by BlocProvider.value instead of BlocProvider(create: read())
You should use context.watch instead of context.read to get a new value every time the state changes. context.read receives state only once.
It's overengineering, please take a look at https://bloclibrary.dev/#/coreconcepts. There are enough tutorials to catch the basic idea.
Then try to use bloc + freezed. Here is an example https://dev.to/ptrbrynt/why-bloc-freezed-is-a-match-made-in-heaven-29ai
I have a Stateless-Provider widget along with its ChangeNotifier-model. Inside the Provider, there is a Stateful widget. When notifyListeners is called, all widgets in the stateless widget get updated, except the Stateful one. What am I missing here, and how do I go about it? Providing a simplified example here: Upon pressing the button, the expected result is First: The value is 1st, but the actual output is First: The value is 2nd
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Model extends ChangeNotifier {
final List<ListElement> elements;
Model({required this.elements});
void add() {
elements.insert(0, ListElement(name: "First", value: "1st"));
notifyListeners();
}
}
class ListElement {
final String name;
var value;
ListElement({required this.name, required this.value});
}
class ValueWidget extends StatefulWidget {
final String value;
ValueWidget({required this.value});
#override
State<StatefulWidget> createState() => _ValueWidget(value: value);
}
class _ValueWidget extends State<ValueWidget> {
String value;
_ValueWidget({required this.value});
#override
Widget build(BuildContext context) {
return Text("The value is ${value}.");
}
}
class StatelessPage extends StatelessWidget {
final model = Model(elements: [
ListElement(name: "Second", value: "2nd"),
ListElement(name: "Third", value: "3rd")]);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (context) => model,
child: ConsumerWidget())
);
}
}
class ConsumerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Model>(builder: (context, model, _) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.fromLTRB(10, 30, 10, 10000),
child: Column(
children: [Column(
children: model.elements.map((element) {
return Row(children: [
Text("${element.name}: "),
ValueWidget(value: element.value)
]);
}).toList(),
),
TextButton(onPressed: model.add,
child: Text("Add element to beginning")),
],
),
),
);
});
}
}
Please consider that this is simplified version of my production code, and changing the whole Provider class to a Stateful one would be difficult.
Edit: Thanks Aimen for showing the path. What finally worked was using only the index of the list elements in the Stateful wiget (ValueWidget). And fetch the data from the model. I think the reason for this is that if the Stateful-widget in independece is not affected, it will not rebuild. We need to affect the build part of the widget. Pasting the changed working code.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Model extends ChangeNotifier {
final List<ListElement> elements;
Model({required this.elements});
void add() {
elements.insert(0, ListElement(name: "First", value: "1st"));
notifyListeners();
}
}
class ListElement {
final String name;
var value;
ListElement({required this.name, required this.value});
}
class ValueWidget extends StatefulWidget {
final int ind;
final Model model;
ValueWidget({required this.ind, required this.model});
#override
State<StatefulWidget> createState() => _ValueWidget(
ind: ind, model: model);
}
class _ValueWidget extends State<ValueWidget> {
final int ind;
final Model model;
_ValueWidget({required this.ind, required this.model});
#override
Widget build(BuildContext context) {
// Can also use Provider like this so that it does not need to be passed
// final model = Provider.of<Model>(context, listen: true);
// This is the part because of which widget is getting rebuilt
final element = model.elements[ind];
return Text("The value is ${element.value}.");
}
}
class StatelessPage extends StatelessWidget {
final model = Model(
elements: [
ListElement(name: "Second", value: "2nd"),
ListElement(name: "Third", value: "3rd")]
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider(
create: (context) => model,
child: ConsumerWidget())
);
}
}
class ConsumerWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Model>(builder: (context, model, _) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.fromLTRB(10, 30, 10, 10000),
child: Column(
children: [Column(
children:
model.elements.asMap().entries.map((ele) {
return Row(children: [
Text("${ele.value.name}: "),
ValueWidget(ind: ele.key, model: model),
]);
}).toList(),
),
TextButton(onPressed: model.add,
child: Text("Add element to beginning")),
],
),
),
);
});
}
}
you are not implementing provider in the stateful widget you are just passing a value through a parameter you need to call a provider and set the listen to true
inside the statful widget
like
var model = Model.of(context, listen = true);
List elements = model.elements;
here the elements variable will change when the elements in the provider will have a new value
I'm trying to navigate to another page using blocs / cubits. I have one cubit that successfully navigates to another page upon completion of a method, but for some reason, it doesn't work on another cubit, despite successful state change, and operation done on the method.
class WalletCreateDialog extends StatefulWidget {
const WalletCreateDialog({required this.mnemonic});
final String mnemonic;
#override
_WalletCreateDialogState createState() => _WalletCreateDialogState();
}
class _WalletCreateDialogState extends State<WalletCreateDialog> {
#override
void initState() {
BlocProvider.of<WalletCreateCubit>(context)
.addCreatedWalletToWalletList(widget.mnemonic);
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocListener<WalletCreateCubit, WalletCreateState>(
listener: (context, state) {
if (state is WalletAdded) {
Navigator.of(context).pop();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Text(
'Wallet added! Navigating back to home screen...',
),
),
);
Navigator.of(context).pushNamedAndRemoveUntil(
WalletOverviewHomeScreen.routeName,
(route) => false,
);
}
},
child: AlertDialog(
content: Container(
height: MediaQuery.of(context).size.height * 0.08,
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Adding wallet..."),
const LoadingIndicator(),
],
),
),
],
),
),
),
);
}
}
In the line of code above, it successfully navigates to WalletOverviewHomeScreen upon successful completion of the addCreatedWalletToWalletList method.
class WalletDeleteDialog extends StatefulWidget {
const WalletDeleteDialog({required this.walletAddress});
final String walletAddress;
#override
State<WalletDeleteDialog> createState() => _WalletDeleteDialogState();
}
class _WalletDeleteDialogState extends State<WalletDeleteDialog> {
#override
void initState() {
BlocProvider.of<WalletDeleteCubit>(context)
.deleteWallet(widget.walletAddress);
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocListener<WalletDeleteCubit, WalletDeleteState>(
listener: (context, state) {
if (state is WalletDeleteFinished) {
Navigator.of(context).pop();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Text(
'Wallet deleted! Navigating back to home screen...',
),
),
);
Navigator.of(context).pushNamedAndRemoveUntil(
WalletOverviewHomeScreen.routeName,
(route) => false,
);
}
},
child: AlertDialog(
content: Container(
height: MediaQuery.of(context).size.height * 0.08,
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Deleting wallet..."),
const LoadingIndicator(),
],
),
),
],
),
),
),
);
}
}
On the other hand, in the line of code above, it doesn't navigate to the same screen after completion of the method. I've already verified that the state has changed in both cubits. Additionally, hot restarting the app would actually show that what was supposed to get deleted, did actually get deleted, thus there's no issue with regards to the implementation of the deleteWallet method itself.
How can I navigate to the WalletOverviewHomeScreen after completion of the deleteWallet method?
For context, below are the state classes for the Cubits.
part of 'wallet_create_cubit.dart';
abstract class WalletCreateState extends Equatable {
const WalletCreateState();
#override
List<Object> get props => [];
}
class WalletCreateInitial extends WalletCreateState {
const WalletCreateInitial();
#override
List<Object> get props => [];
}
class WalletCreateLoading extends WalletCreateState {
const WalletCreateLoading();
#override
List<Object> get props => [];
}
class WalletCreated extends WalletCreateState {
final String mnemonic;
const WalletCreated({required this.mnemonic});
#override
List<Object> get props => [mnemonic];
}
class WalletAdding extends WalletCreateState {
const WalletAdding();
#override
List<Object> get props => [];
}
class WalletAdded extends WalletCreateState {
const WalletAdded();
#override
List<Object> get props => [];
}
part of 'wallet_delete_cubit.dart';
abstract class WalletDeleteState extends Equatable {
const WalletDeleteState();
#override
List<Object> get props => [];
}
class WalletDeleteInitial extends WalletDeleteState {
const WalletDeleteInitial();
#override
List<Object> get props => [];
}
class WalletDeleteOngoing extends WalletDeleteState {
const WalletDeleteOngoing();
#override
List<Object> get props => [];
}
class WalletDeleteFinished extends WalletDeleteState {
const WalletDeleteFinished();
#override
List<Object> get props => [];
}
remove Navigator.of(context).pop();
because you don't need it. when you use Navigator.of(context).pushNamedAndRemoveUntil
Looks like the fix was to directly copy the contents of deleteFromWallet to the deleteWallet function. That is, in the WalletDeleteCubit it went from this:
Future<void> deleteWallet(String address) async {
FlutterSecureStorage storage = FlutterSecureStorage();
emit(WalletDeleteOngoing());
deleteFromWallet(storage, address);
debugPrint("Wallet with address: $address is deleted");
emit(WalletDeleteFinished());
debugPrint('Emit WalletDeleteFinished');
}
To this:
void deleteWallet(String address) async {
FlutterSecureStorage storage = FlutterSecureStorage();
emit(WalletDeleteOngoing());
await storage.delete(
key: WalletOverviewHomeScreen.walletKey + address,
);
debugPrint("Wallet with address: $address is deleted");
emit(WalletDeleteFinished());
debugPrint('Emit WalletDeleteFinished');
}
I develop an app using BLoC pattern.
In my app there are 2 routes, route A and B, and both of them access same data.
A problem caused when moving the routes as below.
Move to route B from route A that shows the data.
Update the data at route B.
Back to route A.
After moving back to route A, the StreamBuilder of showing the data never updates automatically.
How can I let the StreamBuilder update on resumed state?
Here are sample codes.
routeA.dart
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).pushNamed("routeB");
},
),
],
);
}
}
routeB.dart
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.update.add(null);
},
),
);
}
}
bloc.dart
class Bloc {
Stream<String> data;
Sink<void> update;
Model _model;
Bloc() {
_model = Model();
final update = PublishSubject<void>();
this.update = update;
final data = BehaviorSubject<String>(seedValue: "");
this.data = data;
update.map((event) => _model.update()).listen((event) => data.sink.add(_model.getData()));
}
}
model.dart
class Model {
static Model _model;
factory Model() { // model is singleton.
_model ??= Model._();
return _model;
}
Model._();
int _data = 0;
void update() {
_data++;
}
String getData() {
return _data.toString();
}
}
StreamBuilder updates the data whenever it gets changed not when just by calling stream
RouteA
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (ctx) {
return RouteB();
}));
},
),
],
);
}
}
Route B
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.updateData();
},
),
);
}
}
Bloc
class Bloc {
final _update = PublishSubject<String>();
Model _model = Model();
Stream<String> get data => _update.stream;
void updateData() async {
_model.update();
_update.sink.add(_model.getData());
_update.stream.listen((event) {
print(event);
});
}
dispose() {
_update.close();
}
}
final bloc = Bloc();
just follow above changes, it will do the trick for you.
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),
),
],
),
);
}
}