- 投稿日:2019-07-16T00:48:41+09:00
アプリ開発にGoを利用する(Android/iOS/Flutter)
この記事の読み方
- Android/iOS + Go
- Flutter + Android/iOS ネイティブ
- その組み合わせ
この3つから成る記事です。
複数のアプリを作りながら手順等を確認していきます。Flutter は使わない、Go は知らない、といった方にも参考にしていただけると思います。
Android 開発者の方
Flutter を使わない方は Android + Go までをご覧ください。iOS 開発者の方
iOS 開発環境がなく未検証のため、iOS の情報は少なめです。
特に Flutter で使うために Objective-C/Swift で橋渡しする部分はほぼありません。
それでも、Android での MethodChannel に近いと思われますので、雰囲気は掴めるはずです。
ライブラリ作成自体や Dart/Flutter で使う部分は OS に関わらず共通です。Flutter 開発者の方
Go を使わない方は Flutter + Android と Flutter + iOS をご覧ください。細かなことは 付録 にまとめましたので、そちらも参考になさってください。
gomobileとは
Go をモバイルアプリ開発に活用できるという素晴らしい代物です。
Mobile · golang/go Wiki · GitHubこれを使って作れるアプリは二種類あります。
ネイティブアプリ
Go だけで作るネイティブアプリ。SDKアプリ
Go で作ったライブラリを使って作るアプリ。この記事で扱うのは SDK アプリのほうです。
SDKアプリ
gomobile によって Go のパッケージを基にバインディングが行われてライブラリ化されます。
Kotlin、Swift 等からライブラリを使えるだけでなく、逆方向に呼び出すこともできます。ライブラリとして生成されるのは次のファイルです。
Android
aar ファイル(Android Archive)iOS
framework ファイル(Framework Bundle)Android では ARM / ARM64 / 386 / AMD64 のアーキテクチャに対応しています。
MIPS は非対応です。Flutter を使い始めるまでは Android/iOS のロジックをこれで共通化して楽をしようと考えていました。
Goを使う理由
Dart でやりにくいことを Go に任せられる
Go には有用なパッケージがあるのに、相当するものが Dart にない場合など。Go が得意なことを Dart/Flutter に持ち込める
Go は簡単に使える便利な標準ライブラリが豊富です。
Goroutine による並行処理も得意です。
サーバサイドで人気の Go をアプリで使えればコードを流用できます。実行速度の優位性
Flutter では Dart のコードがネイティブのライブラリにコンパイルされる 1 ので速度に大きな差はなさそうに思えますが、試してみると違いがありました。2C/C++ より扱いやすい
C/C++ など Go 以外の言語でもライブラリは作れます。
でも Go ならシンプルな文法、GC 等によって楽をして安全に書けます。Dart より Go に慣れている人が書きやすい
Dart を使ってみると Web でも使ってみたくなるような素敵な言語でしたが、好みの問題や人的リソースの都合があるので・・・。楽しい!
楽しい Go と楽しい Dart/Flutter を組み合わせて使えるなんて至福(著者調べ)。Goを使うデメリット
- Android NDK が必要(Android のみ)3
- アプリのサイズが大きくなる
- 言語間のバインディングにオーバーヘッドがある 4
- ターゲット言語側の制限により、エクスポートされた API の見た目に少し制限がある 4 5
- 使える型が限られている
- ライブラリ内に作った環境のパスが含まれる 6
- gomobile 製ライブラリと Flutter を繋ぐ Java/Kotlin、Objective-C/Swift のコードも必要
- Flutter がせっかくマルチプラットフォーム対応なのに Dart 以外も使うなんて面倒
- ウェブアプリも作れる Flutter で Go を使うとモバイルアプリ限定になってしまう
- ライブラリには
compute()を使えず、重い処理だとメインスレッドがブロックされるこう見ると結構ありますね。
メリットとデメリットのどちらが大きいか、ご自身で判断ください。準備
Windows での手順になりますが、他の環境でもほぼ同じだと思います。7
Android/iOS の開発環境は既に用意されている前提です。
Android NDK のインストール(Android のみ)
Android Studio にて
Tools>SDK Manager> 右ペインのSDK Toolsタブ
⇒NDKにチェックが付いていなければ付けてOKまたはApplygomobile のインストール
コマンドプロンプトか PowerShell にて
> go get -d golang.org/x/mobile/example/bind/...
> gomobile initこれだけです。
-ndk /path/to/ndkという NDK のパス指定を説明しているサイトがありますが、> gomobile init -ndk /path/to/ndk flag provided but not defined: -ndkのように怒られました。
NDK のパスを指定する必要はないようです。8
Windows 以外では未確認ですので、もし NDK のパスのエラーが出たら指定してみてください。Goによるライブラリ作成
1. コード
非常にシンプルなライブラリを作ってみます。
わざわざ Go でライブラリにしたい類ではありませんが、あくまでわかりやすい例として。
- 整数を受け取り、倍にした値を返す
- 受け取る整数の範囲は 0 ~ 10 とする
- 範囲外の値ならエラーを返す
- 値を LogCat で確認できるように出力
simple.gopackage simple import "fmt" func Multiply(value int32) (int32, error) { fmt.Println(value) if value < 0 || value > 10 { return 0, fmt.Errorf("value out of range: must be within the range of 0 to 10") } return value * 2, nil }
- これを
GOPATH以下のどこかに作ったフォルダの中に置く- パッケージ名がライブラリの名前になる(フォルダ名は関係ない)
- Android/iOS のコードや Dart/Flutter から利用したい関数は、先頭を大文字にして export する
→ Android で使うときは先頭は小文字、先頭以外は Go で書いたまま
[例] Go で GetHoge なら Android で使うときは getHoge(iOS では異なるようです)関数にコメントを付けておいても、ライブラリの使用時にその情報を参照することはできませんでした。
整数型
Go の int はアーキテクチャに依存し、64 ビット実装の Go では 64 ビット の整数になります。
それに対応する Java と Objective-C の型は それぞれLong、numberWithLongです。
Integer、numberWithIntにするには、より小さなサイズの int32 等を使いましょう。型の対応 については付録にまとめています。
情報出力とエラーの扱い
複数の方法で動作を見てみると、かなり癖がありました。
基本的に次のように考えておけば大丈夫かと思います。
詳細は 付録 をご覧ください。
情報を LogCat や Run のウィンドウに表示したい
fmt.Println()を使う。
fmt.Print()やfmt.Printf()で第一引数の末尾に改行するのも OK。Android/iOS や Dart/Flutter で例外として捕捉したい
ライブラリで値を返すとき、二つ目の戻り値にerror型のデータを付ける。他のポイント
長くなるので 付録 に収めました。
2. ライブラリ生成
Android では aar ファイル、iOS では framework ファイルを生成します。
次のようなパスになっているとします。
GOPATH
C:\Gosimple.go
C:\Go\src\hoge\gomobile_example\simple.go生成には
gomobile bindを使います。
ファイルのあるディレクトリを指定する方法と指定しない方法があります。
Android 向けに生成する場合は下のようになります。コマンド
(a) ディレクトリへの相対パスを指定して生成する場合
※最後の引数は GOPATH/src/ からの相対パス です。
※Windows でもスラッシュ区切りにしないとエラーになりました。> gomobile bind -target android hoge/gomobile_example(b) ディレクトリに移動してから生成する場合
> cd C:\Go\src\hoge\gomobile_example\simple.go > gomobile bind -target androidオプション
-o
出力先を指定するには-oを使います(例: -o path/to/library.aar)。
ここで指定するパスは カレントディレクトリからの相対パス ですのでご注意ください。
(a) のほうでは相対パスの起点がややこしいので (b) がオススメです。
なお、パスにはファイル名まで含める必要があります。
また、存在しないディレクトリを指定した場合、自動的に作ってくれるわけではありません。-target android/arm64
Android ではターゲットのアーキテクチャも指定できます。
スラッシュの後ろはarm、arm64、386、amd64のいずれかです。
指定しない場合、サポートする全4アーキテクチャの so ファイルを含んだ aar ファイルになります。9-target ios
iOS では-targetにiosを指定します。
Android のようなアーキテクチャの指定には対応していないようです。
そもそも幅広いバリエーションがあるわけでもないので不要ですね。他
オプションは他にもあり、gomobile bind -hで確認できます。ビルド時間、aarファイルのサイズ
これくらい小規模のコードを普段 Go でビルドするときと比べて長くかかります。
環境によりますが、私の PC で Android 向けにビルドしたところ 40 秒ほどでした。また、aar 内の共有ライブラリ(.so)が一つあたり 2MB 以上、圧縮状態で 1MB 程度になりました。
aar ファイルには 4 アーキテクチャ分が入っていて計 4MB 台です。大きめですね。
ユーザが Google Play ストアからダウンロードするときにはもっと小さくなります。9Android + Go
ライブラリ導入
Android Studio を使っていきます。
使わずに、次の 1 ~ 2 に載せた diff を参考にしてファイル追加や記述変更を手動で行っても OK です。1. ライブラリのモジュールを追加
モジュールとは、プロジェクトを分割した機能ごとのアプリのようなものです。
ここでは、ライブラリを一つのモジュールとしてプロジェクトに追加します。
起動後のウィンドウで「Start a new Android Studio project」を選ぶ
開いたウィザードで「Empty Activity」を選び、プロジェクトが開くところまで進める
フォルダアイコンを押し、先ほど生成された aar ファイルを指定してから「Finish」
Subproject name のところには自動的にサブプロジェクト(モジュール)の名前が入ります。
自分で変えても良いでしょう。
これで simple モジュールが追加された状態になりました。
ここまでの操作による変化は次のとおりです(Android Studio 関連ファイルは省いています)。
settings.gradle-include ':app' +include ':app', ':simple'simple/build.gradlenew file mode 100644 +configurations.maybeCreate("default") +artifacts.add("default", file('simple.aar')) \ No newline at end of filesimple/simple.aarnew file mode 1006442. 追加したモジュールを使う設定
追加しただけでは使えません。
メインのモジュールである app から simple を利用できるように依存関係の設定を行います。
使うための設定はこれで完了です。
この操作による変化は次のとおりです。app/build.gradledependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation project(path: ':simple') }ライブラリを使う
Simple ライブラリを実際に使ったアプリを作ります。
app/src/main/res/values/strings.xml<resources> <string name="app_name">GomobileAndroid</string> <string name="button">Tap here!</string> </resources>app/src/main/res/layout/activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="32sp"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button"/> </LinearLayout>app/src/main/java/com/example/gomobile/gomobileandroid/MainActivity.ktimport simple.Simple // これ以外のインポートは割愛 class MainActivity : AppCompatActivity() { private lateinit var textView: TextView private var value = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById(R.id.textView) updateText() findViewById<Button>(R.id.button).setOnClickListener { value++ updateText() } } private fun updateText() { try { textView.text = Simple.multiply(value).toString() } catch (e: Exception) { Log.e("MainActivity", e.message) } } }とても簡単ですね。
ポイントは下記箇所のみです。import simple.Simple .... try { textView.text = Simple.multiply(value).toString() } catch (e: Exception) { Log.e("MainActivity", e.message) }ライブラリのメソッドを使っているだけです。
そのメソッドではエラー時に例外を発生させるようにしているためtry~catchを使っています。ボタンを 12 回押したときの LogCat の出力は下のようになります(途中省略)。
端末画面上の表示は 10 回目の「20」で止まります。07-14 13:58:37.951 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 1 07-14 13:58:38.246 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 2 ... 07-14 13:58:41.244 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 10 07-14 13:58:41.630 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 11 07-14 13:58:41.635 1622-1622/com.example.gomobile.gomobileandroid E/MainActivity: value out of range: must be within the range of 0 to 10 07-14 13:59:00.962 1622-1688/com.example.gomobile.gomobileandroid I/GoLog: 12 07-14 13:59:00.966 1622-1622/com.example.gomobile.gomobileandroid E/MainActivity: value out of range: must be within the range of 0 to 10ライブラリを使う記述をする際のコード補完等については 付録 をご覧ください。
iOS + Go
iOS 開発環境がないため動作は未確認です。
ライブラリ導入
生成した framework ファイルを Xcode でプロジェクトに導入する方法は Wiki に書かれています。
参考にしながら導入してみてください。ライブラリを使う
Wiki には挨拶のテキストを出力するサンプルコードのスクリーンショットがあります。
ライブラリを利用するためのメソッド使用箇所は次のようになっています。bind/ViewController.m(スクショより)textLabel.text = GoHelloGreetings(@"iOS and Gopher");しかし こちらのサンプル ではメソッド名が異なります。
bind/ViewController.m(サンプルより)textLabel.text = HelloGreetings(@"iOS and Gopher");いずれかの情報がアップデートされていなくて古いのかもしれません。
なお、Go で書いた関数 は下記のとおりです。
上記の二つのメソッド名はどちらも、この元の関数名と異なります。
iOS で使うときにはその点の注意が必要です。hello/hello.goの一部func Greetings(name string) string { return fmt.Sprintf("Hello, %s!", name) }他にも異なる部分があるかもしれませんので、サンプル全体を一度ご確認ください。
Flutter + Android
Go 製ライブラリを Flutter で使う前に、Android 側に書いた機能を Flutter で使う方法を見てみます。
Flutter と Android/iOS の間で連携できるようにする Platform Channel というものを使います。この図は上記リンク先より拝借したものです。
Platform Channelというのはこの全体の仕組みのことだと思われます。
使うのはMethodChannel(iOS 側だけはFlutterMethodChannel)というものです。1. Android側(使う機能の作成)
Go で作ったライブラリと同様の機能にしてみます。
まず Flutter の新しいプロジェクトを作りますが、今回も simple という名前にしておきます。Kotlin をサポートするプロジェクトにするには、Android Studio のウィザードで「Include Kotlin support for Android code」にチェックを付けるか、
flutter createコマンドで-a kotlinを付けます。private fun multiply(value: Int): Int? { return if (value in 0..10) value * 2 else null }受け取る値が 0 ~ 10 の範囲なら倍数、範囲外なら null を返します。
2. Android側(連携処理)
作ったメソッドを Flutter から使えるように Android 側に連携処理を書きます。
そのために用意されているMethodChannelを使います。android/app/src/main/kotlin/com/example/simple/MainActivity.ktclass MainActivity: FlutterActivity() { ... val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 val r = multiply(v) result.success(r) } else -> result.notImplemented() } } ... }
MethodChannel(flutterView, "example.com/simple")
第二引数のexample.com/simpleはチャンネルの名前です。
Flutter のほうでも同じ名前を使うことでやり取りできるようになります。call.method == "simple_multiply"
simple_multiplyは Flutter から機能を呼び出すときの名前です。
使いたいメソッドがmultiply()なので、それを使うことがわかる名前にしました。
そのようなわかりやすい名前であれば何でも大丈夫です。call.argument("value")
Flutter 側から渡された引数を取り出す部分です。
valueという引数名を Android と Flutter で共通使用する必要があります。
受け取った値はnullの場合もあるため、そのことを考慮しておく必要があります。result.success(~)
成功したときに結果を返す処理です。
( ) 内に指定した値を Flutter 側で受け取ることができます。result.error("エラーコード", "エラーメッセージ", "エラー詳細")
上のコードにはありませんが、これを使うと Flutter 側でPlatformExceptionになります。
各引数に指定する情報を Flutter で取得できます。
第3引数は Object 型なので String に限りません(使わないなら null で OK)。result.notImplemented()
存在しない名前で機能を呼び出された場合にこれを使っています。
このとき Flutter 側でMissingPluginExceptionとして捕捉することができます。MainActivity 全体のコードは次のようになります。
android/app/src/main/kotlin/com/example/simple/MainActivity.ktpackage com.example.simple import android.os.Bundle import io.flutter.app.FlutterActivity import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith(this) val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 val r = multiply(v) if (r == null) { result.error("Out of range", "value must be within the range of 0 to 10", v) } else { result.success(r) } } else -> result.notImplemented() } } } private fun multiply(value: Int): Int? { return if (value in 0..10) value * 2 else null } }
multiply()の結果がnullのときはresult.error()でエラーにしています。3. Flutter側(ヘルパークラス)
Flutter 側でも
MethodChannelを使います。
使うためにはpackage:flutter/services.dartのインポートが必要です。UI のコードにロジックが混じらないように、Simple というヘルパークラスを作ることにしました。
lib/simple.dartimport 'package:flutter/services.dart'; class Simple { static const _platform = MethodChannel('example.com/simple'); static Future<int> multiply(int count) async { final arguments = {'value': count}; try { return await _platform.invokeMethod<int>('simple_multiply', arguments); } on PlatformException catch (e) { print(e); } catch (e) { print(e); } return null; } }
MethodChannel('example.com/simple')
Android 側で設定したのと同じチャンネル名を指定します。Future<int> multiply(int count) async
Android 側から返ってくるのはFutureです。final arguments = {'value': count};
Android 側に値を渡すには、このように Map にする必要があります。
キーは Android 側で設定した名前に合わせます。return await platform.invokeMethod('simplemultiply', arguments);
第一引数は Android 側で設定した呼び出し名です。
第二引数には渡したい引数の Map を指定します。
awaitはここでしないと例外を補足できません。
「invoke」で始まるメソッドは他にinvokeListMethod()とinvokeMapMethod()があります。on PlatformException catch (e)
Android 側でresult.error()に指定した情報をここで得ることができます。
- e.code エラーコード
- e.message エラーメッセージ
- e.details エラー詳細
4. Flutter側(完成)
ヘルパークラスを使うメインのファイルは次のようにしました(一部省略)。
lib/main.dartimport 'simple.dart'; ... class _MyAppState extends State<MyApp> { int _count = 0; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FutureBuilder<int>( future: Simple.multiply(_count), initialData: 0, builder: (_, snapshot) { return Text(snapshot.hasData ? snapshot.data.toString() : '--'); }, ), RaisedButton( onPressed: () { setState(() => ++_count); }, child: const Text('Tap Here!'), ), ], ), ), ), ); } }カウンターの値を Dart で持ち、ボタンが押されたときにインクリメントします。
その際にsetState()しているため全体がリビルドされます。
都度Simple.multiply(_count)が実行され、返ってきたFutureをFutureBuilderで処理しています。
multiply()に渡す値が 11 以上だとエラーになり、Flutter 側では例外が発生します。
アプリを起動してボタンを 11 回押すと、Run ウィンドウに次のように出力されました。I/flutter ( 3664): PlatformException(Out of range, value must be within the range of 0 to 10, 11)例外については、付録 にもう少し細かく書いています。
Hot Reload/Restart
Hot Reload/Restart は Flutter の機能です。
Android 側の処理を変えたときには当然 Hot Restart しても反映されません。しかし Android 側はしっかりと書いてしまえばその後はあまり変えることはないはずです。
さほど不便ではないと思います。Flutter + iOS
flutter.dev のドキュメント を参考にしてみてください。
iOS ホスト側でバッテリーの情報を取得して Flutter で利用する方法が解説されています。Android の
MethodChannelに相当するものは iOS ではFlutterMethodChannelです。
環境の都合で未検証ですが、コードを見るとMethodChannelの使い方に近いです。
チャンネルや呼び出しの名前を設定する点や、エラー時にコード等3種類の情報を返せる点が同じです。
OS によって Flutter 側の書き方を変えなくていいように共通化されているようです。Flutter + Android + Go
Go で書いた処理を Flutter で使うのはもうここまでのことを組み合わせるだけです。
操作に関して少しだけ違いがあります。Flutter のプロジェクトで Android の MainActivity.kt を開くと、ライブラリが認識されません。
上のスクリーンショットでは Nudity が赤くなっています。
右上に出るリンクで Android のプロジェクトを開き、ライブラリの導入等の操作はそちらで行いましょう。Simple カウンター
Go で作った Simple ライブラリを Flutter で使ってみます。
Flutter + Android のコードと違うのは下記の
try~catchの部分だけです。
ライブラリのエラーによって発生した例外を Android 側 で catch し、result.error()を使って Flutter 側でも catch できるようにしました。MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/simple") methodChannel.setMethodCallHandler { call, result -> when { call.method == "simple_multiply" -> { val v = call.argument<Int>("value") ?: 0 try { result.success(Simple.multiply(v)) } catch (e: Exception) { result.error("Go Simple", e.message, null) } } else -> result.notImplemented() } }このように、本当にここまでの技術の組み合わせるだけでできてしまいます。
ヌード写真判定
Awesome Go で go-nude という Go のパッケージを見つけました。
nude.js というライブラリを Go に移植したものだそうです。JavaScript でできるなら Dart でもできそうですが、まだ存在しないようです。
使いたくてもまだ無いケースとして Go の利用が適していると考えました。※このスクリーンキャスト内では go-nude の example/images/ にある画像を使いました。
※実用性は低そうです。
判定が厳しすぎるかと思ったら、逆に景色の写真がヌードと判定されることもあったりします…。10nudity.gopackage nudity import ( "github.com/koyachi/go-nude" ) const ( Unknown int = iota IsNotNude IsNude ) func Check(path string) (int, error) { isNude, err := nude.IsNude(path) if err != nil { return Unknown, err } if isNude { return IsNude, nil } return IsNotNude, nil }MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/nudity") methodChannel.setMethodCallHandler { call, result -> when { call.method == "nudity_check" -> { val imagePath = call.argument<String>("imagePath") result.success(Nudity.check(imagePath)) } else -> result.notImplemented() } }nudity.dartimport 'package:flutter/services.dart'; class Nudity { static const _platform = MethodChannel('example.com/nudity'); static const unknown = 0; static const isNotNude = 1; static const isNude = 2; static Future<int> check(String path) async { final arguments = {'imagePath': path}; return await _platform.invokeMethod<int>('nudity_check', arguments); } }画像選択は Flutter で行い、画像パスをライブラリに渡して判定結果の数値を受け取っています。
ここまでに見てきたことと大差なく、特筆することはありません。
例外処理は省きました(以下同様)。画像変換
画像変換は時間がかかることがあります。
でもグレースケール変換くらいは一瞬でできてほしいところです。ところが、Dart で変換してみると待たされてしまいました(画像サイズ等にもよります)。
こういったものは Go でやれば速くなるのではないかと考えました。grayscale.dartpackage grayscale import ( "bytes" "fmt" "github.com/anthonynsimon/bild/effect" "github.com/anthonynsimon/bild/imgio" "image/jpeg" ) func Convert(path string) ([]byte, error) { img, err := imgio.Open(path) if err != nil { return nil, fmt.Errorf("failed to open image: %v", err) } img = effect.Grayscale(img) buf := new(bytes.Buffer) err = jpeg.Encode(buf, img, nil) if err != nil { return nil, fmt.Errorf("failed to save image: %v", err) } return buf.Bytes(), nil }MainActivity.ktの一部val methodChannel = MethodChannel(flutterView, "example.com/grayscale") methodChannel.setMethodCallHandler { call, result -> when { call.method == "grayscale_convert" -> { val path = call.argument<String>("imagePath") result.success(Grayscale.convert(path)) } else -> result.notImplemented() } }grayscale.dartimport 'dart:typed_data'; import 'package:flutter/services.dart'; class GrayScale { static const _platform = MethodChannel('example.com/grayscale'); static Future<Uint8List> convert(String path) async { final arguments = {'imagePath': path}; return await _platform.invokeMethod<Uint8List>('grayscale_convert', arguments); } }
MainActivityは先ほどとほとんど同じです。
Go では変換後の画像を[]byte型にして返します。
それを Kotlin ではByteArray、Dart ではUint8Listとして受け取っています。
Uint8Listのデータは Flutter でImage.memory()にそのまま渡して画像表示できます。リリースビルドして大きめの画像を変換したところ、所要時間に差が出ました。
Dart Go 1 回目 11.96 秒 1.55 秒 2 回目 11.92 秒 1.41 秒 3 回目 11.89 秒 1.44 秒 4 回目 11.96 秒 1.46 秒 5 回目 11.94 秒 1.49 秒 メインスレッドのブロッキング
画像変換の間に CircularProgressIndicator を表示すると、クルクル回るアニメーションが止まりました。
そこでcompute()を使うようにしてみたのですが、ライブラリのほうに使うと例外が発生しました。
解決法は不明です。
これが解決できないと辛い場合があるかもしれません。ojichat
これで最後です。
Go のパッケージに ojichat というものがあります。
おじさんがLINEやメールで送ってきそうな文を生成してくれる楽しいパッケージです。これを使ったチャット風アプリが簡単にできました。
コードは省略します。付録
ここまでに書いた以外に知っておくと良いことをまとめました。
gomobileのポイント
型の対応
Go からエクスポートするものは全てサポートされている型である必要があります。
Type restrictions(gobind - GoDoc)
符号付きの整数型・浮動小数点型
文字列型、論理型
byte スライス型
参照渡しとなり、渡した先での変更は元のスライスに反映されます。関数型
仮引数や戻り値はサポート対象の型にすること。
戻り値を二つにする場合、二つ目は error 型に限られます。インタフェース型
export されるメソッドはサポート対象の関数型にすること。構造体型
export されるメソッドはサポート対象の関数型にすること。
export されるフィールドはサポート対象の型にすること。サポートされている型は以上です。
スライスとマップが含まれていないのが気になるので試すと、やはりどちらもダメでした。
Go ではよく使うものなので、これらが使えないのはちょっと不便かもしれません。また、byte スライス型の「参照渡し」も試しました。
Kotlin から受け取って中身を Go で変えると Kotlin 側でも変わっていました。
しかし、逆だと変化がありませんでした。
(Kotlin に不慣れで、扱い方を間違えた可能性もあります。)情報出力、エラー
★印が付いている二つのどちらかを用途に合わせて使いましょう。
fmt.Print("message")fmt.Printf("%s", "message")
意外なことに、何も起こりませんでした。
fmt.Println("message")fmt.Printf("message\n")★
LogCat や Flutter の Run ウィンドウに Info レベルの情報として出てきます。
タグは「GoLog」です。
Printf()でもPrintln()と同じ意味になるように末尾に改行を置けば OK のようです。
fmt.Print("message\nhoge")fmt.Printf("%s\nhoge", "message")
なんと!
メッセージの途中に改行があると、順序が逆転して「hoge message」になりました…。関数の二つ目の戻り値としてエラーを返す ★
Android 側や Flutter 側で例外として補足できます。
例外処理をしない場合、Flutter ではないネイティブの Android アプリは異常終了します。
一方 Flutter のアプリは、Android 側で例外処理をし忘れていても生き続けます。
アプリが丸ごと落ちないように対策されているようで、例外の情報が出力されるだけです。
その場合、Flutter 側ではMissingPluginExceptionとなります。
Android 側で例外を無視して Flutter でハンドルすることもできますが、微妙です。
ライブラリの異常は全て上記例外となり、種類をメッセージで判別するしかなくなります。
それよりもきちんと Android 側で対応したほうが良さそうです。
log.Fatal("message")log.Fatalf("message")log.Fatalln("message")
os.Exit(1) を呼ぶものなのでアプリごと終了します。
その際、指定したメッセージが Info レベルの情報として出力されます。
改行の有無は関係なく情報が出力されました。
また、途中に改行があっても出力順序は逆転せず、改行は改行として出ました。
panic("message")
Android 側を巻き込んで異常終了してしまいます。
その際、指定したメッセージが Error レベルの情報として出力されます。
タグは「GoLog」ではなく「Go」です。
当然ですが、次のようにrecover()で回復させれば異常終了は防げます。defer func() { if r := recover(); r != nil { fmt.Println(r) } }()構造体、レシーバー
Passing Go objects to target languages(gobind - GoDoc)
この記事で見てきた例では、値を保持するのは Flutter 側や Android 側でした。
実験的に Go のライブラリ内で状態保持させてみたいと思います。
ライブラリのパッケージ内の変数に持たせれば簡単ですが、あえて構造体を使ってみます。counter.gopackage counter type GoCounter struct { value int32 } func NewGoCounter() *GoCounter { c := new(GoCounter) c.value = 0 return c } func (c *GoCounter) Increment() int32 { c.value++ return c.value }
NewGoCounter()でGoCounterという構造体を初期化してそのポインタを返します。11
それをレシーバーとするIncrement()では、構造体が持つ value の値が 1 増やして結果を返します。Java や Dart にレシーバーはありませんが、どうなるのでしょうか。
上記コードで作ったライブラリを Android のプロジェクトに導入し、デコンパイルした情報を見てみます。
見方については バインディングの中身 を参照してください。Counter.class(デコンパイル)package counter; public abstract class Counter { private Counter() { /* compiled code */ } public static void touch() { /* compiled code */ } private static native void _init(); public static native counter.GoCounter newGoCounter(); }
Counterというクラスが作られ、newGoCounter()をメソッドとして持っているのがわかります。
newGoCounter()が返すのはGoCounter型であり、ポインタではありません。
GoCounterのクラスも作られているので見てみましょう。GoCounter.class(デコンパイル)package counter; public final class GoCounter implements go.Seq.Proxy { private final int refnum; public final int incRefnum() { /* compiled code */ } public GoCounter() { /* compiled code */ } private static native int __NewGoCounter(); GoCounter(int i) { /* compiled code */ } public native int increment(); public boolean equals(java.lang.Object o) { /* compiled code */ } public int hashCode() { /* compiled code */ } public java.lang.String toString() { /* compiled code */ } }こちらには自分で書かなかったプロパティやメソッドも含まれています。
それよりも注目すべきはコンストラクタです。
NewGoCounterという名前から判断して勝手にコンストラクタを用意してくれています。また、もし構造体のフィールドを export していた場合にはゲッターとセッターが自動的に用意されます。
今回は value を export していないので含まれていません。次は Android 側です。
MainActivity.ktimport counter.GoCounter ... val methodChannel = MethodChannel(flutterView, "example.com/counter") methodChannel.setMethodCallHandler { call, result -> when { call.method == "counter_init" -> { goCounter = GoCounter() } call.method == "counter_increment" -> result.success(goCounter.increment()) else -> result.notImplemented() } }
GoCounterの初期化はCounter.newGoCounter()でもできますが、先ほどのコンストラクタを使ってGoCounter()としました。
これにより、Counterは使わずに済みました。
GoCounterのインスタンスを Flutter に渡せると良いのですが、無理でした。
Java/Kotlin のオブジェクトを渡す何らかの方法ができるかもしれません。
代わりに Android 側で保持しておくことにしました。Flutter 側のヘルパークラスは次のようにしました。
gocounter.dartimport 'package:flutter/services.dart'; class Counter { static const _platform = MethodChannel('example.com/counter'); static Future init() async { await _platform.invokeMethod('counter_init'); } static Future<int> increment() async { return _platform.invokeMethod<int>('counter_increment'); } }ライブラリの
increment()を呼び出し、結果を UI に反映するだけで済みます。
Flutter側で状態管理しないでカウンターのアプリが実現しました。
残りのコードは割愛します。インタフェース
gobind のドキュメント のコード例がわかりやすいので抜粋します。
Goのインタフェースpackage myfmt type Printer interface { Print(s string) } func PrintHello(p Printer) { p.Print("Hello, World!") }bindによって自動生成されるJavaのインタフェースpublic abstract class Myfmt { public static void printHello(Printer p0); } public interface Printer { public void print(String s); }Javaでインタフェースを実装して利用public class SysPrint implements Printer { public void print(String s) { System.out.println(s); } } Printer printer = new SysPrint(); Myfmt.printHello(printer);
- Go で書いたライブラリに Printer というインタフェースがある
- そのインタフェースを Java で実装して SysPrint クラスとする
- インタフェースが持つメソッドである print() を SysPrint 内で具象化する
- SysPrint のインスタンスを生成し、それをライブラリの PrintHello() に渡す
- PrintHello() の結果が SysPrint.print() を使って出力される
Go と Java/Kotlin ではインタフェースの書き方が大きく異なります。
それにもかかわらず、違和感なく使えるようにうまくできていますね。先ほどの 構造体、レシーバー のところでもそうでしたが、言語間の差異がうまく緩衝されているのがわかると思います。
Goからアプリ側ネイティブAPIへのアクセス
Reverse bindings(gobind - GoDoc)
ここまでとは逆に Go から Java や Objective-C で用意された API にアクセスできる旨が書かれています。
上記ページには次のような例が掲載されています。java.lang.SystemをGoで読み込んでcurrentTimeMillisメソッドを利用import "Java/java/lang/System" t := System.CurrentTimeMillis()実際にやってみると確かにできました。
ただし、GoLand 等の IDE では存在しないメソッドのように扱われ、利用しにくかったです。NSDateをGoで読み込んでdateメソッドを利用import "ObjC/Foundation/NSDate" d := NSDate.Date()これだけに留まらず、例えば Android なら次のように Go で Activity を継承することもできるようです。
面白いですね。GoでAndroidのActivityを継承してMainActivityを作るimport "Java/android/app/Activity" type MainActivity struct { app.Activity }メモリリークの危険性
Avoid reference cycles(gobind - GoDoc)
今見たように、Go とターゲットの間で双方向にデータをやり取りできます。
片方が他方のオブジェクトへの参照を持っている場合、そのオブジェクトへのアクセスがなくなると、オブジェクトの実体を持っているほうの言語で GC によって適切に参照が破棄されるようです。12しかし、もし参照を相互に持っているとオブジェクトを回収できなくなり、メモリリークが発生します。
そんなことはあまりしないと思いますが、ちょっと注意が必要なところだと思います。Flutter側の例外処理
Flutter + Android で扱ったコードを使って見ていきます。
simple.dart の
on PlatformException catch (e)のブロックを変えてみましょう。
次のように変えると、Android 側のresult.error()で指定した情報がちゃんと出力されます。lib/simple.dartの一部を改変on PlatformException catch (e) { print(e.code); print(e.message); print(e.details); }I/flutter ( 3664): Out of range I/flutter ( 3664): value must be within the range of 0 to 10 I/flutter ( 3664): 11今後はチャンネル名を変えてみます。
「simple」を「hoge」に変えるとMissingPluginExceptionが出ました。
括弧内を訳すと「example.com/hoge チャンネルには simple_multiply メソッドの実装が見つからない」です。lib/simple.dartの一部を改変static const _platform = MethodChannel('example.com/hoge');I/flutter ( 3664): MissingPluginException(No implementation found for method simple_multiply on channel example.com/hoge)最後に
try~catchを使わないようにしてみます。lib/simple.dartの一部を改変final arguments = {'value': count}; return await _platform.invokeMethod<int>('simple_multiply', arguments);ボタンを 11 回以上押してもチャンネル名を変えても、何も出力されませんでした。
意図的に無視することもできるようになっているようです。
しかし、異常に気づいて対応できるようにtry~catchしておくのが良いと思います。Docker
go4droid/Dockerfile at master · mpl/go4droid · GitHub
https://github.com/mpl/go4droid/blob/master/Dockerfilegomobile の Wiki からリンクされている Dockerfile です。
既存環境を汚したくない方にはおすすめです。
また、Go で作ったライブラリに環境の情報(パス)が含まれるのを気にする方は対策に使えます。ただし、ファイルの中身を見ると対象の環境が古いです。
Android や Go のバージョンを書き換えて使う必要があると思います。Android Studioについて
バインディングの中身
自作ライブラリであっても、Android Studio は使い方がわかるように補助してくれます。13
MainActivity のコードの中でライブラリのクラス名にカーソルの上で
右クリック >Go To>Declaration
と操作すると、ライブラリの class ファイルをデコンパイルしたものが表示されます。Simple.class(デコンパイル)package simple; public abstract class Simple { private Simple() { /* compiled code */ } public static void touch() { /* compiled code */ } private static native void _init(); public static native int multiply(int i) throws java.lang.Exception; }最初に見たサンプル(Simple ライブラリ)だと次のようになります。
作ったライブラリを Java/Kotlin でどう使えばいいのかわかりやすくて助かりますね。
int multiply(int i)
仮引数も戻り値も int になっています。
これは Go でMultiply(value int32) int32のようにint32を使ったためです。
64 ビットの Go でMultiply(value int) intとするとlong multiply(long l)になります。throws java.lang.Exception
multiply() で二つ目の戻り値によってエラーを返さない場合、例外はスローされません。デコンパイルで得られたクラス/メソッド等の定義の情報は
View>Quick Definition
の操作でも表示されます。
コード補完やクラス・メソッド等の情報表示もしてくれて助かります。
ライブラリの更新方法
ライブラリの中身を変えた場合、aar ファイルを上書きするだけで変更が適用されます。
ただし、Android Studio はその変更をすぐに認識しません。
変更をコード補完などにも反映するには、プロジェクトを開き直す必要があります。その方法で反映されないときは、app の build.gradle から
implementation project(path: ':simple')を消してから再追加し、プロジェクトの sync をしたところ、ようやく反映されました。
少し手間ですが、そこまですると確実です。Android App Bundle
Go で作ったライブラリはサイズが大きめになりがちです。
特に複数のアーキテクチャ向けのファイルが含まれていると大きくなります。少しでもユーザにやさしいサイズになるよう、ストアには APK ではなく App Bundle にしましょう。
そうすれば、必要なアーキテクチャの APKs にしてくれたり、モジュール単位のダウンロードが可能になったりします。
Flutter の FAQ の中で説明されています。 ↩
必要に応じて CGO を使って C も組み合わせれば速度の違いは更に大きくなるかもしれません。 ↩
C/C++ で作る場合も NDK は必要で、Go だからではありません。 ↩
https://github.com/golang/go/wiki/Mobile#sdk-applications-and-generating-bindings ↩
"The equivalent of calling newCounter in Go is GoMypkgNewCounter in Objective-C. The returned GoMypkgCounter* holds a reference to an underlying Go *Counter." 見た目の制限とはこのあたりのことかなと思います。https://godoc.org/golang.org/x/mobile/cmd/gobind#hdr-Passing_Go_objects_to_target_languages ↩
gomobile に限らず Go 自体がそういうものです ↩
記事執筆時の調査等には Go 1.12.7 (windows/amd64)、Flutter 1.7.8+hotfix.3 (channel stable)、Dart 2.4.0、Android Studio 3.4.2 を使用しました。 ↩
NDK のパスを環境変数の
Pathに設定する必要もありませんでした。数年前に使っていたときには設定した記憶があるのですが、不要になったのかもしれません。 ↩サポートしたいアーキテクチャ分をすべて含んだ App Bundle をストアにアップロードすると、ユーザの利用端末に合わせて自動的に最適化した APK を配信してくれるため、複数を含んでいることを気にする必要はないと思います。32/64 ビット両方を対象に含めた App Bundle の生成は、先日リリースされたばかりの Flutter 1.7 で可能になりました。 ↩
研究論文に基づいて実装されたものだそうです。また、nude.js の作者の ブログ には "I wouldn’t recommend using the library in production mode right now because the detection rate is about 60%" と書かれています。 ↩
型を初期化する関数(コンストラクタのようなもの)の名前の先頭に「New」を付けるのは Go の慣習です。 ↩
ちょっと理解があやふやです。間違っていればご指摘ください。 ↩
Visual Studio Code はこの点は不十分なようです。 ↩
















