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
Related
I'm trying to get my state to persist using hydrated bloc but it is not working. When i restart the app the state is not persisting
This is the code i have to start the app:
void bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
HydratedBlocOverrides.runZoned(
() => runApp(
RepositoryProvider<void>(
create: (context) => DatabaseCubit(),
child: const RunApp(),
),
),
storage: storage,
);
}
this is the relevent code in the cubit:
class DatabaseCubit extends HydratedCubit<DatabaseState>{
DatabaseCubit() : super(databaseInitial);
#override
DatabaseState? fromJson(Map<String, dynamic> json) {
return DatabaseState.fromMap(json);
}
#override
Map<String, dynamic> toJson(DatabaseState state) {
return state.toMap();
}
I have set up unit tests that make sure my toMap and fromMap functions are working. The tests are passing, here is the code for them:
test('Database state should be converted to and from json', () {
final databaseStateAsJson = databaseState.toMap();
final databaseStateBackToNormal =
DatabaseState.fromMap(databaseStateAsJson);
expect(databaseStateBackToNormal, databaseState);
});
Please tell me what i am doing wrong
I've just solved a similar problem.
Try digging a little into hydrated_bloc.dart hydrate() method and you might find out why it refuses to load your state :).
Could be type conversion problem between collections - as it was in my case.
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
Trying to use Mockito to test my BLoC, the BLoC makes a server call using a repository class and the server call function is supposed to throw a custom exception if the user is not authenticated.
But when I am trying to stub the repository function to throw that custom exception, the test just fails with the following error:
sunapsis Authorization error (test error): test description
package:mockito/src/mock.dart 342:7 PostExpectation.thenThrow.<fn>
package:mockito/src/mock.dart 119:37 Mock.noSuchMethod
package:sunapsis/datasource/models/notifications_repository.dart 28:37 MockNotificationRepository.getNotificationList
package:sunapsis/blocs/notification_blocs/notification_bloc.dart 36:10 NotificationBloc.fetchNotifications
test/blocs/notification_blocs/notification_bloc_test.dart 53:48 main.<fn>.<fn>.<fn>
===== asynchronous gap ===========================
dart:async scheduleMicrotask
test/blocs/notification_blocs/notification_bloc_test.dart 53:7 main.<fn>.<fn>
And this is what my BLoC code looks like: fetchNotifications function calls the repository function and handles the response and errors. There are two catchError blocks, one handles AuthorizationException case and other handles any other Exception. Handling AuthorizationException differently because it will be used to set the Login state of the application.
notification_bloc.dart
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:sunapsis/datasource/dataobjects/notification.dart';
import 'package:sunapsis/datasource/models/notifications_repository.dart';
import 'package:sunapsis/utils/authorization_exception.dart';
class NotificationBloc {
final NotificationsRepository _notificationsRepository;
final Logger log = Logger('NotificationBloc');
final _listNotifications = PublishSubject<List<NotificationElement>>();
final _isEmptyList = PublishSubject<bool>();
final _isLoggedIn = PublishSubject<bool>();
Observable<List<NotificationElement>> get getNotificationList =>
_listNotifications.stream;
Observable<bool> get isLoggedIn => _isLoggedIn.stream;
Observable<bool> get isEmptyList => _isEmptyList.stream;
NotificationBloc({NotificationsRepository notificationsRepository})
: _notificationsRepository =
notificationsRepository ?? NotificationsRepository();
void fetchNotifications() {
_notificationsRepository
.getNotificationList()
.then((List<NotificationElement> list) {
if (list.length > 0) {
_listNotifications.add(list);
} else {
_isEmptyList.add(true);
}
})
.catchError((e) => _handleErrorCase,
test: (e) => e is AuthorizationException)
.catchError((e) {
log.shout("Error occurred while fetching notifications $e");
_listNotifications.sink.addError("$e");
});
}
void _handleErrorCase(e) {
log.shout("Session invalid: $e");
_isLoggedIn.sink.add(false);
_listNotifications.sink.addError("Error");
}
}
This is what my repository code looks like:
notifications_repository.dart
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:sunapsis/datasource/dataobjects/notification.dart';
import 'package:sunapsis/datasource/db/sunapsis_db_provider.dart';
import 'package:sunapsis/datasource/network/api_response.dart';
import 'package:sunapsis/datasource/network/sunapsis_api_provider.dart';
import 'package:sunapsis/utils/authorization_exception.dart';
/// Repository class which makes available all notifications related API functions
/// for server calls and database calls
class NotificationsRepository {
final Logger log = Logger('NotificationsRepository');
final SunapsisApiProvider apiProvider;
final SunapsisDbProvider dbProvider;
/// Optional [SunapsisApiProvider] and [SunapsisDbProvider] instances expected for unit testing
/// If instances are not provided - default case - a new instance is created
NotificationsRepository({SunapsisApiProvider api, SunapsisDbProvider db})
: apiProvider = api ?? SunapsisApiProvider(),
dbProvider = db ?? SunapsisDbProvider();
/// Returns a [Future] of [List] of [NotificationElement]
/// Tries to first look for notifications on the db
/// if notifications are found that list is returned
/// else a server call is made to fetch notifications
Future<List<NotificationElement>> getNotificationList([int currentTime]) {
return dbProvider.fetchNotifications().then(
(List<NotificationElement> notifications) {
if (notifications.length == 0) {
return getNotificationsListFromServer(currentTime);
}
return notifications;
}, onError: (_) {
return getNotificationsListFromServer(currentTime);
});
}
}
The function getNotificationsListFromServer is supposed to throw the AuthorizationException, which is supposed to be propagated through getNotificationList
This is the test case that is failing with the error mentioned before:
test('getNotification observable gets error on AuthorizationException',
() async {
when(mockNotificationsRepository.getNotificationList())
.thenThrow(AuthorizationException("test error", "test description"));
scheduleMicrotask(() => notificationBloc.fetchNotifications());
await expectLater(
notificationBloc.getNotificationList, emitsError("Error"));
});
And this is what the custom exception looks like:
authorization_exception.dart
class AuthorizationException implements Exception {
final String error;
final String description;
AuthorizationException(this.error, this.description);
String toString() {
var header = 'sunapsis Authorization error ($error)';
if (description != null) {
header = '$header: $description';
}
return '$header';
}
}
PS: When I tested my repository class and the function throwing the custom exception those tests were passed.
test('throws AuthorizationException on invalidSession()', () async {
when(mockSunapsisDbProvider.fetchNotifications())
.thenAnswer((_) => Future.error("Error"));
when(mockSunapsisDbProvider.getCachedLoginSession(1536333713))
.thenAnswer((_) => Future.value(authorization));
when(mockSunapsisApiProvider.getNotifications(authHeader))
.thenAnswer((_) => Future.value(ApiResponse.invalidSession()));
expect(notificationsRepository.getNotificationList(1536333713),
throwsA(TypeMatcher<AuthorizationException>()));
});
Above test passed and works as expected.
I am a new college grad working my first full time role and I might be doing something wrong. I will really appreciate any feedback or help, everything helps. Thanks for looking into this question.
You're using thenThrow to throw an exception, but because the mocked method returns a Future you should use thenAnswer.
The test would be like that:
test('getNotification observable gets error on AuthorizationException', () async {
// Using thenAnswer to throw an exception:
when(mockNotificationsRepository.getNotificationList())
.thenAnswer((_) async => throw AuthorizationException("test error", "test description"));
scheduleMicrotask(() => notificationBloc.fetchNotifications());
await expectLater(notificationBloc.getNotificationList, emitsError("Error"));
});
I think you are using the wrong TypeMatcher class. You need to use the one from the testing framework and not the one from the Flutter framework.
import 'package:flutter_test/flutter_test.dart';
import 'package:matcher/matcher.dart';
class AuthorizationException implements Exception {
const AuthorizationException();
}
Future<List<String>> getNotificationList(int id) async {
throw AuthorizationException();
}
void main() {
test('getNotification observable gets error on AuthorizationException',
() async {
expect(getNotificationList(1536333713),
throwsA(const TypeMatcher<AuthorizationException>()));
});
}
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.
callbacks or asynchronous methods or other options
A solution to the callback plague is "await" and "async" or more specifacally 'dart:async' library.
Now, what is the cost of asynchrony?
When should we not use them?
What are the other alternatives?
The below is a badly coded non-polymer custom element that acts like a messageBox in desktop environment. It gives me less braces and parenthesis-es but requires the caller to be also async or use "show().then((v){print(v);});" pattern. Should I avoid the pattern like this?
Is callback better? Or there is an even smarter way?
Polling version
import 'dart:html';
import 'dart:async';
void init(){
document.registerElement('list-modal',ListModal);
}
class ListModal extends HtmlElement{
ListModal.created():super.created();
String _modal_returns="";
void set modal_returns(String v){
///use the modal_returns setter to
///implement a custom behaviour for
///the return value of the show method
///within the callback you can pass on calling append .
_modal_returns=v;
}
factory ListModal(){
var e = new Element.tag('list-modal');
e.style..backgroundColor="olive"
..position="absolute"
..margin="auto"
..top="50%"
..verticalAlign="middle";
var close_b = new DivElement();
close_b.text = "X";
close_b.style..right="0"
..top="0"
..margin="0"
..verticalAlign="none"
..backgroundColor="blue"
..position="absolute";
close_b.onClick.listen((_){
e.hide();
});
e.append(close_b,(_)=>e.hide());
e.hide();
return e;
}
#override
ListModal append(
HtmlElement e,
[Function clickHandler=null]
){
super.append(e);
if(clickHandler!=null) {
e.onClick.listen(clickHandler);
}else{
e.onClick.listen((_){
this.hide();
_modal_returns = e.text;
});
}
return this;
}
Future<String> show() async{
_modal_returns = '';
this.hidden=false;
await wait_for_input();
print(_modal_returns);
return _modal_returns;
}
wait_for_input() async{
while(_modal_returns=="" && !this.hidden){
await delay();
}
}
void hide(){
this.hidden=true;
}
Future delay() async{
return new Future.delayed(
new Duration(milliseconds: 100));
}
}
Non-polling version
In response to Günter Zöchbauer's wisdom(avoid polling), posting a version that uses a completer. Thanks you as always Günter Zöchbauer:
import 'dart:html';
import 'dart:async';
void init(){
document.registerElement('list-modal',ListModal);
}
class ListModal extends HtmlElement{
ListModal.created():super.created();
String _modal_returns="";
Completer _completer;
void set modal_returns(String v){
///use the modal_returns setter to
///implement a custom behaviour for
///the return value of the show method.
///Use this setter within the callback for
///append. Always call hide() after
///setting modal_returns.
_modal_returns=v;
}
factory ListModal(){
var e = new Element.tag('list-modal');
e.style..backgroundColor="olive"
..position="absolute"
..margin="auto"
..top="50%"
..verticalAlign="middle";
var close_b = new DivElement();
close_b.text = "X";
close_b.style..right="0"
..top="0"
..margin="0"
..verticalAlign="none"
..backgroundColor="blue"
..position="absolute";
close_b.onClick.listen((_){
e.hide();
});
e.append(close_b,(_){e.hide();});
e.hide();
return e;
}
#override
ListModal append(
HtmlElement e,
[Function clickHandler=null]
){
super.append(e);
if(clickHandler!=null) {
e.onClick.listen(clickHandler);
}else{
e.onClick.listen((_){
_modal_returns = e.text;
this.hide();
});
}
return this;
}
Future<String> show() async{
_modal_returns = '';
_completer = new Completer();
this.hidden=false;
return _completer.future;
}
void hide(){
hidden=true;
_completer?.complete(_modal_returns);
_completer=null;
}
}
Usually there is no question whether async should be used or not. Usually one would try to avoid it. As soon as you call an async API your code goes async without a possibility to choose if you want that or not.
There are situations where async execution is intentionally made async. For example to split up large computation in smaller chunks to not starve the event queue from being processed.
On the server side there are several API functions that allow to choose between sync and async versions. There was an extensive discussion about when to use which. I'll look it up and add the link.
The disadvantages of using async / await instead of .then() should be minimal.
minimal Dart SDK version with async / await support is 1.9.1
the VM needs to do some additional rewriting before the code is executed the first time, but this is usually neglectable.
Your code seems to do polling.
wait_for_input() async {
while(_modal_returns=="" && !this.hidden){
await delay();
}
}
This should be avoided if possible.
It would be better to let the modal manage its hidden state itself (by adding a hide() method for example), then it doesn't have to poll whether it was hidden from the outside.