Skip to content

Commit f7e9b3a

Browse files
committed
refactor: code improvements
1 parent 3fd01a2 commit f7e9b3a

File tree

5 files changed

+208
-59
lines changed

5 files changed

+208
-59
lines changed

app/src/main/java/to/bitkit/data/WidgetsStore.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.datastore.core.DataStoreFactory
66
import androidx.datastore.dataStoreFile
77
import dagger.hilt.android.qualifiers.ApplicationContext
88
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.map
910
import kotlinx.serialization.Serializable
1011
import to.bitkit.data.dto.ArticleDTO
1112
import to.bitkit.data.serializers.WidgetsSerializer
@@ -23,10 +24,7 @@ class WidgetsStore @Inject constructor(
2324
)
2425

2526
val data: Flow<WidgetsData> = store.data
26-
27-
suspend fun update(transform: (WidgetsData) -> WidgetsData) {
28-
store.updateData(transform)
29-
}
27+
val articlesFlow: Flow<List<ArticleDTO>> = data.map { it.articles }
3028

3129
suspend fun updateArticles(articles: List<ArticleDTO>) {
3230
store.updateData {

app/src/main/java/to/bitkit/data/widgets/NewsService.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,29 @@ import io.ktor.client.statement.HttpResponse
77
import io.ktor.http.isSuccess
88
import to.bitkit.data.dto.ArticleDTO
99
import to.bitkit.env.Env
10+
import to.bitkit.models.WidgetType
1011
import to.bitkit.utils.AppError
1112
import to.bitkit.utils.Logger
1213
import javax.inject.Inject
1314
import javax.inject.Singleton
15+
import kotlin.time.Duration.Companion.minutes
1416

1517
@Singleton
1618
class NewsService @Inject constructor(
1719
private val client: HttpClient,
18-
) {
19-
20-
/**
21-
* Fetches articles from the news API
22-
* @return List of articles
23-
* @throws NewsError on network or parsing errors
24-
*/
25-
suspend fun fetchLatestNews(): List<ArticleDTO> {
26-
return get<List<ArticleDTO>>(Env.newsBaseUrl + "/articles")
20+
) : WidgetService<List<ArticleDTO>> {
21+
22+
23+
override val widgetType = WidgetType.NEWS
24+
override val refreshInterval = 10.minutes
25+
26+
override suspend fun fetchData(): Result<List<ArticleDTO>> = runCatching {
27+
get<List<ArticleDTO>>(Env.newsBaseUrl + "/articles").take(10)
28+
}.onFailure {
29+
Logger.warn(e = it, msg = "Failed to fetch news", context = TAG)
2730
}
2831

32+
// Future services can be added here
2933
private suspend inline fun <reified T> get(url: String): T {
3034
val response: HttpResponse = client.get(url)
3135
Logger.debug("Http call: $response")
@@ -36,10 +40,13 @@ class NewsService @Inject constructor(
3640
}
3741
responseBody
3842
}
39-
4043
else -> throw NewsError.InvalidResponse(response.status.description)
4144
}
4245
}
46+
47+
companion object {
48+
private const val TAG = "NewsService"
49+
}
4350
}
4451

4552
/**
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package to.bitkit.data.widgets
2+
3+
import to.bitkit.models.WidgetType
4+
5+
interface WidgetService<T> {
6+
val widgetType: WidgetType
7+
suspend fun fetchData(): Result<T>
8+
val refreshInterval: kotlin.time.Duration
9+
}

app/src/main/java/to/bitkit/repositories/WidgetsRepo.kt

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
package to.bitkit.repositories
22

33
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.SupervisorJob
46
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.coroutines.flow.MutableStateFlow
9+
import kotlinx.coroutines.flow.StateFlow
10+
import kotlinx.coroutines.flow.asStateFlow
11+
import kotlinx.coroutines.flow.map
12+
import kotlinx.coroutines.flow.update
13+
import kotlinx.coroutines.launch
514
import kotlinx.coroutines.withContext
615
import to.bitkit.data.WidgetsStore
716
import to.bitkit.data.widgets.NewsService
17+
import to.bitkit.data.widgets.WidgetService
818
import to.bitkit.di.BgDispatcher
19+
import to.bitkit.models.WidgetType
920
import to.bitkit.utils.Logger
1021
import javax.inject.Inject
1122
import javax.inject.Singleton
@@ -17,25 +28,122 @@ class WidgetsRepo @Inject constructor(
1728
private val newsService: NewsService,
1829
private val widgetsStore: WidgetsStore
1930
) {
20-
private val refreshInterval = 10.minutes
31+
private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob())
2132

22-
val articlesFlow = widgetsStore.data
33+
val widgetsDataFlow = widgetsStore.data
34+
val articlesFlow = widgetsStore.articlesFlow
2335

24-
suspend fun updateArticlesInLoop() {
25-
updateArticles()
26-
delay(refreshInterval)
27-
updateArticlesInLoop()
36+
private val _refreshStates = MutableStateFlow(
37+
WidgetType.entries.associateWith { false }
38+
)
39+
val refreshStates: StateFlow<Map<WidgetType, Boolean>> = _refreshStates.asStateFlow()
40+
41+
init {
42+
startPeriodicUpdates()
2843
}
2944

30-
suspend fun updateArticles(): Result<Unit> = withContext(bgDispatcher) {
31-
return@withContext try {
32-
val news = newsService.fetchLatestNews().take(10)
33-
widgetsStore.updateArticles(news)
45+
/**
46+
* Start periodic updates for all widgets
47+
*/
48+
private fun startPeriodicUpdates() {
49+
startPeriodicUpdate(newsService) { articles ->
50+
widgetsStore.updateArticles(articles)
51+
}
52+
}
53+
54+
/**
55+
* Generic method to start periodic updates for any widget service
56+
*/
57+
private fun <T> startPeriodicUpdate(
58+
service: WidgetService<T>,
59+
updateStore: suspend (T) -> Unit
60+
) {
61+
repoScope.launch {
62+
while (true) {
63+
updateWidget(service, updateStore)
64+
delay(service.refreshInterval)
65+
}
66+
}
67+
}
68+
69+
/**
70+
* Update a specific widget type
71+
*/
72+
private suspend fun <T> updateWidget(
73+
service: WidgetService<T>,
74+
updateStore: suspend (T) -> Unit
75+
) {
76+
val widgetType = service.widgetType
77+
_refreshStates.update { it + (widgetType to true) }
78+
79+
service.fetchData()
80+
.onSuccess { data ->
81+
updateStore(data)
82+
Logger.debug("Updated $widgetType widget successfully")
83+
}
84+
.onFailure { error ->
85+
Logger.warn(e = error, msg = "Failed to update $widgetType widget", context = TAG)
86+
}
87+
88+
_refreshStates.update { it + (widgetType to false) }
89+
}
90+
91+
/**
92+
* Manually refresh all widgets
93+
*/
94+
suspend fun refreshAllWidgets(): Result<Unit> = runCatching {
95+
listOf(
96+
updateWidget(newsService) { articles ->
97+
widgetsStore.updateArticles(articles)
98+
},
99+
)
100+
}
101+
102+
/**
103+
* Manually refresh specific widget
104+
*/
105+
suspend fun refreshWidget(widgetType: WidgetType): Result<Unit> = runCatching {
106+
when (widgetType) {
107+
WidgetType.NEWS -> updateWidget(newsService) { articles ->
108+
widgetsStore.updateArticles(articles)
109+
}
110+
WidgetType.WEATHER -> {
111+
// TODO: Implement when PriceService is ready
112+
throw NotImplementedError("Weather widget not implemented yet")
113+
}
114+
WidgetType.PRICE -> {
115+
// TODO: Implement when PriceService is ready
116+
throw NotImplementedError("Price widget not implemented yet")
117+
}
118+
WidgetType.BLOCK -> {
119+
// TODO: Implement when BlockService is ready
120+
throw NotImplementedError("Block widget not implemented yet")
121+
}
122+
WidgetType.CALCULATOR -> {
123+
// TODO: Implement when CalculatorService is ready
124+
throw NotImplementedError("Calculator widget not implemented yet")
125+
}
126+
WidgetType.FACTS -> {
127+
// TODO: Implement when FactsService is ready
128+
throw NotImplementedError("Facts widget not implemented yet")
129+
}
130+
}
131+
}
132+
133+
/**
134+
* Get refresh state for a specific widget type
135+
*/
136+
fun getRefreshState(widgetType: WidgetType): Flow<Boolean> {
137+
return refreshStates.map { it[widgetType] ?: false }
138+
}
34139

35-
Result.success(Unit)
36-
} catch (e: Exception) {
37-
Logger.warn(e = e, msg = e.message, context = TAG)
38-
Result.failure(e)
140+
/**
141+
* Check if a widget type is currently supported
142+
*/
143+
fun isWidgetSupported(widgetType: WidgetType): Boolean {
144+
return when (widgetType) {
145+
WidgetType.NEWS, WidgetType.WEATHER -> true
146+
else -> false
39147
}
40148
}
41149

app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import dagger.hilt.android.lifecycle.HiltViewModel
66
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.flow.Flow
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.SharingStarted
910
import kotlinx.coroutines.flow.StateFlow
1011
import kotlinx.coroutines.flow.asStateFlow
1112
import kotlinx.coroutines.flow.combine
1213
import kotlinx.coroutines.flow.first
14+
import kotlinx.coroutines.flow.flow
1315
import kotlinx.coroutines.flow.map
16+
import kotlinx.coroutines.flow.shareIn
1417
import kotlinx.coroutines.flow.stateIn
1518
import kotlinx.coroutines.flow.update
1619
import kotlinx.coroutines.launch
1720
import to.bitkit.data.AppStorage
1821
import to.bitkit.data.SettingsStore
1922
import to.bitkit.models.Suggestion
23+
import to.bitkit.models.WidgetType
2024
import to.bitkit.models.toSuggestionOrNull
2125
import to.bitkit.models.widget.ArticleModel
2226
import to.bitkit.models.widget.toArticleModel
@@ -33,20 +37,70 @@ class HomeViewModel @Inject constructor(
3337
private val settingsStore: SettingsStore,
3438
) : ViewModel() {
3539

40+
// Suggestions flow
3641
val suggestions: StateFlow<List<Suggestion>> = createSuggestionsFlow()
37-
private val articles: StateFlow<List<ArticleModel>> = createArticlesFlow()
38-
private val _currentArticle = MutableStateFlow(articles.value.firstOrNull())
39-
val currentArticle: StateFlow<ArticleModel?> = _currentArticle.asStateFlow()
42+
43+
// Widget-related flows
4044
private val showWidgets = settingsStore.data.map { it.showWidgets }
4145

46+
private val articles: StateFlow<List<ArticleModel>> = widgetsRepo.articlesFlow
47+
.map { articles -> articles.map { it.toArticleModel() } }
48+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
49+
50+
// Current article state (with rotation)
51+
private val _currentArticle = MutableStateFlow<ArticleModel?>(null)
52+
val currentArticle: StateFlow<ArticleModel?> = _currentArticle.asStateFlow()
53+
54+
// Widget refresh states
55+
val widgetRefreshStates = widgetsRepo.refreshStates
56+
4257
init {
43-
setupWidgets()
58+
setupArticleRotation()
4459
}
4560

61+
private fun setupArticleRotation() {
62+
viewModelScope.launch {
63+
combine(
64+
articles,
65+
showWidgets
66+
) { articlesList, showWidgets ->
67+
Pair(articlesList, showWidgets)
68+
}.collect { (articlesList, showWidgets) ->
69+
if (showWidgets && articlesList.isNotEmpty()) {
70+
startArticleRotation(articlesList)
71+
} else {
72+
_currentArticle.value = null
73+
}
74+
}
75+
}
76+
}
77+
78+
private suspend fun startArticleRotation(articlesList: List<ArticleModel>) {
79+
while (showWidgets.first() && articlesList.isNotEmpty()) {
80+
_currentArticle.value = articlesList.randomOrNull()
81+
delay(30.seconds)
82+
}
83+
_currentArticle.value = null
84+
}
85+
86+
87+
4688
fun removeSuggestion(suggestion: Suggestion) {
4789
appStorage.addSuggestionToRemovedList(suggestion)
4890
}
4991

92+
fun refreshWidgets() {
93+
viewModelScope.launch {
94+
widgetsRepo.refreshAllWidgets()
95+
}
96+
}
97+
98+
fun refreshSpecificWidget(widgetType: WidgetType) {
99+
viewModelScope.launch {
100+
widgetsRepo.refreshWidget(widgetType)
101+
}
102+
}
103+
50104
private fun createSuggestionsFlow(): StateFlow<List<Suggestion>> {
51105
val removedSuggestions = appStorage.removedSuggestionsFlow
52106
.map { stringList -> stringList.mapNotNull { it.toSuggestionOrNull() } }
@@ -99,31 +153,4 @@ class HomeViewModel @Inject constructor(
99153
return@combine baseSuggestions.filterNot { it in removedList }
100154
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
101155
}
102-
103-
private fun createArticlesFlow(): StateFlow<List<ArticleModel>> {
104-
val articles = widgetsRepo.articlesFlow.map { it.articles.map { article -> article.toArticleModel() } }
105-
return articles.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
106-
}
107-
108-
private fun setupWidgets() {
109-
viewModelScope.launch {
110-
showWidgets.collect { show ->
111-
if (show) {
112-
widgetsRepo.updateArticles()
113-
articles.first { it.isNotEmpty() }
114-
getRandomArticle()
115-
} else {
116-
_currentArticle.update { null }
117-
}
118-
}
119-
}
120-
}
121-
122-
private fun getRandomArticle() {
123-
viewModelScope.launch {
124-
_currentArticle.update { articles.value.randomOrNull() }
125-
delay(30.seconds)
126-
if (showWidgets.first()) getRandomArticle()
127-
}
128-
}
129156
}

0 commit comments

Comments
 (0)