How to show errors from ChangeNotifier using Provider in Flutter - 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),
),
],
),
);
}
}

Related

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

Flutter async methods for widget initialize

Let's say I create a new screen team_screen which is the first parent of the tree.
Now for my team screen there are many widgets, some of theme have their own request, I want to show loader until every widget/request finished and ready.
I thought on 2 approaches.
All the requests are executed in team_screen with future builder and I pass the props to my widgets by demand.
Every widget with request get function that get executed in the async function in the initState function, then in my parent I make to every widget state parameter that is equal to true by the function I passed and when all is don't I stop the loader.
To sum up my problem is how to maintain a widget with many children and requests and showing one loader for entire page, making all the request on same widget? Pass isInitialize function to every widget?.
Which approach is better and if there are more approaches, I would like to hear.
Thank you for your help
Example for the second approach:
import 'package:flutter/material.dart';
import 'package:info_striker/locator.dart';
import 'package:info_striker/models/fixture/fixture.dart';
import 'package:info_striker/models/odds/bookmaker.dart';
import 'package:info_striker/models/synced-team/synced_team.dart';
import 'package:info_striker/services/fixture_service.dart';
import 'package:info_striker/utils/date_utilities.dart';
class TeamNextMatch extends StatefulWidget {
Function isInitialized;
SyncedTeam team;
TeamNextMatch({
Key? key,
required this.isInitialized,
required this.team,
}) : super(key: key);
#override
State<TeamNextMatch> createState() => _TeamNextMatchState();
}
class _TeamNextMatchState extends State<TeamNextMatch> {
Fixture? _fixture;
Bookmaker? _matchResult;
bool _isInitialized = false;
#override
void initState() {
super.initState();
init();
}
init() async {
final response = await locator<FixturesService>().getData(widget.team.id);
if (response != null) {
setState(() {
_fixture = Fixture.fromMap(response["fixture"]);
_matchResult = Bookmaker.fromMap(response["matchResultOdds"]);
});
}
widget.isInitialized(true);
}
#override
Widget build(BuildContext context) {
String? _date;
bool show = _fixture != null && _matchResult != null;
_fixture != null ? "${DateUtilities.getShortDateString(_fixture!.date)}, ${DateUtilities.getTimeString(_fixture!.date)}" : null;
return show
? Column(
children: [
Text(_fixture?.league?["name"]),
if (_date != null) Text(_date),
],
)
: const SizedBox();
}
}
You can show loader as described below -
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/data_model.dart';
import 'package:http/http.dart' as http;
class APiTest extends StatefulWidget {
const APiTest({Key? key}) : super(key: key);
#override
_APiTestState createState() => _APiTestState();
}
class _APiTestState extends State<APiTest> {
final String _url = "https://jsonplaceholder.typicode.com/todos/";
bool _isLoading = true;
final List<DataModel> _allData = [];
#override
void initState() {
super.initState();
_initData().then((value) {
setState(() {
_isLoading = false;
});
});
}
Future<void> _initData() async {
final response = await http.get(Uri.parse(_url));
final List res = jsonDecode(response.body);
res.forEach((element) {
_allData.add(DataModel.fromJson(element));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Loading Demo"),
),
body: Stack(
children: [
ListView.separated(
itemCount: _allData.length,
controller: ScrollController(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: ((context, index) {
return ListTile(
tileColor: Colors.grey[200],
title: Text(_allData[index].title!),
subtitle: Text(_allData[index].id.toString()),
);
}),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
)
],
),
);
}
}

How to make a reusuable Futurebuilder

I have to build multiple future builders for different types of categories for a screen, like: Weekly deal, All, Newly Arrived, etc. My codes right now is pretty barebone, but here it is.
+Home Screen
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:vgo_customer_side/models/Product.dart';
import 'package:vgo_customer_side/repos/ProductRepo.dart';
import 'package:vgo_customer_side/widgets/Boxes.dart';
import 'package:vgo_customer_side/widgets/MyFunction.dart';
class GeneralScreen extends StatefulWidget {
const GeneralScreen({Key? key}) : super(key: key);
#override
_GeneralScreenState createState() => _GeneralScreenState();
}
class _GeneralScreenState extends State<GeneralScreen> with AutomaticKeepAliveClientMixin<GeneralScreen> {
List list = ["Weekly Deal", "Relevant", "Freshly"];
late Future<Product> futureProduct;
#override
void initState(){
futureProduct = readAllProducts();
super.initState();
}
String? rise;
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
constraints: BoxConstraints.tightForFinite(),
child: SingleChildScrollView(
child: Column(
children: [
deliverField(),
SizedBox(height: 29,),
FutureBuilder<Product>(
future: readAllProducts(),
builder: (context, AsyncSnapshot<Product> snapshot){
if(snapshot.hasData){
return RowBoxes(categoryName: "Weekly Deal", icon: Icon(Icons.arrow_forward, color: Colors.orange,));
}
return Text("waiting");
}),
SizedBox(height: 10,),
ElevatedButton(onPressed: (){
setState(() {
futureProduct = readAllProducts();
});
}, child: Text("press me")),
Center(child: Text("All")),
Center(child: Text("Just for you")),
_justForYou(),
],),
),
);
}
_justForYou(){
return Container();
}
_bottomGrid(){
return Container();
}
}
+RepoProduct
import 'package:vgo_customer_side/models/ApiRespone.dart';
import 'package:vgo_customer_side/models/Product.dart';
import 'package:http/http.dart' as http;
Future<Product> readAllProducts() async{
final response = await http.get(Uri.parse('https://vgo-buyer.herokuapp.com/api/v1/shopping/products/'));
if(response.statusCode == 200){
for(dynamic data in getAllProductsResponseFromJson(response.body).payload) {
return Product.fromJson(data);
}
throw Exception("Failed to load Products");
}
else{
throw Exception("Failed to load Products");
}
}
Now, having to add each "Weekly deal, All, Newly Arrived" to a futurebuilder is a lot of boilerplates, and I would like to simplify it into a reusable widget instead. Like I do with RowBoxes().
I know how to make normal reusable widgets but when it comes to a futurebuilder, it requires generic type(for the futurebuilder widget) and specific class for its "future:" function. Can anyone show me how to do it?
You can write a generic widget similar to this one:
class DealsWidget<T> extends StatelessWidget {
final Future<T> future;
final String category;
final IconData iconData;
final Color color;
final String loadingText;
DealsWidget({
this.future,
this.category,
this.iconData,
this.color,
this.loadingText,
});
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: future,
builder: (context, AsyncSnapshot<T> snapshot) {
if (snapshot.hasData) {
return RowBoxes(
categoryName: category,
icon: Icon(
iconData,
color: color,
),
);
}
return Text(loadingText);
},
);
}
}
And use it like:
DealsWidget<Product>(
future: readAllProducts(),
category: 'Weekly Deals',
iconData: Icons.arrow_forward,
color: Colors.orange,
loadingText: 'Please Wait...'
)

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

BlocBuilder() not being updated after BLoC yielding new state

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())