В настоящее время изучаю язык Kotlin, я пытаюсь изучить архитектуру MVVM с помощью API TMDB.
В моем приложении я начал с извлечения самых популярных фильмов и разрешения бесконечной прокрутки с помощью разбивки на страницы .
Процесс выглядит следующим образом (вы найдете код после):
- Наблюдение за моей деятельностью в
LiveData<PagedList<Movie>>
ViewModel - Это
LiveData<PageList<Movie>>
создается с использованием DataSourceFactory - Сам
DataSourceFactory
создает экземпляр DataSource
.
Проблема в том, что DataSource создается в начале приложения и loadInit метод вызывается автоматически (см. схему «путь» и код ниже).
============= ==================== Активность ============================= =
class MainActivity : AppCompatActivity() {
//private lateinit var viewModel_mainActivity : MainActivityViewModel
private lateinit var viewModel_mainActivity : MainActivityViewModelSearch
private lateinit var m_recyclerview : RecyclerView
private lateinit var m_progressBar : ProgressBar
private lateinit var m_errorTextView : TextView
private lateinit var m_editText : EditText
private lateinit var m_buttonSearch : Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val apiService : TMDBInterface = TMDBClient.getClient()
viewModel_mainActivity = getViewModel(apiService)
val movieAdapter = MoviePagedListAdapter(this)
[...]
m_buttonSearch = findViewById(R.id.activity_main_buttonSearch)
m_buttonSearch.setOnClickListener {
if(m_editText.text.length > 0){
viewModel_mainActivity.searchMovie(m_editText.text.toString())
}
}
viewModel_mainActivity.moviePagedList.observe(this, Observer {
movieAdapter.submitList(it)
})
viewModel_mainActivity.networkState.observe(this, Observer {
m_progressBar.visibility = if (viewModel_mainActivity.listIsEmpty() && it == NetworkState.LOADING) View.VISIBLE else View.GONE
m_errorTextView.visibility = if (viewModel_mainActivity.listIsEmpty() && it == NetworkState.ERROR) View.VISIBLE else View.GONE
if(!viewModel_mainActivity.listIsEmpty()){
movieAdapter.setNetworkState(it)
}
})
}
private fun getViewModel(apiService:TMDBInterface): MainActivityViewModelSearch {
return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainActivityViewModelSearch(apiService) as T
}
})[MainActivityViewModelSearch::class.java]
}
}
================================ ViewModel ======= ======================
class MainActivityViewModelSearch(private val apiService : TMDBInterface) : ViewModel(){
private val compositeDisposable = CompositeDisposable()
lateinit var movieDataSourceSearchFactory : MovieDataSourceFactorySearch
val moviePagedList : LiveData<PagedList<Movie>> by lazy {
fetchLiveMoviePagedList()
}
val networkState : LiveData<NetworkState> by lazy {
Transformations.switchMap<MovieDataSourceSearch,NetworkState>( //permet d'accéder a networkState inclu dans movieLiveDataSource de la factory
movieDataSourceSearchFactory.movieLiveDataSourceSearch,MovieDataSourceSearch::networkState
)
}
fun listIsEmpty(): Boolean{
return moviePagedList.value?.isEmpty() ?: true //retourne si la liste est vide ou non et vrai si la liste est nulle
}
fun fetchLiveMoviePagedList() : LiveData<PagedList<Movie>>{
movieDataSourceSearchFactory = MovieDataSourceFactorySearch(apiService,compositeDisposable) //on créer la factory
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(POST_PER_PAGE)
.build()
val moviePagedListbuild = LivePagedListBuilder(movieDataSourceSearchFactory,config).build()
return moviePagedListbuild
}
override fun onCleared(){
super.onCleared()
compositeDisposable.dispose()
}
}
=================== ================ DataSourceFactory ================================
class MovieDataSourceFactorySearch(private val apiService : TMDBInterface, private val compositeDisposable : CompositeDisposable)
: DataSource.Factory<Int, Movie>() {
val movieLiveDataSourceSearch = MutableLiveData<MovieDataSourceSearch>()
override fun create(): DataSource<Int, Movie> {
Log.i("ON CREATE FACTORY",this.toString())
val movieDataSourceSearch = MovieDataSourceSearch(apiService,compositeDisposable) //on créer la dataSource pour notre liste de la main activity
movieLiveDataSourceSearch.postValue(movieDataSourceSearch)
return movieDataSourceSearch
}
}
==== ============================ Источник данных ===================== ========
class MovieDataSourceSearch(private val apiService : TMDBInterface, private val compositeDisposable: CompositeDisposable) : PageKeyedDataSource<Int, Movie>(){ //on étend pagekeyedData source car on en a besoin pour gérer la pagination
private var page = FIRST_PAGE
val networkState : MutableLiveData<NetworkState> = MutableLiveData()
//fonction utile pour chargée la toute première page demandée
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Movie>) {
networkState.postValue(NetworkState.LOADING) //on charge la premiere page, on change l'état de notre réseau
Log.i("LOAD INITIAL DATASOURCE",this.toString())
compositeDisposable.add(
apiService.getSearchMovie("Star", page)
.subscribeOn(Schedulers.io()) //notre méthode retourne un Single de movie details, on s'abonne au pool de thread Scheduler.io pour surveiller l'appel réseaux
.subscribe( //le subscribe permet de s'abonner au résultat qui à deux paramètre, un pour le succès et un pour l'échec
{ // l'appel est un succès, on a récupérer un film
callback.onResult(it.movieList,null,page+1) //on passe à la callback le résultat de la requete
networkState.postValue(NetworkState.LOADED)
},
{ //l'appel est un échec
networkState.postValue(NetworkState.ERROR)
Log.e("MovieDataSource",it.message)
}
)
)
}
//fonction utile pour charger lors d'un scroll vers le bas de l'utilisateur
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
networkState.postValue(NetworkState.LOADING) //on charge une nouvelle page, on change l'état de notre réseau
Log.i("LOAD AFTER DATASOURCE",this.toString())
compositeDisposable.add(
apiService.getSearchMovie("Star",params.key)
.subscribeOn(Schedulers.io()) //notre méthode retourne un Single de movie details, on s'abonne au pool de thread Scheduler.io pour surveiller l'appel réseaux
.subscribe( //le subscribe permet de s'abonner au résultat qui à deux paramètre, un pour le succès et un pour l'échec
{ // l'appel est un succès, on a récupérer un film
if(it.totalPages >= params.key){ //si on est pas encore à la fin
callback.onResult(it.movieList,params.key+1) //on renvoi la nouvelle page qui vien d'être chargée
networkState.postValue(NetworkState.LOADED) //on indique le chargement est effectué
}
else{ //si on est à la fin
networkState.postValue(NetworkState.ENDOFLIST) //on change notre networkstate pour indiqué que c'est la fin
}
},
{ //l'appel est un échec
networkState.postValue(NetworkState.ERROR)
Log.e("MovieDataSource",it.message)
}
)
)
}
}
Теперь я хотел бы разрешить пользователю искать фильмы по тексту.
Мой идеальный сценарий был бы следующим:
Пользователь запускает приложение и видит EditText и кнопку (представление корзины пусто, поэтому PagedList тоже пуст).
Он входит текст и щелчок по кнопке, PagedList полностью очищается, поэтому, если предыдущий mov ie был показан, они исчезли.
После очистки мы вызываем DataSource для вызова запроса и получить список фильмов.
Мы обновляем адаптер новым PagedList.
В настоящее время, если я изменяю loadInit и loadAfter с помощью "searchMov *" 1074 * ", он работает, но текст для поиска вводится непосредственно в DataSource и не может быть изменен, так что это похоже на известные фильмы ... th Пользователь искал фильмы со звездочкой в названии, но не может это изменить ...
Мой вопрос:
Как получить контроль над методами loadInit, loadAfter, чтобы они вызывались только после поиска или прокрутки вниз и, прежде всего, с учетом текста пользователя