feat(core/ui): add safeLaunch, UiState, KMP permissions, and CMP lifecycle modernization (#5118)

This commit is contained in:
James Rich 2026-04-13 19:45:34 -05:00 committed by GitHub
parent 27367e9064
commit e46a8296cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 374 additions and 184 deletions

View file

@ -18,7 +18,6 @@ package org.meshtastic.feature.node.compass
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -26,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.bearing
import org.meshtastic.core.common.util.formatString
@ -37,6 +35,7 @@ import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.ui.component.precisionBitsToMeters
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.proto.Config
import org.meshtastic.proto.Position
import kotlin.math.abs
@ -92,13 +91,17 @@ class CompassViewModel(
updatesJob?.cancel()
updatesJob = viewModelScope.launch {
combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) { heading, location ->
buildState(heading, location)
updatesJob =
safeLaunch(tag = "compassUpdates") {
combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) {
heading,
location,
->
buildState(heading, location)
}
.flowOn(dispatchers.default)
.collect { _uiState.value = it }
}
.flowOn(dispatchers.default)
.collect { _uiState.value = it }
}
}
fun stop() {

View file

@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import okio.ByteString.Companion.decodeBase64
@ -60,6 +59,7 @@ import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.view_on_map
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
@ -181,7 +181,8 @@ open class MetricsViewModel(
fun getUser(nodeNum: Int) = nodeRepository.getUser(nodeNum)
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) { meshLogRepository.deleteLog(uuid) }
fun deleteLog(uuid: String) =
safeLaunch(context = dispatchers.io, tag = "deleteLog") { meshLogRepository.deleteLog(uuid) }
fun getTracerouteOverlay(requestId: Int): TracerouteOverlay? {
val cached = tracerouteOverlayCache.value[requestId]
@ -216,7 +217,7 @@ open class MetricsViewModel(
private fun List<Node>.numSet(): Set<Int> = map { it.num }.toSet()
init {
viewModelScope.launch {
safeLaunch(tag = "tracerouteCollector") {
serviceRepository.tracerouteResponse.filterNotNull().collect { response ->
val overlay =
TracerouteOverlay(
@ -232,7 +233,7 @@ open class MetricsViewModel(
Logger.d { "MetricsViewModel created" }
}
fun clearPosition() = viewModelScope.launch(dispatchers.io) {
fun clearPosition() = safeLaunch(context = dispatchers.io, tag = "clearPosition") {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value)
}
@ -276,7 +277,7 @@ open class MetricsViewModel(
overlay: TracerouteOverlay?,
onViewOnMap: (Int, String) -> Unit,
) {
viewModelScope.launch {
safeLaunch(tag = "showTracerouteDetail") {
val snapshotPositions = tracerouteSnapshotRepository.getSnapshotPositions(responseLogUuid).first()
alertManager.showAlert(
titleRes = Res.string.traceroute,
@ -299,7 +300,7 @@ open class MetricsViewModel(
if (errorRes != null) {
// Post the error alert after the current alert is dismissed to avoid
// the wrapping dismissAlert() in AlertManager immediately clearing it.
viewModelScope.launch {
safeLaunch(tag = "tracerouteError") {
alertManager.showAlert(titleRes = Res.string.traceroute, messageRes = errorRes)
}
} else {
@ -336,7 +337,7 @@ open class MetricsViewModel(
epochSeconds: (T) -> Long,
rowMapper: (T) -> String,
) {
viewModelScope.launch(dispatchers.io) {
safeLaunch(context = dispatchers.io, tag = "exportCsv") {
fileService.write(uri) { sink ->
sink.writeUtf8(header)
rows.forEach { item ->