Flutter equivalent to Swift's NotificationCenter? - dart

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.

Related

Multiple rooms authorisation with Spring WebSocket and security

I'm making multi rooms chat with user authorization: users can have access only to some assigned rooms.
For every room I creating a topic with unique room id
How can I check permissions during the opening socket for reading?
On the server-side, for new inbound connection, I want to get room id from topic URL and check user access permissions for the room. But I didn't find how I can do it. I don't see the place, there it's possible.
AbstractSecurityWebSocketMessageBrokerConfigurer -- no way for dynamic check
#Configuration
class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
message.nullDestMatcher().permitAll()
.simpDestMatchers("/app/**").authenticated()
.anyMessage().hasRole("USER")
}
}
WebSocketMessageBrokerConfigurer -- can't get current url
#Configuration
class WebSocketSecurityConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
...
}
return message
}
});
}
}
I know, how to check access during writing messages, but can't find, how to do it during opening a web socket for reading. What is the standard mechanism for this case?
Dependencies:
compile 'org.grails.plugins:grails-spring-websocket:2.5.0.RC1'
compile "org.springframework.security:spring-security-messaging"
compile "org.springframework.security:spring-security-config"
compile "org.springframework.security:spring-security-core:5.1.8.RELEASE"
compile "org.springframework:spring-messaging:5.1.6.RELEASE"
UPDATE
I can pass room id from the client as a header, but on the server in configureClientInboundChannel I can't be sure, that room id in header same with id in topic URL. I can use some hashes, generated on the server-side, but it looks too complex
var socket = new SockJS("${createLink(uri: '/stomp')}");
var client = webstomp.over(socket);
client.connect({room-id:"0"}, function() {
client.subscribe("/topic/room/1", function(message) {
console.log("/topic/room/1");
}, {roomId:"1"});
client.subscribe("/topic/room/2", function(message) {
console.log("/topic/room/2");
}, {roomId:"2"});
});
During debugging, I have checked headers of command with type StompCommand.CONNECT.
For StompCommand.SUBSCRIBE command current topic URL presented in simpDestination header
Final solution is:
#Configuration
class WebSocketSecurityConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
def currentAuthentication = accessor.getHeader('simpUser') // from spring security
String destinationUrl = (String )accessor.getHeader('simpDestination')
// do check, and throw AuthenticationException
}
return message
}
});
}
}

How to call A-BLoC function from B-BLoC function?

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

Open certain page on push notification using firebase cloud messaging on flutter

I've manage to push a notification to my flutter app using firebase cloud messaging. what I'm trying to do right now is that, once i click the notification, it will directly to a certain page that the app have. How do i redirect the notification to a certain page? thank you
I have something like this, in my FCM class:
static StreamController<Map<String, dynamic>> _onMessageStreamController =
StreamController.broadcast();
static StreamController<Map<String, dynamic>> _streamController =
StreamController.broadcast();
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
static final Stream<Map<String, dynamic>> onFcmMessage =
_streamController.stream;
static setupFCMListeners() {
print("Registered FCM Listeners");
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
_onMessageStreamController.add(message);
},
onLaunch: (Map<String, dynamic> message) async {
_streamController.add(message);
},
onResume: (Map<String, dynamic> message) async {
_streamController.add(message);
},
);
}
static Widget handlePath(Map<String, dynamic> dataMap) {
var path = dataMap["route"];
var id = dataMap["id"];
return handlePathByRoute(path, id);
}
static Widget handlePathByRoute(String route, String routeId) {
switch (route) {
case "user":
return Profile(guid: routeId);
case "event":
return EventInfo(eventId: routeId);
case "messaging":
return MessagingView(guid: routeId);
default:
return null;
}
}
My main.dart subscribes to onFcmMessage stream, but you don't need streams to do all this. Also, you need some code to handle stream failure and closure.
But when the app comes to foreground it gets the message on either onMessage callback or the onLaunch or onResume callback. Check their differences on the FCM flutter pub docs.
The methods handlePath and handlePathByRoute are methods that usually my main.dart or other classes listening to notifications call to get the path to route to, but you can simply call them directly by replacing the stream code here like:
static setupFCMListeners() {
print("Registered FCM Listeners");
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("Message: $message"); // Not handling path since on notification in app it can be weird to open a new page randomly.
},
onLaunch: (Map<String, dynamic> message) async {
handlePath(message);
},
onResume: (Map<String, dynamic> message) async {
handlePath(message);
},
);
}
This honestly may not even be the best or even a good approach but due to lack of documentation, this is what I'm working with for now. I'd love to try Günter Zöchbauer's approach and save some object creation if possible!
Hope this is helpful! :)
EDIT: Profile, EventInfo and MessagingView are three classes that extend StatefulWidget, sorry if that wasn't clear.
You can also try using named routes, they make it easier like api routes and avoid a lot of imports and have a central router, but AFAIK they lacked transition configurations.
Don't forget to add click_action inside your notification data.
'click_action': 'FLUTTER_NOTIFICATION_CLICK'
see this
https://stackoverflow.com/a/65597471/11230524

Connecting a sink of BLoC with another BLoC

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

Is there any example for dart's `spawnUri(...)` in library "dart:isolate"?

There is a spawnUri(uri) function in dart:isolate, but I don't find any example. I have guessed its usage, but failed.
Suppose there are 2 files, in the first one, it will call spawnUri for the 2nd one, and communicate with it.
first.dart
import "dart:isolate";
main() {
ReceivePort port = new ReceivePort();
port.receive((msg, _) {
print(msg);
port.close();
});
var c = spawnUri("./second.dart");
c.send(["Freewind", "enjoy dart"], port.toSendPort());
}
second.dart
String hello(String who, String message) {
return "Hello, $who, $message";
}
void isolateMain(ReceivePort port) {
port.receive((msg, reply) => reply.send(hello(msg[0], msg[1]));
}
main() {}
But this example doesn't work. I don't know what's the correct code, how to fix it?
Here is a simple example that works with Dart 1.0.
app.dart:
import 'dart:isolate';
import 'dart:html';
import 'dart:async';
main() {
Element output = querySelector('output');
SendPort sendPort;
ReceivePort receivePort = new ReceivePort();
receivePort.listen((msg) {
if (sendPort == null) {
sendPort = msg;
} else {
output.text += 'Received from isolate: $msg\n';
}
});
String workerUri;
// Yikes, this is a hack. But is there another way?
if (identical(1, 1.0)) {
// we're in dart2js!
workerUri = 'worker.dart.js';
} else {
// we're in the VM!
workerUri = 'worker.dart';
}
int counter = 0;
Isolate.spawnUri(Uri.parse(workerUri), [], receivePort.sendPort).then((isolate) {
print('isolate spawned');
new Timer.periodic(const Duration(seconds: 1), (t) {
sendPort.send('From app: ${counter++}');
});
});
}
worker.dart:
import 'dart:isolate';
main(List<String> args, SendPort sendPort) {
ReceivePort receivePort = new ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((msg) {
sendPort.send('ECHO: $msg');
});
}
Building is a two-step process:
pub build
dart2js -m web/worker.dart -obuild/worker.dart.js
See the complete project here: https://github.com/sethladd/dart_worker_isolates_dart2js_test
WARNING : This code is out of date.
Replace your second.dart with the following to make it work :
import "dart:isolate";
String hello(String who, String message) {
return "Hello, $who, $message";
}
main() {
port.receive((msg, reply) => reply.send(hello(msg[0], msg[1])));
}
This gist: https://gist.github.com/damondouglas/8620350 provides a working (I tested it) Dart 1.5 example. An Isolate.spawn(...) example can be found there as well.
Reproducing here (adding import statements):
echo.dart:
import 'dart:isolate';
void main(List<String> args, SendPort replyTo) {
replyTo.send(args[0]);
}
main.dart:
import 'dart:isolate';
import 'dart:async';
main() {
var response = new ReceivePort();
Future<Isolate> remote = Isolate.spawnUri(Uri.parse("echo.dart"), ["foo"], response.sendPort);
remote.then((_) => response.first)
.then((msg) { print("received: $msg"); });
}
shameless copied from
Dart Web Development › Example on how to use Isolate.spawn
I hope the author doesn't mind
The spawned isolate has no idea where/how to respond to its parent.
In the parent, you could create a ReceivePort which will receive all message from child isolates.
Whenever you spawn an isolate, pass it the SendPort instance from your ReceivePort (via the message argument of Isolate.spawn).
The child isolate may/should create its own ReceivePort as well, so it can receive messages.
When instantiated, the child isolate must send its own SendPort (from its own ReceivePort) to its parent (via the parent's SendPort).
The current API is, in its own, really not helpful. But it provides all the necessary building blocks for a full-blown implementation.
You may need to wrap messages inside headers, something along these lines:
class _Request {
/// The ID of the request so the response may be associated to the request's future completer.
final Capability requestId;
/// The SendPort we must respond to, because the message could come from any isolate.
final SendPort responsePort;
/// The actual message of the request.
final dynamic message
const _Request(this.requestId, this.responsePort, this.message);
}
class _Response {
/// The ID of the request this response is meant to.
final Capability requestId;
/// Indicates if the request succeeded.
final bool success;
/// If [success] is true, holds the response message.
/// Otherwise, holds the error that occured.
final dynamic message;
const _Response.ok(this.requestId, this.message): success = true;
const _Response.error(this.requestId, this.message): success = false;
}
Every isolate could have a singleton message bus like this:
final isolateBus = new IsolateBus();
class IsolateBus {
final ReceivePort _receivePort = new ReceivePort();
final Map<Capability, Completer> _completers = {};
IsolateBus() {
_receivePort.listen(_handleMessage, onError: _handleError);
}
void _handleMessage(portMessage) {
if (portMessage is _Request) {
// This is a request, we should process.
// Here we send back the same message
portMessage.responsePort.send(
new _Response.ok(portMessage.requestId, portMessage.message));
} else if (portMessage is _Response) {
// We received a response
final completer = _completers[portMessage.requestId];
if (completer == null) {
print("Invalid request ID received.");
} else if (portMessage.success) {
completer.complete(portMessage.message);
} else {
completer.completeError(portMessage.message);
}
} else {
print("Invalid message received: $portMessage");
}
}
void _handleError(error) {
print("A ReceivePort error occured: $error");
}
Future request(SendPort port, message) {
final completer = new Completer();
final requestId = new Capability();
_completers[requestId] = completer;
port.send(new _Request(requestId, _receivePort.sendPort, message));
return completer.future;
}
}
SendPort anotherIsolatePort = ...
isolateBus.request(anotherIsolatePort, "Some message");
This is just one architectural example. You could of course roll-out your own.
This could be extended to support notifications (requests without response), streams, etc.
A global isolate registry could be needed to keep track of all SendPort instances from every isolates and eventually register them as services.

Resources