はじめに
こんにちは、WEARフロントエンド部Androidブロックの酒井柊輔です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。
WEARアプリは2024年5月に大規模なリニューアルをしました。そのため新たに多くの画面やUIを開発する必要がありました。しかしWEARアプリはビルド時間が長く、少しの変更を確認するだけでも数分かかるため、新規のUIの作成やUIの変更と確認に多大な時間を要していました。
このような課題を弊チームでは、UIの開発と確認をするための、ビルド時間の短い簡易アプリを作成することで解決しました。本投稿ではそのUI確認用簡易アプリを用いた課題解決アプローチの詳細と、その成果についてご紹介します。
目次
WEARプロジェクトのモジュール構成
弊チームでは現在マルチモジュール化を進めており、以下の図のようなモジュール構成(イメージ)で当時は開発をしていました。
├── Project │ ├── :app │ ├── :feature │ │ ├── :home │ │ │ ├── HomeFragment │ │ │ ├── HomeFragmentViewModel │ │ │ └── HomeScreen │ │ ├── :search │ │ │ ├── SearchFragment │ │ │ ├── SearchFragmentViewModel │ │ │ └── SearchScreen │ │ └── :mypage │ │ ├── MyPageFragment │ │ ├── MyPageFragmentViewModel │ │ └── MyPageScreen │ ├── :core │ │ └── :ui │ │ ├── CommonButton.kt │ │ ├── CommonCard.kt │ │ └── etc... │ ├── :infrastructure │ └── :domain
- :app:元々利用していたモジュール。各モジュールに分割しきれていないファイル群(navigation関連ファイル、ネットワークアクセス関連ファイル等)が格納されている
- :feature:アプリの画面や機能毎に分割されたモジュール群
- :home, :search, :mypage:各画面に関連するファイル群を格納するモジュール
- :core:アプリ共通のファイルを格納するモジュール群
- :ui:UI共通部品を格納するモジュール
- :infrastructure:ネットワークアクセスロジックがまとめられているモジュール群
- :domain:ビジネスロジックや共通のモデルがまとめられているモジュール群
ビルド時間が大幅にかかるという課題
些細な変更を確認するにも、多大なビルド時間を要することが課題でした。
当時はWEARの大規模リニューアル開発の真っ最中だったので、多くの新たなUIをJetpack Composeで作成する必要がありました。しかしアプリの長いビルド時間によって、作成したUIの確認と修正のサイクルを効率よく回せず、開発が滞ってしまう問題を抱えていました。
解決への取り組み
ビルドが遅い原因を調査したところ、モジュール分割しきれていないファイル群が入っている:appのビルドに大幅な時間を要していることが分かりました。
幸い、UIに関するファイルはほとんど:appから別モジュールへ分割できている状態でした。そのため弊チームでは課題を、:appをはじめとする不要なモジュールを抜いた、必要最低限の依存関係を持つ簡易アプリを、UI開発用に作ることで解決しました。
具体的なアプローチ
作成したUI開発用アプリの概要
UI開発用アプリの開発環境として、Project配下にui-appというディレクトリを作成しました。ui-appの中には各開発者のUI確認用モジュールが格納されています。
├── Project │ ├── ui-app │ │ ├── :developer1 │ │ ├── :developer2 │ │ └── :developer3 │ ├── :app │ ├── :feature │ │ ├── :home │ │ │ ├── HomeFragment │ │ │ ├── HomeFragmentViewModel │ │ │ └── HomeScreen │ │ ├── :search │ │ │ ├── SearchFragment │ │ │ ├── SearchFragmentViewModel │ │ │ └── SearchScreen │ │ └── :mypage │ │ ├── MyPageFragment │ │ ├── MyPageFragmentViewModel │ │ └── MyPageScreen │ ├── :core │ │ └── :ui │ │ ├── CommonButton.kt │ │ ├── CommonCard.kt │ │ └── etc... │ ├── :infrastructure │ └── :domain
各開発者専用のUI確認用モジュールの構成
各開発者のモジュールの構成は以下のようにしました。
├── :developer1 │ ├── src │ │ └── main │ │ ├── java │ │ │ └── com.xxx │ │ │ ├── UiApplication │ │ │ ├── UiActivity │ │ │ └── UiFragment │ │ └── res │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.webp │ │ └── AndroidManifest.xml │ ├── .gitignore │ └── build.gradle.kts
UiApplication
Applicationです。最小構成での実装です。
class UiApplication : Application() { override fun onCreate() { super.onCreate() // サードパーティライブラリの初期化処理等 } }
UiActivity
Activityです。replaceメソッドの第二引数に任意のFragmentを渡すことで、好きな画面を表示できるようにしています。
class UiActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val containerId by lazy { View.generateViewId() } setContent { AndroidView( factory = { context -> FragmentContainerView(context).apply { id = containerId } }, update = { supportFragmentManager.commit { replace(containerId, UiFragment()) } }, ) } } }
例えば:feature:homeをこのモジュールでimplementしていたとしたら、replace(containerId, HomeFragment())
とすればホーム画面を表示できます。
UiFragment
Fragmentです。主にJetpack Composeで作成したUIを確認する用途で利用していました。
単純にsetContent内に作成したUI配置し、端末上で表示して確認することを行なっていました。例えば:core:uiをこのモジュールでimplementしていたとしたら、作成したUI共通部品をsetContent内から参照し確認できます。
class UiFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View = ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner), ) setContent { Surface( modifier = Modifier.fillMaxSize(), ) { // 作成したComposeのUI CommonButton() } } } }
build.gradle
UI開発用アプリモジュールのbuild.gradleです。
plugins { apply("com.android.application") apply("org.jetbrains.kotlin.android") apply("org.jetbrains.kotlin.plugin.compose") } android { // 既存プロジェクトの設定と同じものを記述 } dependencies { // UI確認に最低限必要な依存関係 implementation xxx implementation yyy implementation zzz // 後から各開発者が必要に応じて追加する依存関係 implementation project(":feature:home") implementation project(":feature:search") implementation project(":feature:mypage") implementation project(":core:ui") }
pluginsにはモジュールのビルドに必要なcom.android.application
と、Kotlinを扱うのに必要な2つのプラグインを記述しました。
dependenciesには最低限必要な依存関係のみ予め記述しておき、その他の依存関係は各開発者が必要に応じて自身のモジュールのbuild.gradleに記述する運用としました。
WEARでは、:feature:homeや:core:ui等のUI開発に必要な依存関係のみをimplementすることで、ビルド時間のかかる不要な依存関係(:app等)を省いたアプリを実現しました。
UI開発用アプリで得られた成果
このようなアプリを作成することで、以下のような成果を得られました。
- 依存モジュールの制限によるビルド時間の短縮
- 既存画面やUI共通部品を利用できる
- 各開発者が独自の環境を運用できる
依存モジュールの制限によるビルド時間の短縮
ビルド時間が5〜10分かかっていたのを10秒程度に短縮でき、チームの開発効率が上がりました。
また、UI作成のトライアンドエラーのサイクルを回しやすくなったので、Android開発にまだ慣れていないチームメンバーの技術キャッチアップの手助けにもなりました。
既存画面やUI共通部品を利用できる
別Projectではなく同Project内にUI開発用アプリを作ることで、既存画面に新規作成UIを組み込みながら開発できたり、アプリ内で利用されるUI共通部品を利用した開発も行えたりしました。
WEARでの事例だと、:feature:homeをimplementして既存のホーム画面を利用した開発をしたり、:core:uiをimplementして共通UI部品を利用した開発をしたりしました。
各開発者が独自の環境を運用できる
developer1, developer2のように、各開発者のモジュールを作成することによって各々が独自のUI開発用アプリの環境を作ることができました。
WEARチームではこれらの各開発者のモジュールを、他のコードと同様にGitで管理していました。そのため作成したUIを確認できるようなコードを開発者専用のモジュールに記述しておけば、レビューする人がPR確認時にそのモジュールを手元でビルドしUIを確認することにも利用できました。
おわりに
本記事では、ビルド時間の短いUI開発用アプリの作成方法とその運用方法、得られた成果をご紹介しました。
UIに関するファイルのモジュール分割が既にできていれば、どの開発現場でも適用できる事例であると思います。もし同様の問題を抱えていれば、アプリの作成を検討してみてはいかがでしょうか。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。