mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 08:42:01 -04:00
fix(widget): drive updates via debounced state observer (#5185)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
@@ -17,22 +17,48 @@
|
||||
package org.meshtastic.feature.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
|
||||
private const val WIDGET_UPDATE_DEBOUNCE_MS = 500L
|
||||
|
||||
@Single
|
||||
class AndroidAppWidgetUpdater(private val context: Context) : AppWidgetUpdater {
|
||||
class AndroidAppWidgetUpdater(private val context: Context, stateProvider: LocalStatsWidgetStateProvider) :
|
||||
AppWidgetUpdater {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
// Observe state changes and trigger a widget re-render whenever the data changes.
|
||||
// Glance compositions are ephemeral — the widget cannot self-update via collectAsState()
|
||||
// alone, so we must call updateAll() externally to drive re-renders.
|
||||
@OptIn(FlowPreview::class)
|
||||
scope.launch {
|
||||
stateProvider.state
|
||||
.debounce(WIDGET_UPDATE_DEBOUNCE_MS)
|
||||
.distinctUntilChanged { old, new -> old.copy(updateTimeMillis = 0) == new.copy(updateTimeMillis = 0) }
|
||||
.collect { if (hasWidgetInstances()) updateAll() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasWidgetInstances(): Boolean =
|
||||
GlanceAppWidgetManager(context).getGlanceIds(LocalStatsWidget::class.java).isNotEmpty()
|
||||
|
||||
override suspend fun updateAll() {
|
||||
// Kickstart the widget composition.
|
||||
// The widget internally uses collectAsState() and its own sampled StateFlow
|
||||
// to drive updates automatically without excessive IPC and recreation.
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
LocalStatsWidget().updateAll(context)
|
||||
} catch (e: Exception) {
|
||||
co.touchlab.kermit.Logger.e(e) { "Failed to update widgets" }
|
||||
Logger.e(e) { "Failed to update widgets" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,6 @@ data class LocalStatsWidgetUiState(
|
||||
val updateTimeMillis: Long = 0,
|
||||
)
|
||||
|
||||
private const val WIDGET_SUBSCRIPTION_TIMEOUT_MS = 5_000L
|
||||
|
||||
@Single
|
||||
class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepository: ServiceRepository) {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
@@ -100,12 +98,7 @@ class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepos
|
||||
.map { input ->
|
||||
mapToUiState(input.connectionState, input.totalNodes, input.onlineNodes, input.stats, input.localNode)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(WIDGET_SUBSCRIPTION_TIMEOUT_MS),
|
||||
initialValue = LocalStatsWidgetUiState(),
|
||||
)
|
||||
.stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = LocalStatsWidgetUiState())
|
||||
|
||||
private data class StateInput(
|
||||
val connectionState: ConnectionState,
|
||||
|
||||
20
feature/widget/src/main/res/values/strings.xml
Normal file
20
feature/widget/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2025-2026 Meshtastic LLC
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="widget_local_stats_label">Meshtastic</string>
|
||||
</resources>
|
||||
@@ -16,6 +16,7 @@
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:label="@string/widget_local_stats_label"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:previewLayout="@layout/widget_local_stats_preview"
|
||||
android:minWidth="110dp"
|
||||
|
||||
Reference in New Issue
Block a user