2

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
    }

}



Ahmadddd4
  • 77
  • 7

2 Answers2

0

As mentioned here you should use liveData builder function when using Coroutine with LiveData. Something like this

val movies = currentQuery.switchMap { queryString ->
    liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
        val result = repository.getSearchResults(queryString)
        emit(result)
    }
}
Amin Mousavi
  • 1,220
  • 10
  • 21
0

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))
         }
    }
}
Dương Minh
  • 2,062
  • 1
  • 9
  • 21
  • Thank you! By any chance do you have the link to the documentation that explains setvalue and postvalue more? – Ahmadddd4 Aug 21 '21 at 12:54
  • @Ahmadddd4 You can refer to some links of [StackOverFlow](https://stackoverflow.com/questions/51299641/difference-of-setvalue-postvalue-in-mutablelivedata) or [the doc provided by Google](https://developer.android.com/reference/androidx/lifecycle/MutableLiveData?authuser=3#public-methods_1). – Dương Minh Aug 21 '21 at 13:02
  • And like @Amin Mousavi said, you can learn more about this LiveData builder function through this [documentation](https://developer.android.com/topic/libraries/architecture/coroutines#livedata). – Dương Minh Aug 21 '21 at 13:05
  • Thank you! One more question, the app is crashing and I'm getting this in the logcat " expected begin_array but was begin_object at line 1 column 2 path $ at com.google.gson.stream.JsonReader.beginArray" I think it has to do with the list of dataclass no? – Ahmadddd4 Aug 22 '21 at 07:27
  • @Ahmadddd4 This is the error of parsing data from Json Api to your Data class. I think you need to provide more about your data class with what json your api returns. – Dương Minh Aug 22 '21 at 07:32
  • I didn't quite understand that. I'm using the moviedb api. https://developers.themoviedb.org/3/trending/get-trending – Ahmadddd4 Aug 22 '21 at 07:47
  • @Ahmadddd4 Please push that code into your Api response and Model class (data class) into your ask section. Thanks for that. – Dương Minh Aug 22 '21 at 07:51
  • I couldn't upload it without it getting messy so I took a screenshot of the response https://ibb.co/VpHj5kV – Ahmadddd4 Aug 22 '21 at 08:07
  • I mean you should push your `MoviesResults.Movies` code. Sorry for that. – Dương Minh Aug 22 '21 at 08:09
  • So I know where you went wrong. Because of your api where `result` is returning `list`. But in model `MoviesResults` you return `result` as object and not as `list`. So let's change it to `val results: List`. And in `MoviesApi` you should return as `MoviesResults`. In `Repository`, where `getTrendingMovies()` you return `moviesApi.getTrendingMovies(API_KEY, MEDIA_TYPE, TIME_WINDOW).result` – Dương Minh Aug 22 '21 at 08:17
  • It worked! But for some reason the movies are not showing up. I added the adapter and one fragment. I used to have a paging adapter but then I switched to a listadapter and I'm going through everything again – Ahmadddd4 Aug 22 '21 at 08:32
  • I see in `MoviesListFragment` you already call `submitList`. but one thing is it seems you haven't init `_binding` so `_binding` returns null and you can't set adapter for your recycler view. Make sure you init viewbinding or databinding in your Fragment. – Dương Minh Aug 22 '21 at 08:38
  • I did, in onviewcreated _binding = FragmentMoviesListBinding.bind(view) – Ahmadddd4 Aug 22 '21 at 08:42
  • You are init viewbinding wrong. Please check this link again. [Use view binding in fragments](https://developer.android.com/topic/libraries/view-binding#fragments) – Dương Minh Aug 22 '21 at 08:46
  • I don't think that's the issue because before I switched the adapter viewbinding was working fine – Ahmadddd4 Aug 22 '21 at 08:57
  • Could it be because of the functions in the viewmodel? Because in the fragments I just called them without adding anything else. For example in the fragment I put i just wrote "viewmodel.getTrending()" then I observed the livedata – Ahmadddd4 Aug 22 '21 at 08:59
  • I see another part is in your adapter you can define a variable `movies` with `empty()`. I think you don't need this. To get the item list in `ListAdapter`, you just need to call `getItem(position)`. Change `holder.bind(currentItem)` to `holder.bind(getItem(position))` – Dương Minh Aug 22 '21 at 09:00
  • It worked! Thanks a lot man. One more question, is there anyway I could handle loading states without paging library? I have a progress bar and a retry button I wanna show when the app is loading and when loading has failed – Ahmadddd4 Aug 22 '21 at 10:41
  • I think you should create another question on SO. And if your current question on this SO is solved, please accept my answer. Welcome to SO! – Dương Minh Aug 22 '21 at 10:48