From 3b9b4126ba9f675624ebddd15cdb8a0298835956 Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Mon, 11 May 2026 13:02:03 +0100 Subject: [PATCH] Workout list: live-refresh on ACTION_NEW_DATA without blink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-running per-line fetches (e.g. ExploreSync's catalog walk) hold the device's busy task for the entire session, so the busy→idle edge WorkoutListActivity refreshes on only fires at session end. Listen for ACTION_NEW_DATA too so each newly imported activity appears as it lands. Add a silent loadSummaries path that skips the isLoading flips, and disable the RecyclerView change animator so the dashboard row's notifyItemChanged(0) doesn't cross-fade — both would otherwise read as a distracting blink during an active sync. --- .../workouts/WorkoutListActivity.kt | 69 +++++++++++++------ .../workouts/WorkoutListViewModel.kt | 28 ++++++-- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListActivity.kt index c5fc593a9e..abd8f01b58 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListActivity.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListActivity.kt @@ -14,6 +14,8 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.core.content.FileProvider import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -52,26 +54,36 @@ class WorkoutListActivity : AbstractListActivity() { private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action != GBDevice.ACTION_DEVICE_CHANGED) { - LOG.warn("Got unexpected action {}", intent.action) - return - } - val device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE) - if (device == null) { - LOG.error("Got device changed without device") - return - } - if (device != gbDevice) { - return - } - if (device.isBusy) { - swipeLayout.isRefreshing = true - } else { - val wasBusy = swipeLayout.isRefreshing - swipeLayout.isRefreshing = false - if (wasBusy) { - refresh() + when (intent.action) { + GBDevice.ACTION_DEVICE_CHANGED -> { + val device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE) + if (device == null) { + LOG.error("Got device changed without device") + return + } + if (device != gbDevice) { + return + } + if (device.isBusy) { + swipeLayout.isRefreshing = true + } else { + val wasBusy = swipeLayout.isRefreshing + swipeLayout.isRefreshing = false + if (wasBusy) { + refresh() + } + } } + // ExploreSync (and other long-running fetches) hold the + // device busy for the whole catalog walk, so the + // busy→idle edge above only fires at session end. + // Refresh on every per-line flush as well so newly + // imported activities appear in the list right away. + // Pass silent=true so the dashboard's loading shimmer + // and swipe-refresh spinner don't blink on every + // incremental update. + GBApplication.ACTION_NEW_DATA -> refresh(silent = true) + else -> LOG.warn("Got unexpected action {}", intent.action) } } } @@ -132,7 +144,10 @@ class WorkoutListActivity : AbstractListActivity() { // Load and apply saved quick filter applySavedQuickFilter() - val filterLocal = IntentFilter(GBDevice.ACTION_DEVICE_CHANGED) + val filterLocal = IntentFilter().apply { + addAction(GBDevice.ACTION_DEVICE_CHANGED) + addAction(GBApplication.ACTION_NEW_DATA) + } LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filterLocal) super.onCreate(savedInstanceState) @@ -195,6 +210,13 @@ class WorkoutListActivity : AbstractListActivity() { setItemAdapter(workoutSummariesAdapter) + // The dashboard row gets a notifyItemChanged on every silent + // refresh during sync; the default ChangeAnimator cross-fades + // that, which reads as a distracting opacity blink. Disable + // just the change animation — add/remove animations still run. + val recycler = findViewById(R.id.itemListView) + (recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false + swipeLayout = findViewById(R.id.list_activity_swipe_layout) swipeLayout.setOnRefreshListener { if (GBApplication.getPrefs().refreshOnSwipe()) { @@ -478,6 +500,10 @@ class WorkoutListActivity : AbstractListActivity() { } override fun refresh() { + refresh(silent = false) + } + + private fun refresh(silent: Boolean) { gbDevice?.let { device -> viewModel.loadSummaries( device, @@ -486,7 +512,8 @@ class WorkoutListActivity : AbstractListActivity() { dateToFilter, nameContainsFilter, deviceFilter, - itemsFilter + itemsFilter, + silent ) } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListViewModel.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListViewModel.kt index 37649b4215..9153dbdd3a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListViewModel.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutListViewModel.kt @@ -42,9 +42,17 @@ class WorkoutListViewModel : ViewModel() { dateToFilter: Long, nameContainsFilter: String?, deviceFilter: Long, - itemsFilter: List? + itemsFilter: List?, + /** When true, skip the [isLoading] / [isDashboardLoading] + * flips so the swipe-refresh spinner and dashboard shimmer + * don't blink. Used by per-line refreshes during + * long-running syncs, where the loading animation + * distracts from the actual content updates. */ + silent: Boolean = false ) { - _isLoading.value = true + if (!silent) { + _isLoading.value = true + } _error.value = null viewModelScope.launch { @@ -70,18 +78,22 @@ class WorkoutListViewModel : ViewModel() { _summaries.value = allSummaries - loadDashboardStats(gbDevice, summaries) + loadDashboardStats(gbDevice, summaries, silent) } catch (e: Exception) { LOG.error("Error loading summaries", e) _error.value = "Error loading summaries: ${e.localizedMessage}" } finally { - _isLoading.value = false + if (!silent) { + _isLoading.value = false + } } } } - private fun loadDashboardStats(gbDevice: GBDevice, summaries: List) { - _isDashboardLoading.value = true + private fun loadDashboardStats(gbDevice: GBDevice, summaries: List, silent: Boolean) { + if (!silent) { + _isDashboardLoading.value = true + } viewModelScope.launch { try { @@ -94,7 +106,9 @@ class WorkoutListViewModel : ViewModel() { LOG.error("Error loading dashboard stats", e) _error.value = "Error loading dashboard stats: ${e.localizedMessage}" } finally { - _isDashboardLoading.value = false + if (!silent) { + _isDashboardLoading.value = false + } } } }