From db4270b38a8368f59330ad0d0c543917177564bd Mon Sep 17 00:00:00 2001 From: Kostia Date: Sun, 21 Jun 2020 17:19:56 +0300 Subject: [PATCH 1/4] custom rx edit text --- .../movieapp/dagger/module/NetworkModule.kt | 3 +- .../movieapp/ui/search/SearchFragment.kt | 51 +++++++++---------- .../movieapp/ui/search/SearchViewModel.kt | 26 +++++----- .../com/example/movieapp/utils/Extensions.kt | 39 ++++++++++++++ .../movieapp/utils/adapters/SearchAdapter.kt | 1 + google-services.json | 40 +++++++++++++++ 6 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 google-services.json diff --git a/app/src/main/java/com/example/movieapp/dagger/module/NetworkModule.kt b/app/src/main/java/com/example/movieapp/dagger/module/NetworkModule.kt index 5d93202..86cd839 100644 --- a/app/src/main/java/com/example/movieapp/dagger/module/NetworkModule.kt +++ b/app/src/main/java/com/example/movieapp/dagger/module/NetworkModule.kt @@ -44,7 +44,8 @@ class NetworkModule { .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .addInterceptor(interceptor) - .build() } + .build() + } @Provides @Reusable diff --git a/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt b/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt index dd0ddb0..1f56b41 100644 --- a/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt @@ -5,23 +5,19 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import com.example.movieapp.dagger.App import com.example.movieapp.dagger.module.viewModule.ViewModelFactory import com.example.movieapp.databinding.SearchFragmentBinding import com.example.movieapp.model.network.data.search.SearchItem import com.example.movieapp.utils.adapters.SearchAdapter import com.example.movieapp.utils.injectViewModel -import com.jakewharton.rxbinding.widget.RxTextView +import com.example.movieapp.utils.toFlowable +import com.example.movieapp.utils.withParams import dagger.android.support.DaggerFragment +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.android.synthetic.main.search_fragment.* -import rx.Single -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import java.util.concurrent.TimeUnit import javax.inject.Inject class SearchFragment : DaggerFragment() { @@ -31,13 +27,13 @@ class SearchFragment : DaggerFragment() { lateinit var viewModel: SearchViewModel lateinit var binding: SearchFragmentBinding lateinit var adapter: SearchAdapter + private val disposables = CompositeDisposable() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = injectViewModel(viewModelFactory) binding = SearchFragmentBinding.inflate(inflater) adapter = SearchAdapter(SearchAdapter.ClickListener { @@ -57,34 +53,28 @@ class SearchFragment : DaggerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - RxTextView.textChanges(binding.edit) - .subscribeOn(AndroidSchedulers.mainThread()) - .filter { - it.length > 2 - } - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { showProgress() } - .observeOn(Schedulers.io()) - .switchMap { - apiRequest(it).toObservable() - } + + binding.edit.toFlowable() + .withParams( + minLength = 3, + debounce = 300, + switchMapper = ::apiRequest, + doOnNext = ::showProgress + ) .map { resp -> - resp - .filterNot { it.name == null || it.name == "null" } + resp.filterNot { it.name.isNullOrEmpty() || it.name == "null" } } - .observeOn(AndroidSchedulers.mainThread()) .subscribe({ - Log.d("response", "result: $it") hideProgress() adapter.submitList(it) }, { Log.d("response", "error: $it") - }) + } + ).let(disposables::add) } - private fun apiRequest(chars: CharSequence): Single> { - return viewModel.getSearchResult(chars.toString()) + private fun apiRequest(chars: CharSequence): Flowable> { + return viewModel.getSearchResult(chars.toString()).toFlowable() } private fun showProgress() { @@ -96,4 +86,9 @@ class SearchFragment : DaggerFragment() { searchProgressBar.visibility = View.INVISIBLE search_rv.visibility = View.VISIBLE } + + override fun onDestroy() { + super.onDestroy() + disposables.clear() + } } diff --git a/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt b/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt index 41025c1..a41db21 100644 --- a/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt @@ -2,11 +2,9 @@ package com.example.movieapp.ui.search import androidx.lifecycle.ViewModel import com.example.movieapp.model.network.SearchApi -import com.example.movieapp.model.network.data.search.Genre import com.example.movieapp.model.network.data.search.SearchItem import com.example.movieapp.model.network.data.search.SearchResponse -import io.reactivex.rxjava3.schedulers.Schedulers -import rx.Single +import io.reactivex.rxjava3.core.Single import javax.inject.Inject class SearchViewModel @Inject constructor(private val api: SearchApi) : ViewModel() { @@ -15,11 +13,13 @@ class SearchViewModel @Inject constructor(private val api: SearchApi) : ViewMode // get() = api.getGenres().map { it.genres }.cache().subscribeOn(Schedulers.io()) fun getSearchResult(query: String): Single> { - return Single.create> { s -> + return api.getListOfPosters(query).map(SearchResponse::results) + } + - api.getListOfPosters(query) - .map(SearchResponse::results) + // .map(SearchResponse::results) +/* // .flattenAsFlowable { it.results } // .flatMap { r -> // Flowable.just(r.genreIds) @@ -33,11 +33,11 @@ class SearchViewModel @Inject constructor(private val api: SearchApi) : ViewMode // } // } // .toList() - .subscribe({ - s.onSuccess(it) - }, { - s.onError(it) - }) - } - } + */ + +// .subscribe({ +// s.onSuccess(it) +// }, { +// s.onError(it) +// }) } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/utils/Extensions.kt b/app/src/main/java/com/example/movieapp/utils/Extensions.kt index 39f2ceb..ff82352 100644 --- a/app/src/main/java/com/example/movieapp/utils/Extensions.kt +++ b/app/src/main/java/com/example/movieapp/utils/Extensions.kt @@ -1,9 +1,48 @@ package com.example.movieapp.utils +import android.widget.EditText +import androidx.core.widget.doAfterTextChanged +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.subjects.PublishSubject +import java.util.concurrent.TimeUnit inline fun Fragment.injectViewModel(factory: ViewModelProvider.Factory): T { return ViewModelProvider(this, factory)[T::class.java] } + +fun EditText.toFlowable(): Flowable { + return Flowable.fromPublisher { subscriber -> + doAfterTextChanged { + subscriber.onNext(it) + } + } +} + +fun Flowable.withParams( + minLength: Int = 2, + debounce: Long = 300, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS, + switchMapper: ((CharSequence) -> Flowable)? = null, + doOnNext: (() -> Unit)? = null +): Flowable { + return subscribeOn(AndroidSchedulers.mainThread()) + .filter { + it.length >= minLength + } + .debounce(debounce, timeUnit) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { doOnNext?.invoke() } + .observeOn(Schedulers.io()) + .switchMap { + switchMapper?.let { mapper -> return@switchMap mapper(it) } + return@switchMap Flowable.just(it) as Flowable + } + .observeOn(AndroidSchedulers.mainThread()) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt b/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt index 9c20351..714ecc8 100644 --- a/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt @@ -22,6 +22,7 @@ class SearchAdapter(private val onClickListener: ClickListener) : } } + class ClickListener(val clickListener: (movie: SearchItem) -> Unit) { fun onClick(movie: SearchItem) = clickListener(movie) } diff --git a/google-services.json b/google-services.json new file mode 100644 index 0000000..335e661 --- /dev/null +++ b/google-services.json @@ -0,0 +1,40 @@ +{ + "project_info": { + "project_number": "328407397490", + "firebase_url": "https://movieapp-f1849.firebaseio.com", + "project_id": "movieapp-f1849", + "storage_bucket": "movieapp-f1849.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:328407397490:android:14c46be1a39fa997b03eeb", + "android_client_info": { + "package_name": "com.example.movieapp" + } + }, + "oauth_client": [ + { + "client_id": "328407397490-818v96c0okmeeg0ed5g7e63ba8i84evh.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyA3J_r4Bjo5qoJDDTw8NMRNK2PatmnvDKU" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "328407397490-818v96c0okmeeg0ed5g7e63ba8i84evh.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file From e85a9af0a68549d6354e363d9a114fed682041a2 Mon Sep 17 00:00:00 2001 From: Kostia Date: Tue, 23 Jun 2020 14:30:57 +0300 Subject: [PATCH 2/4] genres --- .../movieapp/model/network/MovieApi.kt | 7 +++ .../movieapp/model/network/NetworkSource.kt | 17 ++++-- .../model/network/data/movie/SmallMovie.kt | 6 ++- .../ui/home/overview/OverviewViewModel.kt | 29 ++++++++-- .../movieapp/ui/search/SearchFragment.kt | 4 +- .../movieapp/ui/search/SearchViewModel.kt | 54 ++++++++++--------- .../movieapp/utils/adapters/ChildAdapter.kt | 8 +++ .../movieapp/utils/adapters/SearchAdapter.kt | 15 +++++- app/src/main/res/layout/item.xml | 8 +++ app/src/main/res/layout/search_item.xml | 27 ++++++---- 10 files changed, 127 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/example/movieapp/model/network/MovieApi.kt b/app/src/main/java/com/example/movieapp/model/network/MovieApi.kt index ae4dcf0..56b8632 100644 --- a/app/src/main/java/com/example/movieapp/model/network/MovieApi.kt +++ b/app/src/main/java/com/example/movieapp/model/network/MovieApi.kt @@ -3,6 +3,7 @@ package com.example.movieapp.model.network import com.example.movieapp.model.network.data.movie.MovieInfo import com.example.movieapp.model.network.data.movie.Results import com.example.movieapp.model.network.data.movie.SmallMovie +import com.example.movieapp.model.network.data.search.GenreResponse import com.example.movieapp.utils.API_KEY import io.reactivex.rxjava3.core.Single import retrofit2.http.GET @@ -31,4 +32,10 @@ interface MovieApi { @GET("movie/{id}?api_key=$API_KEY&language=ru") fun getMovieByID(@Path("id") id: Int): Single + + @GET("genre/movie/list") + fun getGenres( + @Query("api_key") key: String = API_KEY, + @Query("language") language: String = "ru" + ): Single } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt index 331486d..d5388d8 100644 --- a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt +++ b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt @@ -3,6 +3,8 @@ package com.example.movieapp.model.network import com.example.movieapp.model.network.data.movie.ListMovie import com.example.movieapp.model.network.data.movie.MovieInfo import com.example.movieapp.model.network.data.movie.SmallMovieList +import com.example.movieapp.model.network.data.search.Genre +import com.example.movieapp.model.network.data.search.GenreResponse import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single @@ -18,7 +20,9 @@ class MovieListSource @Inject constructor(private val api: MovieApi) { } } -class SmallMovieListSource @Inject constructor(private val api: MovieApi) { +class SmallMovieListSource @Inject constructor( + private val api: MovieApi +) { fun fetchSmallMovieList( categories: List, key: String, @@ -35,6 +39,12 @@ class SmallMovieListSource @Inject constructor(private val api: MovieApi) { .observeOn(AndroidSchedulers.mainThread()) } + fun fetchGenres(language: String): Single> { + return api.getGenres(language = language) + .map(GenreResponse::genres) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } } class MovieDetailSource @Inject constructor(private val api: MovieApi) { @@ -43,7 +53,4 @@ class MovieDetailSource @Inject constructor(private val api: MovieApi) { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } -} - - - +} \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/model/network/data/movie/SmallMovie.kt b/app/src/main/java/com/example/movieapp/model/network/data/movie/SmallMovie.kt index 31f65eb..662e11e 100644 --- a/app/src/main/java/com/example/movieapp/model/network/data/movie/SmallMovie.kt +++ b/app/src/main/java/com/example/movieapp/model/network/data/movie/SmallMovie.kt @@ -18,7 +18,9 @@ data class SmallMovieList constructor( @SerializedName("poster_path") val posterPath: String, @SerializedName("title") val title: String, @SerializedName("vote_average")val voteAverage: Float, - @SerializedName("backdrop_path") val backdropPath: String + @SerializedName("backdrop_path") val backdropPath: String, + @SerializedName("genre_ids") var genreIds: List = emptyList(), + var genres: List = emptyList() ):Parcelable{ - constructor(): this(0,"","", 0.0F, "") + constructor(): this(0,"","", 0.0F, "", emptyList(), emptyList()) } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt index 1a299dc..820ca90 100644 --- a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt +++ b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt @@ -8,6 +8,9 @@ import androidx.lifecycle.ViewModel import com.example.movieapp.model.network.SmallMovieListSource import com.example.movieapp.model.network.data.movie.ParentListMovie import com.example.movieapp.model.network.data.movie.SmallMovieList +import com.example.movieapp.model.network.data.search.Genre +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import javax.inject.Inject @@ -41,11 +44,13 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi val errorClickListener = View.OnClickListener { fetchMoviesLists() } + private lateinit var allGenres: List + init { println("initialization viewModel") fetchMoviesLists() } - + private fun fetchMoviesLists() { Log.d("ViewModel", "load data") @@ -57,13 +62,13 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi 4 to ("Рекомендации" to "upcoming") ) - val dis = networkSource.fetchSmallMovieList(categoryList, "26f381d6ab8dd659b22d983cab9aa255", "ru") + val dis = fetchGenresFlowable().concatWith(networkSource.fetchSmallMovieList(categoryList, "26f381d6ab8dd659b22d983cab9aa255", "ru")) .subscribe({ collectionList -> val mListMovie = collectionList.mapIndexed { index, list -> ParentListMovie( titleCategoryMap[index]?.first ?: "", titleCategoryMap[index]?.second ?: "", - list + list.withGenres(allGenres) ) } _eventNetworkError.value = false @@ -78,6 +83,13 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi disposableBack.add(dis) } + fun fetchGenresFlowable(): Flowable>> { + return networkSource.fetchGenres("ru") + .doOnSuccess { allGenres = it } + .toFlowable() + .flatMap { Flowable.empty>>() } + } + fun displayPropertyDetails(movie: SmallMovieList) { _navigateToSelectProperty.value = movie } @@ -94,4 +106,15 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi super.onCleared() disposableBack.dispose() } +} + + +fun List.withGenres(allGenres: List): List { + return map { item -> + item.apply { + genres = genreIds.map { id -> + allGenres.find { genre -> genre.id == id }?.name ?: "" + }.filterNot(String::isNullOrEmpty) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt b/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt index 1f56b41..8926bfa 100644 --- a/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/example/movieapp/ui/search/SearchFragment.kt @@ -56,8 +56,8 @@ class SearchFragment : DaggerFragment() { binding.edit.toFlowable() .withParams( - minLength = 3, - debounce = 300, + minLength = 2, + debounce = 500, switchMapper = ::apiRequest, doOnNext = ::showProgress ) diff --git a/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt b/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt index a41db21..62e5879 100644 --- a/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/example/movieapp/ui/search/SearchViewModel.kt @@ -2,9 +2,14 @@ package com.example.movieapp.ui.search import androidx.lifecycle.ViewModel import com.example.movieapp.model.network.SearchApi +import com.example.movieapp.model.network.data.movie.SmallMovieList +import com.example.movieapp.model.network.data.search.Genre import com.example.movieapp.model.network.data.search.SearchItem import com.example.movieapp.model.network.data.search.SearchResponse +import com.google.gson.annotations.SerializedName +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class SearchViewModel @Inject constructor(private val api: SearchApi) : ViewModel() { @@ -12,32 +17,31 @@ class SearchViewModel @Inject constructor(private val api: SearchApi) : ViewMode // val genres: io.reactivex.rxjava3.core.Single> // get() = api.getGenres().map { it.genres }.cache().subscribeOn(Schedulers.io()) - fun getSearchResult(query: String): Single> { - return api.getListOfPosters(query).map(SearchResponse::results) + private lateinit var allGenres: List + + init { + api.getGenres() + .map { it.genres } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + allGenres = it + }, { + allGenres = emptyList() + }) } - - - // .map(SearchResponse::results) -/* -// .flattenAsFlowable { it.results } -// .flatMap { r -> -// Flowable.just(r.genreIds) -// .flatMap { gId -> -// genres.toFlowable().zipWith( -// Flowable.just(r), -// BiFunction, SearchItem, SearchItem> { t1, t2 -> -// t2.apply { genres = t1.filter { u -> gId.contains(u.id) }.map(Genre::name) } -// } -// ) -// } + fun getSearchResult(query: String): Single> { + return api.getListOfPosters(query) + .map(SearchResponse::results) +// .map { items -> +// items.map { item -> +// item.apply { +// genres = genreIds.map { id -> +// allGenres.find { genre -> genre.id == id }?.name ?: "" +// }.filterNot(String::isNullOrEmpty) +// } // } -// .toList() - */ - -// .subscribe({ -// s.onSuccess(it) -// }, { -// s.onError(it) -// }) +// } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt index a6aad66..614e587 100644 --- a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt +++ b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt @@ -11,6 +11,7 @@ import com.example.movieapp.databinding.ItemBinding import com.example.movieapp.databinding.ItemCustomBinding import com.example.movieapp.model.network.data.movie.SmallMovieList import com.example.movieapp.ui.home.overview.OverviewFragmentDirections +import com.google.android.material.chip.Chip import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -88,6 +89,13 @@ class ChildAdapter(private val clickListener: MovieListener) : fun bind(item: SmallMovieList) { binding.movie = item + item.genres.forEach { genre -> + val chip = Chip(binding.root.context) + chip.text = genre + chip.width = 1 + chip.textSize = 10f + binding.genresChipGroup.addView(chip) + } binding.executePendingBindings() } diff --git a/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt b/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt index 714ecc8..8d28f8b 100644 --- a/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/example/movieapp/utils/adapters/SearchAdapter.kt @@ -7,6 +7,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.example.movieapp.databinding.SearchItemBinding import com.example.movieapp.model.network.data.search.SearchItem +import com.google.android.material.chip.Chip class SearchAdapter(private val onClickListener: ClickListener) : ListAdapter(SearchDiffUtil()) { @@ -28,13 +29,23 @@ class SearchAdapter(private val onClickListener: ClickListener) : } } -class SearchViewHolder(private val binding: SearchItemBinding): RecyclerView.ViewHolder(binding.root) { +class SearchViewHolder(private val binding: SearchItemBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(searchItem: SearchItem) { binding.movie = searchItem + +// searchItem.genres.forEach { genre -> +// val chip = Chip(binding.root.context).apply { +// text = genre +// } +// binding.genresChipGroup.addView(chip) +// } +// binding.genres.text = searchItem.genres.joinToString(separator = " ") { it } } } -class SearchDiffUtil: DiffUtil.ItemCallback() { +class SearchDiffUtil : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchItem, newItem: SearchItem): Boolean { return oldItem.id == newItem.id } diff --git a/app/src/main/res/layout/item.xml b/app/src/main/res/layout/item.xml index 2377129..551920f 100644 --- a/app/src/main/res/layout/item.xml +++ b/app/src/main/res/layout/item.xml @@ -73,6 +73,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/name" /> + + diff --git a/app/src/main/res/layout/search_item.xml b/app/src/main/res/layout/search_item.xml index 50c3713..81fdaea 100644 --- a/app/src/main/res/layout/search_item.xml +++ b/app/src/main/res/layout/search_item.xml @@ -55,16 +55,25 @@ app:layout_constraintStart_toEndOf="@+id/cardView" app:layout_constraintTop_toTopOf="@+id/cardView" /> - + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/name" + + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + app:layout_constraintTop_toBottomOf="@+id/genresChipGroup" /> From a62c6e49e41960c6040fab686f94d1f628876c85 Mon Sep 17 00:00:00 2001 From: Kostia Date: Thu, 25 Jun 2020 15:00:30 +0300 Subject: [PATCH 3/4] combining genres and movies streams, refactoring --- .../movieapp/model/network/NetworkSource.kt | 3 +- .../ui/home/overview/OverviewViewModel.kt | 53 +++++++++---------- .../movieapp/utils/adapters/ChildAdapter.kt | 14 ++--- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt index d5388d8..4d22a38 100644 --- a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt +++ b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt @@ -25,12 +25,11 @@ class SmallMovieListSource @Inject constructor( ) { fun fetchSmallMovieList( categories: List, - key: String, language: String ): Single>> { return Flowable.fromIterable(categories) .concatMap { category -> - api.getListOfPosters(category, key, language) + api.getListOfPosters(category = category, language = language) .toFlowable() .subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt index 820ca90..f85dbb7 100644 --- a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt +++ b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt @@ -12,6 +12,7 @@ import com.example.movieapp.model.network.data.search.Genre import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.functions.BiFunction import javax.inject.Inject class OverviewViewModel @Inject constructor(private val networkSource: SmallMovieListSource) : @@ -44,36 +45,41 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi val errorClickListener = View.OnClickListener { fetchMoviesLists() } - private lateinit var allGenres: List - init { println("initialization viewModel") fetchMoviesLists() } - + + private val genresSource: Single> + get() = networkSource.fetchGenres("ru") + + private val moviesSource: Single>> + get() = networkSource.fetchSmallMovieList(categoryList, "ru") + + private val titleCategoryMap = hashMapOf( + 1 to ("Сейчас в кино" to "now_playing"), + 2 to ("Топ рейтинг" to "top_rated"), + 3 to ("Популярное" to "popular"), + 4 to ("Рекомендации" to "upcoming") + ) private fun fetchMoviesLists() { Log.d("ViewModel", "load data") - val titleCategoryMap = hashMapOf( - 1 to ("Сейчас в кино" to "now_playing"), - 2 to ("Топ рейтинг" to "top_rated"), - 3 to ("Популярное" to "popular"), - 4 to ("Рекомендации" to "upcoming") - ) - - val dis = fetchGenresFlowable().concatWith(networkSource.fetchSmallMovieList(categoryList, "26f381d6ab8dd659b22d983cab9aa255", "ru")) - .subscribe({ collectionList -> - val mListMovie = collectionList.mapIndexed { index, list -> - ParentListMovie( - titleCategoryMap[index]?.first ?: "", - titleCategoryMap[index]?.second ?: "", - list.withGenres(allGenres) - ) - } + val dis = + moviesSource.zipWith(genresSource, + BiFunction { movies: List>, genres: List -> + movies.mapIndexed { index, list -> + ParentListMovie( + titleCategoryMap[index]?.first ?: "", + titleCategoryMap[index]?.second ?: "", + list.withGenres(genres) + ) + } + }).subscribe({ parentMoviesList -> _eventNetworkError.value = false _isNetworkErrorShown.value = false - _parentListMovie.value = mListMovie + _parentListMovie.value = parentMoviesList }, { if (parentListMovie.value.isNullOrEmpty()) { _eventNetworkError.value = true @@ -83,13 +89,6 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi disposableBack.add(dis) } - fun fetchGenresFlowable(): Flowable>> { - return networkSource.fetchGenres("ru") - .doOnSuccess { allGenres = it } - .toFlowable() - .flatMap { Flowable.empty>>() } - } - fun displayPropertyDetails(movie: SmallMovieList) { _navigateToSelectProperty.value = movie } diff --git a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt index 614e587..a0bab3a 100644 --- a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt +++ b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt @@ -89,12 +89,14 @@ class ChildAdapter(private val clickListener: MovieListener) : fun bind(item: SmallMovieList) { binding.movie = item - item.genres.forEach { genre -> - val chip = Chip(binding.root.context) - chip.text = genre - chip.width = 1 - chip.textSize = 10f - binding.genresChipGroup.addView(chip) + if (binding.genresChipGroup.childCount == 0) { + item.genres.first().let { genre -> + val chip = Chip(binding.root.context) + chip.text = genre + chip.width = 1 + chip.textSize = 10f + binding.genresChipGroup.addView(chip) + } } binding.executePendingBindings() } From e651fa3ceb9cb2ae804accfd8b07da09844f332e Mon Sep 17 00:00:00 2001 From: Kostia Date: Thu, 25 Jun 2020 15:26:48 +0300 Subject: [PATCH 4/4] moving out overview logic into Source class --- .../movieapp/model/network/NetworkSource.kt | 50 +++++++++++++++++-- .../ui/home/overview/OverviewViewModel.kt | 49 +++--------------- .../movieapp/utils/adapters/ChildAdapter.kt | 2 - 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt index 4d22a38..62934af 100644 --- a/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt +++ b/app/src/main/java/com/example/movieapp/model/network/NetworkSource.kt @@ -2,17 +2,24 @@ package com.example.movieapp.model.network import com.example.movieapp.model.network.data.movie.ListMovie import com.example.movieapp.model.network.data.movie.MovieInfo +import com.example.movieapp.model.network.data.movie.ParentListMovie import com.example.movieapp.model.network.data.movie.SmallMovieList import com.example.movieapp.model.network.data.search.Genre import com.example.movieapp.model.network.data.search.GenreResponse import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.functions.BiFunction import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class MovieListSource @Inject constructor(private val api: MovieApi) { - fun fetchMovieList(category: String, key: String, language: String, page: Int): Single> { + fun fetchMovieList( + category: String, + key: String, + language: String, + page: Int + ): Single> { return api.getPropertyAsync(category, key, language, page) .map { it.networkMovie } .subscribeOn(Schedulers.io()) @@ -23,9 +30,36 @@ class MovieListSource @Inject constructor(private val api: MovieApi) { class SmallMovieListSource @Inject constructor( private val api: MovieApi ) { + + private val titleCategoryMap = hashMapOf( + 1 to ("Сейчас в кино" to "now_playing"), + 2 to ("Топ рейтинг" to "top_rated"), + 3 to ("Популярное" to "popular"), + 4 to ("Рекомендации" to "upcoming") + ) + fun fetchSmallMovieList( categories: List, language: String + ): Single> { + val moviesSource = fetchMovies(categories, language) + val genresSource = fetchGenres(language) + + return moviesSource.zipWith(genresSource, + BiFunction { movies: List>, genres: List -> + movies.mapIndexed { index, list -> + ParentListMovie( + titleCategoryMap[index]?.first ?: "", + titleCategoryMap[index]?.second ?: "", + list.withGenres(genres) + ) + } + }).observeOn(AndroidSchedulers.mainThread()) + } + + private fun fetchMovies( + categories: List, + language: String ): Single>> { return Flowable.fromIterable(categories) .concatMap { category -> @@ -35,14 +69,12 @@ class SmallMovieListSource @Inject constructor( } .map { it.smallMovieList } .toList() - .observeOn(AndroidSchedulers.mainThread()) } - fun fetchGenres(language: String): Single> { + private fun fetchGenres(language: String): Single> { return api.getGenres(language = language) .map(GenreResponse::genres) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) } } @@ -52,4 +84,14 @@ class MovieDetailSource @Inject constructor(private val api: MovieApi) { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } +} + +fun List.withGenres(allGenres: List): List { + return map { item -> + item.apply { + genres = genreIds.map { id -> + allGenres.find { genre -> genre.id == id }?.name ?: "" + }.filterNot(String::isNullOrEmpty) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt index f85dbb7..c105369 100644 --- a/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt +++ b/app/src/main/java/com/example/movieapp/ui/home/overview/OverviewViewModel.kt @@ -8,15 +8,12 @@ import androidx.lifecycle.ViewModel import com.example.movieapp.model.network.SmallMovieListSource import com.example.movieapp.model.network.data.movie.ParentListMovie import com.example.movieapp.model.network.data.movie.SmallMovieList -import com.example.movieapp.model.network.data.search.Genre -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.functions.BiFunction import javax.inject.Inject -class OverviewViewModel @Inject constructor(private val networkSource: SmallMovieListSource) : - ViewModel() { +class OverviewViewModel @Inject constructor( + private val networkSource: SmallMovieListSource +) : ViewModel() { private var disposableBack = CompositeDisposable() private val categoryList = listOf("upcoming", "top_rated", "popular", "now_playing") @@ -50,33 +47,12 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi fetchMoviesLists() } - private val genresSource: Single> - get() = networkSource.fetchGenres("ru") - - private val moviesSource: Single>> - get() = networkSource.fetchSmallMovieList(categoryList, "ru") - - private val titleCategoryMap = hashMapOf( - 1 to ("Сейчас в кино" to "now_playing"), - 2 to ("Топ рейтинг" to "top_rated"), - 3 to ("Популярное" to "popular"), - 4 to ("Рекомендации" to "upcoming") - ) private fun fetchMoviesLists() { Log.d("ViewModel", "load data") - val dis = - moviesSource.zipWith(genresSource, - BiFunction { movies: List>, genres: List -> - movies.mapIndexed { index, list -> - ParentListMovie( - titleCategoryMap[index]?.first ?: "", - titleCategoryMap[index]?.second ?: "", - list.withGenres(genres) - ) - } - }).subscribe({ parentMoviesList -> + networkSource.fetchSmallMovieList(categoryList, "ru") + .subscribe({ parentMoviesList -> _eventNetworkError.value = false _isNetworkErrorShown.value = false _parentListMovie.value = parentMoviesList @@ -84,9 +60,7 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi if (parentListMovie.value.isNullOrEmpty()) { _eventNetworkError.value = true } - } - ) - disposableBack.add(dis) + }).let(disposableBack::add) } fun displayPropertyDetails(movie: SmallMovieList) { @@ -105,15 +79,4 @@ class OverviewViewModel @Inject constructor(private val networkSource: SmallMovi super.onCleared() disposableBack.dispose() } -} - - -fun List.withGenres(allGenres: List): List { - return map { item -> - item.apply { - genres = genreIds.map { id -> - allGenres.find { genre -> genre.id == id }?.name ?: "" - }.filterNot(String::isNullOrEmpty) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt index a0bab3a..5d9bdff 100644 --- a/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt +++ b/app/src/main/java/com/example/movieapp/utils/adapters/ChildAdapter.kt @@ -93,8 +93,6 @@ class ChildAdapter(private val clickListener: MovieListener) : item.genres.first().let { genre -> val chip = Chip(binding.root.context) chip.text = genre - chip.width = 1 - chip.textSize = 10f binding.genresChipGroup.addView(chip) } }