7
7

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.

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

Last updated at Posted at 2020-11-30

#この記事は
・前回(MVVM+Repositoryのアーキテクチャを使っています①)の続きです。

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

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

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を取得する

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{}を利用して受け継げます。

RecordPublishActivity
// TIPS: subscribeで結果(音声のダウンロードURL)を受け取れる
viewModel.storageUploadSuccess.subscribe {
        postUrl = it.data
        Log.d("TAG", postURL.toString()) // ダウンロードURLが取得できる
}

#Repository

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の実装)。コードの内容としては、Firestoreに保存したデータを日付降順で取得するというプログラムです。

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

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)
RxRelayでもLiveDataでもrepositoryの書き方は変わりません。どちらもresume()でFirebaseにアクセスした結果をViewModelに渡しています。

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

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

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

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

7
7
0

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
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?