entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Android版 pixiv Sketchの新ドロー機能を開発したのでその裏側を赤裸々に語ります

Androidアプリをつくっているkobakenです。DroidKaigi 2019の登壇予告記事を投稿して以来ですね。皆さん元気にしていましたか?
kobakenはというと、Android版 pixiv Sketchをもりもり開発しておりました。Jetpack Composeをプロダクション投入したすぎてウズウズしています。

さて、今回はそんなAndroid版 pixiv Sketchのドロー機能を刷新して(以下、新ドローと呼びます。)新しい体験をお届けできたので、その開発の裏側についてご紹介していこうと思います。

Android版 pixiv Sketchの新ドロー機能をリリースしました!

Androidをお持ちの方は、とりあえず触ってみてください。

play.google.com

今回紹介するpixiv Sketchの新ドロー機能とは、

  • お絵かき新規勢でも直感的に触れるUI
  • 普段お絵かきする勢でもラフやらくがきを十分に楽しめるシンプルな体験
  • 紙に鉛筆でスケッチするような、思い通りに仕上がる手書きの書き味

などたくさんの魅力ある機能になっています。詳しい説明はこちらの記事を参照ください。

www.pixivision.net

開発環境について

当時の開発状況や環境について振り返ってみたいと思います。
まずは新ドローと以前までのドロー(以下、旧ドローと呼びます。)の実装の違いを見てみましょう。

実装 保存形式
旧ドロー 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/layoutres/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が爆上がりするのでおすすめです。

チュートリアル

旧ドローと比較してガラッとレイアウトが変わったため、新規ユーザーはもちろん、既存ユーザーの混乱を最小限に抑えるためのチュートリアルは大切な機能になります。

f:id:pxvpxv:20210315152545p:plain
チュートリアルの様子

この実装に妥協せず、かつ最小のコストで実現したいと思っていたkobakenの前に現れたのがBalloonというフルKotlinなUIライブラリでした。詳しい利用方法はRepositoryのREADME.mdを参照ください。 これにより面倒な以下の実装を省くことが出来ました!

  • 画面にオーバーレイするようなDialogFragment等の実装
  • Anchor(指定したView)に対して矢印を当てる(これが一番めんどい)
  • Anchorをハイライトする

雑にTextだけにしたり、カスタムレイアウトを適用できたりと表現力が高くておすすめです。もし似たようなバルーン実装を自前で用意しているプロジェクトを抱えているAndroidアプリエンジニアがいたら、置き換えを検討してみるとよいです。

Android Studioの最大ヒープサイズを変更する

kobakenが普段開発で利用しているマシンのスペックは以下の通りです。

f:id:pxvpxv:20210315164134p:plain
弊社の福利厚生の制度を利用して買ってもらいました😊
部署や役職問わず定番な構成の一つです

なんか端末スペックの割に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に馴染みのある方であれば、SharedFlowPublishSubjectStateFlowBehaviorSubjectと言いかえると理解が進むかもしれません。SharedFlow/StateFlowの登場により、Flow待望のHot Streamが実現し、またLiveDataでは叶わなかった複数箇所からのイベント監視が可能になりました。 Kotlin 1.4.31ではFlowの実装にthrottledebounceなど、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アプリをつくっていきましょう👏

recruit.jobcan.jp recruit.jobcan.jp recruit.jobcan.jp

※応募の際に「この記事を読んで応募しました!」と一言いただけると、kobakenが大変喜びます。

20191219012714
kobaken
2017年10月新卒入社。声優案件やpixiv SketchのAndroidアプリ開発を担当しています。チームのムードメーカー的存在。ハヤテのごとく!が人生のバイブル。最近はポケカに夢中。座右の銘は「常にユーザであれ」