fix: prime widget data after saving config
This commit is contained in:
@@ -14,6 +14,8 @@ import com.sub2api.monitor.data.MockSub2ApiRepository
|
||||
import com.sub2api.monitor.sync.WorkManagerRefreshScheduler
|
||||
import com.sub2api.monitor.ui.ConfigScreen
|
||||
import com.sub2api.monitor.ui.ConfigViewModel
|
||||
import com.sub2api.monitor.widget.GlanceWidgetUpdater
|
||||
import com.sub2api.monitor.widget.WidgetStateRepository
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -22,11 +24,15 @@ class MainActivity : ComponentActivity() {
|
||||
val configRepository = remember { DataStoreConfigRepository(applicationContext) }
|
||||
val sub2ApiRepository = remember { MockSub2ApiRepository() }
|
||||
val refreshScheduler = remember { WorkManagerRefreshScheduler(applicationContext) }
|
||||
val widgetStateRepository = remember { WidgetStateRepository(applicationContext) }
|
||||
val widgetUpdater = remember { GlanceWidgetUpdater(applicationContext) }
|
||||
val configViewModel: ConfigViewModel = viewModel(
|
||||
factory = ConfigViewModelFactory(
|
||||
configRepository = configRepository,
|
||||
sub2ApiRepository = sub2ApiRepository,
|
||||
refreshScheduler = refreshScheduler,
|
||||
widgetStateRepository = widgetStateRepository,
|
||||
widgetUpdater = widgetUpdater,
|
||||
),
|
||||
)
|
||||
Sub2ApiMonitorApp(configViewModel)
|
||||
@@ -52,9 +58,17 @@ private class ConfigViewModelFactory(
|
||||
private val configRepository: DataStoreConfigRepository,
|
||||
private val sub2ApiRepository: MockSub2ApiRepository,
|
||||
private val refreshScheduler: WorkManagerRefreshScheduler,
|
||||
private val widgetStateRepository: WidgetStateRepository,
|
||||
private val widgetUpdater: GlanceWidgetUpdater,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ConfigViewModel(configRepository, sub2ApiRepository, refreshScheduler) as T
|
||||
return ConfigViewModel(
|
||||
configRepository,
|
||||
sub2ApiRepository,
|
||||
refreshScheduler,
|
||||
widgetStateRepository,
|
||||
widgetUpdater,
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ class ConfigViewModel(
|
||||
private val configRepository: ConfigRepository,
|
||||
private val sub2ApiRepository: Sub2ApiRepository,
|
||||
private val refreshScheduler: RefreshScheduler,
|
||||
private val widgetSnapshotCache: WidgetSnapshotCache,
|
||||
private val widgetUpdater: WidgetUpdater,
|
||||
) : ViewModel() {
|
||||
var uiState by mutableStateOf(ConfigUiState())
|
||||
private set
|
||||
@@ -80,9 +82,15 @@ class ConfigViewModel(
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isSaving = true, statusMessage = null)
|
||||
val config = uiState.toAppConfig()
|
||||
configRepository.save(config)
|
||||
refreshScheduler.schedule(config.refreshIntervalMinutes)
|
||||
uiState = uiState.copy(isSaving = false, statusMessage = "配置已保存")
|
||||
val result = saveConfigAndPrimeWidget(
|
||||
config = config,
|
||||
configRepository = configRepository,
|
||||
sub2ApiRepository = sub2ApiRepository,
|
||||
refreshScheduler = refreshScheduler,
|
||||
widgetSnapshotCache = widgetSnapshotCache,
|
||||
widgetUpdater = widgetUpdater,
|
||||
)
|
||||
uiState = uiState.copy(isSaving = false, statusMessage = result.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.sub2api.monitor.ui
|
||||
|
||||
import com.sub2api.monitor.data.AppConfig
|
||||
import com.sub2api.monitor.data.ConfigRepository
|
||||
import com.sub2api.monitor.data.Sub2ApiRepository
|
||||
import com.sub2api.monitor.domain.Sub2ApiSnapshot
|
||||
import com.sub2api.monitor.sync.RefreshScheduler
|
||||
|
||||
interface WidgetSnapshotCache {
|
||||
suspend fun saveSuccess(snapshot: Sub2ApiSnapshot)
|
||||
suspend fun saveError(message: String)
|
||||
}
|
||||
|
||||
interface WidgetUpdater {
|
||||
suspend fun updateAll()
|
||||
}
|
||||
|
||||
data class SaveConfigResult(
|
||||
val message: String,
|
||||
)
|
||||
|
||||
suspend fun saveConfigAndPrimeWidget(
|
||||
config: AppConfig,
|
||||
configRepository: ConfigRepository,
|
||||
sub2ApiRepository: Sub2ApiRepository,
|
||||
refreshScheduler: RefreshScheduler,
|
||||
widgetSnapshotCache: WidgetSnapshotCache,
|
||||
widgetUpdater: WidgetUpdater,
|
||||
): SaveConfigResult {
|
||||
configRepository.save(config)
|
||||
refreshScheduler.schedule(config.refreshIntervalMinutes)
|
||||
|
||||
val message = runCatching {
|
||||
sub2ApiRepository.fetchSnapshot()
|
||||
}.fold(
|
||||
onSuccess = { snapshot ->
|
||||
widgetSnapshotCache.saveSuccess(snapshot)
|
||||
"配置已保存,监控数据已刷新"
|
||||
},
|
||||
onFailure = { error ->
|
||||
widgetSnapshotCache.saveError(error.message ?: "首次刷新失败")
|
||||
"配置已保存,首次刷新失败"
|
||||
},
|
||||
)
|
||||
|
||||
widgetUpdater.updateAll()
|
||||
return SaveConfigResult(message)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sub2api.monitor.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import com.sub2api.monitor.ui.WidgetUpdater
|
||||
|
||||
class GlanceWidgetUpdater(
|
||||
private val context: Context,
|
||||
) : WidgetUpdater {
|
||||
override suspend fun updateAll() {
|
||||
Sub2ApiMonitorWidget().updateAll(context)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.sub2api.monitor.domain.Sub2ApiSnapshot
|
||||
import com.sub2api.monitor.ui.WidgetSnapshotCache
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.decodeFromString
|
||||
@@ -16,7 +17,7 @@ private val Context.widgetStateDataStore by preferencesDataStore(name = "sub2api
|
||||
class WidgetStateRepository(
|
||||
private val context: Context,
|
||||
private val json: Json = Json { ignoreUnknownKeys = true },
|
||||
) {
|
||||
) : WidgetSnapshotCache {
|
||||
val lastSuccessfulSnapshot: Flow<Sub2ApiSnapshot?> = context.widgetStateDataStore.data.map { preferences ->
|
||||
preferences[SNAPSHOT]?.let { value ->
|
||||
runCatching { json.decodeFromString<Sub2ApiSnapshot>(value) }.getOrNull()
|
||||
@@ -27,14 +28,14 @@ class WidgetStateRepository(
|
||||
preferences[ERROR]
|
||||
}
|
||||
|
||||
suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) {
|
||||
override suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) {
|
||||
context.widgetStateDataStore.edit { preferences ->
|
||||
preferences[SNAPSHOT] = json.encodeToString(snapshot)
|
||||
preferences.remove(ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveError(message: String) {
|
||||
override suspend fun saveError(message: String) {
|
||||
context.widgetStateDataStore.edit { preferences ->
|
||||
preferences[ERROR] = message
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.sub2api.monitor.ui
|
||||
|
||||
import com.sub2api.monitor.data.AppConfig
|
||||
import com.sub2api.monitor.data.ConfigRepository
|
||||
import com.sub2api.monitor.data.MockSub2ApiRepository
|
||||
import com.sub2api.monitor.data.Sub2ApiRepository
|
||||
import com.sub2api.monitor.domain.Sub2ApiSnapshot
|
||||
import com.sub2api.monitor.sync.RefreshScheduler
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SaveConfigAndPrimeWidgetTest {
|
||||
@Test
|
||||
fun savingConfiguredAppImmediatelyPrimesWidgetSnapshot() = runTest {
|
||||
val configRepository = FakeConfigRepository()
|
||||
val scheduler = FakeRefreshScheduler()
|
||||
val cache = FakeWidgetSnapshotCache()
|
||||
val updater = FakeWidgetUpdater()
|
||||
val config = AppConfig("https://sub2api.example.com", "secret", 30)
|
||||
|
||||
val result = saveConfigAndPrimeWidget(
|
||||
config = config,
|
||||
configRepository = configRepository,
|
||||
sub2ApiRepository = MockSub2ApiRepository(),
|
||||
refreshScheduler = scheduler,
|
||||
widgetSnapshotCache = cache,
|
||||
widgetUpdater = updater,
|
||||
)
|
||||
|
||||
assertEquals(config, configRepository.savedConfig)
|
||||
assertEquals(30, scheduler.scheduledInterval)
|
||||
assertNotNull(cache.savedSnapshot)
|
||||
assertEquals(null, cache.savedError)
|
||||
assertTrue(updater.updated)
|
||||
assertEquals("配置已保存,监控数据已刷新", result.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedInitialRefreshStoresErrorAndStillSavesConfiguration() = runTest {
|
||||
val configRepository = FakeConfigRepository()
|
||||
val scheduler = FakeRefreshScheduler()
|
||||
val cache = FakeWidgetSnapshotCache()
|
||||
val updater = FakeWidgetUpdater()
|
||||
val config = AppConfig("https://sub2api.example.com", "secret", 30)
|
||||
|
||||
val result = saveConfigAndPrimeWidget(
|
||||
config = config,
|
||||
configRepository = configRepository,
|
||||
sub2ApiRepository = FailingSub2ApiRepository(),
|
||||
refreshScheduler = scheduler,
|
||||
widgetSnapshotCache = cache,
|
||||
widgetUpdater = updater,
|
||||
)
|
||||
|
||||
assertEquals(config, configRepository.savedConfig)
|
||||
assertEquals(30, scheduler.scheduledInterval)
|
||||
assertEquals(null, cache.savedSnapshot)
|
||||
assertEquals("mock failure", cache.savedError)
|
||||
assertTrue(updater.updated)
|
||||
assertEquals("配置已保存,首次刷新失败", result.message)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeConfigRepository : ConfigRepository {
|
||||
private val state = MutableStateFlow(AppConfig())
|
||||
var savedConfig: AppConfig? = null
|
||||
|
||||
override val config: Flow<AppConfig> = state
|
||||
|
||||
override suspend fun save(config: AppConfig) {
|
||||
savedConfig = config
|
||||
state.value = config
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeRefreshScheduler : RefreshScheduler {
|
||||
var scheduledInterval: Int? = null
|
||||
|
||||
override fun schedule(intervalMinutes: Int) {
|
||||
scheduledInterval = intervalMinutes
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeWidgetSnapshotCache : WidgetSnapshotCache {
|
||||
var savedSnapshot: Sub2ApiSnapshot? = null
|
||||
var savedError: String? = null
|
||||
|
||||
override suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) {
|
||||
savedSnapshot = snapshot
|
||||
savedError = null
|
||||
}
|
||||
|
||||
override suspend fun saveError(message: String) {
|
||||
savedError = message
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeWidgetUpdater : WidgetUpdater {
|
||||
var updated = false
|
||||
|
||||
override suspend fun updateAll() {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
private class FailingSub2ApiRepository : Sub2ApiRepository {
|
||||
override suspend fun fetchSnapshot(): Sub2ApiSnapshot {
|
||||
error("mock failure")
|
||||
}
|
||||
|
||||
override suspend fun testConnection(config: AppConfig): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
Reference in New Issue
Block a user