Skip to content

Commit

Permalink
Merge branch 'fix/home-performance'
Browse files Browse the repository at this point in the history
X1nto committed Jan 6, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents c19814e + 6b9e74a commit 64b3356
Showing 11 changed files with 152 additions and 54 deletions.
12 changes: 6 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -49,12 +49,6 @@ android {
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" +
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"

freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
"${projectDir.absolutePath}/compose_stability.conf"
)

val buildDir = layout.buildDirectory.asFile.get().absolutePath
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf(
@@ -75,6 +69,10 @@ android {
buildConfig = true
}

composeCompiler {
stabilityConfigurationFile.set(project.layout.projectDirectory.file("compose_stability.conf"))
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -134,6 +132,8 @@ dependencies {

implementation("androidx.datastore:datastore-preferences:1.1.1")

implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8")

implementation("dev.olshevski.navigation:reimagined:1.5.0")

implementation("commons-codec:commons-codec:1.15")
Original file line number Diff line number Diff line change
@@ -6,12 +6,14 @@ import androidx.compose.runtime.Immutable
sealed interface DomainOtpRealtimeData {
val code: String

@Immutable
data class Totp(
override val code: String,
val progress: Float,
val countdown: Int,
) : DomainOtpRealtimeData

@Immutable
data class Hotp(
override val code: String,
val count: Int,
61 changes: 32 additions & 29 deletions app/src/main/java/com/xinto/mauth/ui/screen/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ package com.xinto.mauth.ui.screen.home
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.TopAppBarDefaults
@@ -12,6 +11,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -25,6 +26,8 @@ import com.xinto.mauth.ui.screen.home.state.HomeScreenEmpty
import com.xinto.mauth.ui.screen.home.state.HomeScreenError
import com.xinto.mauth.ui.screen.home.state.HomeScreenLoading
import com.xinto.mauth.ui.screen.home.state.HomeScreenSuccess
import com.xinto.mauth.ui.util.collectAsStateListWithLifecycle
import com.xinto.mauth.ui.util.collectAsStateMapWithLifecycle
import org.koin.androidx.compose.koinViewModel
import java.util.UUID

@@ -40,8 +43,8 @@ fun HomeScreen(
) {
val viewModel: HomeViewModel = koinViewModel()
val state by viewModel.state.collectAsStateWithLifecycle()
val realTimeData by viewModel.realTimeData.collectAsStateWithLifecycle()
val selectedAccounts by viewModel.selectedAccounts.collectAsStateWithLifecycle()
val realTimeData = viewModel.realTimeData.collectAsStateMapWithLifecycle()
val selectedAccounts = viewModel.selectedAccounts.collectAsStateListWithLifecycle()
val activeSortSetting by viewModel.activeSortSetting.collectAsStateWithLifecycle()

val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
@@ -96,8 +99,8 @@ fun HomeScreen(
onAccountCounterIncrease: (UUID) -> Unit,
onAccountCopyCode: (String, String, Boolean) -> Unit,
state: HomeScreenState,
accountRealtimeData: Map<UUID, DomainOtpRealtimeData>,
selectedAccounts: List<UUID>,
accountRealtimeData: SnapshotStateMap<UUID, DomainOtpRealtimeData>,
selectedAccounts: SnapshotStateList<UUID>,
activeSortSetting: SortSetting,
onActiveSortChange: (SortSetting) -> Unit
) {
@@ -118,33 +121,33 @@ fun HomeScreen(
onActiveSortChange = onActiveSortChange,
scrollBehavior = scrollBehavior
) {
Box(
modifier = Modifier
val modifier = remember {
Modifier
.fillMaxSize()
.padding(it)
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
when (state) {
is HomeScreenState.Loading -> {
HomeScreenLoading()
}
is HomeScreenState.Empty -> {
HomeScreenEmpty()
}
is HomeScreenState.Success -> {
HomeScreenSuccess(
onAccountSelect = onAccountSelect,
onAccountEdit = onAccountEdit,
onAccountCounterIncrease = onAccountCounterIncrease,
onAccountCopyCode = onAccountCopyCode,
accounts = state.accounts,
selectedAccounts = selectedAccounts,
accountRealtimeData = accountRealtimeData
)
}
is HomeScreenState.Error -> {
HomeScreenError()
}
}
when (state) {
is HomeScreenState.Loading -> {
HomeScreenLoading(modifier)
}
is HomeScreenState.Empty -> {
HomeScreenEmpty(modifier)
}
is HomeScreenState.Success -> {
HomeScreenSuccess(
modifier = modifier,
onAccountSelect = onAccountSelect,
onAccountEdit = onAccountEdit,
onAccountCounterIncrease = onAccountCounterIncrease,
onAccountCopyCode = onAccountCopyCode,
accounts = state.accounts,
selectedAccounts = selectedAccounts,
accountRealtimeData = accountRealtimeData
)
}
is HomeScreenState.Error -> {
HomeScreenError(modifier)
}
}
}
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package com.xinto.mauth.ui.screen.home

import androidx.compose.runtime.Immutable
import com.xinto.mauth.domain.account.model.DomainAccount
import kotlinx.collections.immutable.ImmutableList

@Immutable
sealed interface HomeScreenState {
@@ -14,7 +15,7 @@ sealed interface HomeScreenState {

@Immutable
@JvmInline
value class Success(val accounts: List<DomainAccount>) : HomeScreenState
value class Success(val accounts: ImmutableList<DomainAccount>) : HomeScreenState

@Immutable
@JvmInline
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import com.xinto.mauth.domain.account.AccountRepository
import com.xinto.mauth.domain.account.model.DomainAccountInfo
import com.xinto.mauth.domain.otp.OtpRepository
import com.xinto.mauth.util.catchMap
import kotlinx.collections.immutable.adapters.ImmutableListAdapter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
@@ -43,7 +44,7 @@ class HomeViewModel(
val state = accounts.getAccounts()
.map {
when (it.isNotEmpty()) {
true -> HomeScreenState.Success(it)
true -> HomeScreenState.Success(ImmutableListAdapter(it))
false -> HomeScreenState.Empty
}
}.catchMap {
Original file line number Diff line number Diff line change
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -107,13 +106,16 @@ fun HomeAccountCard(
)
},
bottomContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
RealtimeInformation(
realtimeData = realtimeData,
showCode = showCode,
onCounterClick = onCounterClick
)
Spacer(modifier = Modifier.weight(1f))
InteractionButtons(
showCode = showCode,
onShowCodeChange = {
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package com.xinto.mauth.ui.screen.home.state

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -17,9 +16,9 @@ import androidx.compose.ui.unit.dp
import com.xinto.mauth.R

@Composable
fun HomeScreenEmpty() {
fun HomeScreenEmpty(modifier: Modifier = Modifier) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package com.xinto.mauth.ui.screen.home.state

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -14,9 +13,9 @@ import androidx.compose.ui.unit.dp
import com.xinto.mauth.R

@Composable
fun HomeScreenError() {
fun HomeScreenError(modifier: Modifier = Modifier) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package com.xinto.mauth.ui.screen.home.state

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
fun HomeScreenLoading() {
fun HomeScreenLoading(modifier: Modifier = Modifier) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = modifier,
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
Original file line number Diff line number Diff line change
@@ -2,30 +2,33 @@ package com.xinto.mauth.ui.screen.home.state

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.xinto.mauth.domain.account.model.DomainAccount
import com.xinto.mauth.domain.otp.model.DomainOtpRealtimeData
import com.xinto.mauth.ui.screen.home.component.HomeAccountCard
import kotlinx.collections.immutable.ImmutableList
import java.util.UUID

@Composable
fun HomeScreenSuccess(
modifier: Modifier = Modifier,
onAccountSelect: (UUID) -> Unit,
onAccountEdit: (UUID) -> Unit,
onAccountCounterIncrease: (UUID) -> Unit,
onAccountCopyCode: (String, String, Boolean) -> Unit,
accounts: List<DomainAccount>,
selectedAccounts: List<UUID>,
accountRealtimeData: Map<UUID, DomainOtpRealtimeData>,
accounts: ImmutableList<DomainAccount>,
selectedAccounts: SnapshotStateList<UUID>,
accountRealtimeData: SnapshotStateMap<UUID, DomainOtpRealtimeData>,
) {
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
modifier = modifier,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
89 changes: 89 additions & 0 deletions app/src/main/java/com/xinto/mauth/ui/util/FlowProducers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.xinto.mauth.ui.util

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

@Composable
fun <T> Flow<List<T>>.collectAsStateListWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
) = collectAsStateListWithLifecycle(
lifecycle = lifecycleOwner.lifecycle,
minActiveState = minActiveState,
context = context
)

@Composable
fun <T> Flow<List<T>>.collectAsStateListWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): SnapshotStateList<T> {
val result = remember { mutableStateListOf<T>() }
LaunchedEffect(this, lifecycle, minActiveState, context) {
lifecycle.repeatOnLifecycle(minActiveState) {
if (context == EmptyCoroutineContext) {
this@collectAsStateListWithLifecycle.collect {
result.clear()
result.addAll(it)
}
} else withContext(context) {
this@collectAsStateListWithLifecycle.collect {
result.clear()
result.addAll(it)
}
}
}
}
return result
}

@Composable
fun <K, V> Flow<Map<K, V>>.collectAsStateMapWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
) = collectAsStateMapWithLifecycle(
lifecycle = lifecycleOwner.lifecycle,
minActiveState = minActiveState,
context = context
)

@Composable
fun <K, V> Flow<Map<K, V>>.collectAsStateMapWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): SnapshotStateMap<K, V> {
val result = remember { mutableStateMapOf<K, V>() }
LaunchedEffect(this, lifecycle, minActiveState, context) {
lifecycle.repeatOnLifecycle(minActiveState) {
if (context == EmptyCoroutineContext) {
this@collectAsStateMapWithLifecycle.collect {
result.clear()
result.putAll(it)
}
} else withContext(context) {
this@collectAsStateMapWithLifecycle.collect {
result.clear()
result.putAll(it)
}
}
}
}
return result
}

0 comments on commit 64b3356

Please sign in to comment.