- 投稿日:2019-10-20T22:14:19+09:00
FirebaseのRoboTestのroboscriptで多言語対応を使えるようにしたった
はじめに
RoboTestだと多言語対応が楽そうな印象があったのでちょっとだけ頑張った話です。
一個一個キャプチャ取るのとかやですよね。
インターフェース的にもできそうじゃないですか。
絶対できるんですが、そのままじゃできなかった。
スタンダードな手法で作ったroboscriptだと多言語対応できません。問い合わせた
https://firebase.google.com/support?authuser=0
こんなメールが帰ってくる
結果
メールでのやり取りで解決しませんでした。
- roboscript
- apk
これらを送るように言われたのですが、apkを送るためにgoogleDriveを使うよう指示があったところで
会社のGSuiteを使った状態では共有が難しかったため、代替案を提供することも含めて
メール一往復で一日くらいかかることで、解決まで時間がかかりすぎ打ち切りました。
(最初に問い合わせてから4日たっていた)roboscriptが実行されていないことを確認
RoboTestを実行するデバイスとroboscriptを取得するデバイスを合わせたりしたんですがどうにもいかず、実行されたのは
モンキーテスト的な動きだけ。roboscriptでうまくいかない場合、RoboTestで実行した動画をみると
Robo Script
<実行するアクションの数> actions to performと表示した後に
Robo Script
<実行できたアクションの数> of <実行するアクションの数> actions successfullと表示され、その後にRoboTestが判断したアクションが自動で実行されます
Robo Script
0 of <実行するアクションの数> actions successfullAndroid Studioから書き出し、実行がうまくいっていないroboscriptをよく見てみることにしました。
roboscript
クリックするだけなのに、replacementTextはいるのかと
- replacementTextに
""
を設定- textの部分はそのまま
多言語で複数ロケールを指定して実行してみました。
端末設定の言語が異なる状態でRobo Script
<実行できたアクションの数> of <実行するアクションの数> actions successfullとなりました。
さいごに
Firebaseの問い合わせ時に、"日本語"で記載して問い合わせました。
Firebase担当者からの問い合わせの回答には文字化けしたメールが引用についていました。
どういう質問の仕方をしたのか、後から見てわからなくなってしまったのもあって
よくよく考えるとroboscriptでうまくいかないことだけを伝えた質問になったことで、メールでのやり取りを長引かせた可能性がありました。やり取りはgoogle翻訳を使いやすいように、というFirebaseの担当者の理由で英語でリターンされるので、こちらもgoogle翻訳で訳した英語で返信するようにしました。
- 質問は英語で
- 多言語で正しく動作しないroboscriptからは、使用した端末の言語に限定した情報を抜いてみる
もうちょっと検証は必要かと思います。公式の情報があまり充実していません。
roboscriptは構造上そんなに難しくないので、色々いじって効率の良い方法を見つけていただけると助かります。
- 投稿日:2019-10-20T21:03:26+09:00
[Android] NavigationView(Navigation Drawer)で動的にメニューを作ると押した時色がつかないのを直す
こんばんは。今解決しましたので記事にします。
それにしてもタイトルなっが。BottomNavigationBarのアプリが増えてきましたがハンバーガーメニューもかっこいいよね。
症状
静的に作ったメニュー(xml)を押す分には色が変わるんだけど、動的にメニューを作成するとなんか色がつかない。
とりあえず原因がわかるまではメニューを押した時に
isChecked
をtrueにすることで押したメニューがわかるようにしていた。
しかし静的メニューの押したときとisChecked
をtrueにしたのでは少し見た目が違うので気になる気になる。画像のように一番上のメニューみたいになってほしいんだけどなんか
isChecked
をtrueだとベスフレホームの様にテキストの色が変わるだけになってしまう。直す!!
調べたら出ました。さすがStackOverflow先生。
https://stackoverflow.com/questions/31233279/navigation-drawer-how-do-i-set-the-selected-item-at-startup動的にメニューを生成する時に、メニューの
isCheckable
をtrueにしておく必要があったようです。val item = navigationView.menu.add(//以下省略) item.isCheckable = true //これそして押した時に
isChecked
をtrueにしてあげれば完成です。item.setOnMenuItemClickListener { //チェックつける it.isChecked = true }以上です。おつ。88888888
- 投稿日:2019-10-20T20:35:10+09:00
Androidでダークモード対応のアプリを作ってみる
はじめに
- このスライドは最近流行っている?ダークテーマに ついてまとめてみたので情報整理として記録したものです。
- 参考ソース:ダークテーマ|Android Developers
ダークテーマとは
- Android Qから登場した黒を基調としたUI
- iOSでも13から対応
- アプリ以外ではすでに取り入れられているサービスもある
- Android StudioとかmacOS、Windowsとか
ダークモードのメリット(個人的に)
- 目に優しい
- ディスプレイの明るさと画面の白さを組み合わせたらあかん
- コーディングの際文字が見やすい
- 黒板に文字を書いているイメージ
- バッテリーの消費量を抑えれる(条件あり)
Androidの場合
Androidの場合
設定方法(開発側)
<!-- 通常のダークテーマ--> <style name="AppTheme" parent="Theme.AppCompat.DayNight"> <!-- MaterialComponentsのダークテーマ--> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
- Android Manifestに上記を記述
- アプリのテーマとして設定をする
- 最低SDKバージョン(minSDK)についての記載は確認できず
- 筆者は17で動作確認しましたが特にエラーはなかったです
設定方法(ユーザ側)
- Android Qを載せた端末で設定 -> ディスプレイ -> ダークテーマをON
未対応OSとの混合について
- OSがダークテーマ未対応の場合、 通常のレイアウトとして表示される。
- 現段階では影響なしと筆者は判断
使用する際の注意点
- アプリ起動時にダークテーマの切り替えを行うと アクティビティが再生成される
- ライフサイクルには注意
- 切り替え時のタイミングで処理をしたい時は別途設定が必要
切り替わりのタイミングをハンドリングする
<activity android:name=".MainActivity" android:configChanges="uiMode">
- AndroidManifestでハンドリングを行いたいActivityで上記を記述
切り替わりのタイミングをハンドリングする
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // ダークテーマがOS側で設定されているか調べる val isDarkTheme = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK when (isDarkTheme) { Configuration.UI_MODE_NIGHT_NO -> Log.d("msg", "通常モード") Configuration.UI_MODE_NIGHT_YES -> Log.d("msg", "ダークテーマ") } }
- onConfigurationChangedメソッドを宣言し、適宜処理を行う。
- 画面の縦横切り替えなどのハンドリングを行うメソッド
- 参考資料:構成の変更を処理する
まとめ
- いくつかの項目さえ気を付ければ、ダークテーマの導入は簡単
- ライフサイクル、色の相性など
- ただQ自身がまだシェアは低そうなので急いで対応する必要はなさそう
- wikipediaによると0.4%
- 黒いテーマがもっと普及するといいね!
- 投稿日:2019-10-20T20:15:10+09:00
Androidにプリビルドjarライブラリを追加してフレームワーク内で呼び出す
行うこと
- テストjarライブラリを作成してAOSPにjarライブラリを追加
- フレームワーク内(クイック設定パネル)から1で追加したライブラリのAPIの呼び出し
- AWSのライブラリを追加してフレームワーク内から呼び出してみる
1. テストjarライブラリ作成
テスト文字列"test"を返す簡易的なAPIを実装したjarライブラリを作成する。
Test.javapackage com.testlib; public class Test { public String getTestString() { return "test"; } }
- jar作成
$ javac Test.java $ jar -cvf test-lib.jar *.class上記で作成されたjarをAOSPソースの以下にコピー
/prebuilts/misc/common/test-lib/test-lib.jar
Android.mkも作成
/prebuilts/misc/common/test-lib/Android.mkLOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_PREBUILT_JAVA_LIBRARIES := test-lib$(COMMON_JAVA_PACKAGE_SUFFIX) include $(BUILD_HOST_PREBUILT)2. クイック設定パネルから1で追加したライブラリのAPIの呼び出し
2-1. ソース修正
適当に追加したクイック設定パネルのタイルをタップ時に、上記APIから取得した文字列"test"をToastで表示するようにAOSPソースに修正を入れる。
/frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/LoggingTile.java+import android.widget.Toast; ... +import com.testlib.Test; ... @Override protected void handleClick() { + Test test = new Test(); + Toast.makeText(mContext , test.getTestString(), Toast.LENGTH_SHORT).show(); try {/frameworks/base/packages/SystemUI/Android.mk... LOCAL_STATIC_JAVA_LIBRARIES := \ SystemUI-tags \ SystemUI-proto LOCAL_STATIC_JAVA_LIBRARIES += test-lib ... include $(BUILD_PACKAGE) +include $(CLEAR_VARS) +LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := test-lib:../../../../prebuilts/misc/common/test-lib/test-+lib.jar#libs/test_lib/test-lib.jar +include $(BUILD_MULTI_PREBUILT) include $(call all-makefiles-under,$(LOCAL_PATH)) ...2-2. ビルド
- ビルド
$ make -j4
- SystemUI差し替え
$ adb remount $ adb push out/target/product/bullhead/system/priv-app/SystemUI /system/priv-app/ $ adb reboot2-3. 動作確認
タイルをタップしてみる
追加したライブラリのAPIが正常に呼ばれていることが確認できる。
3. AWSのライブラリを追加してフレームワーク内から呼び出してみる
3-1. ライブラリ準備
AWS SDK for Androidの
- aws-android-sdk-core.jar
- aws-android-sdk-lambda.jar
- aws-android-sdk-s3.jar
を/prebuilts/misc/common/aws配下にコピーする。
Gsonが見つからないとエラーが出たのでGsonのjarも/prebuilts/misc/common/gsonにコピーしておく。
3-2. Lambdaを使ってみる
同様にタイルタップ時にLambdaを呼び出すように改変する。
公式参考
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-android-example.html
https://docs.aws.amazon.com/ja_jp/aws-mobile/latest/developerguide/how-to-android-lambda.html3-2. S3にbugreportを送ってみる
メモ
エラー1
起動後permission android.permission.READ_CONTACTSが~といったFatal ExceptionでSystemUIが落ちることがあったが、単にFDRしたら解決。
01-22 18:59:06.852: D/AndroidRuntime(4784): Shutting down VM 01-22 18:59:06.855: E/AndroidRuntime(4784): FATAL EXCEPTION: main 01-22 18:59:06.855: E/AndroidRuntime(4784): Process: com.android.systemui, PID: 4784 01-22 18:59:06.855: E/AndroidRuntime(4784): java.lang.RuntimeException: Unable to create service com.android.systemui.SystemUIService: android.view.InflateException: Binary XML file line #73: uid=10062 needs permission android.permission.READ_CONTACTS to read lock_screen_owner_info_enabled for user 0 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.handleCreateService(ActivityThread.java:3349) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.-wrap4(Unknown Source:0) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1677) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Handler.dispatchMessage(Handler.java:106) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Looper.loop(Looper.java:164) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.main(ActivityThread.java:6494) 01-22 18:59:06.855: E/AndroidRuntime(4784): at java.lang.reflect.Method.invoke(Native Method) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 01-22 18:59:06.855: E/AndroidRuntime(4784): Caused by: android.view.InflateException: Binary XML file line #73: uid=10062 needs permission android.permission.READ_CONTACTS to read lock_screen_owner_info_enabled for user 0 01-22 18:59:06.855: E/AndroidRuntime(4784): Caused by: java.lang.SecurityException: uid=10062 needs permission android.permission.READ_CONTACTS to read lock_screen_owner_info_enabled for user 0 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Parcel.readException(Parcel.java:2004) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Parcel.readException(Parcel.java:1950) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.widget.ILockSettings$Stub$Proxy.getBoolean(ILockSettings.java:476) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.widget.LockPatternUtils.getBoolean(LockPatternUtils.java:1271) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.widget.LockPatternUtils.isOwnerInfoEnabled(LockPatternUtils.java:738) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.keyguard.KeyguardStatusView.getOwnerInfo(KeyguardStatusView.java:273) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.keyguard.KeyguardStatusView.updateOwnerInfo(KeyguardStatusView.java:244) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.keyguard.KeyguardStatusView.onFinishInflate(KeyguardStatusView.java:170) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflate(LayoutInflater.java:876) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.parseInclude(LayoutInflater.java:995) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflate(LayoutInflater.java:859) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.parseInclude(LayoutInflater.java:995) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflate(LayoutInflater.java:859) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.inflate(LayoutInflater.java:515) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.inflate(LayoutInflater.java:423) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.LayoutInflater.inflate(LayoutInflater.java:374) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.view.View.inflate(View.java:23239) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.statusbar.phone.StatusBar.inflateStatusBarWindow(StatusBar.java:1440) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.statusbar.phone.StatusBar.makeStatusBarView(StatusBar.java:1009) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.statusbar.phone.StatusBar.addStatusBarWindow(StatusBar.java:3664) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.statusbar.phone.StatusBar.createAndAddWindows(StatusBar.java:3660) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.statusbar.phone.StatusBar.start(StatusBar.java:889) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.SystemBars.createStatusBarFromConfig(SystemBars.java:71) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.SystemBars.start(SystemBars.java:42) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.SystemUIApplication.startServicesIfNeeded(SystemUIApplication.java:215) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.SystemUIApplication.startServicesIfNeeded(SystemUIApplication.java:164) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.systemui.SystemUIService.onCreate(SystemUIService.java:33) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.handleCreateService(ActivityThread.java:3339) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.-wrap4(Unknown Source:0) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1677) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Handler.dispatchMessage(Handler.java:106) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.os.Looper.loop(Looper.java:164) 01-22 18:59:06.855: E/AndroidRuntime(4784): at android.app.ActivityThread.main(ActivityThread.java:6494) 01-22 18:59:06.855: E/AndroidRuntime(4784): at java.lang.reflect.Method.invoke(Native Method) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 01-22 18:59:06.855: E/AndroidRuntime(4784): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
- 投稿日:2019-10-20T19:58:32+09:00
Oculus Store 申請時の 'Enable Android NSC to Prevent Cleartext Traffic' という警告への対処方法
はじめに
Quest アプリを Oculus Store に提出する際に警告が出てきてしまいました。。
![]()
警告内容を見ると、ネットワークのセキュリティ設定に関するものでした![]()
The app has not enabled Android N's Network Security Configuration(https://developer.android.com/training/articles/security-config.html) feature, which forces the use of encryption (HTTPS) for all of the app's connections. The feature will block most cleartext HTTP traffic initiated by the app, which helps ensure that all data to and from the app has a base level of protection at all times.
対処方法を調べた所、
どうやら network_security_config.xml というファイルをプロジェクトに追加することで対策可能そうでした![]()
そこで、
今回は network_security_config.xml の追加手順 & 設定方法について書いていきます![]()
network_security_config.xml をプロジェクトに追加する
追加するフォルダは
/Assets/Plugins/Android/res/xml
になります
/Assets/Plugins/Android/res/xml/network_security_config.xml<?xml version="1.0" encoding="utf-8"?> <network-security-config> <!-- サブドメイン含む nikaera.com 以外のアクセスには HTTPS 通信が必須とする --> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">nikaera.com</domain> </domain-config> </network-security-config>
network_security_config.xml
に記載する内容はプロジェクトによって異なると思いますが、
私の場合は特定ドメイン以外のアクセスには HTTPS 通信を必須とするよう設定しました![]()
AndroidManifest.xml に network_security_config.xml を利用することを宣言する
プロジェクトに
network_security_config.xml
の内容を反映させるには、
AndroidManifest.xml
に 1行設定に必要な記述を追加する必要あります
/Assets/Plugins/Android/AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest> ... <!-- android:networkSecurityConfig の設定を application タグに追加する --> <application android:networkSecurityConfig="@xml/network_security_config"> ... </application> ... </manifest>
AndroidManifest.xml
のapplication
タグにandroid:networkSecurityConfig
属性を追加し、
network_security_config.xml
のファイルパスを指定することで設定が有効となります![]()
この状態でビルドを行い APK の動作に問題が無いこと確認出来次第、
Oculus Store に再度アップロードしてみると、警告が無くなっていることが確認できるはずです![]()
おわりに
今回は
Enable Android NSC to Prevent Cleartext Traffic
の対処法について紹介しました![]()
実は他にも警告が出ているのですが、どうやらどれも Android に関わるもののようで、
Oculus Store 申請時は、「ココらへんも気をつけないといけないんだなあ」と思いました。。参考リンク
- 投稿日:2019-10-20T15:26:56+09:00
KotlinでProtocol Buffersを使う(Wire 3.0.x)
この記事の内容
Square社のProtocol Buffers実装であるWireが、3.0.0でKotlinに対応しました。
これまでKotlinメインのプロジェクトでもproto生成ファイルだけはJavaコードを使用することが多かったと思いますが(実際、Kotlin protobuf
等で検索しても、出てくる記事の多くはJavaを使っていました)、Kotlin化できるかもー! ということで使ってみました。
この記事では、執筆時点での最新版 Wire 3.0.1 を使用しています。現在も絶賛開発中なので、導入される場合は本家サイトの状況をご確認ください。Kotlinファイルの生成
公式では wire-gradle-plugin での導入が紹介されていますが、執筆時点では
plugins { id 'com.squareup.wire' }
では参照できずclasspath
+apply plugin
形式で記述することでビルドできました(gradle plugin検索でもヒットしなかったので、まだ登録されていないようです)。
また、gradle pluginではproto3の変換はできず、proto3を変換しようとすると以下のメッセージが出てスキップされます。Skipped .proto files with unsupported syntax. Add this line to fix:
syntax = "proto2";proto3対応自体がin progressのようなので、ここは今後に期待です。
この記事では、以前のバージョンと同じように、コマンドラインでjarを実行する方法でコードを生成していきます。
コマンド実行時の違いは、--java_out
オプションを--kotlin_out
に変更するだけです。--java_out
と--kotlin_out
を同時に指定することはできません。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --kotlin_out=src/main/kotlin生成されたJavaコードとKotlinコードの違い
生成されたJavaとKotlinのコードを比較するため、以下の簡単なprotoファイルからそれぞれのファイルを生成してみます。
proto2_sample.protosyntax = "proto2"; package com.github.kazukinr.sample; option java_package = "com.github.kazukinr.sample.proto"; message Proto2Sample { required int64 id = 1; optional string name = 2; repeated string roles = 3; }Java
Android用の
--android
オプション(Parcelable
が実装されます)、および3.0.0で追加された--android-annotations
オプションをつけて生成した結果を記載します。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --java_out=src/main/java \ --android \ --android-annotations出力結果
Proto2Sample.java// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto2_sample.proto package com.github.kazukinr.sample.proto; import android.os.Parcelable; import androidx.annotation.Nullable; import com.squareup.wire.AndroidMessage; import com.squareup.wire.FieldEncoding; import com.squareup.wire.Message; import com.squareup.wire.ProtoAdapter; import com.squareup.wire.ProtoReader; import com.squareup.wire.ProtoWriter; import com.squareup.wire.WireField; import com.squareup.wire.internal.Internal; import java.io.IOException; import java.lang.Long; import java.lang.Object; import java.lang.Override; import java.lang.String; import java.lang.StringBuilder; import java.util.List; import okio.ByteString; public final class Proto2Sample extends AndroidMessage<Proto2Sample, Proto2Sample.Builder> { public static final ProtoAdapter<Proto2Sample> ADAPTER = new ProtoAdapter_Proto2Sample(); public static final Parcelable.Creator<Proto2Sample> CREATOR = AndroidMessage.newCreator(ADAPTER); private static final long serialVersionUID = 0L; public static final Long DEFAULT_ID = 0L; public static final String DEFAULT_NAME = ""; @WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64", label = WireField.Label.REQUIRED ) public final Long id; @WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) @Nullable public final String name; @WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) public final List<String> roles; public Proto2Sample(Long id, @Nullable String name, List<String> roles) { this(id, name, roles, ByteString.EMPTY); } public Proto2Sample(Long id, @Nullable String name, List<String> roles, ByteString unknownFields) { super(ADAPTER, unknownFields); this.id = id; this.name = name; this.roles = Internal.immutableCopyOf("roles", roles); } @Override public Builder newBuilder() { Builder builder = new Builder(); builder.id = id; builder.name = name; builder.roles = Internal.copyOf(roles); builder.addUnknownFields(unknownFields()); return builder; } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof Proto2Sample)) return false; Proto2Sample o = (Proto2Sample) other; return unknownFields().equals(o.unknownFields()) && id.equals(o.id) && Internal.equals(name, o.name) && roles.equals(o.roles); } @Override public int hashCode() { int result = super.hashCode; if (result == 0) { result = unknownFields().hashCode(); result = result * 37 + id.hashCode(); result = result * 37 + (name != null ? name.hashCode() : 0); result = result * 37 + roles.hashCode(); super.hashCode = result; } return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(", id=").append(id); if (name != null) builder.append(", name=").append(name); if (!roles.isEmpty()) builder.append(", roles=").append(roles); return builder.replace(0, 2, "Proto2Sample{").append('}').toString(); } public static final class Builder extends Message.Builder<Proto2Sample, Builder> { public Long id; public String name; public List<String> roles; public Builder() { roles = Internal.newMutableList(); } public Builder id(Long id) { this.id = id; return this; } public Builder name(String name) { this.name = name; return this; } public Builder roles(List<String> roles) { Internal.checkElementsNotNull(roles); this.roles = roles; return this; } @Override public Proto2Sample build() { if (id == null) { throw Internal.missingRequiredFields(id, "id"); } return new Proto2Sample(id, name, roles, super.buildUnknownFields()); } } private static final class ProtoAdapter_Proto2Sample extends ProtoAdapter<Proto2Sample> { public ProtoAdapter_Proto2Sample() { super(FieldEncoding.LENGTH_DELIMITED, Proto2Sample.class); } @Override public int encodedSize(Proto2Sample value) { return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields().size(); } @Override public void encode(ProtoWriter writer, Proto2Sample value) throws IOException { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id); ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name); ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles); writer.writeBytes(value.unknownFields()); } @Override public Proto2Sample decode(ProtoReader reader) throws IOException { Builder builder = new Builder(); long token = reader.beginMessage(); for (int tag; (tag = reader.nextTag()) != -1;) { switch (tag) { case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break; case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break; case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break; default: { reader.readUnknownField(tag); } } } builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token)); return builder.build(); } @Override public Proto2Sample redact(Proto2Sample value) { Builder builder = value.newBuilder(); builder.clearUnknownFields(); return builder.build(); } } }
optional
を定義したnameフィールドに@Nullable
アノテーションが指定されているのがわかります。Kotlin
こちらも
--android
オプションを指定して生成した結果を記載します。Kotlinの場合、--android-annotations
オプションの有無による生成ファイルの違いはありませんでした。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --java_out=src/main/java \ --android出力結果
Proto2Sample.kt// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto2_sample.proto package com.github.kazukinr.sample.proto import android.os.Parcelable import com.squareup.wire.AndroidMessage import com.squareup.wire.FieldEncoding import com.squareup.wire.ProtoAdapter import com.squareup.wire.ProtoReader import com.squareup.wire.ProtoWriter import com.squareup.wire.WireField import com.squareup.wire.internal.missingRequiredFields import kotlin.Any import kotlin.AssertionError import kotlin.Boolean import kotlin.Deprecated import kotlin.DeprecationLevel import kotlin.Int import kotlin.Long import kotlin.Nothing import kotlin.String import kotlin.collections.List import kotlin.hashCode import kotlin.jvm.JvmField import okio.ByteString class Proto2Sample( @field:WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64", label = WireField.Label.REQUIRED ) val id: Long, @field:WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) val name: String? = null, @field:WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) val roles: List<String> = emptyList(), unknownFields: ByteString = ByteString.EMPTY ) : AndroidMessage<Proto2Sample, Nothing>(ADAPTER, unknownFields) { @Deprecated( message = "Shouldn't be used in Kotlin", level = DeprecationLevel.HIDDEN ) override fun newBuilder(): Nothing = throw AssertionError() override fun equals(other: Any?): Boolean { if (other === this) return true if (other !is Proto2Sample) return false return unknownFields == other.unknownFields && id == other.id && name == other.name && roles == other.roles } override fun hashCode(): Int { var result = super.hashCode if (result == 0) { result = unknownFields.hashCode() result = result * 37 + id.hashCode() result = result * 37 + name.hashCode() result = result * 37 + roles.hashCode() super.hashCode = result } return result } override fun toString(): String { val result = mutableListOf<String>() result += """id=$id""" if (name != null) result += """name=$name""" if (roles.isNotEmpty()) result += """roles=$roles""" return result.joinToString(prefix = "Proto2Sample{", separator = ", ", postfix = "}") } fun copy( id: Long = this.id, name: String? = this.name, roles: List<String> = this.roles, unknownFields: ByteString = this.unknownFields ): Proto2Sample = Proto2Sample(id, name, roles, unknownFields) companion object { @JvmField val ADAPTER: ProtoAdapter<Proto2Sample> = object : ProtoAdapter<Proto2Sample>( FieldEncoding.LENGTH_DELIMITED, Proto2Sample::class ) { override fun encodedSize(value: Proto2Sample): Int = ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields.size override fun encode(writer: ProtoWriter, value: Proto2Sample) { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id) ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name) ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles) writer.writeBytes(value.unknownFields) } override fun decode(reader: ProtoReader): Proto2Sample { var id: Long? = null var name: String? = null val roles = mutableListOf<String>() val unknownFields = reader.forEachTag { tag -> when (tag) { 1 -> id = ProtoAdapter.INT64.decode(reader) 2 -> name = ProtoAdapter.STRING.decode(reader) 3 -> roles.add(ProtoAdapter.STRING.decode(reader)) else -> reader.readUnknownField(tag) } } return Proto2Sample( id = id ?: throw missingRequiredFields(id, "id"), name = name, roles = roles, unknownFields = unknownFields ) } override fun redact(value: Proto2Sample): Proto2Sample = value.copy( unknownFields = ByteString.EMPTY ) } @JvmField val CREATOR: Parcelable.Creator<Proto2Sample> = AndroidMessage.newCreator(ADAPTER) } }
required
、optional
の設定がKotlinでも同様に定義されています。
また、optional propertyに関してはコンストラクタでデフォルト引数としてnullが設定されています。
newBuilder()
は例外を投げるようになり、かわりにcopy
関数が定義されています。生成ファイル使用時の違い
protoの参照
参照に関しては、JavaとKotlinで大きな違いはありません。Javaコードを生成した場合もKotlinからはプロパティ参照していると思うので、Java参照時にきちんとnullをケアしている場合は、Kotlin変更時にコードを修正する必要はなさそうです。nullチェックがコンパイル時に走るため(これまで
@Nullable
がついてないのであれば)、より安全にコードを書くことができます。protoの生成
JavaコードではBuilderパターンが使用されていましたが、KotlinではBuilderは生成されず、proto生成時にはコンストラクタに必要な項目を名前付き引数で与えることになります。optional propertyにはデフォルト値が設定されているので、必要な項目だけ指定すれば大丈夫です。
// Proto2Sample.javaを使用 val proto = Proto2Sample.Builder() .id("id") .name("name") .roles(listOf("user", "staff")) .build() // Proto2Sample.ktを使用 val proto = Proto2Sample( id = "id", name = "name", roles = listOf("user", "staff") )protoのコピー・一部更新
生成と同様にprotoのコピーや一部propertyの更新も書き方が変わります。
// Proto2Sample.javaを使用 val proto2 = proto.newBuilder() .name("name2") .build() // Proto2Sample.ktを使用 val proto2 = proto.copy(name = "name2")
copy
が定義されているので、data class
のような使い方ができます。proto3 syntax
proto3形式についても同様に出力した結果を記載します。前述しましたが、proto3対応はまだ完全ではないようで、一部期待通りの出力になっていない部分もありました。
元ネタのprotoファイルです。
proto3_sample.protosyntax = "proto3"; package com.github.kazukinr.sample; option java_package = "com.github.kazukinr.sample.proto"; message Proto3Sample { int64 id = 1; string name = 2; repeated string roles = 3; }proto3では
required
、optional
は廃止されており、すべてoptional
扱いとなります。Java
Proto3Sample.java// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto3_sample.proto package com.github.kazukinr.sample.proto; import android.os.Parcelable; import com.squareup.wire.AndroidMessage; import com.squareup.wire.FieldEncoding; import com.squareup.wire.Message; import com.squareup.wire.ProtoAdapter; import com.squareup.wire.ProtoReader; import com.squareup.wire.ProtoWriter; import com.squareup.wire.WireField; import com.squareup.wire.internal.Internal; import java.io.IOException; import java.lang.Long; import java.lang.Object; import java.lang.Override; import java.lang.String; import java.lang.StringBuilder; import java.util.List; import okio.ByteString; public final class Proto3Sample extends AndroidMessage<Proto3Sample, Proto3Sample.Builder> { public static final ProtoAdapter<Proto3Sample> ADAPTER = new ProtoAdapter_Proto3Sample(); public static final Parcelable.Creator<Proto3Sample> CREATOR = AndroidMessage.newCreator(ADAPTER); private static final long serialVersionUID = 0L; public static final Long DEFAULT_ID = 0L; public static final String DEFAULT_NAME = ""; @WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64" ) public final Long id; @WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) public final String name; @WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) public final List<String> roles; public Proto3Sample(Long id, String name, List<String> roles) { this(id, name, roles, ByteString.EMPTY); } public Proto3Sample(Long id, String name, List<String> roles, ByteString unknownFields) { super(ADAPTER, unknownFields); this.id = id; this.name = name; this.roles = Internal.immutableCopyOf("roles", roles); } @Override public Builder newBuilder() { Builder builder = new Builder(); builder.id = id; builder.name = name; builder.roles = Internal.copyOf(roles); builder.addUnknownFields(unknownFields()); return builder; } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof Proto3Sample)) return false; Proto3Sample o = (Proto3Sample) other; return unknownFields().equals(o.unknownFields()) && Internal.equals(id, o.id) && Internal.equals(name, o.name) && roles.equals(o.roles); } @Override public int hashCode() { int result = super.hashCode; if (result == 0) { result = unknownFields().hashCode(); result = result * 37 + (id != null ? id.hashCode() : 0); result = result * 37 + (name != null ? name.hashCode() : 0); result = result * 37 + roles.hashCode(); super.hashCode = result; } return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); if (id != null) builder.append(", id=").append(id); if (name != null) builder.append(", name=").append(name); if (!roles.isEmpty()) builder.append(", roles=").append(roles); return builder.replace(0, 2, "Proto3Sample{").append('}').toString(); } public static final class Builder extends Message.Builder<Proto3Sample, Builder> { public Long id; public String name; public List<String> roles; public Builder() { roles = Internal.newMutableList(); } public Builder id(Long id) { this.id = id; return this; } public Builder name(String name) { this.name = name; return this; } public Builder roles(List<String> roles) { Internal.checkElementsNotNull(roles); this.roles = roles; return this; } @Override public Proto3Sample build() { return new Proto3Sample(id, name, roles, super.buildUnknownFields()); } } private static final class ProtoAdapter_Proto3Sample extends ProtoAdapter<Proto3Sample> { public ProtoAdapter_Proto3Sample() { super(FieldEncoding.LENGTH_DELIMITED, Proto3Sample.class); } @Override public int encodedSize(Proto3Sample value) { return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields().size(); } @Override public void encode(ProtoWriter writer, Proto3Sample value) throws IOException { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id); ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name); ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles); writer.writeBytes(value.unknownFields()); } @Override public Proto3Sample decode(ProtoReader reader) throws IOException { Builder builder = new Builder(); long token = reader.beginMessage(); for (int tag; (tag = reader.nextTag()) != -1;) { switch (tag) { case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break; case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break; case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break; default: { reader.readUnknownField(tag); } } } builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token)); return builder.build(); } @Override public Proto3Sample redact(Proto3Sample value) { Builder builder = value.newBuilder(); builder.clearUnknownFields(); return builder.build(); } } }proto3では繰り返しのない全てのフィールドは
optional
扱いでnullが入る可能性がありますが、--android-annotations
オプションをつけても残念ながら@Nullable
アノテーションは追加されませんでした。現時点では明示的にoptional
指定された場合のみアノテーションがつくような挙動になっているようです。
やはり、proto3対応に関しては今後に期待ということになります。Kotlin
Proto3Sample.kt// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto3_sample.proto package com.github.kazukinr.sample.proto import android.os.Parcelable import com.squareup.wire.AndroidMessage import com.squareup.wire.FieldEncoding import com.squareup.wire.ProtoAdapter import com.squareup.wire.ProtoReader import com.squareup.wire.ProtoWriter import com.squareup.wire.WireField import kotlin.Any import kotlin.AssertionError import kotlin.Boolean import kotlin.Deprecated import kotlin.DeprecationLevel import kotlin.Int import kotlin.Long import kotlin.Nothing import kotlin.String import kotlin.collections.List import kotlin.hashCode import kotlin.jvm.JvmField import okio.ByteString class Proto3Sample( @field:WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64" ) val id: Long? = null, @field:WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) val name: String? = null, @field:WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) val roles: List<String> = emptyList(), unknownFields: ByteString = ByteString.EMPTY ) : AndroidMessage<Proto3Sample, Nothing>(ADAPTER, unknownFields) { @Deprecated( message = "Shouldn't be used in Kotlin", level = DeprecationLevel.HIDDEN ) override fun newBuilder(): Nothing = throw AssertionError() override fun equals(other: Any?): Boolean { if (other === this) return true if (other !is Proto3Sample) return false return unknownFields == other.unknownFields && id == other.id && name == other.name && roles == other.roles } override fun hashCode(): Int { var result = super.hashCode if (result == 0) { result = unknownFields.hashCode() result = result * 37 + id.hashCode() result = result * 37 + name.hashCode() result = result * 37 + roles.hashCode() super.hashCode = result } return result } override fun toString(): String { val result = mutableListOf<String>() if (id != null) result += """id=$id""" if (name != null) result += """name=$name""" if (roles.isNotEmpty()) result += """roles=$roles""" return result.joinToString(prefix = "Proto3Sample{", separator = ", ", postfix = "}") } fun copy( id: Long? = this.id, name: String? = this.name, roles: List<String> = this.roles, unknownFields: ByteString = this.unknownFields ): Proto3Sample = Proto3Sample(id, name, roles, unknownFields) companion object { @JvmField val ADAPTER: ProtoAdapter<Proto3Sample> = object : ProtoAdapter<Proto3Sample>( FieldEncoding.LENGTH_DELIMITED, Proto3Sample::class ) { override fun encodedSize(value: Proto3Sample): Int = ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields.size override fun encode(writer: ProtoWriter, value: Proto3Sample) { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id) ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name) ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles) writer.writeBytes(value.unknownFields) } override fun decode(reader: ProtoReader): Proto3Sample { var id: Long? = null var name: String? = null val roles = mutableListOf<String>() val unknownFields = reader.forEachTag { tag -> when (tag) { 1 -> id = ProtoAdapter.INT64.decode(reader) 2 -> name = ProtoAdapter.STRING.decode(reader) 3 -> roles.add(ProtoAdapter.STRING.decode(reader)) else -> reader.readUnknownField(tag) } } return Proto3Sample( id = id, name = name, roles = roles, unknownFields = unknownFields ) } override fun redact(value: Proto3Sample): Proto3Sample = value.copy( unknownFields = ByteString.EMPTY ) } @JvmField val CREATOR: Parcelable.Creator<Proto3Sample> = AndroidMessage.newCreator(ADAPTER) } }Kotlinコードとして出力した場合、きちんと
optional
として出力されました。null安全という観点だけで言えば、proto3を出力する場合はKotlinのほうが安全です(が、Kotlin生成自体がリリース直後のため、その他の安定性では劣ると思います)。冒頭でも記述しましたが、Kotlin対応、proto3対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。
- 投稿日:2019-10-20T15:26:56+09:00
Protocol BuffersをKotlinで出力する(Wire 3.0.x)
この記事の内容
Square社のProtocol Buffers実装であるWireが、3.0.0でKotlinに対応しました。
これまでKotlinメインのプロジェクトでもproto生成ファイルだけはJavaコードを使用することが多かったと思いますが(実際、Kotlin protobuf
等で検索しても、出てくる記事の多くはJavaを使っていました)、Kotlin化できるかもー! ということで使ってみました。
この記事では、執筆時点での最新版 Wire 3.0.1 を使用しています。現在も絶賛開発中なので、導入される場合は本家サイトの状況をご確認ください。Kotlinファイルの生成
公式では wire-gradle-plugin での導入が紹介されていますが、執筆時点では
plugins { id 'com.squareup.wire' }
では参照できずclasspath
+apply plugin
形式で記述することでビルドできました(gradle plugin検索でもヒットしなかったので、まだ登録されていないようです)。
また、gradle pluginではproto3の変換はできず、proto3を変換しようとすると以下のメッセージが出てスキップされます。Skipped .proto files with unsupported syntax. Add this line to fix:
syntax = "proto2";proto3対応自体がin progressのようなので、ここは今後に期待です。
この記事では、以前のバージョンと同じように、コマンドラインでjarを実行する方法でコードを生成していきます。
コマンド実行時の違いは、--java_out
オプションを--kotlin_out
に変更するだけです。--java_out
と--kotlin_out
を同時に指定することはできません。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --kotlin_out=src/main/kotlin生成されたJavaコードとKotlinコードの違い
生成されたJavaとKotlinのコードを比較するため、以下の簡単なprotoファイルからそれぞれのファイルを生成してみます。
proto2_sample.protosyntax = "proto2"; package com.github.kazukinr.sample; option java_package = "com.github.kazukinr.sample.proto"; message Proto2Sample { required int64 id = 1; optional string name = 2; repeated string roles = 3; }Java
Android用の
--android
オプション(Parcelable
が実装されます)、および3.0.0で追加された--android-annotations
オプションをつけて生成した結果を記載します。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --java_out=src/main/java \ --android \ --android-annotations出力結果
Proto2Sample.java// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto2_sample.proto package com.github.kazukinr.sample.proto; import android.os.Parcelable; import androidx.annotation.Nullable; import com.squareup.wire.AndroidMessage; import com.squareup.wire.FieldEncoding; import com.squareup.wire.Message; import com.squareup.wire.ProtoAdapter; import com.squareup.wire.ProtoReader; import com.squareup.wire.ProtoWriter; import com.squareup.wire.WireField; import com.squareup.wire.internal.Internal; import java.io.IOException; import java.lang.Long; import java.lang.Object; import java.lang.Override; import java.lang.String; import java.lang.StringBuilder; import java.util.List; import okio.ByteString; public final class Proto2Sample extends AndroidMessage<Proto2Sample, Proto2Sample.Builder> { public static final ProtoAdapter<Proto2Sample> ADAPTER = new ProtoAdapter_Proto2Sample(); public static final Parcelable.Creator<Proto2Sample> CREATOR = AndroidMessage.newCreator(ADAPTER); private static final long serialVersionUID = 0L; public static final Long DEFAULT_ID = 0L; public static final String DEFAULT_NAME = ""; @WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64", label = WireField.Label.REQUIRED ) public final Long id; @WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) @Nullable public final String name; @WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) public final List<String> roles; public Proto2Sample(Long id, @Nullable String name, List<String> roles) { this(id, name, roles, ByteString.EMPTY); } public Proto2Sample(Long id, @Nullable String name, List<String> roles, ByteString unknownFields) { super(ADAPTER, unknownFields); this.id = id; this.name = name; this.roles = Internal.immutableCopyOf("roles", roles); } @Override public Builder newBuilder() { Builder builder = new Builder(); builder.id = id; builder.name = name; builder.roles = Internal.copyOf(roles); builder.addUnknownFields(unknownFields()); return builder; } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof Proto2Sample)) return false; Proto2Sample o = (Proto2Sample) other; return unknownFields().equals(o.unknownFields()) && id.equals(o.id) && Internal.equals(name, o.name) && roles.equals(o.roles); } @Override public int hashCode() { int result = super.hashCode; if (result == 0) { result = unknownFields().hashCode(); result = result * 37 + id.hashCode(); result = result * 37 + (name != null ? name.hashCode() : 0); result = result * 37 + roles.hashCode(); super.hashCode = result; } return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(", id=").append(id); if (name != null) builder.append(", name=").append(name); if (!roles.isEmpty()) builder.append(", roles=").append(roles); return builder.replace(0, 2, "Proto2Sample{").append('}').toString(); } public static final class Builder extends Message.Builder<Proto2Sample, Builder> { public Long id; public String name; public List<String> roles; public Builder() { roles = Internal.newMutableList(); } public Builder id(Long id) { this.id = id; return this; } public Builder name(String name) { this.name = name; return this; } public Builder roles(List<String> roles) { Internal.checkElementsNotNull(roles); this.roles = roles; return this; } @Override public Proto2Sample build() { if (id == null) { throw Internal.missingRequiredFields(id, "id"); } return new Proto2Sample(id, name, roles, super.buildUnknownFields()); } } private static final class ProtoAdapter_Proto2Sample extends ProtoAdapter<Proto2Sample> { public ProtoAdapter_Proto2Sample() { super(FieldEncoding.LENGTH_DELIMITED, Proto2Sample.class); } @Override public int encodedSize(Proto2Sample value) { return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields().size(); } @Override public void encode(ProtoWriter writer, Proto2Sample value) throws IOException { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id); ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name); ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles); writer.writeBytes(value.unknownFields()); } @Override public Proto2Sample decode(ProtoReader reader) throws IOException { Builder builder = new Builder(); long token = reader.beginMessage(); for (int tag; (tag = reader.nextTag()) != -1;) { switch (tag) { case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break; case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break; case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break; default: { reader.readUnknownField(tag); } } } builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token)); return builder.build(); } @Override public Proto2Sample redact(Proto2Sample value) { Builder builder = value.newBuilder(); builder.clearUnknownFields(); return builder.build(); } } }
optional
を定義したnameフィールドに@Nullable
アノテーションが指定されているのがわかります。Kotlin
こちらも
--android
オプションを指定して生成した結果を記載します。Kotlinの場合、--android-annotations
オプションの有無による生成ファイルの違いはありませんでした。$ java -jar wire-compiler-3.0.1-jar-with-dependencies.jar \ --proto_path=src/main/proto \ --java_out=src/main/java \ --android出力結果
Proto2Sample.kt// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto2_sample.proto package com.github.kazukinr.sample.proto import android.os.Parcelable import com.squareup.wire.AndroidMessage import com.squareup.wire.FieldEncoding import com.squareup.wire.ProtoAdapter import com.squareup.wire.ProtoReader import com.squareup.wire.ProtoWriter import com.squareup.wire.WireField import com.squareup.wire.internal.missingRequiredFields import kotlin.Any import kotlin.AssertionError import kotlin.Boolean import kotlin.Deprecated import kotlin.DeprecationLevel import kotlin.Int import kotlin.Long import kotlin.Nothing import kotlin.String import kotlin.collections.List import kotlin.hashCode import kotlin.jvm.JvmField import okio.ByteString class Proto2Sample( @field:WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64", label = WireField.Label.REQUIRED ) val id: Long, @field:WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) val name: String? = null, @field:WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) val roles: List<String> = emptyList(), unknownFields: ByteString = ByteString.EMPTY ) : AndroidMessage<Proto2Sample, Nothing>(ADAPTER, unknownFields) { @Deprecated( message = "Shouldn't be used in Kotlin", level = DeprecationLevel.HIDDEN ) override fun newBuilder(): Nothing = throw AssertionError() override fun equals(other: Any?): Boolean { if (other === this) return true if (other !is Proto2Sample) return false return unknownFields == other.unknownFields && id == other.id && name == other.name && roles == other.roles } override fun hashCode(): Int { var result = super.hashCode if (result == 0) { result = unknownFields.hashCode() result = result * 37 + id.hashCode() result = result * 37 + name.hashCode() result = result * 37 + roles.hashCode() super.hashCode = result } return result } override fun toString(): String { val result = mutableListOf<String>() result += """id=$id""" if (name != null) result += """name=$name""" if (roles.isNotEmpty()) result += """roles=$roles""" return result.joinToString(prefix = "Proto2Sample{", separator = ", ", postfix = "}") } fun copy( id: Long = this.id, name: String? = this.name, roles: List<String> = this.roles, unknownFields: ByteString = this.unknownFields ): Proto2Sample = Proto2Sample(id, name, roles, unknownFields) companion object { @JvmField val ADAPTER: ProtoAdapter<Proto2Sample> = object : ProtoAdapter<Proto2Sample>( FieldEncoding.LENGTH_DELIMITED, Proto2Sample::class ) { override fun encodedSize(value: Proto2Sample): Int = ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields.size override fun encode(writer: ProtoWriter, value: Proto2Sample) { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id) ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name) ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles) writer.writeBytes(value.unknownFields) } override fun decode(reader: ProtoReader): Proto2Sample { var id: Long? = null var name: String? = null val roles = mutableListOf<String>() val unknownFields = reader.forEachTag { tag -> when (tag) { 1 -> id = ProtoAdapter.INT64.decode(reader) 2 -> name = ProtoAdapter.STRING.decode(reader) 3 -> roles.add(ProtoAdapter.STRING.decode(reader)) else -> reader.readUnknownField(tag) } } return Proto2Sample( id = id ?: throw missingRequiredFields(id, "id"), name = name, roles = roles, unknownFields = unknownFields ) } override fun redact(value: Proto2Sample): Proto2Sample = value.copy( unknownFields = ByteString.EMPTY ) } @JvmField val CREATOR: Parcelable.Creator<Proto2Sample> = AndroidMessage.newCreator(ADAPTER) } }
required
、optional
の設定がKotlinでも同様に定義されています。
また、optional propertyに関してはコンストラクタでデフォルト引数としてnullが設定されています。
newBuilder()
は例外を投げるようになり、かわりにcopy
関数が定義されています。生成ファイル使用時の違い
protoの参照
参照に関しては、JavaとKotlinで大きな違いはありません。Javaコードを生成した場合もKotlinからはプロパティ参照していると思うので、Java参照時にきちんとnullをケアしている場合は、Kotlin変更時にコードを修正する必要はなさそうです。nullチェックがコンパイル時に走るため(これまで
@Nullable
がついてないのであれば)、より安全にコードを書くことができます。protoの生成
JavaコードではBuilderパターンが使用されていましたが、KotlinではBuilderは生成されず、proto生成時にはコンストラクタに必要な項目を名前付き引数で与えることになります。optional propertyにはデフォルト値が設定されているので、必要な項目だけ指定すれば大丈夫です。
// Proto2Sample.javaを使用 val proto = Proto2Sample.Builder() .id("id") .name("name") .roles(listOf("user", "staff")) .build() // Proto2Sample.ktを使用 val proto = Proto2Sample( id = "id", name = "name", roles = listOf("user", "staff") )protoのコピー・一部更新
生成と同様にprotoのコピーや一部propertyの更新も書き方が変わります。
// Proto2Sample.javaを使用 val proto2 = proto.newBuilder() .name("name2") .build() // Proto2Sample.ktを使用 val proto2 = proto.copy(name = "name2")
copy
が定義されているので、data class
のような使い方ができます。proto3 syntax
proto3形式についても同様に出力した結果を記載します。前述しましたが、proto3対応はまだ完全ではないようで、一部期待通りの出力になっていない部分もありました。
元ネタのprotoファイルです。
proto3_sample.protosyntax = "proto3"; package com.github.kazukinr.sample; option java_package = "com.github.kazukinr.sample.proto"; message Proto3Sample { int64 id = 1; string name = 2; repeated string roles = 3; }proto3では
required
、optional
は廃止されており、すべてoptional
扱いとなります。Java
Proto3Sample.java// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto3_sample.proto package com.github.kazukinr.sample.proto; import android.os.Parcelable; import com.squareup.wire.AndroidMessage; import com.squareup.wire.FieldEncoding; import com.squareup.wire.Message; import com.squareup.wire.ProtoAdapter; import com.squareup.wire.ProtoReader; import com.squareup.wire.ProtoWriter; import com.squareup.wire.WireField; import com.squareup.wire.internal.Internal; import java.io.IOException; import java.lang.Long; import java.lang.Object; import java.lang.Override; import java.lang.String; import java.lang.StringBuilder; import java.util.List; import okio.ByteString; public final class Proto3Sample extends AndroidMessage<Proto3Sample, Proto3Sample.Builder> { public static final ProtoAdapter<Proto3Sample> ADAPTER = new ProtoAdapter_Proto3Sample(); public static final Parcelable.Creator<Proto3Sample> CREATOR = AndroidMessage.newCreator(ADAPTER); private static final long serialVersionUID = 0L; public static final Long DEFAULT_ID = 0L; public static final String DEFAULT_NAME = ""; @WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64" ) public final Long id; @WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) public final String name; @WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) public final List<String> roles; public Proto3Sample(Long id, String name, List<String> roles) { this(id, name, roles, ByteString.EMPTY); } public Proto3Sample(Long id, String name, List<String> roles, ByteString unknownFields) { super(ADAPTER, unknownFields); this.id = id; this.name = name; this.roles = Internal.immutableCopyOf("roles", roles); } @Override public Builder newBuilder() { Builder builder = new Builder(); builder.id = id; builder.name = name; builder.roles = Internal.copyOf(roles); builder.addUnknownFields(unknownFields()); return builder; } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof Proto3Sample)) return false; Proto3Sample o = (Proto3Sample) other; return unknownFields().equals(o.unknownFields()) && Internal.equals(id, o.id) && Internal.equals(name, o.name) && roles.equals(o.roles); } @Override public int hashCode() { int result = super.hashCode; if (result == 0) { result = unknownFields().hashCode(); result = result * 37 + (id != null ? id.hashCode() : 0); result = result * 37 + (name != null ? name.hashCode() : 0); result = result * 37 + roles.hashCode(); super.hashCode = result; } return result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); if (id != null) builder.append(", id=").append(id); if (name != null) builder.append(", name=").append(name); if (!roles.isEmpty()) builder.append(", roles=").append(roles); return builder.replace(0, 2, "Proto3Sample{").append('}').toString(); } public static final class Builder extends Message.Builder<Proto3Sample, Builder> { public Long id; public String name; public List<String> roles; public Builder() { roles = Internal.newMutableList(); } public Builder id(Long id) { this.id = id; return this; } public Builder name(String name) { this.name = name; return this; } public Builder roles(List<String> roles) { Internal.checkElementsNotNull(roles); this.roles = roles; return this; } @Override public Proto3Sample build() { return new Proto3Sample(id, name, roles, super.buildUnknownFields()); } } private static final class ProtoAdapter_Proto3Sample extends ProtoAdapter<Proto3Sample> { public ProtoAdapter_Proto3Sample() { super(FieldEncoding.LENGTH_DELIMITED, Proto3Sample.class); } @Override public int encodedSize(Proto3Sample value) { return ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields().size(); } @Override public void encode(ProtoWriter writer, Proto3Sample value) throws IOException { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id); ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name); ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles); writer.writeBytes(value.unknownFields()); } @Override public Proto3Sample decode(ProtoReader reader) throws IOException { Builder builder = new Builder(); long token = reader.beginMessage(); for (int tag; (tag = reader.nextTag()) != -1;) { switch (tag) { case 1: builder.id(ProtoAdapter.INT64.decode(reader)); break; case 2: builder.name(ProtoAdapter.STRING.decode(reader)); break; case 3: builder.roles.add(ProtoAdapter.STRING.decode(reader)); break; default: { reader.readUnknownField(tag); } } } builder.addUnknownFields(reader.endMessageAndGetUnknownFields(token)); return builder.build(); } @Override public Proto3Sample redact(Proto3Sample value) { Builder builder = value.newBuilder(); builder.clearUnknownFields(); return builder.build(); } } }proto3では繰り返しのない全てのフィールドは
optional
扱いでnullが入る可能性がありますが、--android-annotations
オプションをつけても残念ながら@Nullable
アノテーションは追加されませんでした。現時点では明示的にoptional
指定された場合のみアノテーションがつくような挙動になっているようです。
やはり、proto3対応に関しては今後に期待ということになります。Kotlin
Proto3Sample.kt// Code generated by Wire protocol buffer compiler, do not edit. // Source file: proto3_sample.proto package com.github.kazukinr.sample.proto import android.os.Parcelable import com.squareup.wire.AndroidMessage import com.squareup.wire.FieldEncoding import com.squareup.wire.ProtoAdapter import com.squareup.wire.ProtoReader import com.squareup.wire.ProtoWriter import com.squareup.wire.WireField import kotlin.Any import kotlin.AssertionError import kotlin.Boolean import kotlin.Deprecated import kotlin.DeprecationLevel import kotlin.Int import kotlin.Long import kotlin.Nothing import kotlin.String import kotlin.collections.List import kotlin.hashCode import kotlin.jvm.JvmField import okio.ByteString class Proto3Sample( @field:WireField( tag = 1, adapter = "com.squareup.wire.ProtoAdapter#INT64" ) val id: Long? = null, @field:WireField( tag = 2, adapter = "com.squareup.wire.ProtoAdapter#STRING" ) val name: String? = null, @field:WireField( tag = 3, adapter = "com.squareup.wire.ProtoAdapter#STRING", label = WireField.Label.REPEATED ) val roles: List<String> = emptyList(), unknownFields: ByteString = ByteString.EMPTY ) : AndroidMessage<Proto3Sample, Nothing>(ADAPTER, unknownFields) { @Deprecated( message = "Shouldn't be used in Kotlin", level = DeprecationLevel.HIDDEN ) override fun newBuilder(): Nothing = throw AssertionError() override fun equals(other: Any?): Boolean { if (other === this) return true if (other !is Proto3Sample) return false return unknownFields == other.unknownFields && id == other.id && name == other.name && roles == other.roles } override fun hashCode(): Int { var result = super.hashCode if (result == 0) { result = unknownFields.hashCode() result = result * 37 + id.hashCode() result = result * 37 + name.hashCode() result = result * 37 + roles.hashCode() super.hashCode = result } return result } override fun toString(): String { val result = mutableListOf<String>() if (id != null) result += """id=$id""" if (name != null) result += """name=$name""" if (roles.isNotEmpty()) result += """roles=$roles""" return result.joinToString(prefix = "Proto3Sample{", separator = ", ", postfix = "}") } fun copy( id: Long? = this.id, name: String? = this.name, roles: List<String> = this.roles, unknownFields: ByteString = this.unknownFields ): Proto3Sample = Proto3Sample(id, name, roles, unknownFields) companion object { @JvmField val ADAPTER: ProtoAdapter<Proto3Sample> = object : ProtoAdapter<Proto3Sample>( FieldEncoding.LENGTH_DELIMITED, Proto3Sample::class ) { override fun encodedSize(value: Proto3Sample): Int = ProtoAdapter.INT64.encodedSizeWithTag(1, value.id) + ProtoAdapter.STRING.encodedSizeWithTag(2, value.name) + ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(3, value.roles) + value.unknownFields.size override fun encode(writer: ProtoWriter, value: Proto3Sample) { ProtoAdapter.INT64.encodeWithTag(writer, 1, value.id) ProtoAdapter.STRING.encodeWithTag(writer, 2, value.name) ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 3, value.roles) writer.writeBytes(value.unknownFields) } override fun decode(reader: ProtoReader): Proto3Sample { var id: Long? = null var name: String? = null val roles = mutableListOf<String>() val unknownFields = reader.forEachTag { tag -> when (tag) { 1 -> id = ProtoAdapter.INT64.decode(reader) 2 -> name = ProtoAdapter.STRING.decode(reader) 3 -> roles.add(ProtoAdapter.STRING.decode(reader)) else -> reader.readUnknownField(tag) } } return Proto3Sample( id = id, name = name, roles = roles, unknownFields = unknownFields ) } override fun redact(value: Proto3Sample): Proto3Sample = value.copy( unknownFields = ByteString.EMPTY ) } @JvmField val CREATOR: Parcelable.Creator<Proto3Sample> = AndroidMessage.newCreator(ADAPTER) } }Kotlinコードとして出力した場合、きちんと
optional
として出力されました。null安全という観点だけで言えば、proto3を出力する場合はKotlinのほうが安全です(が、Kotlin生成自体がリリース直後のため、その他の安定性では劣ると思います)。冒頭でも記述しましたが、Kotlin対応、proto3対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。