挨拶
こんにちは、ソーシャル経済メディア「NewsPicks」のモバイルアプリチームでAndroidを担当していますsefwgweoです。
この記事は NewsPicks アドベントカレンダー 2023 の4日目の記事です。
概要
今年、Android版Newspicksのダークモードを半年がかりでリリースした時にどのようにして見積もったかを実例をまじえて振り返ろうと思います
デザイナチームによるまとめはこちら
iOSは本プロジェクトでモバイルチームとして陣頭指揮をとってくれた@_asa08_さんが後日記載予定ですのでお楽しみに!
前提
Android版Newspicksは10年以上サービスが続いているため、見積もる際以下な問題をはらんでいました
- 画面数が多い
- 機能数が多い
- 仕様が複雑
見積もり方法
見積もる単位は1画面毎
前提にあるように、そもそも対応が必要な画面数がとても多かったためまずはチームメンバー全員で全ての画面(ダイアログ含む)のスクショを撮りNotionにまとめました。
枚数としてはおよそ240枚ほどで、そもそも対象画面を出すのもひと手間必要な画面もありました。
また、スクショだけだと見積もりはもとよりダークモード対応実作業者にも優しくないため、初期段階では対応画面クラス名の記載及び必要に応じて画面到達方法を入れるというルールで進行しました。
1画面にかかるコストをパターン化
無事スクショ一覧が完成後は、見積もりに必要な項目や懸念を洗い出すため約1ヶ月ほど1人が稼働の3割〜5割を使ってダークモード対応を行いました。
この1ヶ月があったおかげでかなり精度のよい見積もりが出来たと思っています。
結果、以下のような4つのケースに分類できることがわかりました。
- 1画面につき30分程度で完了する最もライトなケース(全体の2割程度)
- 1画面につき60分程度で完了する最も対応が多かったケース(全体の4割程度)
- 1画面につき120分程度で完了する2番目に多かったケース(全体の3割程度)
- 1画面につき半日〜1日で完了する最も重いケース(全体の1割以下)
見積もり時間の内訳
PullRequestにBefore/Afterのスクリーンショットを貼ってデザイナにOKをもらい、masterブランチにマージされるまでの時間としています
ケースの具体
最もライトな30分のケースについて
変更がXMLのみで完結し、かつ対応箇所も多くない(1桁程度)ケースにおいては30分と定義しました。
具体的には以下のような内容です。
修正前XML(一部を抜粋)
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_600">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_tab_setting".. />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_70".. />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/gray_70".. />
</androidx.constraintlayout.widget.ConstraintLayout>
修正後XML(一部を抜粋)
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/surface_base_default">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/label_tab_setting".. />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_base_secondary".. />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_base_secondary".. />
</androidx.constraintlayout.widget.ConstraintLayout>
作業内容としては以下です
- 文字色をダークモードも対応しているカラーパレットのものにする
- 画面背景色をダークモードも対応しているカラーパレットのものにする
- 画像をライトモードとダークモード用にFigmaからダウンロードし、drawable配下とdrawable-night配下に置いたものを参照するようにする
- ストア版、修正したライトモード版とダークモード版の3種類のスクリーンショットを撮ってPullRequestに貼る
- デザインOKが出たらPullRequestをマージする
最も対応が多かった60分のケースについて
1画面内に何らかのアクションをするパーツがあると変更箇所と確認箇所が増えるため、この60分のケースが対応数が最も多い結果となりました。
例えば「いいね」をダークモード対応しようとすると、ONの状態・OFFの状態・タップ時のアニメーションと3つのケースを確認する必要があります。
弊社ではアニメーションは初期の頃から両OS共Lottieを用いておりダークモードに対応する時少し苦労しました。
詳しくはこちらの記事にまとめてあります
作業内容としては上記の通り最もライトなケースにプラスアルファでアクションパーツがあるケースが大半で、スクリーンショットとデプロイゲートを配布して実挙動を確認してもらう手間が追加される感じでした
特にアニメーション等は実際に動いている状態を見て差し替え、ということもあったため、平均的に60分程度かかっていました。
2番目に対応が多かった120分のケースについて
最も対応数が多かった60分のケースとの違いとしては大きく2パターンありました
1つは、画面自体作成されたのが結構昔で実装が古すぎるケース、もう1つは技術的な問題に当たって時間がかかったケースです。
前者で一番面倒だったのは、CustomViewで共通的にパーツを使いまわしていて該当画面で直すと他でも影響が出てしまう場合でした。この場合は基本的にはCustomViewを使わずに作り直す必要があったため、時間がかかりました
後者は例えば以下のようなことがありました。
OS13以上の端末でButtonレイアウトにborderlessButtonStyleを用いていると、backgroundを指定しているのに色が反映されなかった
以下NGだったXML
<Button
style="?android:attr/borderlessButtonStyle"
android:background="@{@drawable/bg_radius_34dp_subtle_button}".. />
以下OKだったXML
<androidx.appcompat.widget.AppCompatButton
style="?android:attr/borderlessButtonStyle"
android:background="@{@drawable/bg_radius_34dp_subtle_button}".. />
作業内容としては上記の通り60分のケースにプラスアルファで技術的な調査や作り直しのコスト分で平均的に120分程度かかっていました。
最も時間がかかった半日〜1日のケースについて
半日以上かかるケースというのは実際は2件しかなく、原因は古い実装の箇所でした。
具体的にはタブ機能が独自に1クラス内で実装されており、画面単位での見積もりとしたためタブの数だけ対応する必要が出た、という感じでかなりイレギュラーかと思います。
まとめ
上記をふまえて1つ1つ反映してようやく約4人月と算出できました(お試し期間は除いています)。
個人的にはスタートからリリースまで約半年というスパンでの見積もりが初めてだったため、色々不安でしたが大きく予定がズレることもなく、途中別のエンジニアがアサインされてもスケジュール通り進行できたので満足でした。
おまけ:ダークモード対応時ハマったこと
アルファ値ありの枠線レイアウトをFrameLayoutで上から被せると期待通りの色にならなかった
以下NGだったXML(画像を角丸にし、角丸の枠線をつける)
<androidx.constraintlayout.widget.ConstraintLayout..>
<androidx.cardview.widget.CardView..>
<androidx.appcompat.widget.AppCompatImageView.. />
</androidx.cardview.widget.CardView>
<!-- gray色枠線背景 -->
<FrameLayout
android:background="@drawable/bg_radius_4dp_stroke_border_gray"../>
</androidx.constraintlayout.widget.ConstraintLayout>
以下OKだったXML(画像を角丸にし、角丸の枠線をつける)
<androidx.constraintlayout.widget.ConstraintLayout..>
<!-- gray色枠線背景 -->
<FrameLayout
android:background="@drawable/bg_radius_4dp_stroke_border_gray"../>
<androidx.cardview.widget.CardView..>
<androidx.appcompat.widget.AppCompatImageView.. />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
ダーク/ライト切り替え方法
こちらを参考に以下のようにダークモードかどうかを判定するようにしました
fun isDark(): Boolean {
val defaultNightMode = AppCompatDelegate.getDefaultNightMode()
if (defaultNightMode == AppCompatDelegate.MODE_NIGHT_YES) {
return true
}
if (defaultNightMode == AppCompatDelegate.MODE_NIGHT_NO) {
return false
}
// システム設定に従うケース
val currentNightMode: Int = (context.resources.configuration.uiMode
and Configuration.UI_MODE_NIGHT_MASK)
when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> return false
Configuration.UI_MODE_NIGHT_YES -> return true
Configuration.UI_MODE_NIGHT_UNDEFINED -> return false
}
return false
}
res/menu配下にあるようなIcon(DrawerMenu等)のダークモード対応方法
以下にあるようにapp:itemIconTintとapp:itemTextColorで設定可能です
<com.google.android.material.navigation.NavigationView
app:itemIconTint="@color/object_base_primary"
app:itemTextColor="@color/text_base_primary"
app:menu="@menu/drawer_menu".. />
SearchViewに関してはActivity内で動的に色を指定してもOS13以降はView内の色が期待通りに変わらなかったため、こちらの対処をしました
ヘッダーを独自のCustomViewで定義している画面がそこそこ沢山あり、それらをすべてToolbarを用いた共通のXMLで置き換えました。
画面によっては同名XMLで410dp用等が存在しており、片方だけ変えても変更しませんでした。
終わりに
今回はAndroidにおける巨大プロジェクトのダークモード対応する際の見積もり手法について紹介しました。
記事では大変だった箇所ばかり紹介していますが、デザインチームの用意してくれたカラーパレットを含めたデザインシステムが本当に便利で開発効率がとても向上しました。
まだまだデザインシステム化が完全にできてはいないので道半ばですが、今後もっとデザインシステムとの連携がよいものになっていくのが楽しみです
NewsPicks ではエンジニアを募集中です!ご興味のある方はこちらまで。
明日は@nakamichiさんが書いてくれます。お楽しみに!