Help us understand the problem. What is going on with this article?

Go MobileでAndroid/iOSの共通ロジックをライブラリ化してみる

More than 1 year has passed since last update.

この記事は、Recruit Engineers Advent Calendar 2018 24日目の記事です。

はじめに

Androidアプリの開発をしている@sasakissaです。
最近Go言語を勉強しているのですが、GoでAndroidやiOSの開発ができるmobileというパッケージが用意されていることを知りました。

本記事では、gomobileの概要を簡単にまとめて、お試しでアプリを作成してみた感想を書きます。

今回実装したコードはこちらです。
https://github.com/sasakissa/GithubRepository-GoMobile

Go Mobile

image.png

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の実装メインで話を進めます...🙇。

GoMobile-GithubRepository

設計

今回作成したアプリの設計は以下の通りです。
MVVMっぽいつくりで、ViewModelから直接APIリクエストを行います。
この図中のAPIにリクエストする部分を共通ロジックとしてGoで実装しました。
後述するGoMobileの制約から、API以上のロジックの共通化をGoで実現する方法が思いつかなかったので、View-ViewModel-APIの最小構成にしてあります。

GoMobile-archi.png

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上で
1. [File] > [New] > [New Module]を選択。
2. [Create New Module]ウィンドウで、[Import .JAR/.AAR Package]を選択して、[Next]ボタンを押す。
3. 表示されたダイアログで「File Name」に生成したaarファイル、「Subproject name」に任意の名前を指定する。

Config
最後に以下の手順に従って依存関係を設定します。
1. [File] > [Project Structure]を選択。
2. サイドメニューからaarライブラリを使用するモジュールを選択し、「Dependency」タブを選択する。
3. 左下の [+]ボタン > [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レスポンスの構造体定義

github.go
package github

import (
    "io/ioutil"
    "net/http"
    "net/url"
)

type RepositorySearchResult struct {
    Result string
    Status int
}

(2) レポジトリ一覧APIにリクエストを行うAPIクライアントのインタフェース定義

github.go
const searchURL = "https://api.github.com/search/repositories"

type GithubService interface {
    SearchRepos(terms string) *RepositorySearchResult
}

(3) APIクライアントの構造体定義とメソッドの実装

github.go
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)パッケージ関数を使ったステータスコードの定義

github.go
// (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レスポンスの構造体定義

RepositorySerachResult.java
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クライアントのインタフェース定義

GithubService
package github;

import go.Seq;

/*
 *  インタフェース → インタフェース
 */
public interface GithubService {
    public RepositorySearchResult searchRepos(String terms);

}

(3) APIクライアントの構造体定義とメソッドの実装

GithubClient.Java
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クライアントを返すパッケージ関数とステータスコードの定義

Github.java
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を使って、更新を監視しており、その値に応じて表示を更新します。

SearchRepoViewModel.kt
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の今後の発展に期待です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away