MovieListViewModel.kt

package com.louisfn.somovie.feature.movielist

import androidx.annotation.AnyThread
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import androidx.paging.cachedIn
import com.louisfn.somovie.core.common.annotation.DefaultDispatcher
import com.louisfn.somovie.core.common.extension.takeAs
import com.louisfn.somovie.domain.model.ExploreCategory
import com.louisfn.somovie.domain.usecase.movie.MovieInteractor
import com.louisfn.somovie.feature.movielist.MovieListUiState.LoadNextPageState
import com.louisfn.somovie.ui.common.base.BaseViewModel
import com.louisfn.somovie.ui.common.base.NoneAction
import com.louisfn.somovie.ui.common.error.ErrorsDispatcher
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
internal class MovieListViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    @DefaultDispatcher defaultDispatcher: CoroutineDispatcher,
    private val movieInteractor: MovieInteractor,
    private val errorsDispatcher: ErrorsDispatcher,
) : BaseViewModel<NoneAction>(defaultDispatcher) {

    private val category: ExploreCategory =
        ExploreCategory.valueOf(
            savedStateHandle[MovieListNavigation.ARGS_CATEGORY]
                ?: throw IllegalArgumentException("${MovieListNavigation.ARGS_CATEGORY} not found"),
        )

    private val loadStates = MutableStateFlow<CombinedLoadStates?>(null)

    val pagedMovies = movieInteractor.moviesPagingChanges(category)
        .cachedIn(viewModelScope)

    val uiState: StateFlow<MovieListUiState> =
        loadStates
            .filterNotNull()
            .map(::createUiState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = MovieListUiState(category),
            )

    private fun createUiState(loadStates: CombinedLoadStates): MovieListUiState {
        val appendLoadState = loadStates.append

        return MovieListUiState(
            category = category,
            loadNextPageState = when (appendLoadState) {
                is LoadState.Error -> LoadNextPageState.FAILED
                is LoadState.Loading -> LoadNextPageState.LOADING
                is LoadState.NotLoading -> LoadNextPageState.IDLE
            },
        )
    }

    //region Paging

    @AnyThread
    fun onLoadStateChanged(loadStates: CombinedLoadStates) {
        this.loadStates.value = loadStates

        loadStates.refresh.takeAs<LoadState.Error>()?.let {
            onPagingError(it.error)
        }
        loadStates.append.takeAs<LoadState.Error>()?.let {
            onPagingError(it.error)
        }
    }

    @AnyThread
    fun onPagingError(e: Throwable) {
        errorsDispatcher.dispatch(e)
    }

    //endregion
}