I am building a movie app based on "The Movie DB". My app has only one activity with a recyclerview and one search action button (SearchView).
As soon as the app starts,the recyclerview filled with popularMovies list.
When I search for a movie I get the results and everything is fine,but when I rotate the screen I get the popularMovies list and not my search results.
Now I know the reason,its happening because in onCreate I have initMoviesRecyclerView() that fill the recyclerview with popularMovies.
My question is how can I keep the search results without filling the recyclerview with popularMovies ? How can I do this the correct way ?
This is the Repository Class:
class MainRepository {
//MutableLiveData
private val popularMoviesMutableLiveData = MutableLiveData<List<Movie>>()
private val searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
// API
private val apiService : GetFromApi = APIService.retrofitClientRequest
private val apiKey = "NOT"
fun getPopularMoviesList() : MutableLiveData<List<Movie>>{
apiService.getPopularMovies(apiKey,1)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
popularMoviesMutableLiveData.value = response.body()?.moviesResults
Log.e("MovieListResults","Result: ${popularMoviesMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("MovieListResults","Failed: ${t.message}")
}
})
return popularMoviesMutableLiveData
}
fun searchAfter(searchAfter : String) : MutableLiveData<List<Movie>>{
apiService.searchAfter(apiKey,searchAfter)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
searchAfterMutableLiveData.value = response.body()?.moviesResults
Log.e("SearchMovieListResults","Result: ${searchAfterMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("SearchMovieListResults","Failed: ${t.message}")
}
})
return searchAfterMutableLiveData
}
}
This is the viewModel class:
class MainViewModel : ViewModel(){
//Repository
private val mainRepository = MainRepository()
//MutableLiveData
var popularMoviesMutableLiveData = MutableLiveData<List<Movie>>()
var searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
//The Main Movie List
var mainMovieList = listOf<Movie>()
fun getPopularMovies() : LiveData<List<Movie>>{
popularMoviesMutableLiveData = mainRepository.getPopularMoviesList()
popularMoviesMutableLiveData.value = mainMovieList
return popularMoviesMutableLiveData
}
fun getMovieBySearch(searchAfter : String) : LiveData<List<Movie>>{
searchAfterMutableLiveData = mainRepository.searchAfter(searchAfter)
searchAfterMutableLiveData.value = mainMovieList
return searchAfterMutableLiveData
}
}
This is the MainActivity class:
class MainActivity : AppCompatActivity() {
//ViewModel
private val mainViewModel : MainViewModel by viewModels()
// Views
private lateinit var mainRecyclerView : RecyclerView
private lateinit var mainAdapter : MainRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMoviesRecyclerView()
}
private fun initMoviesRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
mainViewModel.getPopularMovies().observe(this, object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainViewModel.mainMovieList = newList
mainAdapter = MainRecyclerViewAdapter(mainViewModel.mainMovieList)
mainRecyclerView.adapter = mainAdapter
}
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu,menu)
val searchView = menu.findItem(R.id.menu_search_movie).actionView as androidx.appcompat.widget.SearchView
searchView.queryHint = "Search By Name,Actor .."
searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(whileTextChange: String?): Boolean {
//Clear SearchView
searchView.isIconified = true
searchView.setQuery("", false)
searchView.onActionViewCollapsed()
mainViewModel.getMovieBySearch(whileTextChange.toString()).observe(this#MainActivity,object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainViewModel.mainMovieList = newList
mainAdapter.changeCurrentList(mainViewModel.mainMovieList)
}
}
})
return false
}
override fun onQueryTextChange(whileTextChange: String?): Boolean {
Log.e("onQueryTextChange","Text: $whileTextChange")
return false
}
})
return true
}
}
Thank you!
I don't see where you actually populate the data inside the ViewModel. But it all looked good.
This line is a bit suspicious:
mainViewModel.mainMovieList = newList
You should not update the ViewModel from the View lifeCycle. Also, you should not update the ViewModel during the observe call.
So after sometime I manage to work around it with a simple solution:
I created a function in the viewModel,to check if the search list value is null:
fun startFromSearch(): Boolean {
return searchAfterMutableLiveData.value != null
}
After that I made an if statement in the initMoviesRecyclerView(), and that's it!
private fun initMoviesRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
mainAdapter = MainRecyclerViewAdapter(mainViewModel.mainMovieList,this)
mainRecyclerView.adapter = mainAdapter
if (!mainViewModel.startFromSearch()){
mainViewModel.getPopularMovies(1).observe(this, object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainAdapter = MainRecyclerViewAdapter(newList,this#MainActivity)
mainRecyclerView.adapter = mainAdapter
}
}
})
}
}
Related
I want to use my args.breweryName from my reviewActivity in my reviewViewModel but i don't know what's the best way to do it. I have tried using savestatehandle but i'm not experienced with that. I'm new to programming with kotlin so i would appreciate the help!
My ReviewActivity
class ReviewActivity : AppCompatActivity() {
val breweryText: TextView
get() = findViewById(R.id.breweryName)
private val args: ReviewActivityArgs by navArgs<ReviewActivityArgs>()
private var shownFragment: Fragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_review)
initView()
if (savedInstanceState == null) {
showFragment(ReviewFragment.newInstance())
}
breweryText.text = args.breweryName
}
fun test(): String {
return args.breweryName
}
private fun initView() {
val button : FloatingActionButton = findViewById(R.id.add)
showFragment(ReviewFragment.newInstance())
button.setOnClickListener { showSaveDialog() }
}
private fun showFragment(fragment: Fragment) {
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentHolder, fragment)
fragmentTransaction.commitNow()
shownFragment = fragment
}
private fun showSaveDialog() {
val dialogFragment: DialogFragment
val tag: String
dialogFragment = ReviewDialog.newInstance(null, null, args.breweryName)
tag = ReviewDialog.TAG_DIALOG_REVIEW_SAVE
dialogFragment.show(supportFragmentManager, tag)
}
My ReviewViewModel
class ReviewViewModel constructor(application: Application) : AndroidViewModel(application) {
private val reviewDao: ReviewDao = ReviewsDatabase.getDatabase(application).reviewDao()
private val userDao: UserDao = ReviewsDatabase.getDatabase(application).userDao()
val data = MutableLiveData<String>()
val reviewList: LiveData<List<Review>>
val userList: LiveData<List<User>>
fun data(item: String) {
data.value = item
}
init {
reviewList = reviewDao.allReviews
userList = userDao.allUsers
}
fun insert(vararg reviews: Review) {
reviewDao.insert(*reviews)
}
fun update(review: Review) {
reviewDao.update(review)
}
fun deleteAll() {
reviewDao.deleteAll()
}
}
I have the following ViewModel and Fragment -
class HeroesViewModel(private val heroesRepository: HeroesRepository) : ViewModel() {
private val internalUiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState = internalUiState.asLiveData()
private val internalUiAction = MutableSharedFlow<UiAction>(1).apply {
tryEmit(UiAction.GetSuggestedList)
}
val uiAction = internalUiAction.asLiveData()
private val externalUiEvent = MutableSharedFlow<UiEvent>(1)
private val uiEvent = externalUiEvent.asSharedFlow()
init {
observeUiEvents()
}
private fun observeUiEvents() = viewModelScope.launch {
uiEvent.collect { event ->
when (event) {
is UiEvent.ListItemClicked -> {
navigateToHeroDetails(event.heroModel)
}
is UiEvent.SearchTextChanged -> {
getHeroesByName(event.searchText)
}
}
}
}
private fun navigateToHeroDetails(heroModel: HeroesListModel) =
submitAction(UiAction.NavigateToHeroesDetails(heroModel))
private fun getHeroesByName(name: String) = viewModelScope.launch(Dispatchers.IO) {
when (val response = heroesRepository.getHeroesByNameWithSuggestions(name)) {
is NetworkResponse.Success -> {
internalUiState.emit(UiState.Data(response.body as List<HeroesListModel>))
}
is NetworkResponse.Error -> {
response.error.message?.let { message ->
internalUiState.emit(UiState.Error(message))
}
}
else -> {}
}
}
fun getSuggestedHeroesList() = viewModelScope.launch(Dispatchers.IO) {
when (val response = heroesRepository.getSuggestedHeroesList(true)) {
is NetworkResponse.Success -> {
submitState(UiState.Data(response.body as List<HeroesListModel>))
}
is NetworkResponse.Error -> {
response.error.message?.let { message ->
submitState(UiState.Error(message))
}
}
else -> {}
}
}
private fun submitAction(uiAction: UiAction) = internalUiAction.tryEmit(uiAction)
private fun submitState(uiState: UiState) = viewModelScope.launch {
internalUiState.emit(uiState)
}
fun submitEvent(uiEvent: UiEvent) = externalUiEvent.tryEmit(uiEvent)
sealed class UiEvent {
data class SearchTextChanged(val searchText: String) : UiEvent()
data class ListItemClicked(val heroModel: HeroesListModel) : UiEvent()
}
sealed class UiState {
data class Data(val modelsListResponse: List<BaseHeroListModel>) : UiState()
data class Error(val errorMessage: String) : UiState()
object Initial : UiState()
}
sealed class UiAction {
data class NavigateToHeroesDetails(val heroModel: HeroesListModel) : UiAction()
object GetSuggestedList : UiAction()
}
}
class DashboardFragment : Fragment() {
//Class Variables - UI
private lateinit var binding: FragmentDashboardBinding
//Class Variables - Dependency Injection
private val heroesViewModel = get<HeroesViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
init()
observeUiState()
observeUiAction()
}
private fun observeUiAction() = heroesViewModel.uiAction.observe(viewLifecycleOwner) { action ->
when(action){
is HeroesViewModel.UiAction.GetSuggestedList -> {
getSuggestedHeroesList()
}
is HeroesViewModel.UiAction.NavigateToHeroesDetails -> {
navigateToHeroesDetails(action.heroModel)
}
}
}
private fun init() {
binding.heroesSearchView.setOnQueryTextListener(object : OnSearchViewOnlyTextChangedListener() {
override fun onQueryTextChange(newText: String?): Boolean {
if (newText.isNullOrEmpty()) return false
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchTextChanged(newText))
binding.progressBar.setVisiblyAsVisible()
return false
}
})
}
private fun observeUiState() = heroesViewModel.uiState.observe(viewLifecycleOwner) { uiAction ->
when (uiAction) {
is HeroesViewModel.UiState.Data -> {
showHeroesList(uiAction)
}
is HeroesViewModel.UiState.Error -> {
showGeneralError(uiAction)
}
HeroesViewModel.UiState.Initial -> Unit
}
}
private fun navigateToHeroesDetails(heroModel: HeroesListModel) =
findNavController().navigate(DashboardFragmentDirections.actionMainFragmentToHeroesDetailsFragment(heroModel))
private fun showHeroesList(result: HeroesViewModel.UiState.Data) {
binding.heroesList.setContent {
LazyColumn {
items(result.modelsListResponse.toList()) { model ->
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
}
binding.progressBar.setVisiblyAsGone()
}
private fun showGeneralError(result: HeroesViewModel.UiState.Error) {
Toast.makeText(requireContext(), result.errorMessage, Toast.LENGTH_LONG).show()
binding.progressBar.setVisiblyAsGone()
}
private fun getSuggestedHeroesList() {
heroesViewModel.getSuggestedHeroesList()
binding.progressBar.setVisiblyAsVisible()
}
}
As you can see, I have the replayCache set to 1 in internalUiAction but the value keeps emitting itself. When I navigate using the navigateToHeroesDetails() method and go back using the navigation bar I immediately observe the last uiAction emitted value which is NavigateToHeroesDetails, causing me to navigate again and again to the heroes details screen. This is an endless loop of navigation.
As far as a hint for a solution, if I double tap the navigation 2 times quickly it does indeed go back to the first Fragment. Seems like I am missing something related to SharedFlow
So I got a problem, When the Main Page is shown, it's supposed to call the API to get the data from it. But, the API never called. What I find strange is, that before Main Page, there is a login page that successfully called the API for login. But, on Main Page, the Retrofit/Okhttp never called the Page to get the data. Do you guys know what's wrong?
Here's my code screenshot:
StoryPageSource
StoryRepository
ViewModel
StoryAdapter
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var bind: ActivityMainBinding
private lateinit var storyAdapter: StoryAdapter
private val timelineStoryViewModel: TimelineStoryViewModel by viewModels {
ViewModelFactory(this, application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind = ActivityMainBinding.inflate(layoutInflater)
setContentView(bind.root)
showLoadingProcess(true)
displayStories()
bind.floatingActionButton.setOnClickListener {
startActivity(Intent(this, AddStoryActivity::class.java))
}
supportActionBar?.setTitle(R.string.app_name)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflaterMenu = menuInflater
inflaterMenu.inflate(R.menu.menus, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.app_bar_settings -> {
val context = this
val pref = context.getSharedPreferences(
R.string.token_pref.toString(),
Context.MODE_PRIVATE
)
val editor = pref.edit()
editor.remove(R.string.token.toString())
editor.remove(getString(R.string.email))
editor.remove(getString(R.string.password))
editor.apply()
startActivity(Intent(this, LoginActivity::class.java))
}
R.id.app_bar_maps -> {
startActivity(Intent(this, MapsActivity::class.java))
}
}
return super.onOptionsItemSelected(item)
}
private fun displayStories() {
showLoadingProcess(false)
storyAdapter = StoryAdapter()
bind.storyRV.layoutManager = LinearLayoutManager(this)
bind.storyRV.adapter = storyAdapter
timelineStoryViewModel.story().observe(this#MainActivity) { story ->
storyAdapter.submitData(lifecycle, story)
}
storyAdapter.setOnProfileCallback(object : StoryAdapter.OnProfileCallback {
override fun onProfileClicked(data: TimelineStory) {
chosenProfile(data)
}
})
}
private fun chosenProfile(story: TimelineStory) {
var name: TextView = findViewById(R.id.userName)
var storyPict: ImageView = findViewById(R.id.userImageStory)
val optionsCompat: ActivityOptionsCompat =
ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(storyPict, getString(R.string.imageStoryDetail)),
Pair(name, getString(R.string.nama_pengguna)),
)
val sendData = Intent(this, StoryDetailActivity::class.java)
sendData.putExtra(StoryDetailActivity.EXTRA_STORY, story)
startActivity(sendData, optionsCompat.toBundle())
Toast.makeText(this, "Memuat Story " + story.name, Toast.LENGTH_SHORT).show()
}
private fun showLoadingProcess(isLoading: Boolean) {
if (isLoading) {
bind.loading.visibility = View.VISIBLE
} else {
bind.loading.visibility = View.GONE
}
}
}
I have following project in Github : https://github.com/AliRezaeiii/TMDb-Paging
I have to postDelay calling methods in my ViewModel since datasource is not initialized :
abstract class DetailViewModel(private val item: TmdbItem) : BaseViewModel() {
private val handler = Handler(Looper.getMainLooper())
val trailers: ObservableList<Video> = ObservableArrayList()
val isTrailersVisible = ObservableBoolean(false)
private val _cast = MutableLiveData<List<Cast>>()
val cast: LiveData<List<Cast>> = _cast
val isCastVisible = ObservableBoolean(false)
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
protected abstract fun getTrailers(id: Int): Observable<List<Video>>
protected abstract fun getCast(id: Int): Observable<List<Cast>>
private fun showTrailers() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getTrailers(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ videos ->
if (videos.isNotEmpty()) {
isTrailersVisible.set(true)
}
with(trailers) {
clear()
addAll(videos)
}
}
) { throwable -> Timber.e(throwable) })
}
private fun showCast() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getCast(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ cast ->
if (cast.isNotEmpty()) {
isCastVisible.set(true)
}
this._cast.postValue(cast)
}
) { throwable -> Timber.e(throwable) })
}
}
And here is my Fragment :
abstract class DetailFragment<T : TmdbItem>
: BaseDaggerFragment(), CastClickCallback {
protected abstract fun getViewModel(): DetailViewModel
protected abstract fun getLayoutId(): Int
protected abstract fun initViewBinding(root: View): ViewDataBinding
protected abstract fun getTmdbItem(): T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val viewModel = getViewModel()
val root = inflater.inflate(getLayoutId(), container, false)
initViewBinding(root).apply {
setVariable(BR.vm, viewModel)
lifecycleOwner = viewLifecycleOwner
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(details_toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
summary_label.visibleGone(getTmdbItem().overview.trim().isNotEmpty())
// Make the MotionLayout draw behind the status bar
details_motion.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
summary.setOnClickListener {
val maxLine = resources.getInteger(R.integer.max_lines)
summary.maxLines = if (summary.maxLines > maxLine) maxLine else Int.MAX_VALUE
}
viewModel.cast.observe(viewLifecycleOwner, Observer {
it?.apply {
val adapter = CastAdapter(it, this#DetailFragment)
cast_list.apply {
setHasFixedSize(true)
cast_list.adapter = adapter
}
}
})
with(details_rv) {
postDelayed({ scrollTo(0, 0) }, 100)
}
}
return root
}
}
And BaseDaggerFragment :
open class BaseDaggerFragment : DaggerFragment() {
#Inject
lateinit var dataSource: RemoteDataSource
}
Could be any better solution than :
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
You can lazy initialize like this way
private val users:MutableLiveData<List<Cast>> by lazy {
MutableLiveData().also {
showTrailers()
showCast()
}
}
more details refer ViewModel
While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView.
The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.
AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter
Fragment
class GradeFragment : BaseFragment(), GradeView {
#Inject
lateinit var presenter: GradePresenter
private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
companion object {
fun newInstance() = GradeFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this#GradeFragment)
loadData()
}
}
override fun initView() {
gradeAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnUpdateListener { presenter.onUpdateDataList(it) }
setOnItemClickListener { position ->
getItem(position).let {
if (it is GradeItem) {
GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
}
}
}
}
gradeRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeAdapter
}
gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}
override fun updateData(data: List<GradeHeader>) {
gradeAdapter.updateDataSet(data, true)
}
override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty
override fun showEmptyView(show: Boolean) {
gradeEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else GONE
}
override fun setRefresh(show: Boolean) {
gradeSwipe.isRefreshing = show
}
Presenter
class GradePresenter #Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
override fun attachView(view: GradeView) {
super.attachView(view)
view.initView()
}
fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradeRepository.getGrades(it, forceRefresh) }
.map { it.groupBy { grade -> grade.subject } }
.map { createGradeItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally { view?.setRefresh(false) }
.doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
.doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
return items.map {
val gradesAverage = calcAverage(it.value)
GradeHeader().apply {
subject = it.key
average = view?.run {
if (gradesAverage == 0f) emptyAverageString()
else averageString().format(gradesAverage)
}.orEmpty()
number = view?.gradeNumberString(it.value.size).orEmpty()
subItems = (it.value.map { item ->
GradeItem().apply {
grade = item
weightString = view?.weightString().orEmpty()
valueColor = getValueColor(item.value)
}
})
}
}
}
fun onUpdateDataList(size: Int) {
if (size != 0) view?.showProgress(false)
}
After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating