Skip to content

Commit 2b32f83

Browse files
Merge pull request #17 from reactivedroid/livedata-to-stateflow
Migration from LiveData to StateFlow
2 parents d1144f7 + d83318a commit 2b32f83

File tree

17 files changed

+262
-248
lines changed

17 files changed

+262
-248
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# TvFlix :tv:
55

66
The aim of this app is to replicate the high level functionality of www.tvmaze.com and showcase an android app out of it.
7-
It connects with [TVDB API](api.thetvdb.com) to give you popular shows and let you mark anyone as favorite.
7+
It connects with [TVDB API](https://api.thetvdb.com) to give you popular shows and let you mark anyone as favorite.
88
TvFlix consists of 3 pieces of UI right now:
99
1. Home with Popular Shows
1010
2. Favorites
@@ -17,14 +17,14 @@ This app is under development. :construction_worker: :hammer_and_wrench:
1717
## Android Development and Architecture
1818

1919
* The entire codebase is in [Kotlin](https://kotlinlang.org/)
20-
* Uses Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html)
21-
* Uses MVVM Architecture by [Architecture Components](https://developer.android.com/topic/libraries/architecture/). Room, LiveData, ViewModel, Paging
20+
* Uses Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html).
21+
* Uses MVVM Architecture by [Architecture Components](https://developer.android.com/topic/libraries/architecture/). Room, ViewModel, Paging
2222
* Uses [Hilt Android](https://developer.android.com/training/dependency-injection/hilt-android) with [Dagger](https://dagger.dev/) for dependency injection
2323
* Unit Testing by [Mockito](https://github.com/mockito/mockito)
2424
* Tests Coroutines and architecture components like ViewModel
2525
* UI Test by [Espresso](https://developer.android.com/training/testing/espresso) based on [Robot Pattern](https://academy.realm.io/posts/kau-jake-wharton-testing-robots/)
26-
27-
*Note* For reference, the Java Codebase has been tagged on `tvmaze_java`. Just checkout the tag and you are in TvMaze Java Land.
26+
* Uses [Kotlin Coroutines Test](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/) to unit test Kotlin Coroutines
27+
* Uses [StateFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) as a replacement over LiveData as a state-holder observable
2828

2929
## Further Reading
3030

app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ dependencies {
109109
implementation(Deps.AndroidX.Lifecycle.extensions)
110110
kapt(Deps.AndroidX.Lifecycle.compiler)
111111
implementation(Deps.AndroidX.Lifecycle.viewmodel)
112-
implementation(Deps.AndroidX.Lifecycle.livedata)
113112
implementation(Deps.AndroidX.Paging.runtime)
114113
testImplementation(Deps.AndroidX.Paging.common)
115114
implementation(Deps.AndroidX.Room.runtime)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.android.tvmaze.di
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.Dispatchers.Default
5+
import kotlinx.coroutines.Dispatchers.IO
6+
import kotlinx.coroutines.Dispatchers.Main
7+
import javax.inject.Inject
8+
9+
/**
10+
* Provide coroutines context.
11+
*/
12+
data class CoroutinesDispatcherProvider(
13+
val main: CoroutineDispatcher,
14+
val computation: CoroutineDispatcher,
15+
val io: CoroutineDispatcher
16+
) {
17+
18+
@Inject
19+
constructor() : this(Main, Default, IO)
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.android.tvmaze.favorite
2+
3+
import com.android.tvmaze.db.favouriteshow.FavoriteShow
4+
5+
sealed class FavoriteShowState {
6+
data class AllFavorites(val favoriteShows: List<FavoriteShow>) : FavoriteShowState()
7+
data class Error(val message: String) : FavoriteShowState()
8+
object Loading : FavoriteShowState()
9+
object Empty : FavoriteShowState()
10+
data class AddedToFavorites(val show: FavoriteShow) : FavoriteShowState()
11+
data class RemovedFromFavorites(val show: FavoriteShow) : FavoriteShowState()
12+
}

app/src/main/java/com/android/tvmaze/favorite/FavoriteShowsActivity.kt

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ import android.text.SpannableString
77
import android.text.Spanned
88
import android.text.style.ImageSpan
99
import android.view.MenuItem
10-
import android.view.View
1110
import android.widget.Toast
1211
import androidx.activity.viewModels
1312
import androidx.appcompat.app.AppCompatActivity
1413
import androidx.core.content.ContextCompat
14+
import androidx.core.view.isVisible
15+
import androidx.lifecycle.lifecycleScope
1516
import androidx.recyclerview.widget.GridLayoutManager
1617
import com.android.tvmaze.R
1718
import com.android.tvmaze.databinding.ActivityFavoriteShowsBinding
1819
import com.android.tvmaze.db.favouriteshow.FavoriteShow
1920
import com.android.tvmaze.utils.GridItemDecoration
2021
import dagger.hilt.android.AndroidEntryPoint
22+
import kotlinx.coroutines.flow.collect
2123

2224
@AndroidEntryPoint
2325
class FavoriteShowsActivity : AppCompatActivity(), FavoriteShowsAdapter.Callback {
@@ -29,8 +31,22 @@ class FavoriteShowsActivity : AppCompatActivity(), FavoriteShowsAdapter.Callback
2931
setContentView(binding.root)
3032
setToolbar()
3133
favoriteShowsViewModel.loadFavoriteShows()
32-
favoriteShowsViewModel.getFavoriteShowsLiveData()
33-
.observe(this, { showFavorites(it) })
34+
lifecycleScope.launchWhenStarted {
35+
favoriteShowsViewModel.favoriteShowsStateFlow.collect { setViewState(it) }
36+
}
37+
}
38+
39+
private fun setViewState(favoriteShowState: FavoriteShowState) {
40+
when (favoriteShowState) {
41+
is FavoriteShowState.Loading -> binding.progress.isVisible = true
42+
is FavoriteShowState.AllFavorites ->
43+
showFavorites(favoriteShowState.favoriteShows)
44+
is FavoriteShowState.Error, FavoriteShowState.Empty -> showEmptyState()
45+
is FavoriteShowState.AddedToFavorites ->
46+
Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT).show()
47+
is FavoriteShowState.RemovedFromFavorites ->
48+
Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show()
49+
}
3450
}
3551

3652
private fun setToolbar() {
@@ -43,35 +59,30 @@ class FavoriteShowsActivity : AppCompatActivity(), FavoriteShowsAdapter.Callback
4359
}
4460

4561
private fun showFavorites(favoriteShows: List<FavoriteShow>) {
46-
binding.progress.visibility = View.GONE
47-
if (favoriteShows.isNotEmpty()) {
48-
val layoutManager = GridLayoutManager(this, COLUMNS_COUNT)
49-
binding.shows.layoutManager = layoutManager
50-
val favoriteShowsAdapter = FavoriteShowsAdapter(favoriteShows.toMutableList(), this)
51-
binding.shows.adapter = favoriteShowsAdapter
52-
val spacing = resources.getDimensionPixelSize(R.dimen.show_grid_spacing)
53-
binding.shows.addItemDecoration(GridItemDecoration(spacing, COLUMNS_COUNT))
54-
binding.shows.visibility = View.VISIBLE
55-
} else {
56-
val bookmarkSpan = ImageSpan(this, R.drawable.favorite_border)
57-
val spannableString = SpannableString(getString(R.string.favorite_hint_msg))
58-
spannableString.setSpan(
59-
bookmarkSpan, FAVORITE_ICON_START_OFFSET,
60-
FAVORITE_ICON_END_OFFSET, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
61-
)
62-
binding.favoriteHint.text = spannableString
63-
binding.favoriteHint.visibility = View.VISIBLE
64-
}
62+
binding.progress.isVisible = false
63+
val layoutManager = GridLayoutManager(this, COLUMNS_COUNT)
64+
binding.shows.layoutManager = layoutManager
65+
val favoriteShowsAdapter = FavoriteShowsAdapter(favoriteShows.toMutableList(), this)
66+
binding.shows.adapter = favoriteShowsAdapter
67+
val spacing = resources.getDimensionPixelSize(R.dimen.show_grid_spacing)
68+
binding.shows.addItemDecoration(GridItemDecoration(spacing, COLUMNS_COUNT))
69+
binding.shows.isVisible = true
70+
}
71+
72+
private fun showEmptyState() {
73+
binding.progress.isVisible = false
74+
val bookmarkSpan = ImageSpan(this, R.drawable.favorite_border)
75+
val spannableString = SpannableString(getString(R.string.favorite_hint_msg))
76+
spannableString.setSpan(
77+
bookmarkSpan, FAVORITE_ICON_START_OFFSET,
78+
FAVORITE_ICON_END_OFFSET, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
79+
)
80+
binding.favoriteHint.text = spannableString
81+
binding.favoriteHint.isVisible = true
6582
}
6683

6784
override fun onFavoriteClicked(show: FavoriteShow) {
68-
if (!show.isFavorite) {
69-
favoriteShowsViewModel.addToFavorite(show)
70-
Toast.makeText(this, R.string.added_to_favorites, Toast.LENGTH_SHORT).show()
71-
} else {
72-
favoriteShowsViewModel.removeFromFavorite(show)
73-
Toast.makeText(this, R.string.removed_from_favorites, Toast.LENGTH_SHORT).show()
74-
}
85+
favoriteShowsViewModel.onFavoriteClick(show)
7586
}
7687

7788
companion object {

app/src/main/java/com/android/tvmaze/favorite/FavoriteShowsRepository.kt

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ package com.android.tvmaze.favorite
33
import com.android.tvmaze.db.favouriteshow.FavoriteShow
44
import com.android.tvmaze.db.favouriteshow.ShowDao
55
import com.android.tvmaze.network.home.Show
6-
import kotlinx.coroutines.CoroutineScope
7-
import kotlinx.coroutines.Dispatchers
8-
import kotlinx.coroutines.launch
96
import javax.inject.Inject
107
import javax.inject.Singleton
118

@@ -17,7 +14,7 @@ constructor(private val showDao: ShowDao) {
1714
return showDao.allFavouriteShows()
1815
}
1916

20-
fun insertShowIntoFavorites(show: Show) {
17+
suspend fun insertShowIntoFavorites(show: Show) {
2118
val favoriteShow = FavoriteShow(
2219
id = show.id,
2320
name = show.name,
@@ -28,10 +25,10 @@ constructor(private val showDao: ShowDao) {
2825
runtime = show.runtime!!,
2926
isFavorite = true
3027
)
31-
CoroutineScope(Dispatchers.IO).launch { showDao.insert(favoriteShow) }
28+
showDao.insert(favoriteShow)
3229
}
3330

34-
fun removeShowFromFavorites(show: Show) {
31+
suspend fun removeShowFromFavorites(show: Show) {
3532
val favoriteShow = FavoriteShow(
3633
id = show.id,
3734
name = show.name,
@@ -42,22 +39,22 @@ constructor(private val showDao: ShowDao) {
4239
runtime = show.runtime!!,
4340
isFavorite = false
4441
)
45-
CoroutineScope(Dispatchers.IO).launch { showDao.remove(favoriteShow) }
42+
showDao.remove(favoriteShow)
4643
}
4744

48-
fun insertIntoFavorites(favoriteShow: FavoriteShow) {
49-
CoroutineScope(Dispatchers.IO).launch { showDao.insert(favoriteShow) }
45+
suspend fun insertIntoFavorites(favoriteShow: FavoriteShow) {
46+
showDao.insert(favoriteShow)
5047
}
5148

52-
fun removeFromFavorites(favoriteShow: FavoriteShow) {
53-
CoroutineScope(Dispatchers.IO).launch { showDao.remove(favoriteShow) }
49+
suspend fun removeFromFavorites(favoriteShow: FavoriteShow) {
50+
showDao.remove(favoriteShow)
5451
}
5552

5653
suspend fun allFavoriteShowIds(): List<Long> {
5754
return showDao.getFavoriteShowIds()
5855
}
5956

60-
fun clearAll() {
61-
CoroutineScope(Dispatchers.IO).launch { showDao.clear() }
57+
suspend fun clearAll() {
58+
showDao.clear()
6259
}
6360
}
Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package com.android.tvmaze.favorite
22

33
import androidx.hilt.lifecycle.ViewModelInject
4-
import androidx.lifecycle.LiveData
5-
import androidx.lifecycle.MutableLiveData
64
import androidx.lifecycle.ViewModel
75
import androidx.lifecycle.viewModelScope
86
import com.android.tvmaze.db.favouriteshow.FavoriteShow
7+
import com.android.tvmaze.di.CoroutinesDispatcherProvider
98
import kotlinx.coroutines.CoroutineExceptionHandler
10-
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.asStateFlow
1111
import kotlinx.coroutines.launch
1212
import kotlinx.coroutines.withContext
1313
import timber.log.Timber
14-
import java.util.ArrayList
14+
import java.util.*
1515

1616
class FavoriteShowsViewModel @ViewModelInject
17-
constructor(private val favoriteShowsRepository: FavoriteShowsRepository) : ViewModel() {
18-
private val favoriteShowsLiveData: MutableLiveData<List<FavoriteShow>> = MutableLiveData()
17+
constructor(
18+
private val favoriteShowsRepository: FavoriteShowsRepository,
19+
// Inject coroutineDispatcher to facilitate Unit Testing
20+
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider
21+
) : ViewModel() {
22+
private val _favoriteShowsStateFlow =
23+
MutableStateFlow<FavoriteShowState>(FavoriteShowState.Loading)
24+
25+
// Represents _favoriteShowsStateFlow mutable state flow as a read-only state flow.
26+
val favoriteShowsStateFlow = _favoriteShowsStateFlow.asStateFlow()
27+
1928
private lateinit var removedFromFavoriteShows: List<FavoriteShow>
2029

2130
fun loadFavoriteShows() {
@@ -24,33 +33,35 @@ constructor(private val favoriteShowsRepository: FavoriteShowsRepository) : View
2433
}
2534

2635
viewModelScope.launch(coroutineExceptionHandler) {
27-
val favoriteShows = withContext(Dispatchers.IO + coroutineExceptionHandler) {
28-
favoriteShowsRepository.allFavoriteShows()
29-
}
30-
withContext(Dispatchers.Main + coroutineExceptionHandler) {
31-
onFavoritesFetched(favoriteShows)
36+
val favoriteShows =
37+
withContext(coroutinesDispatcherProvider.io) {
38+
favoriteShowsRepository.allFavoriteShows()
39+
}
40+
withContext(coroutinesDispatcherProvider.main) {
41+
removedFromFavoriteShows = ArrayList(favoriteShows.size)
42+
if (favoriteShows.isNotEmpty()) {
43+
_favoriteShowsStateFlow.emit(FavoriteShowState.AllFavorites(favoriteShows))
44+
} else {
45+
_favoriteShowsStateFlow.emit(FavoriteShowState.Empty)
46+
}
3247
}
3348
}
3449
}
3550

3651
private fun onError(throwable: Throwable) {
52+
_favoriteShowsStateFlow.value = FavoriteShowState.Error(throwable.localizedMessage)
3753
Timber.d(throwable)
3854
}
3955

40-
private fun onFavoritesFetched(favoriteShows: List<FavoriteShow>) {
41-
removedFromFavoriteShows = ArrayList(favoriteShows.size)
42-
favoriteShowsLiveData.value = favoriteShows
43-
}
44-
45-
fun getFavoriteShowsLiveData(): LiveData<List<FavoriteShow>> {
46-
return favoriteShowsLiveData
47-
}
48-
49-
fun addToFavorite(show: FavoriteShow) {
50-
favoriteShowsRepository.insertIntoFavorites(show)
51-
}
52-
53-
fun removeFromFavorite(show: FavoriteShow) {
54-
favoriteShowsRepository.removeFromFavorites(show)
56+
fun onFavoriteClick(show: FavoriteShow) {
57+
viewModelScope.launch(coroutinesDispatcherProvider.io) {
58+
if (!show.isFavorite) {
59+
favoriteShowsRepository.insertIntoFavorites(show)
60+
_favoriteShowsStateFlow.emit(FavoriteShowState.AddedToFavorites(show))
61+
} else {
62+
favoriteShowsRepository.removeFromFavorites(show)
63+
_favoriteShowsStateFlow.emit(FavoriteShowState.RemovedFromFavorites(show))
64+
}
65+
}
5566
}
5667
}

0 commit comments

Comments
 (0)