コネヒト開発者ブログ

コネヒト開発者ブログ

Android版ママリアプリのリファクタ事情 ~ ViewModel編 ~

これは コネヒト Advent Calendar 2021 16日目の記事です。

こんにちは。2017年11月にAndroidエンジニアとしてjoinした@katsutomuです。

昨年のエントリーで緑髪にした報告を行いましたが。今は緑髪 -> 赤髪 -> 金髪と定期的にアップデートを続けております。

tech.connehito.com

今回はAndroid版ママリアプリのリファクタリングの事情について紹介しています。

はじめに

ママリのAndroidアプリは2014年から開発を続けています。7年間の技術トレンドの変遷のスピードに完全には追いつけず、少しずつコードのレガシー化が進んでおり、さまざまな課題が顕在化しています。

例えば以下のような課題が、あげられます。

  • パッケージ構成の見通しが悪い
  • 複数種類の実装方法が混在している
    • ViewModel層の実装、リストビューの実装、データ永続化、(etc
  • 全てのレイヤーがRxJavaに依存している
  • UseCaseをうまく活用できていない
  • UIのコンポーネントが古い

などなど、あげ始めるとまだまだキリがありませんが、段階的に改善を続けていく予定ですので、今後は本ブログなどで、折に触れて改善事例を紹介していければと思います。

今回はその第一弾としてViewModel層の実装方法が複数存在する問題に着手したので、リファクタリング事例として紹介します。

なぜやるのか

まずこの課題の背景について紹介します。先述したとおり、ママリのAndroidアプリは7年の歴史があります。 その間でAndroid開発では、DataBindingの登場、ViewModel/LiveDataの登場、JetpackComposeの登場など、技術的な変遷がいくつか行われ、開発の負担を減らすように進化していきました。 その反面、当時最新だったコードのリファクタリングが追いつかず、レガシーな方法で書かれたコードが残されている部分があります。

具体的にはViewModel層でJetpackのViewModelとBaseObservableの2種類書き方が混在しています。同じ層に複数の書き方が混在しているため、見通しが悪くなり、開発のボトルネック要因となっています。

この課題を解決し、今よりも快適かつスピーディに開発を進めることを目指します。

狙い

今回のリファクタリングの狙いは以下の2つです。

  • 設計ルールを一つに絞ることで、開発のボトルネックを減らす
  • Jetpack Composeの導入のための投資を行う

本来の開発のボトルネックを減らす目的に加えて、Jetpack Composeの導入を考慮しました。今後Androidアプリを、長期的に開発継続し続ける際には、UI実装をJetpack Composeに置き換えていくことが、開発効率向上にベターな選択になると感じています。このタイミングで導入を進めやすい設計を実現しておくことで、段階的に改善を進めることを狙っています。

やることは以下の2つです

1.ViewModel コンポーネントの設計を統一する

BaseObservableやObservableFieldのコードを、LiveDataに置き換えます。 UIとViewModelの接続部分をLiveDataで統一し、コーディング時の迷いをなくす目的です。 同様にRxでViewと接続しているコードもLiveDataに置き換えます。

LiveDataをStateFlowに置き換えることがよぎりましたが、Coroutineの知識が必要になるため現段階では選択していません。 馴染みのあるメンバーがいないことや、修正範囲が広いことが理由です。段階的にリファクタリングを進める過程で、取捨選択をしていく予定です。

2. xmlでのbindingをやめる

xmlにViewModelを渡し、レイアウト要素ごとに行うbindingはやめて、ActivityやFragment内でレイアウトを更新する設計に変更を行います。

developer.android.com

コード例

現時点では、以下のようなコードが点在しています。

// ViewModel: Layout要素ごとにObservableFieldを用意している
class EntityViewModel constructor(
    private val entityRepository: EntityRepository
)  {

    val frameVisible = ObservableField<Boolean>()
    val name = ObservableField<String?>()
    val imageUrl = ObservableField<String?>()
}

// xml: ViewModelを受け取り bindingをしている
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout">

    <data>
        <variable name="viewModel"
            type="com.connehito.mamariq.viewmodel.ViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:id="@+id/frame"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.frameVisible ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent">

            <ImageView
                android:id="@+id/image"
                original:contentPhotoUrl="@{viewModel.imageUrl}"/>

                <TextView
                    android:id="@+id/name"
                    android:text="@{viewModel.name}" />
            </LinearLayout>
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

これを以下のようなコードに置き換えていく予定です。

// ViewModel: 変更されるオブジェクトをLiveDataで持つ
class EntityViewModel constructor(
    private val entityRepository: EntityRepository
)  {

    private val _entity = NonNullMutableLiveData(Entity.INVALID)
    val entity = _entity as LiveData<Entity>
}

// Activity: オブジェクトを監視し、変更があればUIに反映する
class EntityActivity : AppCompatActivity() {

    private fun bindViewModel() {
        viewModel.entity.observe(this) {
            if (it == Entity.INVALID) {
                return@observe
            }

            binding.name.text = it.name
            ImageLoader.getInstance().displayImage(it.imageUrl, binding.categoryImage)
        }
    }
}

アーキテクチャー図

図にするとこのような感じになります。

Before After
f:id:katsutomu0124:20211201004129p:plain f:id:katsutomu0124:20211201004157p:plain

より理想を目指して

今回はあくまでもリファクタリングの第一歩で、段階的に改善を続けていき、最終的には以下のようなアーキテクチャにたどり着くことを現時点では、目指しています。

f:id:katsutomu0124:20211201003652p:plain

この状態を目指すために、以下の取り組みを進めていく予定です。

  • [Now] ViewModelをリファクタリングする
  • [Now]ドメイン知識を元にしたコンポーネント再設計
  • Jetpack Composeの導入
    • 宣言的UIの導入を行い、開発スピードを向上する。
    • リストビューの実装方法を一つに統一する
  • Kotlin coroutineの導入
    • 非同期処理をサードパーティのライブラリに依存しないようにする
    • LiveDataをFlowに置き換える
  • Material Component利用の拡大

もちろんここに挙げた以外にも、改善が必要なことは多く、同時に技術の変遷の速度も早いです。 今後も、理想像もアップデートしながら改善をつづけ、こちらのブログで紹介していければと思います。

PR

コネヒトでは、今回挙げた課題を一緒に解決してくれるAndroidエンジニアも募集中です!

hrmos.co