Skip to content

Commit 4a11732

Browse files
committed
refactor: improves compose stability and performance
- Adds the Compose Stability Analyzer plugin to identify and fix unstable classes. - Marks UI model classes like `@Immutable` or `@Stable`.
1 parent cd92542 commit 4a11732

23 files changed

Lines changed: 119 additions & 89 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.kotlin.compose)
77
alias(libs.plugins.ksp)
88
alias(libs.plugins.jetbrains.kotlin.serialization)
9+
alias(libs.plugins.stability.analyzer)
910
id("kotlin-parcelize")
1011
}
1112

app/src/main/kotlin/com/androidvip/sysctlgui/models/SearchHint.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.androidvip.sysctlgui.models
22

3+
import androidx.compose.runtime.Immutable
4+
35
/**
46
* Represents a search hint displayed to the user.
57
*
@@ -9,6 +11,7 @@ package com.androidvip.sysctlgui.models
911
* @property hint The text of the search hint.
1012
* @property isFromHistory A boolean flag indicating whether the hint is from the user's search history
1113
*/
14+
@Immutable
1215
data class SearchHint(
1316
val hint: String,
1417
val isFromHistory: Boolean = false

app/src/main/kotlin/com/androidvip/sysctlgui/models/UiKernelParam.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.androidvip.sysctlgui.models
22

33
import android.os.Build
44
import android.os.Parcelable
5-
import androidx.compose.runtime.Stable
5+
import androidx.compose.runtime.Immutable
66
import com.androidvip.sysctlgui.domain.models.KernelParam
77
import com.androidvip.sysctlgui.utils.Consts
88
import kotlinx.parcelize.IgnoredOnParcel
@@ -14,7 +14,7 @@ import kotlin.io.path.isDirectory
1414
/**
1515
* Represents a kernel parameter with additional UI-specific properties.
1616
*/
17-
@Stable
17+
@Immutable
1818
@Parcelize
1919
data class UiKernelParam(
2020
override val name: String = "",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.androidvip.sysctlgui.models
2+
3+
import androidx.compose.runtime.Immutable
4+
import com.androidvip.sysctlgui.domain.models.ParamDocumentation
5+
6+
@Immutable
7+
data class UiParamDocumentation(
8+
override val title: String = "",
9+
override val documentationText: String = "",
10+
override val documentationHtml: String? = null,
11+
override val url: String? = null
12+
) : ParamDocumentation
13+
14+
fun ParamDocumentation.toUiParamDocumentation() = UiParamDocumentation(
15+
title = this.title,
16+
documentationText = this.documentationText,
17+
documentationHtml = this.documentationHtml,
18+
url = this.url
19+
)

app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,18 @@ import androidx.compose.ui.text.fromHtml
2626
import androidx.compose.ui.text.style.TextDecoration
2727
import androidx.compose.ui.tooling.preview.PreviewLightDark
2828
import androidx.compose.ui.unit.dp
29-
import androidx.core.text.HtmlCompat
3029
import com.androidvip.sysctlgui.R
3130
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
32-
import com.androidvip.sysctlgui.domain.models.ParamDocumentation
31+
import com.androidvip.sysctlgui.models.UiParamDocumentation
3332
import com.androidvip.sysctlgui.utils.browse
3433
import kotlinx.coroutines.CoroutineScope
3534
import kotlinx.coroutines.launch
3635
import org.intellij.lang.annotations.Language
37-
import kotlin.text.append
3836

3937
@OptIn(ExperimentalMaterial3Api::class)
4038
@Composable
4139
internal fun DocumentationBottomSheet(
42-
documentation: ParamDocumentation,
40+
documentation: UiParamDocumentation,
4341
sheetState: SheetState
4442
) {
4543
val coroutineScope = rememberCoroutineScope()
@@ -57,7 +55,7 @@ internal fun DocumentationBottomSheet(
5755
@OptIn(ExperimentalMaterial3Api::class)
5856
@Composable
5957
private fun DocumentationBottomSheetContent(
60-
documentation: ParamDocumentation,
58+
documentation: UiParamDocumentation,
6159
sheetState: SheetState,
6260
coroutineScope: CoroutineScope = rememberCoroutineScope(),
6361
) {
@@ -98,7 +96,7 @@ private fun DocumentationBottomSheetContent(
9896
if (documentation.url != null) {
9997
TextButton(
10098
onClick = {
101-
context.browse(documentation.url.orEmpty())
99+
context.browse(documentation.url)
102100
coroutineScope.launch { sheetState.hide() }
103101
},
104102
modifier = Modifier
@@ -137,7 +135,7 @@ private fun DocumentationBottomSheetPreview() {
137135
</ul>
138136
""".trimIndent()
139137

140-
val documentation = ParamDocumentation(
138+
val documentation = UiParamDocumentation(
141139
title = "/proc/sys/fs",
142140
url = "https://docs.kernel.org/admin-guide/sysctl/fs.html",
143141
documentationText = """

app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ import com.androidvip.sysctlgui.R
6161
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
6262
import com.androidvip.sysctlgui.design.utils.isLandscape
6363
import com.androidvip.sysctlgui.domain.models.KernelParam
64-
import com.androidvip.sysctlgui.domain.models.ParamDocumentation
6564
import com.androidvip.sysctlgui.models.UiKernelParam
65+
import com.androidvip.sysctlgui.models.UiParamDocumentation
6666
import com.androidvip.sysctlgui.ui.main.MainViewEvent
6767
import com.androidvip.sysctlgui.ui.main.MainViewModel
6868
import com.androidvip.sysctlgui.ui.main.MainViewState
@@ -84,7 +84,7 @@ fun ParamBrowseScreen(
8484
viewModel: ParamBrowseViewModel = koinViewModel(),
8585
onParamSelected: (KernelParam) -> Unit
8686
) {
87-
var documentation by remember { mutableStateOf<ParamDocumentation?>(null) }
87+
var documentation by remember { mutableStateOf<UiParamDocumentation?>(null) }
8888
val documentationSheetState = rememberModalBottomSheetState()
8989
val context = LocalContext.current
9090
val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -148,9 +148,9 @@ fun ParamBrowseScreen(
148148
private fun ParamBrowseScreenContent(
149149
params: List<UiKernelParam>,
150150
currentPath: String,
151-
documentation: ParamDocumentation?,
151+
documentation: UiParamDocumentation?,
152152
onParamClicked: (UiKernelParam) -> Unit,
153-
onDocumentationClicked: (ParamDocumentation) -> Unit,
153+
onDocumentationClicked: (UiParamDocumentation) -> Unit,
154154
backEnabled: Boolean = false,
155155
onBackPressed: () -> Unit,
156156
isRefreshing: Boolean,
@@ -341,7 +341,7 @@ internal fun ParamBrowseScreenContentPreview() {
341341
ParamBrowseScreenContent(
342342
params = params,
343343
currentPath = currentPath,
344-
documentation = ParamDocumentation(
344+
documentation = UiParamDocumentation(
345345
title = currentPath,
346346
documentationText = "Documentation for $currentPath",
347347
url = null
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.androidvip.sysctlgui.ui.params.browse
22

3-
import com.androidvip.sysctlgui.domain.models.ParamDocumentation
3+
import androidx.compose.runtime.Immutable
44
import com.androidvip.sysctlgui.models.UiKernelParam
5+
import com.androidvip.sysctlgui.models.UiParamDocumentation
56

7+
@Immutable
68
data class ParamBrowseState(
79
val loading: Boolean = false,
810
val params: List<UiKernelParam> = emptyList(),
911
val currentPath: String = "",
1012
val backEnabled: Boolean = false,
11-
val documentation: ParamDocumentation? = null
13+
val documentation: UiParamDocumentation? = null
1214
)
1315

1416
sealed interface ParamBrowseViewEffect {
@@ -19,7 +21,7 @@ sealed interface ParamBrowseViewEffect {
1921

2022
sealed interface ParamBrowseViewEvent {
2123
data class ParamClicked(val param: UiKernelParam) : ParamBrowseViewEvent
22-
data class DocumentationClicked(val docs: ParamDocumentation) : ParamBrowseViewEvent
24+
data class DocumentationClicked(val docs: UiParamDocumentation) : ParamBrowseViewEvent
2325
object BackRequested : ParamBrowseViewEvent
2426
object RefreshRequested : ParamBrowseViewEvent
2527
}

app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.androidvip.sysctlgui.domain.usecase.GetParamsFromFilesUseCase
88
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
99
import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
1010
import com.androidvip.sysctlgui.models.UiKernelParam
11+
import com.androidvip.sysctlgui.models.toUiParamDocumentation
1112
import com.androidvip.sysctlgui.utils.BaseViewModel
1213
import com.androidvip.sysctlgui.utils.Consts
1314
import com.topjohnwu.superuser.nio.FileSystemManager
@@ -88,7 +89,7 @@ class ParamBrowseViewModel(
8889
params = newParams,
8990
currentPath = parentParam.path,
9091
backEnabled = parentParam.path != Consts.PROC_SYS,
91-
documentation = directoryDocumentation,
92+
documentation = directoryDocumentation?.toUiParamDocumentation(),
9293
loading = false
9394
)
9495
}

app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamLandscapeContent.kt

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ import androidx.compose.ui.unit.dp
4040
import com.androidvip.sysctlgui.R
4141
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
4242
import com.androidvip.sysctlgui.domain.enums.CommitMode
43-
import com.androidvip.sysctlgui.domain.models.ParamDocumentation
4443
import com.androidvip.sysctlgui.models.UiKernelParam
44+
import com.androidvip.sysctlgui.models.UiParamDocumentation
4545
import com.androidvip.sysctlgui.ui.components.ErrorContainer
4646
import com.androidvip.sysctlgui.utils.performHapticFeedbackForToggle
4747
import kotlinx.coroutines.launch
@@ -64,20 +64,18 @@ internal fun EditParamLandscapeContent(
6464
val context = LocalContext.current
6565
val coroutineScope = rememberCoroutineScope()
6666
val clipboardManager = LocalClipboard.current
67+
val clipLabelText = stringResource(R.string.kernel_params)
68+
val toastCopiedText = stringResource(R.string.copied_to_clipboard)
69+
6770
val copyParamContentToClipboard = {
6871
val clipData = ClipData.newPlainText(
69-
context.getString(R.string.kernel_params),
70-
"${param.lastNameSegment}=${param.value} (${param.path})"
72+
clipLabelText, "${param.lastNameSegment}=${param.value} (${param.path})"
7173
)
7274
val clipEntry = ClipEntry(clipData)
7375
coroutineScope.launch {
7476
clipboardManager.setClipEntry(clipEntry)
7577
}
76-
Toast.makeText(
77-
context,
78-
context.getString(R.string.copied_to_clipboard),
79-
Toast.LENGTH_SHORT
80-
).show()
78+
Toast.makeText(context, toastCopiedText, Toast.LENGTH_SHORT).show()
8179
}
8280

8381
Row {
@@ -87,6 +85,7 @@ internal fun EditParamLandscapeContent(
8785
.background(MaterialTheme.colorScheme.background)
8886
.verticalScroll(rememberScrollState())
8987
) {
88+
val toastCopyMessage = stringResource(R.string.long_press_to_copy)
9089
Text(
9190
text = param.lastNameSegment,
9291
style = MaterialTheme.typography.displayMedium,
@@ -98,7 +97,7 @@ internal fun EditParamLandscapeContent(
9897
onClick = {
9998
Toast.makeText(
10099
context,
101-
context.getString(R.string.long_press_to_copy),
100+
toastCopyMessage,
102101
Toast.LENGTH_SHORT
103102
).show()
104103
},
@@ -112,10 +111,8 @@ internal fun EditParamLandscapeContent(
112111

113112
Row(
114113
modifier = Modifier.padding(
115-
horizontal = 16.dp,
116-
vertical = if (param.isTaskerParam) 0.dp else 24.dp
117-
),
118-
verticalAlignment = Alignment.CenterVertically
114+
horizontal = 16.dp, vertical = if (param.isTaskerParam) 0.dp else 24.dp
115+
), verticalAlignment = Alignment.CenterVertically
119116
) {
120117
Column(modifier = Modifier.weight(1f)) {
121118
Text(
@@ -138,22 +135,18 @@ internal fun EditParamLandscapeContent(
138135

139136
if (state.taskerAvailable) {
140137
TaskerButton(
141-
isTaskerParam = param.isTaskerParam,
142-
onToggle = { newState ->
138+
isTaskerParam = param.isTaskerParam, onToggle = { newState ->
143139
performHapticFeedbackForToggle(newState, view)
144140
onTaskerClicked(newState)
145-
},
146-
modifier = Modifier.scale(0.85f)
141+
}, modifier = Modifier.scale(0.85f)
147142
)
148143
}
149144

150145
FavoriteButton(
151-
isFavorite = param.isFavorite,
152-
onFavoriteClick = { newState ->
146+
isFavorite = param.isFavorite, onFavoriteClick = { newState ->
153147
performHapticFeedbackForToggle(newState, view)
154148
onFavoriteToggle(newState)
155-
},
156-
modifier = Modifier.scale(0.85f)
149+
}, modifier = Modifier.scale(0.85f)
157150
)
158151
}
159152

@@ -169,8 +162,7 @@ internal fun EditParamLandscapeContent(
169162
contentDescription = stringResource(R.string.tasker_list),
170163
tint = MaterialTheme.colorScheme.tertiary
171164
)
172-
}
173-
)
165+
})
174166
}
175167
}
176168

@@ -207,8 +199,7 @@ internal fun EditParamLandscapeContent(
207199
@Preview(device = "spec:parent=pixel_5,orientation=landscape")
208200
private fun EditParamContentPreview() {
209201

210-
@Language("html")
211-
val htmlDocs = """
202+
@Language("html") val htmlDocs = """
212203
<p>Correctable <a href="../">memory errors</a> are very common on servers.
213204
Soft-offline is kernel’s solution for memory pages having
214205
(excessive) corrected memory errors.</p>
@@ -220,12 +211,10 @@ private fun EditParamContentPreview() {
220211
<li>For a page that is part of a HugeTLB <b>hugepage</b>, <code>soft-offline</code> first migrates the entire HugeTLB hugepage, during which a free hugepage will be consumed as migration target. Then the original hugepage is dissolved into raw pages without compensation, reducing the capacity of the HugeTLB pool by 1.</li>
221212
<li>It is user’s call to choose between reliability <i>(staying away from fragile physical memory)</i> vs performance / capacity implications in transparent and HugeTLB cases.</li>
222213
</ul>
223-
""".trimIndent()
224-
.replace(
214+
""".trimIndent().replace(
225215
"<code>",
226216
"<font face=\"monospace\" color=\"#222\"><b><span style=\"background-color: #DCDCF5\">"
227-
)
228-
.replace("</code>", "</span></b></font>")
217+
).replace("</code>", "</span></b></font>")
229218

230219
var showError by remember { mutableStateOf(true) }
231220

@@ -243,7 +232,7 @@ private fun EditParamContentPreview() {
243232
),
244233
taskerAvailable = true,
245234
keyboardType = KeyboardType.Number,
246-
documentation = ParamDocumentation(
235+
documentation = UiParamDocumentation(
247236
title = "vm.enable_soft_offline",
248237
documentationText = "",
249238
documentationHtml = htmlDocs,
@@ -253,15 +242,12 @@ private fun EditParamContentPreview() {
253242
EditParamLandscapeContent(
254243
state = state,
255244
showError = showError,
256-
errorMessage = "Sysctl command for 'wm.swappiness' executed, " +
257-
"but output did not confirm the change. Output: 'Access denied'. " +
258-
"Try using '${CommitMode.ECHO}' mode.",
245+
errorMessage = "Sysctl command for 'wm.swappiness' executed, " + "but output did not confirm the change. Output: 'Access denied'. " + "Try using '${CommitMode.ECHO}' mode.",
259246
onValueApply = {},
260247
onTaskerClicked = {},
261248
onDocsReadMorePressed = {},
262249
onFavoriteToggle = {},
263-
onErrorAnimationEnd = { showError = false }
264-
)
250+
onErrorAnimationEnd = { showError = false })
265251
}
266252
}
267253
}

0 commit comments

Comments
 (0)