diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index ad71ca28b..42df2ef5f 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -52,6 +52,7 @@ import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.util.anonymize import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import timber.log.Timber import javax.inject.Inject @@ -168,33 +169,21 @@ constructor( val mockDevice = DeviceListEntry.Mock("Demo Mode") val bleDevicesForUi: StateFlow> = - bleDevicesFlow.stateIn(viewModelScope, SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS), emptyList()) + bleDevicesFlow.stateInWhileSubscribed(initialValue = emptyList()) /** UI StateFlow for discovered TCP devices. */ val discoveredTcpDevicesForUi: StateFlow> = - processedDiscoveredTcpDevicesFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS), - listOf(), - ) + processedDiscoveredTcpDevicesFlow.stateInWhileSubscribed(initialValue = listOf()) /** UI StateFlow for recently connected TCP devices that are not currently discovered. */ val recentTcpDevicesForUi: StateFlow> = - filteredRecentTcpDevicesFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS), - listOf(), - ) + filteredRecentTcpDevicesFlow.stateInWhileSubscribed(initialValue = listOf()) val usbDevicesForUi: StateFlow> = combine(usbDevicesFlow, showMockInterface) { usb, showMock -> usb + if (showMock) listOf(mockDevice) else emptyList() } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS), - if (showMockInterface.value) listOf(mockDevice) else emptyList(), - ) + .stateInWhileSubscribed(initialValue = if (showMockInterface.value) listOf(mockDevice) else emptyList()) init { serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope) @@ -217,11 +206,7 @@ constructor( val selectedNotNullFlow: StateFlow = selectedAddressFlow .map { it ?: NO_DEVICE_SELECTED } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS), - selectedAddressFlow.value ?: NO_DEVICE_SELECTED, - ) + .stateInWhileSubscribed(initialValue = selectedAddressFlow.value ?: NO_DEVICE_SELECTED) val scanResult = MutableLiveData>(mutableMapOf()) @@ -385,4 +370,3 @@ constructor( } const val NO_DEVICE_SELECTED = "n" -private const val SHARING_STARTED_TIMEOUT_MS = 5000L diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 58c993d6d..4a0ae5954 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.data.repository.FirmwareReleaseRepository @@ -58,6 +57,7 @@ import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.toSharedContact +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AdminProtos import org.meshtastic.proto.AppOnlyProtos import org.meshtastic.proto.MeshProtos @@ -171,10 +171,7 @@ constructor( get() = serviceRepository.meshService val unreadMessageCount = - packetRepository - .getUnreadCountTotal() - .map { it.coerceAtLeast(0) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), 0) + packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0) // hardware info about our local device (can be null) val myNodeInfo: StateFlow diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index afc4a98a2..1990fa6a0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -18,20 +18,18 @@ package com.geeksville.mesh.ui.connections import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.geeksville.mesh.repository.bluetooth.BluetoothRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.stateIn import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.database.model.Node import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalOnlyProtos.LocalConfig import javax.inject.Inject @@ -46,11 +44,7 @@ constructor( private val uiPrefs: UiPrefs, ) : ViewModel() { val localConfig: StateFlow = - radioConfigRepository.localConfigFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000L), - LocalConfig.getDefaultInstance(), - ) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance()) val connectionState = serviceRepository.connectionState diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt index a46d5fd57..26de6916f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt @@ -24,9 +24,7 @@ import com.geeksville.mesh.model.Contact import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository @@ -37,6 +35,7 @@ import org.meshtastic.core.model.util.getChannel import org.meshtastic.core.model.util.getShortDate import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.channelSet import javax.inject.Inject import kotlin.collections.map @@ -55,12 +54,7 @@ constructor( val connectionState = serviceRepository.connectionState - val channels = - radioConfigRepository.channelSetFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - channelSet {}, - ) + val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {}) val contactList = combine( @@ -117,11 +111,7 @@ constructor( ) } } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + .stateInWhileSubscribed(initialValue = emptyList()) fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST) diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt index ac99e2512..6896a448f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt @@ -23,9 +23,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics @@ -33,6 +31,7 @@ import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.core.proto.getChannelList import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AppOnlyProtos import org.meshtastic.proto.ChannelProtos import org.meshtastic.proto.ConfigProtos.Config @@ -55,18 +54,9 @@ constructor( val connectionState = serviceRepository.connectionState val localConfig = - radioConfigRepository.localConfigFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - LocalConfig.getDefaultInstance(), - ) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance()) - val channels = - radioConfigRepository.channelSetFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - channelSet {}, - ) + val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {}) // managed mode disables all access to configuration val isManaged: Boolean diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt index 0636464bd..23392775e 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeViewModel.kt @@ -21,12 +21,11 @@ import android.os.RemoteException import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.proto.getChannelList import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AppOnlyProtos import org.meshtastic.proto.ChannelProtos import org.meshtastic.proto.ConfigProtos.Config @@ -44,19 +43,10 @@ constructor( private val serviceRepository: ServiceRepository, ) : ViewModel() { - val channels = - radioConfigRepository.channelSetFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000L), - channelSet {}, - ) + val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {}) private val localConfig = - radioConfigRepository.localConfigFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000L), - LocalConfig.getDefaultInstance(), - ) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance()) /** Set the radio config (also updates our saved copy in preferences). */ fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch { diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt index b8d4ef686..5079eac6e 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/share/SharedContactViewModel.kt @@ -20,14 +20,13 @@ package org.meshtastic.core.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.model.Node import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AdminProtos import javax.inject.Inject @@ -40,13 +39,7 @@ constructor( ) : ViewModel() { val unfilteredNodes: StateFlow> = - nodeRepository - .getNodes() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) fun addSharedContact(sharedContact: AdminProtos.SharedContact) = viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/viewmodel/ViewModelExtensions.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/viewmodel/ViewModelExtensions.kt new file mode 100644 index 000000000..c51f8b332 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/viewmodel/ViewModelExtensions.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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 . + */ + +@file:Suppress("Wrapping", "UnusedImports", "SpacingAroundColon") + +package org.meshtastic.core.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Extension for converting a [Flow] to a [StateFlow] in a [ViewModel] context. + * + * @param initialValue the initial value of the state flow + * @param stopTimeout configures a delay between the disappearance of the last subscriber and the stopping of the + * sharing coroutine. + */ +context(viewModel: ViewModel) +fun Flow.stateInWhileSubscribed(initialValue: T, stopTimeout: Duration = 5.seconds): StateFlow = stateIn( + scope = viewModel.viewModelScope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = stopTimeout.inWholeMilliseconds), + initialValue = initialValue, +) diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 79efb3e2f..93e5c03bf 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -17,10 +17,7 @@ package org.meshtastic.feature.map -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository @@ -28,6 +25,7 @@ import org.meshtastic.core.data.repository.RadioConfigRepository import org.meshtastic.core.model.DataPacket import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalOnlyProtos.LocalConfig import javax.inject.Inject @@ -50,11 +48,7 @@ constructor( } val localConfig = - radioConfigRepository.localConfigFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000L), - LocalConfig.getDefaultInstance(), - ) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance()) val config get() = localConfig.value diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 91a11aa3e..088bfb874 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -32,13 +32,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -53,6 +51,7 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.ConfigProtos import timber.log.Timber import java.io.File @@ -97,9 +96,7 @@ constructor( val errorFlow: SharedFlow = _errorFlow.asSharedFlow() val customTileProviderConfigs: StateFlow> = - customTileProviderRepository - .getCustomTileProviders() - .stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList()) + customTileProviderRepository.getCustomTileProviders().stateInWhileSubscribed(initialValue = emptyList()) private val _selectedCustomTileProviderUrl = MutableStateFlow(null) val selectedCustomTileProviderUrl: StateFlow = _selectedCustomTileProviderUrl.asStateFlow() @@ -110,11 +107,7 @@ constructor( val displayUnits = radioConfigRepository.deviceProfileFlow .mapNotNull { it.config.display.units } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC, - ) + .stateInWhileSubscribed(initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC) fun addCustomTileProvider(name: String, urlTemplate: String) { viewModelScope.launch { diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index c4662dccf..a6ce9e322 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -23,12 +23,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.PacketRepository @@ -38,6 +36,7 @@ import org.meshtastic.core.model.DataPacket import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.service.ServiceRepository import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.MeshProtos import timber.log.Timber import java.util.concurrent.TimeUnit @@ -78,11 +77,7 @@ abstract class BaseMapViewModel( nodeRepository .getNodes() .map { nodes -> nodes.filterNot { node -> node.isIgnored } } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + .stateInWhileSubscribed(initialValue = emptyList()) val waypoints: StateFlow> = packetRepository @@ -94,7 +89,7 @@ abstract class BaseMapViewModel( it.data.waypoint!!.expire == 0 || it.data.waypoint!!.expire > System.currentTimeMillis() / 1000 } } - .stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = emptyMap()) + .stateInWhileSubscribed(initialValue = emptyMap()) private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites) @@ -119,9 +114,7 @@ abstract class BaseMapViewModel( val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo val isConnected = - serviceRepository.connectionState - .map { it.isConnected() } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) + serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false) fun toggleOnlyFavorites() { val current = showOnlyFavorites.value @@ -187,9 +180,7 @@ abstract class BaseMapViewModel( ) { favoritesOnly, showWaypoints, showPrecisionCircle, lastHeardFilter, lastHeardTrackFilter -> MapFilterState(favoritesOnly, showWaypoints, showPrecisionCircle, lastHeardFilter, lastHeardTrackFilter) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), + .stateInWhileSubscribed( initialValue = MapFilterState( showOnlyFavorites.value, diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 8965cc94e..4bfc111e5 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -19,16 +19,13 @@ package org.meshtastic.feature.map.node import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.toList import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.data.repository.MeshLogRepository @@ -36,6 +33,7 @@ import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.navigation.NodesRoutes import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.proto.toPosition +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.feature.map.model.CustomTileSource import org.meshtastic.proto.MeshProtos.Position import org.meshtastic.proto.Portnums.PortNum @@ -57,7 +55,7 @@ constructor( nodeRepository.nodeDBbyNum .mapLatest { it[destNum] } .distinctUntilChanged() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), null) + .stateInWhileSubscribed(initialValue = null) val applicationId = buildConfigProvider.applicationId @@ -73,7 +71,7 @@ constructor( } .toList() } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), emptyList()) + .stateInWhileSubscribed(initialValue = emptyList()) val tileSource get() = CustomTileSource.getTileSource(mapPrefs.mapStyle) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index f20817102..04b9bba49 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -23,12 +23,10 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository @@ -43,6 +41,7 @@ import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.service.MeshServiceNotifications import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.channelSet import org.meshtastic.proto.sharedContact import timber.log.Timber @@ -69,40 +68,21 @@ constructor( val connectionState = serviceRepository.connectionState - val nodeList: StateFlow> = - nodeRepository - .getNodes() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + val nodeList: StateFlow> = nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) - val channels = - radioConfigRepository.channelSetFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - channelSet {}, - ) + val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(channelSet {}) private val _showQuickChat = MutableStateFlow(uiPrefs.showQuickChat) val showQuickChat: StateFlow = _showQuickChat - val quickChatActions = - quickChatActionRepository - .getAllActions() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + val quickChatActions = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList()) private val contactKeyForMessages: MutableStateFlow = MutableStateFlow(null) private val messagesForContactKey: StateFlow> = contactKeyForMessages .filterNotNull() .flatMapLatest { contactKey -> packetRepository.getMessagesFrom(contactKey, ::getNode) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + .stateInWhileSubscribed(initialValue = emptyList()) fun setTitle(title: String) { viewModelScope.launch { _title.value = title } diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt index c49cdd6f0..e6f0762c4 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt @@ -21,21 +21,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.QuickChatActionRepository import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import javax.inject.Inject @HiltViewModel class QuickChatViewModel @Inject constructor(private val quickChatActionRepository: QuickChatActionRepository) : ViewModel() { val quickChatActions - get() = - quickChatActionRepository - .getAllActions() - .stateIn(viewModelScope, SharingStarted.Companion.WhileSubscribed(5_000), emptyList()) + get() = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList()) fun updateActionPositions(actions: List) { viewModelScope.launch(Dispatchers.IO) { diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt index 9ebc13190..6ff0f2ba6 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt @@ -24,13 +24,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.data.repository.RadioConfigRepository @@ -39,6 +37,7 @@ import org.meshtastic.core.database.model.NodeSortOption import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.service.ServiceAction import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AdminProtos import timber.log.Timber import javax.inject.Inject @@ -55,19 +54,9 @@ constructor( val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo - val onlineNodeCount = - nodeRepository.onlineNodeCount.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = 0, - ) + val onlineNodeCount = nodeRepository.onlineNodeCount.stateInWhileSubscribed(initialValue = 0) - val totalNodeCount = - nodeRepository.totalNodeCount.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = 0, - ) + val totalNodeCount = nodeRepository.totalNodeCount.stateInWhileSubscribed(initialValue = 0) val connectionState = serviceRepository.connectionState @@ -103,11 +92,7 @@ constructor( tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit, ) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = NodesUiState(), - ) + .stateInWhileSubscribed(initialValue = NodesUiState()) val nodeList: StateFlow> = combine(nodeFilter, nodeSortOption, ::Pair) @@ -122,20 +107,10 @@ constructor( ) .map { list -> list.filter { it.isIgnored == filter.showIgnored } } } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + .stateInWhileSubscribed(initialValue = emptyList()) val unfilteredNodeList: StateFlow> = - nodeRepository - .getNodes() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList()) fun setNodeFilterText(text: String) { nodeFilterText.value = text diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index 75e0f9b3b..7c5394ee8 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -24,14 +24,12 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -47,6 +45,7 @@ import org.meshtastic.core.model.util.positionToMeter import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.service.IMeshService import org.meshtastic.core.service.ServiceRepository +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalOnlyProtos.LocalConfig import org.meshtastic.proto.MeshProtos import org.meshtastic.proto.Portnums @@ -81,16 +80,10 @@ constructor( val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo val isConnected = - serviceRepository.connectionState - .map { it.isConnected() } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), false) + serviceRepository.connectionState.map { it.isConnected() }.stateInWhileSubscribed(initialValue = false) val localConfig: StateFlow = - radioConfigRepository.localConfigFlow.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000L), - LocalConfig.getDefaultInstance(), - ) + radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance()) val meshService: IMeshService? get() = serviceRepository.meshService @@ -105,7 +98,7 @@ constructor( uiPrefs.shouldProvideNodeLocation(myNodeEntity.myNodeNum) } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) + .stateInWhileSubscribed(initialValue = false) private val _excludedModulesUnlocked = MutableStateFlow(false) val excludedModulesUnlocked: StateFlow = _excludedModulesUnlocked.asStateFlow() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index 20ccb5ddd..fabd3a0eb 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -27,16 +27,15 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.data.repository.NodeRepository import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.AdminProtos import org.meshtastic.proto.MeshProtos import org.meshtastic.proto.PaxcountProtos @@ -205,10 +204,7 @@ constructor( ) : ViewModel() { val meshLog: StateFlow> = - meshLogRepository - .getAllLogs() - .map(::toUiState) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), persistentListOf()) + meshLogRepository.getAllLogs().map(::toUiState).stateInWhileSubscribed(initialValue = persistentListOf()) // --- Managers --- val searchManager = LogSearchManager()