Search code examples
androidkotlinkotlin-coroutinesandroid-livedataswitchmap

Launching a coroutine within a switchmap


So I'm writing an app that displays a list of movies. I want to implement a search functionality where the function to display the search results will be called from the api.

However, I'm having trouble implementing the switchmap within the coroutine. I'm specifically having trouble with the return types as the viewmodelscope returns a job where I want a livedata. Below is the relevant code.

Thank you!

Movies.kt

package com.example.moviesapp.network

import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.Json
import kotlinx.parcelize.Parcelize

@Parcelize
@Entity
data class MoviesResults(
    @Json(name = "results") val results: Movies,
) : Parcelable {
    @Parcelize
    @Entity
    data class Movies(
        @Json(name = "title") val title: String,
        @PrimaryKey(autoGenerate = true)
        @Json(name = "id") val id: Int,
        @Json(name = "release_date") val release_date: String,
        @Json(name = "overview") val overview: String,
        @Json(name = "vote_average") val vote_average: String,
        @Json(name = "poster_path") val poster_path: String,
        @Json(name = "original_language") val original_language: String,

    ) : Parcelable {

    }
}

MoviesApi.kt


package com.example.moviesapp.network

import retrofit2.http.GET
import retrofit2.http.Query


const val API_KEY = "[mykey]"
const val MEDIA_TYPE = "movie"
const val TIME_WINDOW = "week"

interface MoviesApi {


    companion object {

        const val BASE_URL = "https://api.themoviedb.org/3/"
    }


    @GET("search/movie")
    suspend fun getMovies(
        @Query("query") query: String,
        @Query("api_key") key: String = API_KEY,
    ): List<MoviesResults.Movies>


    @GET("trending/${MEDIA_TYPE}/${TIME_WINDOW}")
    suspend fun getTrendingMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("media_type") media_type: String = MEDIA_TYPE,
        @Query("time_window") time_window: String = TIME_WINDOW,
    ): List<MoviesResults.Movies>


    @GET("discover/movie")
    suspend fun getActionMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("with_genres") with_genres: String = "28"

    ): List<MoviesResults.Movies>

    @GET("discover/movie")
    suspend fun getComedyMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("with_genres") with_genres: String = "35"

    ): List<MoviesResults.Movies>

    @GET("discover/movie")
    suspend fun getHorrorMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("with_genres") with_genres: String = "27"

    ): List<MoviesResults.Movies>

    @GET("discover/movie")
    suspend fun getRomanceMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("with_genres") with_genres: String = "10749"

    ): List<MoviesResults.Movies>

    @GET("discover/movie")
    suspend fun getScifiMovies(
        @Query("api_key") api_key: String = API_KEY,
        @Query("with_genres") with_genres: String = "878"

    ): List<MoviesResults.Movies>


}

MoviesRepository.kt

package com.example.moviesapp.network

import androidx.lifecycle.MutableLiveData
import javax.inject.Inject
import javax.inject.Singleton


@Singleton
//We use Inject because I own this class, unlike the Retrofit and MoviesApi class
class
MoviesRepository @Inject constructor(private val moviesApi: MoviesApi) {
    //This function will be called later on in the ViewModel
suspend fun getSearchResults(query:String): MutableLiveData<List<MoviesResults.Movies>> {
    return moviesApi.getMovies(query, API_KEY,).

}

    suspend fun getTrendingMovies(): List<MoviesResults.Movies>  {
        return moviesApi.getTrendingMovies(API_KEY, MEDIA_TYPE, TIME_WINDOW)

    }


    suspend fun getActionMovies(): List<MoviesResults.Movies> {
        return moviesApi.getActionMovies(API_KEY,"28")

    }


    suspend fun getComedyMovies(): List<MoviesResults.Movies> {
        return moviesApi.getComedyMovies(API_KEY,"35")

    }

    suspend fun getHorrorMovies(): List<MoviesResults.Movies> {
        return moviesApi.getHorrorMovies(API_KEY,"27")

    }

    suspend fun getRomanceMovies(): List<MoviesResults.Movies> {
        return moviesApi.getRomanceMovies(API_KEY,"10749")

    }

    suspend fun getScifiMovies(): List<MoviesResults.Movies> {
        return moviesApi.getScifiMovies(API_KEY,"878")

    }








}

MoviesListViewModel.kt

package com.example.moviesapp.ui

import androidx.lifecycle.*
import com.example.moviesapp.network.MoviesRepository
import com.example.moviesapp.network.MoviesResults
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

const val DEFAULT_QUERY = " "


@HiltViewModel
class MoviesListViewModel @Inject constructor(
    private val repository: MoviesRepository,

): ViewModel() {

       private val _moviesAction = MutableLiveData<List<MoviesResults.Movies>>()
       val moviesAction: LiveData<List<MoviesResults.Movies>> = _moviesAction

       private val _moviesComedy = MutableLiveData<List<MoviesResults.Movies>>()
       val moviesComedy: LiveData<List<MoviesResults.Movies>> = _moviesComedy

       private val _moviesHorror = MutableLiveData<List<MoviesResults.Movies>>()
       val moviesHorror: LiveData<List<MoviesResults.Movies>> = _moviesHorror

       private val _moviesRomance = MutableLiveData<List<MoviesResults.Movies>>()
       val moviesRomance: LiveData<List<MoviesResults.Movies>> = _moviesRomance

       private val _moviesScifi = MutableLiveData<List<MoviesResults.Movies>>()
       val moviesScifi: LiveData<List<MoviesResults.Movies>> = _moviesScifi

    private val _moviesTrending= MutableLiveData<List<MoviesResults.Movies>>()
    val moviesTrending: LiveData<List<MoviesResults.Movies>> = _moviesTrending






 fun getAction() {
     viewModelScope.launch {
         _moviesAction.value = repository.getActionMovies()
     }
 }

     fun getComedy() {
         viewModelScope.launch {
             _moviesComedy.value = repository.getComedyMovies()

         }

     }

    fun getHorror() {
        viewModelScope.launch {
            _moviesHorror.value = repository.getHorrorMovies()
        }

    }
    fun getRomance() {
        viewModelScope.launch {
            _moviesRomance.value = repository.getRomanceMovies()
        }

    }

    fun getScifi() {
        viewModelScope.launch {
            _moviesScifi.value = repository.getScifiMovies()

        }

    }

    fun getTrending() {
        viewModelScope.launch {
            _moviesTrending.value = repository.getTrendingMovies()
        }

    }









    private var currentQuery = MutableLiveData(DEFAULT_QUERY)



 

    val movies = currentQuery.switchMap {
            queryString ->
        viewModelScope.launch {
            repository.getSearchResults(queryString)
        }
   }

    fun searchMovies(query: String) {

    currentQuery.value = query

    }

    class MoviesListViewModelFactory @Inject constructor(private val repository: MoviesRepository, private val movie: MoviesResults.Movies): ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(MoviesListViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return MoviesListViewModel(repository) as T
            }
            throw IllegalArgumentException("Unknown ViewModel class")

        }


    }



}


MoviesListAdapter.kt

package com.example.moviesapp.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.moviesapp.R
import com.example.moviesapp.databinding.MovieLayoutBinding
import com.example.moviesapp.network.MoviesResults
import java.util.*

val IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"

class MoviesListAdapter constructor(private val listener: OnItemClickListener) :
     ListAdapter<MoviesResults.Movies, MoviesListAdapter.MoviesListViewHolder>(DiffCallback) {

    private var movies: List<MoviesResults.Movies> = Collections.emptyList()


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesListViewHolder {
        val binding = MovieLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)


        return MoviesListViewHolder(binding)
    }


    override fun onBindViewHolder(holder: MoviesListViewHolder, position: Int) {
        val currentItem = movies[position]
        holder.bind(currentItem)


    }


    inner class MoviesListViewHolder(val binding: MovieLayoutBinding) :
        RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = absoluteAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val item = movies[position]
                    listener.onItemClick(item)
                }
            }
        }

        init {
            binding.root.setOnClickListener {
                val position = absoluteAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val item = movies[position]
                    listener.onFavoriteClick(item)
                }


            }


        }

        init {

            if (binding.favoritesCheckbox.isChecked) {
                showToast("Movie added to favorites")

            } else {
                showToast("Movie removed from favorites")
            }

        }


        fun bind(movie: MoviesResults.Movies) {
            binding.apply {
                movieTitle.text = movie.title
                movieRating.text = movie.vote_average
                movieYear.text = movie.release_date
                Glide.with(itemView)
                    .load(IMAGE_BASE_URL + movie.poster_path)
                    .centerCrop()
                    .error(R.drawable.ic_baseline_error_outline_24)
                    .into(movieImage)


            }

        }


        private fun showToast(string: String) {
            Toast.makeText(itemView.context, string, Toast.LENGTH_SHORT).show()

        }


    }


    interface OnItemClickListener {
        fun onItemClick(movie: MoviesResults.Movies)
        fun onFavoriteClick(movie: MoviesResults.Movies)
    }


    override fun getItemCount(): Int {
        return movies.size
    }




    companion object DiffCallback : DiffUtil.ItemCallback<MoviesResults.Movies>() {
        override fun areItemsTheSame(
            oldItem: MoviesResults.Movies,
            newItem: MoviesResults.Movies
        ): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(
            oldItem: MoviesResults.Movies,
            newItem: MoviesResults.Movies
        ): Boolean {
            return oldItem == newItem
        }
    }


}



MoviesListFragment.kt

package com.example.moviesapp.ui.Fragments

import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.moviesapp.R
import com.example.moviesapp.databinding.FragmentMoviesListBinding
import com.example.moviesapp.network.MoviesResults
import com.example.moviesapp.ui.DaoViewModel
import com.example.moviesapp.ui.MoviesListAdapter
import com.example.moviesapp.ui.MoviesListViewModel
import dagger.hilt.android.AndroidEntryPoint


@AndroidEntryPoint
class MoviesListFragment : Fragment(), MoviesListAdapter.OnItemClickListener {


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_movies_list, container, false)
    }



    private val daoViewModel by viewModels<DaoViewModel>()
    private val viewModel by viewModels<MoviesListViewModel>()
    private var _binding: FragmentMoviesListBinding? = null
    private val binding get() = _binding!!


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //View is inflated layout

        _binding = FragmentMoviesListBinding.bind(view)

        val adapter = MoviesListAdapter(this)

        binding.apply {
            recyclerView.layoutManager = LinearLayoutManager(requireContext())
            //Disable animations
            recyclerView.setHasFixedSize(true)
            recyclerView.adapter = adapter


        }
        //Observe the movies livedata
        //Use viewLifecycleOwner instead of this because the UI should stop being updated when the fragment view is destroyed
        viewModel.getTrending()

       viewModel.moviesTrending.observe(viewLifecycleOwner) {
           adapter.submitList(it)

       }



        //Display trending movies

        //loadstate is of type combined loadstates, which combines the loadstate of different scenarios(when we refresh dataset or when we append new data to it) into this one object
        //We can use it to check for these scenarios and make our views visible or unvisible according to it

        setHasOptionsMenu(true)
    }

    override fun  onItemClick(movie: MoviesResults.Movies) {
        val action = MoviesListFragmentDirections.actionMoviesListFragmentToMoviesDetailsFragment(movie)
        findNavController().navigate(action)
    }

    override fun onFavoriteClick(movie: MoviesResults.Movies) {
       daoViewModel.addMovieToFavs(movie)
    }



    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)

        // Inflate the gallery menu
        inflater.inflate(R.menu.menu_gallery, menu)




    }





    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}




Solution

  • For me, I usually use the builder function liveData {}. It is an builder function that allows you to trigger livedata with coroutine. The difference from regular livedata is that instead of you using setValue() or postValue() to trigger LiveData, with this builder function, you trigger LiveData with the emit() function.

    @HiltViewModel
    class MoviesListViewModel @Inject constructor(private val repository: MoviesRepository): ViewModel() {
    
        val switchMapLiveData = _yourLiveData.switchMap { yourLiveDataValue ->
             liveData {
                  emit(repository.yourFunction(yourLiveDataValue))
             }
        }
    }