この記事は、Recruit Engineers Advent Calendar 2018 24日目の記事です。
はじめに
Androidアプリの開発をしている@sasakissaです。
最近Go言語を勉強しているのですが、GoでAndroidやiOSの開発ができるmobileというパッケージが用意されていることを知りました。
本記事では、gomobileの概要を簡単にまとめて、お試しでアプリを作成してみた感想を書きます。
今回実装したコードはこちらです。
https://github.com/sasakissa/GithubRepository-GoMobile
Go Mobile
https://github.com/golang/mobile
Go MobileはGoでAndroid/iOSアプリを開発するための、サポートライブラリとビルドツールが提供されたパッケージです。まだExperimentalなパッケージですので注意して下さい。
開発方法としては
- Nativeアプリ: GoでOpenGLなどを呼び出してアプリを書く
- SDKアプリ:Goで書いたライブラリをJava/Objective-Cから呼びだす
の2通りの方法があるのですが、今回は後者について扱います。
解決したい課題
Android/iOSなどプラットフォームごとにネイティブアプリを実装すると、ビジネスロジックの重複管理が問題になります。最近だとReactNativeやFlutterのようなマルチプラットフォームに対応したフレームワークも有名ですが、UIなどのネイティブの機能が使いにくくなります。
そこで共通化したいロジックの部分だけ、ライブラリモジュール化することでこの問題の解決を目指します。
作成したアプリ
Githubのリポジトリをキーワード検索してリストで表示するだけのアプリを作ってみました。
共通部分のモジュール化を課題に挙げておきながら、今回はAndroidしか実装していないので、以降はAndroidの実装メインで話を進めます...🙇。
設計
今回作成したアプリの設計は以下の通りです。
MVVMっぽいつくりで、ViewModelから直接APIリクエストを行います。
この図中のAPIにリクエストする部分を共通ロジックとしてGoで実装しました。
後述するGoMobileの制約から、API以上のロジックの共通化をGoで実現する方法が思いつかなかったので、View-ViewModel-APIの最小構成にしてあります。
GoMobile実践
導入
公式のドキュメントに従って、gomobileを導入しましょう。
gomobileを使用するには、Go 1.5以上の環境が必要になります。
まずgomobileコマンドをgo get
で導入します。
$ go get golang.org/x/mobile/cmd/gomobile
これで$GOPATH/bin
以下にgomobile
コマンドがインストールされます。
次にgomobile init
を実行します。このコマンドで、Go Mobileを使用するために必要なツール類がインストールされます。
$ gomobile init # it might take a few minu
ちなみにこのコマンドandroid-ndkもインストールされるようなのですが、すでに導入している場合は、ndkフラグをつけてパスを指定してそちらを使うことも可能なようです。
$ gomobile init -ndk /path/to/your/android/ndk
BuildとAndoridアプリへのDeploy
Build
注意点として、Go MobileはARM,ARM64,38,amd64のCPUを持つエミュレータかデバイス上でしか動作しませんので。気をつけてください。
以下のコマンドを実行することで、Goで記述されたソースからaarファイルを生成します。
$ gomobile bind -o "output/path/filename.aar" -target=android "source/godir"
Install
生成したライブラリモジュールを以下の手順でAndroidプロジェクトに導入します。
Android Studio上で
- [File] > [New] > [New Module]を選択。
- [Create New Module]ウィンドウで、[Import .JAR/.AAR Package]を選択して、[Next]ボタンを押す。
- 表示されたダイアログで「File Name」に生成したaarファイル、「Subproject name」に任意の名前を指定する。
Config
最後に以下の手順に従って依存関係を設定します。
- [File] > [Project Structure]を選択。
- サイドメニューからaarライブラリを使用するモジュールを選択し、「Dependency」タブを選択する。
- 左下の [+]ボタン > [Module Dependency] を選択し、先ほど追加したサブプロジェクトを選択、OKボタンを押す。
以上の手順で、Goで記述したモジュールがAndroidプロジェクトで使用可能になります。
実装
Bind
go bind
コマンドによって、Goパッケージをターゲット言語から呼び出すためのバインディングが作成されます。GoとJavaとのバインディングでは、 以下のような対応でJavaコードが生成されます。
Go | Java |
---|---|
パッケージ | 抽象クラス |
パッケージ関数 | staticメソッド |
構造体 | クラス |
メソッド | 抽象メソッド |
JNIを通じた実装の呼び出しは隠蔽されています。
また、GoからJava/Objective-Cへ公開可能な型には制限があり、現時点でサポートされているのは、
- 符号付整数と浮動小数点数
- 文字列とBoolean
- バイトスライス
- 引数と戻り値が対応型のみで構築された関数
- 制限をクリアしたメソッドのみを公開しているインタフェース
- 制限をクリアしたメソッドとフィールドのみを公開している構造体
のみとなります。結構厳しいです。
ちなみにGoからJavaやObjective-CのAPIを呼び出す逆バインディングも可能です。
共通モジュールの実装
今回Goで実装したAPIクライアントは以下の通りです。
ネイティブアプリから検索キーワードをstringで受け取って、APIリクエストを行い、レスポンスのstringとステータスコードをintで返すようにしました。本来はパースなどのロジックもライブラリ化しないと中途半端なのですが、ネイティブアプリ側に公開できる型が限られており、structの入れ子などもバインディングできなかったために、今回は通信部分だけをGoで実装してあります。
Goでの実装
(1) Javaに公開するAPIレスポンスの構造体定義
package github
import (
"io/ioutil"
"net/http"
"net/url"
)
type RepositorySearchResult struct {
Result string
Status int
}
(2) レポジトリ一覧APIにリクエストを行うAPIクライアントのインタフェース定義
const searchURL = "https://api.github.com/search/repositories"
type GithubService interface {
SearchRepos(terms string) *RepositorySearchResult
}
(3) APIクライアントの構造体定義とメソッドの実装
type GithubClient struct {
RequestUrl string
}
func (client *GithubClient) SearchRepos(terms string) *RepositorySearchResult {
q := url.QueryEscape(terms)
resp, err := http.Get(client.RequestUrl + "?q=" + q)
if err != nil {
return &RepositorySearchResult{err.Error(), ErrorNetwork()}
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return &RepositorySearchResult{resp.Status, ErrorHttp()}
}
byteArray, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return &RepositorySearchResult{string(byteArray), Success()}
}
(4)パッケージ関数を使ったステータスコードの定義
// (4) 本番URLにリクエストするAPIクライアントを返すパッケージ関数
func DefaultClient() *GithubClient {
return &GithubClient{searchURL}
}
type StatusCode int
func Success() int {
return 1
}
func ErrorNetwork() int {
return -1
}
func ErrorHttp() int {
return -2
}
生成されるクラス
上記のGoパッケージから、go bind
で生成されるバインディングクラスは以下の通りです。
(1) Javaに公開するAPIレスポンスの構造体定義
package github;
import go.Seq;
/*
* 構造体 → クラス
*/
public final class RepositorySearchResult implements Seq.Proxy {
...
public final native void setResult(String v);
public final native long getStatus();
public final native void setStatus(long v);
...
}
(2) レポジトリ一覧APIにリクエストを行うAPIクライアントのインタフェース定義
package github;
import go.Seq;
/*
* インタフェース → インタフェース
*/
public interface GithubService {
public RepositorySearchResult searchRepos(String terms);
}
(3) APIクライアントの構造体定義とメソッドの実装
package github;
import go.Seq;
/*
* 構造体 → クラス
*/
public final class GithubClient implements Seq.Proxy, GithubService {
...
public final native String getRequestUrl();
public final native void setRequestUrl(String v);
public native RepositorySearchResult searchRepos(String terms);
...
}
(4) 本番URLにリクエストするAPIクライアントを返すパッケージ関数とステータスコードの定義
package github;
import go.Seq;
/*
* パッケージ関数 → パッケージ名の抽象クラスstatic関数
*/
public abstract class Github {
static {
Seq.touch(); // for loading the native library
_init();
}
...
// クライアントをnewして返すstaticメソッド
public static native GithubClient defaultClient();
// ステータスコードの定義
public static native long errorHttp();
public static native long errorNetwork();
public static native long success();
}
利用する側の実装は以下の通りです。
githubService.searchRepos(term)
で受け取った結果に応じて、ViewModelが自身のフィードプロパティを更新します。ViewであるActivityはLiveDataを使って、更新を監視しており、その値に応じて表示を更新します。
class SearchRepoViewModel : ViewModel() {
val items: MutableLiveData<List<Repo>> = MutableLiveData()
val isLoading: MutableLiveData<Boolean> = MutableLiveData()
val isError: MutableLiveData<Boolean> = MutableLiveData()
val githubService: GithubService = Github.defaultClient()
fun search(term: String) {
isLoading.value = true
Single.fromCallable { githubService.searchRepos(term) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
when (result.status) {
Github.success() -> {
val repoSearchResponse = Gson().fromJson(result.result, RepoSearchResponse::class.java)
items.value = repoSearchResponse.items
isError.value = false
}
Github.errorNetwork() -> {
isError.value = true
}
Github.errorHttp() -> {
isError.value = true
}
}
isLoading.value = false
}
}
}
まとめ
本記事では、GoMobileを使って、OSに依存しないロジックをライブラリ化したAndroidアプリの実装を行いました。
今回は非常に簡単な例+iOS側を実装していないのでなんとも言えませんが、Goでの実装とAndroidとのつなぎこみ自体は思ったよりスムーズにできました。Goは標準ライブラリが充実しているので、通信部分以外にも実現できることはかなり幅広いと思います。ただ、公開できる型にかなり制限があるのが厳しいという印象です。iOSで利用した場合、コードベースにObjective-Cが残ってしまうのも厳しいのでは...。
開発が停滞しているように見えますが、gomobileの今後の発展に期待です。