はじめに
10連休のひまなときにこつこつコンバートを行い、
自作のJava製Androidアプリ(約300ファイル)をフルKotlin化しました。
ちょうど10連休明けに行われたGoogle I/O 2019ではKolinファーストが表明され
JavaからKotlinへ変換する機会が増えそうなのでつまづいたこととその回避方法をまとめます。
環境はAndroidStudio 3.4です。
変換方法
すべて手作業で行うのが確実ですがAndroidStudioに変換コマンドが用意されています。
「メニュー > Code > Convert Java File to Kotlin File」
ショートカットは「Shift + Option + Command + k」です。
Kotlin変換コマンドの注意点
コメント
地味にコメントが変換を妨げる事がありました。
コメントアウトした箇所はKotlin変換後もJavaのままになります。
また、Kotlinではコメントを開く /*
の数とコメントを閉じる */
の数が揃っている必要があります。
【変換後サンプル】
- コメント部分がJavaのまま
- コメントの閉じる数が足りずビルドエラー
Nullable / NonNullが曖昧なもの
KotlinはNullSafeな言語仕様のため、Java側で Nullable / NonNullが曖昧な場合、
プラットフォーム型(NullableかもしれないしNonNullかもしれない型)として扱われます。
NonNullが決定的でない限りNullableとして扱うほうが安全です。
実際に実行時エラーになったコードは以下です。
変数の後に !
がつくものはプラットフォーム型でコード上はNonNullとして扱えたのですが
実行時にNullが入ってきてエラーになることがあります。
いったんNullable扱いとして早期リターンなどすると次の行からNonNullとして扱うことができます。
Javaで定義されたクラスのサブクラスをKotlinで定義する場合
Nullable/NonNull関連では、Kotlin変換コマンドを使用した場合、
特に以下の例外が発生しやすかったです。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null:
これはスーパークラスがJavaでサブクラスがKotlinの場合でnon-nullとnullの扱いを誤った場合に発生する例外です。
例えばAppCompatActivityを継承した以下の場合に例外が発生します。
実行時エラーなので、コードは正常にビルドできてしまいます。
class MainActivity : AppCompatActivity() {
:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
:
以下のように Bundle?
を引数の型とするのが正しいコードです。
class MainActivity : AppCompatActivity() {
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
:
これはスーパークラス側のコードを読まないと判明しづらいです。
実際のコードを読むとBundleに @Nullable
アノテーションがついており
Bundle
でなく Bundle?
が正しいことがわかります。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
Kotlin変換前にやると良い点
上記の注意点を踏まえて、変換前にやると良いことを挙げます。
Kotlin変換に関するドキュメントを読む
Kotlin公式にもAndroid公式にもJavaとの相互運用ドキュメントがあります。
- https://kotlinlang.org/docs/reference/java-interop.html
- https://developer.android.com/kotlin/interop
NonNullアノテーションを活用する
Javaの時点でできる限りNonNullを区別したほうが良いです。
Kotlinドキュメントによると以下のアノテーションに対応しています。
JetBrains (@Nullable and @NotNull from the org.jetbrains.annotations package)
Android (com.android.annotations and android.support.annotations)
JSR-305 (javax.annotation, more details below)
FindBugs (edu.umd.cs.findbugs.annotations)
Eclipse (org.eclipse.jdt.annotation)
Lombok (lombok.NonNull).
AndroidXに対応する
旧サポートライブラリを使用している場合はAndroidXに移行後に変換を行ったほうが良いです。
AndroidXのほうがNonNull, Nullableがしっかり判別されているのでKotlinへの変換リスクが少ないです。
以下RecyclerViewのJavaコードの比較です。
旧サポートライブラリのRecyclerView
https://android.googlesource.com/platform/frameworks/support/+/121ba96/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java#5143
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
AndroidX ライブラリのRecyclerView
https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java#6888
@NonNull
public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
ユニットテストで動作保証する
Java時点でユニットテストを追加すればKotlin変換後に動作確認が容易です。
最低限でも良いのでユニットテストを追加することをおすすめします。
Lintを活用する
Editer > Inspectionで"interop"などで検索するとKotlin変換に活用できる相互運用系のLintが表示されます。
チェックをつけると変換前後に危ういコードが警告されます。
https://developer.android.com/kotlin/interop#lint_checks
https://developer.android.com/studio/write/lint?hl=ja
Kotlinの予約語の変数名を避ける
is, object, whenなどのKotlin予約語や、内部のJavaコードで使用されるget, setなどは予めリネームすると良いです。
一応Kotlinでもバッククオートで囲めば使用できますが避けたほうが良いです。
Kotlin変換後にやると良い点
Kotlin変換コマンドに生成されたKotlinコードはKotlinを活用しているとは言い難いコードです。
最低限、警告が出ずある程度安全な状態にする方法を挙げます。
Nullable型のアンラップ方法を検討する
変換コマンド後のコードでは基本的に !!
を利用してNonNullにアンラップしています。
?.
などで安全にアンラップできないか検討します。
Kotlin変換直後のコード例
var webView: WebView? = null
fun load() {
if (webView != null) {
webView!!.load()
}
:
?.
を活用する例
var webView: WebView? = null
fun load() {
webView?.load()
:
varの扱いは適切か検討する
Nullableのプロパティは lateinit
や by lazy
など活用できるか検討します。
View関連で Activity#onCreate
など init
以外で初期化する場合は lateinit
を利用するとNonNullのプロパティとして扱うことができます。
private var mRecyclerView: RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
:
mRecyclerView = findViewById(R.id.recycler_view)
private lateinit var mRecyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
:
mRecyclerView = findViewById(R.id.recycler_view)
定数valはconst valにできないか検討する
Javaでstatic finalの定数の場合、Kotlin変換コマンド後のコードではvalとして扱われます。
const valにできないか検討します(通常、Lintに従えばconstを付与することになります)
companion object {
private val TAG = "MainActivity"
companion object {
private const val TAG = "MainActivity"
仕上げにFirebase Robo Testを活用する
変換後、テストコードをちゃんと書いたつもりでも自分の予想の範囲外のバグが潜んでいることがあります。
第三者やロボにテストをさせることで防ぐことができます。
Nullable / NonNull / Platform と型が増え、初期化前のlateinitなど状態も増えています。
Firebase Robo Testを使用して「うっかりミス」を防ぐことをおすすめします。
100%Kotlinの感想
Kotlin自体はスッキリするけどKotlin⇔Javaの橋渡しでエラーが起きがち。
けっこう茨の道なので事前に治安の良いJavaコードにしておいたほうが良さげ。
いったん変換してしまえばCoroutineや拡張関数など便利な機能が使えて生活が変わります。