Skip to content

Commit f6f594e

Browse files
Refactor Settings Compose Setup
This PR refines the Settings screen setup using Jetpack Compose: * **Navigation Routing**: Updated navigation logic to use **Serialized Routes**, as recommended in the [official Compose navigation documentation](https://developer.android.com/develop/ui/compose/navigation). * **Screen Title Management**: Migrated screen title handling to a Compose-native approach instead of using `addOnDestinationChangedListener`. * **Toolbar Enhancements**: * Refined the toolbar layout and styling to improve alignment with Material theming. * Implemented proper ripple effects and consistent color usage. * Fixed back navigation behavior to align with Compose conventions. * **Theming Note**: The current Compose-based theme system does not yet fully replicate the color palette used in the legacy (pre-Compose) implementation, especially across different services. A `TODO` has been added in code, and a separate task will be created to address this comprehensively.
1 parent 840084d commit f6f594e

File tree

6 files changed

+130
-61
lines changed

6 files changed

+130
-61
lines changed

app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ import androidx.compose.runtime.Composable
66
import androidx.compose.ui.Modifier
77
import androidx.compose.ui.graphics.Color
88
import org.schabi.newpipe.R
9+
import org.schabi.newpipe.ui.SettingsRoutes
910
import org.schabi.newpipe.ui.TextPreference
1011

1112
@Composable
1213
fun SettingsScreen(
13-
onSelectSettingOption: (SettingsScreenKey) -> Unit,
14+
onSelectSettingOption: (settingsRoute: SettingsRoutes) -> Unit,
1415
modifier: Modifier = Modifier
1516
) {
1617
Column(modifier = modifier) {
1718
TextPreference(
1819
title = R.string.settings_category_debug_title,
19-
onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) }
20+
onClick = { onSelectSettingOption(SettingsRoutes.SettingsDebugRoute) }
2021
)
2122
HorizontalDivider(color = Color.Black)
2223
}

app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ import androidx.compose.material3.Scaffold
1010
import androidx.compose.runtime.getValue
1111
import androidx.compose.runtime.mutableIntStateOf
1212
import androidx.compose.runtime.remember
13-
import androidx.compose.runtime.setValue
1413
import androidx.compose.ui.Modifier
1514
import androidx.compose.ui.res.stringResource
1615
import androidx.navigation.compose.NavHost
1716
import androidx.navigation.compose.composable
17+
import androidx.navigation.compose.currentBackStackEntryAsState
1818
import androidx.navigation.compose.rememberNavController
19-
import androidx.navigation.navArgument
2019
import dagger.hilt.android.AndroidEntryPoint
2120
import org.schabi.newpipe.R
2221
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
22+
import org.schabi.newpipe.ui.SettingsRoutes
2323
import org.schabi.newpipe.ui.Toolbar
2424
import org.schabi.newpipe.ui.theme.AppTheme
2525

26-
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
27-
2826
@AndroidEntryPoint
2927
class SettingsV2Activity : ComponentActivity() {
3028

@@ -35,37 +33,42 @@ class SettingsV2Activity : ComponentActivity() {
3533

3634
setContent {
3735
val navController = rememberNavController()
38-
var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) }
39-
navController.addOnDestinationChangedListener { _, _, arguments ->
40-
screenTitle =
41-
arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle
36+
val navBackStackEntry by navController.currentBackStackEntryAsState()
37+
@StringRes val screenTitleRes by remember(navBackStackEntry) {
38+
mutableIntStateOf(
39+
when (navBackStackEntry?.destination?.route) {
40+
SettingsRoutes.SettingsMainRoute::class.java.canonicalName -> SettingsRoutes.SettingsMainRoute.screenTitleRes
41+
SettingsRoutes.SettingsDebugRoute::class.java.canonicalName -> SettingsRoutes.SettingsDebugRoute.screenTitleRes
42+
else -> R.string.settings
43+
}
44+
)
4245
}
4346

4447
AppTheme {
4548
Scaffold(topBar = {
4649
Toolbar(
47-
title = stringResource(id = screenTitle),
50+
title = stringResource(screenTitleRes),
51+
onNavigateBack = {
52+
if (!navController.popBackStack()) {
53+
finish()
54+
}
55+
},
4856
hasSearch = true,
49-
onSearchQueryChange = null // TODO: Add suggestions logic
57+
onSearch = {},
58+
searchResults = emptyList()
5059
)
5160
}) { padding ->
5261
NavHost(
5362
navController = navController,
54-
startDestination = SettingsScreenKey.ROOT.name,
63+
startDestination = SettingsRoutes.SettingsMainRoute,
5564
modifier = Modifier.padding(padding)
5665
) {
57-
composable(
58-
SettingsScreenKey.ROOT.name,
59-
listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle))
60-
) {
61-
SettingsScreen(onSelectSettingOption = { screen ->
62-
navController.navigate(screen.name)
66+
composable<SettingsRoutes.SettingsMainRoute> {
67+
SettingsScreen(onSelectSettingOption = { route ->
68+
navController.navigate(route)
6369
})
6470
}
65-
composable(
66-
SettingsScreenKey.DEBUG.name,
67-
listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle))
68-
) {
71+
composable<SettingsRoutes.SettingsDebugRoute> {
6972
DebugScreen(settingsViewModel)
7073
}
7174
}
@@ -74,12 +77,3 @@ class SettingsV2Activity : ComponentActivity() {
7477
}
7578
}
7679
}
77-
78-
fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) {
79-
defaultValue = screenTitle
80-
}
81-
82-
enum class SettingsScreenKey(@StringRes val screenTitle: Int) {
83-
ROOT(R.string.settings),
84-
DEBUG(R.string.settings_category_debug_title)
85-
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.schabi.newpipe.ui
2+
3+
import androidx.annotation.StringRes
4+
import kotlinx.serialization.Serializable
5+
import org.schabi.newpipe.R
6+
7+
// Settings screens
8+
@Serializable
9+
sealed class SettingsRoutes(
10+
@get:StringRes
11+
val screenTitleRes: Int
12+
) {
13+
@Serializable
14+
object SettingsMainRoute : SettingsRoutes(R.string.settings)
15+
@Serializable
16+
object SettingsDebugRoute : SettingsRoutes(R.string.settings_category_debug_title)
17+
}

app/src/main/java/org/schabi/newpipe/ui/Toolbar.kt

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,60 @@
11
package org.schabi.newpipe.ui
22

3+
import androidx.compose.foundation.clickable
34
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.Column
56
import androidx.compose.foundation.layout.RowScope
6-
import androidx.compose.foundation.layout.fillMaxHeight
7+
import androidx.compose.foundation.layout.fillMaxSize
78
import androidx.compose.foundation.layout.fillMaxWidth
89
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.rememberScrollState
11+
import androidx.compose.foundation.text.input.rememberTextFieldState
12+
import androidx.compose.foundation.verticalScroll
913
import androidx.compose.material.icons.Icons
1014
import androidx.compose.material.icons.automirrored.filled.ArrowBack
1115
import androidx.compose.material3.ExperimentalMaterial3Api
1216
import androidx.compose.material3.Icon
1317
import androidx.compose.material3.IconButton
18+
import androidx.compose.material3.ListItem
1419
import androidx.compose.material3.MaterialTheme
1520
import androidx.compose.material3.SearchBar
21+
import androidx.compose.material3.SearchBarDefaults
1622
import androidx.compose.material3.Text
1723
import androidx.compose.material3.TopAppBar
24+
import androidx.compose.material3.TopAppBarDefaults
1825
import androidx.compose.runtime.Composable
1926
import androidx.compose.runtime.getValue
2027
import androidx.compose.runtime.mutableStateOf
2128
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.saveable.rememberSaveable
2230
import androidx.compose.runtime.setValue
2331
import androidx.compose.ui.Alignment
2432
import androidx.compose.ui.Modifier
2533
import androidx.compose.ui.res.painterResource
2634
import androidx.compose.ui.res.stringResource
35+
import androidx.compose.ui.semantics.isTraversalGroup
36+
import androidx.compose.ui.semantics.semantics
37+
import androidx.compose.ui.semantics.traversalIndex
2738
import androidx.compose.ui.tooling.preview.Preview
2839
import org.schabi.newpipe.R
2940
import org.schabi.newpipe.ui.theme.AppTheme
3041
import org.schabi.newpipe.ui.theme.SizeTokens
42+
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall
3143

3244
@Composable
3345
fun TextAction(text: String, modifier: Modifier = Modifier) {
3446
Text(text = text, color = MaterialTheme.colorScheme.onSurface, modifier = modifier)
3547
}
3648

3749
@Composable
38-
fun NavigationIcon() {
39-
Icon(
40-
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back",
41-
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
42-
)
50+
fun NavigationIcon(navigateBack: () -> Unit) {
51+
IconButton(onClick = navigateBack) {
52+
Icon(
53+
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
54+
contentDescription = "Back",
55+
modifier = Modifier.padding(horizontal = SizeTokens.SpacingExtraSmall)
56+
)
57+
}
4358
}
4459

4560
@Composable
@@ -53,19 +68,28 @@ fun SearchSuggestionItem(text: String) {
5368
fun Toolbar(
5469
title: String,
5570
modifier: Modifier = Modifier,
56-
hasNavigationIcon: Boolean = true,
71+
onNavigateBack: (() -> Unit)? = null,
5772
hasSearch: Boolean = false,
58-
onSearchQueryChange: ((String) -> List<String>)? = null,
73+
onSearch: (String) -> Unit,
74+
searchResults: List<String>,
5975
actions: @Composable RowScope.() -> Unit = {}
6076
) {
6177
var isSearchActive by remember { mutableStateOf(false) }
62-
var query by remember { mutableStateOf("") }
78+
var expanded by rememberSaveable { mutableStateOf(false) }
79+
val textFieldState = rememberTextFieldState()
6380

6481
Column {
6582
TopAppBar(
6683
title = { Text(text = title) },
6784
modifier = modifier,
68-
navigationIcon = { if (hasNavigationIcon) NavigationIcon() },
85+
colors = TopAppBarDefaults.topAppBarColors(
86+
containerColor = MaterialTheme.colorScheme.primaryContainer,
87+
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
88+
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
89+
),
90+
navigationIcon = {
91+
onNavigateBack?.let { NavigationIcon(onNavigateBack) }
92+
},
6993
actions = {
7094
actions()
7195
if (hasSearch) {
@@ -80,33 +104,62 @@ fun Toolbar(
80104
}
81105
)
82106
if (isSearchActive) {
83-
SearchBar(
84-
query = query,
85-
onQueryChange = { query = it },
86-
onSearch = {},
87-
placeholder = {
88-
Text(text = stringResource(id = R.string.search))
89-
},
90-
active = true,
91-
onActiveChange = {
92-
isSearchActive = it
93-
}
107+
Box(
108+
modifier
109+
.fillMaxSize()
110+
.semantics { isTraversalGroup = true }
94111
) {
95-
onSearchQueryChange?.invoke(query)?.takeIf { it.isNotEmpty() }
96-
?.map { suggestionText -> SearchSuggestionItem(text = suggestionText) }
97-
?: run {
112+
SearchBar(
113+
modifier = Modifier
114+
.align(Alignment.TopCenter)
115+
.semantics { traversalIndex = 0f },
116+
inputField = {
117+
SearchBarDefaults.InputField(
118+
query = textFieldState.text.toString(),
119+
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
120+
onSearch = {
121+
onSearch(textFieldState.text.toString())
122+
expanded = false
123+
},
124+
expanded = expanded,
125+
onExpandedChange = { expanded = it },
126+
placeholder = { Text(text = stringResource(id = R.string.search)) },
127+
modifier = Modifier.padding(horizontal = SpacingExtraSmall)
128+
)
129+
},
130+
expanded = expanded,
131+
onExpandedChange = { expanded = it },
132+
) {
133+
if (searchResults.isEmpty()) {
98134
Box(
99135
modifier = Modifier
100-
.fillMaxHeight()
101-
.fillMaxWidth(),
102-
contentAlignment = Alignment.Center
136+
.fillMaxSize()
137+
.padding(SpacingExtraSmall),
138+
contentAlignment = Alignment.Center,
103139
) {
104140
Column {
105141
Text(text = "╰(°●°╰)")
106142
Text(text = stringResource(id = R.string.search_no_results))
107143
}
108144
}
145+
} else {
146+
Column(Modifier.verticalScroll(rememberScrollState())) {
147+
searchResults.forEach { result ->
148+
ListItem(
149+
headlineContent = {
150+
SearchSuggestionItem(result)
151+
},
152+
modifier = Modifier
153+
.clickable {
154+
textFieldState.edit { replace(0, length, result) }
155+
expanded = false
156+
}
157+
.fillMaxWidth()
158+
)
159+
}
160+
}
109161
}
162+
}
110163
}
111164
}
112165
}
@@ -119,7 +172,8 @@ fun ToolbarPreview() {
119172
Toolbar(
120173
title = "Title",
121174
hasSearch = true,
122-
onSearchQueryChange = { emptyList() },
175+
onSearch = {},
176+
searchResults = emptyList(),
123177
actions = {
124178
TextAction(text = "Action1")
125179
TextAction(text = "Action2")

app/src/main/java/org/schabi/newpipe/ui/theme/Color.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.schabi.newpipe.ui.theme
22

3+
// Color.kt is generated using the Material theme builder https://material-foundation.github.io/material-theme-builder/
4+
// TODO: Update the colors to properly match the existing color scheme + also add colors schemes for other services
5+
36
import androidx.compose.ui.graphics.Color
47

58
val primaryLight = Color(0xFF904A45)

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p
137137
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
138138
kotlinx-coroutines-rx3 = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-rx3", version.ref = "kotlinxCoroutinesRx3" }
139139
kotlinx-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
140-
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
140+
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
141141
lazycolumnscrollbar = { group = "com.github.nanihadesuka", name = "LazyColumnScrollbar", version.ref = "lazycolumnscrollbar" }
142142
leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "leakcanary" }
143143
leakcanary-object-watcher = { group = "com.squareup.leakcanary", name = "leakcanary-object-watcher-android", version.ref = "leakcanary" }

0 commit comments

Comments
 (0)