LINEアプリの課題

大石将邦氏(以下、大石):「LINE Androidアプリの基盤を支えるOSSライブラリLich」というタイトルで発表いたします。LINE株式会社の大石と申します。よろしくお願いします。

最初に自己紹介させてください。私はLINEコミュニケーションプラットフォーム開発室に所属しています。最近組織変更があり名前が変わりましたが、基本的にいわゆるみなさんが使われているメッセンジャーアプリのLINEの基盤部分の開発を担当しています。

以前はLINE DEV DAYの2019とか2020にも登壇しました。2019のときは仕様とかおもしろい話をしたと思うので、ぜひ見てもらえればいいと思います。あとは、KotlinもAndroidもぜんぜん関係ない個人的なブログでちょろっと話をしています。

さっそく今回の話ですが、まずそもそもみなさん利用されていると思いますLINEアプリ。これはもう10年近い歴史があり、技術的負債が非常に大きい話になります。さすがに10年も経って、それもAndroidというプラットフォーム自体が大きく変化する上で作られているコードなので、コードの複雑度が非常に高くて、修正や新規機能の追加のコストが高いという問題があります。

単純にわかりやすい支障だと、とにかくビルドに時間がかかる。差分ビルドでだいぶ速くはなってきていますが、単純なフルビルドをやっちゃうと20分ぐらいかかることもあります。

本当に初期の頃はAndroid OSのいろいろな制限を回避するためとかいろいろ問題があって、SPDYプロトコルを採用したりとか独自の作り込みのコードがあったりしました。今となっては、SPDYはHTTP/2に置き換わっていますし、OSそのもののフレームワークも大きく変わっています。特にJavaからKotlinへの変化というのは大きいですね。

LINTプロジェクト

そういう変化に対応できていないところがもともとあり、これはいかんということで、とにかく将来に向けていわゆる技術的負債を解消していこうというプロジェクトがチームとして立ち上がりました。それがLINT、LINE Improvement for Next Ten yearsと、将来10年を見据えて基盤を改善していこうと。

それもクライアント、サーバーの両方が協力してやっていくプロジェクトが立ち上がりました。特にクライアントとサーバーが協力しないとなかなかできなかったネットワークプロトコルとか同期まわりの改善を集中してやっています。これは私ではありませんが、別の者が2019年のLINE DEV DAYのセッションで話していますので、興味がある方はぜひ見てもらえればと思います。

このLINTプロジェクトについてはもう2年以上、もうすぐ3年まではいっていないのかな。だけど2年半ぐらいはやっていますね。その間、いろいろな制度ができておりまして、これはLINEアプリにもちろん適用すればいいんですが、LINE株式会社自体としては別に他にもいろいろアプリを作っていますので、LINE以外のアプリにも展開していきたいと。

なので、必要なものであればライブラリモジュールとして他のアプリやSDKからも利用しやすくするということを考えています。さらにもっと汎用的な機能についてはオープンソースライブラリとして社外に公開していくことも考えていますというモチベーションもあります。これは外部からのフィードバックを受けられれば質をより高められるだろうということです。

実際にもうすでに1年以上前にLichというライブラリを公開しています。今でも継続的に開発、メンテナンスがされています。このLichについては、Androidアプリ向けのライブラリコレクションになります。今回のお話はご存知の通りPure Kotlinですね。Kotlinに基本的に書かれているライブラリです。

現在では5つのライブラリがあります。ネットワークまわりのOkHttpとかThriftとかの他にAndroidで一般的に使われる機能をライブラリ化したものがいくつかあります。それぞれのライブラリは独立していますので、必要なものだけ利用可能になっています。今回は特徴的なものとしてLich Componentというライブラリについて話していきたいなと思います。

Lich Component

さてこのLich Componentなのですが、簡単に言うとSingletonオブジェクトの管理をするためのライブラリです。Androidでそういうものと言うとDaggerとかKoinとかそういうDIツールをご存知の人も多いと思いますけれども、それの代わりとなるものとして作りました。もちろん既存のDIを置き換えずに補完的に利用することもできるように考えて作られています。

すでにDaggerとかKoinとかがあるのに、なぜ新たに作るのか。オレオレDIツールなんて実際作ってみたという人は多いかもしれませんが、なぜわざわざ作ってそれを公開しているのかというところなんですが、モチベーションがいろいろあります。とにかく今までのDaggerやKoinとかそういう既存のライブラリには欠点というか気になる点があったので、それに対して改善したいところはいろいろあったわけですね。

最初に、利用するために覚えることをできるだけ少なくしたい。Daggerとか特に覚えることが多かったりしますよね。他にも、グローバルな状態を持ちたくない。アプリケーションの初期化の.onCreate()、アプリケーションの初期化のときに何か初期化の処理を行う必要があってほしくないと。起動を速くしたいということですね。

同じ意味で、lazyな取得というのがKotlinのdelegated propertyと。これはKotlinの機能なのですが、Kotlinのコードとして自然にlazyなオブジェクトの取得も簡単に書けます。これも起動の速度を早くするという点では非常に有効ですね。次にインターフェイスと実装を簡単に分離する。これはDIツールとしては非常に当たり前の機能で、ライブラリモジュールとかDynamic feature moduleです。

Dynamic feature moduleはAndroidの開発をしていないとちょっとわかりづらいかもしれませんけれども、一部の機能を最初のインストール時以外のタイミングでコードをダウンロードするみたいな感じですね。一部のコードをダイナミックに切り替えるみたいな感じのかたちでモジュール化する機能を動かそうと思ったときに、既存のDIツールではこれでなかなかうまく動かないので、どうにかしたかったというところがあります。

あとは細かいところですが、ビルドタイプやフレーバーによる実装の切り替えが簡単にできるとか。もちろん、ユニットテストでモックオブジェクトを差し替えるのが簡単だったとかそういうのもありましたが、できるだけコード生成したりしてビルド時間が延びるのは嫌だと。ここら辺のことをやりたいというのを目的として作りました。

基本的な使い方

具体的にどうなの? というところで、実際に基本的なコードなんですが、Lich ComponentにおいてコンポーネントのそのSingletonの定義というのはKotlinのcompanionオブジェクトという機能を使ってやります。このcompanionオブジェクトにComponentFactoryという機能を実装してもらって、ここでコンポーネントの初期化のコードを書いてもらう。コンポーネントの定義の方法はこれで終わり。

このcompanionオブジェクトを使うというのはちょっと肝で、実際に取得する方法なんですが、KotlinのExtension functions のcontext.getComponent、この後ろにさっき定義したFooComponentをポンと置くと、Singletonのインスタンスがバッと作られます。

同じようにlazyに書きたい場合は = をbyに変えて.getComponentを単純な.componentに変えれば終わりです。例えばこのFragmentに書きたい場合は、その中にExtension functionsを定義してあるので、FooComponentをこんな感じで簡単に書けます。

基本的な感じはこれで終わりなんですね。ただこれだけだと何がうれしいの? というところがあるんですが、こういうDIツールを使うときに一番うれしいのは、インターフェイスと実装が分離できることです。Lich Componentについてはインターフェイスと実装の分離が非常に簡単にできます。

まずインターフェイスのほうに先ほどのようにcompanionオブジェクトでその実装の定義を書きます。このときこのdelegateToServiceLoaderというファンクションを単純に呼ぶだけにしておきます。一方で実装クラス、ここでいうとFooComponentImplですが、これをに@AutoServiceというアノテーションを付けておきます。これで終わりです。

こうするとこのServiceLoaderという仕組みがこのFooComponentImplのインスタンスを自動的に探し出して、newしてセットしてくれます。ここですごくうれしいのは、FooComponentを使うときにはFooComponentのインターフェイスだけ見えていればいい。このインプリメンテーションのほうは、任意のモジュールに置くことができます。

この仕組みは既存のDIツールで、特にいわゆるDIツールのときの初期化のタイミングで、初期化のクラスが実装クラスが見えていないといけないとか、そういう問題がよくあります。Lich Componentにおいては、特にメインのアプリケーションからすべての実装クラスが見えなきゃいけないとか、そういう制限は一切ありません。最終的なバイナリに含まれていればOKですね。ここが非常にうれしいところです。

さらにもっと便利な機能として、複数の実装クラスを切り替えるということが簡単にできます。先ほどのFooComponentImplですが、プロジェクトのメインはプライオリティが一番低いプライオリティ0というかたちで作っておいて、例えばデバッグビルド用にDebugFooComponentImplというクラスを作って、それはより高いプライオリティでデバッグ用のソースコードをソースツリーの中に入れておく。

そういうことにしておくと、デバッグビルドの場合だけ、このDebugFooComponentImplが使われる。リリースビルドやそれ以外のビルドでは、このプライオリティの低かったFooComponentImplが使われる、ということができます。

これは実際にデバッグビルドでデバッグメニューを出すコードとそうじゃないコードを単純に切り替えることが、特に設定ファイルをいじったりせずに、クラスパスを切り替えるだけでできるという便利な機能として、実際に使われています。

実行時のコストについて

ちょっとここまで話して、少し気になっている人がいると思います。このインターフェイスと実装を分離する機能というのは、もともとはJavaにあるServiceLoaderという機能を使っています。このServiceLoaderは内部でリフレクションを使っています。リフレクションということで非常に、特にAndroidでは遅いんじゃないかと気にする方がいると思います。

しかしこれは、Androidのツールチェーンが非常にうまくできているところです。リリースビルドでは、R8というAndroidのビルドツールの一部なのですが、これが最適化を行ってくれまして、ServiceLoaderの呼び出しをインライン化してくれます。リフレクションのコードを完全にアプリケーションのリンクのタイミングでインライン化してくれるので、リフレクションコードが消えてしまうんですね。

なのでオーバーヘッドがありません。ということで、実際のリリース版のビルドの中ではリフレクションコストなしに、先ほどのような便利な実装とインターフェイスの分離を簡単にできるます。かつしかもこの仕組みは、そのDynamic feature moduleみたいなものでもちゃんと問題なく動作しています。このあたりが特にLich Componentの非常に強い、得意としていることになります。

あとはおまけですが、ユニットテストなんていうのもLich Componentだと簡単に書けます。例えばこのFooRepositoryみたいなクラスを作りました。これを使うFooUseCaseというのがいたとします。FooRepositoryの定義は上のほうにありまして、それを使うFooUseCaseは下のほうですね。

FooUseCaseはそのさっき言ったlazyな取得方法を使って、FooRepositoryのインスタンスを取っています。このFooRepositoryのインスタンスをモックして、FooUseCaseのテストクラスを書きたいと思った場合はどうするかといいますと、画面の中央にあるとおり、mockComponent(FooRepository)と、こう書けば終わりです。

この中にさっき言ったfindFooなどをモックするときは、こう書きます。ここに書いてあるやつは内部的には先ほどちょっと話にありました、mockito-kotlinを使っています。だからmockito-kotlinの文法ですね。MockKに対応したバージョンも実はあります。とにかくこのようにmockComponentをポンと書くだけで、先ほどのFooRepositoryをモックしてテストが簡単に書けます。

Lich Componentを導入した効果

このLich Componentを導入した効果なんですが、先ほど言いましたように、lazyが簡単に書けます。これはやっぱり特に、いろいろな初期化を行っているコードがあったときに、それを起動時に初期化のコードをできるだけ減らせるので、だいぶ削減しやすくなった。まだまだリファクタリングの途中なので、LINEアプリそのものは起動時にちょっと時間がかかっていますけれども、それでもだいぶ少しずつ改善はできているという感じですね。

もう1つ、マルチモジュール間の依存関係がとても簡単に切れるようになった。これは先ほど言いましたように、Lich Componentの最大の得意とするところです。もともとその巨大なモノリシックなLINEアプリが機能ごとに分割されて開発効率は良くなってきていますし、分割することによってビルドを変更したときに再コンパイルになるコードが減るので、そのインクリメンタルなビルドが速くなったりとか、いろいろメリットがあります。

さらにまだわずかですが、Dynamic feature moduleによって、アプリの容量を少しずつ削減していく方向ができています。まだまだ基盤であって、まだ道半ばというところなんですが、その足場ができたというのがLich Componentの効果ですね。

これまではLich Componentについて紹介しましたが、基本的にはLich ComponentはSingletonのオブジェクトを扱うものなんですね。当然の話としてSingleton以外のオブジェクトはどうやって扱うんだという話になりますが、ViewModelについては、AndroidのViewModelクラスにでLich ViewModelという、同じくLichの一部のライブラリとして作っています。

これはLich Componentも同じような設計方針で作っていますし、Androidでちょっと扱いづらいとされているSavedStateについても、Kotlinのdelegated propertiesで簡単にアクセスするような機能とかもあります。それ以外のスコープみたいなものが他のライブラリではよくあるんですが、あえてLichでは作っていません。

軽量かつ高速である点で使いやすいLich Component

特にActivity-scopedやFragment-scopedみたいなオブジェクトは、モダンなAndroidアプリでは不要というか、機能を減らして作りたいという、特にメイン開発する私の思いが込められているという感じですね。基本的にはモダンなAndroidアプリでは、そのビジネスロジックはViewModel層以下に作るべきであって、そのActivityやFragmentは基本的にはViewModelとViewのbindingに集中してほしい。

であれば、Activity-scopedとかFragment-scopedみたいなものは、基本的には使う必要がないだろうという設計思想です。実際には他のDIツールに慣れた人というのは、もしくはもうすでに既存のアプリに対しては、DIツールは別にDaggerをすでに使っています、Koinを使っていますみたいな人は当然いると思います。

実際にそういうところでも、Lich Componentのマルチモジュール対応の機能は非常に強力なので、これだけは使いたいという話が時々あります。そのために、Lich Componentというのはできるだけコンパクトに作られているので、他のDIツールと組み合わせて使うのも簡単にできます。

具体的にDaggerと組み合わせた例と言いますと、このようにprovideFooComponentみたいなDaggerのモジュールとして書くところに、Lich Componentの.getComponentという記述を書いてやると。これだけでLich ComponentをDaggerから使うことができることになります。

Lich Componentについて紹介しました。最後にまとめです。LichはLINEアプリの改善プロジェクトの中で生まれたOSSライブラリです。こちらのURLからアクセスできます。その中でもLich Componentはアプリのマルチモジュール化を特に支援することを大きな目的として作られたDIツールです。

軽量かつ高速である点が使いやすい、シンプルであるというところがアピールポイントになります。もうすでにDaggerやKoinといった他のDIツールを使っている方でも、そのマルチモジュールの依存関係を解決するところについて。ピンポイントで使うのもいいのかなと思っています。

それ以外にも、Lichについては、ViewモデルやOkHttpのExtension functionsみたいなものもあります。興味があったらこれを見てもらえればなと思います。以上になります。本日はありがとうございました。