From 884c726cdd51dbb51f7fc16b36e5ed2a2cc5256a Mon Sep 17 00:00:00 2001 From: quanda-0562 Date: Thu, 11 May 2023 18:22:22 +0700 Subject: [PATCH] change liveData to stateFlow --- .../moviedb/compose/ui/home/HomeScreen.kt | 4 +- .../example/moviedb/ui/base/BaseActivity.kt | 21 +++++----- .../ui/base/BaseBottomSheetDialogFragment.kt | 21 +++++----- .../moviedb/ui/base/BaseDialogFragment.kt | 21 +++++----- .../example/moviedb/ui/base/BaseFragment.kt | 21 +++++----- .../example/moviedb/ui/base/BaseViewModel.kt | 39 +++++++++++-------- .../BaseLoadMoreRefreshFragment.kt | 9 +++-- .../BaseLoadMoreRefreshViewModel.kt | 35 ++++++++--------- .../ui/base/paging/BasePagingViewModel.kt | 15 ++++--- .../moviedb/ui/screen/image/ImageViewModel.kt | 4 +- .../moviedetail/MovieDetailViewModel.kt | 5 +-- .../screen/moviepager/MoviePagerFragment.kt | 13 ++++--- .../screen/moviepager/MoviePagerViewModel.kt | 4 +- .../ui/screen/oldmain/OldMainViewModel.kt | 4 +- .../popularmovie/PopularMovieViewModel.kt | 4 +- .../example/moviedb/utils/SingleLiveData.kt | 38 ------------------ 16 files changed, 122 insertions(+), 136 deletions(-) delete mode 100644 app/src/main/java/com/example/moviedb/utils/SingleLiveData.kt diff --git a/app/src/main/java/com/example/moviedb/compose/ui/home/HomeScreen.kt b/app/src/main/java/com/example/moviedb/compose/ui/home/HomeScreen.kt index 0997336..be87e0c 100644 --- a/app/src/main/java/com/example/moviedb/compose/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/example/moviedb/compose/ui/home/HomeScreen.kt @@ -41,8 +41,8 @@ fun HomeScreen( navController: NavController, viewModel: PopularMovieViewModel = hiltViewModel() ) { - val movieList by viewModel.itemList.observeAsState(listOf()) - val refreshing by viewModel.isRefreshing.observeAsState(false) + val movieList by viewModel.itemList.collectAsState(listOf()) + val refreshing by viewModel.isRefreshing.collectAsState(false) val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.doRefresh() }) val scrollState = rememberLazyGridState() val endOfListReached by remember { diff --git a/app/src/main/java/com/example/moviedb/ui/base/BaseActivity.kt b/app/src/main/java/com/example/moviedb/ui/base/BaseActivity.kt index dd5dfb7..8b0112d 100644 --- a/app/src/main/java/com/example/moviedb/ui/base/BaseActivity.kt +++ b/app/src/main/java/com/example/moviedb/ui/base/BaseActivity.kt @@ -5,11 +5,14 @@ import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding +import androidx.lifecycle.lifecycleScope import com.example.moviedb.BR import com.example.moviedb.R import com.example.moviedb.utils.dismissLLoadingDialog import com.example.moviedb.utils.showDialog import com.example.moviedb.utils.showLoadingDialog +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch abstract class BaseActivity : AppCompatActivity() { @@ -33,26 +36,26 @@ abstract class BaseActivity : BottomSheetDialogFragment() { @@ -47,26 +50,26 @@ abstract class BaseBottomSheetDialogFragment : DialogFragment() { @@ -46,26 +49,26 @@ abstract class BaseDialogFragment : Fragment() { @@ -47,26 +50,26 @@ abstract class BaseFragment().apply { value = false } } + val isLoading by lazy { MutableStateFlow(false) } // error message - val errorMessage by lazy { SingleLiveData() } + val errorMessage by lazy { MutableSharedFlow() } // optional flags - val noInternetConnectionEvent by lazy { SingleLiveData() } - val connectTimeoutEvent by lazy { SingleLiveData() } - val forceUpdateAppEvent by lazy { SingleLiveData() } - val serverMaintainEvent by lazy { SingleLiveData() } - val unknownErrorEvent by lazy { SingleLiveData() } + val noInternetConnectionEvent by lazy { MutableSharedFlow() } + val connectTimeoutEvent by lazy { MutableSharedFlow() } + val forceUpdateAppEvent by lazy { MutableSharedFlow() } + val serverMaintainEvent by lazy { MutableSharedFlow() } + val unknownErrorEvent by lazy { MutableSharedFlow() } // exception handler for coroutine private val exceptionHandler by lazy { @@ -40,31 +41,35 @@ open class BaseViewModel : ViewModel() { /** * handle throwable when load fail */ - protected open fun onError(throwable: Throwable) { + protected open suspend fun onError(throwable: Throwable) { when (throwable) { // case no internet connection is UnknownHostException -> { - noInternetConnectionEvent.call() + noInternetConnectionEvent.emit(Unit) } + is ConnectException -> { - noInternetConnectionEvent.call() + noInternetConnectionEvent.emit(Unit) } // case request time out is SocketTimeoutException -> { - connectTimeoutEvent.call() + connectTimeoutEvent.emit(Unit) } + else -> { // convert throwable to base exception to get error information val baseException = throwable.toBaseException() when (baseException.httpCode) { HttpURLConnection.HTTP_UNAUTHORIZED -> { - errorMessage.value = baseException.message + errorMessage.emit(baseException.message ?: "") } + HttpURLConnection.HTTP_INTERNAL_ERROR -> { - errorMessage.value = baseException.message + errorMessage.emit(baseException.message ?: "") } + else -> { - unknownErrorEvent.call() + unknownErrorEvent.emit(Unit) } } } @@ -72,8 +77,8 @@ open class BaseViewModel : ViewModel() { hideLoading() } - open fun showError(e: Throwable) { - errorMessage.value = e.message + open suspend fun showError(e: Throwable) { + errorMessage.emit(e.message ?: "") } fun showLoading() { diff --git a/app/src/main/java/com/example/moviedb/ui/base/loadmorerefresh/BaseLoadMoreRefreshFragment.kt b/app/src/main/java/com/example/moviedb/ui/base/loadmorerefresh/BaseLoadMoreRefreshFragment.kt index 997924a..bd7c254 100644 --- a/app/src/main/java/com/example/moviedb/ui/base/loadmorerefresh/BaseLoadMoreRefreshFragment.kt +++ b/app/src/main/java/com/example/moviedb/ui/base/loadmorerefresh/BaseLoadMoreRefreshFragment.kt @@ -3,12 +3,15 @@ package com.example.moviedb.ui.base.loadmorerefresh import android.os.Bundle import android.view.View import androidx.databinding.ViewDataBinding +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.example.moviedb.R import com.example.moviedb.ui.base.BaseFragment import com.example.moviedb.ui.base.BaseListAdapter +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * should use paging 3 @@ -39,11 +42,11 @@ abstract class BaseLoadMoreRefreshFragment() : BaseViewModel() { // refresh flag - val isRefreshing = MutableLiveData().apply { value = false } + val isRefreshing = MutableStateFlow(false) // load more flag - private val isLoadMore = MutableLiveData().apply { value = false } + private val isLoadMore = MutableStateFlow(false) // current page - private val currentPage = MutableLiveData().apply { value = getPreFirstPage() } + private val currentPage = MutableStateFlow(getPreFirstPage()) // last page flag - private val isLastPage = MutableLiveData().apply { value = false } + private val isLastPage = MutableStateFlow(false) // scroll listener for recycler view val onScrollListener = object : EndlessRecyclerOnScrollListener(getLoadMoreThreshold()) { @@ -30,10 +30,10 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { } // item list - val itemList = MutableLiveData>() + val itemList = MutableStateFlow(arrayListOf()) // empty list flag - val isEmptyList = MutableLiveData().apply { value = false } + val isEmptyList = MutableStateFlow(false) /** * load data @@ -44,7 +44,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { * check first time load data */ private fun isFirst() = currentPage.value == getPreFirstPage() - && itemList.value?.isEmpty() ?: true + && itemList.value.isEmpty() /** * first load @@ -57,7 +57,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { } fun doRefresh() { - if (isLoading.value == true || isRefreshing.value == true) return + if (isLoading.value || isRefreshing.value) return isRefreshing.value = true refreshData() } @@ -70,10 +70,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { } fun doLoadMore() { - if (isLoading.value == true - || isRefreshing.value == true - || isLoadMore.value == true - || isLastPage.value == true + if (isLoading.value || isRefreshing.value || isLoadMore.value || isLastPage.value ) return isLoadMore.value = true loadMore() @@ -83,7 +80,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { * load next page */ protected fun loadMore() { - loadData(currentPage.value?.plus(1) ?: getFirstPage()) + loadData(currentPage.value.plus(1)) } /** @@ -118,9 +115,9 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { // load success then update current page currentPage.value = page // case load first page then clear data from listItem - if (currentPage.value == getFirstPage()) itemList.value?.clear() + if (currentPage.value == getFirstPage()) itemList.value.clear() // case refresh then reset load more - if (isRefreshing.value == true) resetLoadMore() + if (isRefreshing.value) resetLoadMore() // add new data to listItem val newList = itemList.value ?: ArrayList() @@ -128,7 +125,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { itemList.value = newList // check last page - isLastPage.value = items?.size ?: 0 < getNumberItemPerPage() + isLastPage.value = (items?.size ?: 0) < getNumberItemPerPage() // reset load hideLoading() @@ -142,7 +139,7 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { /** * handle load fail */ - override fun onError(throwable: Throwable) { + override suspend fun onError(throwable: Throwable) { super.onError(throwable) onScrollListener.isLoading = false @@ -158,6 +155,6 @@ abstract class BaseLoadMoreRefreshViewModel() : BaseViewModel() { * check list is empty */ private fun checkEmptyList() { - isEmptyList.value = itemList.value?.isEmpty() ?: true + isEmptyList.value = itemList.value.isEmpty() ?: true } } diff --git a/app/src/main/java/com/example/moviedb/ui/base/paging/BasePagingViewModel.kt b/app/src/main/java/com/example/moviedb/ui/base/paging/BasePagingViewModel.kt index 2e4fea3..cad6d48 100644 --- a/app/src/main/java/com/example/moviedb/ui/base/paging/BasePagingViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/base/paging/BasePagingViewModel.kt @@ -1,12 +1,17 @@ package com.example.moviedb.ui.base.paging -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import androidx.paging.* +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn import com.example.moviedb.data.constant.Constants import com.example.moviedb.data.source.BasePagingSource import com.example.moviedb.ui.base.BaseViewModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch abstract class BasePagingViewModel : BaseViewModel() { @@ -16,10 +21,10 @@ abstract class BasePagingViewModel : BaseViewModel() { } // refresh flag - val isRefresh by lazy { MutableLiveData(false) } + val isRefresh by lazy { MutableStateFlow(false) } // empty list flag - val isEmptyList by lazy { MutableLiveData(false) } + val isEmptyList by lazy { MutableStateFlow(false) } // number item per page protected open val pageSize by lazy { Constants.DEFAULT_ITEM_PER_PAGE } @@ -86,7 +91,7 @@ abstract class BasePagingViewModel : BaseViewModel() { /** * handler error */ - override fun onError(throwable: Throwable) { + override suspend fun onError(throwable: Throwable) { super.onError(throwable) // reset load hideLoadRefresh() diff --git a/app/src/main/java/com/example/moviedb/ui/screen/image/ImageViewModel.kt b/app/src/main/java/com/example/moviedb/ui/screen/image/ImageViewModel.kt index 431d383..855a3d6 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/image/ImageViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/image/ImageViewModel.kt @@ -1,13 +1,13 @@ package com.example.moviedb.ui.screen.image import com.example.moviedb.ui.base.BaseViewModel -import com.example.moviedb.utils.SingleLiveData import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @HiltViewModel class ImageViewModel @Inject constructor() : BaseViewModel() { - val imageUrl = SingleLiveData() + val imageUrl by lazy { MutableStateFlow("") } } \ No newline at end of file diff --git a/app/src/main/java/com/example/moviedb/ui/screen/moviedetail/MovieDetailViewModel.kt b/app/src/main/java/com/example/moviedb/ui/screen/moviedetail/MovieDetailViewModel.kt index e2cfc26..9177a7a 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/moviedetail/MovieDetailViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/moviedetail/MovieDetailViewModel.kt @@ -5,7 +5,6 @@ import com.example.moviedb.data.model.Cast import com.example.moviedb.data.model.Movie import com.example.moviedb.data.repository.UserRepository import com.example.moviedb.ui.base.BaseViewModel -import com.example.moviedb.utils.SingleLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +18,7 @@ class MovieDetailViewModel @Inject constructor( private val userRepository: UserRepository ) : BaseViewModel() { - val movie = SingleLiveData() + val movie = MutableStateFlow(null) private val _castList = MutableStateFlow>(emptyList()) val castList = _castList.asStateFlow() @@ -31,7 +30,7 @@ class MovieDetailViewModel @Inject constructor( if (favoriteMovie?.isFavorite == true) { val newMovie = movie.value newMovie?.isFavorite = true - movie.value = newMovie + movie.emit(newMovie) } else { movie.value?.isFavorite = false } diff --git a/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerFragment.kt b/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerFragment.kt index 1d2afed..f4060d5 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerFragment.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerFragment.kt @@ -5,14 +5,18 @@ import android.os.Bundle import android.view.View import androidx.databinding.ViewDataBinding import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.example.moviedb.R +import com.example.moviedb.data.constant.MovieListType import com.example.moviedb.data.model.Movie import com.example.moviedb.databinding.FragmentMoviePagerBinding import com.example.moviedb.ui.base.BaseListAdapter import com.example.moviedb.ui.base.loadmorerefresh.BaseLoadMoreRefreshFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlin.math.abs @AndroidEntryPoint @@ -59,7 +63,7 @@ class MoviePagerFragment : super.onViewCreated(view, savedInstanceState) viewModel.apply { - mode.value = arguments?.getInt(TYPE) + mode.value = arguments?.getInt(TYPE) ?: MovieListType.POPULAR.type } viewBinding.container.setBackgroundColor(Color.BLACK) @@ -86,12 +90,11 @@ class MoviePagerFragment : // view.translationY = abs(position) * ((MAX_SCALE - scaleY) / 2 * view.height) } } - - viewModel.apply { - itemList.observe(viewLifecycleOwner) { + lifecycleScope.launch { + viewModel.itemList.collectLatest { listAdapter.submitList(it) } - firstLoad() + viewModel.firstLoad() } } diff --git a/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerViewModel.kt b/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerViewModel.kt index 15caa03..48a62a0 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/moviepager/MoviePagerViewModel.kt @@ -1,7 +1,6 @@ package com.example.moviedb.ui.screen.moviepager import android.content.res.Resources -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.example.moviedb.data.constant.MovieListType import com.example.moviedb.data.model.Movie @@ -9,6 +8,7 @@ import com.example.moviedb.data.remote.api.ApiParams import com.example.moviedb.data.repository.UserRepository import com.example.moviedb.ui.base.loadmorerefresh.BaseLoadMoreRefreshViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,7 +18,7 @@ class MoviePagerViewModel @Inject constructor( private val userRepository: UserRepository ) : BaseLoadMoreRefreshViewModel() { - val mode = MutableLiveData().apply { value = MovieListType.POPULAR.type } + val mode = MutableStateFlow(MovieListType.POPULAR.type) override fun loadData(page: Int) { val hashMap = HashMap() diff --git a/app/src/main/java/com/example/moviedb/ui/screen/oldmain/OldMainViewModel.kt b/app/src/main/java/com/example/moviedb/ui/screen/oldmain/OldMainViewModel.kt index ff20c20..8bd2383 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/oldmain/OldMainViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/oldmain/OldMainViewModel.kt @@ -1,13 +1,13 @@ package com.example.moviedb.ui.screen.oldmain -import androidx.lifecycle.MutableLiveData import com.example.moviedb.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @HiltViewModel class OldMainViewModel @Inject constructor() : BaseViewModel() { - val currentTab = MutableLiveData() + val currentTab = MutableStateFlow(Tab.POPULAR.position) } \ No newline at end of file diff --git a/app/src/main/java/com/example/moviedb/ui/screen/popularmovie/PopularMovieViewModel.kt b/app/src/main/java/com/example/moviedb/ui/screen/popularmovie/PopularMovieViewModel.kt index 35dbb1b..8733411 100644 --- a/app/src/main/java/com/example/moviedb/ui/screen/popularmovie/PopularMovieViewModel.kt +++ b/app/src/main/java/com/example/moviedb/ui/screen/popularmovie/PopularMovieViewModel.kt @@ -1,6 +1,5 @@ package com.example.moviedb.ui.screen.popularmovie -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.example.moviedb.data.constant.MovieListType import com.example.moviedb.data.model.Movie @@ -8,6 +7,7 @@ import com.example.moviedb.data.remote.api.ApiParams import com.example.moviedb.data.repository.UserRepository import com.example.moviedb.ui.base.loadmorerefresh.BaseLoadMoreRefreshViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -16,7 +16,7 @@ class PopularMovieViewModel @Inject constructor( private val userRepository: UserRepository ) : BaseLoadMoreRefreshViewModel() { - val mode = MutableLiveData().apply { value = MovieListType.POPULAR.type } + val mode = MutableStateFlow(MovieListType.POPULAR.type) override fun loadData(page: Int) { val hashMap = HashMap() diff --git a/app/src/main/java/com/example/moviedb/utils/SingleLiveData.kt b/app/src/main/java/com/example/moviedb/utils/SingleLiveData.kt deleted file mode 100644 index 6ca52fb..0000000 --- a/app/src/main/java/com/example/moviedb/utils/SingleLiveData.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.moviedb.utils - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import java.util.concurrent.atomic.AtomicBoolean - -/** - * https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 - * Description: Custom mutable live data that used for single event - * such as navigation (for configuration change), show toast.. - * NOTE: can has only 1 observer - */ -class SingleLiveData : MutableLiveData() { - - private val pending = AtomicBoolean(false) - - override fun setValue(value: T?) { - pending.set(true) - super.setValue(value) - } - - override fun observe(owner: LifecycleOwner, observer: Observer) { - super.observe(owner) { - if (pending.compareAndSet(true, false)) { - observer.onChanged(it) - } - } - } - - /** - * Single event for no data input. Make call more clear - * Example: navigation with no data: SingleLiveEvent() - */ - fun call() { - value = null - } -}