18
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android実装MVVM+Repositoryパターン2020年08月時点

Posted at

はじめに

1年に2本くらいAndroidのアプリ実装案件をやっていて、その度にアーキテクチャとライブラリの選定をしています。
アーキテクチャは基本的にはMVVM+Repositoryパターンをベースにしていますが、ライブラリに関してはプロジェクトの状況によって使用するものを変えています。昨今のAndroidアプリの実装における流行り廃りの激しさも含め、毎回選定内容が変わっているので、今回の選定に関して自分用のメモとして残し、色々な方の意見をお聞きできたらと思っています。

プロジェクトの前提条件

企業が新規事業として行うサービスで公開されるアプリです。

  • 納期が短い(私がお願いされる時点でだいたい短い)
  • 最終的に企業内のシステム担当に引き継ぐ可能性がある
  • システム担当の方のスキルは未知数
  • 予算の関係でテストコードなしでOK

こういう案件は多いです。テストコードについては省略したくないところではありますがPMFの測定段階では、次のフェーズに進む際に大きくアプリの構造を変える場合があるのでView周りのテストコードを落とすという判断をしています。ビジネスロジックはサーバ側に寄せる設計にしていますが、アプリ側で実装する場合もあるので、そういったものはテストコードを書くようにしています。

考え方

基本的には、Android Developersなどの公式のドキュメントと、導入例をみて考えていますが、毎回悩むのは__非同期処理__と__DI__周りのライブラリです。非同期処理でいうとAsyncTaskRxcoroutineなど含めて何を選択するかを、DIに関してはライブラリ以前にそもそも導入するかどうかを考えています。

また、これらのライブラリの選定に関しては何を導入するかもそうですが、導入したライブラリをどこで使うかをはっきりさせることを大切にしています。

この辺のライブラリは学習コストが非常に高いと思っています。__ちゃんと知っていれば__便利で、コードもスマートになったりしますが、この__ちゃんと知っていれば__のハードルが高いと思ってるので、なるべく使用箇所を限定して、ライブラリの導入意図をはっきりさせた使い方を心がけています。

アーキテクチャに関しては、アーキテクチャというよりRecyclerView実装時の各クラスの責務を毎回迷っています。MVVMと言っていますがここだけはどうしてもいびつになってしまうのでしっかりと方針を決めるようにしています。

今回の選定

1.非同期処理
coroutineにしました。公式のドキュメントで推奨されたのは大きいです。

Android での Kotlin コルーチン

ただし、今回は比較的シンプルなアプリなので、Repositoryクラス内でのデータ取得関連部分に使用をとどめ、ViewModel内でのviewModelScope.launch等は使用しないようにする予定です。共通部分のみで使用することで、ソースコードを引き継いだ人がcoroutineに詳しくなくてもある程度改修は行えるようにしています。

2.DI
Hiltを導入することにしました。これも公式ドキュメントの記載は大きいです。

Hilt を使用した依存関係の注入

ただし、ViewModelRepositoryクラスをDIするためと、KTX拡張機能を使用した、Fragment等へのViewModelのDIのみに使用することで、新規画面追加時でもお手前として記述方法さえわかってれば良いようにしました。

3.RecyclerViewの実装方法
Data bindingは行わず、Fragment内でAdapterにデータをセットする予定です。本当はViewModelRecyclerViewのデータをLiveData形式で持たせて、直接RecyclerViewにデータをバインドしたいと思っていました。

RecyclerView + ListAdapter + ViewModel + LiveDataでData Binding

この記事のようにきれいに書ければこうなっている方が良いと思っています。
ただし、Repositoryパターンを使って、API等でデータを取ってくる場合、以下ようなResourceクラスでラップしたLiveDataを返すようにしているので、直接データをViewModelに持とうとすると、毎回ViewModelにレスポンスを処理するコードを書かなければならず冗長になってしまいます。また、公式ドキュメントでも以下のような記載があるのでFragmentでの値セットと判断しました。
FragmentLiveDataを監視し、データを受け取った際に、Adapterにセットするか、ViewModelにデータをセットするかも迷いどころですが、今回はリストの追加・削除等がないので前者で考えています。

ViewModel の概要

ライフサイクル対応の監視可能オブジェクト(LiveData オブジェクトなど)への変更を監視しないでください。

Resource.kt
class Resource<out T> private constructor(val status: Status, val data: T?, val message: String?) {
    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String?, data: T? = null): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(): Resource<T> {
            return Resource(Status.LOADING, null, null)
        }
    }
}

RecyclerViewはほとんどのアプリで使用するため、毎回MVVMと言いながら実際にはきれいなMVVMになってないなと感じています。最近Flutterで実装しているときれいにMVVMにできるため、Android実装時の迷いが大きくなりました。

4.失敗談(Jack vs Kotlin)

2017年の2月くらいのプロジェクトでJava8の言語機能を使うためにJackを使うか、Kotlinを導入するか悩んで、Jackを選択したのですが、そのアプリのリリース後、7月のGoogle I/OでKotlinが正式な開発言語と発表されたときはかなり参りました。

主要クラスの実装

まだ改良は重ねますが、現状お知らせ一覧を表示する画面はこんな感じになっています。

Repositoryクラスの実行時だけ、coroutineを使用しています。

AppRepository.kt
@Singleton
class AppRepository @Inject constructor(
    private val apiService: ApiService
) {
    fun getAnnouncements(): LiveData<Resource<AnnouncementsResponse>> {
        return execute { apiService.getAnnouncements() }
    }

    private fun <T> execute(function: suspend () -> T): LiveData<Resource<T>> {
        return liveData(Dispatchers.IO) {
            emit(Resource.loading())
            try {
                emit(Resource.success(function()))
            } catch (exception: Exception) {
                emit(Resource.error(exception.message))
            }
        }
    }
}

ViewModelはDI周りの構文があるくらいで、処理はRepositoryクラスのメソッドを呼ぶくらいになっています。

AnnouncementsViewModel.kt
class AnnouncementsViewModel @ViewModelInject constructor (
    private val appRepository: AppRepository
): ViewModel() {
    fun getAnnouncements() = appRepository.getAnnouncements()
}

FragmentにおいてはAPIの返りを監視し、データ取得時にAdapterにデータをセットしています。

AnnouncementsFragment.kt
@AndroidEntryPoint
class AnnouncementsFragment : BaseFragment(), AnnouncementRecyclerAdapter.AnnouncementListener {
    private val viewModel: AnnouncementsViewModel by viewModels()
    private val announcementAdapter by lazy { AnnouncementRecyclerAdapter(this) }
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? =
        FragmentAnnouncementsBinding.inflate(inflater, container, false).also {
            it.handler = this
            it.viewModel = viewModel
            it.lifecycleOwner = this
            it.announcementRecyclerView.adapter = announcementAdapter
        }.root

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        loadAnnouncements()
    }

    private fun loadAnnouncements() {
        viewModel.getAnnouncements().observe(viewLifecycleOwner, Observer { resource ->
            handleResource(resource,
                successProcess = {
                    it.data?.let { response ->
                        announcementAdapter.bindAnnouncements(response.announcements)
                        announcementAdapter.notifyDataSetChanged()
                    }
                })
        })
    }
}

最後に

とはいえ、アーキテクチャにしろライブラリにしろ好きにするべきだし、それでいいと思います。ただ最近、何か割れてきた(実装のパターンが増えた)気がしたので現時点の考えをまとめてみました。※選択肢が増えることは良いと思います。

18
17
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?