@ -1,3 +1,3 @@
|
||||
# :app-nia-catalog module
|
||||
|
||||

|
||||
## Dependency graph
|
||||

|
||||
|
@ -1,167 +1,107 @@
|
||||
androidx.activity:activity-compose:1.8.0
|
||||
androidx.activity:activity-ktx:1.8.0
|
||||
androidx.activity:activity:1.8.0
|
||||
androidx.annotation:annotation-experimental:1.3.0
|
||||
androidx.annotation:annotation-jvm:1.6.0
|
||||
androidx.annotation:annotation:1.6.0
|
||||
androidx.activity:activity-compose:1.8.2
|
||||
androidx.activity:activity-ktx:1.8.2
|
||||
androidx.activity:activity:1.8.2
|
||||
androidx.annotation:annotation-experimental:1.4.0
|
||||
androidx.annotation:annotation-jvm:1.7.1
|
||||
androidx.annotation:annotation:1.7.1
|
||||
androidx.appcompat:appcompat-resources:1.6.1
|
||||
androidx.arch.core:core-common:2.2.0
|
||||
androidx.arch.core:core-runtime:2.2.0
|
||||
androidx.autofill:autofill:1.0.0
|
||||
androidx.browser:browser:1.6.0
|
||||
androidx.collection:collection:1.2.0
|
||||
androidx.compose.animation:animation-android:1.5.4
|
||||
androidx.compose.animation:animation-core-android:1.5.4
|
||||
androidx.compose.animation:animation-core:1.5.4
|
||||
androidx.compose.animation:animation:1.5.4
|
||||
androidx.compose.foundation:foundation-android:1.5.4
|
||||
androidx.compose.foundation:foundation-layout-android:1.5.4
|
||||
androidx.compose.foundation:foundation-layout:1.5.4
|
||||
androidx.compose.foundation:foundation:1.5.4
|
||||
androidx.compose.material3:material3:1.1.2
|
||||
androidx.compose.material:material-icons-core-android:1.5.4
|
||||
androidx.compose.material:material-icons-core:1.5.4
|
||||
androidx.compose.material:material-icons-extended-android:1.5.4
|
||||
androidx.compose.material:material-icons-extended:1.5.4
|
||||
androidx.compose.material:material-ripple-android:1.5.4
|
||||
androidx.compose.material:material-ripple:1.5.4
|
||||
androidx.compose.runtime:runtime-android:1.5.4
|
||||
androidx.compose.runtime:runtime-livedata:1.5.4
|
||||
androidx.compose.runtime:runtime-saveable-android:1.5.4
|
||||
androidx.compose.runtime:runtime-saveable:1.5.4
|
||||
androidx.compose.runtime:runtime:1.5.4
|
||||
androidx.compose.ui:ui-android:1.5.4
|
||||
androidx.compose.ui:ui-geometry-android:1.5.4
|
||||
androidx.compose.ui:ui-geometry:1.5.4
|
||||
androidx.compose.ui:ui-graphics-android:1.5.4
|
||||
androidx.compose.ui:ui-graphics:1.5.4
|
||||
androidx.compose.ui:ui-text-android:1.5.4
|
||||
androidx.compose.ui:ui-text:1.5.4
|
||||
androidx.compose.ui:ui-tooling-preview-android:1.5.4
|
||||
androidx.compose.ui:ui-tooling-preview:1.5.4
|
||||
androidx.compose.ui:ui-unit-android:1.5.4
|
||||
androidx.compose.ui:ui-unit:1.5.4
|
||||
androidx.compose.ui:ui-util-android:1.5.4
|
||||
androidx.compose.ui:ui-util:1.5.4
|
||||
androidx.compose.ui:ui:1.5.4
|
||||
androidx.compose:compose-bom:2023.10.01
|
||||
androidx.browser:browser:1.8.0
|
||||
androidx.collection:collection-jvm:1.4.0
|
||||
androidx.collection:collection-ktx:1.4.0
|
||||
androidx.collection:collection:1.4.0
|
||||
androidx.compose.animation:animation-android:1.6.3
|
||||
androidx.compose.animation:animation-core-android:1.6.3
|
||||
androidx.compose.animation:animation-core:1.6.3
|
||||
androidx.compose.animation:animation:1.6.3
|
||||
androidx.compose.foundation:foundation-android:1.6.3
|
||||
androidx.compose.foundation:foundation-layout-android:1.6.3
|
||||
androidx.compose.foundation:foundation-layout:1.6.3
|
||||
androidx.compose.foundation:foundation:1.6.3
|
||||
androidx.compose.material3:material3-android:1.2.1
|
||||
androidx.compose.material3:material3:1.2.1
|
||||
androidx.compose.material:material-icons-core-android:1.6.3
|
||||
androidx.compose.material:material-icons-core:1.6.3
|
||||
androidx.compose.material:material-icons-extended-android:1.6.3
|
||||
androidx.compose.material:material-icons-extended:1.6.3
|
||||
androidx.compose.material:material-ripple-android:1.6.3
|
||||
androidx.compose.material:material-ripple:1.6.3
|
||||
androidx.compose.runtime:runtime-android:1.6.3
|
||||
androidx.compose.runtime:runtime-saveable-android:1.6.3
|
||||
androidx.compose.runtime:runtime-saveable:1.6.3
|
||||
androidx.compose.runtime:runtime:1.6.3
|
||||
androidx.compose.ui:ui-android:1.6.3
|
||||
androidx.compose.ui:ui-geometry-android:1.6.3
|
||||
androidx.compose.ui:ui-geometry:1.6.3
|
||||
androidx.compose.ui:ui-graphics-android:1.6.3
|
||||
androidx.compose.ui:ui-graphics:1.6.3
|
||||
androidx.compose.ui:ui-text-android:1.6.3
|
||||
androidx.compose.ui:ui-text:1.6.3
|
||||
androidx.compose.ui:ui-tooling-preview-android:1.6.3
|
||||
androidx.compose.ui:ui-tooling-preview:1.6.3
|
||||
androidx.compose.ui:ui-unit-android:1.6.3
|
||||
androidx.compose.ui:ui-unit:1.6.3
|
||||
androidx.compose.ui:ui-util-android:1.6.3
|
||||
androidx.compose.ui:ui-util:1.6.3
|
||||
androidx.compose.ui:ui:1.6.3
|
||||
androidx.compose:compose-bom:2024.02.02
|
||||
androidx.concurrent:concurrent-futures:1.1.0
|
||||
androidx.core:core-ktx:1.12.0
|
||||
androidx.core:core:1.12.0
|
||||
androidx.customview:customview-poolingcontainer:1.0.0
|
||||
androidx.customview:customview:1.0.0
|
||||
androidx.datastore:datastore-core:1.0.0
|
||||
androidx.datastore:datastore:1.0.0
|
||||
androidx.documentfile:documentfile:1.0.0
|
||||
androidx.emoji2:emoji2:1.4.0
|
||||
androidx.exifinterface:exifinterface:1.3.6
|
||||
androidx.emoji2:emoji2:1.3.0
|
||||
androidx.exifinterface:exifinterface:1.3.7
|
||||
androidx.fragment:fragment:1.5.1
|
||||
androidx.interpolator:interpolator:1.0.0
|
||||
androidx.legacy:legacy-support-core-utils:1.0.0
|
||||
androidx.lifecycle:lifecycle-common-java8:2.6.1
|
||||
androidx.lifecycle:lifecycle-common:2.6.1
|
||||
androidx.lifecycle:lifecycle-livedata-core:2.6.1
|
||||
androidx.lifecycle:lifecycle-livedata:2.6.1
|
||||
androidx.lifecycle:lifecycle-process:2.6.1
|
||||
androidx.lifecycle:lifecycle-runtime-ktx:2.6.1
|
||||
androidx.lifecycle:lifecycle-runtime:2.6.1
|
||||
androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1
|
||||
androidx.lifecycle:lifecycle-viewmodel:2.6.1
|
||||
androidx.lifecycle:lifecycle-common-java8:2.7.0
|
||||
androidx.lifecycle:lifecycle-common:2.7.0
|
||||
androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0
|
||||
androidx.lifecycle:lifecycle-livedata-core:2.7.0
|
||||
androidx.lifecycle:lifecycle-livedata:2.7.0
|
||||
androidx.lifecycle:lifecycle-process:2.7.0
|
||||
androidx.lifecycle:lifecycle-runtime-ktx:2.7.0
|
||||
androidx.lifecycle:lifecycle-runtime:2.7.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0
|
||||
androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0
|
||||
androidx.lifecycle:lifecycle-viewmodel:2.7.0
|
||||
androidx.loader:loader:1.0.0
|
||||
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
|
||||
androidx.metrics:metrics-performance:1.0.0-alpha04
|
||||
androidx.print:print:1.0.0
|
||||
androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05
|
||||
androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05
|
||||
androidx.profileinstaller:profileinstaller:1.3.1
|
||||
androidx.room:room-common:2.6.0
|
||||
androidx.room:room-ktx:2.6.0
|
||||
androidx.room:room-runtime:2.6.0
|
||||
androidx.savedstate:savedstate-ktx:1.2.1
|
||||
androidx.savedstate:savedstate:1.2.1
|
||||
androidx.sqlite:sqlite-framework:2.4.0
|
||||
androidx.sqlite:sqlite:2.4.0
|
||||
androidx.startup:startup-runtime:1.1.1
|
||||
androidx.tracing:tracing-ktx:1.1.0
|
||||
androidx.tracing:tracing:1.1.0
|
||||
androidx.tracing:tracing-ktx:1.3.0-alpha02
|
||||
androidx.tracing:tracing:1.3.0-alpha02
|
||||
androidx.vectordrawable:vectordrawable-animated:1.1.0
|
||||
androidx.vectordrawable:vectordrawable:1.1.0
|
||||
androidx.versionedparcelable:versionedparcelable:1.1.1
|
||||
androidx.viewpager:viewpager:1.0.0
|
||||
com.caverock:androidsvg-aar:1.4
|
||||
com.google.accompanist:accompanist-drawablepainter:0.30.1
|
||||
com.google.android.datatransport:transport-api:3.0.0
|
||||
com.google.android.datatransport:transport-backend-cct:3.1.8
|
||||
com.google.android.datatransport:transport-runtime:3.1.8
|
||||
com.google.android.gms:play-services-ads-identifier:18.0.0
|
||||
com.google.android.gms:play-services-base:18.0.1
|
||||
com.google.android.gms:play-services-basement:18.1.0
|
||||
com.google.android.gms:play-services-cloud-messaging:17.0.1
|
||||
com.google.android.gms:play-services-measurement-api:21.4.0
|
||||
com.google.android.gms:play-services-measurement-base:21.4.0
|
||||
com.google.android.gms:play-services-measurement-impl:21.4.0
|
||||
com.google.android.gms:play-services-measurement-sdk-api:21.4.0
|
||||
com.google.android.gms:play-services-measurement-sdk:21.4.0
|
||||
com.google.android.gms:play-services-measurement:21.4.0
|
||||
com.google.android.gms:play-services-stats:17.0.2
|
||||
com.google.android.gms:play-services-tasks:18.0.2
|
||||
com.google.accompanist:accompanist-drawablepainter:0.32.0
|
||||
com.google.code.findbugs:jsr305:3.0.2
|
||||
com.google.dagger:dagger-lint-aar:2.48.1
|
||||
com.google.dagger:dagger:2.48.1
|
||||
com.google.dagger:hilt-android:2.48.1
|
||||
com.google.dagger:hilt-core:2.48.1
|
||||
com.google.errorprone:error_prone_annotations:2.11.0
|
||||
com.google.firebase:firebase-analytics-ktx:21.4.0
|
||||
com.google.firebase:firebase-analytics:21.4.0
|
||||
com.google.firebase:firebase-annotations:16.2.0
|
||||
com.google.firebase:firebase-bom:32.4.0
|
||||
com.google.firebase:firebase-common-ktx:20.4.2
|
||||
com.google.firebase:firebase-common:20.4.2
|
||||
com.google.firebase:firebase-components:17.1.5
|
||||
com.google.firebase:firebase-datatransport:18.1.7
|
||||
com.google.firebase:firebase-encoders-json:18.0.0
|
||||
com.google.firebase:firebase-encoders-proto:16.0.0
|
||||
com.google.firebase:firebase-encoders:17.0.0
|
||||
com.google.firebase:firebase-iid-interop:17.1.0
|
||||
com.google.firebase:firebase-installations-interop:17.1.1
|
||||
com.google.firebase:firebase-installations:17.2.0
|
||||
com.google.firebase:firebase-measurement-connector:19.0.0
|
||||
com.google.firebase:firebase-messaging-ktx:23.3.0
|
||||
com.google.firebase:firebase-messaging:23.3.0
|
||||
com.google.guava:failureaccess:1.0.1
|
||||
com.google.guava:guava:31.1-android
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
|
||||
com.google.j2objc:j2objc-annotations:1.3
|
||||
com.google.protobuf:protobuf-javalite:3.24.4
|
||||
com.google.protobuf:protobuf-kotlin-lite:3.24.4
|
||||
com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0
|
||||
com.squareup.okhttp3:logging-interceptor:4.12.0
|
||||
com.google.dagger:dagger-lint-aar:2.51
|
||||
com.google.dagger:dagger:2.51
|
||||
com.google.dagger:hilt-android:2.51
|
||||
com.google.dagger:hilt-core:2.51
|
||||
com.google.guava:listenablefuture:1.0
|
||||
com.squareup.okhttp3:okhttp:4.12.0
|
||||
com.squareup.okio:okio-jvm:3.6.0
|
||||
com.squareup.okio:okio:3.6.0
|
||||
com.squareup.retrofit2:retrofit:2.9.0
|
||||
io.coil-kt:coil-base:2.4.0
|
||||
io.coil-kt:coil-compose-base:2.4.0
|
||||
io.coil-kt:coil-compose:2.4.0
|
||||
io.coil-kt:coil-svg:2.4.0
|
||||
io.coil-kt:coil:2.4.0
|
||||
com.squareup.okio:okio-jvm:3.8.0
|
||||
com.squareup.okio:okio:3.8.0
|
||||
io.coil-kt:coil-base:2.6.0
|
||||
io.coil-kt:coil-compose-base:2.6.0
|
||||
io.coil-kt:coil-compose:2.6.0
|
||||
io.coil-kt:coil:2.6.0
|
||||
javax.inject:javax.inject:1
|
||||
org.checkerframework:checker-qual:3.12.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.9.10
|
||||
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.22
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.9.22
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3
|
||||
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.1
|
||||
org.jetbrains.kotlinx:kotlinx-datetime:0.4.1
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.0
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0
|
||||
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.5.0
|
||||
org.jetbrains.kotlinx:kotlinx-datetime:0.5.0
|
||||
org.jetbrains:annotations:23.0.0
|
||||
|
@ -1,3 +1,3 @@
|
||||
# :app module
|
||||
|
||||

|
||||
## Dependency graph
|
||||

|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2024 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.ui.interests2pane
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class Interests2PaneViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
val selectedTopicId: StateFlow<String?> =
|
||||
savedStateHandle.getStateFlow(TOPIC_ID_ARG, savedStateHandle[TOPIC_ID_ARG])
|
||||
|
||||
fun onTopicClick(topicId: String?) {
|
||||
savedStateHandle[TOPIC_ID_ARG] = topicId
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2024 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.ui.interests2pane
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldDestinationItem
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.INTERESTS_ROUTE
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.TOPIC_ID_ARG
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.TopicDetailPlaceholder
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TOPIC_ROUTE
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.createTopicRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
|
||||
import java.util.UUID
|
||||
|
||||
private const val DETAIL_PANE_NAVHOST_ROUTE = "detail_pane_route"
|
||||
|
||||
fun NavGraphBuilder.interestsListDetailScreen() {
|
||||
composable(
|
||||
route = INTERESTS_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(TOPIC_ID_ARG) {
|
||||
type = NavType.StringType
|
||||
defaultValue = null
|
||||
nullable = true
|
||||
},
|
||||
),
|
||||
) {
|
||||
InterestsListDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun InterestsListDetailScreen(
|
||||
viewModel: Interests2PaneViewModel = hiltViewModel(),
|
||||
) {
|
||||
val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()
|
||||
InterestsListDetailScreen(
|
||||
selectedTopicId = selectedTopicId,
|
||||
onTopicClick = viewModel::onTopicClick,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
internal fun InterestsListDetailScreen(
|
||||
selectedTopicId: String?,
|
||||
onTopicClick: (String) -> Unit,
|
||||
) {
|
||||
val listDetailNavigator = rememberListDetailPaneScaffoldNavigator(
|
||||
initialDestinationHistory = listOfNotNull(
|
||||
ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
|
||||
ThreePaneScaffoldDestinationItem<Nothing>(ListDetailPaneScaffoldRole.Detail).takeIf {
|
||||
selectedTopicId != null
|
||||
},
|
||||
),
|
||||
)
|
||||
BackHandler(listDetailNavigator.canNavigateBack()) {
|
||||
listDetailNavigator.navigateBack()
|
||||
}
|
||||
|
||||
var nestedNavHostStartDestination by remember {
|
||||
mutableStateOf(selectedTopicId?.let(::createTopicRoute) ?: TOPIC_ROUTE)
|
||||
}
|
||||
var nestedNavKey by rememberSaveable(
|
||||
stateSaver = Saver({ it.toString() }, UUID::fromString),
|
||||
) {
|
||||
mutableStateOf(UUID.randomUUID())
|
||||
}
|
||||
val nestedNavController = key(nestedNavKey) {
|
||||
rememberNavController()
|
||||
}
|
||||
|
||||
fun onTopicClickShowDetailPane(topicId: String) {
|
||||
onTopicClick(topicId)
|
||||
if (listDetailNavigator.isDetailPaneVisible()) {
|
||||
// If the detail pane was visible, then use the nestedNavController navigate call
|
||||
// directly
|
||||
nestedNavController.navigateToTopic(topicId) {
|
||||
popUpTo(DETAIL_PANE_NAVHOST_ROUTE)
|
||||
}
|
||||
} else {
|
||||
// Otherwise, recreate the NavHost entirely, and start at the new destination
|
||||
nestedNavHostStartDestination = createTopicRoute(topicId)
|
||||
nestedNavKey = UUID.randomUUID()
|
||||
}
|
||||
listDetailNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
|
||||
}
|
||||
|
||||
ListDetailPaneScaffold(
|
||||
value = listDetailNavigator.scaffoldValue,
|
||||
directive = listDetailNavigator.scaffoldDirective,
|
||||
listPane = {
|
||||
AnimatedPane {
|
||||
InterestsRoute(
|
||||
onTopicClick = ::onTopicClickShowDetailPane,
|
||||
highlightSelectedTopic = listDetailNavigator.isDetailPaneVisible(),
|
||||
)
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
AnimatedPane {
|
||||
key(nestedNavKey) {
|
||||
NavHost(
|
||||
navController = nestedNavController,
|
||||
startDestination = nestedNavHostStartDestination,
|
||||
route = DETAIL_PANE_NAVHOST_ROUTE,
|
||||
) {
|
||||
topicScreen(
|
||||
showBackButton = !listDetailNavigator.isListPaneVisible(),
|
||||
onBackClick = listDetailNavigator::navigateBack,
|
||||
onTopicClick = ::onTopicClickShowDetailPane,
|
||||
)
|
||||
composable(route = TOPIC_ROUTE) {
|
||||
TopicDetailPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
private fun <T> ThreePaneScaffoldNavigator<T>.isListPaneVisible(): Boolean =
|
||||
scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
private fun <T> ThreePaneScaffoldNavigator<T>.isDetailPaneVisible(): Boolean =
|
||||
scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
import android.util.Log
|
||||
import androidx.profileinstaller.ProfileVerifier
|
||||
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Logs the app's Baseline Profile Compilation Status using [ProfileVerifier].
|
||||
*
|
||||
* When delivering through Google Play, the baseline profile is compiled during installation.
|
||||
* In this case you will see the correct state logged without any further action necessary.
|
||||
* To verify baseline profile installation locally, you need to manually trigger baseline
|
||||
* profile installation.
|
||||
*
|
||||
* For immediate compilation, call:
|
||||
* ```bash
|
||||
* adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target
|
||||
* ```
|
||||
* You can also trigger background optimizations:
|
||||
* ```bash
|
||||
* adb shell pm bg-dexopt-job
|
||||
* ```
|
||||
* Both jobs run asynchronously and might take some time complete.
|
||||
*
|
||||
* To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`.
|
||||
* If you don't do either of these steps, you might only see the profile status reported as
|
||||
* "enqueued for compilation" when running the sample locally.
|
||||
*
|
||||
* @see androidx.profileinstaller.ProfileVerifier.CompilationStatus.ResultCode
|
||||
*/
|
||||
class ProfileVerifierLogger @Inject constructor(
|
||||
@ApplicationScope private val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "ProfileInstaller"
|
||||
}
|
||||
|
||||
operator fun invoke() = scope.launch {
|
||||
val status = ProfileVerifier.getCompilationStatusAsync().await()
|
||||
Log.d(TAG, "Status code: ${status.profileInstallResultCode}")
|
||||
Log.d(
|
||||
TAG,
|
||||
when {
|
||||
status.isCompiledWithProfile -> "App compiled with profile"
|
||||
status.hasProfileEnqueuedForCompilation() -> "Profile enqueued for compilation"
|
||||
else -> "Profile not compiled nor enqueued"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.material3.SnackbarDuration.Indefinite
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.test.DeviceConfigurationOverride
|
||||
import androidx.compose.ui.test.ForcedSize
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||
import com.google.samples.apps.nowinandroid.core.testing.util.DefaultRoborazziOptions
|
||||
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import org.robolectric.annotation.LooperMode
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Tests that the Snackbar is correctly displayed on different screen sizes.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
|
||||
// This allows enough room to render the content under test without clipping or scaling.
|
||||
@Config(application = HiltTestApplication::class, qualifiers = "w1000dp-h1000dp-480dpi")
|
||||
@LooperMode(LooperMode.Mode.PAUSED)
|
||||
@HiltAndroidTest
|
||||
class SnackbarScreenshotTests {
|
||||
|
||||
/**
|
||||
* Manages the components' state and is used to perform injection on your test
|
||||
*/
|
||||
@get:Rule(order = 0)
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
/**
|
||||
* Create a temporary folder used to create a Data Store file. This guarantees that
|
||||
* the file is removed in between each test, preventing a crash.
|
||||
*/
|
||||
@BindValue
|
||||
@get:Rule(order = 1)
|
||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
||||
|
||||
/**
|
||||
* Use a test activity to set the content on.
|
||||
*/
|
||||
@get:Rule(order = 2)
|
||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var timeZoneMonitor: TimeZoneMonitor
|
||||
|
||||
@Inject
|
||||
lateinit var userDataRepository: FakeUserDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var topicsRepository: TopicsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userNewsResourceRepository: UserNewsResourceRepository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
|
||||
// Configure user data
|
||||
runBlocking {
|
||||
userDataRepository.setShouldHideOnboarding(true)
|
||||
|
||||
userDataRepository.setFollowedTopicIds(
|
||||
setOf(topicsRepository.getTopics().first().first().id),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setTimeZone() {
|
||||
// Make time zone deterministic in tests
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun phone_noSnackbar() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
400.dp,
|
||||
500.dp,
|
||||
"snackbar_compact_medium_noSnackbar",
|
||||
action = { },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_phone() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
400.dp,
|
||||
500.dp,
|
||||
"snackbar_compact_medium",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_foldable() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
600.dp,
|
||||
600.dp,
|
||||
"snackbar_medium_medium",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun snackbarShown_tablet() {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
testSnackbarScreenshotWithSize(
|
||||
snackbarHostState,
|
||||
900.dp,
|
||||
900.dp,
|
||||
"snackbar_expanded_expanded",
|
||||
) {
|
||||
snackbarHostState.showSnackbar(
|
||||
"This is a test snackbar message",
|
||||
actionLabel = "Action Label",
|
||||
duration = Indefinite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun testSnackbarScreenshotWithSize(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
screenshotName: String,
|
||||
action: suspend () -> Unit,
|
||||
) {
|
||||
lateinit var scope: CoroutineScope
|
||||
composeTestRule.setContent {
|
||||
CompositionLocalProvider(
|
||||
// Replaces images with placeholders
|
||||
LocalInspectionMode provides true,
|
||||
) {
|
||||
scope = rememberCoroutineScope()
|
||||
|
||||
DeviceConfigurationOverride(
|
||||
DeviceConfigurationOverride.ForcedSize(DpSize(width, height)),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
val appState = rememberNiaAppState(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
timeZoneMonitor = timeZoneMonitor,
|
||||
)
|
||||
NiaTheme {
|
||||
NiaApp(appState, snackbarHostState, false, {}, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
action()
|
||||
}
|
||||
|
||||
composeTestRule.onRoot()
|
||||
.captureRoboImage(
|
||||
"src/testDemo/screenshots/$screenshotName.png",
|
||||
roborazziOptions = DefaultRoborazziOptions,
|
||||
)
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 210 KiB |
After Width: | Height: | Size: 96 KiB |
@ -0,0 +1,11 @@
|
||||
// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.
|
||||
// It allows us to define classes that our not part of our codebase without wrapping them in a stable class.
|
||||
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
|
||||
|
||||
// We always use immutable classes for our data model, to avoid running the Compose compiler
|
||||
// in the module we declare it to be stable here.
|
||||
com.google.samples.apps.nowinandroid.core.model.data.*
|
||||
|
||||
// Java standard library classes
|
||||
java.time.ZoneId
|
||||
java.time.ZoneOffset
|
@ -0,0 +1,3 @@
|
||||
# :core:analytics module
|
||||
## Dependency graph
|
||||

|
@ -1,3 +1,3 @@
|
||||
# :core:common module
|
||||
|
||||

|
||||
## Dependency graph
|
||||

|
||||
|
@ -1,3 +1,3 @@
|
||||
# :core:data-test module
|
||||
|
||||

|
||||
## Dependency graph
|
||||

|
||||
|
@ -1,3 +1,3 @@
|
||||
# :core:data module
|
||||
|
||||

|
||||
## Dependency graph
|
||||

|
||||
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2024 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.data.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.tracing.trace
|
||||
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
||||
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
||||
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toKotlinTimeZone
|
||||
import java.time.ZoneId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Utility for reporting current timezone the device has set.
|
||||
* It always emits at least once with default setting and then for each TZ change.
|
||||
*/
|
||||
interface TimeZoneMonitor {
|
||||
val currentTimeZone: Flow<TimeZone>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class TimeZoneBroadcastMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@ApplicationScope appScope: CoroutineScope,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TimeZoneMonitor {
|
||||
|
||||
override val currentTimeZone: SharedFlow<TimeZone> =
|
||||
callbackFlow {
|
||||
// Send the default time zone first.
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
// Registers BroadcastReceiver for the TimeZone changes
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_TIMEZONE_CHANGED) return
|
||||
|
||||
val zoneIdFromIntent = if (VERSION.SDK_INT < VERSION_CODES.R) {
|
||||
null
|
||||
} else {
|
||||
// Starting Android R we also get the new TimeZone.
|
||||
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
|
||||
// We need to convert it from java.util.Timezone to java.time.ZoneId
|
||||
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
|
||||
// Convert to kotlinx.datetime.TimeZone
|
||||
zoneId.toKotlinTimeZone()
|
||||
}
|
||||
}
|
||||
|
||||
// If there isn't a zoneId in the intent, fallback to the systemDefault, which should also reflect the change
|
||||
trySend(zoneIdFromIntent ?: TimeZone.currentSystemDefault())
|
||||
}
|
||||
}
|
||||
|
||||
trace("TimeZoneBroadcastReceiver.register") {
|
||||
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
|
||||
}
|
||||
|
||||
// Send here again, because registering the Broadcast Receiver can take up to several milliseconds.
|
||||
// This way, we can reduce the likelihood that a TZ change wouldn't be caught with the Broadcast Receiver.
|
||||
trySend(TimeZone.currentSystemDefault())
|
||||
|
||||
awaitClose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
// We use to prevent multiple emissions of the same type, because we use trySend multiple times.
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
.flowOn(ioDispatcher)
|
||||
// Sharing the callback to prevent multiple BroadcastReceivers being registered
|
||||
.shareIn(appScope, SharingStarted.WhileSubscribed(5_000), 1)
|
||||
}
|