Android + Microservices
Androidアプリのバックエンドをマイクロサービスにする話ではありません。
Android端末内で複数アプリを用いたマイクロサービスアプリ開発です!!
つまり、変態近未来アーキテクチャの紹介です。(普通のアプリを書いている人には恐らく無縁の技術です。)
でも、一部有用な内容もあるので、とりあえず読んでいってください。
導入
Androidは、ほかのアプリのActivityやServiceにIntentを飛ばせます。
暗黙的IntentとActionを使う方法(特定の機能を持つアプリに遷移する)は有名だと思います。
しかし、この暗黙的Intentを使う方法には一つ大きな欠点があります。
Serviceを叩けないという最大の問題です。
起動しているアプリの裏で複数のアプリが連携するためにはActivityではなく、Serviceを動かす必要があります。
どうしても複数のServiceが連携し、Microservice Android Applicationを実現したい・・・!!
この野望に答える手法がありました。
実は明示的Intent(特定のクラスに遷移する)で、他のアプリのActivityやServiceを直接呼ぶことができます。
実際のところ、他のアプリのActivityやServiceを明示的Intentで呼び出すことなど、普通はありえません。
何故ならば、ユーザが呼び出し先のアプリをインストールしてくれている保証が無いからです。
しかし、この手法を使えば複数のアプリがデータをやり取りして一つの機能を実現するマイクロサービスAndroidアプリケーションが作れます!
有用なユースケースは思いつきませんが、面白いと思いませんか?
あと、Microservice Android Applicationがかっこいい・・・
(Dynamic Feature Moduleに似ていますが、よりダイナミックになっています)
目次
- Android内マイクロサービスアーキテクチャの説明
- 基本編: Serviceとデータやり取りする方法
- 変わった人向け: 他のアプリのServiceやActivityを呼び出す方法
- 変態向け: 端末内にインストールされているアプリのスキャン方法
- で結局何ができるの?
Android内マイクロサービスアーキテクチャの説明
例えばこんな構成が作れます
Host AppからPlugin Appに対してサーバへの通信を依頼し、最終的にその結果を受け取るアプリが作れます!
(別のアプリに分ける必要がある?というツッコミは野暮ってもんですよ。)
基本編: Serviceとデータをやり取りする方法(アプリ内、アプリ間共通)
AndroidのServiceは主に4種類あります。サービスについて
- background Service
- Intent Service
- foreground Service
- bind Service
(実はIntentServiceはbackgroundServiceの仲間だったりするのですが、分かりやすく4種類としています)
このうち、呼び出し元に値を返しやすいのは
- Intent Service
- bind Service
です。
IntentServiceはresultReceiver
というクラスを使って呼び出し元に値を返すことができます。
以下、resultReceiverのサンプルコードです。
//ResultReceiverの引数は実行スレッドを指定するもの。nullなら任意のスレッド
this.startIntentService(Hoge::class.java, object: ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
when (resultCode) {
// 予めIntent先と決めておいた定数
RESULT_CODE -> {
resultData?.getSerializable(Fuga)
// 以下略
}
else -> {}
}
}
})
BindServiceは3種類の値の返し方がありますが、よく使われるのはMessenger
クラスを使った方法です。
Messenger
についてはこの後詳しく説明します。
3種類の返し方については公式に詳しく書いてあります。
ここまでは一般的なServiceの話です。ここからアプリ間通信の話に踏み込みます。
変わった人向け: 他のアプリのServiceやActivityを呼び出す方法
bindService
は他のアプリから呼び出せますが、IntentService
は呼び出せません。
なので、ここから先はbindService
を使います。bindServiceについてはこちら
基本的にはアプリ内のbindServiceを呼び出す方法と変わりません。
しかし、他アプリゆえに気をつけるポイントが3点あります。
serviceの可視性をexported=true
にする
AndroidManifest.xmlのService項目にexported=true
を設定しましょう。
この設定が無いとアプリ外からIntentで呼べません。
AndroidManifest.xml
<service
android:name=".HogeService"
android:enabled="true"
android:exported="true">
</service>
Messenger
クラスでデータのやり取りをする
Host AppもPlugin Appも互いのクラスを知りえません(別アプリなので当然)。
なので、IBinder
を拡張する方法は使用できません。
Messenger
にBundle
を詰めてやり取りします。
HTTP通信で例えるならば、json
に値を詰めてやり取りするイメージです。
class SampleBindService : Service() {
private val messageHandler = Handler { msg ->
when (msg.what) {
// msg.whatに0が来たらログに吐く
0 -> {
Log.d("SampleBindService", "message: ${msg.obj}")
}
else -> {}
}
val bundle = Bundle().also {
it.putString("sample", "This is test.")
}
// what=0にbundleデータを詰めてreplyする
msg.replyTo?.send(Message.obtain(null, 0, bundle))
// replyは何度でも送ることができる
msg.replyTo?.send(Message.obtain(null, 0, bundle))
true
}
private val messenger: Messenger = Messenger(this.messageHandler)
override fun onBind(intent: Intent): IBinder {
return this.messenger.binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
}
Host AppはPlugin AppのapplicationId
とサービスのclassName
(FQDN)を知っていること
Intentで呼び出すときに、呼び出し先のapplicationId
とclassName
が必要になります。
つまり、なんらかの方法でHost AppはPlugin Appの上記2つの情報を知る必要があります。
そして、その方法は変態向け: 端末内にインストールされているアプリのスキャン方法で解説します。
val intent = Intent().also {
it.setClassName(applicationId, className)
}
変態向け: 端末内にインストールされているアプリのスキャン方法
ここから先はほとんど使う人が居なさそうな機能の紹介です。
先ほど、Host AppはPlugin AppのapplicationId
とclassName
を知っていなければならない。という話をしました。
1人でHost AppとPlugin Appの両方を作る場合は、これらの情報を知っているはずなので問題ありません。
一方で、Microserviceのように連携する場合やPlugin AppがHost Appの機能拡張アプリであった場合、Host AppはPlugin Appの存在を知りえません。(プラグインは本体を意識するが、本体がプラグインを意識したら変ですよね?)
では、いかにしてHost AppはPlugin Appの存在を知るのか。
その答えは・・・Host Appが端末内にインストールされているアプリをスキャンすれば良いのです。
(実はAndroidのアプリは端末内のアプリをスキャンすることができます。)
詳しくはこの記事をお読み下さい。
ただ、1点上記の記事と異なる点があります。上記の記事ではActivityを呼び出していましたが、今回はServiceを呼び出します。
LAUNCHERとして登録されているActivityはアプリスキャンで検索できるのですが、それ以外のActivityとServiceは検索できません。(少なくとも私は知らないので、知っていたら教えてください。)
ですので、ServiceのclassName
はスキャンしても知ることはできません。
なので、苦肉の策として、Plugin Appが提供するServiceは
applicationId
と同じパッケージの直下にPluginService
という名前で置く
などの約束をして、Host AppがIntentを飛ばせるようにする必要があります。
スキャンをした後にIntentを飛ばすサンプルコードが以下です。
val pm = packageManager
val packageInfoList = pm.getInstalledPackages(
PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES
)
val targetPackageName = packageInfoList
// 特定のパッケージで始まるアプリを検索対象とする
.filter { it.packageName.startWith("jp.co.hoge.fuga") }
.map { it.packageName }
}.first() // 今回は見つかる前提。見つからないとここで落ちる
val intent = Intent().also {
// 上記で得られたapplicationIdとclassNameを詰める(classNameはパッケージ直下にPluginServiceで固定)
it.setClassName(targetPackageName, targetPackageName + ".PluginService")
}
val connection = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val messenger = Messenger(service)
messenger.send(Message.obtain(null, 0, null).also {
it.replyTo = Messenger(ReplyHandler())
})
}
override fun onServiceDisconnected(name: ComponentName) {
}
}
bindService(intent, connection)
}
で、結局何ができるの?
例えば、私が思いつくのは以下のような物です。
しかし、複数アプリを連携させてまでやるものかと言われると。うーん
案1: ニュースまとめアプリ
各種ニュースサイトをまとめて見れるアプリ。
各Plugin Appはそれぞれのニュースサイトからの情報を取ってくる昨日を有し、
Host AppはPlugin Appから得たデータを元に、画面を作る。
ユーザはPlugin Appを入れれば入れるほど、様々なニュースサイトから情報を集められるようになる。
(でも、それってDynamic Feature Moduleでよくね?そもそもプラグイン構成取る必要ある?最初から様々なサイトから集めておけよ。)
案2: みんなで情報提供Serviceを公開し合う文化を作り、Microserviceを実現
この記事を読んでくれた人がAndroidを書いたときに、何かしらのデータを提供するbindServiceを作り、それを外部に公開したとする。
そして、そのApplicationId
やclassName
を公開したとする。
これらの情報が蓄積されれば、互いに互いのServiceを叩き合うMicroserviceの誕生!!かっこいい!!
(セキュリティ的に大丈夫なんかとか、依存したいアプリが入っているかわからない問題が課題。)
何か使える案下さい
面白そうな機能を見つけたので、変な構想を練って見ましたが、何に使えるのかイメージが湧きません・・・
一番のネックは、ユーザがアプリを入れてくれるとは限らない点ですね。
そこさえ克服できれば、アプリ間をService同士が連携し合う、Android内Microserviceアーキテクチャが陽の目を見る日も近い!