Skip to content

Commit 99a6e27

Browse files
committed
app: modernize architecture and state in apk-ng
Refactor apk-ng to adhere to modern Android and Jetpack Compose best practices: - Migrate VMFactory to the type-safe AndroidX viewModelFactory DSL. - Use collectAsStateWithLifecycle across all screen composables to pause flow collection when backgrounded. - Convert PolicyItem, ModuleItem, DenyAppState, and DenyProcessState to pure immutable data classes, removing Compose MutableState from ViewModels. - Extract shell operations from UI model classes into ViewModels. - Bridge Info.isConnected LiveData to coroutine Flow using callbackFlow and collect in viewModelScope without modifying :core. - Consolidate magisk and manager version state into HomeViewModel.UiState. - Add contentType to LazyColumn items for optimal list recycling. - Restore proper localized string resolution in MainActivityDialogs. Assisted-by: Gemini 3.7 Flash
1 parent 3aaf180 commit 99a6e27

19 files changed

Lines changed: 336 additions & 204 deletions
Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
package com.topjohnwu.magisk.arch
22

3-
import androidx.lifecycle.ViewModel
43
import androidx.lifecycle.ViewModelProvider
4+
import androidx.lifecycle.viewmodel.initializer
5+
import androidx.lifecycle.viewmodel.viewModelFactory
56
import com.topjohnwu.magisk.core.di.ServiceLocator
7+
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
8+
import com.topjohnwu.magisk.ui.flash.FlashViewModel
69
import com.topjohnwu.magisk.ui.home.HomeViewModel
710
import com.topjohnwu.magisk.ui.install.InstallViewModel
811
import com.topjohnwu.magisk.ui.log.LogViewModel
12+
import com.topjohnwu.magisk.ui.module.ActionViewModel
13+
import com.topjohnwu.magisk.ui.module.ModuleViewModel
14+
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
915
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
1016
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
1117

12-
object VMFactory : ViewModelProvider.Factory {
13-
@Suppress("UNCHECKED_CAST")
14-
override fun <T : ViewModel> create(modelClass: Class<T>): T {
15-
return when (modelClass) {
16-
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
17-
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
18-
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
19-
InstallViewModel::class.java ->
20-
InstallViewModel(ServiceLocator.networkService)
21-
SuRequestViewModel::class.java ->
22-
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
23-
else -> modelClass.newInstance()
24-
} as T
25-
}
18+
val VMFactory: ViewModelProvider.Factory = viewModelFactory {
19+
initializer { HomeViewModel(ServiceLocator.networkService) }
20+
initializer { LogViewModel(ServiceLocator.logRepo) }
21+
initializer { SuperuserViewModel(ServiceLocator.policyDB) }
22+
initializer { InstallViewModel(ServiceLocator.networkService) }
23+
initializer { SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs) }
24+
initializer { DenyListViewModel() }
25+
initializer { FlashViewModel() }
26+
initializer { ActionViewModel() }
27+
initializer { ModuleViewModel() }
28+
initializer { SettingsViewModel() }
2629
}

app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import androidx.compose.foundation.layout.fillMaxSize
1616
import androidx.compose.runtime.Composable
1717
import androidx.compose.runtime.CompositionLocalProvider
1818
import androidx.compose.runtime.LaunchedEffect
19-
import androidx.compose.runtime.collectAsState
2019
import androidx.compose.runtime.getValue
2120
import androidx.compose.runtime.mutableIntStateOf
2221
import androidx.compose.runtime.saveable.rememberSaveable
2322
import androidx.compose.runtime.setValue
2423
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.platform.LocalResources
2525
import androidx.core.content.pm.ShortcutManagerCompat
2626
import androidx.core.net.toUri
27+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2728
import androidx.lifecycle.lifecycleScope
2829
import androidx.lifecycle.viewmodel.compose.viewModel
2930
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -185,9 +186,9 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
185186
)
186187
}
187188
MainActivityDialogs(
188-
showInvalid = showInvalidState.collectAsState().value,
189-
unsupportedMessages = showUnsupported.collectAsState().value,
190-
showShortcut = showShortcutPrompt.collectAsState().value,
189+
showInvalid = showInvalidState.collectAsStateWithLifecycle().value,
190+
unsupportedMessages = showUnsupported.collectAsStateWithLifecycle().value,
191+
showShortcut = showShortcutPrompt.collectAsStateWithLifecycle().value,
191192
onInvalidConfirmed = {
192193
showInvalidState.value = false
193194
handleInvalidStateInstall()
@@ -207,7 +208,7 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
207208

208209
@Composable
209210
private fun HandleFlashIntent(navigator: Navigator) {
210-
val intentVersion by intentState.collectAsState()
211+
val intentVersion by intentState.collectAsStateWithLifecycle()
211212
LaunchedEffect(intentVersion) {
212213
val currentIntent = intent ?: return@LaunchedEffect
213214
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
@@ -303,6 +304,7 @@ private fun MainActivityDialogs(
303304
onShortcutDismissed: () -> Unit,
304305
modifier: Modifier = Modifier
305306
) {
307+
val resources = LocalResources.current
306308
val invalidDialog = rememberConfirmDialog(
307309
onConfirm = onInvalidConfirmed,
308310
onDismiss = {}
@@ -311,9 +313,9 @@ private fun MainActivityDialogs(
311313
LaunchedEffect(showInvalid) {
312314
if (showInvalid) {
313315
invalidDialog.showConfirm(
314-
title = CoreR.string.unsupport_nonroot_stub_title.let { "" },
315-
content = CoreR.string.unsupport_nonroot_stub_msg.let { "" },
316-
confirm = CoreR.string.install.let { "" },
316+
title = resources.getString(CoreR.string.unsupport_nonroot_stub_title),
317+
content = resources.getString(CoreR.string.unsupport_nonroot_stub_msg),
318+
confirm = resources.getString(CoreR.string.install),
317319
)
318320
}
319321
}
@@ -327,9 +329,10 @@ private fun MainActivityDialogs(
327329
val currentUnsupported = unsupportedMessages.getOrNull(currentUnsupportedIndex)
328330
LaunchedEffect(currentUnsupported) {
329331
if (currentUnsupported != null) {
332+
val (titleRes, msgRes) = currentUnsupported
330333
unsupportedDialog.showConfirm(
331-
title = "",
332-
content = "",
334+
title = resources.getString(titleRes),
335+
content = resources.getString(msgRes),
333336
)
334337
}
335338
}
@@ -342,8 +345,8 @@ private fun MainActivityDialogs(
342345
LaunchedEffect(showShortcut) {
343346
if (showShortcut) {
344347
shortcutDialog.showConfirm(
345-
title = "",
346-
content = "",
348+
title = resources.getString(CoreR.string.add_shortcut_title),
349+
content = resources.getString(CoreR.string.add_shortcut_msg),
347350
)
348351
}
349352
}

app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import androidx.compose.material3.TopAppBar
4848
import androidx.compose.material3.TopAppBarDefaults
4949
import androidx.compose.material3.TriStateCheckbox
5050
import androidx.compose.runtime.Composable
51-
import androidx.compose.runtime.collectAsState
5251
import androidx.compose.runtime.getValue
5352
import androidx.compose.runtime.mutableStateOf
5453
import androidx.compose.runtime.remember
@@ -60,6 +59,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
6059
import androidx.compose.ui.res.stringResource
6160
import androidx.compose.ui.state.ToggleableState
6261
import androidx.compose.ui.unit.dp
62+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6363
import com.google.accompanist.drawablepainter.rememberDrawablePainter
6464
import com.topjohnwu.magisk.ui.component.verticalScrollbar
6565
import com.topjohnwu.magisk.core.R as CoreR
@@ -71,13 +71,13 @@ fun DenyListScreen(
7171
onBack: () -> Unit,
7272
modifier: Modifier = Modifier
7373
) {
74-
val loading by viewModel.loading.collectAsState()
75-
val apps by viewModel.filteredApps.collectAsState()
76-
val query by viewModel.query.collectAsState()
77-
val showSystem by viewModel.showSystem.collectAsState()
78-
val showOS by viewModel.showOS.collectAsState()
79-
val sortBy by viewModel.sortBy.collectAsState()
80-
val sortReverse by viewModel.sortReverse.collectAsState()
74+
val loading by viewModel.loading.collectAsStateWithLifecycle()
75+
val apps by viewModel.filteredApps.collectAsStateWithLifecycle()
76+
val query by viewModel.query.collectAsStateWithLifecycle()
77+
val showSystem by viewModel.showSystem.collectAsStateWithLifecycle()
78+
val showOS by viewModel.showOS.collectAsStateWithLifecycle()
79+
val sortBy by viewModel.sortBy.collectAsStateWithLifecycle()
80+
val sortReverse by viewModel.sortReverse.collectAsStateWithLifecycle()
8181

8282
var showSortMenu by remember { mutableStateOf(false) }
8383
var showFilterMenu by remember { mutableStateOf(false) }
@@ -225,9 +225,15 @@ fun DenyListScreen(
225225
) {
226226
items(
227227
items = apps,
228-
key = { it.info.packageName }
228+
key = { it.info.packageName },
229+
contentType = { "DenyAppCard" }
229230
) { app ->
230-
DenyAppCard(app = app)
231+
DenyAppCard(
232+
app = app,
233+
onToggleExpand = { viewModel.toggleExpanded(app) },
234+
onToggleAll = { viewModel.toggleAll(app) },
235+
onToggleProcess = { proc -> viewModel.toggleProcess(app, proc) }
236+
)
231237
}
232238
}
233239
}
@@ -278,6 +284,9 @@ private fun SearchInput(
278284
@Composable
279285
private fun DenyAppCard(
280286
app: DenyAppState,
287+
onToggleExpand: () -> Unit,
288+
onToggleAll: () -> Unit,
289+
onToggleProcess: (DenyProcessState) -> Unit,
281290
modifier: Modifier = Modifier
282291
) {
283292
Card(
@@ -305,7 +314,7 @@ private fun DenyAppCard(
305314
Row(
306315
modifier = Modifier
307316
.fillMaxWidth()
308-
.clickable { app.isExpanded = !app.isExpanded }
317+
.clickable(onClick = onToggleExpand)
309318
.padding(14.dp),
310319
verticalAlignment = Alignment.CenterVertically
311320
) {
@@ -334,7 +343,7 @@ private fun DenyAppCard(
334343
app.checkedPercent < 1f -> ToggleableState.Indeterminate
335344
else -> ToggleableState.On
336345
},
337-
onClick = { app.toggleAll() }
346+
onClick = onToggleAll
338347
)
339348
}
340349

@@ -345,7 +354,10 @@ private fun DenyAppCard(
345354
.padding(start = 52.dp, bottom = 8.dp)
346355
) {
347356
app.processes.forEach { proc ->
348-
ProcessRow(proc = proc)
357+
ProcessRow(
358+
proc = proc,
359+
onToggle = { onToggleProcess(proc) }
360+
)
349361
}
350362
}
351363
}
@@ -356,13 +368,14 @@ private fun DenyAppCard(
356368
@Composable
357369
private fun ProcessRow(
358370
proc: DenyProcessState,
371+
onToggle: () -> Unit,
359372
modifier: Modifier = Modifier
360373
) {
361374
Row(
362375
modifier = modifier
363376
.fillMaxWidth()
364377
.clip(RoundedCornerShape(12.dp))
365-
.clickable { proc.toggle() }
378+
.clickable(onClick = onToggle)
366379
.padding(horizontal = 12.dp, vertical = 6.dp),
367380
verticalAlignment = Alignment.CenterVertically
368381
) {
@@ -376,7 +389,7 @@ private fun ProcessRow(
376389
Spacer(Modifier.width(8.dp))
377390
Checkbox(
378391
checked = proc.isEnabled,
379-
onCheckedChange = { proc.toggle() }
392+
onCheckedChange = { onToggle() }
380393
)
381394
}
382395
}

0 commit comments

Comments
 (0)