Skip to content

Commit e3c94bc

Browse files
committed
perf: home page scrollKey
1 parent 480aa97 commit e3c94bc

File tree

8 files changed

+94
-37
lines changed

8 files changed

+94
-37
lines changed

app/src/main/kotlin/li/songe/gkd/MainViewModel.kt

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.navigation3.runtime.NavKey
1010
import io.ktor.client.request.get
1111
import io.ktor.client.statement.bodyAsText
1212
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.flow.MutableSharedFlow
1314
import kotlinx.coroutines.flow.MutableStateFlow
1415
import kotlinx.coroutines.flow.debounce
1516
import kotlinx.coroutines.flow.map
@@ -23,7 +24,6 @@ import li.songe.gkd.data.SubsItem
2324
import li.songe.gkd.data.importData
2425
import li.songe.gkd.db.DbSet
2526
import li.songe.gkd.permission.AuthReason
26-
import li.songe.gkd.permission.canQueryPkgState
2727
import li.songe.gkd.permission.shizukuGrantedState
2828
import li.songe.gkd.service.A11yService
2929
import li.songe.gkd.shizuku.shizukuContextFlow
@@ -192,18 +192,17 @@ class MainViewModel : BaseViewModel(), OnSimpleLife {
192192
}
193193
}
194194

195-
val appListKeyFlow = MutableStateFlow(0)
196195
val tabFlow = MutableStateFlow(BottomNavItem.Control.key)
196+
val resetPageScrollEvent = MutableSharedFlow<BottomNavItem>()
197197
private var lastClickTabTime = 0L
198-
fun updateTab(navItem: BottomNavItem) {
199-
if (navItem == BottomNavItem.AppList && navItem.key == tabFlow.value) {
200-
// double click
201-
if (System.currentTimeMillis() - lastClickTabTime < 500) {
202-
appListKeyFlow.update { it + 1 }
203-
}
198+
fun handleClickTab(navItem: BottomNavItem) {
199+
val t = System.currentTimeMillis()
200+
// double click
201+
if (navItem.key == tabFlow.value && t - lastClickTabTime < 500) {
202+
viewModelScope.launch { resetPageScrollEvent.emit(navItem) }
204203
}
205204
tabFlow.value = navItem.key
206-
lastClickTabTime = System.currentTimeMillis()
205+
lastClickTabTime = t
207206
}
208207

209208
fun handleGkdUri(uri: Uri) {
@@ -362,12 +361,9 @@ class MainViewModel : BaseViewModel(), OnSimpleLife {
362361
githubCookieFlow.value
363362
}
364363

365-
canQueryPkgState.stateFlow.launchOnChange {
366-
appListKeyFlow.update { it + 1 }
367-
}
368-
369364
// for OnSimpleLife
370365
onCreated()
371366
addCloseable { onDestroyed() }
367+
toast("MainViewModel:init")
372368
}
373369
}

app/src/main/kotlin/li/songe/gkd/ui/component/Hooks.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package li.songe.gkd.ui.component
22

33
import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
4+
import androidx.compose.foundation.ScrollState
45
import androidx.compose.foundation.layout.height
56
import androidx.compose.foundation.layout.width
67
import androidx.compose.foundation.lazy.LazyListState
78
import androidx.compose.foundation.lazy.rememberLazyListState
9+
import androidx.compose.foundation.rememberScrollState
810
import androidx.compose.material3.LocalTextStyle
911
import androidx.compose.material3.TopAppBarDefaults
1012
import androidx.compose.material3.TopAppBarScrollBehavior
@@ -73,6 +75,7 @@ private fun getCompatStateValue(v: Any?): Any? = when (v) {
7375
}
7476

7577
// key 函数的依赖变化时, compose 将重置 key 函数那行代码之后所有代码的状态, 因此需要需要将 key 作用域限定在 Composable fun 内
78+
// 所有的 key 参数必须使用 rememberSaveable 或者 viewModel 来保存状态, 以保证正确的 restore 顺序,否则触发 ClassCastException
7679
@Composable
7780
fun useListScrollState(
7881
v1: Any?,
@@ -83,12 +86,22 @@ fun useListScrollState(
8386
return key(
8487
getCompatStateValue(v1),
8588
getCompatStateValue(v2),
86-
getCompatStateValue(v3)
89+
getCompatStateValue(v3),
8790
) {
8891
TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = canScroll) to rememberLazyListState()
8992
}
9093
}
9194

95+
@Composable
96+
fun usePinnedScrollBehaviorState(v1: Any?): Pair<TopAppBarScrollBehavior, LazyListState> {
97+
return key(getCompatStateValue(v1)) { TopAppBarDefaults.pinnedScrollBehavior() to rememberLazyListState() }
98+
}
99+
100+
@Composable
101+
fun useScrollBehaviorState(v1: Any?): Pair<TopAppBarScrollBehavior, ScrollState> {
102+
return key(getCompatStateValue(v1)) { TopAppBarDefaults.enterAlwaysScrollBehavior() to rememberScrollState() }
103+
}
104+
92105
@Composable
93106
fun LazyListState.isAtBottom(): androidx.compose.runtime.State<Boolean> = remember(this) {
94107
derivedStateOf {

app/src/main/kotlin/li/songe/gkd/ui/home/AppListPage.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
2727
import androidx.compose.runtime.Composable
2828
import androidx.compose.runtime.CompositionLocalProvider
2929
import androidx.compose.runtime.DisposableEffect
30+
import androidx.compose.runtime.LaunchedEffect
3031
import androidx.compose.runtime.collectAsState
3132
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.mutableIntStateOf
3234
import androidx.compose.runtime.mutableStateOf
3335
import androidx.compose.runtime.remember
36+
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
37+
import androidx.compose.runtime.saveable.rememberSaveable
3438
import androidx.compose.runtime.setValue
3539
import androidx.compose.ui.Alignment
3640
import androidx.compose.ui.Modifier
@@ -42,7 +46,9 @@ import androidx.compose.ui.semantics.stateDescription
4246
import androidx.compose.ui.text.style.TextOverflow
4347
import androidx.compose.ui.unit.dp
4448
import androidx.lifecycle.viewmodel.compose.viewModel
49+
import kotlinx.coroutines.flow.drop
4550
import kotlinx.coroutines.flow.update
51+
import kotlinx.coroutines.launch
4652
import li.songe.gkd.MainActivity
4753
import li.songe.gkd.R
4854
import li.songe.gkd.data.AppInfo
@@ -75,6 +81,7 @@ import li.songe.gkd.ui.style.EmptyHeight
7581
import li.songe.gkd.ui.style.appItemPadding
7682
import li.songe.gkd.util.AppGroupOption
7783
import li.songe.gkd.util.AppSortOption
84+
import li.songe.gkd.util.LogUtils
7885
import li.songe.gkd.util.appListAuthAbnormalFlow
7986
import li.songe.gkd.util.getUpDownTransform
8087
import li.songe.gkd.util.ruleSummaryFlow
@@ -98,12 +105,33 @@ fun useAppListPage(): ScaffoldExt {
98105
} else {
99106
null
100107
}
101-
val appListKey by mainVm.appListKeyFlow.collectAsState()
102108
val showSearchBar by vm.showSearchBarFlow.collectAsState()
103-
val (scrollBehavior, listState) = useListScrollState(appListKey)
104109
val refreshing by updateAppMutex.state.collectAsState()
105110
val pullToRefreshState = rememberPullToRefreshState()
106111
val editWhiteListMode by vm.editWhiteListModeFlow.collectAsState()
112+
val savedStateRegistry = LocalSaveableStateRegistry.current
113+
if (savedStateRegistry != null) {
114+
LogUtils.d(savedStateRegistry.performSave())
115+
}
116+
val scrollKey = rememberSaveable { mutableIntStateOf(0) }
117+
val (scrollBehavior, listState) = useListScrollState(scrollKey)
118+
LaunchedEffect(null) {
119+
listOf(
120+
canQueryPkgState.stateFlow,
121+
vm.appInfosFlow,
122+
).forEach {
123+
launch {
124+
it.drop(1).collect {
125+
scrollKey.intValue++
126+
}
127+
}
128+
}
129+
mainVm.resetPageScrollEvent.collect {
130+
if (it == BottomNavItem.AppList) {
131+
scrollKey.intValue++
132+
}
133+
}
134+
}
107135
return ScaffoldExt(
108136
navItem = BottomNavItem.AppList,
109137
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -134,7 +162,7 @@ fun useAppListPage(): ScaffoldExt {
134162
val titleModifier = Modifier
135163
.noRippleClickable(
136164
onClick = throttle {
137-
mainVm.appListKeyFlow.update { it + 1 }
165+
scrollKey.intValue++
138166
}
139167
)
140168
if (editWhiteListMode) {

app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@ import androidx.compose.foundation.layout.height
1313
import androidx.compose.foundation.layout.padding
1414
import androidx.compose.foundation.layout.size
1515
import androidx.compose.foundation.layout.width
16-
import androidx.compose.foundation.rememberScrollState
1716
import androidx.compose.foundation.shape.CircleShape
1817
import androidx.compose.foundation.shape.RoundedCornerShape
1918
import androidx.compose.foundation.verticalScroll
2019
import androidx.compose.material3.Card
2120
import androidx.compose.material3.CardDefaults
2221
import androidx.compose.material3.MaterialTheme
2322
import androidx.compose.material3.Text
24-
import androidx.compose.material3.TopAppBarDefaults
2523
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.LaunchedEffect
2625
import androidx.compose.runtime.collectAsState
2726
import androidx.compose.runtime.getValue
27+
import androidx.compose.runtime.mutableIntStateOf
28+
import androidx.compose.runtime.saveable.rememberSaveable
2829
import androidx.compose.ui.Alignment
2930
import androidx.compose.ui.Modifier
3031
import androidx.compose.ui.draw.clip
@@ -63,6 +64,7 @@ import li.songe.gkd.ui.component.PerfIconButton
6364
import li.songe.gkd.ui.component.PerfSwitch
6465
import li.songe.gkd.ui.component.PerfTopAppBar
6566
import li.songe.gkd.ui.component.textSize
67+
import li.songe.gkd.ui.component.useScrollBehaviorState
6668
import li.songe.gkd.ui.share.LocalMainViewModel
6769
import li.songe.gkd.ui.style.EmptyHeight
6870
import li.songe.gkd.ui.style.itemHorizontalPadding
@@ -80,8 +82,15 @@ fun useControlPage(): ScaffoldExt {
8082
val context = LocalActivity.current as MainActivity
8183
val mainVm = LocalMainViewModel.current
8284
val vm = viewModel<HomeVm>()
83-
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
84-
val scrollState = rememberScrollState()
85+
val scrollKey = rememberSaveable { mutableIntStateOf(0) }
86+
val (scrollBehavior, scrollState) = useScrollBehaviorState(scrollKey)
87+
LaunchedEffect(null) {
88+
mainVm.resetPageScrollEvent.collect {
89+
if (it == BottomNavItem.Control) {
90+
scrollKey.intValue++
91+
}
92+
}
93+
}
8594
return ScaffoldExt(
8695
navItem = BottomNavItem.Control,
8796
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),

app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ fun HomePage() {
7070
NavigationBarItem(
7171
selected = page.navItem.key == tab,
7272
modifier = Modifier,
73-
onClick = {
74-
mainVm.updateTab(page.navItem)
75-
},
73+
onClick = { mainVm.handleClickTab(page.navItem) },
7674
icon = {
7775
PerfIcon(
7876
imageVector = page.navItem.icon,

app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package li.songe.gkd.ui.home
22

33
import kotlinx.coroutines.flow.MutableStateFlow
44
import kotlinx.coroutines.flow.combine
5-
import li.songe.gkd.MainViewModel
65
import li.songe.gkd.store.actionCountFlow
76
import li.songe.gkd.store.blockMatchAppListFlow
87
import li.songe.gkd.store.storeFlow
@@ -69,11 +68,7 @@ class HomeVm : BaseViewModel() {
6968
}
7069
}
7170
}
72-
val appInfosFlow = appFilter.appListFlow.apply {
73-
launchOnChange {
74-
MainViewModel.instance.appListKeyFlow.value++
75-
}
76-
}
71+
val appInfosFlow = appFilter.appListFlow
7772

7873
val showToastInputDlgFlow = MutableStateFlow(false)
7974
val showNotifTextInputDlgFlow = MutableStateFlow(false)

app/src/main/kotlin/li/songe/gkd/ui/home/SettingsPage.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ import androidx.compose.material3.OutlinedTextField
2727
import androidx.compose.material3.Scaffold
2828
import androidx.compose.material3.Text
2929
import androidx.compose.material3.TextButton
30-
import androidx.compose.material3.TopAppBarDefaults
3130
import androidx.compose.runtime.Composable
3231
import androidx.compose.runtime.CompositionLocalProvider
32+
import androidx.compose.runtime.LaunchedEffect
3333
import androidx.compose.runtime.collectAsState
3434
import androidx.compose.runtime.getValue
35+
import androidx.compose.runtime.mutableIntStateOf
3536
import androidx.compose.runtime.mutableStateOf
3637
import androidx.compose.runtime.remember
3738
import androidx.compose.runtime.rememberCoroutineScope
39+
import androidx.compose.runtime.saveable.rememberSaveable
3840
import androidx.compose.runtime.setValue
3941
import androidx.compose.ui.Alignment
4042
import androidx.compose.ui.Modifier
@@ -78,6 +80,7 @@ import li.songe.gkd.ui.component.TextMenu
7880
import li.songe.gkd.ui.component.TextSwitch
7981
import li.songe.gkd.ui.component.autoFocus
8082
import li.songe.gkd.ui.component.updateDialogOptions
83+
import li.songe.gkd.ui.component.useScrollBehaviorState
8184
import li.songe.gkd.ui.component.waitResult
8285
import li.songe.gkd.ui.share.LocalMainViewModel
8386
import li.songe.gkd.ui.share.asMutableState
@@ -335,8 +338,15 @@ fun useSettingsPage(): ScaffoldExt {
335338
BlockA11yDialog(onDismissRequest = { showA11yBlockDlg = false })
336339
}
337340

338-
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
339-
val scrollState = rememberScrollState()
341+
val scrollKey = rememberSaveable { mutableIntStateOf(0) }
342+
val (scrollBehavior, scrollState) = useScrollBehaviorState(scrollKey)
343+
LaunchedEffect(null) {
344+
mainVm.resetPageScrollEvent.collect {
345+
if (it == BottomNavItem.Settings) {
346+
scrollKey.intValue++
347+
}
348+
}
349+
}
340350
return ScaffoldExt(
341351
navItem = BottomNavItem.Settings,
342352
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),

app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.padding
2020
import androidx.compose.foundation.layout.wrapContentSize
2121
import androidx.compose.foundation.lazy.LazyColumn
2222
import androidx.compose.foundation.lazy.itemsIndexed
23-
import androidx.compose.foundation.lazy.rememberLazyListState
2423
import androidx.compose.material3.AlertDialog
2524
import androidx.compose.material3.Checkbox
2625
import androidx.compose.material3.CheckboxDefaults
@@ -31,17 +30,18 @@ import androidx.compose.material3.LocalContentColor
3130
import androidx.compose.material3.MaterialTheme
3231
import androidx.compose.material3.Text
3332
import androidx.compose.material3.TextButton
34-
import androidx.compose.material3.TopAppBarDefaults
3533
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
3634
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
3735
import androidx.compose.runtime.Composable
3836
import androidx.compose.runtime.LaunchedEffect
3937
import androidx.compose.runtime.collectAsState
4038
import androidx.compose.runtime.getValue
4139
import androidx.compose.runtime.key
40+
import androidx.compose.runtime.mutableIntStateOf
4241
import androidx.compose.runtime.mutableStateOf
4342
import androidx.compose.runtime.remember
4443
import androidx.compose.runtime.rememberCoroutineScope
44+
import androidx.compose.runtime.saveable.rememberSaveable
4545
import androidx.compose.runtime.setValue
4646
import androidx.compose.ui.Alignment
4747
import androidx.compose.ui.Modifier
@@ -72,6 +72,7 @@ import li.songe.gkd.ui.component.PerfIconButton
7272
import li.songe.gkd.ui.component.PerfTopAppBar
7373
import li.songe.gkd.ui.component.SubsItemCard
7474
import li.songe.gkd.ui.component.TextMenu
75+
import li.songe.gkd.ui.component.usePinnedScrollBehaviorState
7576
import li.songe.gkd.ui.component.waitResult
7677
import li.songe.gkd.ui.share.ListPlaceholder
7778
import li.songe.gkd.ui.share.LocalMainViewModel
@@ -192,7 +193,15 @@ fun useSubsManagePage(): ScaffoldExt {
192193
)
193194
}
194195

195-
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
196+
val scrollKey = rememberSaveable { mutableIntStateOf(0) }
197+
val (scrollBehavior, lazyListState) = usePinnedScrollBehaviorState(scrollKey)
198+
LaunchedEffect(null) {
199+
mainVm.resetPageScrollEvent.collect {
200+
if (it == BottomNavItem.SubsManage) {
201+
scrollKey.intValue++
202+
}
203+
}
204+
}
196205
return ScaffoldExt(
197206
navItem = BottomNavItem.SubsManage,
198207
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -413,7 +422,6 @@ fun useSubsManagePage(): ScaffoldExt {
413422
)
414423
},
415424
) { contentPadding ->
416-
val lazyListState = rememberLazyListState()
417425
val reorderableLazyColumnState =
418426
rememberReorderableLazyListState(lazyListState) { from, to ->
419427
orderSubItems = orderSubItems.toMutableList().apply {

0 commit comments

Comments
 (0)