20191020のAndroidに関する記事は7件です。

FirebaseのRoboTestのroboscriptで多言語対応を使えるようにしたった

はじめに

RoboTestだと多言語対応が楽そうな印象があったのでちょっとだけ頑張った話です。
一個一個キャプチャ取るのとかやですよね。
スクリーンショット 2019-10-20 20.56.42.png
インターフェース的にもできそうじゃないですか。
絶対できるんですが、そのままじゃできなかった。
スタンダードな手法で作ったroboscriptだと多言語対応できません。

問い合わせた

スクリーンショット 2019-10-20 21.15.07.png

https://firebase.google.com/support?authuser=0

こんなメールが帰ってくる

スクリーンショット 2019-10-20 21.16.57.png

結果

メールでのやり取りで解決しませんでした。

  • 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 successfull

Android Studioから書き出し、実行がうまくいっていないroboscriptをよく見てみることにしました。

roboscript

クリックするだけなのに、replacementTextはいるのかと
スクリーンショット 2019-10-20 21.01.43.png

  • replacementTextに""を設定
  • textの部分はそのまま

多言語で複数ロケールを指定して実行してみました。
端末設定の言語が異なる状態で

Robo Script
<実行できたアクションの数> of <実行するアクションの数> actions successfull

となりました。

さいごに

Firebaseの問い合わせ時に、"日本語"で記載して問い合わせました。

Firebase担当者からの問い合わせの回答には文字化けしたメールが引用についていました。
どういう質問の仕方をしたのか、後から見てわからなくなってしまったのもあって
よくよく考えるとroboscriptでうまくいかないことだけを伝えた質問になったことで、メールでのやり取りを長引かせた可能性がありました。

やり取りはgoogle翻訳を使いやすいように、というFirebaseの担当者の理由で英語でリターンされるので、こちらもgoogle翻訳で訳した英語で返信するようにしました。

  • 質問は英語で
  • 多言語で正しく動作しないroboscriptからは、使用した端末の言語に限定した情報を抜いてみる

もうちょっと検証は必要かと思います。公式の情報があまり充実していません。
roboscriptは構造上そんなに難しくないので、色々いじって効率の良い方法を見つけていただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android] NavigationView(Navigation Drawer)で動的にメニューを作ると押した時色がつかないのを直す

こんばんは。今解決しましたので記事にします。
それにしてもタイトルなっが。

BottomNavigationBarのアプリが増えてきましたがハンバーガーメニューもかっこいいよね。

症状

静的に作ったメニュー(xml)を押す分には色が変わるんだけど、動的にメニューを作成するとなんか色がつかない。

とりあえず原因がわかるまではメニューを押した時にisCheckedをtrueにすることで押したメニューがわかるようにしていた。
しかし静的メニューの押したときとisCheckedをtrueにしたのでは少し見た目が違うので気になる気になる。

Screenshot_20191020-205231.png

画像のように一番上のメニューみたいになってほしいんだけどなんか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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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自身がまだシェアは低そうなので急いで対応する必要はなさそう
  • 黒いテーマがもっと普及するといいね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidにプリビルドjarライブラリを追加してフレームワーク内で呼び出す

行うこと

  1. テストjarライブラリを作成してAOSPにjarライブラリを追加
  2. フレームワーク内(クイック設定パネル)から1で追加したライブラリのAPIの呼び出し
  3. AWSのライブラリを追加してフレームワーク内から呼び出してみる

1. テストjarライブラリ作成

テスト文字列"test"を返す簡易的なAPIを実装したjarライブラリを作成する。

Test.java
package 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.mk
LOCAL_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ソースに修正を入れる。

diff

/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 reboot

2-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.html

ソース修正差分

3-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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Oculus Store 申請時の 'Enable Android NSC to Prevent Cleartext Traffic' という警告への対処方法

はじめに

Quest アプリを Oculus Store に提出する際に警告が出てきてしまいました。。:weary:
警告内容を見ると、ネットワークのセキュリティ設定に関するものでした :arrow_down:

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 というファイルをプロジェクトに追加することで対策可能そうでした :thumbsup:

そこで、
今回は network_security_config.xml の追加手順 & 設定方法について書いていきます :pencil:

network_security_config.xml をプロジェクトに追加する

追加するフォルダは /Assets/Plugins/Android/res/xml になります :arrow_down:

/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 通信を必須とするよう設定しました :gear:

AndroidManifest.xml に network_security_config.xml を利用することを宣言する

プロジェクトに network_security_config.xml の内容を反映させるには、
AndroidManifest.xml に 1行設定に必要な記述を追加する必要あります :writing_hand:

/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.xmlapplication タグに android:networkSecurityConfig 属性を追加し、
network_security_config.xml のファイルパスを指定することで設定が有効となります :white_check_mark:

この状態でビルドを行い APK の動作に問題が無いこと確認出来次第、
Oculus Store に再度アップロードしてみると、警告が無くなっていることが確認できるはずです :thumbsup:

おわりに

今回は Enable Android NSC to Prevent Cleartext Traffic の対処法について紹介しました :hand_splayed:

実は他にも警告が出ているのですが、どうやらどれも Android に関わるもののようで、
Oculus Store 申請時は、「ココらへんも気をつけないといけないんだなあ」と思いました。。 :skull:

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.proto
syntax = "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)
  }
}

requiredoptionalの設定が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.proto
syntax = "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ではrequiredoptionalは廃止されており、すべて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対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.proto
syntax = "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)
  }
}

requiredoptionalの設定が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.proto
syntax = "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ではrequiredoptionalは廃止されており、すべて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対応に関しては現在進行形でいろいろと対応が進んでいるようなので、引き続き変更を追っていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む