Use ImmutableList for request list parameters through the composable chain

Plain List<T> has runtime-determined stability in Compose, preventing
compile-time skip optimizations. Switching to ImmutableList<T> (which
is @Immutable-annotated) lets the compiler guarantee that composables
receiving unchanged list references can be safely skipped.

Also fix redundant smart-cast in BunkerMultiEventHomeScreen.

https://claude.ai/code/session_01PRSKjmFms1Tj1QJ5FhfqZm
This commit is contained in:
Claude
2026-03-20 12:46:57 +00:00
parent 3b66e5b3c1
commit 91aadf89ae
13 changed files with 41 additions and 25 deletions
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import java.time.Duration
import java.time.Instant
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -110,7 +111,7 @@ class MainActivity : AppCompatActivity() {
CircularProgressIndicator()
}
} else {
val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(emptyList())
val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(persistentListOf())
val npub = remember { mainViewModel.getAccount(intent?.getStringExtra("current_user")) }
val accountStateViewModel: AccountStateViewModel =
@@ -16,6 +16,9 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,21 +27,21 @@ import kotlinx.coroutines.launch
@SuppressLint("StaticFieldLeak")
class MainViewModel(val context: Context) : ViewModel() {
private val _intents = MutableStateFlow<List<IntentData>>(listOf())
private val _intents = MutableStateFlow<ImmutableList<IntentData>>(persistentListOf())
val intents = _intents.asStateFlow()
var navController: NavHostController? = null
fun addAll(list: List<IntentData>) {
val newList = list.filter { !intents.value.contains(it) }
_intents.value += newList
_intents.value = (_intents.value + newList).toPersistentList()
}
fun removeAll(intents: List<IntentData>) {
_intents.value -= intents.toSet()
_intents.value = (_intents.value - intents.toSet()).toPersistentList()
}
fun clear() {
_intents.value = emptyList()
_intents.value = persistentListOf()
}
fun getAccount(userFromIntent: String?): String? {
@@ -43,6 +43,7 @@ import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.NostrSignerTheme
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -126,7 +127,7 @@ class SignerActivity : AppCompatActivity() {
CircularProgressIndicator()
}
} else {
val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(emptyList())
val bunkerRequests = BunkerRequestUtils.state.collectAsStateWithLifecycle(persistentListOf())
val npub = remember { mainViewModel.getAccount(intent?.getStringExtra("current_user")) }
val accountStateViewModel: AccountStateViewModel =
@@ -28,25 +28,28 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.toSet
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
object BunkerRequestUtils {
val state = MutableStateFlow(listOf<AmberBunkerRequest>())
val state = MutableStateFlow<ImmutableList<AmberBunkerRequest>>(persistentListOf())
fun addRequest(request: AmberBunkerRequest) {
if (state.value.any { it.request.id == request.request.id }) return
state.tryEmit(state.value + request)
state.tryEmit((state.value + request).toPersistentList())
}
fun clearRequests() {
state.tryEmit(emptyList())
state.tryEmit(persistentListOf())
}
fun remove(id: String) {
state.tryEmit(state.value.filter { it.request.id != id })
state.tryEmit(state.value.filter { it.request.id != id }.toPersistentList())
}
fun getBunkerRequests(): List<AmberBunkerRequest> = state.value
@@ -38,6 +38,8 @@ import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
import com.greenart7c3.nostrsigner.models.IntentResultType
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.ui.navigation.Route
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@SuppressLint("StateFlowValueCalledInComposition", "UnrememberedMutableState")
@Composable
@@ -47,7 +49,7 @@ fun AccountScreen(
packageName: String?,
appName: String?,
mainViewModel: MainViewModel,
bunkerRequests: List<AmberBunkerRequest>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
navController: NavHostController,
isExternalRequest: Boolean = false,
) {
@@ -66,7 +68,7 @@ fun AccountScreen(
MainLoginPage(accountStateViewModel, newNavController)
}
is AccountState.LoggedIn -> {
val intents by mainViewModel.intents.collectAsState(initial = emptyList())
val intents by mainViewModel.intents.collectAsState(initial = persistentListOf())
LaunchedEffect(intent) {
intent?.let {
IntentUtils.getIntentData(
@@ -26,12 +26,13 @@ import com.greenart7c3.nostrsigner.models.IntentResultType
import com.greenart7c3.nostrsigner.ui.components.BunkerSingleEventHomeScreen
import com.greenart7c3.nostrsigner.ui.components.IntentSingleEventHomeScreen
import com.greenart7c3.nostrsigner.ui.components.MultiEventHomeScreen
import kotlinx.collections.immutable.ImmutableList
@Composable
fun IncomingRequestScreen(
modifier: Modifier,
intents: List<IntentData>,
bunkerRequests: List<AmberBunkerRequest>,
intents: ImmutableList<IntentData>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
packageName: String?,
applicationName: String?,
account: Account,
@@ -81,6 +81,7 @@ import com.greenart7c3.nostrsigner.ui.components.AmberFloatingButton
import com.greenart7c3.nostrsigner.ui.components.AmberTopAppBar
import com.greenart7c3.nostrsigner.ui.navigation.Route
import java.util.Base64
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -131,8 +132,8 @@ fun requestIgnoreBatteryOptimizations(context: Context) {
fun MainScreen(
account: Account,
accountStateViewModel: AccountStateViewModel,
intents: List<IntentData>,
bunkerRequests: List<AmberBunkerRequest>,
intents: ImmutableList<IntentData>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
packageName: String?,
appName: String?,
route: MutableState<String?>,
@@ -50,6 +50,7 @@ import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.navigation.routes
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
import java.util.Base64
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -63,8 +64,8 @@ fun AmberTopAppBar(
context: Context,
navBackStackEntry: NavBackStackEntry?,
account: Account,
intents: List<IntentData>,
bunkerRequests: List<AmberBunkerRequest>,
intents: ImmutableList<IntentData>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
packageName: String?,
) {
if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
@@ -64,19 +64,20 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.forEach
import kotlin.collections.set
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun BunkerMultiEventHomeScreen(
modifier: Modifier,
bunkerRequests: List<AmberBunkerRequest>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
packageName: String?,
accountParam: Account,
onLoading: (Boolean) -> Unit,
) {
val context = LocalContext.current
val hasRelayAuthEvents = bunkerRequests.any { it.request is BunkerRequestSign && (it.request as BunkerRequestSign).event.kind == 22242 }
val hasRelayAuthEvents = bunkerRequests.any { it.request is BunkerRequestSign && it.request.event.kind == 22242 }
var localAccount by remember { mutableStateOf("") }
val key = bunkerRequests.first().localKey
var rememberType by remember { mutableStateOf(RememberType.NEVER) }
@@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -39,6 +38,7 @@ import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import kotlin.collections.forEach
@Composable
@@ -64,13 +64,14 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.forEach
import kotlin.collections.set
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun IntentMultiEventHomeScreen(
modifier: Modifier,
intents: List<IntentData>,
intents: ImmutableList<IntentData>,
packageName: String?,
accountParam: Account,
onRemoveIntentData: (List<IntentData>, IntentResultType) -> Unit,
@@ -6,12 +6,13 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.models.IntentResultType
import kotlinx.collections.immutable.ImmutableList
@Composable
fun MultiEventHomeScreen(
modifier: Modifier,
intents: List<IntentData>,
bunkerRequests: List<AmberBunkerRequest>,
intents: ImmutableList<IntentData>,
bunkerRequests: ImmutableList<AmberBunkerRequest>,
packageName: String?,
accountParam: Account,
onRemoveIntentData: (List<IntentData>, IntentResultType) -> Unit,
@@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -39,6 +38,7 @@ import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import kotlin.collections.forEach
@Composable