はじめに
1年に2本くらいAndroidのアプリ実装案件をやっていて、その度にアーキテクチャとライブラリの選定をしています。
アーキテクチャは基本的にはMVVM+Repositoryパターンをベースにしていますが、ライブラリに関してはプロジェクトの状況によって使用するものを変えています。昨今のAndroidアプリの実装における流行り廃りの激しさも含め、毎回選定内容が変わっているので、今回の選定に関して自分用のメモとして残し、色々な方の意見をお聞きできたらと思っています。
プロジェクトの前提条件
企業が新規事業として行うサービスで公開されるアプリです。
- 納期が短い(私がお願いされる時点でだいたい短い)
- 最終的に企業内のシステム担当に引き継ぐ可能性がある
- システム担当の方のスキルは未知数
- 予算の関係でテストコードなしでOK
こういう案件は多いです。テストコードについては省略したくないところではありますがPMFの測定段階では、次のフェーズに進む際に大きくアプリの構造を変える場合があるのでView周りのテストコードを落とすという判断をしています。ビジネスロジックはサーバ側に寄せる設計にしていますが、アプリ側で実装する場合もあるので、そういったものはテストコードを書くようにしています。
考え方
基本的には、Android Developersなどの公式のドキュメントと、導入例をみて考えていますが、毎回悩むのは__非同期処理__と__DI__周りのライブラリです。非同期処理でいうとAsyncTask
、Rx
、coroutine
など含めて何を選択するかを、DIに関してはライブラリ以前にそもそも導入するかどうかを考えています。
また、これらのライブラリの選定に関しては何を導入するかもそうですが、導入したライブラリをどこで使うかをはっきりさせることを大切にしています。
この辺のライブラリは学習コストが非常に高いと思っています。__ちゃんと知っていれば__便利で、コードもスマートになったりしますが、この__ちゃんと知っていれば__のハードルが高いと思ってるので、なるべく使用箇所を限定して、ライブラリの導入意図をはっきりさせた使い方を心がけています。
アーキテクチャに関しては、アーキテクチャというよりRecyclerView
実装時の各クラスの責務を毎回迷っています。MVVMと言っていますがここだけはどうしてもいびつになってしまうのでしっかりと方針を決めるようにしています。
今回の選定
1.非同期処理
coroutine
にしました。公式のドキュメントで推奨されたのは大きいです。
ただし、今回は比較的シンプルなアプリなので、Repository
クラス内でのデータ取得関連部分に使用をとどめ、ViewModel
内でのviewModelScope.launch
等は使用しないようにする予定です。共通部分のみで使用することで、ソースコードを引き継いだ人がcoroutine
に詳しくなくてもある程度改修は行えるようにしています。
2.DI
Hilt
を導入することにしました。これも公式ドキュメントの記載は大きいです。
ただし、ViewModel
にRepository
クラスをDIするためと、KTX拡張機能を使用した、Fragment
等へのViewModel
のDIのみに使用することで、新規画面追加時でもお手前として記述方法さえわかってれば良いようにしました。
3.RecyclerViewの実装方法
Data binding
は行わず、Fragment
内でAdapter
にデータをセットする予定です。本当はViewModel
にRecyclerView
のデータをLiveData
形式で持たせて、直接RecyclerView
にデータをバインドしたいと思っていました。
RecyclerView + ListAdapter + ViewModel + LiveDataでData Binding
この記事のようにきれいに書ければこうなっている方が良いと思っています。
ただし、Repositoryパターンを使って、API等でデータを取ってくる場合、以下ようなResource
クラスでラップしたLiveData
を返すようにしているので、直接データをViewModel
に持とうとすると、毎回ViewModel
にレスポンスを処理するコードを書かなければならず冗長になってしまいます。また、公式ドキュメントでも以下のような記載があるのでFragment
での値セットと判断しました。
Fragment
でLiveData
を監視し、データを受け取った際に、Adapter
にセットするか、ViewModel
にデータをセットするかも迷いどころですが、今回はリストの追加・削除等がないので前者で考えています。
ライフサイクル対応の監視可能オブジェクト(LiveData オブジェクトなど)への変更を監視しないでください。
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
を使用しています。
@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
クラスのメソッドを呼ぶくらいになっています。
class AnnouncementsViewModel @ViewModelInject constructor (
private val appRepository: AppRepository
): ViewModel() {
fun getAnnouncements() = appRepository.getAnnouncements()
}
Fragment
においてはAPIの返りを監視し、データ取得時にAdapter
にデータをセットしています。
@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()
}
})
})
}
}
最後に
とはいえ、アーキテクチャにしろライブラリにしろ好きにするべきだし、それでいいと思います。ただ最近、何か割れてきた(実装のパターンが増えた)気がしたので現時点の考えをまとめてみました。※選択肢が増えることは良いと思います。