Merge branch 'main' of github.com:takagimeow/nowinandroid into fix/provide-true-to-local-inspection-mode

 Conflicts:
	core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt
pull/575/head
Keisuke Takagi 2 years ago
commit 8b51107d7a

@ -10,6 +10,9 @@ jobs:
android-ci: android-ci:
runs-on: macos-12 runs-on: macos-12
strategy:
matrix:
device-config: [ "pixel4api30aospatd", "pixelcapi30aospatd" ]
steps: steps:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
@ -23,7 +26,7 @@ jobs:
- name: Run instrumented tests with GMD - name: Run instrumented tests with GMD
run: ./gradlew cleanManagedDevices --unused-only && run: ./gradlew cleanManagedDevices --unused-only &&
./gradlew pixel4api30DemoDebugAndroidTest -Dorg.gradle.workers.max=1 ./gradlew ${{ matrix.device-config }}DemoDebugAndroidTest -Dorg.gradle.workers.max=1
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true --info -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true --info
- name: Upload test reports - name: Upload test reports

@ -34,7 +34,7 @@
</option> </option>
<option name="taskNames"> <option name="taskNames">
<list> <list>
<option value=":benchmark:pixel6Api31DemoBenchmarkAndroidTest" /> <option value=":benchmark:pixel6Api31atdDemoBenchmarkAndroidTest" />
<option value="--rerun" /> <option value="--rerun" />
<option value="--enable-display" /> <option value="--enable-display" />
</list> </list>

@ -38,7 +38,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaDropdownMenuButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
@ -167,23 +166,6 @@ fun NiaCatalog() {
} }
} }
item { Text("Dropdown menus", Modifier.padding(top = 16.dp)) } item { Text("Dropdown menus", Modifier.padding(top = 16.dp)) }
item {
FlowRow(mainAxisSpacing = 16.dp) {
NiaDropdownMenuButton(
text = { Text("Enabled") },
items = listOf("Item 1", "Item 2", "Item 3"),
onItemClick = {},
itemText = { item -> Text(item) },
)
NiaDropdownMenuButton(
text = { Text("Disabled") },
items = listOf("Item 1", "Item 2", "Item 3"),
onItemClick = {},
itemText = { item -> Text(item) },
enabled = false,
)
}
}
item { Text("Chips", Modifier.padding(top = 16.dp)) } item { Text("Chips", Modifier.padding(top = 16.dp)) }
item { item {
FlowRow(mainAxisSpacing = 16.dp) { FlowRow(mainAxisSpacing = 16.dp) {
@ -315,45 +297,19 @@ fun NiaCatalog() {
item { Text("Tags", Modifier.padding(top = 16.dp)) } item { Text("Tags", Modifier.padding(top = 16.dp)) }
item { item {
FlowRow(mainAxisSpacing = 16.dp) { FlowRow(mainAxisSpacing = 16.dp) {
var expandedTopicId by remember { mutableStateOf<String?>(null) }
var firstFollowed by remember { mutableStateOf(false) }
NiaTopicTag( NiaTopicTag(
expanded = expandedTopicId == "Topic 1", followed = true,
followed = firstFollowed, onClick = {},
onDropdownMenuToggle = { show ->
expandedTopicId = if (show) "Topic 1" else null
},
onFollowClick = { firstFollowed = true },
onUnfollowClick = { firstFollowed = false },
onBrowseClick = {},
text = { Text(text = "Topic 1".uppercase()) }, text = { Text(text = "Topic 1".uppercase()) },
followText = { Text(text = "Follow") },
unFollowText = { Text(text = "Unfollow") },
browseText = { Text(text = "Browse topic") },
) )
var secondFollowed by remember { mutableStateOf(true) }
NiaTopicTag( NiaTopicTag(
expanded = expandedTopicId == "Topic 2", followed = false,
followed = secondFollowed, onClick = {},
onDropdownMenuToggle = { show ->
expandedTopicId = if (show) "Topic 2" else null
},
onFollowClick = { secondFollowed = true },
onUnfollowClick = { secondFollowed = false },
onBrowseClick = {},
text = { Text(text = "Topic 2".uppercase()) }, text = { Text(text = "Topic 2".uppercase()) },
followText = { Text(text = "Follow") },
unFollowText = { Text(text = "Unfollow") },
browseText = { Text(text = "Browse topic") },
) )
NiaTopicTag( NiaTopicTag(
expanded = false,
followed = false, followed = false,
onDropdownMenuToggle = {}, onClick = {},
onFollowClick = {},
onUnfollowClick = {},
onBrowseClick = {},
text = { Text(text = "Disabled".uppercase()) }, text = { Text(text = "Disabled".uppercase()) },
enabled = false, enabled = false,
) )

@ -28,8 +28,8 @@ plugins {
android { android {
defaultConfig { defaultConfig {
applicationId = "com.google.samples.apps.nowinandroid" applicationId = "com.google.samples.apps.nowinandroid"
versionCode = 3 versionCode = 4
versionName = "0.0.3" // X.Y.Z; X = Major, Y = minor, Z = Patch level versionName = "0.0.4" // X.Y.Z; X = Major, Y = minor, Z = Patch level
// Custom test runner to set up Hilt dependency graph // Custom test runner to set up Hilt dependency graph
testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"

@ -37,7 +37,6 @@ import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
@Composable @Composable
fun NiaNavHost( fun NiaNavHost(
navController: NavHostController, navController: NavHostController,
onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startDestination: String = forYouNavigationRoute, startDestination: String = forYouNavigationRoute,
) { ) {
@ -46,14 +45,18 @@ fun NiaNavHost(
startDestination = startDestination, startDestination = startDestination,
modifier = modifier, modifier = modifier,
) { ) {
forYouScreen() // TODO: handle topic clicks from each top level destination
bookmarksScreen() forYouScreen(onTopicClick = {})
bookmarksScreen(onTopicClick = {})
interestsGraph( interestsGraph(
navigateToTopic = { topicId -> onTopicClick = { topicId ->
navController.navigateToTopic(topicId) navController.navigateToTopic(topicId)
}, },
nestedGraphs = { nestedGraphs = {
topicScreen(onBackClick) topicScreen(
onBackClick = navController::popBackStack,
onTopicClick = {},
)
}, },
) )
} }

@ -178,10 +178,7 @@ fun NiaApp(
) )
} }
NiaNavHost( NiaNavHost(appState.navController)
navController = appState.navController,
onBackClick = appState::onBackClick,
)
} }
// TODO: We may want to add padding or spacer when the snackbar is shown so that // TODO: We may want to add padding or spacer when the snackbar is shown so that

@ -136,10 +136,6 @@ class NiaAppState(
} }
} }
fun onBackClick() {
navController.popBackStack()
}
fun setShowSettingsDialog(shouldShow: Boolean) { fun setShowSettingsDialog(shouldShow: Boolean) {
shouldShowSettingsDialog = shouldShow shouldShowSettingsDialog = shouldShow
} }

@ -65,19 +65,6 @@ android {
targetProjectPath = ":app" targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true experimentalProperties["android.experimental.self-instrumenting"] = true
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
create<ManagedVirtualDevice>("pixel6Api31") {
device = "Pixel 6"
apiLevel = 31
systemImageSource = "aosp"
}
}
}
}
} }
dependencies { dependencies {

@ -28,6 +28,7 @@ java {
dependencies { dependencies {
compileOnly(libs.android.gradlePlugin) compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
} }
gradlePlugin { gradlePlugin {
@ -68,6 +69,10 @@ gradlePlugin {
id = "nowinandroid.android.hilt" id = "nowinandroid.android.hilt"
implementationClass = "AndroidHiltConventionPlugin" implementationClass = "AndroidHiltConventionPlugin"
} }
register("androidRoom") {
id = "nowinandroid.android.room"
implementationClass = "AndroidRoomConventionPlugin"
}
register("firebase-perf") { register("firebase-perf") {
id = "nowinandroid.firebase-perf" id = "nowinandroid.firebase-perf"
implementationClass = "FirebasePerfConventionPlugin" implementationClass = "FirebasePerfConventionPlugin"

@ -17,6 +17,7 @@
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import com.google.samples.apps.nowinandroid.configureFlavors import com.google.samples.apps.nowinandroid.configureFlavors
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import org.gradle.api.Plugin import org.gradle.api.Plugin
@ -35,6 +36,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 33 defaultConfig.targetSdk = 33
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this)
} }
extensions.configure<ApplicationAndroidComponentsExtension> { extensions.configure<ApplicationAndroidComponentsExtension> {
configurePrintApksTask(this) configurePrintApksTask(this)

@ -14,7 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.gradle.LibraryExtension import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.artifacts.VersionCatalogsExtension
@ -35,6 +37,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
testInstrumentationRunner = testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
} }
configureGradleManagedDevices(this)
} }
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs") val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

@ -18,6 +18,7 @@ import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.LibraryExtension import com.android.build.gradle.LibraryExtension
import com.google.samples.apps.nowinandroid.configureFlavors import com.google.samples.apps.nowinandroid.configureFlavors
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import org.gradle.api.Plugin import org.gradle.api.Plugin
@ -40,6 +41,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 33 defaultConfig.targetSdk = 33
configureFlavors(this) configureFlavors(this)
configureGradleManagedDevices(this)
} }
extensions.configure<LibraryAndroidComponentsExtension> { extensions.configure<LibraryAndroidComponentsExtension> {
configurePrintApksTask(this) configurePrintApksTask(this)

@ -0,0 +1,63 @@
/*
* 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.
*/
import com.google.devtools.ksp.gradle.KspExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
import org.gradle.process.CommandLineArgumentProvider
import java.io.File
class AndroidRoomConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.google.devtools.ksp")
extensions.configure<KspExtension> {
// The schemas directory contains a schema file for each version of the Room database.
// This is required to enable Room auto migrations.
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
add("implementation", libs.findLibrary("room.runtime").get())
add("implementation", libs.findLibrary("room.ktx").get())
add("ksp", libs.findLibrary("room.compiler").get())
}
}
}
/**
* https://issuetracker.google.com/issues/132245929
* [Export schemas](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
*/
class RoomSchemaArgProvider(
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
val schemaDir: File,
) : CommandLineArgumentProvider {
override fun asArguments() = listOf("room.schemaLocation=${schemaDir.path}")
}
}

@ -15,6 +15,7 @@
*/ */
import com.android.build.gradle.TestExtension import com.android.build.gradle.TestExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
@ -31,6 +32,7 @@ class AndroidTestConventionPlugin : Plugin<Project> {
extensions.configure<TestExtension> { extensions.configure<TestExtension> {
configureKotlinAndroid(this) configureKotlinAndroid(this)
defaultConfig.targetSdk = 31 defaultConfig.targetSdk = 31
configureGradleManagedDevices(this)
} }
} }
} }

@ -0,0 +1,63 @@
/*
* Copyright 2023 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.
*/
package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.ManagedVirtualDevice
import org.gradle.api.Project
import org.gradle.kotlin.dsl.invoke
import java.util.Locale
/**
* Configure project for Gradle managed devices
*/
internal fun configureGradleManagedDevices(
commonExtension: CommonExtension<*, *, *, *>,
) {
val deviceConfigs = listOf(
DeviceConfig("Pixel 4", 30, "aosp-atd"),
DeviceConfig("Pixel 6", 31, "aosp"),
DeviceConfig("Pixel C", 30, "aosp-atd"),
)
commonExtension.testOptions {
managedDevices {
devices {
deviceConfigs.forEach { deviceConfig ->
maybeCreate(deviceConfig.taskName, ManagedVirtualDevice::class.java).apply {
device = deviceConfig.device
apiLevel = deviceConfig.apiLevel
systemImageSource = deviceConfig.systemImageSource
}
}
}
}
}
}
private data class DeviceConfig(
val device: String,
val apiLevel: Int,
val systemImageSource: String,
) {
val taskName = buildString {
append(device.toLowerCase(Locale.ROOT).replace(" ", ""))
append("api")
append(apiLevel.toString())
append(systemImageSource.replace("-", ""))
}
}

@ -40,8 +40,8 @@ internal fun Project.configureKotlinAndroid(
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
@ -59,8 +59,8 @@ internal fun Project.configureKotlinAndroid(
"-opt-in=kotlin.Experimental", "-opt-in=kotlin.Experimental",
) )
// Set JVM target to 1.8 // Set JVM target to 11
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_11.toString()
} }
} }

@ -30,5 +30,6 @@ plugins {
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.hilt) apply false alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.secrets) apply false alias(libs.plugins.secrets) apply false
} }

@ -22,18 +22,11 @@ plugins {
id("nowinandroid.android.library") id("nowinandroid.android.library")
id("nowinandroid.android.library.jacoco") id("nowinandroid.android.library.jacoco")
id("nowinandroid.android.hilt") id("nowinandroid.android.hilt")
alias(libs.plugins.ksp) id("nowinandroid.android.room")
} }
android { android {
defaultConfig { defaultConfig {
// The schemas directory contains a schema file for each version of the Room database.
// This is required to enable Room auto migrations.
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
testInstrumentationRunner = testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
} }
@ -57,10 +50,6 @@ android {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)

@ -1,218 +0,0 @@
/*
* 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.
*/
package com.google.samples.apps.nowinandroid.core.designsystem.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
/**
* Now in Android dropdown menu button with included trailing icon as well as text label and item
* content slots.
*
* @param items The list of items to display in the menu.
* @param onItemClick Called when the user clicks on a menu item.
* @param modifier Modifier to be applied to the button.
* @param enabled Controls the enabled state of the button. When `false`, this button will not be
* clickable and will appear disabled to accessibility services.
* @param dismissOnItemClick Whether the menu should be dismissed when an item is clicked.
* @param itemText The text label content for a given item.
* @param itemLeadingIcon The leading icon content for a given item.
* @param itemTrailingIcon The trailing icon content for a given item.
*/
@Composable
fun <T> NiaDropdownMenuButton(
items: List<T>,
onItemClick: (item: T) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
dismissOnItemClick: Boolean = true,
text: @Composable () -> Unit,
itemText: @Composable (item: T) -> Unit,
itemLeadingIcon: @Composable ((item: T) -> Unit)? = null,
itemTrailingIcon: @Composable ((item: T) -> Unit)? = null,
) {
var expanded by remember { mutableStateOf(false) }
Box(modifier = modifier) {
OutlinedButton(
onClick = { expanded = true },
enabled = enabled,
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground,
),
border = BorderStroke(
width = NiaDropdownMenuDefaults.DropdownMenuButtonBorderWidth,
color = if (enabled) {
MaterialTheme.colorScheme.outline
} else {
MaterialTheme.colorScheme.onSurface.copy(
alpha = NiaDropdownMenuDefaults.DisabledDropdownMenuButtonBorderAlpha,
)
},
),
contentPadding = NiaDropdownMenuDefaults.DropdownMenuButtonContentPadding,
) {
NiaDropdownMenuButtonContent(
text = text,
trailingIcon = {
Icon(
imageVector = if (expanded) {
NiaIcons.ArrowDropUp
} else {
NiaIcons.ArrowDropDown
},
contentDescription = null,
)
},
)
}
NiaDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
items = items,
onItemClick = onItemClick,
dismissOnItemClick = dismissOnItemClick,
itemText = itemText,
itemLeadingIcon = itemLeadingIcon,
itemTrailingIcon = itemTrailingIcon,
)
}
}
/**
* Internal Now in Android dropdown menu button content layout for arranging the text label and
* trailing icon.
*
* @param text The button text label content.
* @param trailingIcon The button trailing icon content. Default is `null` for no trailing icon.
*/
@Composable
private fun NiaDropdownMenuButtonContent(
text: @Composable () -> Unit,
trailingIcon: @Composable (() -> Unit)? = null,
) {
Box(
Modifier
.padding(
end = if (trailingIcon != null) {
ButtonDefaults.IconSpacing
} else {
0.dp
},
),
) {
ProvideTextStyle(value = MaterialTheme.typography.labelSmall) {
text()
}
}
if (trailingIcon != null) {
Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) {
trailingIcon()
}
}
}
/**
* Now in Android dropdown menu with item content slots. Wraps Material 3 [DropdownMenu] and
* [DropdownMenuItem].
*
* @param expanded Whether the menu is currently open and visible to the user.
* @param onDismissRequest Called when the user requests to dismiss the menu, such as by
* tapping outside the menu's bounds.
* @param items The list of items to display in the menu.
* @param onItemClick Called when the user clicks on a menu item.
* @param dismissOnItemClick Whether the menu should be dismissed when an item is clicked.
* @param itemText The text label content for a given item.
* @param itemLeadingIcon The leading icon content for a given item.
* @param itemTrailingIcon The trailing icon content for a given item.
*/
@Composable
fun <T> NiaDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
items: List<T>,
onItemClick: (item: T) -> Unit,
dismissOnItemClick: Boolean = true,
itemText: @Composable (item: T) -> Unit,
itemLeadingIcon: @Composable ((item: T) -> Unit)? = null,
itemTrailingIcon: @Composable ((item: T) -> Unit)? = null,
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
) {
items.forEach { item ->
DropdownMenuItem(
text = { itemText(item) },
onClick = {
onItemClick(item)
if (dismissOnItemClick) onDismissRequest()
},
leadingIcon = if (itemLeadingIcon != null) {
{ itemLeadingIcon(item) }
} else {
null
},
trailingIcon = if (itemTrailingIcon != null) {
{ itemTrailingIcon(item) }
} else {
null
},
)
}
}
}
/**
* Now in Android dropdown menu default values.
*/
object NiaDropdownMenuDefaults {
// TODO: File bug
// OutlinedButton border color doesn't respect disabled state by default
const val DisabledDropdownMenuButtonBorderAlpha = 0.12f
// TODO: File bug
// OutlinedButton default border width isn't exposed via ButtonDefaults
val DropdownMenuButtonBorderWidth = 1.dp
// TODO: File bug
// Various default button padding values aren't exposed via ButtonDefaults
val DropdownMenuButtonContentPadding =
PaddingValues(
start = 24.dp,
top = 8.dp,
end = 16.dp,
bottom = 8.dp,
)
}

@ -20,28 +20,18 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.samples.apps.nowinandroid.core.designsystem.R
@Composable @Composable
fun NiaTopicTag( fun NiaTopicTag(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
expanded: Boolean = false,
followed: Boolean, followed: Boolean,
onDropdownMenuToggle: (show: Boolean) -> Unit = {}, onClick: () -> Unit,
onFollowClick: () -> Unit,
onUnfollowClick: () -> Unit,
onBrowseClick: () -> Unit,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable () -> Unit, text: @Composable () -> Unit,
followText: @Composable () -> Unit = { Text(stringResource(R.string.follow)) },
unFollowText: @Composable () -> Unit = { Text(stringResource(R.string.unfollow)) },
browseText: @Composable () -> Unit = { Text(stringResource(R.string.browse_topic)) },
) { ) {
Box(modifier = modifier) { Box(modifier = modifier) {
val containerColor = if (followed) { val containerColor = if (followed) {
@ -52,7 +42,7 @@ fun NiaTopicTag(
) )
} }
TextButton( TextButton(
onClick = { onDropdownMenuToggle(true) }, onClick = onClick,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
containerColor = containerColor, containerColor = containerColor,
@ -66,25 +56,6 @@ fun NiaTopicTag(
text() text()
} }
} }
NiaDropdownMenu(
expanded = expanded,
onDismissRequest = { onDropdownMenuToggle(false) },
items = if (followed) listOf(UNFOLLOW, BROWSE) else listOf(FOLLOW, BROWSE),
onItemClick = { item ->
when (item) {
FOLLOW -> onFollowClick()
UNFOLLOW -> onUnfollowClick()
BROWSE -> onBrowseClick()
}
},
itemText = { item ->
when (item) {
FOLLOW -> followText()
UNFOLLOW -> unFollowText()
BROWSE -> browseText()
}
},
)
} }
} }
@ -98,7 +69,3 @@ object NiaTagDefaults {
// Button disabled container alpha value not exposed by ButtonDefaults // Button disabled container alpha value not exposed by ButtonDefaults
const val DisabledTopicTagContainerAlpha = 0.12f const val DisabledTopicTagContainerAlpha = 0.12f
} }
private const val FOLLOW = 1
private const val UNFOLLOW = 2
private const val BROWSE = 3

@ -1,21 +0,0 @@
<?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="follow">Follow</string>
<string name="unfollow">Unfollow</string>
<string name="browse_topic">Browse topic</string>
</resources>

@ -41,6 +41,7 @@ class NewsResourceCardTest {
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {}, onClick = {},
onTopicClick = {},
) )
dateFormatted = dateFormatted(publishDate = newsWithKnownResourceType.publishDate) dateFormatted = dateFormatted(publishDate = newsWithKnownResourceType.publishDate)
@ -68,6 +69,7 @@ class NewsResourceCardTest {
isBookmarked = false, isBookmarked = false,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {}, onClick = {},
onTopicClick = {},
) )
dateFormatted = dateFormatted(publishDate = newsWithUnknownResourceType.publishDate) dateFormatted = dateFormatted(publishDate = newsWithUnknownResourceType.publishDate)
@ -81,7 +83,10 @@ class NewsResourceCardTest {
@Test @Test
fun testTopicsChipColorBackground_matchesFollowedState() { fun testTopicsChipColorBackground_matchesFollowedState() {
composeTestRule.setContent { composeTestRule.setContent {
NewsResourceTopics(topics = followableTopicTestData) NewsResourceTopics(
topics = followableTopicTestData,
onTopicClick = {},
)
} }
for (followableTopic in followableTopicTestData) { for (followableTopic in followableTopicTestData) {

@ -47,6 +47,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
fun LazyGridScope.newsFeed( fun LazyGridScope.newsFeed(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
) { ) {
when (feedState) { when (feedState) {
NewsFeedUiState.Loading -> Unit NewsFeedUiState.Loading -> Unit
@ -68,6 +69,7 @@ fun LazyGridScope.newsFeed(
!userNewsResource.isSaved, !userNewsResource.isSaved,
) )
}, },
onTopicClick = onTopicClick,
) )
} }
} }
@ -112,6 +114,7 @@ private fun NewsFeedLoadingPreview() {
newsFeed( newsFeed(
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -129,6 +132,7 @@ private fun NewsFeedContentPreview(
newsFeed( newsFeed(
feedState = NewsFeedUiState.Success(userNewsResources), feedState = NewsFeedUiState.Success(userNewsResources),
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }

@ -80,6 +80,7 @@ fun NewsResourceCardExpanded(
isBookmarked: Boolean, isBookmarked: Boolean,
onToggleBookmark: () -> Unit, onToggleBookmark: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val clickActionLabel = stringResource(R.string.card_tap_action) val clickActionLabel = stringResource(R.string.card_tap_action)
@ -117,7 +118,10 @@ fun NewsResourceCardExpanded(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(userNewsResource.content) NewsResourceShortDescription(userNewsResource.content)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
NewsResourceTopics(userNewsResource.followableTopics) NewsResourceTopics(
topics = userNewsResource.followableTopics,
onTopicClick = onTopicClick,
)
} }
} }
} }
@ -232,26 +236,17 @@ fun NewsResourceShortDescription(
@Composable @Composable
fun NewsResourceTopics( fun NewsResourceTopics(
topics: List<FollowableTopic>, topics: List<FollowableTopic>,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
// Store the ID of the Topic which has its "following" menu expanded, if any.
// To avoid UI confusion, only one topic can have an expanded menu at a time.
var expandedTopicId by remember { mutableStateOf<String?>(null) }
Row( Row(
modifier = modifier.horizontalScroll(rememberScrollState()), // causes narrow chips modifier = modifier.horizontalScroll(rememberScrollState()), // causes narrow chips
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
for (followableTopic in topics) { for (followableTopic in topics) {
NiaTopicTag( NiaTopicTag(
expanded = expandedTopicId == followableTopic.topic.id,
followed = followableTopic.isFollowed, followed = followableTopic.isFollowed,
onDropdownMenuToggle = { show -> onClick = { onTopicClick(followableTopic.topic.id) },
expandedTopicId = if (show) followableTopic.topic.id else null
},
onFollowClick = { }, // ToDo
onUnfollowClick = { }, // ToDo
onBrowseClick = { }, // ToDo
text = { text = {
val contentDescription = if (followableTopic.isFollowed) { val contentDescription = if (followableTopic.isFollowed) {
stringResource( stringResource(
@ -303,7 +298,7 @@ private fun ExpandedNewsResourcePreview(
userNewsResources: List<UserNewsResource>, userNewsResources: List<UserNewsResource>,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalInspectionMode provides true LocalInspectionMode provides true,
) { ) {
NiaTheme { NiaTheme {
Surface { Surface {
@ -312,6 +307,7 @@ private fun ExpandedNewsResourcePreview(
isBookmarked = true, isBookmarked = true,
onToggleBookmark = {}, onToggleBookmark = {},
onClick = {}, onClick = {},
onTopicClick = {},
) )
} }
} }

@ -37,6 +37,7 @@ fun LazyListScope.userNewsResourceCardItems(
items: List<UserNewsResource>, items: List<UserNewsResource>,
onToggleBookmark: (item: UserNewsResource) -> Unit, onToggleBookmark: (item: UserNewsResource) -> Unit,
onItemClick: ((item: UserNewsResource) -> Unit)? = null, onItemClick: ((item: UserNewsResource) -> Unit)? = null,
onTopicClick: (String) -> Unit,
itemModifier: Modifier = Modifier, itemModifier: Modifier = Modifier,
) = items( ) = items(
items = items, items = items,
@ -56,6 +57,7 @@ fun LazyListScope.userNewsResourceCardItems(
else -> onItemClick(userNewsResource) else -> onItemClick(userNewsResource)
} }
}, },
onTopicClick = onTopicClick,
modifier = itemModifier, modifier = itemModifier,
) )
}, },

@ -194,7 +194,7 @@ To write data, the repository provides suspend functions. It is up to the caller
_Example: Follow a topic_ _Example: Follow a topic_
Simply call `TopicsRepository.setFollowedTopicId` with the ID of the topic which the user wishes to follow. Simply call `UserDataRepository.toggleFollowedTopicId` with the ID of the topic the user wishes to follow and `followed=true` to indicate that the topic should be followed (use `false` to unfollow a topic).
### Data sources ### Data sources
@ -309,7 +309,7 @@ User actions are communicated from UI elements to ViewModels using regular metho
**Example: Following a topic** **Example: Following a topic**
The `InterestsScreen` takes a lambda expression named `followTopic` which is supplied from `InterestsViewModel.followTopic`. Each time the user taps on a topic to follow this method is called. The ViewModel then processes this action by informing the topics repository. The `InterestsScreen` takes a lambda expression named `followTopic` which is supplied from `InterestsViewModel.followTopic`. Each time the user taps on a topic to follow this method is called. The ViewModel then processes this action by informing the user data repository.
## Further reading ## Further reading

@ -24,20 +24,6 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks" namespace = "com.google.samples.apps.nowinandroid.feature.bookmarks"
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
maybeCreate<ManagedVirtualDevice>("pixel4api30").apply {
device = "Pixel 4"
apiLevel = 30
// ATDs currently support only API level 30.
systemImageSource = "aosp-atd"
}
}
}
}
} }
dependencies { dependencies {

@ -50,7 +50,8 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
removeFromBookmarks = { }, removeFromBookmarks = {},
onTopicClick = {},
) )
} }
@ -68,7 +69,8 @@ class BookmarksScreenTest {
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
userNewsResourcesTestData.take(2), userNewsResourcesTestData.take(2),
), ),
removeFromBookmarks = { }, removeFromBookmarks = {},
onTopicClick = {},
) )
} }
@ -110,6 +112,7 @@ class BookmarksScreenTest {
assertEquals(userNewsResourcesTestData[0].id, newsResourceId) assertEquals(userNewsResourcesTestData[0].id, newsResourceId)
removeFromBookmarksCalled = true removeFromBookmarksCalled = true
}, },
onTopicClick = {},
) )
} }
@ -138,7 +141,8 @@ class BookmarksScreenTest {
composeTestRule.setContent { composeTestRule.setContent {
BookmarksScreen( BookmarksScreen(
feedState = NewsFeedUiState.Success(emptyList()), feedState = NewsFeedUiState.Success(emptyList()),
removeFromBookmarks = { }, removeFromBookmarks = {},
onTopicClick = {},
) )
} }

@ -66,6 +66,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun BookmarksRoute( internal fun BookmarksRoute(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: BookmarksViewModel = hiltViewModel(), viewModel: BookmarksViewModel = hiltViewModel(),
) { ) {
@ -73,6 +74,7 @@ internal fun BookmarksRoute(
BookmarksScreen( BookmarksScreen(
feedState = feedState, feedState = feedState,
removeFromBookmarks = viewModel::removeFromSavedResources, removeFromBookmarks = viewModel::removeFromSavedResources,
onTopicClick = onTopicClick,
modifier = modifier, modifier = modifier,
) )
} }
@ -85,12 +87,13 @@ internal fun BookmarksRoute(
internal fun BookmarksScreen( internal fun BookmarksScreen(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit, removeFromBookmarks: (String) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (feedState) { when (feedState) {
Loading -> LoadingState(modifier) Loading -> LoadingState(modifier)
is Success -> if (feedState.feed.isNotEmpty()) { is Success -> if (feedState.feed.isNotEmpty()) {
BookmarksGrid(feedState, removeFromBookmarks, modifier) BookmarksGrid(feedState, removeFromBookmarks, onTopicClick, modifier)
} else { } else {
EmptyState(modifier) EmptyState(modifier)
} }
@ -112,6 +115,7 @@ private fun LoadingState(modifier: Modifier = Modifier) {
private fun BookmarksGrid( private fun BookmarksGrid(
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit, removeFromBookmarks: (String) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollableState = rememberLazyGridState() val scrollableState = rememberLazyGridState()
@ -129,6 +133,7 @@ private fun BookmarksGrid(
newsFeed( newsFeed(
feedState = feedState, feedState = feedState,
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) }, onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
onTopicClick = onTopicClick,
) )
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
@ -193,6 +198,7 @@ private fun BookmarksGridPreview(
BookmarksGrid( BookmarksGrid(
feedState = Success(userNewsResources), feedState = Success(userNewsResources),
removeFromBookmarks = {}, removeFromBookmarks = {},
onTopicClick = {},
) )
} }
} }

@ -28,8 +28,8 @@ fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) {
this.navigate(bookmarksRoute, navOptions) this.navigate(bookmarksRoute, navOptions)
} }
fun NavGraphBuilder.bookmarksScreen() { fun NavGraphBuilder.bookmarksScreen(onTopicClick: (String) -> Unit) {
composable(route = bookmarksRoute) { composable(route = bookmarksRoute) {
BookmarksRoute() BookmarksRoute(onTopicClick)
} }
} }

@ -24,20 +24,6 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.foryou" namespace = "com.google.samples.apps.nowinandroid.feature.foryou"
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
maybeCreate<ManagedVirtualDevice>("pixel4api30").apply {
device = "Pixel 4"
apiLevel = 30
// ATDs currently support only API level 30.
systemImageSource = "aosp-atd"
}
}
}
}
} }
dependencies { dependencies {

@ -53,6 +53,7 @@ class ForYouScreenTest {
onboardingUiState = OnboardingUiState.Loading, onboardingUiState = OnboardingUiState.Loading,
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -75,6 +76,7 @@ class ForYouScreenTest {
onboardingUiState = OnboardingUiState.NotShown, onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Success(emptyList()), feedState = NewsFeedUiState.Success(emptyList()),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -103,6 +105,7 @@ class ForYouScreenTest {
feed = emptyList(), feed = emptyList(),
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -146,6 +149,7 @@ class ForYouScreenTest {
feed = emptyList(), feed = emptyList(),
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -182,6 +186,7 @@ class ForYouScreenTest {
OnboardingUiState.Shown(topics = followableTopicTestData), OnboardingUiState.Shown(topics = followableTopicTestData),
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -204,6 +209,7 @@ class ForYouScreenTest {
onboardingUiState = OnboardingUiState.NotShown, onboardingUiState = OnboardingUiState.NotShown,
feedState = NewsFeedUiState.Loading, feedState = NewsFeedUiState.Loading,
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )
@ -227,6 +233,7 @@ class ForYouScreenTest {
feed = userNewsResourcesTestData, feed = userNewsResourcesTestData,
), ),
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
onTopicClick = {},
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
) )

@ -92,6 +92,7 @@ import com.google.samples.apps.nowinandroid.core.ui.newsFeed
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun ForYouRoute( internal fun ForYouRoute(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel(), viewModel: ForYouViewModel = hiltViewModel(),
) { ) {
@ -104,6 +105,7 @@ internal fun ForYouRoute(
onboardingUiState = onboardingUiState, onboardingUiState = onboardingUiState,
feedState = feedState, feedState = feedState,
onTopicCheckedChanged = viewModel::updateTopicSelection, onTopicCheckedChanged = viewModel::updateTopicSelection,
onTopicClick = onTopicClick,
saveFollowedTopics = viewModel::dismissOnboarding, saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved, onNewsResourcesCheckedChanged = viewModel::updateNewsResourceSaved,
modifier = modifier, modifier = modifier,
@ -116,6 +118,7 @@ internal fun ForYouScreen(
onboardingUiState: OnboardingUiState, onboardingUiState: OnboardingUiState,
feedState: NewsFeedUiState, feedState: NewsFeedUiState,
onTopicCheckedChanged: (String, Boolean) -> Unit, onTopicCheckedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
saveFollowedTopics: () -> Unit, saveFollowedTopics: () -> Unit,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit, onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -175,6 +178,7 @@ internal fun ForYouScreen(
newsFeed( newsFeed(
feedState = feedState, feedState = feedState,
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged, onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
onTopicClick = onTopicClick,
) )
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
@ -407,6 +411,7 @@ fun ForYouScreenPopulatedFeed(
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -429,6 +434,7 @@ fun ForYouScreenOfflinePopulatedFeed(
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -453,6 +459,7 @@ fun ForYouScreenTopicSelection(
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -470,6 +477,7 @@ fun ForYouScreenLoading() {
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -492,6 +500,7 @@ fun ForYouScreenPopulatedAndLoading(
onTopicCheckedChanged = { _, _ -> }, onTopicCheckedChanged = { _, _ -> },
saveFollowedTopics = {}, saveFollowedTopics = {},
onNewsResourcesCheckedChanged = { _, _ -> }, onNewsResourcesCheckedChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }

@ -28,8 +28,8 @@ fun NavController.navigateToForYou(navOptions: NavOptions? = null) {
this.navigate(forYouNavigationRoute, navOptions) this.navigate(forYouNavigationRoute, navOptions)
} }
fun NavGraphBuilder.forYouScreen() { fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) {
composable(route = forYouNavigationRoute) { composable(route = forYouNavigationRoute) {
ForYouRoute() ForYouRoute(onTopicClick)
} }
} }

@ -23,18 +23,4 @@ plugins {
} }
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.interests" namespace = "com.google.samples.apps.nowinandroid.feature.interests"
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
maybeCreate<ManagedVirtualDevice>("pixel4api30").apply {
device = "Pixel 4"
apiLevel = 30
// ATDs currently support only API level 30.
systemImageSource = "aosp-atd"
}
}
}
}
} }

@ -109,7 +109,7 @@ class InterestsScreenTest {
InterestsScreen( InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = { _, _ -> }, followTopic = { _, _ -> },
navigateToTopic = {}, onTopicClick = {},
) )
} }
} }

@ -37,7 +37,7 @@ import com.google.samples.apps.nowinandroid.core.ui.FollowableTopicPreviewParame
@OptIn(ExperimentalLifecycleComposeApi::class) @OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun InterestsRoute( internal fun InterestsRoute(
navigateToTopic: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: InterestsViewModel = hiltViewModel(), viewModel: InterestsViewModel = hiltViewModel(),
) { ) {
@ -46,7 +46,7 @@ internal fun InterestsRoute(
InterestsScreen( InterestsScreen(
uiState = uiState, uiState = uiState,
followTopic = viewModel::followTopic, followTopic = viewModel::followTopic,
navigateToTopic = navigateToTopic, onTopicClick = onTopicClick,
modifier = modifier, modifier = modifier,
) )
} }
@ -55,7 +55,7 @@ internal fun InterestsRoute(
internal fun InterestsScreen( internal fun InterestsScreen(
uiState: InterestsUiState, uiState: InterestsUiState,
followTopic: (String, Boolean) -> Unit, followTopic: (String, Boolean) -> Unit,
navigateToTopic: (String) -> Unit, onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -71,7 +71,7 @@ internal fun InterestsScreen(
is InterestsUiState.Interests -> is InterestsUiState.Interests ->
TopicsTabContent( TopicsTabContent(
topics = uiState.topics, topics = uiState.topics,
onTopicClick = navigateToTopic, onTopicClick = onTopicClick,
onFollowButtonClick = followTopic, onFollowButtonClick = followTopic,
modifier = modifier, modifier = modifier,
) )
@ -98,7 +98,7 @@ fun InterestsScreenPopulated(
topics = followableTopics, topics = followableTopics,
), ),
followTopic = { _, _ -> }, followTopic = { _, _ -> },
navigateToTopic = {}, onTopicClick = {},
) )
} }
} }
@ -112,7 +112,7 @@ fun InterestsScreenLoading() {
InterestsScreen( InterestsScreen(
uiState = InterestsUiState.Loading, uiState = InterestsUiState.Loading,
followTopic = { _, _ -> }, followTopic = { _, _ -> },
navigateToTopic = {}, onTopicClick = {},
) )
} }
} }
@ -126,7 +126,7 @@ fun InterestsScreenEmpty() {
InterestsScreen( InterestsScreen(
uiState = InterestsUiState.Empty, uiState = InterestsUiState.Empty,
followTopic = { _, _ -> }, followTopic = { _, _ -> },
navigateToTopic = {}, onTopicClick = {},
) )
} }
} }

@ -31,7 +31,7 @@ fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) {
} }
fun NavGraphBuilder.interestsGraph( fun NavGraphBuilder.interestsGraph(
navigateToTopic: (String) -> Unit, onTopicClick: (String) -> Unit,
nestedGraphs: NavGraphBuilder.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit,
) { ) {
navigation( navigation(
@ -39,9 +39,7 @@ fun NavGraphBuilder.interestsGraph(
startDestination = interestsRoute, startDestination = interestsRoute,
) { ) {
composable(route = interestsRoute) { composable(route = interestsRoute) {
InterestsRoute( InterestsRoute(onTopicClick)
navigateToTopic = navigateToTopic,
)
} }
nestedGraphs() nestedGraphs()
} }

@ -24,18 +24,4 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.settings" namespace = "com.google.samples.apps.nowinandroid.feature.settings"
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
maybeCreate<ManagedVirtualDevice>("pixel4api30").apply {
device = "Pixel 4"
apiLevel = 30
// ATDs currently support only API level 30.
systemImageSource = "aosp-atd"
}
}
}
}
} }

@ -24,20 +24,6 @@ plugins {
android { android {
namespace = "com.google.samples.apps.nowinandroid.feature.topic" namespace = "com.google.samples.apps.nowinandroid.feature.topic"
testOptions {
// TODO: Convert it as a convention plugin once Flamingo goes out (https://github.com/android/nowinandroid/issues/523)
managedDevices {
devices {
maybeCreate<ManagedVirtualDevice>("pixel4api30").apply {
device = "Pixel 4"
apiLevel = 30
// ATDs currently support only API level 30.
systemImageSource = "aosp-atd"
}
}
}
}
} }
dependencies { dependencies {

@ -55,8 +55,9 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = { }, onBackClick = {},
onFollowClick = { }, onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
) )
} }
@ -73,8 +74,9 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Success(testTopic), topicUiState = TopicUiState.Success(testTopic),
newsUiState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = { }, onBackClick = {},
onFollowClick = { }, onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
) )
} }
@ -96,8 +98,9 @@ class TopicScreenTest {
TopicScreen( TopicScreen(
topicUiState = TopicUiState.Loading, topicUiState = TopicUiState.Loading,
newsUiState = NewsUiState.Success(userNewsResourcesTestData), newsUiState = NewsUiState.Success(userNewsResourcesTestData),
onBackClick = { }, onBackClick = {},
onFollowClick = { }, onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
) )
} }
@ -117,8 +120,9 @@ class TopicScreenTest {
newsUiState = NewsUiState.Success( newsUiState = NewsUiState.Success(
userNewsResourcesTestData, userNewsResourcesTestData,
), ),
onBackClick = { }, onBackClick = {},
onFollowClick = { }, onFollowClick = {},
onTopicClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
) )
} }

@ -65,6 +65,7 @@ import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading
@Composable @Composable
internal fun TopicRoute( internal fun TopicRoute(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TopicViewModel = hiltViewModel(), viewModel: TopicViewModel = hiltViewModel(),
) { ) {
@ -78,6 +79,7 @@ internal fun TopicRoute(
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle, onFollowClick = viewModel::followTopicToggle,
onBookmarkChanged = viewModel::bookmarkNews, onBookmarkChanged = viewModel::bookmarkNews,
onTopicClick = onTopicClick,
) )
} }
@ -88,6 +90,7 @@ internal fun TopicScreen(
newsUiState: NewsUiState, newsUiState: NewsUiState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onFollowClick: (Boolean) -> Unit, onFollowClick: (Boolean) -> Unit,
onTopicClick: (String) -> Unit,
onBookmarkChanged: (String, Boolean) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -124,6 +127,7 @@ internal fun TopicScreen(
news = newsUiState, news = newsUiState,
imageUrl = topicUiState.followableTopic.topic.imageUrl, imageUrl = topicUiState.followableTopic.topic.imageUrl,
onBookmarkChanged = onBookmarkChanged, onBookmarkChanged = onBookmarkChanged,
onTopicClick = onTopicClick,
) )
} }
} }
@ -139,13 +143,14 @@ private fun LazyListScope.TopicBody(
news: NewsUiState, news: NewsUiState,
imageUrl: String, imageUrl: String,
onBookmarkChanged: (String, Boolean) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
) { ) {
// TODO: Show icon if available // TODO: Show icon if available
item { item {
TopicHeader(name, description, imageUrl) TopicHeader(name, description, imageUrl)
} }
userNewsResourceCards(news, onBookmarkChanged) userNewsResourceCards(news, onBookmarkChanged, onTopicClick)
} }
@Composable @Composable
@ -176,12 +181,14 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) {
private fun LazyListScope.userNewsResourceCards( private fun LazyListScope.userNewsResourceCards(
news: NewsUiState, news: NewsUiState,
onBookmarkChanged: (String, Boolean) -> Unit, onBookmarkChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
) { ) {
when (news) { when (news) {
is NewsUiState.Success -> { is NewsUiState.Success -> {
userNewsResourceCardItems( userNewsResourceCardItems(
items = news.news, items = news.news,
onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) }, onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) },
onTopicClick = onTopicClick,
itemModifier = Modifier.padding(24.dp), itemModifier = Modifier.padding(24.dp),
) )
} }
@ -202,11 +209,12 @@ private fun TopicBodyPreview() {
NiaTheme { NiaTheme {
LazyColumn { LazyColumn {
TopicBody( TopicBody(
"Jetpack Compose", name = "Jetpack Compose",
"Lorem ipsum maximum", description = "Lorem ipsum maximum",
NewsUiState.Success(emptyList()), news = NewsUiState.Success(emptyList()),
"", imageUrl = "",
{ _, _ -> }, onBookmarkChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -263,6 +271,7 @@ fun TopicScreenPopulated(
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }
@ -279,6 +288,7 @@ fun TopicScreenLoading() {
onBackClick = {}, onBackClick = {},
onFollowClick = {}, onFollowClick = {},
onBookmarkChanged = { _, _ -> }, onBookmarkChanged = { _, _ -> },
onTopicClick = {},
) )
} }
} }

@ -42,6 +42,7 @@ fun NavController.navigateToTopic(topicId: String) {
fun NavGraphBuilder.topicScreen( fun NavGraphBuilder.topicScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTopicClick: (String) -> Unit,
) { ) {
composable( composable(
route = "topic_route/{$topicIdArg}", route = "topic_route/{$topicIdArg}",
@ -49,6 +50,6 @@ fun NavGraphBuilder.topicScreen(
navArgument(topicIdArg) { type = NavType.StringType }, navArgument(topicIdArg) { type = NavType.StringType },
), ),
) { ) {
TopicRoute(onBackClick = onBackClick) TopicRoute(onBackClick = onBackClick, onTopicClick = onTopicClick)
} }
} }

@ -6,7 +6,7 @@ androidxActivity = "1.6.1"
androidxAppCompat = "1.5.1" androidxAppCompat = "1.5.1"
androidxBrowser = "1.4.0" androidxBrowser = "1.4.0"
androidxComposeBom = "2022.12.00" androidxComposeBom = "2022.12.00"
androidxComposeCompiler = "1.4.0-alpha02" androidxComposeCompiler = "1.4.0"
androidxComposeRuntimeTracing = "1.0.0-alpha01" androidxComposeRuntimeTracing = "1.0.0-alpha01"
androidxCore = "1.9.0" androidxCore = "1.9.0"
androidxCoreSplashscreen = "1.0.0" androidxCoreSplashscreen = "1.0.0"
@ -32,18 +32,18 @@ hilt = "2.44.2"
hiltExt = "1.0.0" hiltExt = "1.0.0"
jacoco = "0.8.7" jacoco = "0.8.7"
junit4 = "4.13.2" junit4 = "4.13.2"
kotlin = "1.7.21" kotlin = "1.8.0"
kotlinxCoroutines = "1.6.4" kotlinxCoroutines = "1.6.4"
kotlinxDatetime = "0.4.0" kotlinxDatetime = "0.4.0"
kotlinxSerializationJson = "1.4.1" kotlinxSerializationJson = "1.4.1"
ksp = "1.7.21-1.0.8" ksp = "1.8.0-1.0.9"
lint = "30.3.1" lint = "30.3.1"
okhttp = "4.10.0" okhttp = "4.10.0"
protobuf = "3.21.12" protobuf = "3.21.12"
protobufPlugin = "0.8.19" protobufPlugin = "0.8.19"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "0.8.0" retrofitKotlinxSerializationJson = "0.8.0"
room = "2.5.0-rc01" room = "2.5.0"
secrets = "2.0.1" secrets = "2.0.1"
turbine = "0.12.1" turbine = "0.12.1"
@ -121,6 +121,7 @@ turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine
# Dependencies of the included build-logic # Dependencies of the included build-logic
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

@ -85,7 +85,6 @@ class DesignSystemDetector : Detector(), Detector.UastScanner {
"TextButton" to "NiaTextButton", "TextButton" to "NiaTextButton",
"FilterChip" to "NiaFilterChip", "FilterChip" to "NiaFilterChip",
"ElevatedFilterChip" to "NiaFilterChip", "ElevatedFilterChip" to "NiaFilterChip",
"DropdownMenu" to "NiaDropdownMenu",
"NavigationBar" to "NiaNavigationBar", "NavigationBar" to "NiaNavigationBar",
"NavigationBarItem" to "NiaNavigationBarItem", "NavigationBarItem" to "NiaNavigationBarItem",
"NavigationRail" to "NiaNavigationRail", "NavigationRail" to "NiaNavigationRail",

Loading…
Cancel
Save