第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.
Coding Edge 記事ランキング