How would you pass info from Flutter back to Android/Native code if needed to interact with a specific API / hardware component?
Are there any Event Channels that can send info the other way or something similar to a callback?
The platform_channel documentation points out "method calls can also be sent in the reverse direction, with the platform acting as client to methods implemented in Dart. A concrete example of this is the quick_actions plugin." I don't see how the native side is receiving a message from Flutter in this instance.
It looks like a BasicMessageChannel’s send() method can be used to send "the specified message to the platform plugins on this channel". Can anyone provide a simple implementation example of this?
This is a simple implementation showcasing:
Passing a string Value from flutter to Android code
Getting back response from Android code to flutter
Code is based on example from: https://flutter.io/platform-channels/#codec
Passing string value "text":
String text = "whatever";
Future<Null> _getBatteryLevel(text) async {
String batteryLevel;
try {
final String result = await platform.invokeMethod('getBatteryLevel',{"text":text});
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
Getting back response "batterylevel" after RandomFunction();
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.equals("getBatteryLevel")) {
text = call.argument("text");
String batteryLevel = RandomFunction(text);
if (batteryLevel != null) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
Objective C
call.arguments[#"parameter"]
Android
call.argument("parameter");
Yes, flutter does has an EventChannel class which is what you are looking for exactly.
Here is an example of that demonstrates how MethodChannel and EventChannel can be implemented. And
this medium article shows how an EventChannel can be implemented in flutter.
Hope that helped!
for swift
guard let args = call.arguments as? [String : Any] else {return}
let phoneNumber = args["contactNumber"] as! String
let originalMessage = args["message"] as! String
Passing from Flutter to native:
await platform.invokeMethod('runModel',
{'path': imagePath!.path} // 'path' is the key here to be passed to Native side
);
For Android (Kotlin):
val hashMap = call.arguments as HashMap<*,*> //Get the arguments as a HashMap
val path = hashMap["path"] //Get the argument based on the key passed from Flutter
For iOS (Swift):
guard let args = call.arguments as? [String : Any] else {return}
let text = args["path"] as! String
If anyone wants to share the data from native to flutter with invoke method
follow this:
main.dart
Future<dynamic> handlePlatformChannelMethods() async {
platform.setMethodCallHandler((methodCall) async {
if (methodCall.method == "nativeToFlutter") {
String text = methodCall.arguments;
List<String> result = text.split(' ');
String user = result[0];
String message = result[1];
}
}
}
MainActivity.java
nativeToFlutter(text1:String?,text2:String?){
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger,
CHANNEL.invokeMethod("nativeToFlutter",text1+" "+text2);
}
Related
I am trying to store data received via FCM inside a class in order to navigate to a specific tab of my app after a user clicks on that notification.
My problem is that, as far as I could find on the web, the MainActivity is stopped when the app is not in the Foreground, and so, when I try and retrieve this data, I am not getting the updated variables. I have checked this using print statements throughout the app.
In order to store the information and use it when the app is brought back up, do I need to create a local database, or is there another way around this problem?
PS: I have a Stream that receives information that the user has clicked on the notification, and it updates the main page, but I cannot retrieve anything else from it, as it itself doesn't receive the json.
Thank you. Also, this was my first question posted here, be gentle if I didn't follow the protocol by the letter.
Sample code below.
Initialization:
FirebaseOption options = FirebaseOptions(
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXx',
appId: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
messagingSenderId: 'XXXXXXXXXXXXXX',
projectId: 'XXXXXXXXXXXXx',
);
Future<void> backgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(
options: options,
);
var decoded = await NotificationModelPusherAG.fromJson(message.data);
var encodedMessage = await json.decode(decoded.message);
var decodedMessage = await PusherMessage.fromJson(encodedMessage);
notifications.type = message.data;
FirebaseNotifications.showNotification(decodedMessage.title, decodedMessage.description, message.data);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final String INSTANCE_ID = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
await PusherBeams.start(INSTANCE_ID);
await Firebase.initializeApp(
options: options,
);
/// Run this funk when a notification is received and app is in BG
FirebaseMessaging.onBackgroundMessage(backgroundHandler);
runApp(MyApp());
}
The class in which the data is stored:
class Notifications {
var tab = 0;
int get returnTab => return tab;
final _notificationUpdateController = BehaviorSubject();
Stream get update => _notificationUpdateController.stream;
shouldUpdate(add) {
_notificationUpdateController.sink.add(add);
}
void set type(messageData) {
if (messageData['type'] == 'xxxxxxx') {
this.tab = 1;
}
}
var notifications = Notifications();
The widget that should update:
StreamBuilder<Object>(
stream: notifications.update,
builder: (context, snapshot) {
if (updateMulti == true) {
print(notifications.returnTab); /// Here it is '0'
return multiScreen;
} else {
return multiScreen;
}
}
);
And the function that updates it:
flutterNotificationPlugin.initialize(
initSettings,
onSelectNotification: onSelectNotification
);
static Future onSelectNotification(String payload) {
print(payload); /// The payload is always null for some reason
print(notifications.returnTab); /// Here it shows '1' as it should
updateMulti = true;
notifications.shouldUpdate(true);
}
I kind of shortened the code a bit, if I missed something important do tell me, and I shall update accordingly.
Thank you again.
From what I can tell most of the flutter guides out there can open from local storage, but nothing about file sharing. Anybody know how to do this. This is a guide in enabling it specifically for ios https://developer.apple.com/library/archive/qa/qa1587/_index.html.
I mean there is the https://pub.dartlang.org/packages/open_file extension, but opens from the file storage.
To clarify this question isn't about sharing a file from the app with another, but when sharing from an external app being prompted to open in this flutter app.
To do this in iOS you first define the Document Types and Imported UTIs in XCode as described in the guide you mentioned, and then in your AppDelegate.m file you do:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
/* custom code begin */
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* myChannel = [FlutterMethodChannel
methodChannelWithName:#"my/file"
binaryMessenger:controller];
__block NSURL *initialURL = launchOptions[UIApplicationLaunchOptionsURLKey];
[myChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if ([#"checkintent" isEqualToString:call.method]) {
if (initialURL) {
[myChannel invokeMethod:#"loaded" arguments: [initialURL absoluteString]];
initialURL = nil;
result(#TRUE);
}
}
}];
/* custom code end */
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
On the Dart side:
class PlayTextPageState extends State<MyHomePage> with WidgetsBindingObserver{
static const platform = const MethodChannel('my/file');
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
platform.setMethodCallHandler((MethodCall call) async {
String method = call.method;
if (method == 'loaded') {
String path = call.arguments; // this is the path
...
}
});
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
...
} else if (state == AppLifecycleState.resumed) {
platform.invokeMethod("checkintent")
.then((result) {
// result == 1 if the app was opened with a file
});
}
}
}
Adding on to lastant's answer, you actually also need to override application(_:open:options:) in AppDelegate.swift for this to work.
So the idea is to use UIActivityViewController in iOS to open a file in Flutter (eg: restore a backup of the SQL DB into the Flutter app from an email).
First, you need to set the UTIs in the info.plist. Here's a good link to explain how that works. https://www.raywenderlich.com/813044-uiactivityviewcontroller-tutorial-sharing-data
Second, add the channel controller code in AppDelegate.swift.
We also need to override application(:open:options:) in AppDelegate.swift because iOS will invoke application(:open:options:) when an external application wants to send your application a file. Hence we store the filename as a variable inside AppDelegate.
Here we are have a 2-way channel controller between iOS and Flutter. Everytime the Flutter app enter the AppLifecycleState.resumed state, it will invoke "checkIntent" to check back into AppDelegate to see if the filename has been set. If a filename has been set, AppDelegate will invoke the "load" method in flutter whereby you do your required processing with the file.
Remember to delete the file given to you from AppDelegate after you are done with your processing. Otherwise, it will bloat up your application.
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var initialURL: URL?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
/* channel controller code */
let controller: FlutterViewController = self.window?.rootViewController as! FlutterViewController
let myChannel = FlutterMethodChannel(name: "my/file", binaryMessenger: controller.binaryMessenger)
myChannel.setMethodCallHandler({(call: FlutterMethodCall, result: #escaping FlutterResult)-> Void in
if(call.method == "checkintent"){
if(self.initialURL != nil){
myChannel.invokeMethod("loaded", arguments: self.initialURL?.absoluteString );
self.initialURL = nil;
result(true);
} else{
print("initialURL is null");
}
} else{
print("no such channel method");
}
});
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
print("import URL: \(url)");
initialURL = url;
// should not remove here.. remove after i get into flutter...
// try? FileManager.default.removeItem(at: url);
return true;
}
}
Trying my first Flutter plugin, I try to invoke a method in both, the iOS and the Android-world. I successfully was able to invoke such a method without any parameters.
But now I would like to invoke a method that has parameters.
For iOS, I can't get it to work for some reason. (maybe it is just an autocomplete thing that I keep overseeing since VSCode is not autocompleting my Swift code). But maybe it is something else. Please any help on this.
Here is my code:
My lib (Flutter-world) looks like this:
import 'dart:async';
import 'package:flutter/services.dart';
class SomeName {
static const MethodChannel _channel =
const MethodChannel('myTestMethod');
static Future<String> get sendParamsTest async {
final String version = await _channel.invokeMethod('sendParams',<String, dynamic>{
'someInfo1': "test123",
'someInfo2': "hello",
});
return version;
}
}
.
My swift plugin (iOS-world) looks like this:
import Flutter
import UIKit
public class SwiftSomeNamePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "myTestMethod", binaryMessenger: registrar.messenger())
let instance = SwiftSomeNamePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
// flutter cmds dispatched on iOS device :
if call.method == "sendParams" {
guard let args = call.arguments else {
result("iOS could not recognize flutter arguments in method: (sendParams)")
}
String someInfo1 = args["someInfo1"]
String someInfo2 = args["someInfo2"]
print(someInfo1)
print(someInfo2)
result("Params received on iOS = \(someInfo1), \(someInfo2)")
} else {
result("Flutter method not implemented on iOS")
}
}
}
The error messages say:
note: add arguments after the type to construct a value of the type
String someInfo1 = args["someInfo1"]
note: add arguments after the type to construct a value of the type
String someInfo2 = args["someInfo2"]
note: use '.self' to reference the type object
String someInfo1 = args["someInfo1"]
note: use '.self' to reference the type object
String someInfo2 = args["someInfo2"]
warning: expression of type 'String.Type' is unused
String someInfo1 = args["someInfo1"]
warning: expression of type 'String.Type' is unused
String someInfo2 = args["someInfo2"]
With the help of miguelpruivo, I found the solution.
Here is the working code:
The Flutter-world in Dart was correct:
import 'dart:async';
import 'package:flutter/services.dart';
class SomeName {
static const MethodChannel _channel =
const MethodChannel('myTestMethod');
static Future<String> get sendParamsTest async {
final String version = await _channel.invokeMethod('sendParams',<String, dynamic>{
'someInfo1': "test123",
'someInfo2': 3.22,
});
return version;
}
}
.
And here below, the iOS-world in Swift - now working as well...
(Dart's dynamic corresponds to Swift's Any)
(the method parameter is a dictionary of type [String:Any] - kind of like Swift's often used userInfo - therefore you need to cast at the receiver handler)...
import Flutter
import UIKit
public class SwiftSomeNamePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "myTestMethod", binaryMessenger: registrar.messenger())
let instance = SwiftSomeNamePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: #escaping FlutterResult) {
// flutter cmds dispatched on iOS device :
if call.method == "sendParams" {
guard let args = call.arguments else {
return
}
if let myArgs = args as? [String: Any],
let someInfo1 = myArgs["someInfo1"] as? String,
let someInfo2 = myArgs["someInfo2"] as? Double {
result("Params received on iOS = \(someInfo1), \(someInfo2)")
} else {
result(FlutterError(code: "-1", message: "iOS could not extract " +
"flutter arguments in method: (sendParams)", details: nil))
}
} else if call.method == "getPlatformVersion" {
result("Running on: iOS " + UIDevice.current.systemVersion)
} else {
result(FlutterMethodNotImplemented)
}
}
}
This looks like a swift syntax error.
You want to do let someInfo1 : String = args[“someInfo1”]
So i made this lambda function, and in that function I made an if statement, which creates me a user in a user table in dynamoDB. How do i call that ONLY call that if statement in my lambda function from android?
Here is my lambda function
exports.handler = function(event, context)
{
console.log('stageA');
console.log(JSON.stringify(event, null, ' '));
var responseCode = 200;
var userTableName = "usersTable";
var requestBody = event.body;
var pathParams = event.path;
var httpMethod = event.httpMethod; // HTTP Method (e.g., POST, GET, HEAD)
//User parameters
var displayName;
var email;
var fbUserID;
var firstName;
var folders;
var lastName;
var origin;
var profileImageRef;
var level;
var username;
var birthdate;
var experience;
var folder;
if (pathParams == "/user/createByEmail" && httpMethod == "POST")
{
console.log('create by email action');
requestBody = JSON.parse(requestBody);
//Set variables
firstName = requestBody.firstName;
lastName = requestBody.lastName;
email = requestBody.email;
username = requestBody.username;
experience = "0";
birthdate = requestBody.birthdate;
dynamodb.putItem(
{
"TableName": userTableName,
"Item" :
{
"displayName":{"S": username},
"email":{"S": email},
"firstName" : {"S" : firstName},
"folderNames" : {"M" : {
"My Cards": {"N": "0" }
} },
//"folders" : {"M" : {"My Cards": {}}},
"lastName" : {"S" : lastName},
"experience" : {"N" : experience},
"username": {"S": username},
"birthdate": {"S": birthdate}
}
},
function(err, data)
{ if (err) {
console.log(err);
context.done(err);
} else {
var response =
{
statusCode: responseCode,
headers:
{
"x-custom-header" : "custom header value"
},
body: JSON.stringify(username)
};
console.log('great success: %j',data);
context.succeed(response);
}
});
}
And in android, i made an interface which will be called in an AsyncTask to call my lambda function:
public interface MyInterface {
/**
* Invoke lambda function "echo". The function name is the method name
*/
#LambdaFunction
String user(String userEmail);
/**
* Invoke lambda function "echo". The functionName in the annotation
* overrides the default which is the method name
*/
#LambdaFunction(functionName = "myUserAPiFxnName")
void noUser(NameInfo nameInfo);
}
I am new to AWS and lambda, and really hope for your guide. I am not sure if i am doing this write, and hope someone has a clear cut way with steps!
Cheers!
it seems as though you would use Cognito to map user identities to IAM roles providing temporary access privileges to call the function directly. I'm not using cognito in production, nor developing Android, but my cognito does two things for you: one, it provides authentication and authorization with many federated identity services ( facebook, google, saml, oath2, etc ), and two, it provides a user access model so you don't have to program one yourself. you can pull displayName, email, birthdate from the identity service, and not have to handle logins. You can go straight to the part where you make your program.
You'd configure an identity pool in cognito and attach some identity provider(s), and invoke apis of first the identity providers and then AWS. Finally, your application would be able to invoke the lambda functions you write.
Not knowing anything about android, I'd guess there was some user identity you already had access to, probably a google identity.
Is there any way to unsubscribe from all topics at once?
I'm using Firebase Messaging to receive push notification from some topics subscribed, and somehow I need to unsubscribe from all topics without unsubscribing one by one. Is that possible?
You can use Instance API to query all the available topics subscribed to given token and than call multiple request to unsubscribe from all the topics.
However, if you want to stop receiving from all the topics and then the token is not useful at all, you can call FirebaseInstanceId.getInstance().deleteInstanceId() (reference: deleteInstanceId() and surround with a try/catch for a potential IOException) that will reset the instance id and than again you can subscribe to new topics from the new instance id and token.
Hope this helps someone.
If you want to avoid deleting InstanceId and moreover, to avoid missing some of the subscribed topics when saved in a local database or a remote database due to highly probable buggy implementation.
First get all subscribed topics:
var options = BaseOptions(headers: {'Authorization':'key = YOUR_KEY'});
var dio = Dio(options);
var response = await dio.get('https://iid.googleapis.com/iid/info/' + token,
queryParameters: {'details': true});
Map<String, dynamic> subscribedTopics = response.data['rel']['topics'];
Get your key here:
Firebase console -> your project -> project settings -> cloud messaging -> server key
Get your token as:
var firebaseMessaging = FirebaseMessaging();
String token;
firebaseMessaging.getToken().then((value) {
token = value;
});
Now unsubscribe from all topics:
Future<void> unsubscribeFromAllTopics() async {
for (var entry in subscribedTopics.entries) {
await Future.delayed(Duration(milliseconds: 100)); // throttle due to 3000 QPS limit
unawaited(firebaseMessaging.unsubscribeFromTopic(entry.key)); // import pedantic for unawaited
debugPrint('Unsubscribed from: ' + entry.key);
}
}
All code is in Dart.
For more information about instance id:
https://developers.google.com/instance-id/reference/server
For Java users:
If you want to do it topic wise, refer others answers and If you want to stop recieving FCM push notification, do below:
new Thread(new Runnable() {
#Override
public void run() {
try {
FirebaseInstanceId.getInstance().deleteInstanceId();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
I have placed deleteInstanceId() in a separate thread to stop java.io.IOException: MAIN_THREAD W/System.err and wrapped with try / catch to handle IOException.
I know this is not the best way but it works!
You can store list of all topics in Database and then unsubscribe from all topics when user sign-outs
final FirebaseMessaging messaging= FirebaseMessaging.getInstance();
FirebaseDatabase.getInstance().getReference().child("topics").addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
String topic = dataSnapshot.getValue(String.class);
messaging.unsubscribeFromTopic(topic);
}...//rest code
Keep a private list of subscribed topics in Preferences.
It's not that hard. Here's what I do:
public class PushMessagingSubscription {
private static SharedPreferences topics;
public static void init(ApplicationSingleton applicationSingleton) {
topics = applicationSingleton.getSharedPreferences("pushMessagingSubscription", 0);
}
public static void subscribeTopic(String topic) {
if (topics.contains(topic)) return; // Don't re-subscribe
topics.edit().putBoolean(topic, true).apply();
// Go on and subscribe ...
}
public static void unsubscribeAllTopics() {
for (String topic : topics.getAll().keySet()) {
FirebaseMessaging.getInstance().unsubscribeFromTopic(topic);
}
topics.edit().clear().apply();
// FirebaseInstanceId.getInstance().deleteInstanceId();
}
}
We can use the unsubcribeAllTopics in server side as below.
Example,
interface GetTopics {
rel: {topics: {[key: string]: any}}
}
/**
* Unsubcribe all topics except one topic
*
* Example response of `https://iid.googleapis.com/iid/info/${fcmToken}?details=true`
* {
"applicationVersion": "...",
"application": "...",
"scope": "*",
"authorizedEntity": "...",
"rel": {
"topics": {
"TOPIC_KEY_STRING": { // topic key
"addDate": "2020-12-23"
}
}
},
"appSigner": "...",
"platform": "ANDROID"
}
*/
export const unsubcribeAllTopics = async (
fcmTokens: string | string[],
exceptTopic?: string,
) => {
const headers = {
'Content-Type': 'application/json',
Authorization: `key=${process.env.FCM_SERVER_KEY}`,
}
const url = `https://iid.googleapis.com/iid/info/${fcmTokens}?details=true`
try {
const response = await fetch(url, {method: 'GET', headers: headers})
const result: GetTopics = await response.json()
const keys = Object.keys(result.rel.topics)
keys.forEach(key => {
key !== exceptTopic &&
messaging()
.unsubscribeFromTopic(fcmTokens, key)
.catch(error => {
console.error('error', {data: error})
})
})
} catch (error) {
console.error('error', {data: error})
}
}
https://gist.github.com/JeffGuKang/62c280356b5632ccbb6cf146e2bc4b9d
if you are using flutter if you want to unsubscribe from all topics use-
final FirebaseMessaging _fcm = FirebaseMessaging();
_fcm.deleteInstanceID().then((value){
print('deleted all'+value.toString());
});
Firebase.messaging.deleteToken()
Is actual answer for 2021.
try {
FirebaseInstallations.getInstance().delete()
} catch (e: IOException) {
}