Build Your Own Flutter Plugin Using Android Native Kotlin — Part II

Kathryn
Geek Culture
Published in
7 min readMay 30, 2021

In the part I of this tutorial, we’ve explained how to initiate a Flutter Plugin project in IntelliJ IDEA, and what does a Flutter Plugin template look like.

Next, we are going to complete the content of codes to bring the Flutter API and the MIDI keyboard app to live. The file structures were introduced in the Part I tutorial, please refer to part I tutorial.

piano.dart

Let’s start from the Flutter API piano.dart under the lib. Again, this will be your Flutter API interface, which defines all the callable functions for your clients to call:

import 'dart:async';
import 'package:flutter/services.dart';
class Piano {
static const MethodChannel _channel = // 1
const MethodChannel("piano");
static Future<String?> get platformVersion async { // 2
final String? version = await _channel.invokeMethod('getPlatformVersion'); // 3
return version; // 4
}
static Future<int?> onKeyDown(int key) async { // 2
final int? numNotesOn = await _channel.invokeMethod('onKeyDown', [key]); // 3
return numNotesOn; // 4
}
static Future<int?> onKeyUp(int key) async { // 2
final int? numNotesOn = await
_channel.invokeMethod('onKeyUp', [key]); // 3
return numNotesOn; // 4
}
}

Note: The question mark in the dart syntax is supporting Null Safety dart version. Basically it’s a stricter type check, which requires you to explicitly identify the type can be ‘int’ or ‘null’, e.g., int?

If you want to migrate your Dart to null safety version, simply go to each top level folder of a Flutter project, and in the terminal, do dart migrate --apply-changes. For example, in our plugin project, there are 2 Flutter projects, i.e., the Plugin folder itself, and the example folder.

>> cd <your-project-dir>
>> dart migrate --apply-changes
>> cd <your-project-dir>/example
>> dart migrate --apply-changes

This API is fairly simple:

  1. First, it create a MethodChannel instance _channel which registers the channel name as piano.
  2. Defined the callable functions and Getter for your client to call: platformVersion (getter), onKeyDown (function), onKeyUp (function).
  3. These functions invokeMethod( <plugin-method-name>, [args]) with the method name identified in the pianoPlugin.kt
  4. Simply return the client with what was returned from the invokeMethod.

So this API simply provides an interface for your client to call android native functions, written in Kotlin:

Your client requests methods call from API, i.e., in piano.dart, and API goes ahead to forward the methods call to MethodChannel factory, i.e., from pianoPlugin.kt, and returns the client with what is returned from the MethodChannel. This is all the API interface does.

PianoPlugin.kt

The PianoPlugin class inherited from FlutterPlugin class and extends the MethodCallHandler interface.

package com.example.piano

import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar

import com.example.piano.Synth

/** PianoPlugin */
class PianoPlugin: FlutterPlugin, MethodCallHandler {
/////////////////////// Part 1 ///////////////////////////// private lateinit var channel : MethodChannel
private lateinit var synth : Synth
/////////////////////// Part 2 /////////////////////////////override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "piano")
channel.setMethodCallHandler(this)
Factory.setup(this, flutterPluginBinding.binaryMessenger)
}

/////////////////////// Part 3 /////////////////////////////
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") {
result.success(android.os.Build.VERSION.RELEASE)
} else if (call.method == "onKeyDown"){
try {
val arguments: ArrayList<Int> = call.arguments as ArrayList<Int>
val numKeysDown: Int = synth.keyDown(arguments.get(0) as Int)
result.success(numKeysDown)
} catch (ex: Exception) {
result.error("1", ex.message, ex.getStackTrace())
}
} else if (call.method == "onKeyUp") {
try {
val arguments: ArrayList<Int> = call.arguments as ArrayList<Int>
val numKeysDown: Int = synth.keyUp(arguments.get(0) as Int)
result.success(numKeysDown)
} catch (ex: Exception) {
result.error("1", ex.message, ex.getStackTrace())
}
} else {
result.notImplemented()
}
}
/////////////////////// Part 4/////////////////////////////override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
/////////////////////// Part 5/////////////////////////////private companion object Factory {
fun setup(plugin: PianoPlugin, binaryMessenger: BinaryMessenger) {
plugin.synth = Synth()
plugin.synth.start()
}
}
}

It does several things:

override fun onAttachedToEngine (*override from FlutterPlugin)

  • Generate a channel from MethodChannel constructer to communicate with Flutter. Note that the channel name “piano” is the same as step //1 in piano.dart.
  • Set the channel’s MethodCallHandler to be the current class instance (with implementation under override fun onMethodCall)
  • Initiate a Singleton object, i.e., synth, of Synth class (See Synth class in Synth.kt)

(Optional: Read example on how to set MethodChannel from official doc.)

override fun onDetachedFromEngine (*override from FlutterPlugin)

  • Unregister the the MethodCallHandler to null

override fun onMethodCall (*override from MethodCallHandler)

  • if (call.method == “getPlatformVersion”)
  • if (call.method == “onKeyDown”), call keyDown method from synth
  • if (call.method == “onKeyUp”)), call keyUp method from synth

companion object Factory

  • “companion object” is to create a Singleton object in the class.
  • Define the initialization of the Synth class (which is a Runnable).

Synth.kt

package com.example.piano

import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack

/** A synthesizer that plays sin waves for Android. */
class Synth : Runnable {
private lateinit var mThread: Thread
private var mRunning = false
private var mFreq = 440.0
private var mAmp = 0.0
private var mNumKeysDown = 0


fun start() {
mThread = Thread(this)
mRunning = true
mThread.start()
}

fun stop() {
mRunning = false
}

fun keyDown(key: Int): Int {
mFreq = Math.pow(1.0594630f.toDouble(), key.toDouble() - 69.0) * 440.0
mAmp = 1.0
mNumKeysDown += 1
return mNumKeysDown
}

fun keyUp(key: Int): Int {
mAmp = 0.0
mNumKeysDown -= 1
return mNumKeysDown
}

override fun run() {
val sampleRate = 44100
val bufferSize = 1024
val buffer = ShortArray(bufferSize)
val audioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM
)
val fSampleRate = sampleRate.toDouble()
val pi2: Double = 2.0 * Math.PI
var counter = 0.0
audioTrack.play()
while (mRunning) {
val tau = pi2 * mFreq / fSampleRate
val maxValue = Short.MAX_VALUE * mAmp
for (i in 0 until bufferSize) {
buffer[i] = (Math.sin(tau * counter) * maxValue).toInt().toShort()
counter += 1.0
}
audioTrack.write(buffer, 0, bufferSize)
}
audioTrack.stop()
audioTrack.release()

}
}

This class calls Android Media API: android.media

fun start()

  • Initiate a new thread that runs the procedures defined in run().

fun keyDown()

  • Set the audio amplitude to 1.0
  • Calculate the frequency of the Sine wave corresponding to the key note.
    (Optional: Read how the pitch of a sound has to do with Sine wave article.)

fun keyUp()

  • Set the audio amplitude to 0.0

override fun run()

  • It generates the Sine wave values into a buffer array.
  • Write the buffer values to audioTrack to play the sound.
  • Stop playing and release the memory.
  • Note that this thread is constantly running, and keeps generating values to the buffer array. However, when keyUp, the amplitude is set to 0.0, and thus the written value is 0, so no sound would be heard.

example/lib/main.dart

Now, we have finished the API functionalities, and the corresponding native android codes. Next, we will add a Flutter front-end UI /example/lib/main.dart to display the piano keyboard and call the Flutter API piano.dart when the keys are pressed (onKeyDown) and lifted (onKeyUp).

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:piano/piano.dart';


enum _KeyType { Black, White }

void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight])
.then((_) {
runApp(new MyApp());
});
}

class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp>
{
String? _platformVersion = 'Unknown';

@override
void initState() {
super.initState();
initPlatformState();
}

// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String? platformVersion;
try {
platformVersion = await Piano.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}

if (!mounted) return;

setState(() {
_platformVersion = platformVersion;
});
}

void _onKeyDown(int key) {
print("key down:$key");
Piano.onKeyDown(key).then((value) => print(value));
}

void _onKeyUp(int key) {
print("key up:$key");
Piano.onKeyUp(key).then((value) => print(value));
}

Widget _makeKey({required _KeyType keyType, required int key}) {
return AnimatedContainer(
height: 200,
width: 44,
duration: Duration(seconds: 2),
curve: Curves.easeIn,
child: Material(
color: keyType == _KeyType.White
? Colors.white
: Color.fromARGB(255, 60, 60, 80),
child: InkWell(
onTap: () => _onKeyUp(key),
onTapDown: (details) => _onKeyDown(key),
onTapCancel: () => _onKeyUp(key),
),
),
);
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color.fromARGB(255, 250, 30, 0),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('Running on: $_platformVersion\n'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_makeKey(keyType: _KeyType.White, key: 60),
_makeKey(keyType: _KeyType.Black, key: 61),
_makeKey(keyType: _KeyType.White, key: 62),
_makeKey(keyType: _KeyType.Black, key: 63),
_makeKey(keyType: _KeyType.White, key: 64),
_makeKey(keyType: _KeyType.White, key: 65),
_makeKey(keyType: _KeyType.Black, key: 66),
_makeKey(keyType: _KeyType.White, key: 67),
_makeKey(keyType: _KeyType.Black, key: 68),
_makeKey(keyType: _KeyType.White, key: 69),
_makeKey(keyType: _KeyType.Black, key: 70),
_makeKey(keyType: _KeyType.White, key: 71),
],
)
],
),
),
),
);
}
}

This demo app is a straight forward Flutter front end widget, which creates the keyboard layout through _makeKey() Widget, and defines the _onKeyUp() and _onKeyDown() behaviour that calls the corresponding Flutter API functions in piano.dart.

Build and Run On an Emulator or Phone

Now, you can select an emulator, and chose example/lib/main.dart as the entry configuration to run on the virtual emulator. Click ‘Build’ (the hammer icon on the left) and then Run (the triangle button on the right).

You shall see the demo running on the virtual device.

Should you want to test the app on your mobile phone, you can follow this instruction to download the app to your phone, and run it from there.

In this tutorial, we went through the code implementation of this PianoPlugin, which communicates between Flutter (written in Dart) and native Android code (written in Kotlin) through MethodChannel. We also go through how a Flutter front-end app calls the Flutter API to access the methods defined in PianoPlugin.

The original tutorial can be found here. This current tutorial is a Kotlin equivalent of the original one (in Java).

Thank you for reading through this tutorial! You are welcome to share your thoughts in the comment! Happy coding!

--

--