サナギわさわさ.json

サナギさんとキルミーベイベーとプログラミングが好きです

Java→Kotlin変換時のハマりポイント (potatotips #53)

potatotips#53Androidブログまとめ枠で参加させていただきました。 初参加でしたがどの発表もクオリティが高く楽しかったです。次に参加する時は自分もLTしたい。

全発表を議事録的にまとめようかと思っていたのですが、当日の夜にはすでにまとめられていました。すごい。

kumamotone.hatenadiary.jp

というわけで、今回はLTの中で一番心に残った@paraya3636さんの発表「J2Kコンバータをカスタマイズする」について記事を書こうと思います。

発表資料はこちら

J2Kコンバータをカスタマイズする ver: 5min - Speaker Deck

Java → Kotlinの流れ

GoogleAndroid開発のためのファーストクラス言語としてKotlinを公式サポートすることを発表したのは2017年の5月でして、そのあたりからKotlinを本番プロダクトに導入する流れが加速したように思います。

jp.techcrunch.com

KotlinとJavaは共存できますので、新しく追加するコードはKotlinで書く、みたいな部分的な導入をしている方もいると思います。しかし既存のJavaコードをKotlinに書き換えKotlinメインにシフトするという選択をした方も多いのではないでしょうか。(弊社は後者です)

ここで大量のJavaコードをKotlinに変換するという結構重めのタスクが出てきます。

Kotlinコンバートの方法と辛さ

発表でも触れられているように、通常JavaからKotlinに変換する際はAndroid Studio標準搭載のJ2Kコンバータを利用します。

このコンバータは結構良くできていて、多くのコードがエラーなく変換できます。個人的にはObjective-CをSwiftに変換した時よりも数倍楽でした。

しかしいくつか問題はあり、その一つがNullableをNonNullに変換してしまうという点です。

具体的には下記のように、引数にNullableアノテーションがない場合にNullableがNonNullに変換されてしまいます。(発表スライドよりコードを引用させていただきます)

public interface SampleInteface {
    //Nullable引数
    String getText(Context context);
    
    int getValue(Context context); 
}
interface SampleInteface {
    //NonNull引数
    fun getText(context Context): String
    
    fun getValue(context Context): Int 
}

この変換をされたKotlinコードをJavaからnull引数で呼び出すと実行時に以下の例外でクラッシュしてしまいます。

IllegalArgumentException: Parameter specified as non-null is null

この例外はKotlinとJavaを共存させていると結構見るやつで、折角KotlinがOptionalで型安全を担保しているのに勿体無いなといつも思います。

所感ですが、これはKotlinとJavaの連携がスムーズすぎて呼び出し時に特に意識しなくても呼び出せてしまうのが一因な気がします。 SwiftとObjective-Cは連携自体が少し面倒なので、少なくとも自分の周りではあまりこういう事は起こっていません。

J2Kコンバータのカスタマイズ

この問題に対する解決策として、J2Kコンバータ自体をカスタマイズしてNullableを保持したまま変換する、という方法が発表では触れられています。

正直自分はこの方法は発想から抜けており、目視でのチェックとアプリを手元で動かしてのチェックを頑張るというぬくもり溢れる対応で解決していました。お恥ずかしい限りです。

自分でもJ2Kコンバータのカスタマイズをして動かすところまで記事にしようと思ったのですが、少し重かったのでそこまで行けませんでした。すみません。。。

自分がJava→Kotlinの変換でハマったところ

代わりと言っては何ですが、自分がJava→Kotlinの変換でハマったところについて少し触れておきます。もし参考になれば幸いです。環境はAndroidStudio2.3 + Kotlin 1.1.51です。

ライフサイクル系関数の実行時例外

onActivityResultをJ2Kコンバータで変換すると以下のようになります。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
}

しかしonActivityResultのIntentにはシステムからnullが渡される可能性がありますので、 この変換されたコードは実行時例外になります。なので手動で以下のように書き換える必要があります。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
}

他にもonCreate, onCreateView, onViewCreated, onAttachなども変換ミスが起こることがあります。これに関しては@Overrideアノテーションがある時は変換が上手く行ったりと、イマイチ変換に成功する条件が分かっていません。 取り急ぎ正しい変換後の形を記述しておきます。もし何か間違いあれば教えてください。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.fragment_base, container, false)
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
}

override fun onAttach(context: Context?) {
    super.onAttach(context)
}

Listener系の変換時エラー

Animation.AnimationListenerのような関数に関しても、自分の環境では自動変換が上手く行きませんでした。

//変換前
anim.setAnimationListener( new AnimationListener() {
    @Override public void onAnimationStart(Animation animation) {}
    @Override public void onAnimationRepeat(Animation animation) {}
    @Override public void onAnimationEnd(Animation animation) {}
});
//変換後(コンパイルエラー)
anim.setAnimationListener(object : AnimationListener() {
    fun onAnimationStart(animation: Animation) {}
    fun onAnimationRepeat(animation: Animation) {}
    fun onAnimationEnd(animation: Animation) {}
})

以下のように手動で変換します。

anim.setAnimationListener(object : Animation.AnimationListener {
    override fun onAnimationStart(animation: Animation) {}
    override fun onAnimationRepeat(animation: Animation) {}
    override fun onAnimationEnd(animation: Animation) {}
})

Dagger2の移植が辛い

弊社ではDagger2.11を使用していたのですが、これをKotlin移植する際に色々とハマりました。取り急ぎ、現在問題なく動いているKotlin移植後のコードのみ掲載します。もし何か意見・問題点などあれば是非お願いします。

class SampleApplication : Application(), HasActivityInjector, HasServiceInjector {
    @Inject
    lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    @Inject
    lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service>

    override fun onCreate() {
        super.onCreate()
        DaggerAppComponent.builder().application(this).build().inject(this)
    }

    override fun activityInjector(): DispatchingAndroidInjector<Activity> {
        return activityDispatchingAndroidInjector
    }

    override fun serviceInjector(): AndroidInjector<Service> {
        return serviceDispatchingAndroidInjector
    }
}
class SampleActivity : AppCompatActivity(), HasSupportFragmentInjector {
    
    @Inject
   lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
    
   override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
   }
   
   override fun supportFragmentInjector(): AndroidInjector<Fragment> {
        return fragmentDispatchingAndroidInjector
   }
}
        
class SampleFragment : Fragment() {
    
    override fun onAttach(context: Context?) {
        super.onAttach(context)
        AndroidSupportInjection.inject(this)
    }
}       

まとめ

Java → Kotlinの変換では色々とハマる点もありますが、J2Kコンバータは基本的に質が高く、割とスムーズに変換できます。 ただNullable周りでやられる事が多々ありますので、注意が必要ですね。

良い勉強会でした。また是非参加したいと思います!