Provider's watch() dosen't catch changing state in flutter - flutter

I am studying, provider in flutter. I try to make login process by using beamer and provider.
If user's auth state that is dectected by Provider context.watch<AuthenticationNotifier>().isAuthenticated; is false, BeamGuard force user to go auth screen.
final _routerDelegate = BeamerDelegate(
guards: [
BeamGuard(
pathPatterns: ['/'],
check: (context, location) {
return context.watch<AuthenticationNotifier>().isAuthenticated;
},
beamToNamed: (origin, target) => '/auth',
)
],
locationBuilder: BeamerLocationBuilder(
beamLocations: [PostListLocations(), AuthLocations()]),
);
User click login button in auth screen, auth state change true. I checked user'auth state is changed in AuthScreen.
void attemptVerify(BuildContext context) {
var authNotifier = context.read<AuthenticationNotifier>();
authNotifier.setUserAuth(true);
logger.d(authNotifier.userState);
}
}
but provider in BeamGuard is not watch state change. user do not go to main page, stay in auth page. if i set user's auth state True, user go to directly main page.So I think beamer is not problem. I think Provider doesn't work. I cannot find my mistake. could you help me?
this is full code.
main.dart
final _routerDelegate = BeamerDelegate(
guards: [
BeamGuard(
pathPatterns: ['/'],
check: (context, location) {
return context.watch<AuthenticationNotifier>().isAuthenticated;
},
beamToNamed: (origin, target) => '/auth',
)
],
locationBuilder: BeamerLocationBuilder(
beamLocations: [PostListLocations(), AuthLocations()]),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (BuildContext context, Widget? child) {
return ChangeNotifierProvider<AuthenticationNotifier>( //provider
create: (context) => AuthenticationNotifier(),
child: MaterialApp.router(
routeInformationParser: BeamerParser(),
routerDelegate: _routerDelegate,
),
);
},
);
}
}
auth_notifier.dart
import 'package:flutter/widgets.dart';
class AuthenticationNotifier extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void setUserAuth(bool authState) {
_isAuthenticated = authState;
notifyListeners();
}
}
auth_scree.dart
class AuthScreen extends StatefulWidget {
const AuthScreen({Key? key}) : super(key: key);
#override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
body: SingleChildScrollView(
child: ElevatedButton(
onPressed: () {
attemptVerify(context);
},
child: Text("button"),
),
)),
),
);
}
void attemptVerify(BuildContext context) {
var authNotifier = context.read<AuthenticationNotifier>();
authNotifier.setUserAuth(true);
}
}

Related

Flutter Bloc State Only Updates Once

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

Flutter: Error says - Could not find a generator for route RouteSettings while trying to navigate to another screen

Although questions with such error messages exist in this site, none solves my problem.
I have a button and on clicking the button, I just need to go to a different screen. But when ever I tap on the screen, the error shows up.
I first setup a route in MaterialApp and then tried to navigate to that route on tapping the button. The full code and the error message are given below:
Code:
import 'livesession1to1.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: CountDownTimer(),
navigatorKey: NavigationService.navigatorKey, // set property// Added by me later from prev project
// initialRoute: "/",
routes: <String, WidgetBuilder> {
'/liveSession1to1': (context) =>LiveSession1to1(),
},
)
);
}// end of main
class CountDownTimer extends StatefulWidget {
const CountDownTimer();
final String? title='';
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> {
#override
void initState() {
super.initState();
}// end of initstate
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Live Session'),
),
body: Text('Demo Text'),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_button(title: "Go", onPressed: () =>
Navigator.of(context ,rootNavigator: true).pushNamed('/liveSession1to1', arguments: {'room_found': 123 } )
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget _button({required String title, VoidCallback? onPressed}) {
return Expanded(
child: TextButton(
child: Text(
title,
style: const TextStyle(color: Colors.white),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: onPressed,
));
}
}
Error found:
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/liveSession1to1", {room_found: 123}) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
For the "/" route, the "home" property, if non-null, is used.
Otherwise, the "routes" table is used, if it has an entry for the route.
Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
So how to solve the problem ?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
void main() {
locatorSetup();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateRoute: generateRoute,
navigatorKey: _navService.navigatorKey,
// I don't know what your first screen is, so I'm assuming it's a Splash Screen
home: SplashScreen());
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.pushNamed(Routes.LiveSession1to1);
},
child: Text("Go to next page"),
),
));
}
}
class LiveSession1to1 extends StatefulWidget {
const LiveSession1to1({Key? key}) : super(key: key);
#override
State<LiveSession1to1> createState() => _LiveSession1to1State();
}
class _LiveSession1to1State extends State<LiveSession1to1> {
final _navService = locator<NavigationHandler>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
_navService.goBack();
},
child: Text("Go to previous page"),
),
));
}
}
GetIt locator = GetIt.instance;
void locatorSetup() {
locator
.registerLazySingleton<NavigationHandler>(() => NavigationHandlerImpl());
}
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.LiveSession1to1:
return _getPageRoute(view: LiveSession1to1(), routeName: settings.name);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
PageRoute _getPageRoute({String? routeName, Widget? view}) {
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => view!,
);
}
class Routes {
static const String LiveSession1to1 = "liveSession1to1";
}
abstract class NavigationHandler {
///Pushes `destinationRoute` route onto the stack
Future<dynamic>? pushNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` onto stack and removes stack items until
///`lastRoute` is hit
Future<dynamic>? pushNamedAndRemoveUntil(
String destinationRoute, String lastRoute,
{dynamic arg});
///Pushes `destinationRoute` onto stack with replacement
Future<dynamic>? pushReplacementNamed(String destinationRoute, {dynamic arg});
///Pushes `destinationRoute` after popping current route off stack
Future<dynamic>? popAndPushNamed(String destinationRoute, {dynamic arg});
///Pops current route off stack
void goBack();
///Pops routes on stack until `destinationRoute` is hit
void popUntil(String destinationRoute);
///Exits app
void exitApp();
late GlobalKey<NavigatorState> navigatorKey;
}
/// Handles navigation
class NavigationHandlerImpl implements NavigationHandler {
#override
late GlobalKey<NavigatorState> navigatorKey;
/// Constructs a NavigationHandler instance
NavigationHandlerImpl({GlobalKey<NavigatorState>? navigatorKey}) {
this.navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
}
NavigatorState? get state => navigatorKey.currentState;
#override
void exitApp() {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
#override
void goBack() {
if (state != null) {
return state!.pop();
}
}
#override
Future? popAndPushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.popAndPushNamed(destinationRoute, arguments: arg);
}
}
#override
void popUntil(String destinationRoute) {
if (state != null) {
return state!.popUntil(ModalRoute.withName(destinationRoute));
}
}
#override
Future? pushNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushNamed(destinationRoute, arguments: arg);
}
}
#override
Future? pushNamedAndRemoveUntil(String destinationRoute, String lastRoute,
{arg}) {
if (state != null) {
return state!.pushNamedAndRemoveUntil(
destinationRoute,
ModalRoute.withName(lastRoute),
arguments: arg,
);
}
}
#override
Future? pushReplacementNamed(String destinationRoute, {arg}) {
if (state != null) {
return state!.pushReplacementNamed(destinationRoute, arguments: arg);
}
}
}

BlocListener confusion

I am trying to make an app using flutter blocs, but I am having troubles with the BlocListener not being called and I can't figure out what I'm doing wrong.
Here is a minimalish code reproducing my issue:
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => AuthBloc(),
child: const AppView(),
);
}
}
/**************** APP VIEW **************/
class AppView extends StatefulWidget {
const AppView({Key? key}) : super(key: key);
#override
State<AppView> createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
final _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState!;
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
builder: (context, child) {
print('App builder');
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
print('Bloc listener');
switch (state.status) {
case AuthStatus.authenticated:
_navigator.pushAndRemoveUntil<void>(
MaterialPageRoute(
builder: (context) {
return Center(
child: Column(
children: [
const Text('Home'),
ElevatedButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthStatusChanged(
AuthStatus.unauthenticated));
},
child: const Text('Log out'),
),
],
),
);
},
),
(route) => false,
);
break;
default:
_navigator.pushAndRemoveUntil<void>(
MaterialPageRoute(
builder: (context) {
return Center(
child: Column(
children: [
const Text('Login'),
ElevatedButton(
onPressed: () {
context.read<AuthBloc>().add(
const AuthStatusChanged(
AuthStatus.authenticated));
},
child: const Text('Log in'),
),
],
),
);
},
),
(route) => false,
);
break;
}
},
child: child,
);
},
onGenerateRoute: (_) => MaterialPageRoute(
builder: (context) {
return const Center(
child: Text('splash'),
);
},
),
);
}
}
/**************** AUTH BLOC CLASSES **************/
/**************** AUTH State **************/
enum AuthStatus { unknown, unauthenticated, authenticated }
class AuthState extends Equatable {
final AuthStatus status;
const AuthState._({
this.status = AuthStatus.unknown,
});
const AuthState.unknown() : this._();
const AuthState.authenticated() : this._(status: AuthStatus.authenticated);
const AuthState.unauthenticated()
: this._(status: AuthStatus.unauthenticated);
#override
List<Object?> get props => [status];
}
/**************** AUTH Event **************/
abstract class AuthEvent extends Equatable {
const AuthEvent();
#override
List<Object> get props => [];
}
class AuthStatusChanged extends AuthEvent {
final AuthStatus status;
const AuthStatusChanged(this.status);
#override
List<Object> get props => [status];
}
/**************** AUTH BLOC **************/
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(const AuthState.unknown()) {
print('Bloc constructor');
on<AuthStatusChanged>(_onAuthStatusChanged);
}
_onAuthStatusChanged(
AuthStatusChanged event,
Emitter<AuthState> emit,
) async {
switch (event.status) {
case AuthStatus.unauthenticated:
return emit(const AuthState.unauthenticated());
case AuthStatus.authenticated:
return emit(const AuthState.authenticated());
default:
return emit(const AuthState.unknown());
}
}
}
When I launch the app I would expect the BlocListener to be called once but instead it sits on the splash page.
I used this tutorial to produce this code : https://bloclibrary.dev/#/flutterlogintutorial
Edit:
Thank you all for your insight, I didn't understand that the BlocListener won't fire an event on the initialState (RTFM I guess xD). Looking back at the tutorial I used, this is dealt with by the "Repository" that feeds a stream delayed on creation and the Bloc is listening for that stream to fire a change of state events. Reusing the same concept works for me!
BlocListener only trigger when state has changed. On application load you may want to trigger a bloc event to change the AuthBloc state.
This could be achieved by adding a bloc event in the initState() function and placing a breakpoint to see if the listener is being triggered.
https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocListener-class.html

How to pause and resume StreamProvider in Flutter

I am experimenting on forex live update using StreamProvider.
The demo will auto-update the exchange rate by fetching latest data from external API periodically. (every 60 seconds in this example)
Below is the diagram of implementation.
Diagram
API call (Future event) --> Put data in stream
^ |
| V
Wait for 60 seconds <-- StreamProvider listens for
new event and rebuild widget
Problem
The stream continues even when navigating to main view.
If we use StreamBuilder, we may be able to call listen() method, which
will return StreamSubscription. Then, either cancel(), pause(), or resume() method can be called on demand.
I wonder if there is similar method to pause and resume while using StreamProvider?
Expected
pause when leaving dashboard view and resume when return to dashboard view.
Codes
Model
class Currency {
String? base;
String? quote;
double? rate;
// constructor, factory constructor, etc.
// ...
}
Controller
class CurrencyService {
Currency? _currency;
Stream<Currency?> get currencyStream async* {
yield* Stream.periodic(Duration(seconds: 60), (_) {
return getCurrencyData();
}).asyncMap((event) async => await event);
}
Future<Currency?> getCurrencyData() async {
try {
// Perform API call and
// update Currency object
// ...
} catch (e) {
print('Error: $e');
}
return _currency;
}
}
View
void main() async {
runApp(
MultiProvider(
providers: [
// some providers,
// another one,
// ...
StreamProvider<Currency?>(
create: (_) => CurrencyService().currencyStream,
initialData: await CurrencyService().getCurrencyData(),
),
],
child: TestApp(),
),
);
}
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Live Update Demo',
initialRoute: '/',
routes: routes,
);
}
}
Main view (page 1)
class MainView extends StatefulWidget {
const MainView({Key? key}) : super(key: key);
#override
_MainViewState createState() => _MainViewState();
}
class _MainViewState extends State<MainView> {
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
// ...
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/dashboard');
},
child: Text('Dashboard')),
],
),
);
}
}
Dashboard view (page 2)
class DashboardView extends StatelessWidget {
const DashboardView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: Consumer<Currency?>(
builder: (context, currency, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: Text('${currency?.base ?? ''}${currency?.quote ?? ''}'),
),
Container(
child: Text('${currency?.rate ?? ''}'),
),
],
),
);
},
));
}
}
Thank you.
Pausing and resuming StreamProvider on Stream.periodic don't seem possible. Instead, the implementation can still be achieved using Timer.periodic and StreamController, as suggested by #Abion47
We can simulate the pause and resume by controlling when to start and stop adding new data to stream. One of the ways is to start the Timer.periodic when navigating to dashboard view (after a button is pressed) and cancel the timer when returning to main view (dashboard view is popped up).
ElevatedButton(
onPressed: () {
// start timer
// ...
Navigator.pushNamed(...).then((_) {
// stop timer
// this section is triggered when returning from dashboard to main view
});
}
Revised codes
// Controller
class CurrencyService {
Currency? _currency;
Timer? _pollingTimer;
StreamController<Currency?> _currencyController = StreamController.broadcast();
Future<void> addCurrencyData() async {
await getCurrencyData()
.then((currency) => _currencyController.add(currency));
}
void closeStream() {
_currencyController.close();
}
void startPolling() {
addCurrencyData();
_pollingTimer = Timer.periodic(Duration(seconds: 60), (_) => addCurrencyData());
}
void stopPolling() {
_pollingTimer?.cancel();
}
Stream<Currency?> get currencyStream => _currencyController.stream;
Future<Currency?> getCurrencyData() async {
try {
// Perform API call and
// update Currency object
// ...
} catch (e) {
print('Error: $e');
}
return _currency;
}
}
// Main
void main() async {
runApp(
MultiProvider(
providers: [
// some providers,
// another one,
// ...
Provider(create: (_) => CurrencyService()),
],
child: TestApp(),
),
);
}
class TestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Live Update Demo',
initialRoute: '/',
routes: routes,
);
}
}
// Main view (page 1)
class MainView extends StatelessWidget {
const MainView({Key? key}) : super(key: key);
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
// ...
ElevatedButton(
onPressed: () {
Provider.of<CurrencyService>(context, listen: false)
.startPolling();
Navigator.pushNamed(
context,
'/dashboard',
).then((_) => Provider.of<CurrencyService>(context, listen: false).stopPolling());
},
child: Text('Dashboard')),
],
),
);
}
}
// Dashboard view (page 2)
class DashboardView extends StatelessWidget {
const DashboardView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final currencyService = Provider.of<CurrencyService>(context);
return Scaffold(
body: StreamProvider<Currency?>.value(
initialData: null,
value: currencyService.currencyStream,
child: CurrencyRate(),
),
);
}
}
class CurrencyRate extends StatelessWidget {
const CurrencyRate({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final currency = context.watch<Currency?>();
return Center(
child: currency == null
? CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: Text('${currency?.base ?? ''}${currency?.quote ?? ''}'),
),
Container(
child: Text('${currency?.rate ?? ''}'),
),
],
),
);
}
}

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