MVVM+Repositoryのアーキテクチャを使っています②

#この記事は
・前回([MVVM+Repositoryのアーキテクチャを使っています①](https://qiita.com/mi_iroha/items/9d4da0c1a4f76864eaab))の続きです。

#もくじ
今回はViewModelとRepositoryの部分について書いていきます。![スクリーンショット 2020-11-29 212627.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/e1257630-2660-6b81-efc4-1573b3948adf.jpeg)
②はViewModelから非同期処理で`async{}.await()`や`withContext()`を使ってRepositoryにある関数を呼び出します。Repositoryにはデータベース(今回はFirestore)にアクセスする処理が直接かいてあり、`resume()`という関数で処理の結果を再びViewModelへ返します(⑤に当たる)。便宜上少しだけ⑥のRxRelayやLiveDataについても触れていきます。⑥の全体のコードや詳細はまた次回の記事に書かせてください。

#ViewModel(非同期処理：async/awaitを使うver)
まずRxRelayを使うための下準備をします。アプリレベルの`build.gradle`の`dependencies{}`の中に以下を追加してください。

```build.gradle
// 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を取得する](https://firebase.google.com/docs/storage/android/upload-files?hl=ja)

```kotlin:RecordPublishViewModel.kt
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{}`を利用して受け継げます。

```kotlin:RecordPublishActivity
// TIPS: subscribeで結果(音声のダウンロードURL)を受け取れる
viewModel.storageUploadSuccess.subscribe {
        postUrl = it.data
        Log.d("TAG", postURL.toString()) // ダウンロードURLが取得できる
}
```
#Repository
```kotlin:StorageRepository
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の実装](https://developer.android.com/topic/libraries/architecture/livedata?hl=ja))。コードの内容としては、Firestoreに保存したデータを日付降順で取得するというプログラムです。

```kotlin:HomeViewModel
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をみていきましょう。

```kotlin:LoadPostsRepository
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)](https://qiita.com/morayl/items/c14741ebf17b7c9e4e8b)
RxRelayでもLiveDataでもrepositoryの書き方は変わりません。どちらも`resume()`でFirebaseにアクセスした結果をViewModelに渡しています。

また次回以降詳しくかきますが、LiveDataをつかった場合Activity/Fragmentでは以下のように`observe()`を使ってデータを受け取ります。

```kotlin:HomeFragment
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に表示するためです。

ちなみに、取得したドキュメントの中から特定のフィールドのみ取り出したい場合は以下の様にします。

```kotlin
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スレッドと分けるためのものだから)