Add settings module, refactor top bar

pull/330/head
Don Turner 2 years ago
parent b7500ea09b
commit 8c30ab9f80

@ -82,6 +82,7 @@ dependencies {
implementation(project(":feature:foryou"))
implementation(project(":feature:bookmarks"))
implementation(project(":feature:topic"))
implementation(project(":feature:settings"))
implementation(project(":core:common"))
implementation(project(":core:ui"))

@ -16,17 +16,22 @@
package com.google.samples.apps.nowinandroid.ui
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import com.google.samples.apps.nowinandroid.MainActivity
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
import com.google.samples.apps.nowinandroid.feature.interests.R as FeatureInterestsR
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
@ -67,6 +72,9 @@ class NavigationTest {
private lateinit var forYou: String
private lateinit var interests: String
private lateinit var sampleTopic: String
private lateinit var appName: String
private lateinit var saved: String
private lateinit var settings: String
@Before
fun setup() {
@ -77,6 +85,9 @@ class NavigationTest {
forYou = getString(FeatureForyouR.string.for_you)
interests = getString(FeatureInterestsR.string.interests)
sampleTopic = "Headlines"
appName = getString(R.string.app_name)
saved = getString(BookmarksR.string.saved)
settings = getString(SettingsR.string.top_app_bar_action_icon_description)
}
}
@ -146,6 +157,38 @@ class NavigationTest {
}
}
@Test
fun topLevelDestinations_showTopBarWithTitle() {
composeTestRule.apply {
// Verify that the top bar contains the app name on the first screen.
onNodeWithText(appName).assertExists()
// Go to the bookmarks tab, verify that the top bar contains the app name.
onNodeWithText(saved).performClick()
onNodeWithText(appName).assertExists()
// Go to the interests tab, verify that the top bar contains "Interests". This means
// we'll have 2 elements with the text "Interests" on screen. One in the top bar, and
// one in the bottom navigation.
onNodeWithText(interests).performClick()
onAllNodesWithText(interests).assertCountEquals(2)
}
}
@Test
fun topLevelDestinations_showSettingsIcon() {
composeTestRule.apply {
onNodeWithContentDescription(settings).assertExists()
onNodeWithText(saved).performClick()
onNodeWithContentDescription(settings).assertExists()
onNodeWithText(interests).performClick()
onNodeWithContentDescription(settings).assertExists()
}
}
/*
* There should always be at most one instance of a top-level destination at the same time.
*/

@ -87,6 +87,19 @@ class NiaAppStateTest {
assertTrue(state.topLevelDestinations[2].name.contains("interests", true))
}
@Test
fun niaAppState_showTopBarForTopLevelDestinations() {
composeTestRule.setContent {
val navController = rememberTestNavController()
state = rememberNiaAppState(
windowSizeClass = getCompactWindowClass(),
navController = navController
)
// Do nothing - we should already be
}
}
@Test
fun niaAppState_showBottomBar_compact() {
composeTestRule.setContent {

@ -23,6 +23,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as bookmarksR
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
import com.google.samples.apps.nowinandroid.R
/**
* Type for the top level destinations in the application. Each of these destinations
@ -32,21 +33,25 @@ import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
enum class TopLevelDestination(
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
val iconTextId: Int,
val titleTextId: Int
) {
FOR_YOU(
selectedIcon = DrawableResourceIcon(NiaIcons.Upcoming),
unselectedIcon = DrawableResourceIcon(NiaIcons.UpcomingBorder),
iconTextId = forYouR.string.for_you
iconTextId = forYouR.string.for_you,
titleTextId = R.string.app_name
),
BOOKMARKS(
selectedIcon = DrawableResourceIcon(NiaIcons.Bookmarks),
unselectedIcon = DrawableResourceIcon(NiaIcons.BookmarksBorder),
iconTextId = bookmarksR.string.saved
iconTextId = bookmarksR.string.saved,
titleTextId = bookmarksR.string.saved
),
INTERESTS(
selectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
unselectedIcon = ImageVectorIcon(NiaIcons.Grid3x3),
iconTextId = interestsR.string.interests
iconTextId = interestsR.string.interests,
titleTextId = interestsR.string.interests
)
}

@ -32,6 +32,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
@ -49,8 +50,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavig
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRail
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationRailItem
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.DrawableResourceIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVectorIcon
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.feature.settings.R as settingsR
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -78,6 +82,22 @@ fun NiaApp(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
topBar = {
val destination = appState.topLevelDestinations[appState.currentDestination?.route]
if (appState.shouldShowTopBar && destination != null) {
NiaTopAppBar(
titleRes = destination.titleTextId,
actionIcon = NiaIcons.Settings,
actionIconContentDescription = stringResource(
id = settingsR.string.top_app_bar_action_icon_description
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
),
onActionClick = { /*openAccountDialog = true*/ }
)
}
},
bottomBar = {
if (appState.shouldShowBottomBar) {
NiaBottomBar(

@ -59,6 +59,9 @@ class NiaAppState(
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
val shouldShowTopBar: Boolean
@Composable get() = (currentDestination?.route in topLevelDestinations)
val shouldShowBottomBar: Boolean
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
@ -67,7 +70,8 @@ class NiaAppState(
get() = !shouldShowBottomBar
/**
* Top level destinations to be used in the BottomBar and NavRail
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
* route.
*/
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()

@ -31,4 +31,5 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.secrets) apply false
id("org.jetbrains.kotlin.android") version "1.7.10" apply false
}

@ -40,6 +40,7 @@ message UserPreferences {
map<string, bool> followed_topic_ids = 13;
map<string, bool> followed_author_ids = 14;
map<string, bool> bookmarked_news_resource_ids = 15;
ThemeBrandProto theme_brand = 12;
DarkThemeConfigProto dark_theme_config = 13;
ThemeBrandProto theme_brand = 16;
DarkThemeConfigProto dark_theme_config = 17;
}

@ -32,7 +32,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.Preview
import com.google.samples.apps.nowinandroid.core.designsystem.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -97,7 +99,7 @@ fun NiaTopAppBar(
}
},
colors = colors,
modifier = modifier
modifier = modifier,
)
}

@ -32,6 +32,7 @@ import androidx.compose.material.icons.rounded.Grid3x3
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.ShortText
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.ViewDay
@ -64,6 +65,7 @@ object NiaIcons {
val Person = Icons.Rounded.Person
val PlayArrow = Icons.Rounded.PlayArrow
val Search = Icons.Rounded.Search
val Settings = Icons.Rounded.Settings
val ShortText = Icons.Rounded.ShortText
val Tag = Icons.Rounded.Tag
val Upcoming = R.drawable.ic_upcoming

@ -18,4 +18,5 @@
<string name="follow">Follow</string>
<string name="unfollow">Unfollow</string>
<string name="browse_topic">Browse topic</string>
<string name="screen_title">Screen title</string>
</resources>

@ -34,7 +34,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@ -46,8 +45,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@ -74,18 +71,6 @@ fun BookmarksScreen(
modifier: Modifier = Modifier
) {
Scaffold(
topBar = {
NiaTopAppBar(
titleRes = R.string.top_app_bar_title_saved,
actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource(
id = R.string.top_app_bar_action_menu
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
)
)
},
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->

@ -17,7 +17,7 @@
<resources>
<string name="saved">Saved</string>
<string name="saved_loading">Loading saved…</string>
<string name="top_app_bar_title_saved">Saved</string>
<string name="top_app_bar_title">Saved</string>
<string name="top_app_bar_action_search">Search</string>
<string name="top_app_bar_action_menu">Menu</string>
</resources>

@ -63,7 +63,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -93,7 +92,6 @@ import coil.compose.AsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilledButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
@ -173,19 +171,6 @@ internal fun ForYouScreen(
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
NiaTopAppBar(
titleRes = R.string.top_app_bar_title,
actionIcon = NiaIcons.AccountCircle,
actionIconContentDescription = stringResource(
id = R.string.for_you_top_app_bar_action_my_account
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
),
onActionClick = { openAccountDialog = true }
)
},
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->

@ -22,7 +22,6 @@
<string name="onboarding_guidance_title">What are you interested in?</string>
<string name="onboarding_guidance_subtitle">Updates from topics you follow will appear here. Follow some things to get started.</string>
<string name="top_app_bar_title">Now in Android</string>
<string name="for_you_top_app_bar_action_my_account">My account</string>
<string name="for_you_top_app_bar_action_search">Search</string>
<string name="for_you_not_connected">⚠️ You arent connected to the internet</string>

@ -19,12 +19,10 @@ package com.google.samples.apps.nowinandroid.feature.interests
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
@ -33,8 +31,6 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackg
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTab
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRow
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
@ -90,16 +86,6 @@ internal fun InterestsScreen(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
NiaTopAppBar(
titleRes = R.string.interests,
actionIcon = NiaIcons.MoreVert,
actionIconContentDescription = stringResource(
id = R.string.interests_top_app_bar_action_menu
),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
)
)
when (uiState) {
InterestsUiState.Loading ->
NiaLoadingWheel(

@ -15,6 +15,7 @@
limitations under the License.
-->
<resources>
<!-- TODO: Remove the redundant "interests" prefix -->
<string name="interests">Interests</string>
<string name="interests_topics">Topics</string>
<string name="interests_people">People</string>
@ -22,6 +23,7 @@
<string name="interests_empty_header">"No available data"</string>
<string name="interests_card_follow_button_content_desc">Follow interest button</string>
<string name="interests_card_unfollow_button_content_desc">Unfollow interest button</string>
<string name="interests_top_app_bar_title">Interests</string>
<string name="interests_top_app_bar_action_menu">Menu</string>
<string name="interests_top_app_bar_action_seearch">Search</string>
</resources>

@ -0,0 +1 @@
/build

@ -0,0 +1,24 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("nowinandroid.android.feature")
id("nowinandroid.android.library.compose")
id("nowinandroid.android.library.jacoco")
}
android {
namespace = "com.google.samples.apps.nowinandroid.feature.settings"
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="top_app_bar_action_icon_description">Settings</string>
</resources>

@ -51,6 +51,7 @@ include(":feature:foryou")
include(":feature:interests")
include(":feature:bookmarks")
include(":feature:topic")
include(":feature:settings")
include(":lint")
include(":sync:work")
include(":sync:sync-test")

Loading…
Cancel
Save