Skip to content

Commit d80f15b

Browse files
committed
perf: UpsertRuleGroupPage
1 parent 9d837f5 commit d80f15b

File tree

4 files changed

+139
-73
lines changed

4 files changed

+139
-73
lines changed

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Activity
44
import android.app.ActivityManager
55
import android.content.Context
66
import android.content.Intent
7+
import android.os.Build
78
import android.os.Bundle
89
import androidx.activity.ComponentActivity
910
import androidx.activity.compose.setContent
@@ -41,8 +42,12 @@ import androidx.compose.ui.Modifier
4142
import androidx.compose.ui.draw.clip
4243
import androidx.compose.ui.unit.dp
4344
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
45+
import androidx.core.view.ViewCompat
46+
import androidx.core.view.WindowInsetsAnimationCompat
47+
import androidx.core.view.WindowInsetsCompat
4448
import androidx.lifecycle.lifecycleScope
4549
import androidx.navigation.compose.rememberNavController
50+
import com.blankj.utilcode.util.KeyboardUtils
4651
import com.dylanc.activityresult.launcher.PickContentLauncher
4752
import com.dylanc.activityresult.launcher.StartActivityLauncher
4853
import com.ramcosta.composedestinations.DestinationsNavHost
@@ -51,7 +56,10 @@ import com.ramcosta.composedestinations.generated.destinations.AuthA11YPageDesti
5156
import com.ramcosta.composedestinations.utils.currentDestinationAsState
5257
import kotlinx.coroutines.Dispatchers
5358
import kotlinx.coroutines.flow.MutableStateFlow
59+
import kotlinx.coroutines.flow.drop
60+
import kotlinx.coroutines.flow.first
5461
import kotlinx.coroutines.flow.update
62+
import kotlinx.coroutines.flow.updateAndGet
5563
import kotlinx.coroutines.launch
5664
import kotlinx.coroutines.sync.Mutex
5765
import kotlinx.coroutines.sync.withLock
@@ -96,6 +104,54 @@ class MainActivity : ComponentActivity() {
96104
val launcher by lazy { StartActivityLauncher(this) }
97105
val pickContentLauncher by lazy { PickContentLauncher(this) }
98106

107+
val imeFullHiddenFlow = MutableStateFlow(true)
108+
val imeShowingFlow = MutableStateFlow(false)
109+
110+
private val imeVisible: Boolean
111+
get() = ViewCompat.getRootWindowInsets(window.decorView)!!
112+
.isVisible(WindowInsetsCompat.Type.ime())
113+
114+
private fun watchKeyboardVisible() {
115+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
116+
ViewCompat.setWindowInsetsAnimationCallback(
117+
window.decorView,
118+
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
119+
override fun onStart(
120+
animation: WindowInsetsAnimationCompat,
121+
bounds: WindowInsetsAnimationCompat.BoundsCompat
122+
): WindowInsetsAnimationCompat.BoundsCompat {
123+
imeShowingFlow.update { imeVisible }
124+
return super.onStart(animation, bounds)
125+
}
126+
127+
override fun onProgress(
128+
insets: WindowInsetsCompat,
129+
runningAnimations: List<WindowInsetsAnimationCompat>
130+
): WindowInsetsCompat {
131+
return insets
132+
}
133+
134+
override fun onEnd(animation: WindowInsetsAnimationCompat) {
135+
imeFullHiddenFlow.update { !imeVisible }
136+
imeShowingFlow.update { false }
137+
super.onEnd(animation)
138+
}
139+
})
140+
} else {
141+
KeyboardUtils.registerSoftInputChangedListener(window) { height ->
142+
// onEnd
143+
imeFullHiddenFlow.update { height == 0 }
144+
}
145+
}
146+
}
147+
148+
suspend fun hideSoftInput() {
149+
if (!imeFullHiddenFlow.updateAndGet { !imeVisible }) {
150+
KeyboardUtils.hideSoftInput(this)
151+
imeFullHiddenFlow.drop(1).first()
152+
}
153+
}
154+
99155
override fun onCreate(savedInstanceState: Bundle?) {
100156
installSplashScreen()
101157
enableEdgeToEdge()
@@ -116,9 +172,10 @@ class MainActivity : ComponentActivity() {
116172
mainVm.handleIntent(it)
117173
intent = null
118174
}
175+
watchKeyboardVisible()
119176
setContent {
120177
val navController = rememberNavController()
121-
mainVm.navController = navController
178+
mainVm.updateNavController(navController)
122179
CompositionLocalProvider(
123180
LocalNavController provides navController,
124181
LocalMainViewModel provides mainVm

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import android.content.ComponentName
55
import android.content.Intent
66
import android.net.Uri
77
import android.os.Build
8+
import android.os.Looper
89
import android.service.quicksettings.TileService
910
import android.webkit.URLUtil
1011
import androidx.lifecycle.ViewModel
1112
import androidx.lifecycle.viewModelScope
1213
import androidx.navigation.NavHostController
14+
import androidx.navigation.NavOptionsBuilder
1315
import com.blankj.utilcode.util.LogUtils
1416
import com.ramcosta.composedestinations.generated.destinations.AdvancedPageDestination
1517
import com.ramcosta.composedestinations.generated.destinations.SnapshotPageDestination
@@ -63,18 +65,27 @@ import li.songe.gkd.util.toast
6365
import li.songe.gkd.util.updateSubsMutex
6466
import li.songe.gkd.util.updateSubscription
6567
import rikka.shizuku.Shizuku
66-
import java.lang.ref.WeakReference
6768

6869
private var tempTermsAccepted = false
6970

7071
class MainViewModel : ViewModel() {
7172

72-
private var navControllerRef: WeakReference<NavHostController>? = null
73-
var navController: NavHostController
74-
get() = navControllerRef?.get() ?: error("not found navController")
75-
set(value) {
76-
navControllerRef = WeakReference(value)
73+
private lateinit var navController: NavHostController
74+
fun updateNavController(navController: NavHostController) {
75+
this.navController = navController
76+
}
77+
78+
fun popBackStack() {
79+
if (Looper.getMainLooper() == Looper.myLooper()) {
80+
navController.popBackStack()
81+
} else {
82+
viewModelScope.launch {
83+
withContext(Dispatchers.Main) {
84+
navController.popBackStack()
85+
}
86+
}
7787
}
88+
}
7889

7990
val enableDarkThemeFlow = storeFlow.debounce(300).map { s -> s.enableDarkTheme }.stateIn(
8091
viewModelScope,
@@ -183,11 +194,15 @@ class MainViewModel : ViewModel() {
183194
lastClickTabTime = System.currentTimeMillis()
184195
}
185196

186-
fun navigatePage(direction: Direction) {
197+
fun navigatePage(direction: Direction, builder: (NavOptionsBuilder.() -> Unit)? = null) {
187198
if (direction.route == navController.currentDestination?.route) {
188199
return
189200
}
190-
navController.navigate(direction.route)
201+
if (builder != null) {
202+
navController.navigate(direction.route, builder)
203+
} else {
204+
navController.navigate(direction.route)
205+
}
191206
}
192207

193208
fun navigateWebPage(url: String) {

app/src/main/kotlin/li/songe/gkd/ui/UpsertRuleGroupPage.kt

Lines changed: 45 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.compose.material3.TextFieldDefaults
2222
import androidx.compose.material3.TopAppBar
2323
import androidx.compose.runtime.Composable
2424
import androidx.compose.runtime.CompositionLocalProvider
25-
import androidx.compose.runtime.DisposableEffect
2625
import androidx.compose.runtime.collectAsState
2726
import androidx.compose.runtime.getValue
2827
import androidx.compose.ui.Alignment
@@ -33,26 +32,23 @@ import androidx.compose.ui.graphics.RectangleShape
3332
import androidx.compose.ui.unit.dp
3433
import androidx.lifecycle.viewModelScope
3534
import androidx.lifecycle.viewmodel.compose.viewModel
36-
import com.blankj.utilcode.util.KeyboardUtils
3735
import com.ramcosta.composedestinations.annotation.Destination
3836
import com.ramcosta.composedestinations.annotation.RootGraph
3937
import com.ramcosta.composedestinations.generated.destinations.SubsAppGroupListPageDestination
4038
import com.ramcosta.composedestinations.generated.destinations.SubsGlobalGroupListPageDestination
4139
import com.ramcosta.composedestinations.generated.destinations.UpsertRuleGroupPageDestination
4240
import kotlinx.coroutines.Dispatchers
41+
import kotlinx.coroutines.launch
4342
import kotlinx.coroutines.withContext
4443
import li.songe.gkd.MainActivity
4544
import li.songe.gkd.ui.component.autoFocus
4645
import li.songe.gkd.ui.component.waitResult
4746
import li.songe.gkd.ui.local.LocalDarkTheme
4847
import li.songe.gkd.ui.local.LocalMainViewModel
49-
import li.songe.gkd.ui.local.LocalNavController
5048
import li.songe.gkd.ui.style.ProfileTransitions
51-
import li.songe.gkd.ui.style.clearJson5TransformationCache
5249
import li.songe.gkd.ui.style.getJson5Transformation
5350
import li.songe.gkd.ui.style.scaffoldPadding
5451
import li.songe.gkd.util.launchAsFn
55-
import li.songe.gkd.util.launchTry
5652
import li.songe.gkd.util.throttle
5753

5854
@Suppress("unused")
@@ -66,71 +62,56 @@ fun UpsertRuleGroupPage(
6662
) {
6763
val mainVm = LocalMainViewModel.current
6864
val context = LocalActivity.current as MainActivity
69-
val navController = LocalNavController.current
7065
val vm = viewModel<UpsertRuleGroupVm>()
7166
val text by vm.textFlow.collectAsState()
72-
fun checkIfSaveText() = mainVm.viewModelScope.launchTry(Dispatchers.Default) {
73-
if (vm.textChanged) {
67+
68+
val checkIfSaveText = throttle(mainVm.viewModelScope.launchAsFn(Dispatchers.Default) {
69+
if (vm.hasTextChanged()) {
70+
vm.viewModelScope.launch {
71+
context.hideSoftInput()
72+
}
7473
mainVm.dialogFlow.waitResult(
7574
title = "放弃编辑",
7675
text = "当前内容未保存,是否放弃编辑?",
7776
)
77+
} else {
78+
context.hideSoftInput()
7879
}
79-
withContext(Dispatchers.Main) { mainVm.navController.popBackStack() }
80-
}.let { }
80+
mainVm.popBackStack()
81+
})
8182

82-
val onClickSave = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {
83-
vm.saveRule()
84-
if (KeyboardUtils.isSoftInputVisible(context)) {
85-
KeyboardUtils.hideSoftInput(context)
86-
}
87-
withContext(Dispatchers.Main) {
88-
if (forward) {
89-
if (appId == null) {
90-
navController.navigate(SubsGlobalGroupListPageDestination(subsItemId = subsId).route) {
91-
popUpTo(UpsertRuleGroupPageDestination.route) {
92-
inclusive = true
93-
}
94-
}
95-
} else {
96-
navController.navigate(
97-
SubsAppGroupListPageDestination(
98-
subsItemId = subsId,
99-
vm.addAppId ?: appId
100-
).route
101-
) {
102-
popUpTo(UpsertRuleGroupPageDestination.route) {
103-
inclusive = true
104-
}
83+
val onClickSave = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Main) {
84+
withContext(Dispatchers.Default) { vm.saveRule() }
85+
context.hideSoftInput()
86+
if (forward) {
87+
if (appId == null) {
88+
mainVm.navigatePage(SubsGlobalGroupListPageDestination(subsItemId = subsId)) {
89+
popUpTo(UpsertRuleGroupPageDestination.route) {
90+
inclusive = true
10591
}
10692
}
10793
} else {
108-
navController.popBackStack()
94+
mainVm.navigatePage(
95+
SubsAppGroupListPageDestination(
96+
subsItemId = subsId,
97+
vm.addAppId ?: appId
98+
)
99+
) {
100+
popUpTo(UpsertRuleGroupPageDestination.route) {
101+
inclusive = true
102+
}
103+
}
109104
}
105+
} else {
106+
mainVm.popBackStack()
110107
}
111108
})
112-
BackHandler(true) {
113-
if (KeyboardUtils.isSoftInputVisible(context)) {
114-
KeyboardUtils.hideSoftInput(context)
115-
return@BackHandler
116-
}
117-
checkIfSaveText()
118-
}
119-
DisposableEffect(null) {
120-
onDispose {
121-
clearJson5TransformationCache()
122-
}
123-
}
109+
BackHandler(true, checkIfSaveText)
124110
Scaffold(modifier = Modifier, topBar = {
125111
TopAppBar(
126112
modifier = Modifier.fillMaxWidth(),
127113
navigationIcon = {
128-
IconButton(onClick = throttle {
129-
if (KeyboardUtils.isSoftInputVisible(context)) {
130-
KeyboardUtils.hideSoftInput(context)
131-
}
132-
checkIfSaveText()
133-
}) {
114+
IconButton(onClick = checkIfSaveText) {
134115
Icon(
135116
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
136117
contentDescription = null,
@@ -159,14 +140,21 @@ fun UpsertRuleGroupPage(
159140
.fillMaxSize(),
160141
) {
161142
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
162-
// need compose 1.9.0
143+
val imeShowing by context.imeShowingFlow.collectAsState()
144+
val modifier = Modifier
145+
.autoFocus()
146+
.fillMaxSize()
147+
.run {
148+
if (imeShowing) {
149+
this
150+
} else {
151+
imePadding()
152+
}
153+
}
163154
TextField(
164155
value = text,
165156
onValueChange = { vm.textFlow.value = it },
166-
modifier = Modifier
167-
.autoFocus()
168-
.fillMaxSize()
169-
.imePadding(),
157+
modifier = modifier,
170158
shape = RectangleShape,
171159
colors = textColors,
172160
visualTransformation = getJson5Transformation(LocalDarkTheme.current),

app/src/main/kotlin/li/songe/gkd/ui/UpsertRuleGroupVm.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonArray
99
import kotlinx.serialization.json.JsonObject
1010
import kotlinx.serialization.json.JsonPrimitive
1111
import li.songe.gkd.data.RawSubscription
12+
import li.songe.gkd.ui.style.clearJson5TransformationCache
1213
import li.songe.gkd.util.subsIdToRawFlow
1314
import li.songe.gkd.util.toast
1415
import li.songe.gkd.util.updateSubscription
@@ -40,13 +41,13 @@ class UpsertRuleGroupVm(stateHandle: SavedStateHandle) : ViewModel() {
4041
private val initText = initialGroup?.cacheStr ?: ""
4142
val textFlow = MutableStateFlow(initText)
4243

43-
val textChanged: Boolean
44-
get() {
45-
val text = textFlow.value
46-
if (!isEdit) return !text.isBlank()
47-
if (initText == text) return false
48-
return initialGroup?.cacheJsonObject != runCatching { Json5.parseToJson5Element(text) }.getOrNull()
49-
}
44+
fun hasTextChanged(): Boolean {
45+
val text = textFlow.value
46+
if (!isEdit) return !text.isBlank()
47+
if (initText == text) return false
48+
return initialGroup?.cacheJsonObject != runCatching { Json5.parseToJson5Element(text) }.getOrNull()
49+
}
50+
5051

5152
var addAppId: String? = null
5253

@@ -213,6 +214,11 @@ class UpsertRuleGroupVm(stateHandle: SavedStateHandle) : ViewModel() {
213214
toast("添加成功")
214215
}
215216
}
217+
218+
override fun onCleared() {
219+
super.onCleared()
220+
clearJson5TransformationCache()
221+
}
216222
}
217223

218224
private fun checkGroupKeyName(

0 commit comments

Comments
 (0)