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”]
Related
I am getting this error:This expression has a type of 'void' so its value can't be used.
Try checking to see if you're using the correct API; there might be a function or call that returns void you didn't expect. Also check type parameters and variables which might also be void.
code:
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_hot/angel_hot.dart';
import 'package:logging/logging.dart';
import 'dart:async';
import 'package:mongo_dart/mongo_dart.dart';
main() async {
var hot = HotReloader(createServer, ['main.dart']);
await hot.startServer('127.0.0.1', 3000);
}
Future<Angel> createServer() async {
var app = Angel();
app.logger = Logger('Log')..onRecord.listen((event) => print(event));
print('start server..');
Db db = Db('mongodb://localhost:27017/wearina');
await db.open();
print('connected to ${db.databaseName}');
DbCollection userscoll = DbCollection(db, 'users');
print('${userscoll.collectionName}');
app.post('/signup', (req, res) async {
var body = await req.parseBody(); //// parseBody => Future<void> , I want => Future<Map> ):
var name = body['name'];
var lastname = body['lastname'];
var email = body['email'];
var phone = body['phone'];
var pass = body['pass'];
});
return app;
}
I don't understand what this is. I am new to flutter. This is my first app. Can someone please help me with this.
It seems that parseBody does not return a map of the pasred body. It just makes sure the body is parsed and you can access it from the req.bodyAsMap property. So your line should be:
await req.parseBody();
var body = req.bodyAsMap;
Im working on my first Ionic + Firebase project, and im not understanding this:
Im searching and getting an object from firebase, I can access its details on html and show it to the user.
But now I need to save the createdBy field on that object so I can use it to search for its creator on firebase.
But when I try to access that info its always undefined. Why is that? Any tips on how to fix this?
export class VisitDetailsPage implements OnInit {
public trips: Observable<HomeTripCardsModel>;
public trip: HomeTripCardsModel;
public buddyInfo;
public targetBuddyId: any;
constructor(private router: Router, private navCtrl: NavController,
public fireStorageService: FireStorageService,
private route: ActivatedRoute, public db: AngularFirestore) {
}
ngOnInit() {
const tripId: string = this.route.snapshot.paramMap.get('id');
this.db.collection('users').get()
.subscribe(querySnapshot => {
querySnapshot.forEach(doc => {
this.trips = this.fireStorageService.getTripDetail(tripId, doc.id);
this.trips.forEach((element: HomeTripCardsModel) => {
if (element?.id === tripId) {
this.trip = element;
this.targetBuddyId = element.createdBy;
}
});
});
});
// buddy
console.log(this.trip?.createdBy); // returns undefined
console.log('saved ', this.targetBuddyId) // returns undefined
}}
Data is loaded from Firebase asynchronously. If you set some breakpoints and run in the debugger, or add a log inside the subscribe method, you'll see that your console.log(this.trip?.createdBy) runs before this.trip = element has ever been run. So at that point, it indeed doesn't have a value yet.
For this reason, all code that needs data from the database, needs ot be inside the subscribe callback:
this.db.collection('users').get()
.subscribe(querySnapshot => {
querySnapshot.forEach(doc => {
this.trips = this.fireStorageService.getTripDetail(tripId, doc.id);
this.trips.forEach((element: HomeTripCardsModel) => {
if (element?.id === tripId) {
this.trip = element;
this.targetBuddyId = element.createdBy;
}
});
// buddy
console.log(this.trip?.createdBy); // returns undefined
console.log('saved ', this.targetBuddyId) // returns undefined
});
});
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;
}
}
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);
}
I read the introduction to platform-specific plugins/channels on the Flutter website and I browsed some simple examples of a plugin, like url_launcher:
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
const _channel = const MethodChannel('plugins.flutter.io/url_launcher');
/// Parses the specified URL string and delegates handling of it to the
/// underlying platform.
///
/// The returned future completes with a [PlatformException] on invalid URLs and
/// schemes which cannot be handled, that is when [canLaunch] would complete
/// with false.
Future<Null> launch(String urlString) {
return _channel.invokeMethod(
'launch',
urlString,
);
}
In widgets tests or integration tests, how can I mock out or stub channels so I don't have to rely on the real device (running Android or iOS) say, actually launching a URL?
MethodChannel#setMockMethodCallHandler is deprecated and removed as of now.
Looks like this is the way to go now:
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void mockUrlLauncher() {
const channel = MethodChannel('plugins.flutter.io/url_launcher');
handler(MethodCall methodCall) async {
if (methodCall.method == 'yourMethod') {
return 42;
}
return null;
}
TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, handler);
}
The details are on GitHub.
And here is a tested example for package_info plugin for future references:
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void mockPackageInfo() {
const channel = MethodChannel('plugins.flutter.io/package_info');
handler(MethodCall methodCall) async {
if (methodCall.method == 'getAll') {
return <String, dynamic>{
'appName': 'myapp',
'packageName': 'com.mycompany.myapp',
'version': '0.0.1',
'buildNumber': '1'
};
}
return null;
}
TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, handler);
}
You can use setMockMethodCallHandler to register a mock handler for the underlying method channel:
https://docs.flutter.io/flutter/services/MethodChannel/setMockMethodCallHandler.html
final List<MethodCall> log = <MethodCall>[];
MethodChannel channel = const MethodChannel('plugins.flutter.io/url_launcher');
// Register the mock handler.
channel.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
await launch("http://example.com/");
expect(log, equals(<MethodCall>[new MethodCall('launch', "http://example.com/")]));
// Unregister the mock handler.
channel.setMockMethodCallHandler(null);
When you create a plugin, you are automatically provided a default test:
void main() {
const MethodChannel channel = MethodChannel('my_plugin');
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('getPlatformVersion', () async {
expect(await MyPlugin.platformVersion, '42');
});
}
Let me add some notes about it:
Calling setMockMethodCallHandler allows you to bypass whatever the actual plugin does and return your own value.
You can differentiate methods using methodCall.method, which is a string of the called method name.
For plugin creators this is a way to verify the public API names, but it does not test the functionality of the API. You need to use integration tests for that.