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.
Related
I have CounterProvider mixin with ChangeNotifier and inside the class i have two counters (_counterOne and _counterTwo) when _counterOne is inremented all Consumers are executed hence Widgets that consume _counterOne and _counterTwo are executed. But I want only the _counterOne consumer widget to execute.
I couldn't found any salution to do that.
Thanks in advance.
Provider:
class CounterProvider with ChangeNotifier {
int _counterOne = 1;
int getCounterOne() => _counterOne;
void incrementCounterOne() {
_counterOne++;
notifyListeners();
}
int _counterTwo = 2;
int getCounterTwo() => _counterTwo;
void incrementCounterTwo() {
_counterTwo++;
notifyListeners();
}
}
View:
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
#override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
late CounterProvider _provider;
#override
void initState() {
_provider = Provider.of<CounterProvider>(context, listen: false);
super.initState();
}
#override
Widget build(BuildContext context) => Scaffold(
body: Column(
children: [
Consumer<CounterProvider>(builder: (context, value, child) {
print("Consumer of CounterOne executed");
return Text("CounterOne: ${value.getCounterOne()}");
}),
Consumer<CounterProvider>(builder: (context, value, child) {
print("Consumer of CounterTwo executed");
return Text("CounterTwo: ${value.getCounterTwo()}");
}),
// Buttons
ElevatedButton(
onPressed: () {
_provider.incrementCounterOne();
},
child: const Text("Increment CounterOne"),
),
ElevatedButton(
onPressed: () {
_provider.incrementCounterTwo();
},
child: const Text("Increment CounterTwo"),
),
],
));
}
according to how Provider works if you use Consumer on a Provider class it will listen to any changes on that class and update every time there is change, To solve this you can use Selector as explained in here.
But ValueListenableBuilder also works fine as suggested in the comments.
I use a Provider with ChangeNotifier, but inside it I need to make a StreamController that will process the data. For example, let there be a list of sites
class ExampleProvider extends ChangeNotifier {
//list of sites for data processing
final List<String> _weblinks = [];
ExampleProvider() {
//Fill in demo data
_weblinks.add("https://stackoverflow.com/");
_weblinks.add("https://github.com");
_weblinks.add("http://microsoft.com");
//subscribe to the stream in the constructor
eventStream.listen((event) async {
if (event is StartProcessing) {
for (var i = 0; i < _weblinks.length; i++) {
//Doing some fake calculations
await Future.delayed(Duration(seconds: 3));
//add the result to the stream
dataSink.add(_weblinks[i]);
}
}
if (event is FinishProcessing) {}
});
}
final _streamController = StreamController<String>.broadcast();
Stream<String> get dataStream => _streamController.stream;
StreamSink<String> get dataSink => _streamController.sink;
final _eventController = StreamController<StreamControllerAction>.broadcast();
Stream<StreamControllerAction> get eventStream => _eventController.stream;
StreamSink<StreamControllerAction> get eventSink => _eventController.sink;
}
The problem is that the list of sites can be very large and if a data processing thread has started, then I can’t interrupt or cancel it, I cannot stop the execution of the loop, and until the list of sites ends, the thread will be executed. I also cannot use _streamController.close() because the stream is inside the provider, which is and the stream will be disabled forever, and I would like to reuse it. is there a way to interrupt the execution of a stream if the data is evaluated in a loop?
Full code
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ExampleProvider(),
),
],
child: MaterialApp(
home: MyHomePage(),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
StreamBuilder<String>(
initialData: null,
stream: Provider.of<ExampleProvider>(context, listen: false)
.dataStream,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
List<Widget> children;
if (snapshot.hasError) {
children = <Widget>[Text("error")];
} else {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
children = <Widget>[
ElevatedButton(
child: Text("processing data"),
onPressed: () {
Provider.of<ExampleProvider>(context, listen: false)
.eventSink
.add(StartProcessing());
},
),
];
break;
default:
{
children = <Widget>[
Column(
children: [
ElevatedButton(
child: Text("finish"),
onPressed: () {},
),
Text(snapshot.data),
],
)
];
}
break;
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
)
],
),
),
);
}
}
class ExampleProvider extends ChangeNotifier {
final List<String> _weblinks = [];
ExampleProvider() {
_weblinks.add("https://stackoverflow.com/");
_weblinks.add("https://github.com");
_weblinks.add("http://microsoft.com");
eventStream.listen((event) async {
if (event is StartProcessing) {
for (var i = 0; i < _weblinks.length; i++) {
await Future.delayed(Duration(seconds: 3));
dataSink.add(_weblinks[i]);
}
}
if (event is FinishProcessing) {}
});
}
final _streamController = StreamController<String>.broadcast();
Stream<String> get dataStream => _streamController.stream;
StreamSink<String> get dataSink => _streamController.sink;
final _eventController = StreamController<StreamControllerAction>.broadcast();
Stream<StreamControllerAction> get eventStream => _eventController.stream;
StreamSink<StreamControllerAction> get eventSink => _eventController.sink;
}
abstract class StreamControllerAction {}
class StartProcessing extends StreamControllerAction {}
class FinishProcessing extends StreamControllerAction {}
If you want to stop listening to eventStream you can save its subscription in a StreamSubscription object and then you will have access to .pause() and .resume().
When you use stream.listen() it returns a StreamSubscription you can save in a variable and use later.
You can read more about it in the documentation https://api.dart.dev/stable/2.12.4/dart-async/StreamSubscription-class.html
I have a Stream Provider (connected to firebase) that is not working. I am guessing that the problem lies in the fact that I am using a named navigator [Navigator.pushNamed(context, '/route',)]. I guess this makes the 'route' widget to not be the son of the widget that calls it. Let me show it better below.
My app structure is as follows:
My main widget which handles routing and receives the Stream with user authentication (there is no problem here):
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: AuthService().user,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Wrapper(),
routes: {
'/home': (context) => Wrapper(),
'/edit_profile': (context) => UserProfile() //This is where I am having trouble.
}
),
);
}
}
The Wrapper that validates if the user is authenticated and acts accordingly:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
// return either the Home or Authenticate widget
if (user == null){
return Authenticate();
} else {
return HomeWrapper();
}
}
}
The HomeWrapper which receives the second stream and redirects to the widget I am having trouble with:
class HomeWrapper extends StatefulWidget {
#override
_HomeWrapperState createState() => _HomeWrapperState();
}
class _HomeWrapperState extends State<HomeWrapper> {
String currentBodyName = 'home';
Widget currentBodyWidget = Home();
#override
Widget build(BuildContext context) {
Widget _drawerOptions = Row(
children: [
FlatButton(child: someChild, onPressed: () {Navigator.pushNamed(context, '/edit_profile',);},), //This is the actual call to the navigator.
],
);
return StreamProvider<Map>.value( //This is the problematic Stream!
value: DatabaseService().userDetail,
child: Scaffold(
//Body
body: currentBodyWidget,
//I am simplifying this to show the most important parts
bottomNavigationBar: myBottomNavigationBar(
buttons: <Widget>[
FlatButton(
icon: someIcon,
onPressed: () => _onItemTapped('home'),
),
FlatButton(
icon: otherIcon,
onPressed: () => _onItemTapped('second_screen'),
),
],)
//Drawer
drawer: Drawer(child: _drawerOptions,), //This one has the call to the problematic edit_profile route.
);
}
void _onItemTapped(String newBodyName) {
if (newBodyName != currentBodyName){
setState(() {
currentBodyName = newBodyName;
switch(newBodyName) {
case 'home': {
currentBodyWidget = Home();
}
break;
case 'second_screen': {
currentBodyWidget = SecondScreen();
}
break;
default: {
currentBodyWidget = Home();
}
break;
}
});
}
}
}
Finally the edit_profile route calls the UserProfile Widget which looks like this:
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
//This is where the error occurs!!
final userDocument = Provider.of<Map>(context) ?? [];
print(userDocument);
return Scaffold(body: Container());
}
}
This is the error that it throws:
The following ProviderNotFoundError was thrown building UserProfile(dirty, state: _UserProfileState#09125):
Error: Could not find the correct Provider<Map<dynamic, dynamic>> above this UserProfile Widget
Thank you very much!!
Turns out my approach was wrong.
Instead of wrapping the HomeWrapper with the StreamProvider, hoping that it would pass the data to the next route (UserProfile ), what I did was to wrap the UserProfile widget with a StreamProvider, as follows:
(Note: I changed the Map StreamProvider for a UserData StreamProvider.)
class UserProfile extends StatefulWidget {
#override
_UserProfileState createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
return Scaffold(
body: Container(
//My Widget here
);
} else
return Loading();
});
}
}
This series was very helpful: https://www.youtube.com/playlist?list=PL4cUxeGkcC9j--TKIdkb3ISfRbJeJYQwC
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),
),
],
),
);
}
}