第6回は、第4回で実装したサーバストリーミングgRPCサービスを利用するモバイルアプリケーションを、Android OS用にKotlinで開発します。ここでは、gRPCとモバイルアプリケーションの相性などを理解し、異なるプラットフォームとプログラミング言語で構成されるサービスを問題なく利用できることを理解します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
今回のテーマは、KotlinによるgRPCクライアントAndroidアプリの開発です。連載第4回で、PythonによるサーバサイドストリーミングのgRPCサービスを開発しましたが、そのサーバにAndroidアプリからクライアントとしてアクセスします。連載第1回で紹介したように、gRPCのサービスはプログラミング言語やプラットフォームに依存しない設計となっています。今回は、サーバがPython、クライアントがAndroidとして、問題なく利用できることを確認します。
サーバサイドストリーミング、そしてPythonによるgRPCサービスの開発については、連載第4回を参照してください。サーバも、連載第4回のものをそのまま利用します。
開発に必要な環境を準備しておきます。今回はクライアントがAndroidアプリとなるので、Androidアプリの開発環境を作っていきます。サーバは同一ホストで動作させます。Androidアプリの開発には幾つかの方法がありますが、今回はgRPC公式リポジトリから入手できるgrpc-kotlinをベースに、コードエディタとCLIツールを使って開発していきます。以下で紹介するソフトウェアも、基本的にこの方針に沿ったものとなっています。
本連載で共通で使用するコードエディタVisual Studio Code(以降、VSCode)をはじめとして、必要なソフトウェアをインストールしておいてください。
このうち、Kotlin、JDK、Android SDKについては、Android Studioによってまとめてインストールが可能です。本稿ではAndroid Studioは使用しませんが、環境の整備を簡略化できますので、個別のインストールが困難な場合には利用を検討してください。
Download Android Studio & App Tools - Android Developers
また、本稿ではAndroidアプリの動作をAndroid仮想デバイス(AVD)上で確認します。スペックは特に問わないので、API level 16以降のAVDをあらかじめ作成しておき、デバイス名を控えておいてください。
ソフトウェアのインストールが済んだら、VSCodeのメニューから[ターミナル]−[新しいターミナル]を選択し、ターミナルを開いて作業します。本連載で共通して利用するフォルダatmarkit_grpcにカレントフォルダをあらかじめ移動しておきます。そして、公式サンプルであるgrpc-kotlinをgRPC公式リポジトリからクローンします。
% git clone https://github.com/grpc/grpc-kotlin
grpc-kotlinは、多数のサブプロジェクトからなるプロジェクトです。以下は、その代表的なものです。
全てを利用するわけではないですが、CLIベースのgRPCサーバとクライアントも含まれているので、KotlinによるgRPCサービスの基本的な実装を知るのに有用です。チュートリアルを参考に、CLIベースのプログラムも実行、確認してみるとよいでしょう。
クローン後は、grpc-kotlin/examplesフォルダに移動して作業します。ターミナル上でCLIコマンドによる操作が基本ですが、必要に応じて(フォルダの作成やファイルのコピーなど)VSCodeのエクスプローラーでの操作を併用すると楽でしょう。
BookInfoサービスのクライアントであるAndroidアプリの開発の前に、既定のAndroidアプリをビルドして実行してみましょう。これを通じて、grpc-kotlinにおけるビルドや実行のイメージをつかんでください。
既定のAndroidアプリは、既定のgRPCサーバを使用します。ですので、まずはgRPCサーバをビルドしておきます。ビルドは、プロジェクトに用意されたgradlewコマンドによって実行します。Windows環境ではgradlew.batの実行になるので、「./」は不要です。
% ./gradlew installDist
「BUILD SUCCESSFUL」と緑色で出力されれば成功です。この時点で、server、protos、stubの3つのプロジェクトがビルドされています。
既定のAndroidアプリであるandroidプロジェクトは、helloworldサービスのクライアントです。androidプロジェクトをビルドして、アプリをAVDにインストールします。ビルドに先立ち、Android SDKのあるパスを環境変数ANDROID_SDK_ROOTに設定し、AVDを起動しておきます。Android StudioでAndroid SDKをインストールした場合には、Android SDKのパスはSDK Managerの[Android SDK Location]から確認できます。emulatorコマンドとadbコマンドは、既定ではパスが通っていないので、フルパスで指定しています。ユーザー名naoは読者の環境に合わせて変更してください。
% export ANDROID_SDK_ROOT="/Users/nao/Library/Android/sdk" % /Users/nao/Library/Android/sdk/emulator/emulator -avd Pixel_4_API_30 % ./gradlew :android:installDebug …略… % /Users/nao/Library/Android/sdk/platform-tools/adb install android/build/outputs/apk/debug/android-debug.apk
サーバは、以下のように起動します。
% ./server/build/install/server/bin/hello-world-server Server started, listening on 50051
AVDでgRPC Kotlin Androidアプリを起動し、テキストボックスに適当な名前を入れて[Send gRPC Request]をクリックし、以下のようにレスポンスが表示されれば成功です。
動作確認が済んだら、Ctrl+Cを入力してサーバを停止させておいてください。なお、既定のAndroidアプリはAVDで動作させることを想定しています。実機にインストールして実行するには、サーバURLの変更が環境に合わせて必要になってきますが、それについては割愛します。
既定のAndroidアプリの動作確認が済んだところで、BookInfoサービスのクライアントをAndroidアプリとして実装していきます。作業の内容は、grpc-kotlinにBookInfoサービスのためのファイルを追加し、既存のファイルを複製してそれを修正していくというイメージです。
プロトコル定義ファイルのプロジェクトprotosに、BookInfoサービスを既存のサービスに倣って追加します。protos/src/main/proto/io/grpc/examplesフォルダにbookinfoフォルダを作成し、以下のファイルをコピーします。
bookinfo.protoファイルには、既存のpackage文のあとにoption文を2つ追加します。
…略… package bookinfo; option java_package = "io.grpc.examples.bookinfo"; (1) option java_multiple_files = true; (2) …略…
(1)は、gRPCサービスのパッケージ名bookinfoとは別に、生成されるKotlinのクラスの名前空間を指定するためのものです。(2)は、メッセージのファイルを分割して複数にしてもよいという指定です。指定しないと、一つのファイルにまとめられます。
BUILD.bazelファイルは、ビルドツールBazelのための設定ファイルです。ここでは修正内容は割愛しますので、配布サンプルを参照してください。基本的に、ファイル中の「route_guide」を「bookinfo」に修正しただけの内容となっています。
ここでいったんビルドして、追加したプロトコル定義ファイルから正しくソースファイルやクラスファイルが生成されるか確認しておきます。
% ./gradlew installDist
例えば、stub/build/generated/source/proto/main/grpckt/io/grpc/examples/bookinfo/BookinfoGrpcKt.ktファイルが生成され、そこにはプロトコル定義ファイルにおけるMultiBookInfoサービスの実装が記述されています。
BookInfoサービス対応のAndroidアプリを作成していきます。既存のandroidフォルダをコピーして、これを改変していくことで作成します。フォルダをコピー後、andoid-bookinfoにリネームします。ここから、android-bookinfoプロジェクト内のファイルを一つずつ修正していきます。
ビルドスクリプトであるandroid-bookinfo/build.gradle.ktsファイルを以下のように修正します。applicationIdの修正のみです。
…略… defaultConfig { applicationId = "io.grpc.examples.bookinfo" 修正 …略…
マニフェストファイルであるandroid-bookinfo/src/main/AndroidManifest.xmlファイルを以下のように修正します。パッケージの変更に合わせた修正のみです。
<?xml version="1.0" encoding="utf-8"?> <<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.grpc.examples.bookinfo"> 修正 …略… <application …略… <activity android:theme="@style/Theme.AppCompat.NoActionBar" android:name="io.grpc.examples.bookinfo.MainActivity" 修正 …略…
アクティビティーで使用する文字列リソースを以下のように修正、追加します。
<string name="name_hint">検索キーワード</string> <string name="send_request">検索開始</string> <string name="app_label">gRPC BookInfo Kotlin Android</string> <string name="not_found">検索結果が見つかりません。</string>
アクティビティーファイルMainActivity.ktの修正に先立ち、パッケージ名の変更に伴いフォルダ名をhelloworldからbookinfoにリネームしておきます。そして、MainActivity.ktファイルを以下のように修正します。元のファイルから変更しない箇所は省いていますので、必要に応じて公式サンプルも参照してください。
アクティビティーファイルは、大きく(3)からのアクティビティークラスの定義、(7)からの手続き呼び出しクラスの定義、(13)からのUI構築関数に分かれます。
// 名前空間の変更 package io.grpc.examples.bookinfo (1) …略… // Jetpack Composeのためのインポート import androidx.compose.foundation.layout.Spacer (2) import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items // アクティビティークラスの定義 class MainActivity : AppCompatActivity() { (3) …略… // サービス呼び出しのためのクラスをインスタンス化 private val bookinfoService by lazy { MultiBookInfoRCP(uri) } (4) // アクティビティー作成時に呼び出されるメソッド override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Jetpack Composeでコンテンツを描画 setContent { (5) Surface(color = MaterialTheme.colors.background) { MultiBookInfo(bookinfoService) } } } // アクティビティー破棄時に呼び出されるメソッド override fun onDestroy() { super.onDestroy() // サービスをクローズ bookinfoService.close() (6) } }
(1)では、BookInfoサービスに合わせてパッケージ名を修正しています。
(2)では、UI構築関数で使用するJetpack Compose(後述)の名前空間を追加でインポートしています。
(3)からは、アクティビティーのクラスです。(4)で、サービス呼び出しのためのクラスをインスタンス化しています。
(5)は、アクティビティーの作成時に呼び出されますが、ここのsetContentメソッドによってUIが構築されています。これはJetpack Composeと呼ばれる機能で、コードによってUIを構築する新しい仕組みです。ここでは、Surfaceメソッドでコンテンツの背景を指定し、内容についてはMultiBookInfo関数にさらに委ねている、と理解してください。
(6)は、(4)でインスタンス化したサービスをクローズしています。
// サービス呼び出しのためのクラス class MultiBookInfoRCP(uri: Uri) : Closeable { (7) // 状態保持、検索結果保持のためのインスタンスを生成 val responseState = mutableStateOf("") (8) val list = arrayListOf("") // サービスのためのチャネルを生成 private val channel = let { (9) …略… } // スタブ(クライアント)の生成 private val bookinfo = MultiBookInfoGrpcKt.MultiBookInfoCoroutineStub(channel) (10) // Search手続きを呼び出すメソッド suspend fun search(text: String) { (11) try { var count = 0 list.clear() val request = searchRequest { this.text = text } // Search手続きを呼び出して結果をイテレータで受け取る bookinfo.search(request).collect { searchResponse -> (12) list.add(searchResponse.item.title) count += 1 } responseState.value = "${count} 個が見つかりました。" } catch (e: Exception) { responseState.value = e.message ?: "Unknown Error" e.printStackTrace() } } …略… }
(7)からは、サービス呼び出しのためのクラスの定義です。このクラスのインスタンスで、サービスへ接続したり、手続きを呼び出したりします。(8)では、手続き呼び出し結果を保持するMutableStateと、Search手続き呼び出しで得られた結果を保持するArrayListを宣言しています。
(9)では、処理内容は省略していますがサーバへのチャネルを生成しています。(10)では、このチャネルを用いてスタブ(クライアント)オブジェクトを取得しています。実際の通信は、このスタブでやりとりします。
(11)は、Search手続きを呼び出すメソッドとなります。処理としてはシンプルで、(12)でスタブ経由でSearch手続きを呼び出し、collectメソッドによって返されるイテレータによってArrayListに結果を追加していく、というものです。正常に処理されれば、取得件数をステータスにセットし、例外が発生すればエラーメッセージをステータスにセットします。ここでセットしたステータスは、UI側で参照されて表示内容が切り替わります。
// Jetpack ComposeによるUI構築関数 @Composable (13) fun MultiBookInfo(multiBookInfoRCP: MultiBookInfoRCP) { val scope = rememberCoroutineScope() val nameState = remember { mutableStateOf(TextFieldValue()) } // Columnメソッドによって内部のコンポーネントを縦方向に配置 Column(Modifier.fillMaxWidth().fillMaxHeight(), Arrangement.Top, Alignment.CenterHorizontally) { // TextコンポーネントとTextFieldコンポーネントを配置 Text(stringResource(R.string.name_hint), modifier = Modifier.padding(top = 10.dp)) OutlinedTextField(nameState.value, { nameState.value = it }) // Buttonコンポーネントを配置、タップ時にSearch手続きを呼び出す Button({ scope.launch { multiBookInfoRCP.search(nameState.value.text) } }, Modifier.padding(10.dp)) { Text(stringResource(R.string.send_request)) } // ステータスが有効ならTextコンポーネントにステータスをセット if (multiBookInfoRCP.responseState.value.isNotEmpty()) { Text(multiBookInfoRCP.responseState.value) } // LazyColumnメソッドで縦方向のリストを生成 LazyColumn(modifier = Modifier.padding(all = 8.dp)) { // itemsでArrayListの各要素を順番に処理 items(multiBookInfoRCP.list) { item -> // 各要素はシェイプで囲ったTextコンポーネントで表示 Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) { Text(item, modifier = Modifier.padding(all = 4.dp)) } Spacer(modifier = Modifier.height(4.dp)) } } } }
(13)からは、UI構築関数です。@Composableというアノテーションがあるように、Jetpack Composeによる処理対象となることが明示されています。Jetpack Composeでは、従来のXMLファイルによるUI構築ではなく、コードによって動的にUIを生成します。ビューに配置するコンポーネントの指定や、タップなどのイベントに反応する処理など、これらを関数の処理内容として記述していきます。
大まかな処理内容は、コメントを参照してください。本稿はJetpack Composeの紹介が目的ではないので詳細は省略しますが、Jetpack Composeのチュートリアルが公開されていますので、合わせて参照してください。
Jetpack Compose チュートリアル - Android Developers
最後に、プロジェクトのルートにあるsettings.gradle.ktsファイルを以下のように修正します。これは、サブプロジェクトにandroid-bookinfoを加える設定変更です。
…略… include("protos", "stub", "stub-lite", "client", "native-client", "server", "stub-android", "android", "android-bookinfo") …略…
android-bookinfoをビルドして、生成されたapkファイルをAPKにインストールします。
% ./gradlew :android-bookinfo:installDebug % /Users/nao/Library/Android/sdk/platform-tools/adb install android-bookinfo/build/outputs/apk/debug/android-bookinfo-debug.apk
別ターミナルを開き、連載第4回で紹介したPythonによるBookInfoサーバを起動してください。そして、AVD上でgRPC BookInfo Kotlin Androidアプリを起動します。[キーワード]テキストボックスに適当なキーワードを入力してボタンをクリックし、検索結果がリスト形式で表示されれば成功です。
今回は、書籍情報検索サービスのクライアントを、KotlinによるAndroidアプリとして開発してみました。サーバはPythonで開発したものですが、問題なく利用できることを紹介しました。
次回は、SwiftでgRPCクライアントを開発してみます。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.