I have this code:
import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:finalprojectapp/Providers/Message_provider.dart';
import 'package:finalprojectapp/Providers/Client_provider.dart';
class MQTTManager with ChangeNotifier{
//Properties
MqttServerClient client;
MQTTMessageProvider messageProvider = MQTTMessageProvider();
String _identifier;
String _topic;
String _host;
//Getters
//Setters
Future initialize({String host, String identifier}) async {
MqttServerClient _client = MqttServerClient(host, identifier);
this._identifier = _client.clientIdentifier;
this._host = host;
_client.port = 1883;
_client.keepAlivePeriod = 20;
_client.onDisconnected = onDisconnected;
_client.onConnected = onConnected;
_client.onSubscribed = onSubscribed;
_client.logging(on: false);
final conMess = MqttConnectMessage()
.withClientIdentifier(identifier)
.keepAliveFor(20)
.withWillTopic('willtopic')
.withWillMessage('willmessage')
.startClean()
.withWillQos(MqttQos.atLeastOnce);
_client.connectionMessage = conMess;
try {
MqttClientConnectionStatus result = await _client.connect('BBFF-qkHkFkvJ6oFUw9m6Pa9bzQTCbVCddH','');
this.client = _client;
notifyListeners();
return result.state;
} on Exception catch (e) {
print('Something went wrong $e');
disconnect();
return null;
}
}
void subscription({String topic}) {
this._topic = topic;
print('EXAMPLE::Subscribing to the $_topic topic');
this.client.subscribe(this._topic, MqttQos.atMostOnce);
}
void unsubscribe({String topic}) {
print('unsubscribing from $topic');
this.client.unsubscribe(topic);
print('Unsubscribbed!');
}
void publish({String topic, String message}) async {
final builder = MqttClientPayloadBuilder();
builder.addString(message);
this.client.publishMessage(topic, MqttQos.atMostOnce, builder.payload);
}
void disconnect() async {
await MqttUtilities.asyncSleep(2);
print('EXAMPLE::Disconnecting');
this.client.disconnect();
}
/// The subscribed callback
void onSubscribed(String topic) {
this._topic = topic;
print('EXAMPLE::Subscription confirmed for topic $this._topic');
this.client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage _recMess = c[0].payload;
final String _message =
MqttPublishPayload.bytesToStringAsString(_recMess.payload.message);
messageProvider.setMessage(_message);
});
}
/// The unsolicited disconnect callback
void onDisconnected() {
print('EXAMPLE::OnDisconnected client callback - Client disconnection');
if (this.client.connectionStatus.returnCode == MqttConnectReturnCode.solicited) {
print('EXAMPLE::OnDisconnected callback is solicited, this is correct');
}
}
/// The successful connect callback
void onConnected() {
print(
'EXAMPLE::OnConnected client callback - Client connection was sucessful');
}
/// Pong callback
void pong() {
print('EXAMPLE::Ping response client callback invoked');
}
}
Which is in charge of notifying when ive obtained a client from Mqtt broker, and is then passed to the client property of this class. When this happens, notifyListeners is supposed to notify this Provider:
class AppWrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MQTTMessageProvider>(
create: (_) => MQTTMessageProvider(),
),
ChangeNotifierProvider<MQTTManager>(
create: (_) => MQTTManager(),
),
],
child: MQTTInitialize()
);
}
}
and then retrieve it in this class ( which is child of MQTTInitialize() ) :
final MQTTManager managerProvider = Provider.of<MQTTManager>(context);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blueGrey,
title: Text('Subscribe'),
actions: <Widget>[
FlatButton.icon(
onPressed: (){
managerProvider.disconnect();
},
icon: Icon(Icons.arrow_back),
label: Text('return'),
),
],
),
... (it continues)
Problem is that, when I press the button, it throws the following error:
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: NoSuchMethodError: The method 'disconnect' was called on null.
E/flutter (30793): Receiver: null
E/flutter (30793): Tried calling: disconnect()
E/flutter (30793): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
Which I suppose is because managerProvider in final MQTTManager managerProvider = Provider.of<MQTTManager>(context); is null, so I can't call managerProvider.disconnect();
How can I get my ChangeNotifierProvider to provide a correct instance of manager?
Link to project: https://github.com/TacoMariachi/Mqtt_flutter_app.git
Related
I subscribe to a graphql document:
// Dart imports:
import 'dart:async';
// Package imports:
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:phoenix_socket/phoenix_socket.dart';
// Project imports:
import 'package:core/util/confirmations/phoenix_link.dart';
class SubscriptionChannel {
PhoenixSocket? socket;
PhoenixChannel? channel;
GraphQLClient? client;
final StreamController<Map> _onMessageController = StreamController<Map>();
Stream<Map> get onMessage => _onMessageController.stream;
Future<void> connect(
String phoenixHttpLinkEndpoint, String websocketUriEndpoint) async {
final HttpLink phoenixHttpLink = HttpLink(
phoenixHttpLinkEndpoint,
);
channel =
await PhoenixLink.createChannel(websocketUri: websocketUriEndpoint);
final phoenixLink = PhoenixLink(
channel: channel!,
);
var link = Link.split(
(request) => request.isSubscription, phoenixLink, phoenixHttpLink);
client = GraphQLClient(
link: link,
cache: GraphQLCache(),
);
}
void addSubscriptionTransactionConfirmed(
String address, Function(QueryResult) function) {
final subscriptionDocument = gql(
'subscription { transactionConfirmed(address: "$address") { nbConfirmations } }',
);
Stream<QueryResult> subscription = client!.subscribe(
SubscriptionOptions(document: subscriptionDocument),
);
subscription.listen(function);
}
Future<Message> onPushReply(Push push) async {
final Completer<Message> completer = Completer<Message>();
final Message result = await channel!.onPushReply(push.replyEvent);
completer.complete(result);
return completer.future;
}
void close() {
_onMessageController.close();
if (socket != null) {
socket!.close();
}
}
}
The goal is, after a api request, to wait a notification with a nb of confirmations:
{
...
await subscriptionChannel.connect(
'https://mainnet.archethic.net/socket/websocket',
'ws://mainnet.archethic.net/socket/websocket');
subscriptionChannel.addSubscriptionTransactionConfirmed(
transaction.address!, waitConfirmations);
transactionStatus = sendTx(signedTx); // API Request
...
}
void waitConfirmations(QueryResult event) {
if (event.data != null &&
event.data!['transactionConfirmed'] != null &&
event.data!['transactionConfirmed']['nbConfirmations'] != null) {
EventTaxiImpl.singleton().fire(TransactionSendEvent(
response: 'ok',
nbConfirmations: event.data!['transactionConfirmed']
['nbConfirmations']));
} else {
EventTaxiImpl.singleton().fire(
TransactionSendEvent(nbConfirmations: 0, response: 'ko'),
);
}
subscriptionChannel.close();
}
My code works in a StatefulWidget but doesn't work in a class
Have you got some examples in a class where you subscribe to a grapqhql notification please to understand how to code this in a class
NB: i'm using Phoenix link
// Dart imports:
import 'dart:async';
// Package imports:
import 'package:gql_exec/gql_exec.dart';
import 'package:gql_link/gql_link.dart';
import 'package:phoenix_socket/phoenix_socket.dart';
/// a link for subscriptions (or also mutations/queries) over phoenix channels
class PhoenixLink extends Link {
/// the underlying phoenix channel
final PhoenixChannel channel;
final RequestSerializer _serializer;
final ResponseParser _parser;
/// create a new [PhoenixLink] using an established PhoenixChannel [channel].
/// You can use the static [createChannel] method to create a [PhoenixChannel]
/// from a websocket URI and optional parameters (e.g. for authentication)
PhoenixLink(
{required PhoenixChannel channel,
ResponseParser parser = const ResponseParser(),
RequestSerializer serializer = const RequestSerializer()})
: channel = channel,
_serializer = serializer,
_parser = parser;
/// create a new phoenix socket from the given websocketUri,
/// connect to it, and create a channel, and join it
static Future<PhoenixChannel> createChannel(
{required String websocketUri, Map<String, String>? params}) async {
final socket = PhoenixSocket(websocketUri,
socketOptions: PhoenixSocketOptions(params: params));
await socket.connect();
final channel = socket.addChannel(topic: '__absinthe__:control');
final push = channel.join();
await push.future;
return channel;
}
#override
Stream<Response> request(Request request, [NextLink? forward]) async* {
assert(forward == null, '$this does not support a NextLink (got $forward)');
final payload = _serializer.serializeRequest(request);
String? phoenixSubscriptionId;
StreamSubscription<Response>? websocketSubscription;
StreamController<Response>? streamController;
final push = channel.push('doc', payload);
try {
final pushResponse = await push.future;
//set the subscription id in order to cancel the subscription later
phoenixSubscriptionId =
pushResponse.response['subscriptionId'] as String?;
if (phoenixSubscriptionId != null) {
//yield all messages for this subscription
streamController = StreamController();
websocketSubscription = channel.socket
.streamForTopic(phoenixSubscriptionId)
.map((event) => _parser.parseResponse(
event.payload!['result'] as Map<String, dynamic>))
.listen(streamController.add, onError: streamController.addError);
yield* streamController.stream;
} else if (pushResponse.isOk) {
yield _parser
.parseResponse(pushResponse.response as Map<String, dynamic>);
} else if (pushResponse.isError) {
throw _parser.parseError(pushResponse.response as Map<String, dynamic>);
}
} finally {
await websocketSubscription?.cancel();
await streamController?.close();
//this will be called once the caller stops listening to the stream
// (yield* stops if there is no one listening)
if (phoenixSubscriptionId != null) {
channel.push('unsubscribe', {'subscriptionId': phoenixSubscriptionId});
}
}
}
}
I'm using flutter_callkit_incoming package to get callsNotification in my application through the payload of FCM in all states of my App i.e background/forground/terminated state.
Navigation is fine now to the VideoCallingAgoraPage after clicking Accept button on call incoming notification on forground state of my app. -> Using listenerEvent from NikahMatch class
But problem comes when this listenerEvent is used for navigation in background/terminated state. -> Using listenerEvent as top level function because of background handler as shown below in my background handler function
When the compiler reads this line await NavigationService.instance.pushNamed(AppRoute.voiceCall); in listener event on clicking accept of notification from flutter_callKit_incoming in the background/terminated state of my app, I am getting this error in console.
E/flutter (11545): Receiver: null
E/flutter (11545): Tried calling: pushNamed<Object>("/videoCall_agora", arguments: null)
E/flutter (11545): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:38:5)
E/flutter (11545): #1 NavigationService.pushNamed (package:nikah_match/helpers/navigationService.dart:38:39)
E/flutter (11545): #2 listenerEvent.<anonymous closure> (package:nikah_match/main.dart:311:46)
E/flutter (11545): <asynchronous suspension>
E/flutter (11545):
As well as in the logs, I find that log(navigationKey.currentState.toString()); defined in pushNamed function is also null. While in the case of forground navigation, navigationKey.currentState from pushNamed function is never null.
When I received call notification in terminated state, accept case of listener event(top level function) was called without creating widget tree and initializing GetMaterialPage that caused navigator state to be null.
I think that the listnerEvent Accept case is run before starting/building widget tree and navigator key in GetMaterialPage is never assigned.
How can I get rid of that?
This is my backgroundHandler function:
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
bool videoCallEnabled = false;
bool audioCallEnabled = false;
if (message != null) {
debugPrint("Handling background is called");
print(
"Handling a background message and background handler: ${message.messageId}");
try {
videoCallEnabled = message.data.containsKey('videoCall');
audioCallEnabled = message.data.containsKey('voiceCall');
if (videoCallEnabled || audioCallEnabled) {
log("Video call is configured and is started");
showCallkitIncoming(Uuid().v4(), message: message);
//w8 for streaming
debugPrint("Should listen to events in background/terminated state");
listenerEvent(message);
} else {
log("No Video or audio call was initialized");
}
} catch (e) {
debugPrint("Error occured:" + e.toString());
}
}
}
This is my listener event:
Future<void> listenerEvent(RemoteMessage message) async {
log("Listner event background/terminated handler app has run");
backgroundChatRoomId = message.data['chatRoomId'];
backgroundCallsDocId = message.data['callsDocId'];
backgroundRequesterName = message.data['callerName'];
backgroundRequesterImageUrl = message.data['imageUrl'];
// String imageUrl = message.data['imageUrl'];
bool videoCallEnabled = false;
if (message.data != null) {
videoCallEnabled = message.data.containsKey('videoCall');
} else {
log("Data was null");
}
try {
FlutterCallkitIncoming.onEvent.listen((event) async {
print('HOME: $event');
switch (event.name) {
case CallEvent.ACTION_CALL_INCOMING:
// TODO: received an incoming call
log("Call is incoming");
break;
case CallEvent.ACTION_CALL_START:
// TODO: started an outgoing call
// TODO: show screen calling in Flutter
log("Call is started");
break;
case CallEvent.ACTION_CALL_ACCEPT:
// TODO: accepted an incoming call
// TODO: show screen calling in Flutter
log("......Call Accepted background/terminated state....");
currentChannel = backgroundChatRoomId;
log("currentChannel in accepted is: $currentChannel");
debugPrint("Details of call"+backgroundChatRoomId+backgroundCallsDocId );
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(backgroundChatRoomId)
.collection("calls")
.doc(backgroundCallsDocId)
.update({
'receiverCallResponse': 'Accepted',
'callResponseDateTime': FieldValue.serverTimestamp()
}).then((value) => log("Values updated at firebase firestore as Accepted"));
if (videoCallEnabled) {
log("in video call enabled in accept call of listener event");
await NavigationService.instance.pushNamed(AppRoute.videoAgoraCall,);
}
break;
}
});
} on Exception {}
}
This is my first stateful GetMaterial page which initializes all Firebase Messaging functions (Forground Local FLutter local notifications excluded from code for readability):
class NikkahMatch extends StatefulWidget {
const NikkahMatch({Key key}) : super(key: key);
#override
State<NikkahMatch> createState() => _NikkahMatchState();
}
class _NikkahMatchState extends State<NikkahMatch> with WidgetsBindingObserver {
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addObserver(this);
//Function if from terminated state
FirebaseMessaging.instance.getInitialMessage().then((message) async {
log("get Initial Message function is used.. ");
String screenName = 'No screen';
bool screenEnabled = false;
if (message != null) {
if (message.data != null) {
log("Remote message data is null for now");
if (message.data.isNotEmpty) {
screenEnabled = message.data.containsKey('screenName');
if (screenEnabled) {
if (screenName == 'chatScreen') {
log("Screen is Chat");
String type = 'Nothing';
String chatRoomId = 'Nothing';
if (message.data['type'] != null) {
type = message.data['type'];
if (type == 'profileMatched') {
String likerId = message.data['likerId'];
String likedId = message.data['likedId'];
chatRoomId = chatController.getChatRoomId(likerId, likedId);
}
} else {
chatRoomId = message.data['chatRoomId'];
}
log("ChatRoom Id is: ${chatRoomId}");
log("Navigating from onMessagePop to the ChatRoom 1");
//We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(chatRoomId)
.get()
.then((value) async {
if (value.exists) {
log("ChatRoom Doc " + value.toString());
log("Navigating from onMessagePop to the ChatRoom 2");
log("Last Message was : ${value.data()['lastMessage']}");
backGroundLevelChatRoomDoc = value.data();
await NavigationService.instance.pushNamed(AppRoute.chatScreen);
} else {
log("no doc exist for chat");
}
});
}
else if (screenName == 'videoScreen') {
log("Screen is Video");
initCall(message);
} else if (screenName == 'voiceScreen') {
log("Screen is Audio");
initCall(message);
} else {
log("Screen is in Else method of getInitialMessage");
}
} else {
debugPrint("Notification Pay load data is Empty");
}
} else {
log("Screen isn't enabled");
}
} else {
log("message data is null");
}
} else {
log("...........message data is null in bahir wala else");
}
});
//This function will constantly listen to the notification recieved from firebase
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
log("onMessageOpenedApp function is used.. ");
String screenName = 'No screen';
bool screenEnabled = false;
if (message.data.isNotEmpty) {
screenEnabled = message.data.containsKey('screenName');
if (screenEnabled) {
//Move to the screen which is needed to
log("Screen is Enabled");
screenName = message.data['screenName'];
log("Screen name is: $screenName");
if (screenName == 'chatScreen') {
log("Screen is Chat");
String type = 'Nothing';
String chatRoomId = 'Nothing';
if (message.data['type'] != null) {
type = message.data['type'];
if (type == 'profileMatched') {
String likerId = message.data['likerId'];
String likedId = message.data['likedId'];
chatRoomId = chatController.getChatRoomId(likerId, likedId);
}
} else {
chatRoomId = message.data['chatRoomId'];
}
log("ChatRoom Id is: ${chatRoomId}");
log("Navigating from onMessagePop to the ChatRoom 1");
//We have chatRoomId here and we need to navigate to the ChatRoomScreen having same Id
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(chatRoomId)
.get()
.then((value) async {
if (value.exists) {
log("ChatRoom Doc " + value.toString());
log("Navigating from onMessagePop to the ChatRoom 2");
log("Last Message was : ${value.data()['lastMessage']}");
backGroundLevelChatRoomDoc = value.data();
/* await NavigationService.instance
.pushNamed(AppRoute.chatScreen, args:ChatArgs(value.data(), false));*/
await NavigationService.instance.pushNamed(AppRoute.chatScreen);
} else {
log("no doc exist for chat");
}
});
}
else if (screenName == 'videoScreen') {
log("Screen is Video");
initCall(message);
} else if (screenName == 'voiceScreen') {
log("Screen is Audio");
initCall(message);
} else {
log("Screen is in Else");
}
}
} else {
debugPrint("Notification Pay load data is Empty");
}
});
}
#override
Widget build(BuildContext context) {
print("Main page build");
return GetMaterialApp(
onGenerateRoute: AppRoute.generateRoute,
debugShowCheckedModeBanner: false,
navigatorKey: NavigationService.instance.navigationKey,
debugShowMaterialGrid: false,
title: 'Nikah Match',
initialRoute: '/splash_screen',
theme: ThemeData(
fontFamily: 'Poppins',
scaffoldBackgroundColor: kScaffoldBgColor,
appBarTheme: const AppBarTheme(
elevation: 0,
backgroundColor: kPrimaryColor,
),
accentColor: kPrimaryColor.withOpacity(0.2),
),
themeMode: ThemeMode.light,
getPages: [
GetPage(name: '/splash_screen', page: () => SplashScreen()),
GetPage(name: '/get_started', page: () => GetStarted()),
GetPage(
name: '/videoCall_agora',
page: () => VideoCallAgoraUIKit(
anotherUserName: backgroundRequesterName,
anotherUserImage: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: "",
anotherUserId: "",
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
),
),
// GetPage(name: '/after_log_in_screen', page: () => AfterLogin()),
],
);
}
}
This is my NavigationService class:
class NavigationService {
// Global navigation key for whole application
GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>();
/// Get app context
BuildContext get appContext => navigationKey.currentContext;
/// App route observer
RouteObserver<Route<dynamic>> routeObserver = RouteObserver<Route<dynamic>>();
static final NavigationService _instance = NavigationService._private();
factory NavigationService() {
return _instance;
}
NavigationService._private();
static NavigationService get instance => _instance;
/// Pushing new page into navigation stack
///
/// `routeName` is page's route name defined in [AppRoute]
/// `args` is optional data to be sent to new page
Future<T> pushNamed<T extends Object>(String routeName,
{Object args}) async {
log(navigationKey.toString());
log(navigationKey.currentState.toString());
return navigationKey.currentState.pushNamed<T>(
routeName,
arguments: args,
);
}
Future<T> pushNamedIfNotCurrent<T extends Object>(String routeName,
{Object args}) async {
if (!isCurrent(routeName)) {
return pushNamed(routeName, args: args);
}
return null;
}
bool isCurrent(String routeName) {
bool isCurrent = false;
navigationKey.currentState.popUntil((route) {
if (route.settings.name == routeName) {
isCurrent = true;
}
return true;
});
return isCurrent;
}
/// Pushing new page into navigation stack
///
/// `route` is route generator
Future<T> push<T extends Object>(Route<T> route) async {
return navigationKey.currentState.push<T>(route);
}
/// Replace the current route of the navigator by pushing the given route and
/// then disposing the previous route once the new route has finished
/// animating in.
Future<T> pushReplacementNamed<T extends Object, TO extends Object>(
String routeName,
{Object args}) async {
return navigationKey.currentState.pushReplacementNamed<T, TO>(
routeName,
arguments: args,
);
}
/// Push the route with the given name onto the navigator, and then remove all
/// the previous routes until the `predicate` returns true.
Future<T> pushNamedAndRemoveUntil<T extends Object>(
String routeName, {
Object args,
bool Function(Route<dynamic>) predicate,
}) async {
return navigationKey.currentState.pushNamedAndRemoveUntil<T>(
routeName,
predicate==null? (_) => false: (_) => true,
arguments: args,
);
}
/// Push the given route onto the navigator, and then remove all the previous
/// routes until the `predicate` returns true.
Future<T> pushAndRemoveUntil<T extends Object>(
Route<T> route, {
bool Function(Route<dynamic>) predicate,
}) async {
return navigationKey.currentState.pushAndRemoveUntil<T>(
route,
predicate==null? (_) => false: (_) => true,
);
}
/// Consults the current route's [Route.willPop] method, and acts accordingly,
/// potentially popping the route as a result; returns whether the pop request
/// should be considered handled.
Future<bool> maybePop<T extends Object>([Object args]) async {
return navigationKey.currentState.maybePop<T>(args as T);
}
/// Whether the navigator can be popped.
bool canPop() => navigationKey.currentState.canPop();
/// Pop the top-most route off the navigator.
void goBack<T extends Object>({T result}) {
navigationKey.currentState.pop<T>(result);
}
/// Calls [pop] repeatedly until the predicate returns true.
void popUntil(String route) {
navigationKey.currentState.popUntil(ModalRoute.withName(route));
}
}
class AppRoute {
static const homePage = '/home_page';
static const chatScreen ='/chat_screen';
static const splash = '/splash_screen';
static const voiceCall = '/voice_call';
static const videoAgoraCall = '/videoCall_agora';
static Route<Object> generateRoute(RouteSettings settings) {
switch (settings.name) {
case homePage:
return MaterialPageRoute(
builder: (_) => HomePage(), settings: settings);
case chatScreen:
return MaterialPageRoute(
builder: (_) =>
ChatScreen(docs: backGroundLevelChatRoomDoc, isArchived: false,), settings: settings);
case splash:
return MaterialPageRoute(
builder: (_) => SplashScreen(), settings: settings);
case voiceCall:
return MaterialPageRoute(
builder: (_) => VoiceCall(
toCallName: backgroundRequesterName,
toCallImageUrl: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: voiceCallToken,
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
), settings: settings);
case videoAgoraCall:
return MaterialPageRoute(
builder: (_) => VideoCallAgoraUIKit(
anotherUserName: backgroundRequesterName,
anotherUserImage: backgroundRequesterImageUrl,
channelName: backgroundChatRoomId,
token: "",
anotherUserId: "",
docId: backgroundCallsDocId,
callDoc: backgroundPassableAbleCdm,
), settings: settings);
default:
return null;
}
}
}
Actually, I was also stuck when using flutter_incoming_callkit for navigation in terminated/background state for weeks, and strikingly, the solution was so simple.
The reason causing you this problem is:
-> For receiving notification in terminated or background state, backgroundHandler function is working in its own isolate and onGenerateRoutes from your NikkahMatch class is not and never known to this isolated function where you are actually trying to navigate through the pushNamed route.
So my solution was:
-> Use the combination of firestore and cloud functions for navigation. This would allow us to have the context and it won't be null as we are navigating from inside the app widget tree and not from an isolated function i.e backgroundHandler. Top-level listenerEvent Function is only used to change the values in the call document on Firestore.
[Note: Top-Level function is a function that is not part of any class]
Upon receiving flutter_incoming_callkit notification on receiver side:
On the click of Accept button, use top-level listenerEvent function to change the call status from incoming to accepted in call's document.
This will open the app from terminated/background state.
I used this function didChangeAppLifecycleState in my first class of widget tree to handle/know if app has come from terminated/background state:
Check out this code:
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
print(state);
if (state == AppLifecycleState.resumed) {
//Check call when open app from background
print("in app life cycle resumed");
checkAndNavigationCallingPage();
}
}
checkAndNavigationCallingPage() async {
print("checkAndNavigationCallingPage CheckCalling page 1");
if (auth.currentUser != null) {
print("auth.currentUser.uid: ${auth.currentUser.uid}");
}
// NavigationService().navigationKey.currentState.pushNamed(AppRoute.videoAgoraCall);
var currentCall = await getCurrentCall();
print("inside the checkAndNavigationCallingPage and $currentCall");
if (currentCall != null) {
print("inside the currentCall != null");
//Here we have to move to calling page
final g = Get;
if (!g.isDialogOpen) {
g.defaultDialog(content: CircularProgressIndicator());
}
Future.delayed(Duration(milliseconds: 50));
await FirebaseFirestore.instance
.collection('Users')
.doc(auth.currentUser.uid)
.collection('calls')
.doc(auth.currentUser.uid)
.get()
.then((userCallDoc) async {
if (userCallDoc.exists) {
if (userCallDoc['type'] == 'chapVoiceCall' ||
userCallDoc['type'] == 'chapVideoCall') {
bool isDeclined = false;
String chapCallDocId = "";
chapCallDocId = userCallDoc['chapCallDocId'];
print(
"............. Call was Accepted by receiver or sender.............");
print("ChapCallDocId $chapCallDocId");
ChapCallDocModel cdm = ChapCallDocModel();
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(userCallDoc['chatRoomId'])
.collection("chapCalls")
.doc(chapCallDocId)
.get()
.then((value) {
if ((value['requestedResponse'] == 'Declined' &&
value['requesterResponse'] == 'Declined') ||
(value['senderResponse'] == 'Declined') ||
(value['requestedResponse'] == 'TimeOut' &&
value['requesterResponse'] == 'TimeOut')) {
isDeclined = true;
print(
"in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
} else {
isDeclined = false;
cdm = ChapCallDocModel.fromJson(value.data());
print("CDM print is: ${cdm.toJson()}");
}
});
currentChannel = userCallDoc['chatRoomId'];
if (!isDeclined) {
if (userCallDoc['type'] == 'chapVoiceCall') {
print("in voice call enabled in accept call of listener event");
var voiceCallToken = await GetToken().getTokenMethod(
userCallDoc['chatRoomId'], auth.currentUser.uid);
print("token before if in splashscreen is: ${voiceCallToken}");
if (voiceCallToken != null) {
if (g.isDialogOpen) {
g.back();
}
Get.to(
() => ChapVoiceCall(
toCallName: userCallDoc['requesterName'],
toCallImageUrl: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: voiceCallToken,
docId: userCallDoc['chapCallDocId'],
callDoc: cdm,
),
);
} else {
print(
"......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
}
} else {
if (g.isDialogOpen) {
g.back();
}
g.to(() => ChapVideoCallAgoraUIKit(
anotherUserName: userCallDoc['requesterName'],
anotherUserImage: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: "",
anotherUserId: userCallDoc['requesterId'],
docId: userCallDoc['chapCallDocId'],
callDoc: cdm,
));
}
} else {
await FlutterCallkitIncoming.endAllCalls();
print(
"the call was either declined by sender or receiver or was timed out.");
}
} else {
bool isDeclined = false;
print(
"............. Call was Accepted by receiver or sender.............");
CallDocModel cdm = CallDocModel();
await FirebaseFirestore.instance
.collection("ChatRoom")
.doc(userCallDoc['chatRoomId'])
.collection("calls")
.doc(userCallDoc['callsDocId'])
.get()
.then((value) {
if (value['receiverCallResponse'] == 'Declined' ||
value['senderCallResponse'] == 'Declined' ||
value['receiverCallResponse'] == 'TimeOut' ||
value['senderCallResponse'] == 'TimeOut') {
isDeclined = true;
print(
"in checking declined ${value['receiverCallResponse'] == 'Declined' || value['senderCallResponse'] == 'Declined'}");
} else {
isDeclined = false;
cdm = CallDocModel.fromJson(value.data());
print("CDM print is: ${cdm.toJson()}");
}
});
currentChannel = userCallDoc['chatRoomId'];
if (!isDeclined) {
if (userCallDoc['type'] == 'voiceCall') {
print("in voice call enabled in accept call of listener event");
var voiceCallToken = await GetToken().getTokenMethod(
userCallDoc['chatRoomId'], auth.currentUser.uid);
print("token before if in splashscreen is: ${voiceCallToken}");
if (voiceCallToken != null) {
if (g.isDialogOpen) {
g.back();
}
Get.to(
() => VoiceCall(
toCallName: userCallDoc['requesterName'],
toCallImageUrl: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: voiceCallToken,
docId: userCallDoc['callsDocId'],
callDoc: cdm,
),
);
} else {
print(
"......Call Accepted background/terminated state....in token being null in voice call enabled in accept call of listener event");
}
} else {
if (g.isDialogOpen) {
g.back();
}
g.to(() => VideoCallAgoraUIKit(
anotherUserName: userCallDoc['requesterName'],
anotherUserImage: userCallDoc['requesterImage'],
channelName: userCallDoc['chatRoomId'],
token: "",
anotherUserId: userCallDoc['requesterId'],
docId: userCallDoc['callsDocId'],
callDoc: cdm,
));
}
} else {
await FlutterCallkitIncoming.endAllCalls();
print(
"the call was either declined by sender or receiver or was timed out.");
}
}
} else {
debugPrint("Document not found");
}
});
}
}
In the above code, I have given my db scenario, so anyone, who needs to know how to handle the different call status can deeply look into it. Or you can comment here and I would be honored to reply.
I am learning Flutter and Dart currently. Now I want to read and write files to memory. I have code for reading and writing. Now I want tests for that. Here is where I run into problems. I always get:
'package:flutter/src/services/platform_channel.dart': Failed assertion: line 134 pos 7: '_binaryMessenger != null || ServicesBinding.instance != null': Cannot use this MethodChannel before the binary messenger has been initialized. This happens when you invoke platform methods before the WidgetsFlutterBinding has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() before this or by passing a custom BinaryMessenger instance to MethodChannel().
dart:core _AssertionError._throwNew
package:flutter/src/services/platform_channel.dart 134:7 MethodChannel.binaryMessenger
package:flutter/src/services/platform_channel.dart 167:36 MethodChannel._invokeMethod
package:flutter/src/services/platform_channel.dart 350:12 MethodChannel.invokeMethod
package:path_provider_macos/path_provider_macos.dart 48:10 PathProviderMacOS.getApplicationDocumentsPath
package:path_provider/path_provider.dart 115:40 getApplicationDocumentsDirectory
package:skeet25pro/main_counter.dart 18:29 CounterStorage._localPath
package:skeet25pro/main_counter.dart 24:24 CounterStorage._localFile
package:skeet25pro/main_counter.dart 43:24 CounterStorage.writeCounter
test/file_io_test.dart 8:27 main.<fn>
test/file_io_test.dart 5:33 main.<fn>
main_counter.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(
MaterialApp(
title: 'Reading and Writing Files',
home: FlutterDemo(storage: CounterStorage()),
),
);
}
class CounterStorage {
Future<String> get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get _localFile async {
final path = await _localPath;
return File('$path/counter.txt');
}
Future<int> readCounter() async {
try {
final file = await _localFile;
// Read the file
final contents = await file.readAsString();
return int.parse(contents);
} catch (e) {
// If encountering an error, return 0
return 0;
}
}
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$counter');
}
}
class FlutterDemo extends StatefulWidget {
const FlutterDemo({Key? key, required this.storage}) : super(key: key);
final CounterStorage storage;
#override
_FlutterDemoState createState() => _FlutterDemoState();
}
class _FlutterDemoState extends State<FlutterDemo> {
int _counter = 0;
#override
void initState() {
super.initState();
widget.storage.readCounter().then((int value) {
setState(() {
_counter = value;
});
});
}
Future<File> _incrementCounter() {
setState(() {
_counter++;
});
// Write the variable as a string to the file.
return widget.storage.writeCounter(_counter);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Reading and Writing Files'),
),
body: Center(
child: Text(
'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
file_io_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:skeet25pro/main_counter.dart';
void main() {
test('Check file save works', () async {
final CounterStorage storage = CounterStorage();
var counter = 6;
var t = await storage.writeCounter(counter);
expect(1, 1);
});
}
When I run the app through a simulator, it works perfectly fine. I would really like to get the tests running.
EDIT: If I try and add WidgetsFlutterBinding.ensureInitialized();
void main() {
test('Check file save works', () async {
WidgetsFlutterBinding.ensureInitialized();
final CounterStorage storage = CounterStorage();
var counter = 6;
var t = await storage.writeCounter(counter);
expect(1, 1);
});
}
I get the error:
MissingPluginException(No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider_macos)
package:flutter/src/services/platform_channel.dart 175:7 MethodChannel._invokeMethod
Seems like one should use something like: setMockMethodCallHandler to intercept the call to the different directory providers. Still no working solution.
You have to mock the path_provider call and maybe put the WidgetsFlutterBinding.ensureInitialized(); at the beginning of main. I guess you want something like
Future<void> main() async {
TestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
const channel = MethodChannel(
'plugins.flutter.io/path_provider_macos',
);
channel.setMockMethodCallHandler((MethodCall methodCall) async {
switch (methodCall.method) {
case 'getApplicationDocumentsDirectory':
return "PATH_TO_MOCK_DIR";
default:
}
});
});
test('Check file save works', () async {
final CounterStorage storage = CounterStorage();
var counter = 6;
var t = await storage.writeCounter(counter);
expect(1, 1);
});
}```
Hello im trying to mock firebase messaging to get token but when i try to test i get some error,can someone help me to solve this error. This error occur only in testing and not in my emulator or mobile phone. Here is my setupFirebaseAuthMocks. Thank you
my test
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
void main() {
setupFirebaseAuthMocks();
late ProviderContainer container;
group('AuthenticationControllerTest -', () {
setUpAll(() async {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
registerThirdPartyServices();
});
tearDown(() {
unregisterThirdPartyServices();
//container.dispose();
});
});
Error
MissingPluginException(No implementation found for method Messaging#getToken on channel plugins.flutter.io/firebase_messaging)
here is the method im trying to call
Future<Result<Failure, bool>> registerUserFirebaseToken() async {
try {
log.i('Registering Firebase');
final fireBaseMessaging = FirebaseMessaging.instance;
final token = await fireBaseMessaging.getToken();
log.v('Firebase token: $token');
await api.post(
link: '${env.getValue(kAuthUrl)}users/auth/firebase',
body: {'token': token},
hasHeader: true,
);
return const Success(true);
} catch (e) {
return Error(Failure(message: 'Firebase registration went wrong, Please try again!', content: e.toString()));
}
}
For those having the same issue, there is an example of a Mock on the official firebase messaging Github
Depending on your Mockito's version, you may have to update this code a little bit.
Here is the Mock file I'm using with Mockito v5.3.2
// ignore_for_file: require_trailing_commas
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart';
import 'package:firebase_messaging_platform_interface/firebase_messaging_platform_interface.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
typedef Callback = Function(MethodCall call);
final MockFirebaseMessaging kMockMessagingPlatform = MockFirebaseMessaging();
Future<T> neverEndingFuture<T>() async {
// ignore: literal_only_boolean_expressions
while (true) {
await Future.delayed(const Duration(minutes: 5));
}
}
void setupFirebaseMessagingMocks() {
TestWidgetsFlutterBinding.ensureInitialized();
setupFirebaseCoreMocks();
// Mock Platform Interface Methods
// ignore: invalid_use_of_protected_member
when(kMockMessagingPlatform.delegateFor(app: anyNamed('app')))
.thenReturn(kMockMessagingPlatform);
// ignore: invalid_use_of_protected_member
when(kMockMessagingPlatform.setInitialValues(
isAutoInitEnabled: anyNamed('isAutoInitEnabled'),
)).thenReturn(kMockMessagingPlatform);
}
// Platform Interface Mock Classes
// FirebaseMessagingPlatform Mock
class MockFirebaseMessaging extends Mock
with MockPlatformInterfaceMixin
implements FirebaseMessagingPlatform {
MockFirebaseMessaging() {
TestFirebaseMessagingPlatform();
}
#override
bool get isAutoInitEnabled {
return super.noSuchMethod(Invocation.getter(#isAutoInitEnabled),
returnValue: true, returnValueForMissingStub: true) as bool;
}
#override
FirebaseMessagingPlatform delegateFor({FirebaseApp? app}) {
return super.noSuchMethod(
Invocation.method(#delegateFor, [], {#app: app}),
returnValue: TestFirebaseMessagingPlatform(),
returnValueForMissingStub: TestFirebaseMessagingPlatform(),
) as FirebaseMessagingPlatform;
}
#override
FirebaseMessagingPlatform setInitialValues({bool? isAutoInitEnabled}) {
return super.noSuchMethod(
Invocation.method(
#setInitialValues, [], {#isAutoInitEnabled: isAutoInitEnabled}),
returnValue: TestFirebaseMessagingPlatform(),
returnValueForMissingStub: TestFirebaseMessagingPlatform(),
) as FirebaseMessagingPlatform;
}
#override
Future<RemoteMessage?> getInitialMessage() {
return super.noSuchMethod(Invocation.method(#getInitialMessage, []),
returnValue: neverEndingFuture<RemoteMessage>(),
returnValueForMissingStub: neverEndingFuture<RemoteMessage>())
as Future<RemoteMessage?>;
}
#override
Future<void> deleteToken() {
return super.noSuchMethod(Invocation.method(#deleteToken, []),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as Future<void>;
}
#override
Future<String?> getAPNSToken() {
return super.noSuchMethod(Invocation.method(#getAPNSToken, []),
returnValue: Future<String>.value(''),
returnValueForMissingStub: Future<String>.value('')) as Future<String?>;
}
#override
Future<String> getToken({String? vapidKey}) {
return super.noSuchMethod(
Invocation.method(#getToken, [], {#vapidKey: vapidKey}),
returnValue: Future<String>.value(''),
returnValueForMissingStub: Future<String>.value('')) as Future<String>;
}
#override
Future<void> setAutoInitEnabled(bool? enabled) {
return super.noSuchMethod(Invocation.method(#setAutoInitEnabled, [enabled]),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as Future<void>;
}
#override
Stream<String> get onTokenRefresh {
return super.noSuchMethod(
Invocation.getter(#onTokenRefresh),
returnValue: const Stream<String>.empty(),
returnValueForMissingStub: const Stream<String>.empty(),
) as Stream<String>;
}
#override
Future<NotificationSettings> requestPermission(
{bool? alert = true,
bool? announcement = false,
bool? badge = true,
bool? carPlay = false,
bool? criticalAlert = false,
bool? provisional = false,
bool? sound = true}) {
return super.noSuchMethod(
Invocation.method(#requestPermission, [], {
#alert: alert,
#announcement: announcement,
#badge: badge,
#carPlay: carPlay,
#criticalAlert: criticalAlert,
#provisional: provisional,
#sound: sound
}),
returnValue: neverEndingFuture<NotificationSettings>(),
returnValueForMissingStub:
neverEndingFuture<NotificationSettings>())
as Future<NotificationSettings>;
}
#override
Future<void> subscribeToTopic(String? topic) {
return super.noSuchMethod(Invocation.method(#subscribeToTopic, [topic]),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as Future<void>;
}
#override
Future<void> unsubscribeFromTopic(String? topic) {
return super.noSuchMethod(Invocation.method(#unsubscribeFromTopic, [topic]),
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as Future<void>;
}
}
class TestFirebaseMessagingPlatform extends FirebaseMessagingPlatform {
TestFirebaseMessagingPlatform() : super();
}
and here is the unit test itself
void main() {
setupFirebaseMessagingMocks();
setUpAll(() async {
await Firebase.initializeApp();
FirebaseMessagingPlatform.instance = kMockMessagingPlatform;
});
test('An example of test', () {
//...
when(kMockMessagingPlatform.getToken(vapidKey: anyNamed('vapidKey')))
.thenAnswer(
(_) => Future.value('DEVICE_ID'),
);
//...
});
}
I have a backend Laravel application that uses Pusher for notifications. I would like to show notifications in my Flutter app (both iOS and Android). I found that https://pub.dev/packages/pusher_websocket_flutter/ package has the best score, but I can't get it to work. I've followed this tutorial, and I get no errors (whatever I put for my APP_KEY, which must be wrong), but I never get anything shown.
Has anyone managed to get this working, or should I switch to firebase?
This is my pusher_service.dart:
import 'package:flutter/services.dart';
import 'package:pusher_websocket_flutter/pusher.dart';
import 'dart:async';
class PusherService {
Event lastEvent;
String lastConnectionState;
Channel channel;
StreamController<String> _eventData = StreamController<String>();
Sink get _inEventData => _eventData.sink;
Stream get eventStream => _eventData.stream;
Future<void> initPusher() async {
try {
await Pusher.init('XXX', PusherOptions(cluster: 'XX'), enableLogging: true);
print("Pusher initialized");
}
on PlatformException catch (e) {
print(e.message);
}
}
void connectPusher() {
Pusher.connect(
onConnectionStateChange: (ConnectionStateChange connectionState) async {
lastConnectionState = connectionState.currentState;
print("Pusher connected");
}, onError: (ConnectionError e) {
print("Error: ${e.message}");
});
}
Future<void> subscribePusher(String channelName) async {
channel = await Pusher.subscribe(channelName);
print("Pusher subscribed to channel");
}
void unSubscribePusher(String channelName) {
Pusher.unsubscribe(channelName);
}
void bindEvent(String eventName) {
channel.bind(eventName, (last) {
final String data = last.data;
_inEventData.add(data);
});
print("Pusher data binded");
}
void unbindEvent(String eventName) {
channel.unbind(eventName);
_eventData.close();
}
Future<void> firePusher(String channelName, String eventName) async {
await initPusher();
connectPusher();
await subscribePusher(channelName);
bindEvent(eventName);
}
}
My pusher_test.dart:
import 'package:flutter/material.dart';
import 'package:chalet/services/pusher_service.dart';
import 'package:pusher/pusher.dart';
import 'dart:async';
class PusherTest extends StatefulWidget {
#override
_PusherTestState createState() => _PusherTestState();
}
class _PusherTestState extends State<PusherTest> {
PusherService pusherService = PusherService();
#override
void initState() {
pusherService = PusherService();
pusherService.firePusher('public', 'create');
testPusher();
super.initState();
}
#override
void dispose() {
pusherService.unbindEvent('create');
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: StreamBuilder(
stream: pusherService.eventStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return Container(
child: Text(snapshot.data),
);
},
),
),
);
}
}
I've checked and my snapshot.connectionState is always waiting.
Try this:
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:pusher_client/pusher_client.dart';
//instantiate Pusher Class
class PusherController {
static final PusherController _pusherController =
PusherController._internal();
factory PusherController() {
return _pusherController;
}
PusherController._internal();
PusherClient pusher;
Channel channel;
StreamController<String> _eventData = StreamController<String>.broadcast();
Sink get _inEventData => _eventData.sink;
Stream get eventStream => _eventData.stream;
String channelName = "";
String prevChannelName = "";
String eventName = "";
void initPusher() {
PusherOptions options = PusherOptions(
cluster: "eu",
);
pusher = new PusherClient("key", options,
autoConnect: true, enableLogging: true);
}
void setChannelName(String name) {
channelName = name;
print("channelName: ${channelName}");
}
void setEventName(String name) {
eventName = name;
print("eventName: ${eventName}");
}
void subscribePusher() {
channel = pusher.subscribe(channelName);
pusher.onConnectionStateChange((state) {
log("previousState: ${state.previousState}, currentState: ${state.currentState}");
});
pusher.onConnectionError((error) {
log("error: ${error.message}");
});
//Bind to listen for events called and sent to channel
channel.bind(eventName, (PusherEvent event) {
print("xxxxxxxxx From pusher xxxxxxxxx");
print('xxxxx This is Event name - $eventName xxxx');
print('xxxxx This is Event gotten - ${event.data} xxx');
_inEventData.add(event.data);
prevChannelName = eventName;
});
}
void connectPusher() {
pusher.connect();
}
void disconnectPusher() async {
await channel.unbind(eventName);
await pusher.disconnect();
}
}
Then use streamBuilder and stream from evenStream.