From fa6d625d90ee41eedbea9bd3cc2f252b1ed9e80d Mon Sep 17 00:00:00 2001 From: James Rich Date: Wed, 6 May 2026 14:02:03 -0500 Subject: [PATCH] perf: Phase 4 Android performance optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - frequentEmojis: computed List → cached StateFlow (avoids parse+sort on every recomposition) - SdkNodeRepositoryImpl: SharingStarted.Eagerly → WhileSubscribed(5_000) for ourNodeInfo, myId, localStats - PacketRepositoryImpl: deduplicate flatMapLatest chains via shared combine(myNodeNumFlow, currentDb) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../data/repository/PacketRepositoryImpl.kt | 32 +++++++++++-------- .../data/repository/SdkNodeRepositoryImpl.kt | 6 ++-- .../meshtastic/feature/messaging/Message.kt | 2 +- .../feature/messaging/MessageViewModel.kt | 27 +++++++++------- .../feature/messaging/MessageViewModelTest.kt | 3 +- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt index d97699660..22c9a0c57 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/PacketRepositoryImpl.kt @@ -21,6 +21,7 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.map import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -61,12 +62,15 @@ class PacketRepositoryImpl( .map { it?.myNodeNum ?: 0 } .distinctUntilChanged() - override fun getWaypoints(): Flow> = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllWaypointsFlow(num) } } + /** Cached upstream combining myNodeNum + currentDb — avoids creating duplicate flatMapLatest chains. */ + private val numAndDb = combine(myNodeNumFlow, dbManager.currentDb) { num, db -> num to db } + + override fun getWaypoints(): Flow> = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getAllWaypointsFlow(num) } .map { list -> list.map { it.data } } - override fun getContacts(): Flow> = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys(num) } } + override fun getContacts(): Flow> = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getContactKeys(num) } .map { map -> map.mapValues { it.value.data } } override fun getContactsPaged(): Flow> = Pager( @@ -87,17 +91,17 @@ class PacketRepositoryImpl( override suspend fun getUnreadCount(contact: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getUnreadCount(currentMyNodeNum, contact) } - override fun getUnreadCountFlow(contact: String): Flow = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountFlow(num, contact) } } + override fun getUnreadCountFlow(contact: String): Flow = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getUnreadCountFlow(num, contact) } - override fun getFirstUnreadMessageUuid(contact: String): Flow = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFirstUnreadMessageUuid(num, contact) } } + override fun getFirstUnreadMessageUuid(contact: String): Flow = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getFirstUnreadMessageUuid(num, contact) } - override fun hasUnreadMessages(contact: String): Flow = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().hasUnreadMessages(num, contact) } } + override fun hasUnreadMessages(contact: String): Flow = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().hasUnreadMessages(num, contact) } - override fun getUnreadCountTotal(): Flow = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal(num) } } + override fun getUnreadCountTotal(): Flow = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getUnreadCountTotal(num) } override suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(currentMyNodeNum, contact, timestamp) } @@ -463,8 +467,8 @@ class PacketRepositoryImpl( suspend fun updateReaction(reaction: RoomReaction) = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(reaction) } - override fun getFilteredCountFlow(contactKey: String): Flow = myNodeNumFlow - .flatMapLatest { num -> dbManager.currentDb.flatMapLatest { db -> db.packetDao().getFilteredCountFlow(num, contactKey) } } + override fun getFilteredCountFlow(contactKey: String): Flow = numAndDb + .flatMapLatest { (num, db) -> db.packetDao().getFilteredCountFlow(num, contactKey) } override suspend fun getFilteredCount(contactKey: String): Int = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getFilteredCount(currentMyNodeNum, contactKey) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/SdkNodeRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/SdkNodeRepositoryImpl.kt index 1eb28ecde..5914593f8 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/SdkNodeRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/SdkNodeRepositoryImpl.kt @@ -99,15 +99,15 @@ class SdkNodeRepositoryImpl( override val ourNodeInfo: StateFlow = combine(_nodeDBbyNum, _myNodeNum) { db, myNum -> myNum?.let { db[it] } } - .stateIn(scope, SharingStarted.Eagerly, null) + .stateIn(scope, SharingStarted.WhileSubscribed(5_000), null) override val myId: StateFlow = ourNodeInfo.map { it?.user?.id } - .stateIn(scope, SharingStarted.Eagerly, null) + .stateIn(scope, SharingStarted.WhileSubscribed(5_000), null) override val localStats: StateFlow = localStatsDataSource.localStatsFlow - .stateIn(scope, SharingStarted.Eagerly, LocalStats()) + .stateIn(scope, SharingStarted.WhileSubscribed(5_000), LocalStats()) override fun updateLocalStats(stats: LocalStats) { scope.launch { localStatsDataSource.setLocalStats(stats) } diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt index d3c64bfde..a7a6bd9aa 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt @@ -401,7 +401,7 @@ fun MessageScreen( onSendMessage = { text, key -> viewModel.sendMessage(text, key) }, onReply = { message -> replyingToPacketId = message?.packetId }, ), - quickEmojis = viewModel.frequentEmojis, + quickEmojis = viewModel.frequentEmojis.collectAsStateWithLifecycle().value, ) // Show FAB if we can scroll towards the newest messages (index 0). if (listState.canScrollBackward) { diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index 85b746c05..f17aeca5a 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.util.ioDispatcher @@ -105,17 +106,21 @@ class MessageViewModel( } .cachedIn(viewModelScope) - val frequentEmojis: List - get() = - customEmojiPrefs.customEmojiFrequency.value - ?.split(",") - ?.associate { entry -> - entry.split("=", limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].toInt() } ?: ("" to 0) - } - ?.toList() - ?.sortedByDescending { it.second } - ?.map { it.first } - ?.take(6) ?: listOf("👍", "👎", "😂", "🔥", "❤️", "😮") + private val defaultEmojis = listOf("👍", "👎", "😂", "🔥", "❤️", "😮") + + val frequentEmojis: StateFlow> = + customEmojiPrefs.customEmojiFrequency + .map { raw -> + raw?.split(",") + ?.associate { entry -> + entry.split("=", limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].toInt() } ?: ("" to 0) + } + ?.toList() + ?.sortedByDescending { it.second } + ?.map { it.first } + ?.take(6) ?: defaultEmojis + } + .stateInWhileSubscribed(defaultEmojis) val homoglyphEncodingEnabled = homoglyphEncodingPrefs.homoglyphEncodingEnabled diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt index 80877834b..14a3e082f 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt @@ -172,8 +172,7 @@ class MessageViewModelTest { fun testFrequentEmojis() = runTest { customEmojiFrequencyFlow.value = "👍=10,👎=5,😂=20" - // frequentEmojis is a property, not a flow. - val emojis = viewModel.frequentEmojis + val emojis = viewModel.frequentEmojis.value assertEquals(listOf("😂", "👍", "👎"), emojis) }