在 switchmap 中启动协程

Launching a coroutine within a switchmap

所以我正在编写一个显示电影列表的应用程序。我想实现一个搜索功能,其中显示搜索结果的函数将从 api.

调用

但是,我在协同程序中实现 switchmap 时遇到了问题。我在 return 类型方面遇到了特别的麻烦,因为 viewmodelscope return 是我需要实时数据的工作。下面是相关代码。

谢谢!

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
    }

}



如前所述,here 在将 CoroutineLiveData 一起使用时,您应该使用 liveData 生成器函数。像这样

val movies = currentQuery.switchMap { queryString ->
    liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
        val result = repository.getSearchResults(queryString)
        emit(result)
    }
}

对于我来说,我通常使用builder函数liveData {}。 它是一个构建器函数,允许您使用协程触发实时数据。与常规 livedata 的不同之处在于,使用此构建器函数,您可以使用 emit() 函数触发 LiveData,而不是使用 setValue()postValue() 来触发 LiveData。

@HiltViewModel
class MoviesListViewModel @Inject constructor(private val repository: MoviesRepository): ViewModel() {

    val switchMapLiveData = _yourLiveData.switchMap { yourLiveDataValue ->
         liveData {
              emit(repository.yourFunction(yourLiveDataValue))
         }
    }
}