Skip to content

Commit 4f89c1b

Browse files
feat(viewmodel): add debug logging and prevent race conditions
- Add `logChanges` `StateFlow` extension to log UI state updates for debugging. - Implement job cancellation in `NewsHomeViewModel` and `NewsDetailViewModel` to prevent race conditions during data fetching, such as on swipe-to-refresh or configuration changes. - Integrate the new `logChanges` extension into `NewsHomeViewModel` for easier debugging of state transitions.
1 parent 7b6535c commit 4f89c1b

File tree

4 files changed

+65
-6
lines changed

4 files changed

+65
-6
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.yogiveloper.yonewsai.core.util
2+
3+
import android.util.Log
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.flow.StateFlow
6+
import kotlinx.coroutines.launch
7+
8+
/**
9+
* Extension function to log all emissions from a StateFlow.
10+
* Useful for debugging UI state changes in ViewModels.
11+
*/
12+
fun <T> StateFlow<T>.logChanges(
13+
tag: String,
14+
scope: CoroutineScope
15+
) {
16+
scope.launch {
17+
this@logChanges.collect {
18+
Log.d(tag, "UI state updated: $it")
19+
}
20+
}
21+
}

app/src/main/java/com/yogiveloper/yonewsai/modules/home_news/data/repository/NewsRepositoryImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import javax.inject.Singleton
1010

1111
@Singleton
1212
class NewsRepositoryImpl @Inject constructor(
13-
private val api: NewsApiService
13+
private val api: NewsApiService,
1414
) : NewsRepository {
1515

1616
/**
@@ -36,6 +36,7 @@ class NewsRepositoryImpl @Inject constructor(
3636
* articleCache.putAll(...) is then used to efficiently
3737
* add all the new articles to the cache in one go.
3838
* */
39+
Log.d("NewsRepositoryImpl", "Caching ${domainArticles.size} articles")
3940
articleCache.clear()
4041
articleCache.putAll(domainArticles.associateBy { it.id })
4142

app/src/main/java/com/yogiveloper/yonewsai/modules/home_news/presentation/detail/NewsDetailViewModel.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import androidx.lifecycle.SavedStateHandle
44
import androidx.lifecycle.ViewModel
55
import androidx.lifecycle.viewModelScope
66
import com.yogiveloper.yonewsai.core.util.Resource
7+
import com.yogiveloper.yonewsai.core.util.logChanges
78
import com.yogiveloper.yonewsai.modules.home_news.domain.model.Article
89
import com.yogiveloper.yonewsai.modules.home_news.domain.usecase.GetArticleByIdUseCase
910
import dagger.hilt.android.lifecycle.HiltViewModel
11+
import kotlinx.coroutines.Job
1012
import kotlinx.coroutines.flow.MutableStateFlow
1113
import kotlinx.coroutines.flow.StateFlow
1214
import kotlinx.coroutines.launch
@@ -26,14 +28,23 @@ class NewsDetailViewModel @Inject constructor(
2628
val uiState: StateFlow<NewsDetailUiState> = _uiState
2729

2830
init {
31+
/**
32+
* because currently a data (repo) is on simple cache
33+
* and ID its already on state
34+
* safe to load directly
35+
* */
2936
val articleID = savedStateHandle.get<Int>("articleID")
3037
if(articleID !== null){
3138
fetchArticleByID(articleID)
3239
}
40+
uiState.logChanges("NewsDetailViewModel", viewModelScope)
3341
}
3442

43+
// prevent race-condition ex: on swipe refresh or fast rotate
44+
private var fetchJob: Job? = null
3545
private fun fetchArticleByID(id: Int){
36-
viewModelScope.launch {
46+
fetchJob?.cancel() // cancel old job if any
47+
fetchJob = viewModelScope.launch {
3748
when(val r = getArticleByIdUseCase(id)){
3849
is Resource.Success -> {
3950
_uiState.value = _uiState.value.copy(article = r.data)

app/src/main/java/com/yogiveloper/yonewsai/modules/home_news/presentation/home/NewsHomeViewModel.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.yogiveloper.yonewsai.modules.home_news.presentation.home
22

33
import android.util.Log
4+
import androidx.lifecycle.SavedStateHandle
45
import androidx.lifecycle.ViewModel
56
import androidx.lifecycle.viewModelScope
67
import com.yogiveloper.yonewsai.core.util.Resource
8+
import com.yogiveloper.yonewsai.core.util.logChanges
79
import com.yogiveloper.yonewsai.modules.home_news.domain.model.Article
810
import com.yogiveloper.yonewsai.modules.home_news.domain.usecase.GetTopHeadlinesUseCase
911
import dagger.hilt.android.lifecycle.HiltViewModel
12+
import kotlinx.coroutines.Job
1013
import kotlinx.coroutines.flow.MutableStateFlow
1114
import kotlinx.coroutines.flow.StateFlow
1215
import kotlinx.coroutines.launch
@@ -21,21 +24,44 @@ data class NewsHomeUiState(
2124

2225
@HiltViewModel
2326
class NewsHomeViewModel @Inject constructor(
24-
private val getTopHeadlines: GetTopHeadlinesUseCase
27+
private val getTopHeadlines: GetTopHeadlinesUseCase,
28+
// private val savedStateHandle: SavedStateHandle
2529
) : ViewModel() {
2630
private val _uiState = MutableStateFlow(NewsHomeUiState())
2731
val uiState: StateFlow<NewsHomeUiState> = _uiState
2832

2933
init {
30-
fetchBreakingNews()
34+
uiState.logChanges("NewsHomeViewModel", viewModelScope)
35+
/**
36+
* if API doesn't have cache mechanism should use these approach:
37+
* (else this expensive solution)
38+
* for HomeScreen should save on repo/local cache (Room/DataStore)
39+
* */
40+
41+
/**
42+
* if API doesn't have or have cache mechanism not problem use these approach:
43+
* for DetailScreen should save ID on state
44+
* then getArticleById from repo/local cache (Room/DataStore)
45+
* */
46+
47+
// val cached = savedStateHandle.get<List<Article>>("articles")
48+
// if (cached != null) {
49+
// _uiState.value = _uiState.value.copy(articles = cached)
50+
// } else {
51+
fetchBreakingNews()
52+
// }
53+
3154
}
3255

56+
// prevent race-condition ex: on swipe refresh or fast rotate
57+
private var fetchJob: Job? = null
3358
fun fetchBreakingNews(country: String = "us", category: String = "technology") {
34-
Log.d("fetchBreakingNews", "fetchBreakingNews: called")
35-
viewModelScope.launch {
59+
fetchJob?.cancel() // cancel old job if any
60+
fetchJob = viewModelScope.launch {
3661
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
3762
when(val r = getTopHeadlines(country, category )){
3863
is Resource.Success -> {
64+
// savedStateHandle["articles"] = r.data
3965
_uiState.value = _uiState.value.copy(
4066
isLoading = false,
4167
articles = r.data,

0 commit comments

Comments
 (0)