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;
}
}
Related
After creating a PWA with react every thing works perfectly except when i want to open the app with a mobile device android/ios....it displays a white screen with no informations.
/////
The app is deployed with IIS manager
my service **service-worker.ts
**
`
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith('.png'),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.
const CACHE_NAME = 'cache_sample';
const urlsToCache = ['index.html', 'offline.html'];
const version = 'v0.0.1';
//install sw at first time
//place to cache assets to speed up the loading time of web page
self.addEventListener('install', (event: any) => {
console.log('sw install event');
event.waitUntil(
caches.open(version + CACHE_NAME).then((cache) => {
console.log('opened cache');
return cache.addAll(urlsToCache);
})
);
});
//Activate the sw after install
//Place where old caches are cleared
self.addEventListener('activate', (event: any) => {
console.log('sw activate event');
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames
.filter((cacheName) => {
return cacheName.indexOf(version) !== 0;
})
.map(function (cachName) {
return caches.delete(cachName);
})
)
)
);
});
//listen for requests
self.addEventListener('fetch', (event: any) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
// Any other custom service worker logic can go here.
`
PS : My start_url : "/" in the manifest.json and i've already set the basename attribute in my Router
I've been looking on how to get this done:
I have a contact list with an "add contact" button. My goal is to open the native add contact page (mainly for iOS) when I click on that button.
I know how to call the method from flutter, but I don't know how to do the calling add contact screen with swift.
Any suggestions on how to do that?
Thanks
Ok so I figured things out.
If someone needs it here's the code:
import UIKit
import Flutter
import Contacts
import ContactsUI
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
setUpMethodChannels(controller: controller)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func setUpMethodChannels(controller: FlutterViewController) {
let contactChannel = FlutterMethodChannel(name: "com.flutter.contact_list/addContact",
binaryMessenger: controller.binaryMessenger)
contactChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: #escaping FlutterResult) -> Void in
guard call.method == "AddContact" else {
result(FlutterMethodNotImplemented)
return
}
let con = CNContact()
let mainVC = CNContactViewController(forNewContact: con)
mainVC.delegate = controller
let navigationController = UINavigationController(rootViewController: mainVC)
self.window?.rootViewController?.present(navigationController, animated: true, completion: nil)
})
}
}
extension FlutterViewController : CNContactViewControllerDelegate, UINavigationControllerDelegate {
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
viewController.dismiss(animated: true, completion: nil)
}
}
I done this according to Xamarin documentation, just copy-pasted a code.
So, in App.xaml.cs I have a code like this:
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
protected override void OnStart()
{
if (!CheckPermisions())
{
AbortApp(3, "Missing required permissions!");
return ;
}
}
//[...]
public bool CheckPermisions()
{
Task<bool> v = performCheckPermisions();
if (v.Result)
initAppFolders();
return v.Result;
}
protected async Task<bool> performCheckPermisions()
{
// storage read
PermissionStatus status = await Xamarin.Essentials.Permissions.CheckStatusAsync<Permissions.StorageRead>();
if (status == PermissionStatus.Denied)
{
this.Context.ToLogger(EAppLogLevel.Warning, string.Format(" ! StorageRead: requesting..."));
status = await Permissions.RequestAsync<Permissions.StorageRead>();
}
if (status == PermissionStatus.Denied)
return false;
// storage write
status = await Xamarin.Essentials.Permissions.CheckStatusAsync<Permissions.StorageWrite>();
if (status == PermissionStatus.Denied)
{
this.Context.ToLogger(EAppLogLevel.Warning, string.Format(" ! StorageWrite: requesting..."));
status = await Permissions.RequestAsync<Permissions.StorageWrite>();
}
if (status == PermissionStatus.Denied)
return false;
return true; // Task.FromResult(true);
}
The problem is - when application started 1st time, so when the OS asks user for permissions it always hangs! :-(
And I do not understand - why?!
How to resolve this problem with hanging on 1st app run?
I tried to debug it but it never returns from await Permissions.RequestAsync<...>() back into debugger! :-
Of course - on OS request I clicked [Allow] in a UI prompt.
Unfortunately, I'm not sure - why it is not returning, it might be bug in VS2019 debugger or it might be bug in Xamarin... or maybe I'm doing something wrong.
Could you please advice?
Please note: I need exactly the blocking/synchronous call to permissions request! Application must stop and confirm if permissions granted. Without permissions - it must not even try to run.
Note: VS 2019 (16.6.5); Xamarin.Forms 4.7.0.1142; Xamarin.Essentials 1.5.3.2 - so, it seems all the latest.
Thanks.
PS.
Also I tried following variants:
Attempt# 1
Task<bool> tsk = performCheckPermisions();
bool result = false;
if (tsk.IsCompleted)
{
this.Context.ToLogger(EAppLogLevel.Info, string.Format(" . CheckPermisions: task completed without waiting..."));
result = tsk.Result;
}
else
{
TaskAwaiter<bool> aw = tsk.GetAwaiter();
int counter = 0;
while (!aw.IsCompleted)
{
Thread.Sleep(330);
counter++;
if ((counter % 10) == 0)
this.Context.ToLogger(EAppLogLevel.Info, string.Format(" . CheckPermisions: still waiting (#{0})...", counter));
if (counter > 100)
{
AbortApp(99, "Permissions were not comfirmed!");
return false;
}
}
result = aw.GetResult();
}
It simply hang because nor tsk.IsCompleted, nor aw.IsCompleted never became true despite user clicks to [Allow] button.
Attempt# 2
var task = Task.Run(async () => await performCheckPermisions());
if (task.IsFaulted && task.Exception != null)
{
throw task.Exception;
}
bool result = task.Result;
this.Context.ToLogger(EAppLogLevel.Info, string.Format(" ? CheckPermisions: {0}", result));
It reported System.AggregateException exception: Message=One or more errors occurred. (Permission request must be invoked on main thread.); Source=mscorlib.
Attempt# 3
bool result = false;
this.isCompleted = false;
MainThread.BeginInvokeOnMainThread(
async () => {
result = await performCheckPermisions();
this.isCompleted = true;
}
);
int counter = 0;
while (!this.isCompleted)
{
Thread.Sleep(330);
counter++;
if ((counter % 10) == 0)
this.Context.ToLogger(EAppLogLevel.Info, string.Format(" . CheckPermisions: still waiting (#{0})...", counter));
if (counter > 100)
{
AbortApp(99, "Permissions were not comfirmed within specified timeout!");
return false;
}
}
It simply hang. It seems there is bug in Xamarin - the await Permissions.RequestAsync<>() call never return back to application!
Below is the edited code. If you saw an earlier version then you saw it had problems. First entry so am new at this. *
I tried many things to get the permissions but it always hung the App, even with the awaits.
I wanted to put the permission requests as close to where the user required them (As recommended) and not abort the App. This is what I finally came up with:
Creating a permission interface in the Xamarin Forms project
Creating an Android implementation of the permissions in the Xamarin Forms Android project
Registered permission as dependency service in the Android Activity before loading the Forms App
In my WIFI Content page I created an async method that checks permission by calling the registered Dependency service
When I click on WIFI page scan button, it calls the async method to see if the user needs to give permission before continuing
Works like a charm.
The only caveat is that if the user selects 'Don't ask again' he will have to set location services manually. Not sure how I can tell the user since the permissions always only return Denied status. He will get a dialog informing him of insufficient permissions but no OS dialog allowing him to request permissions (Duh, because he said that that he did not want to see them)
The permissions Interface in the Xamarin Forms project
public interface ILocationWhileInUsePermission {
Task<PermissionStatus> CheckStatusAsync();
Task<PermissionStatus> RequestAsync();
}
Implementation on Xamarin Forms Android side
public class LocationWhileInUsePermission : Xamarin.Essentials.Permissions.BasePlatformPermission, ILocationWhileInUsePermission {
public override (string androidPermission, bool isRuntime)[]
RequiredPermissions => new List<(string androidPermission, bool isRuntime)> {
(Android.Manifest.Permission.AccessFineLocation, true),
(Android.Manifest.Permission.AccessCoarseLocation, true),
(Android.Manifest.Permission.AccessWifiState, true),
(Android.Manifest.Permission.ChangeWifiState, true)
}.ToArray();
}
Register in the Activity.cs OnCreate before load of the App
DependencyService.Register<ILocationWhileInUsePermission, LocationWhileInUsePermission>();
LoadApplication(new App(DI.Wrapper));
In the Wifi Page create functions to invoke permissions from DependencyService and to set results
private bool permissionsGranted = false;
private async Task SetAreGranted(bool granted) {
await Task.Run(() => this.permissionsGranted = granted);
}
public async Task<bool> GetIsGranted() {
return await Task<bool>.Run(() => { return this.permissionsGranted; });
}
public async Task<bool> ChkWifiPermissions() {
try {
await this.SetAreGranted(false);
var wifiPermissions =
DependencyService.Get<ILocationWhileInUsePermission>();
var status = await wifiPermissions.CheckStatusAsync();
if (status != PermissionStatus.Granted) {
status = await wifiPermissions.RequestAsync();
if (status != PermissionStatus.Granted) {
return await this.GetIsGranted();
}
}
await this.SetAreGranted(true);
}
catch (Exception) {
return await this.GetIsGranted();
}
return await this.GetIsGranted();
}
On my WIFI Content Page, on the button click event I call the async method
private void btnDiscover_Clicked(object sender, EventArgs e) {
Device.BeginInvokeOnMainThread(async () => {
if (await this.ChkWifiPermissions()) {
this.btnSelect.IsVisible = false;
this.ResetWifiList(new List<WifiNetworkInfo>());
this.activity.IsRunning = true;
App.Wrapper.WifiDiscoverAsync();
}
else {
this.OnErr("Insufficient permissions to continue");
}
});
}
Just to add a variance to my WIFI permissions check on WIFI scan button, here is a variance that aborts on App start. It works but I prefer the one that requests closer to the usage of the permission.
This works and never hangs the App. Still the problem if the user has requested not to be asked again.
Start by declaring service interfaces in the Xamarin Forms project to close the App, and another to check and request permissions
public interface ICloseApplication {
void CloseApp();
}
public interface ILocationWhileInUsePermission {
Task<PermissionStatus> CheckStatusAsync();
Task<PermissionStatus> RequestAsync();
}
Then add Android OS implmentations in the Xamarin Forms Android project
public class AndroidCloseApp : ICloseApplication {
public void CloseApp() {
Android.OS.Process.KillProcess(Android.OS.Process.MyPid());
}
}
public class LocationWhileInUsePermission : Xamarin.Essentials.Permissions.BasePlatformPermission, ILocationWhileInUsePermission {
public override (string androidPermission, bool isRuntime)[]
RequiredPermissions => new List<(string androidPermission, bool isRuntime)> {
(Android.Manifest.Permission.AccessFineLocation, true),
(Android.Manifest.Permission.AccessCoarseLocation, true),
(Android.Manifest.Permission.AccessWifiState, true),
(Android.Manifest.Permission.ChangeWifiState, true)
}.ToArray();
}
Register the Services in the Xamarin Forms Android project MainActivity.OnCreate(). BTW, the DI.Wrapper has the results from my dependency injector, with common and OS specific code
DependencyService.Register<ILocationWhileInUsePermission, LocationWhileInUsePermission>();
DependencyService.Register<ICloseApplication, AndroidCloseApp>();
LoadApplication(new App(DI.Wrapper));
Then in the Xamarin Forms project, in the App.OnStart() override method call an async method to request permissions and abort if necessary
protected override void OnStart() {
// This will abort the app at the start if the WIFI permissions are not given
Device.BeginInvokeOnMainThread(async () => {
if (!await this.CheckPermissions()) {
ICloseApplication closeApp = DependencyService.Get<ICloseApplication>();
await Application.Current.MainPage.DisplayAlert(
App.GetText(MsgCode.Error),
"Insufficient permissions",
App.GetText(MsgCode.Ok));
closeApp.CloseApp();
}
});
}
private async Task<bool> CheckPermissions() {
ILocationWhileInUsePermission wifiPermissions =
DependencyService.Get<ILocationWhileInUsePermission>();
PermissionStatus status = await wifiPermissions.CheckStatusAsync();
if (status != PermissionStatus.Granted) {
status = await wifiPermissions.RequestAsync();
}
return status == PermissionStatus.Granted;
}
As Cheesebaron mentioned, you always want to use await when dealing with a Task. You can modify your example like so:
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
protected override async void OnStart()
{
bool result = await CheckPermisions()
if (!result)
{
AbortApp(3, "Missing required permissions!");
return ;
}
}
//[...]
public async Task<bool> CheckPermisions()
{
bool v = await performCheckPermisions();
if (v)
initAppFolders();
return v;
}
protected async Task<bool> performCheckPermisions()
{
// storage read
PermissionStatus status = await Xamarin.Essentials.Permissions.CheckStatusAsync<Permissions.StorageRead>();
if (status == PermissionStatus.Denied)
{
this.Context.ToLogger(EAppLogLevel.Warning, string.Format(" ! StorageRead: requesting..."));
status = await Permissions.RequestAsync<Permissions.StorageRead>();
}
if (status == PermissionStatus.Denied)
return false;
// storage write
status = await Xamarin.Essentials.Permissions.CheckStatusAsync<Permissions.StorageWrite>();
if (status == PermissionStatus.Denied)
{
this.Context.ToLogger(EAppLogLevel.Warning, string.Format(" ! StorageWrite: requesting..."));
status = await Permissions.RequestAsync<Permissions.StorageWrite>();
}
if (status == PermissionStatus.Denied)
return false;
return true; // Task.FromResult(true);
}
}
I want to create a document locally, upload it to Firestore, let the server code run and get the result from this. How can I do that?
My example is working fine, but the result of my startNewGame function does not wait for the server script to finish and returns old data (the server script did not yet update the document). After refreshing the data, my app retrieves the correct data (modified by the server script).
Does someone have an idea how to solve this?
My startNewGame function also uploads the game, this means, the final result should be the modified version from the server, shouldn't it?
Code
fun startNewGame(): Single<GameModel> {
var model = GameModel(UserManager.user!!.uid, null)
// 1) create game
var res: Single<DocumentReference> = Single.create({ emitter ->
MainApp.db.collection(Constants.FIREBASE_GAME).add(model)
.addOnCompleteListener({ task ->
if (!emitter.isDisposed) {
if (task.isSuccessful()) {
emitter.onSuccess(task.result)
} else {
emitter.onError(Throwable(task.getException()))
}
}
})
})
// 2) upload game
res = res.flatMap { game -> uploadGame(game) }
return res
}
private fun uploadGame(game: GameModel): Single<GameModel> {
return Single.create<GameModel>({ emitter ->
game.doc!!.set(game).addOnCompleteListener({ task ->
if (!emitter.isDisposed) {
if (task.isSuccessful()) {
emitter.onSuccess(game)
} else {
emitter.onError(Throwable(task.getException()))
}
}
})
})
}
Simple test server script:
exports.findGame = functions.firestore
.document('/games/{gameId}')
.onCreate(event => {
const newData = event.data.data();
var user1 = newData.player1
console.log('New game created by user ' + newData.player1 + '!');
// returning the promise should have the result, that we wait for the function
return event.data.ref.set({
player2: 'SOME PLAYER 2'
}, {merge: true}).then(res => {
console.log('Player 2 set');
return res;
})
});
Instead of using addOnCompleteListener try addSnapshotListener. When the data is updated, your app should automatically get the update.
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.