Flutter: UI reactions with Provider - flutter

On some event, I want to navigate to another screen with Navigator.
I could easily achieve it with BlocListener:
BlocListener<BlocA, BlocAState>(
bloc: blocA,
listener: (context, state) {
if (state is Success) {
Navigator.of(context).pushNamed('/details');
}
},
child: Container(),
)
But I can't find the direct equivalent for it in a pure Provider.
The only way I see is to swap screens:
home: Consumer<Auth>(
builder: (_, auth, __) => auth.user == null ? LoginPage() : MainPage()
)
It's a common way. But it will not use Navigator, hence it will just 'pop' MainPage without screen transition.
On some event, I want to play some animation in UI.
I found in the documentation that Listenable class is intended for dealing with Animations, but it's not explained in details.
On some event, I want to clear a TextEditingController.
On some event, I want to show a dialog.
And more similar tasks...
How to solve it? Thanks in advance!

After some research I found a way. I'm not sure if it's the only or the best way, or the way foreseen by Provider's creator, however it works.
The idea is to keep a helper Stream inside of my Store class (I mean business-logic class provided with Provider), and to subscribe to its changes in my widget.
So in my Store class I have:
final _eventStream = StreamController.broadcast();
Stream get eventStream => _eventStream.stream;
void dispose() {
_eventStream.close();
super.dispose();
}
I add events to this stream inside of actions:
void navigateToNextScreen() {
_eventStream.sink.add('nav');
}
void openDialog() {
_eventStream.sink.add('dialog');
}
In my UI widget I have:
#override
void afterFirstLayout(BuildContext context) {
context.read<Transactions>().eventStream.listen((event) {
if (event == 'nav') {
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => SecondScreen(),
),
);
} else if (event == 'dialog') {
showDialog(
context: context,
builder: (context) => AlertDialog(content: Text("Meow")));
}
});
}
I used here afterFirstLayout lifecycle method from the after_layout package, which is just a wrapper for WidgetsBinding.instance.addPostFrameCallback
07.07.20 UPD.: Just found a package that can be used for event reactions:
https://pub.dev/packages/event_bus
It basically uses the same approach with StreamController under the hood.

Related

Please comment on 3 flutter_bloc writing styles: BlocBuilder, BlocListener, BlocConsumer

I am practicing with flick_bloc and I wonder when to use BlocBuilder, when to use BlocListener and when to use BlocConsumer. I asked a few people, they said that BlocBuilder is used the most and I also started and just practiced with it, but it seems that Blocbuilder only changed for the first time, I don't know if it's true. Can you guys give me some comments on these spellings
Bloc Builder
Used for building widgets, For Example: If you want to show a list of employee names on a page, you can return a ListView widget based on the bloc state. Also, if the employee list comes from an API, then you will need different states such as Loading, Success and Failure states. Based on these different states you can return different widgets from BlocBuilder. A CircularProgressIndicator for showing loading state, ListView for showing employee list in the success state and Error text widget for showing error message if the API fails.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
Bloc Listener
BlocBuilder can only return widgets. If you want to show a snackbar or want to Navigate from one page to another, then you need to use BlocListener for that.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
}
)
Bloc Consumer
If you have the use of both BlocListener and BlocBuilder, then it is better to use BlocConsumer. It reduces the boilerplate of using BlocListener and BlocBuilder together.
Code Without Bloc Consumer:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
child: BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
),
)
Code using Bloc Consumer:
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
BlocBuilder: You can use it to just build out your widgets, but the draw back is that you can't build in Snackbars or Dialogs into the flow, because you must return a widget in blocbuilder and you don't want to return a snackbar or dialog.
BlocListener: This would permit you to use your dialogs and snackbars, but the issue is that it can't let you do anything a blocbuilder would let you do. Which is as you might have guessed, is to return a widget, it's more suited for dismissible UI components like the dialogs and snackbars.
BlocConsumer: This widget helps you combine both a BlocListener and a BlocBuilder, so you can return static components and dismissible UI components.
So if you won't need Snackbars or Dialogs, use a BlocBuilder, If you need Snackbars or Dialogs, use a BlocListener. If you want both of them to work in synergy use a BlocConsumer.
BlocBuilder
This is used when we want to draw a Widget based on what is the current State. In the following example a new “text” gets drawn every time the state changes.
Sample Example
BlocBuilder<OrdersBloc, OrdersState>(
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Completed!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else if (OrdersState.OrderRequested) {
return Container(child: Text('A customer placed an order!'));
} else {
return Container(child: Text('Waiting for an order'));
}
},
);
BlocListener
This is just a listener not a builder (like the above), that means that its job is keep listening for new changes in the state and not to return a widget. You can use listener when you want to show any dialog or any toast, or navigation from one page to another(these are few examples).
Sample Example
BlocListener<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted;
},
listener: (context, state) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
},
child: Container(child: Text('Always draw this text!')),
);
BlocConsumer
This is used when we want to draw something based on the current state and execute some actions depending on the new arriving states. This is a mix between “BlocListener” and “BlocBuilder”.
Sample Example
BlocConsumer<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderRefunded;
},
listener: (context, state) {
if (state is OrdersState.OrdersCompleted) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
} else if (state is OrdersState.OrderRefunded) {
// Report to analytics
Analytics.reportRefunded(state.orderId);
}
},
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderInProgress ||
state is OrdersState.OrderRequested;
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Served!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else {
return Container(child: Text('No State'));
}
},
);

Why is Bloc skipping this emit command?

I have an bloc that receives an event called OpenTourStop that invokes a function whose first line of code invokes emit(Waiting()); and then proceeds to execute some logic before emitting a different state. In the UI, the BlocConsumer is supposed to print out a message to the console as soon as state equals Waiting, but it NEVER does. The bloc does not emit the Waiting state, but does emit the other states that result from completing the function. What am I doing wrong?
Below are the relevant sections of code for the bloc and UI
Bloc code:
class QuiztourManagerBloc
extends Bloc<QuiztourManagerEvent, QuiztourManagerState> {
final QuiztourRemoteData _repo;
QuiztourManagerBloc({QuiztourRemoteData repo})
: _repo = repo,
super(QuiztourManagerInitial()) {
on<OpenTourStop>(_openTourStop);
}
_openTourStop(event, emit) {
emit(Waiting()); // Why doesn't the Waiting state show up in the UI?
final _tourStopIndex = event.tourStopIndex;
// section of code removed for clarity
if (_quizPlayDoc.seenRules && tourStopGameResults.isEmpty) {
emit(ShowQuizQuestionViewManager(
quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
// emit(ShowQuizQuestions(quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
} else if (tourStopGameResults.length > 0) {
emit(ShowQuizTourStopScreen(
tour: event.tour,
tourStopIndex: event.tourStopIndex,
quizPlayDoc: _quizPlayDoc,
maxTourStopPoints: _maxTourStopPoints.toString(),
pointsEarned: _tourStopScore.toString(),
));
} else {
emit(ShowQuizRules(_quizPlayDoc));
}
}
}
UI code (from class QuizTourStopViewManager) :
#override
Widget build(BuildContext context) {
return BlocConsumer<QuiztourManagerBloc, QuiztourManagerState>(
builder: (context, state) {
if (state is Waiting) {
print('!!!!!!!!!!!!!!!!!!!!! Waiting '); // Why does this line never get executed?
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
else if (state is ShowQuizTourStopScreen) {
return QuizTourStop( );
}
},
listener: (_, state) {},
);
}
The UI that triggers the event is a button. The code associated with that button is below:
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (tourType == "quizTour") {
BlocProvider.of<QuiztourManagerBloc>(context)
.add(OpenTourStop(
tour: tour,
tourStopIndex: selectedTourStopIndex,
));
return QuizTourStopViewManager(
tour: tour,
// game: widget.game,
selectedTourStopIndex: selectedTourStopIndex,
);
I guess that when you send 'OpenTourStop' event at 'onTap' method, 'QuizTourStopViewManager' page is not builded.
So would you try to change event call position after 'OpenTourStop' page is builded?
At initState() method inside.
or
At 'bloc' parameter in BlocConsumer method.

Flutter: How to pass data between screens?

How can I change the visibility of a button on screen "X" from a button on screen "Y".
One popular approach (using the provider architecture) would be something like this:
Define a provider that handles all the logic and holds your data:
class MyProvider extends ChangeNotifier {
bool showMyButton = false;
MyProvider() {}
void showButton() {
showMyButton = true;
// This line notifies all consumers
notifyListeners();
}
void refresh() {
notifyListeners();
}
}
To access the provider everywhere you need to register it:
void main() => runApp(
// You can wrap multiple providers like this
MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (_) => MyProvider()),
],
child: const MyApp(),
),
);
On the button that you want to control you can use a Consumer to listen to the providers values:
Consumer<MyProvider>(builder: (_, model, __) {
return Visibility(
visible: model.showMyButton,
child: MaterialButton(...),
);
})
Now in your second screen you can access that provider with:
Provider.of<MyProvider>(context, listen: false)
.showButton();
However you might have to call notifyListener one more time when returning from screen Y to screen X:
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenY()));
Provider.of<MyProvider>(context, listen: false).refresh();
Keep in mind that there is a lot more to provider so please have a look at their official docs.
Also be aware of the fact that there are easier ways to just pass data between screens but you will often arrive at a point where you will need a better way of managing state and provider provides just that ;)
You can pass the data via push and pop of navigation. Or else use ChangeNotifier class to notify the state of button easily.

Flutter/Dart - Photo changed but not the URL, therefore Provider's Consumer is not Refreshed

I'm using Provider with ChangeNotifier to alert a Consumer once a new photo is uploaded to a server replacing an old photo. The problem is that the URL stays the same as the photo is merely overwritten and keeps the same name. Hence the Consumer doesn't recognize that anything has changed and doesn't refresh the old photo with the new one.
How can I trick the ChangeNotifier into refreshing the URL? Heres' the Consumer in my build;
Consumer<SocialProvider>(
builder: (context, socialProvider, child) {
return Image.network(socialProvider.currentavatar,
);
}),
Here's where the image is chosen in the Gallery and uploaded to overwrite the old image on the server.
GestureDetector(
onTap: () async {
await socialProvider.loadCurrentUserId();
await _listener.openGallery(socialProvider.currentuserid);
String updatedavatar = "http://example.com/same_photo.jpg";
socialProvider.updateAvatar(updatedavatar);
},
And here's the code in the Provider with ChangeNotifier;
Future<void> updateAvatar(String avatar) async {
var box = await Hive.openBox('currentuser');
box.put('currentavatar', avatar);
currentavatar = avatar;
notifyListeners();
}
Any ideas how to trick Consumer into believing the url has changed so that it is refreshed?
I believe the Consumer will rebuild when someone call notifyListeners(). Your issue may be in Image.network(socialProvider.currentavatar,) that flutter reuse same render object when everything is not change. You can try to add key:UniqueLey() to force rebuild the widget every time it build.
Update with some assumption.
Here is the simple code I try to rebuild your environment:
class SimpleProvider with ChangeNotifier {
Future<void> reload() async => notifyListeners();
}
class TestConsumer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => SimpleProvider(),
child: Consumer<SimpleProvider>(
builder: (_, simpleProvider, child) {
print('Consumer build');
return child; // Here is your Image?
},
child: Builder(
builder: (context) {
print('Builder build');
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<SimpleProvider>().reload();
},
),
body: Container(),
);
},
),
),
),
);
}
}
Every time I click the button, SimpleProvider will call the notifyListeners()
(Future is not necessary) and the builder function in Consumer will be called. Here come the questions:
Is the builder really called when you use notifyListeners?
I assume the builder function is called. You can still double check this part. Otherwise you must provide how you call it.
If builder is called, why the Image Widget inside it is not rebuilt?
It is part of flutter design that it reuse as much as it could, for better performance. Sometimes it confuse people that the rebuild is not happen. That's where I guess your problem is here. You can check the basic mechanism from video here:
https://www.youtube.com/watch?v=996ZgFRENMs&ab_channel=Flutter

Recommendation when using bloc pattern in flutter

When using flutter bloc what is the recommendation, is it recomended for each page to have its own bloc or can i reuse one block for multiple pages, if so how?
I think that the best solution is to have one BLoC per page. It helps you to always know in which state each screen is just by looking at its BLoC. If you want to show the state of each of your tabs independently you should create one BLoC for each tab, and create one Repository which will handle fetching the data. But if the state of every tab will be the same, (for example you fetch data only once for all of the screens, so you don't show loading screen on every tab) then I think that you could create just one BLoC for all of this tabs.
It is also worth to add, that BLoCs can communicate with each other. So you can get state of one BLoC from another, or listen to its state changes. That could be helpful when you decide to create separate BLoCs for tabs.
I have addressed this topic in my latest article. You can check it out if you want to dive deeper.
There are no hard-set rules about this. It depends on what you want to accomplish.
An example: if each page is "radically" from each other, then yes, a BLoC per page makes sense. You can still share an "application-wide" BLoC between those pages if some kind of sharing or interaction is required between the pages.
In general, I've noticed that usually a BLoC "per page" is useful as there are always specific things related for each page that you handle within their BLoC. You can the use a general BLoC to share data or some other common thing between them.
You can combine the BLoC pattern with RxDart to handle somewhat more complex interaction scenarios between a BLoC and the UI.
Sharing a BLoC is fairly simple, just nest them or use a MultiProvider (from the provider package):
runApp(
BlocProvider(
builder: (_) => SettingsBloc(),
child: BlocProvider(
builder: (_) => ApplicationBloc(),
child: MyApp()
)
)
);
and then you can just retrieve them via the Provider:
class MyApp extends ... {
#override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
final appBloc = Provider.of<ApplicationBloc>(context);
// do something with the above BLoCs
}
}
You can share different bloc's in different pages using BlocProvider.
Let's define some RootWidget that will be responsible for holding all Bloc's.
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
NavigationBloc _navigationBloc;
ProfileBloc _profileBloc;
ThingBloc _thingBloc;
#override
void initState(){
_navigationBloc = NavigationBloc();
_thingBloc = ThingBloc();
_profileBloc = ProfileBloc();
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(
builder: (BuildContext context) => _navigationBloc
),
BlocProvider<ProfileBloc>(
builder: (BuildContext context) => _profileBloc
),
BlocProvider<ThingBloc>(
builder: (BuildContext context) => _thingBloc
),
],
child: BlocBuilder(
bloc: _navigationBloc,
builder: (context, state){
if (state is DrawProfilePage){
return ProfilePage();
} else if (state is DrawThingsPage){
return ThingsPage();
} else {
return null
}
}
)
)
}
}
And after that, we can use any of bloc from parent and all widgets will share the same state and can dispatch event on the same bloc
class ThingsPage extends StatefulWidget {
#override
_ThingsPageState createState() => _ThingsPageState();
}
class _ThingsPageState extends State<ThingsPage> {
#override
void initState(){
_profileBloc = BlocProvider.of<ProfileBloc>(context);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: BlocBuilder(
bloc: _profileBloc,
builder: (context, state){
if (state is ThingsAreUpdated){
return Container(
Text(state.count.toList())
);
} else {
return Container()
}
}
)
);
}
}