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