Androidアプリをつくっているkobakenです。DroidKaigi 2019の登壇予告記事を投稿して以来ですね。皆さん元気にしていましたか?
kobakenはというと、Android版 pixiv Sketchをもりもり開発しておりました。Jetpack Composeをプロダクション投入したすぎてウズウズしています。
さて、今回はそんなAndroid版 pixiv Sketchのドロー機能を刷新して(以下、新ドローと呼びます。)新しい体験をお届けできたので、その開発の裏側についてご紹介していこうと思います。
Android版 pixiv Sketchの新ドロー機能をリリースしました!
Androidをお持ちの方は、とりあえず触ってみてください。
今回紹介するpixiv Sketchの新ドロー機能とは、
- お絵かき新規勢でも直感的に触れるUI
- 普段お絵かきする勢でもラフやらくがきを十分に楽しめるシンプルな体験
- 紙に鉛筆でスケッチするような、思い通りに仕上がる手書きの書き味
などたくさんの魅力ある機能になっています。詳しい説明はこちらの記事を参照ください。
開発環境について
当時の開発状況や環境について振り返ってみたいと思います。
まずは新ドローと以前までのドロー(以下、旧ドローと呼びます。)の実装の違いを見てみましょう。
実装 | 保存形式 | |
---|---|---|
旧ドロー | OpenGL ES(OSクライアント側で各々実装) | Realm |
新ドロー | C++ | 独自ファイル形式 |
新ドローのコア機能実装にはC++が採用されており、iOS/Androidで共通の基盤を利用することが出来る仕組みになっています。そのためAndroidの場合だと、JNIを経由してやり取りする必要がありました。幸い、kobakenは過去に少しだけJNIに触れてきたことがあったため、最小の学習コストで取り組むことが出来ました。
開発メンバーをざっとまとめると以下の表のとおりです。
役割 | 人数 | 備考 |
---|---|---|
Androidアプリエンジニア | 1名 | kobaken |
C++エンジニア | 3名 | 新ドローのコア機能およびpixiv Sketchアプリへのブリッジを担当してくれた人々 |
UIデザイナー | 2名 | 片方は普段iOSアプリエンジニアをしており、新ドローの大体のUIデザインを担当してくれました |
マネージャ | 1名 | マネージャ |
Androidアプリエンジニアが2,3人に増えた時期が一瞬だけありましたが、今では元気に1人で開発しています!(…ケテ…タスケテ………)
元々iOSで先行リリースされていたため、デザインや仕様に関してはほぼほぼ固まっており、それを追っかけるという形でした。
kobakenは主にアプリのUI実装や新ドローのコア機能とのつなぎ込み(一部ブリッジも実装)を担当しました。開発期間は約6ヶ月ほどで完成に至りました。
新ドロー実装時の工夫点
タブレット対応
今回の新ドローではタブレット利用者も視野に入れており、UIもタブレット用に別途実装してあります。公式ドキュメントにあるように、res/layout
とres/layout-sw600dp
のようにディレクトリを分けてレイアウトを定義しています。デザインの都合上、「スマホレイアウトにはあるけどタブレットレイアウトには存在しないView」というシチュエーションは容易に考えられます。その場合はfindViewById等でNullableとして返却されるので注意して扱いましょう。最近のAndroid Studioでは警告してくるので便利です。サイズを取りまとめるres/dimen
もタブレット向けに定義して要素のサイズやマージンなどを管理しています。
見た目に関しては以上で解決できますが、振る舞いに関しても異なる場合があります。例えば、ブラシボタンをタップした際に、スマホだとBottomSheetが表示されるところをタブレットだとDialogFragmentを表示してあげる、といった具合です。これは「ブラシボタンをタップした」もっと噛み砕くと「現在の状態がブラシモードに切り替わった」際に処理の切り替えを行えば解決できます。後述するKotlin Coroutines Flowを活用してこのイベントをViewModelからActivity/Fragmentに通知してハンドリングします。コード上でのスマホ or タブレットの判定には以下のコードが役に立ちます。
fun isTablet(configuration: Configuration = Resources.getSystem().configuration) { return configuration.smallestScreenWidthDp >= 600 } fun isPhone() = isTablet().not()
また、新ドロー実装ではViewBindingを採用しています。以前まではKotlin Android ExtensionsのKotlin syntheticsを利用していましたが、公式からのお達しがあり、ViewBindingに乗り換えました。ViewBinding-ktxを併用するとDXが爆上がりするのでおすすめです。
チュートリアル
旧ドローと比較してガラッとレイアウトが変わったため、新規ユーザーはもちろん、既存ユーザーの混乱を最小限に抑えるためのチュートリアルは大切な機能になります。
この実装に妥協せず、かつ最小のコストで実現したいと思っていたkobakenの前に現れたのがBalloonというフルKotlinなUIライブラリでした。詳しい利用方法はRepositoryのREADME.mdを参照ください。 これにより面倒な以下の実装を省くことが出来ました!
- 画面にオーバーレイするようなDialogFragment等の実装
- Anchor(指定したView)に対して矢印を当てる(これが一番めんどい)
- Anchorをハイライトする
雑にTextだけにしたり、カスタムレイアウトを適用できたりと表現力が高くておすすめです。もし似たようなバルーン実装を自前で用意しているプロジェクトを抱えているAndroidアプリエンジニアがいたら、置き換えを検討してみるとよいです。
Android Studioの最大ヒープサイズを変更する
kobakenが普段開発で利用しているマシンのスペックは以下の通りです。
なんか端末スペックの割にAndroid StudioがよくOOMするな…と思っていたところ、またまた公式ドキュメントにAndroid Studioの設定という項目を発見しました。ドキュメントに従って、最大ヒープサイズをdefaultの1024MBから4098MBに変更することで動作が改善されました🎉
また、Edit Custom VM Options…
からより細かい設定をすることが可能です。GUI上だと4098MBが上限でしたが、先のオプションから開かれるstudio.vmoptions
を編集することで更に最大ヒープサイズを上げることが可能です。今現在は、試しに8192MBまで上げて開発していますが、不備なく進められています。なお、特段計測した訳ではないため、kobakenの感覚によるレビューになっています。気になった方は是非お手元のAndroid Studioの設定を変更して実感してみてください。
※メモリ割り当てが多すぎると、逆にパフォーマンス低下の原因になる模様です。
挑戦した点
Kotlin Coroutines Flow
新ドローではUI←→ViewModel←→RepositoryのやりとりにKotlin Coroutines Flowを採用しています。実装期間中にSharedFlow
およびStateFlow
が登場し、pixiv Sketchでも早速採用することにしました。Rxに馴染みのある方であれば、SharedFlow
はPublishSubject
でStateFlow
はBehaviorSubject
と言いかえると理解が進むかもしれません。SharedFlow/StateFlowの登場により、Flow待望のHot Streamが実現し、またLiveDataでは叶わなかった複数箇所からのイベント監視が可能になりました。
Kotlin 1.4.31ではFlowの実装にthrottle
やdebounce
など、Rxで実現できていたオペレータが未実装の場合があるので、適宜自前実装が必要になってくることがあります。pixiv Sketchで導入しているFlowの便利拡張関数を置いときます。更に便利なオペレータの実装や拡張関数などがあれば是非SNS等で教えて下さい👀
fun <T> Flow<T>.collectWhenStarted( lifecycleOwner: LifecycleOwner, action: suspend (value: T) -> Unit ) = lifecycleOwner.lifecycleScope.launchWhenStarted { collect(action) } fun <T> Flow<T>.throttleFirst(periodMillis: Long): Flow<T> { require(periodMillis > 0) { "period should be positive" } return flow { var lastTime = 0L collect { value -> val currentTime = System.currentTimeMillis() if (currentTime - lastTime >= periodMillis) { lastTime = currentTime emit(value) } } } }
最後に
苦節6ヶ月と少し…。無事に新ドロー機能のリリースを完遂して、ユーザーに新しい体験を届けることが出来ました。pixiv Sketchでは、この新ドロー機能のさらなる強化にチーム一丸となって取り組んでいます。少しでも興味が湧いてきた方は是非kobaken宛へ気軽にDMください!カジュアルにお話できる場を提供いたします。一緒に最高のAndroidアプリをつくっていきましょう👏
※応募の際に「この記事を読んで応募しました!」と一言いただけると、kobakenが大変喜びます。