Hi everyone! We’d like to share our own plugin playground which allow us to test new ideas on Flutter. You can easily build this playground from scratch but doing it every time you need to test some stuff can be time consuming. Feel free to clone the repo and modify it to your own needs. This post will explain each step needed to create such a project by yourself.
If you haven’t written any platform specific code in Flutter before, you may checkout this tutorial as a start.
Related Topics
Take a look at these posts to learn about common communication patterns between Dart and native code if you already know what you are looking for.
- Flutter 101
- Flutter and Native Communication
- Flutter Plugin Playground
- Native Communication with a Callback using MethodChannel
- Listeners with EventChannel
Ingredients
- An app that shows logs and holds the playground
- A play button to run the playground code
- A pair of channels to communicate with Android or iOS native code
- A way to match channel invocations to native methods by name look up (via reflection)
The Usual Suspects
First, make sure you have a working flutter installation by running this command.
flutter doctor
Proceed to the steps shown on your terminal to fix your Flutter installation if necessary. Then, run the command below to create your playground project.
flutter create flutter_plugin_playground
Use these commands in your project folder whenever you need to make changes on the native side.
flutter devices #see the list of device and ids
flutter run -d <device_id> #run on device
Luckily, changing dart code automatically triggers a hot-swap which will allow us to see the changes immediately.
Open up your favorite IDE and you are good to go.
Playground UI
Open main.dart file to start implementing your playground front-end.
We will need Material UI styles to fashion up our widgets. Services will provide as the platform channels we need to communicate with the native side.
import 'package:flutter/material.dart'; import 'package:flutter/services.dart';
Let’s create our singleton channel first.
const channel = const MethodChannel('playground');
Before invoking our app main, we need to define a stateful widget to hold our code and logs. This widget will store logs in a widget local variable and provide utilities to automatically append logs with newlines. It will also include our main function to call when we run our playground.
class PlaygroundState extends State<Playground> { @override Widget build(BuildContext context) { // TODO : playground UI } /////////////// Playground /////////////////////////////////////////////////// String logs = ""; // Call inside a setState({ }) block to be able to reflect changes on screen void log(String logString) { logs += logString.toString() + "\n"; } // Main function called when playground is run void runPlayground() async { // This will not work until we implement a test() method on the native side var testResult = await channel.invokeMethod("test"); // Update state and UI setState(() { log(testResult); }); } }
Finally, it will show our logs and play button in a user friendly manner.
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text("Press button to run playground"), Text("-"), Text(logs) ], ), ), floatingActionButton: FloatingActionButton( onPressed: runPlayground, tooltip: 'Run Playground', child: Icon(Icons.play_arrow), ) ); }
Once our stateful widget is ready, we can attach it to our app with the following wrapper.
class Playground extends StatefulWidget { Playground({Key key, this.title}) : super(key: key); final String title; @override PlaygroundState createState() => PlaygroundState(); } class PlaygroundApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Plugin Playground', theme: ThemeData( primarySwatch: Colors.blue, ), home: Playground(title: 'Plugin Playground'), ); } }
Then, we can run our app with this simple line.
void main() => runApp(PlaygroundApp());
Android Native Plugin
Now that we have a working front-end with a properly defined channel, we can fill the missing pieces on Java to complete the circle.
Open MainActivity.java file.
Use the lines below when your IDE asks you to choose which classes to import.
import android.os.Bundle; import android.util.Log; import java.lang.reflect.Method; import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.view.FlutterView;
We will create a channel with the same name we used in the dart side.
public class MainActivity extends FlutterActivity { private MethodChannel channel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); // Prepare channel channel = new MethodChannel(getFlutterView(), "playground"); channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { // TODO : invoke native method } }); } }
When dart code calls invokeMethod()
on a channel, it specifies a method name. That name is accessed via methodCall.method
property. We can either compare that name in a switch block or allow java reflection to do that for us automatically.
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { try { // Find a method with the same name in activity Method method = MainActivity.class.getDeclaredMethod( methodCall.method, Object.class, MethodChannel.Result.class ); // Call method if exists method.setAccessible(true); method.invoke(MainActivity.this, methodCall.arguments, result); } catch (Throwable t) { Log.e("Playground", "Exception during channel invoke", t); result.error("Exception during channel invoke", t.getMessage(), null); } } });
This small piece of code will try to find an instance method on MainActivity
with a matching name and signature of void method(Object, MethodChannel.Result)
. If it succeeds, it will invoke that method by providing arguments from dart as the first argument and the result callback as the second.
Having the glue above will allow us to fix the broken playground code. Remember that in dart, we called the native code like this and expected a string in return.
// Main function called when playground is run void runPlayground() async { var testResult = await channel.invokeMethod("test"); setState(() { log(testResult); }); }
Having our glue, we can now define a test()
method on MainActivity
to automatically plug these pieces to each other.
public class MainActivity extends FlutterActivity { ... void test(Object args, MethodChannel.Result result) { result.success("YAY from Java!"); } }
Run your app and click the play button to see if it works.
iOS Native Plugin
Steps for iOS is quite similar to what we did in our MainActivity
. Let’s start editing our AppDelegate
.
First, we will create a channel with the same name we used on the dart side.
@implementation AppDelegate { FlutterMethodChannel* channel; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; // Prepare channel FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; self->channel = [FlutterMethodChannel methodChannelWithName:@"playground" binaryMessenger:controller]; __weak typeof(self) weakSelf = self; [self->channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { // TODO : invoke native method }]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }
When dart code calls invokeMethod()
on a channel, it specifies a method name. That name is accessed via call.method
property. We can either compare that name in an if-else block or allow Objective-C reflection to do that for us automatically.
__weak typeof(self) weakSelf = self; [self->channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { @try { // Find a method with the same name in activity SEL method = NSSelectorFromString([call.method stringByAppendingString:@":result:"]); // Call method if exists [weakSelf performSelector: method withObject:call.arguments withObject:result]; } @catch (NSException *exception) { NSLog(exception.description); result([FlutterError errorWithCode:@"Exception" message:exception.description details:nil]); } @finally { } }];
This small piece of code will try to find an instance function on AppDelegate
with a matching name and signature of -(void) method:(id)args result:(FlutterResult)result
. If it succeeds, it will invoke that function by providing arguments from dart as the first argument and the result callback as the second.
Having the glue above will allow us to fix the broken playground code. Remember that in dart, we called the native code like this and expected a string in return.
// Main function called when playground is run void runPlayground() async { var testResult = await channel.invokeMethod("test"); setState(() { log(testResult); }); }
Having our glue, we can now define a test:
function on AppDelegate
to automatically plug these pieces to each other.
- (void) test:(id)args result:(FlutterResult)result { result(@"YAY from Objective-C!"); } @end
Run your app and click the play button to see if it works.
What now?
From now on, whenever you have a native functionality to bridge to your app, you can clone this project and test your idea from scratch. Remember that you can add native Android and iOS dependencies to your project as well.
- In Android, edit android/app/build.gradle file.
- In iOS, install CocoaPods if you haven’t already.
- Then initiatialize the pods.
pod init
- Then, edit ios/Podfile to add your dependencies.
- Finally download them with the following command.
pod install
You can find the entire project here if you want to get started immediately.
P.S: If you are interested in a Swift version of this project, please let us know. Most of the time, implementing the same behavior in Swift translates quite similarly from the Objective-C implementation. You can find project creation docs for Swift oand Kotlin projects here.