アプリを発展させ続けるためには変化に追従しなければいけない

大石将邦氏:「LINE Android アプリの開発をいかにモダン化していくか」というテーマで、今日発表させていただきます。LINE Androidクライアントチームの大石と申します。よろしくお願いいたします。

LINEアプリは2011年の6月にリリースされてから8年を超えて、現在9年目に入っています。2011年と言いますと東日本大震災があった年ですけど、スマートフォンアプリの状況についてみなさん覚えていらっしゃるでしょうか?

こちらのように、2011年はAndroidだとバージョン2.3 Gingerbreadがようやく最新端末に搭載され始めた頃です。iPhoneですと、まだiPhone 4の時代ですので、まだソフトバンクしか扱っていなかった時代ですね。ちょっとここでお聞きしたいんですけど、こちらの会場で現在Androidアプリの開発をやっているという方、手を挙げていただきますでしょうか?

(会場挙手)

それなりにいますね。じゃあ、この2011年の時点ですでにAndroidアプリを開発していた方は?

(会場挙手)

だいぶ少ないですけど、まだまだいますね。この時代にLINE Androidアプリはリリースされ、そして継続して開発されてきました。先ほど手を挙げていただいた方は覚えていらっしゃると思いますけど、この8年余りという長い期間はAndroidの歴史の大半を占めるわけです。ですから、Androidの開発環境は本当に何から何まで変わってしまっています。

しかし、アプリを発展させ続けるためには、これらの変化に常に追従していかなければなりません。なので、我々LINE Androidクライアントチームにおいても、これらの変化に追従し続けていく取り組みが重要なミッションの1つです。そのために行ってきた内容は、先ほどのスライドのとおり本当にたくさんありまして、全部を話すことはとても難しいので、今日は2つのトピックについて話そうと思います。

Kotlinを導入した経緯

まず1つはKotlinとKotlin Coroutinesについて、もう1つはプロジェクトのマルチモジュール化についてです。では、まず始めにKotlinについて話しましょう。もう一度伺いたいんですけど、Androidに限らず、今Kotlinを使っている方は挙手をお願いします。

(会場挙手)

おぉ! だいぶいますね。じゃあ「Kotlin Coroutinesをバリバリ使ってるよ!」という方。

(会場挙手)

お! 半分ぐらいですかね。けっこういますね。じゃあまず最初にKotlinのほうを話します。

LINE AndroidへのKotlin導入の検討を始めたのが2017年の早い時期です。2017年の4月頃から、まずユニットテストでKotlinの導入を検討してサイロ的に始めて、それと並行しましていくつかのコーディング規約を作成していきました。

基本的にはKotlinのオフィシャルのコーディング規約に合わせていますけど、我々のチームはかなり大きなプロジェクトですので、いくつか追加的に細かなルールを決めた上でコーディング規約を作成していきました。そして、ある程度うまくいったことを確認した上で、2017年の6月頃から本格的にKotlinの適用を始めました。

これ以降、基本的に新しいコードはKotlinで書いたり、既存コードに機能追加をするときもいったんJavaのコードをKotlinにコンバートしてから開発するルールで開発してきました。

そして現在、LINE Androidプロジェクトは、テストコードを抜いたプロダクションのコードでそのうちのほぼ半分がKotlinのコードに変わっています。行数ベースでも1/3を超えるものがKotlinに変わってきています。

Javaとの100パーセントの相互運用性などがKotlinの利点

みなさんご存知のことも多いと思いますが、改めてKotlinの利点についてお話しましょう。まず、KotlinはJavaとの100パーセントの相互運用性をうたっています。さらに、Android StudioがJavaからKotlinへのコンバート機能を持っています。既存の大きなプロジェクトに段階的に適用するという点では、この2つが非常に重要です。

それが大前提としてある上で、Kotlinを採用することによりNull安全性やイミュータブルコレクションといったいろいろな利点を受け取ることができます。その中でもとくに最近注目されているのはCoroutinesでしょう。

Kotlin Coroutinesとは何かを簡単に説明しますと、非同期タスクをシンプルに記述できる言語機能です。今までのデファクトとして使われてきたものにRxJavaがありますけど、このRxJavaだと関数呼び出しをつなげていくような記述方法になります。一方、Coroutinesだともっと手続き的な書き方と言いますか、もっとわかりやすくシンプルな書き方ができるのが大きな特徴です。C#や他の言語にあるasync/awaitという言語機能と、だいたい似たようなものと思っていただけるといいと思います。

このKotlin Coroutinesは非常に便利な機能なので、我々もぜひ導入したいと以前から思っていました。しかし、これはかなり長い期間実験的機能という扱いでして、ようやく安定版の1.0が出たのが去年の10月です。

ですから我々がKotlinを最初に導入した2017年の4月からすると、かなり後ということになります。なので、Coroutinesを導入することはしばらくできませんでした。

我々LINE Androidプロジェクトにおいて、Coroutines導入前は非同期処理にどういうライブラリを使っていたかをお話しますと、それこそ8年以上の長い歴史のあるコードです。リファクタリングによってどんどん新しくしているところもありますが、古いコードが残っているところではAndroidのAsyncTaskクラス、もしくはExecutorクラスを生で使っていたりしました。もしくは、RxJavaのバージョン1とバージョン2が同時に使われているところが残っていたりと、かなり整理されていない状況でした。

ですのでKotlin Coroutinesの導入をきっかけに、我々は乱立している非同期ライブラリの統一を行おうとしました。ただ、先ほども言いましたようにKotlin Coroutinesが安定化するのには少し時間が掛かりましたので、古いコードを段階的にKotlin Coroutinesに持っていくために、いったんワンステップ踏むことにしました。

それは、既存コードの非同期ライブラリをできるだけRxJava2に統一することです。RxJava2を使っておくと、あとで容易にCoroutinesへ移行できることがその理由です。また、先ほども言いましたように我々のコードにはまだ半分ぐらいJavaが残っていますから、そこからKotlin Coroutinesを直接呼ぶことはできなくてもRxJava2を経由すれば呼び出せるというのも理由の一つです。

RxJava2を使う方針とルール

このように、既存の乱立している非同期ライブラリをいったんRxJava2に統一し、その後Coroutinesへ移行していく。そのようなプランで我々はCoroutinesの導入方針を決めました。

そして、将来Kotlin Coroutinesへ移行することを前提とした上で、RxJava2の使い方について我々は1つのルールを決めました。それは、RxJava2を使って非同期処理を行う関数はRxのSingleオブジェクトを返り値とする、というものです。

そのようなルールに従って書かれている関数を我々はasync functionと呼んでいます。このスライドですと、このfooAsyncという関数がSingleを返しています。これがasync functionの例です。このasync functionをどう書くか、もっと具体的な例を挙げましょう。

上のfetchJsonBlocking関数は、URLで示された場所からJSONデータを取得する関数だとします。これは同期的に処理を行います。つまり、実際にネットワークオペレーションが完了するまで処理を戻しません。このような関数をそのまま呼んでしまうとUIをブロックする原因となってしまうので、何らかのかたちで非同期的に実行しないといけません。

なので、このblocking functionをasync functionとして使えるようにしたのが下の例です。Single.fromCallableというブロックで囲った上で”.subscribeOn(Schedulers.io())”と書く。こうすることで、先ほどのblocking functionをバックグラウンドのI/Oスレッド上で実行して、その結果をRxのSingleとして返す、という意味のasync functionになります。

async functionを書く側のルールはこのような感じですが、一方でそれを使う側の話をしますと、先ほどのasync functionに”.observeOn(AndroidSchedulers.Main())”と付けてからsubscribeするようにします。これで先ほどのJSONの結果をUIに反映させるコードになります。これが基本的な使い方です。

もうちょっとだけ複雑な例を挙げますと、例えばもう1つ別のasync functionがあったとします。1つ目のasync functionの結果を別のasync functionで処理をしてその結果を使う場合、この例のようになります。

.flatMapという関数でその2つのasync functionをつなぎます。このように.flatMapなどの「オペレータ」の呼び出しをつなげていって処理を記述するのがRxによる非同期処理の基本になります。

RxJava2からCoroutinesへ移行する

このようなルールに従って書かれたコードは、Coroutinesを使うコードに簡単に書き換えることができます。まずはasync functionをCoroutinesから呼び出す場合を考えましょう。これがそのコードになります。このmainScope.launchのところでCoroutinesが起動されます。

このCoroutinesからasync functionを呼び出す場合は、async functionの末尾に .await() と付けてやるだけです。これで、あとは普通にCoroutinesからasync functionを呼び出すことができます。呼び出しの結果は、RxのときのようなSingleオブジェクトではなく普通のオブジェクトとして受け取ることができます。

UIスレッドをブロックすることがない非同期的な処理を、このようにあたかも同期的な処理のようなかたちで書くことができるのがCoroutinesの特徴です。

もう少し別の例を挙げましょう。先ほどのように複数のasync functionを呼ぶ場合の例です。これもCoroutinesだと簡単で、それぞれに.await()を付けるだけです。このように複数の処理を連続して呼び出しその結果をUIに反映させるようなコードが、Rxの場合と比べると非常にシンプルに記述できることがわかると思います。

さて、ここまで述べたように、RxJavaをCoroutinesに置き換える場合は、まず呼び出し側をCoroutinesに変えるのがおすすめです。呼び出し側をCoroutinesに変えることができたら、次は呼び出される側のasync functionを書き換えていきます。

Coroutinesに対応する場合、async functionはsuspending functionというものに置き換えます。suspending functionとはsuspendというmodifierが付いた関数でして、これは先ほどのasync functionと違って.await()を付けずにCoroutinesから呼び出すことができます。

先ほどのasync functionが上です。これと同等なsuspending functionは下のようになります。上のasync functionと同様にfetchJsonBlockingをI/Oスレッド上で実行してその結果を返すというコードになっています。これをCoroutinesから呼び出せばよいのです。

Coroutinesのキャンセルについて

このようにRxJavaを踏み台にして、それをCoroutinesで書き換えていくという方針で、我々はCoroutinesの導入を行いました。

ただ、ここまでの話で重要なことを1つ省略しています。それはCoroutinesのキャンセルです。非同期処理のキャンセルをどうやって行うのかはなかなか難しい問題です。

RxJavaをAndroidで使っているときの基本的な使い方は、ActivityやFragmentがstopの状態になったときに処理をキャンセルするというものです。我々はRxJavaをCoroutinesで置き換えるという方針をとっているので、Coroutinesの場合においてもonStop()のタイミングで処理をキャンセルさせたいのですが、既存のライブラリではそれをうまくやってくれるものがなかったんですね。

Android標準のJetpackライブラリにはlifecycleScopeというものがあるんですけど、これはonStop()のときにCoroutinesのキャンセルはしてくれません。処理を「一時停止」させることはできるのですが、その場合onStart()のタイミングで処理が再開されます。このようにlifecycleScopeではRxJavaのときとは異なる動作になってしまうので、RxJavaと同じポリシーでCoroutinesのキャンセルを行うライブラリを我々は独自に作ることにしました。それがAutoResetLifecycleScopeというクラスです。このクラスはGitHub上にオープンソースで公開しております。

AutoResetLifecycleScopeを使った例はこのようになります。上のほうでcoroutineScopeという名前でAutoResetLifecycleScopeを宣言しています。これが、Fragmentのライフサイクルに連動して動作します。

ここからCoroutinesを起動することによって、FragmentがstopされたタイミングでそのCoroutinesを自動でキャンセルさせることができます。以上が古い非同期コードをCoroutinesに置き換えていくやり方の説明になります。実際に我々も今年の7月頃からこのやり方でCoroutinesへの移行を行っています。

ここまでのまとめとして、我々がCoroutinesを導入するまでにやってきたことを挙げましょう。まずはじめに、RxJava2からの移行という形でCoroutinesを導入することにし、そのためのルールを定めました。そして、キャンセルなどに関わるライブラリやサンプルコードを作成した上で、それらをチーム内に展開しました。

これらのコードはオープンソースライブラリとして公開していますので、このGitHub上のプロジェクトを参照してみてください。以上がLINE AndroidアプリにおけるKotlinとCoroutinesの話になります。