How can I mock/stub out a Flutter platform channel/plugin? - android

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.

Related

How to call MethodChannel from an isolate created by android_alarm_manager?

I tried to schedule background service. For that i used https://pub.dev/packages/android_alarm_manager. It works well.
For my example, I tried to get from my isolate (android_alarm_manager's callback) the battery level following the flutter tutorial : https://flutter.dev/docs/development/platform-integration/platform-channels?tab=android-channel-java-tab.
If I call manualy my callback it works (so I well do the Android part). If android_alarm_manager call it, I got the following error appear :
Unhandled Exception: MissingPluginException(No implementation found for method getBatteryLevel on channel net.example.com/battery)
It's weird because, from an other isolate where I used https://pub.dev/packages/flutter_downloader to download file, this plugin used MethodChannel...
Here is my code for android_alarm_manager :
import 'package:flutter/services.dart';
class AndroidManagerCallBack {
static Future<void> main() async {
_AndroidManagerCallBack test = _AndroidManagerCallBack();
await test.getBatteryLevel();
}
}
class _AndroidManagerCallBack {
static const platform = const MethodChannel('net.example.com/battery');
Future<void> getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
print(batteryLevel);
}
}
I simply call the callback like :
AndroidAlarmManager.periodic(
Duration(seconds: 20),
0,
AndroidManagerCallBack.main(),
rescheduleOnReboot: true,
wakeup: false,
);
In android_alarm_manager's callback, I can call plugins which used somee MethodChannel but when I tried with my MethodChannel, I got errors...
Someone can guide me :) ?
It seem impossible to call directly MethodChannel through an isolate.
But, with the creation of a plugin i can achieve what i want. So the solution is to create a plugin :) !

flutter open file externally such as on ios "open in"

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

Flutter plugin: invoking iOS and Android method including parameters not working

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”]

React Native check if app notification enabled on Android

User is able to turn off app notification on Android. Is there a way to check if the user did so?
The React Native doc did not mention Notification permission as it's not a permission that requires prompting the user.
Android docs did mention the ability to access notification policy.
I have used this one in previous experiences with good results:
https://github.com/jigaryadav/react-native-check-notification-enable
Basically what I ended up implementing as helpers were this two functions:
export const getNotificationStatus = async () => {
if (Platform.OS === 'ios') {
const status = await Permissions.check(PermissionRequested.NOTIFICATION)
if (status === PermissionsStatus.AUTHORIZED) {
return true
} else {
return false
}
} else {
// This is where I am using the library
const status = await NotificationManager.areNotificationsEnabled()
return status
}
}
export const openNotificationsSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:')
} else {
AndroidOpenSettings.appNotificationSettings()
}
}
Just to explain a bit the whole snippet:
I am also using these libraries in conjunction to achieve all: Check if they are enabled and navigate to the settings page to revoke if needed
import AndroidOpenSettings from 'react-native-android-open-settings'
import NotificationManager from 'react-native-check-notification-enable'
import Permissions from 'react-native-permissions'
I have found one library called React-native-permission-settings
It is available for android only so if you need iOS support also i think you need to modify it.
You can follow installation guide from here
And implement it in your code
import NotificationSettings from 'react-native-permission-settings';
...
NotificationSettings.areNotificationsEnabled((isEnabled: boolean) => {
console.log(`Notifications are enabled: ${isEnabled}`);
});

Can I call Kotlin function from Dart

Using Flutter, a kotlin/swift function can be called by something like:
file.dart:
static const platform = const MethodChannel('my.test.flutterapp/battery');
final int result = await platform.invokeMethod('getBatteryLevel');
file.kt:
private val CHANNEL = "my.test.flutterapp/battery"
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
...
} else {
result.notImplemented()
}
}
Is there something similar to call Kotlin function from standard Dart app, like Dart console app!
https://flutter.io/developing-packages/
Plugin packages: A specialized Dart package which contain an API written in Dart code combined with a platform-specific implementation for Android (using Java or Kotlin), and/or for iOS (using ObjC or Swift). A concrete example is the battery plugin package.
...
flutter create --template=plugin -i swift -a kotlin hello
For the VM the mechanisms available are basic OS operations and native extensions.
By OS operations I mean, you could launch a separate process and interact with it, using files or the network stack. This is probably not as fine grained as you're looking for.
Native extensions allow you to call out to C or C++ code. I don't know enough about kotlin to know if you can easily expose functionality to C/C++. If it's possible, this will give you the tightest integration.
https://www.dartlang.org/articles/dart-vm/native-extensions
You can see this project: Full Sample
In Andriod:
class MainActivity: FlutterActivity() {
private val DATA_CHANNEL = "app.channel.shared.data"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), DATA_CHANNEL).setMethodCallHandler { call, result ->
if (call.method!!.contentEquals("getSharedText")) {
result.success("Shared Text")
}
}
}
}
In Dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('app.channel.shared.data');
String dataShared = "No Data";
#override
void initState() {
super.initState();
getSharedText();
}
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(body: Center(child: Text(dataShared)))
);
}
}

Categories

Resources