#この記事は
・前回(MVVM+Repositoryのアーキテクチャを使っています①)の続きです。
#もくじ
今回はViewModelとRepositoryの部分について書いていきます。
②はViewModelから非同期処理でasync{}.await()
やwithContext()
を使ってRepositoryにある関数を呼び出します。Repositoryにはデータベース(今回はFirestore)にアクセスする処理が直接かいてあり、resume()
という関数で処理の結果を再びViewModelへ返します(⑤に当たる)。便宜上少しだけ⑥のRxRelayやLiveDataについても触れていきます。⑥の全体のコードや詳細はまた次回の記事に書かせてください。
#ViewModel(非同期処理:async/awaitを使うver)
まずRxRelayを使うための下準備をします。アプリレベルのbuild.gradle
のdependencies{}
の中に以下を追加してください。
// RxJava RxRelay
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.0'
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation 'com.jakewharton.rxrelay3:rxrelay:3.0.0'
それではViewModelをみていきましょう。以下は、FirebaseのStorageにdebugフォルダを作り、その中のvoice.mp4に音声データを保存し、その音声のダウンロードURLを取得するコードです。参考:ファイルをアップロードする&ダウンロードURLを取得する
class RecordPublishViewModel : ViewModel() {
// TIPS: repositoryから受け取った結果をActivity/Fragmentへ引き渡すためのRxRelayを用意
val storageUploadSuccess: PublishRelay<StorageSuccess> = PublishRelay.create()
data class StorageSuccess(
val data: String
)
fun uploadVoiceFile(fileName: String, repository: StorageRepository = StorageRepository()) {
viewModelScope.launch {
try {
val firebasePath = "debug/voice.mp4"
// TIPS: async/awaitで非同期処理。repositoryにあるupload()という関数を呼び出しています
val task = async { repository.upload(fileName, firebasePath) }.await()
if (task.isSuccessful) {
val result = StorageSuccess(task.result.toString())
// TIPS: accept()でrepositoryからの結果(result)を受け取り、Activity/Fragmentへ引き渡す
storageUploadSuccess.accept(result)
}
} catch (e: Exception) {
Log.d("ERROR", e.toString())
}
}
}
また次回の記事で詳細を書きますが、RxRelayのaccept()
で受取った値はAvtivity/Fragmentで以下のようにsubscribe{}
を利用して受け継げます。
// TIPS: subscribeで結果(音声のダウンロードURL)を受け取れる
viewModel.storageUploadSuccess.subscribe {
postUrl = it.data
Log.d("TAG", postURL.toString()) // ダウンロードURLが取得できる
}
#Repository
class StorageRepository {
private val storage = Firebase.storage
// TIPS: firebaseのstorageへ音声データを保存してダウンロードURLを取得
suspend fun upload(fileName: String, path: String) : Task<Uri> {
return suspendCoroutine { cont ->
val storageRef = storage.reference
val stream = File(fileName).inputStream()
val ref = storageRef.child(path)
ref.putStream(stream).continueWithTask { task ->
if (!task.isSuccessful) {
task.exception?.let {
throw it
}
}
// TIPS: ダウンロードURLを取得
ref.downloadUrl
}.addOnCompleteListener {
// TIPS: resume()で結果をViewModelへわたす
cont.resume(it)
}
}
}
}
上記が、RxRelayやasync/awaitを使うViewModelとRepositoryのやりとりです。
ちなみに別にRxRelayとasync/awaitは必ずセットで使わないといけないというわけでもないと思います。あくまでRxRelayとasync/awaitの説明をただ一緒のところに書いただけと思ってください。
#ViewModel(非同期処理:withContext()を使うver)
このコードでは、非同期処理部分をasync{}.await()
のかわりにwithContext()
を使っております。また、Repositoryで得た結果をActivity/Fargmentへ伝えるのにはRxRelayではなくLiveDataを使用しております(参考:LiveDataの実装)。コードの内容としては、Firestoreに保存したデータを日付降順で取得するというプログラムです。
class HomeViewModel : ViewModel() {
// TIPS: LiveDataを使用してrepositoryから受け取ったデータをActivity/Fragmetに渡す
private val _postsData = MutableLiveData<List<Post>>()
// "Post"はdataClassの名前
val postsData: LiveData<List<Post>> = _postsData
private val repository = LoadPostsRepository()
fun loadPost() = viewModelScope.launch {
try {
// TIPS: withContext()で非同期処理
// TIPS: repoitoryのload()という関数を呼び出している
val posts = withContext(Dispatchers.Default) { repository.load() }
// TIPS: LiveDataに結果を渡す
_postsData.value = posts
} catch (e: Exception) {
Log.d("ERROR", e.toString())
}
}
}
上のasync{}.await()
で書いてみたバージョンを試してみると分かりますが、黄色の波線が現れて、『asyncを余分に使っていて、withContextを使うと解消できますよ』と表示されます。その表示の少し下にmerge call chain to withContext
と青く書かれた部分があるのでそこをクリックすると自動でasync{}.await()
をwithContext()
にしてくれます。async{}.await()
も使えますが、このコードの場合はwithContext()
の方がきれいなのかもしれません。
では以下で、ViewModelでLiveDataを使った場合のRepositoryをみていきましょう。
class LoadPostsRepository {
suspend fun load(): List<Post> {
return suspendCoroutine { cont ->
val db = FirebaseFirestore.getInstance()
// TIPS: "posts"はアクセスしたいFirestoreのコレクション名です
val task = db.collection("posts")
// TIPS: このPostはDataClassの名前です
.orderBy(Post::createdAt.name, Query.Direction.DESCENDING)
.limit(20)
.get()
task.addOnCompleteListener {
val resultList = task.result.toObjects(Post::class.java)
// resume()で結果をViewModelへ渡します。
cont.resume(resultList)
}
}
}
}
参考:AndroidでFirebaseのCloudFirestoreを使ってみた(Kotlin)
RxRelayでもLiveDataでもrepositoryの書き方は変わりません。どちらもresume()
でFirebaseにアクセスした結果をViewModelに渡しています。
また次回以降詳しくかきますが、LiveDataをつかった場合Activity/Fragmentでは以下のようにobserve()
を使ってデータを受け取ります。
private val viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callHomeViewMode()
viewModel.run {
postsData.observe(
viewLifecycleOwner,
{ homeListAdapter.submitList(it) }
)
}
}
private fun callHomeViewMode() {
viewModel.loadPost()
}
上記のコードはobserve()
で受取ったデータを、submitList()
をつかってListAdapterに送っています。うけとったデータ(List)をRecyclerViewに表示するためです。
ちなみに、取得したドキュメントの中から特定のフィールドのみ取り出したい場合は以下の様にします。
private lateinit var auth: FirebaseAuth
override fun onViewCreated() {
auth = FirebaseAuth.getInstance()
val user = Firebase.auth.currentUser
val uid: String = user?.uid!!
// UserInfoはデータクラスの名前
val userList: List<UserInfo> = userInfoRepository.getUser(uid)
// userNameはUserInfoデータクラスの要素
Log.d("USER_NAME", userList[0].userName!!)
}
上記のコードは、引数のuidと一致するuidを持つドキュメントのデータを全て取得し、リスト型の変数に格納後、インデックスを利用してuserNameというフィールドのみを取り出してログに表示させているコードである。
#まとめ
・ViewModelからRepositoryへアクセスするのは非同期処理でasync{}.await()
やwithContext()
が使えます。
・Repositoryでの結果をViewModelで受取り、さらにそれをActivity/Fragmentに伝えるのにはRxRelayのaccept()
・subscribe{}
を利用したり、LiveDataとobserve()
を利用することもできます。
間違っているところ等あればご指摘よろしくお願い致します!!!
#まめちしき
・Viewがないとき(BackgroundServiceClassの実装など)はViewModelいらない。
・ViewがないときはGlobalScope
もいらない(もともとUIスレッドと分けるためのものだから)