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.sync.WorkManagerRefreshScheduler
|
||||||
import com.sub2api.monitor.ui.ConfigScreen
|
import com.sub2api.monitor.ui.ConfigScreen
|
||||||
import com.sub2api.monitor.ui.ConfigViewModel
|
import com.sub2api.monitor.ui.ConfigViewModel
|
||||||
|
import com.sub2api.monitor.widget.GlanceWidgetUpdater
|
||||||
|
import com.sub2api.monitor.widget.WidgetStateRepository
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -22,11 +24,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
val configRepository = remember { DataStoreConfigRepository(applicationContext) }
|
val configRepository = remember { DataStoreConfigRepository(applicationContext) }
|
||||||
val sub2ApiRepository = remember { MockSub2ApiRepository() }
|
val sub2ApiRepository = remember { MockSub2ApiRepository() }
|
||||||
val refreshScheduler = remember { WorkManagerRefreshScheduler(applicationContext) }
|
val refreshScheduler = remember { WorkManagerRefreshScheduler(applicationContext) }
|
||||||
|
val widgetStateRepository = remember { WidgetStateRepository(applicationContext) }
|
||||||
|
val widgetUpdater = remember { GlanceWidgetUpdater(applicationContext) }
|
||||||
val configViewModel: ConfigViewModel = viewModel(
|
val configViewModel: ConfigViewModel = viewModel(
|
||||||
factory = ConfigViewModelFactory(
|
factory = ConfigViewModelFactory(
|
||||||
configRepository = configRepository,
|
configRepository = configRepository,
|
||||||
sub2ApiRepository = sub2ApiRepository,
|
sub2ApiRepository = sub2ApiRepository,
|
||||||
refreshScheduler = refreshScheduler,
|
refreshScheduler = refreshScheduler,
|
||||||
|
widgetStateRepository = widgetStateRepository,
|
||||||
|
widgetUpdater = widgetUpdater,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Sub2ApiMonitorApp(configViewModel)
|
Sub2ApiMonitorApp(configViewModel)
|
||||||
@@ -52,9 +58,17 @@ private class ConfigViewModelFactory(
|
|||||||
private val configRepository: DataStoreConfigRepository,
|
private val configRepository: DataStoreConfigRepository,
|
||||||
private val sub2ApiRepository: MockSub2ApiRepository,
|
private val sub2ApiRepository: MockSub2ApiRepository,
|
||||||
private val refreshScheduler: WorkManagerRefreshScheduler,
|
private val refreshScheduler: WorkManagerRefreshScheduler,
|
||||||
|
private val widgetStateRepository: WidgetStateRepository,
|
||||||
|
private val widgetUpdater: GlanceWidgetUpdater,
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
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 configRepository: ConfigRepository,
|
||||||
private val sub2ApiRepository: Sub2ApiRepository,
|
private val sub2ApiRepository: Sub2ApiRepository,
|
||||||
private val refreshScheduler: RefreshScheduler,
|
private val refreshScheduler: RefreshScheduler,
|
||||||
|
private val widgetSnapshotCache: WidgetSnapshotCache,
|
||||||
|
private val widgetUpdater: WidgetUpdater,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var uiState by mutableStateOf(ConfigUiState())
|
var uiState by mutableStateOf(ConfigUiState())
|
||||||
private set
|
private set
|
||||||
@@ -80,9 +82,15 @@ class ConfigViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiState = uiState.copy(isSaving = true, statusMessage = null)
|
uiState = uiState.copy(isSaving = true, statusMessage = null)
|
||||||
val config = uiState.toAppConfig()
|
val config = uiState.toAppConfig()
|
||||||
configRepository.save(config)
|
val result = saveConfigAndPrimeWidget(
|
||||||
refreshScheduler.schedule(config.refreshIntervalMinutes)
|
config = config,
|
||||||
uiState = uiState.copy(isSaving = false, statusMessage = "配置已保存")
|
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.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.sub2api.monitor.domain.Sub2ApiSnapshot
|
import com.sub2api.monitor.domain.Sub2ApiSnapshot
|
||||||
|
import com.sub2api.monitor.ui.WidgetSnapshotCache
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
@@ -16,7 +17,7 @@ private val Context.widgetStateDataStore by preferencesDataStore(name = "sub2api
|
|||||||
class WidgetStateRepository(
|
class WidgetStateRepository(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val json: Json = Json { ignoreUnknownKeys = true },
|
private val json: Json = Json { ignoreUnknownKeys = true },
|
||||||
) {
|
) : WidgetSnapshotCache {
|
||||||
val lastSuccessfulSnapshot: Flow<Sub2ApiSnapshot?> = context.widgetStateDataStore.data.map { preferences ->
|
val lastSuccessfulSnapshot: Flow<Sub2ApiSnapshot?> = context.widgetStateDataStore.data.map { preferences ->
|
||||||
preferences[SNAPSHOT]?.let { value ->
|
preferences[SNAPSHOT]?.let { value ->
|
||||||
runCatching { json.decodeFromString<Sub2ApiSnapshot>(value) }.getOrNull()
|
runCatching { json.decodeFromString<Sub2ApiSnapshot>(value) }.getOrNull()
|
||||||
@@ -27,14 +28,14 @@ class WidgetStateRepository(
|
|||||||
preferences[ERROR]
|
preferences[ERROR]
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) {
|
override suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) {
|
||||||
context.widgetStateDataStore.edit { preferences ->
|
context.widgetStateDataStore.edit { preferences ->
|
||||||
preferences[SNAPSHOT] = json.encodeToString(snapshot)
|
preferences[SNAPSHOT] = json.encodeToString(snapshot)
|
||||||
preferences.remove(ERROR)
|
preferences.remove(ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveError(message: String) {
|
override suspend fun saveError(message: String) {
|
||||||
context.widgetStateDataStore.edit { preferences ->
|
context.widgetStateDataStore.edit { preferences ->
|
||||||
preferences[ERROR] = message
|
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