Karunesh Palekar
Karunesh Palekar

Reputation: 2345

Coroutine Flow error while retrieving data from Firebase

This is the first time I trid imlementing Flow with Firebase using the MVVM Architectural Pattern. The issue here is when I press loadbutton Data is loaded in the textview. But then when I upload new data and try and get the new data along with the old it doesn't work, It just displays the old data. After restarting the app it provides with the new data. Below is the code.

Repository

@ExperimentalCoroutinesApi
class PostsRepository {

    private val mPostsCollection =
        FirebaseFirestore.getInstance().collection(Constants.COLLECTION_POST)

    fun getAllPosts() = flow<State<List<Post>>> {

        emit(State.loading())

        val snapshot = mPostsCollection.get().await()
        val posts = snapshot.toObjects((Post::class.java))

        emit(State.success(posts))

    }.catch {
        emit(State.failed(it.message.toString()))
    }.flowOn(Dispatchers.IO)


    fun addPosts(post: Post) = flow<State<DocumentReference>>
    {
        emit(State.loading())

        val postref = mPostsCollection.add(post).await()

        emit(State.success(postref))

    }.catch {
        emit(State.failed(it.message.toString()))
    }.flowOn(Dispatchers.IO)


}

State Class

sealed class State<T> {
    class Loading<T> : State<T>()
    data class Success<T>(val data: T) : State<T>()
    data class Failed<T>(val message: String) : State<T>()

    companion object {
        fun <T> loading() = Loading<T>()
        fun <T> success(data: T) = Success(data)
        fun <T> failed(message: String) = Failed<T>(message)

    }

}

The ViewModel

@ExperimentalCoroutinesApi
class MainViewModel(private val repository: PostsRepository):ViewModel() {


   // fun getAllposts() = repository.getAllPosts()

    val getallpostlivedata :LiveData<State<List<Post>>> = repository.getAllPosts().asLiveData()


    fun addpost(post: Post) = repository.addPosts(post)

}

The MainActivity


@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity(), View.OnClickListener {

    private lateinit var viewModel: MainViewModel

    private lateinit var binding: ActivityMainBinding


    private val uiScope = CoroutineScope(Dispatchers.Main)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        viewModel = ViewModelProvider(this, MainViewModelFactory()).get(MainViewModel::class.java)

        binding.buttonLoad.setOnClickListener(this)

        binding.buttonAdd.setOnClickListener(this)

    }




    private suspend fun addPost(post: Post) {

        viewModel.addpost(post).collect{ state ->
            when (state) {
                is State.Loading -> {
                    showToast("Loading")
                    binding.buttonAdd.isEnabled = false
                }

                is State.Success -> {
                    showToast("Posted")
                    binding.fieldPostContent.setText("")
                    binding.buttonAdd.isEnabled = true
                }

                is State.Failed -> {
                    showToast("Failed! ${state.message}")
                    binding.buttonAdd.isEnabled = true
                }
            }
        }

    }


    override fun onClick(v: View?) {
        when (v!!.id) {
            binding.buttonLoad.id -> {
                uiScope.launch {
                    loadPosts()
                }
            }
            binding.buttonAdd.id -> {
                uiScope.launch {
                    addPost(
                        Post(
                            postContent = binding.fieldPostContent.text.toString(),
                            postAuthor = "Karunesh Palekar"
                        )
                    )
                }
            }
        }


    }

    private  fun loadPosts() {
        viewModel.getallpostlivedata.observe(this, Observer { state->
           when(state){
               is State.Loading ->{
                   showToast("Loading")
               }
               is State.Success ->{
                   val postText = state.data.joinToString("\n") {
                       "${it.postContent} ~ ${it.postAuthor}"
                   }
                   binding.textPostContent.text = postText
               }
               is State.Failed ->{
                   showToast("Failed! ${state.message}")
               }
           }
        })
    }

    private fun showToast(message: String) {
        Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
    }

}

Thank-You for helping out.

Upvotes: 2

Views: 1788

Answers (1)

Shreyas Patil
Shreyas Patil

Reputation: 954

The method getAllPosts() is returning a Flow which is only loading your posts data at once. It'll be only loaded when you'll call collect { } on that Flow.

If you want real-time snapshot updates of Cloud Firestore then you can update method getAllPosts() as below.

fun getPostsRealtime() : Flow<State<List<Post>>> = callbackFlow {

    // Register listener
    val listener = addSnapshotListener { snapshot, exception ->

        offer(State.success(snapshot.toObjects(Post::class.java)))

        // If exception occurs, cancel this scope with exception message.
        exception?.let {
            offer(State.error(it.message.toString()))
            cancel(it.message.toString())
        }
    }

    awaitClose {
        // This block is executed when producer channel is cancelled
        // This function resumes with a cancellation exception.

        // Dispose listener
        listener.remove()
        cancel()
    }
}

Here, we're using callbackFlow {} which allows us to use callback methods and value emission asynchronously. Hope it'll be helpful for you.

Upvotes: 6

Related Questions