So, like most, I am new to Flutter, Bloc and Firebase Analytics. There are several ways to sync blocs by making one listen to another, but it quite doesn't match my situation as I'd like to keep track of some "Analytics related state variables".
AnalyticsBloc extends Bloc<AnalyticsEvent, AnalyticsState> {
final FirebaseAnalytics analytics;
final FirebaseAnalyticsObserver observer;
#override
FirebaseAnalyticsState get initialState => FirebaseAnalyticsState.initial();
void setScreen(String currentScreen) {
// sends and stores the currentScreen in Bloc State
dispatch(SetScreenEvent(currentScreen));
}
void sendEvent(String eventName) {
// uses the stored currentScreen in Bloc State
dispatch(SendEventEvent(eventName));
}
...
}
class AppBloc extends Bloc<AppEvent, AppState> {
#override
AppState get initialState => AppState.initial();
void someApplicationEvent() {
// Problem: is there any way to trigger sendEvent from this Bloc?
sendEvent('someAppEventTriggered');
dispatch(ResetAppEvent());
}
}
Problem: is there any way to trigger AnalyticsBloc.sendEvent from AppBloc?
Or should I just design it differently?
Pass blocA instance to blocB constructor and call function of blocA
Related
I'm doing some experiments with Flutter and met some problem. Basically, I'm using Flutter with BLoC pattern and Firestore as a remote store. I have something like this:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
....
#override
Stream<HomeState> mapEventToState(HomeState state, HomeEvent event) async* {
if(event is HomeEventStartTrackingWherlos) {
Firestore.instance.collection('objects_collection')
.snapshots()
.listen((data) {
var collection = List<MyObject>();
data.documents.forEach((document) {
MyObject mo = MyObject(document["name"], document["description"]);
collection.add(mo);
});
// return HomeState.reloadedCollection(collection);
});
}}
...
}
In line:
// return HomeState.reloadedCollection(collection);
I want to return new state with objects collection. If I simply return new state, then my build method in StatefulWidget isn't called. Do you have any suggestion how can I pass new state with collection after every change in the Firestore collection and based on that rebuild widget?
I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example
I am using the BLoC pattern as described at the Google IO talk.
I have a simple BLoC which is used to display a alert in the UI whenever a string is added to messageSink:
class AlertBloc {
final _message = BehaviorSubject<String>();
AlertBloc() {}
Stream<String> get message => _message.stream;
Sink<String> get messageSink => _message.sink;
void dispose() {
_message.close(); }
}
Elsewhere in the app, I have another BLoC which needs to add a string to messageSink, when a certain condition is met.
I noticed it is not a good idea to provide the whole BLoC from the Google I/O repo for the talk, and they provide advice for connecting a stream from a BLoC to another BLoC sink:
Note that we are not providing [CartBloc] to the
[ProductSquareBloc] directly, although it would be easier to
implement. BLoCs should not depend on other BLoCs (separation of
concerns). They can only communicate with each other using
streams. In this case, the [CartBloc.items] output plugs into the
[ProductSquareBloc.cartItems] input.
My question is how to connect a sink from a BLoC to another BLoC stream?
Here is a simple example for you. Imagine the following two BLoCs:
The first one exposes a Stream and populates it with some values:
class ProducerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _productionController = StreamController<int>();
//Instead, you expose a stream
Stream<int> get production => _productionController.stream;
//This method generates some values and puts them to stream
void produceValue() {
_productionController.sink.add(1);
_productionController.sink.add(2);
_productionController.sink.add(3);
}
//Don't forget to close your controllers
void dispose() {
_productionController.close();
}
}
The other one exposes a Sink and processes values that are put into it.
class ConsumerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _consumptionController = StreamController<int>();
//Instead, you expose a sink
StreamSink<int> get consumption => _consumptionController.sink;
//In class constructor we start listening to the stream of values
ConsumerBLoC() {
_consumptionController.listen((value) {_consumeValue(value);} );
//or simply: _consumptionController.listen(_consumeValue); //theese are the same
}
//This method generates some values and puts them to stream
void consumeValue(int value) {
//Do something with the value
print('Value processed: $value');
}
//Don't forget to close your controllers
void dispose() {
_consumptionController.close();
}
}
Now, the task is to connect production stream to consumption sink. As you have correctly noticed, you do not want for any of two BLoCs to know anything about existence of the other one. So none of the two should hold references to the other one or even create instances of another one. Instead, you connect them using your Widget class:
//Define some widget to represent main screen of your application
class MainScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainScreenState();
}
//And define a state for this widget (state does not need to be public)
class _MainScreenState extends State<MainScreen> {
//You define both blocks here
ProducerBLoC _producer = new ProducerBLoC();
ConsumerBLoC _consumer = new ConsumerBLoC();
//Now, either do it in _MainScreenState constructor, or in the initState() method
#override
void initState() {
super.initState();
//Connect production stream with consumption sink
_producer.production.listen((value) => _consumer.consumption.add(value));
//Or, beautifully: _producer.production.pipe(_consumer.consumption);
}
#override
Widget build(BuildContext context) {
//The exact implementation does not matter in current context
}
//And don't forget to close your controllers
#override
dispose() {
super.dispose();
_producer.dispose();
_consumer.dispose();
}
}
This way, any value generated by ProducerBLoC will immediately be consumed by ConsumerBLoC. And, what's the most important, - both BLoCs are completely independent from one another!
The exact same way as you'd do with streams: Passing it as parameter
class Bloc {
final Sink<int> _external;
Bloc(this._external);
}
I'm pretty new to Flutter and experimenting with the SDK. I'm working on a simple app that has a countdown in the background and want to trigger an event at certain intervals. For example, when clock reaches one minute remaining, send a push notification. In general, I'm trying to get a feel for how to monitor certain activities such as time and usage of the app and once certain conditions are met, trigger other things. Is it as simple as an if-else statement placed in the right place?
What kind of thing am I looking for to implement this?
Thanks in advance.
I prefer to use streams for such tasks
Stream<int> timer = Stream.periodic(Duration(seconds: 1), (int count) => count);
...
_MyTextWidget(timer)
and my widget
class _MyTextWidget extends StatefulWidget {
_MyTextWidget(this.stream);
final Stream<int> stream;
#override
State<StatefulWidget> createState() => _MyTextWidgetState();
}
class _MyTextWidgetState extends State<_MyTextWidget> {
int secondsToDisplay = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: widget.stream,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return snapshot.hasData ? Text(snapshot.data.toString()) : Text('nodata');
});
}
}
The following is a better example of an event listener based on Streams that I have found to work.
In the widget you want to listen to..
class CmVideoPlayer extends StatefulWidget {
String titlePlaying;
StreamController changeController = StreamController<VideoPlayerEvent>();
CmVideoPlayer({Key key, #required this.titlePlaying})
: assert(titlePlaying != null), super(key: key);
#override
_CmVideoPlayerState createState() => _CmVideoPlayerState();
}
See the line "StreamController changeController = StreamController();" that uses a small class VideoPlayerEvent to carry the message.
class VideoPlayerEvent {
var eventType;
var eventMessage;
VideoPlayerEvent(this.eventType, this.eventMessage);
}
Then in the STATEFULLWIDGET...
Refer the the stream as
class _CmVideoPlayerState extends State<CmVideoPlayer> {
void Member() {
widget.changeController.add(new VideoPlayerEvent('state', 'finished'));
}
}
As it is inside the _CmVideoPlayerState class, and using the ability to reach into the parent class via the widget variable.
Then in the area of the code using the widget, and to listen for the messages..
To listen for the messages
CmVideoPlayer myPlayer = CmVideoPlayer();
myPlayer.changeController.stream.listen((e) {
print('Reciever event from CmVideoPlayer: ' + e.eventMessage.toString());
}
That should do it.
HOWEVER, this only allows ONE listener at a time. After I got this going, I moved on. But plan to implement a multi listener down the track.
Maybe some one can expand on this. I am keeping it as simple as possible. If some one has a multi listener example. Please post here.
You can use ValueNotifier for this.
When value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
Helpful Medium link
I'm using FirebaseMessaging to send push notifications to my Flutter app. Those notifications contain chat details.
If the app is currently active and the user is at the chat page, the page should be updated to show the new message (this I can handle).
If the app is on any other page, a local notification/toast should be shown.
My problem, how do I forward the notification to the chat page?
I have FirebaseMessaging listening on the root page. And I can use ModalRoute.of(context).isCurrent to determine if the root page is the current page when the notification comes in. How can I broadcast the notification to the chat page when it is the active page?
In Swift, I'd use NotificationCenter to send data from the app delegate and the chat page would listen for it. I'm hoping something similar is available for Flutter.
Try this dart-event-bus.
An Event Bus using Dart Streams for decoupling applications
I've found a solution and I'm hoping it can help someone else in a similar situation.
I found the package Dart Message Bus
It does everything I need and make handling Streams much easier.
I did have to add one additional method (see the end).
Since the instructions were a bit cryptic, here's how I got it working.
//main.dart
import 'package:dart_message_bus/dart_message_bus.dart';
//global variable
final globalBus = new MessageBus();
class _MyHomePageState extends State<MyHomePage> {
// set up firebase messaging here
#override
void initState() {
super.initState();
_setupPushNotifs;
}
_setupPushNotifs() {
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) {
_processChatPush(message);
},
);
}
_processChatPush(Map<String, dynamic> message) async {
String messageID = message['messageID'];
globalBus.publish(new Message('newChat', data: "$messageID"));
}
}
//chat.dart
import 'package:dart_message_bus/dart_message_bus.dart';
import 'main.dart';
class _ChatPageState extends State<ChatPage> {
StreamSubscription streamListener;
#override
void initState() {
super.initState();
_listen();
}
#override
void dispose() {
streamListener.cancel();
streamListener = null;
}
_listen() async {
streamListener = globals.globalBus.subscribe('newChat', (Message m) async {
Map<String, dynamic> message = m.data;
String messageID = message['messedID'];
}
}
The dispose method is very important or the listener will keep listening and cause problems.
If you need verification that a subscriber is actually listening, modify the calling and listen methods:
// main.dart
_processChatPush(Map<String, dynamic> message) async {
String messageID = message['messageID'];
var callbackMessage = await globalBus.publish(
new Message('newChat', data: "$messageID"),
waitForKey: 'ackChat',
timeout: const Duration(seconds: 2)
);
if(callbackMessage.isEmpty) {
// means the another service's message was not received
// and timeout occured.
// process the push notification here
} else {
// the callback from another service received
// and callbackMessage.data contains callback-data.
// the push notification has been handled by chat.dart
}
}
// chat.dart
_listen() async {
streamListener = globals.globalBus.subscribe('newChat', (Message m) async {
Map<String, dynamic> message = m.data;
String messageID = message['messageID'];
var data = "ack";
var ack = new Message('ackChat', data: data);
globalBus.publish(ack);
}
}
I had to add one additional method in order to close the publishing stream when it's no longer needed.
Add to the end of Class MessageBus in message_bus.dart in the package source:
close() {
_streamController.close();
_streamController = null;
}
and then you can dispose the stream:
void dispose() {
globalBus.close();
super.dispose();
}
I ended up putting the globalBus variable in a library file. Then import that library in main.dart and chat.dart and remove the import main.dart from chat.dart.