Repository: android/nowinandroid Branch: main Commit: e74c06b73a0e Files: 554 Total size: 1.8 MB Directory structure: gitextract_tu9jpf5p/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── docs_issue.yml │ │ └── feature_request.yml │ ├── ci-gradle.properties │ ├── pull_request_template.md │ ├── renovate.json │ └── workflows/ │ ├── Build.yaml │ ├── NightlyBaselineProfiles.yaml │ └── Release.yml ├── .gitignore ├── .google/ │ ├── BUILDME │ └── packaging.yaml ├── .idea/ │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ └── copyright/ │ ├── The_Android_Open_Source_Project.xml │ └── profiles_settings.xml ├── .run/ │ ├── Generate Demo Baseline Profile.run.xml │ └── spotlessApply.run.xml ├── AGENTS.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── README.md │ ├── benchmark-rules.pro │ ├── build.gradle.kts │ ├── dependencies/ │ │ └── prodReleaseRuntimeClasspath.txt │ ├── google-services.json │ ├── prodRelease-badging.txt │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── ui/ │ │ ├── NavigationTest.kt │ │ └── UiTestExtensions.kt │ ├── benchmark/ │ │ └── res/ │ │ ├── values/ │ │ │ └── colors.xml │ │ └── values-night/ │ │ └── colors.xml │ ├── debug/ │ │ └── res/ │ │ ├── values/ │ │ │ └── colors.xml │ │ └── values-night/ │ │ └── colors.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ ├── NiaApplication.kt │ │ │ ├── di/ │ │ │ │ └── JankStatsModule.kt │ │ │ ├── navigation/ │ │ │ │ └── TopLevelNavItem.kt │ │ │ ├── ui/ │ │ │ │ ├── NiaApp.kt │ │ │ │ └── NiaAppState.kt │ │ │ └── util/ │ │ │ ├── ProfileVerifierLogger.kt │ │ │ └── UiExtensions.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_splash.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── values-night/ │ │ ├── colors.xml │ │ └── themes.xml │ ├── prod/ │ │ └── AndroidManifest.xml │ └── testDemo/ │ ├── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── ui/ │ │ ├── DeviceConfigurationOverrideWindowInsets.kt │ │ ├── NiaAppScreenSizesScreenshotTests.kt │ │ ├── NiaAppStateTest.kt │ │ ├── SnackbarInsetsScreenshotTests.kt │ │ └── SnackbarScreenshotTests.kt │ └── resources/ │ └── robolectric.properties ├── app-nia-catalog/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── dependencies/ │ │ └── releaseRuntimeClasspath.txt │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── niacatalog/ │ │ ├── NiaCatalogActivity.kt │ │ └── ui/ │ │ └── Catalog.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── strings.xml │ └── themes.xml ├── benchmarks/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── kotlin/ │ ├── androidx/ │ │ └── test/ │ │ └── uiautomator/ │ │ └── UiAutomatorHelpers.kt │ └── com/ │ └── google/ │ └── samples/ │ └── apps/ │ └── nowinandroid/ │ ├── BaselineProfileMetrics.kt │ ├── GeneralActions.kt │ ├── Utils.kt │ ├── baselineprofile/ │ │ ├── BookmarksBaselineProfile.kt │ │ ├── ForYouBaselineProfile.kt │ │ ├── InterestsBaselineProfile.kt │ │ └── StartupBaselineProfile.kt │ ├── bookmarks/ │ │ └── BookmarksActions.kt │ ├── foryou/ │ │ ├── ForYouActions.kt │ │ └── ScrollForYouFeedBenchmark.kt │ ├── interests/ │ │ ├── InterestsActions.kt │ │ ├── ScrollTopicListBenchmark.kt │ │ ├── ScrollTopicListPowerMetricsBenchmark.kt │ │ └── TopicsScreenRecompositionBenchmark.kt │ └── startup/ │ └── StartupBenchmark.kt ├── build-logic/ │ ├── README.md │ ├── convention/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ ├── AndroidApplicationComposeConventionPlugin.kt │ │ ├── AndroidApplicationConventionPlugin.kt │ │ ├── AndroidApplicationFirebaseConventionPlugin.kt │ │ ├── AndroidApplicationFlavorsConventionPlugin.kt │ │ ├── AndroidApplicationJacocoConventionPlugin.kt │ │ ├── AndroidFeatureApiConventionPlugin.kt │ │ ├── AndroidFeatureImplConventionPlugin.kt │ │ ├── AndroidLibraryComposeConventionPlugin.kt │ │ ├── AndroidLibraryConventionPlugin.kt │ │ ├── AndroidLibraryJacocoConventionPlugin.kt │ │ ├── AndroidLintConventionPlugin.kt │ │ ├── AndroidRoomConventionPlugin.kt │ │ ├── AndroidTestConventionPlugin.kt │ │ ├── HiltConventionPlugin.kt │ │ ├── JvmLibraryConventionPlugin.kt │ │ ├── RootPlugin.kt │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ ├── AndroidCompose.kt │ │ ├── AndroidInstrumentedTests.kt │ │ ├── Badging.kt │ │ ├── GradleManagedDevices.kt │ │ ├── Graph.kt │ │ ├── Jacoco.kt │ │ ├── KotlinAndroid.kt │ │ ├── NiaBuildType.kt │ │ ├── NiaFlavor.kt │ │ ├── PrintTestApks.kt │ │ ├── ProjectExtensions.kt │ │ └── Spotless.kt │ ├── gradle.properties │ └── settings.gradle.kts ├── build.gradle.kts ├── build_android_release.sh ├── compose_compiler_config.conf ├── core/ │ ├── analytics/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── demo/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── analytics/ │ │ │ └── AnalyticsModule.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── analytics/ │ │ │ ├── AnalyticsEvent.kt │ │ │ ├── AnalyticsHelper.kt │ │ │ ├── NoOpAnalyticsHelper.kt │ │ │ ├── StubAnalyticsHelper.kt │ │ │ └── UiHelpers.kt │ │ └── prod/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── analytics/ │ │ ├── AnalyticsModule.kt │ │ └── FirebaseAnalyticsHelper.kt │ ├── common/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── common/ │ │ │ ├── network/ │ │ │ │ ├── NiaDispatchers.kt │ │ │ │ └── di/ │ │ │ │ ├── CoroutineScopesModule.kt │ │ │ │ └── DispatchersModule.kt │ │ │ └── result/ │ │ │ └── Result.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── common/ │ │ └── result/ │ │ └── ResultKtTest.kt │ ├── data/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── data/ │ │ │ ├── SyncUtilities.kt │ │ │ ├── di/ │ │ │ │ ├── DataModule.kt │ │ │ │ └── UserNewsResourceRepositoryModule.kt │ │ │ ├── model/ │ │ │ │ ├── NewsResource.kt │ │ │ │ ├── RecentSearchQuery.kt │ │ │ │ └── Topic.kt │ │ │ ├── repository/ │ │ │ │ ├── AnalyticsExtensions.kt │ │ │ │ ├── CompositeUserNewsResourceRepository.kt │ │ │ │ ├── DefaultRecentSearchRepository.kt │ │ │ │ ├── DefaultSearchContentsRepository.kt │ │ │ │ ├── NewsRepository.kt │ │ │ │ ├── OfflineFirstNewsRepository.kt │ │ │ │ ├── OfflineFirstTopicsRepository.kt │ │ │ │ ├── OfflineFirstUserDataRepository.kt │ │ │ │ ├── RecentSearchRepository.kt │ │ │ │ ├── SearchContentsRepository.kt │ │ │ │ ├── TopicsRepository.kt │ │ │ │ ├── UserDataRepository.kt │ │ │ │ └── UserNewsResourceRepository.kt │ │ │ └── util/ │ │ │ ├── ConnectivityManagerNetworkMonitor.kt │ │ │ ├── NetworkMonitor.kt │ │ │ ├── SyncManager.kt │ │ │ └── TimeZoneMonitor.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ ├── data/ │ │ │ ├── CompositeUserNewsResourceRepositoryTest.kt │ │ │ ├── UserNewsResourceTest.kt │ │ │ ├── model/ │ │ │ │ └── NetworkEntityTest.kt │ │ │ ├── repository/ │ │ │ │ ├── OfflineFirstNewsRepositoryTest.kt │ │ │ │ ├── OfflineFirstTopicsRepositoryTest.kt │ │ │ │ ├── OfflineFirstUserDataRepositoryTest.kt │ │ │ │ └── TestSynchronizer.kt │ │ │ └── testdoubles/ │ │ │ ├── TestNewsResourceDao.kt │ │ │ ├── TestNiaNetworkDataSource.kt │ │ │ └── TestTopicDao.kt │ │ └── database/ │ │ └── model/ │ │ └── PopulatedNewsResourceKtTest.kt │ ├── data-test/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── data/ │ │ └── test/ │ │ ├── AlwaysOnlineNetworkMonitor.kt │ │ ├── DefaultZoneIdTimeZoneMonitor.kt │ │ ├── TestDataModule.kt │ │ └── repository/ │ │ ├── FakeNewsRepository.kt │ │ ├── FakeRecentSearchRepository.kt │ │ ├── FakeSearchContentsRepository.kt │ │ ├── FakeTopicsRepository.kt │ │ └── FakeUserDataRepository.kt │ ├── database/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── schemas/ │ │ │ └── com.google.samples.apps.nowinandroid.core.database.NiaDatabase/ │ │ │ ├── 1.json │ │ │ ├── 10.json │ │ │ ├── 11.json │ │ │ ├── 12.json │ │ │ ├── 13.json │ │ │ ├── 14.json │ │ │ ├── 2.json │ │ │ ├── 3.json │ │ │ ├── 4.json │ │ │ ├── 5.json │ │ │ ├── 6.json │ │ │ ├── 7.json │ │ │ ├── 8.json │ │ │ └── 9.json │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── database/ │ │ │ └── dao/ │ │ │ ├── DatabaseTest.kt │ │ │ ├── NewsResourceDaoTest.kt │ │ │ └── TopicDaoTest.kt │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── database/ │ │ ├── DatabaseMigrations.kt │ │ ├── NiaDatabase.kt │ │ ├── dao/ │ │ │ ├── NewsResourceDao.kt │ │ │ ├── NewsResourceFtsDao.kt │ │ │ ├── RecentSearchQueryDao.kt │ │ │ ├── TopicDao.kt │ │ │ └── TopicFtsDao.kt │ │ ├── di/ │ │ │ ├── DaosModule.kt │ │ │ └── DatabaseModule.kt │ │ ├── model/ │ │ │ ├── NewsResourceEntity.kt │ │ │ ├── NewsResourceFtsEntity.kt │ │ │ ├── NewsResourceTopicCrossRef.kt │ │ │ ├── PopulatedNewsResource.kt │ │ │ ├── RecentSearchQueryEntity.kt │ │ │ ├── TopicEntity.kt │ │ │ └── TopicFtsEntity.kt │ │ └── util/ │ │ └── InstantConverter.kt │ ├── datastore/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── consumer-proguard-rules.pro │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── datastore/ │ │ │ ├── ChangeListVersions.kt │ │ │ ├── IntToStringIdsMigration.kt │ │ │ ├── ListToMapMigration.kt │ │ │ ├── NiaPreferencesDataSource.kt │ │ │ ├── UserPreferencesSerializer.kt │ │ │ └── di/ │ │ │ └── DataStoreModule.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── datastore/ │ │ ├── IntToStringIdsMigrationTest.kt │ │ ├── ListToMapMigrationTest.kt │ │ ├── NiaPreferencesDataSourceTest.kt │ │ └── UserPreferencesSerializerTest.kt │ ├── datastore-proto/ │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── proto/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── data/ │ │ ├── dark_theme_config.proto │ │ ├── theme_brand.proto │ │ └── user_preferences.proto │ ├── datastore-test/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── datastore/ │ │ └── test/ │ │ ├── InMemoryDataStore.kt │ │ └── TestDataStoreModule.kt │ ├── designsystem/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── core/ │ │ │ │ └── designsystem/ │ │ │ │ ├── component/ │ │ │ │ │ ├── Background.kt │ │ │ │ │ ├── Button.kt │ │ │ │ │ ├── Chip.kt │ │ │ │ │ ├── DynamicAsyncImage.kt │ │ │ │ │ ├── IconButton.kt │ │ │ │ │ ├── LoadingWheel.kt │ │ │ │ │ ├── Navigation.kt │ │ │ │ │ ├── Tabs.kt │ │ │ │ │ ├── Tag.kt │ │ │ │ │ ├── TopAppBar.kt │ │ │ │ │ ├── ViewToggle.kt │ │ │ │ │ └── scrollbar/ │ │ │ │ │ ├── AppScrollbars.kt │ │ │ │ │ ├── LazyScrollbarUtilities.kt │ │ │ │ │ ├── Scrollbar.kt │ │ │ │ │ ├── ScrollbarExt.kt │ │ │ │ │ └── ThumbExt.kt │ │ │ │ ├── icon/ │ │ │ │ │ └── NiaIcons.kt │ │ │ │ └── theme/ │ │ │ │ ├── Background.kt │ │ │ │ ├── Color.kt │ │ │ │ ├── Gradient.kt │ │ │ │ ├── Theme.kt │ │ │ │ ├── Tint.kt │ │ │ │ └── Type.kt │ │ │ └── res/ │ │ │ └── drawable/ │ │ │ └── core_designsystem_ic_placeholder_default.xml │ │ └── test/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── designsystem/ │ │ │ ├── BackgroundScreenshotTests.kt │ │ │ ├── ButtonScreenshotTests.kt │ │ │ ├── FilterChipScreenshotTests.kt │ │ │ ├── IconButtonScreenshotTests.kt │ │ │ ├── LoadingWheelScreenshotTests.kt │ │ │ ├── NavigationScreenshotTests.kt │ │ │ ├── TabsScreenshotTests.kt │ │ │ ├── TagScreenshotTests.kt │ │ │ ├── ThemeTest.kt │ │ │ └── TopAppBarScreenshotTests.kt │ │ └── resources/ │ │ └── robolectric.properties │ ├── domain/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── domain/ │ │ │ ├── GetFollowableTopicsUseCase.kt │ │ │ ├── GetRecentSearchQueriesUseCase.kt │ │ │ └── GetSearchContentsUseCase.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── domain/ │ │ └── GetFollowableTopicsUseCaseTest.kt │ ├── model/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── model/ │ │ └── data/ │ │ ├── DarkThemeConfig.kt │ │ ├── FollowableTopic.kt │ │ ├── NewsResource.kt │ │ ├── SearchResult.kt │ │ ├── ThemeBrand.kt │ │ ├── Topic.kt │ │ ├── UserData.kt │ │ ├── UserNewsResource.kt │ │ └── UserSearchResult.kt │ ├── navigation/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── navigation/ │ │ │ ├── NavigationState.kt │ │ │ └── Navigator.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── navigation/ │ │ └── NavigatorTest.kt │ ├── network/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── lint.xml │ │ └── src/ │ │ ├── demo/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── network/ │ │ │ └── di/ │ │ │ └── FlavoredNetworkModule.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets/ │ │ │ │ ├── news.json │ │ │ │ └── topics.json │ │ │ └── kotlin/ │ │ │ ├── JvmUnitTestDemoAssetManager.kt │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── network/ │ │ │ ├── NiaNetworkDataSource.kt │ │ │ ├── demo/ │ │ │ │ ├── DemoAssetManager.kt │ │ │ │ └── DemoNiaNetworkDataSource.kt │ │ │ ├── di/ │ │ │ │ └── NetworkModule.kt │ │ │ ├── model/ │ │ │ │ ├── NetworkChangeList.kt │ │ │ │ ├── NetworkNewsResource.kt │ │ │ │ └── NetworkTopic.kt │ │ │ └── retrofit/ │ │ │ └── RetrofitNiaNetwork.kt │ │ ├── prod/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── network/ │ │ │ └── di/ │ │ │ └── FlavoredNetworkModule.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── network/ │ │ └── demo/ │ │ └── DemoNiaNetworkDataSourceTest.kt │ ├── notifications/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── demo/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── core/ │ │ │ └── notifications/ │ │ │ └── NotificationsModule.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── core/ │ │ │ │ └── notifications/ │ │ │ │ ├── NoOpNotifier.kt │ │ │ │ ├── Notifier.kt │ │ │ │ └── SystemTrayNotifier.kt │ │ │ └── res/ │ │ │ ├── drawable-anydpi-v24/ │ │ │ │ └── core_notifications_ic_nia_notification.xml │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── prod/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── notifications/ │ │ └── NotificationsModule.kt │ ├── screenshot-testing/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── testing/ │ │ └── util/ │ │ └── ScreenshotHelper.kt │ ├── testing/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ ├── rules/ │ │ │ └── GrantPostNotificationsPermissionRule.kt │ │ └── testing/ │ │ ├── NiaTestRunner.kt │ │ ├── data/ │ │ │ ├── FollowableTopicTestData.kt │ │ │ ├── NewsResourcesTestData.kt │ │ │ ├── TopicsTestData.kt │ │ │ └── UserNewsResourcesTestData.kt │ │ ├── di/ │ │ │ ├── TestDispatcherModule.kt │ │ │ └── TestDispatchersModule.kt │ │ ├── notifications/ │ │ │ └── TestNotifier.kt │ │ ├── repository/ │ │ │ ├── TestNewsRepository.kt │ │ │ ├── TestRecentSearchRepository.kt │ │ │ ├── TestSearchContentsRepository.kt │ │ │ ├── TestTopicsRepository.kt │ │ │ └── TestUserDataRepository.kt │ │ └── util/ │ │ ├── MainDispatcherRule.kt │ │ ├── TestAnalyticsHelper.kt │ │ ├── TestNetworkMonitor.kt │ │ ├── TestSyncManager.kt │ │ └── TestTimeZoneMonitor.kt │ └── ui/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── ui/ │ │ └── NewsResourceCardTest.kt │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── ui/ │ │ ├── AnalyticsExtensions.kt │ │ ├── DevicePreviews.kt │ │ ├── FollowableTopicPreviewParameterProvider.kt │ │ ├── InterestsItem.kt │ │ ├── JankStatsExtensions.kt │ │ ├── LocalTimeZone.kt │ │ ├── NewsFeed.kt │ │ ├── NewsResourceCard.kt │ │ ├── NewsResourceCardList.kt │ │ └── UserNewsResourcePreviewParameterProvider.kt │ └── res/ │ └── values/ │ └── strings.xml ├── docs/ │ ├── ArchitectureLearningJourney.md │ └── ModularizationLearningJourney.md ├── feature/ │ ├── bookmarks/ │ │ ├── api/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── feature/ │ │ │ │ └── bookmarks/ │ │ │ │ └── api/ │ │ │ │ └── navigation/ │ │ │ │ └── BookmarksNavKey.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── feature_bookmarks_api_mg_empty_bookmarks.xml │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── impl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── bookmarks/ │ │ │ └── impl/ │ │ │ └── BookmarksScreenTest.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── bookmarks/ │ │ │ └── impl/ │ │ │ ├── BookmarksScreen.kt │ │ │ ├── BookmarksViewModel.kt │ │ │ └── navigation/ │ │ │ └── BookmarksEntryProvider.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── bookmarks/ │ │ └── impl/ │ │ └── BookmarksViewModelTest.kt │ ├── foryou/ │ │ ├── api/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── feature/ │ │ │ │ └── foryou/ │ │ │ │ └── api/ │ │ │ │ └── navigation/ │ │ │ │ └── ForYouNavKey.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── feature_foryou_api_ic_icon_placeholder.xml │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── impl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── foryou/ │ │ │ └── impl/ │ │ │ └── ForYouScreenTest.kt │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── foryou/ │ │ │ └── impl/ │ │ │ ├── ForYouScreen.kt │ │ │ ├── ForYouViewModel.kt │ │ │ ├── OnboardingUiState.kt │ │ │ └── navigation/ │ │ │ └── ForYouEntryProvider.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── foryou/ │ │ └── impl/ │ │ ├── ForYouScreenScreenshotTests.kt │ │ └── ForYouViewModelTest.kt │ ├── interests/ │ │ ├── api/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── feature/ │ │ │ │ └── interests/ │ │ │ │ └── api/ │ │ │ │ └── navigation/ │ │ │ │ └── InterestsNavKey.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── feature_interests_api_ic_detail_placeholder.xml │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── impl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── interests/ │ │ │ └── impl/ │ │ │ └── InterestsScreenTest.kt │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── interests/ │ │ │ └── impl/ │ │ │ ├── InterestsDetailPlaceholder.kt │ │ │ ├── InterestsScreen.kt │ │ │ ├── InterestsViewModel.kt │ │ │ ├── TabContent.kt │ │ │ └── navigation/ │ │ │ └── InterestsEntryProvider.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── interests/ │ │ └── impl/ │ │ ├── InterestsListDetailScreenTest.kt │ │ └── InterestsViewModelTest.kt │ ├── search/ │ │ ├── api/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── feature/ │ │ │ │ └── search/ │ │ │ │ └── api/ │ │ │ │ └── navigation/ │ │ │ │ └── SearchNavKey.kt │ │ │ └── res/ │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── impl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── search/ │ │ │ └── impl/ │ │ │ └── SearchScreenTest.kt │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── search/ │ │ │ └── impl/ │ │ │ ├── RecentSearchQueriesUiState.kt │ │ │ ├── SearchResultUiState.kt │ │ │ ├── SearchScreen.kt │ │ │ ├── SearchUiStatePreviewParameterProvider.kt │ │ │ ├── SearchViewModel.kt │ │ │ └── navigation/ │ │ │ └── SearchEntryProvider.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── search/ │ │ └── impl/ │ │ └── SearchViewModelTest.kt │ ├── settings/ │ │ └── impl/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── settings/ │ │ │ └── impl/ │ │ │ └── SettingsDialogTest.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── google/ │ │ │ │ └── samples/ │ │ │ │ └── apps/ │ │ │ │ └── nowinandroid/ │ │ │ │ └── feature/ │ │ │ │ └── settings/ │ │ │ │ └── impl/ │ │ │ │ ├── SettingsDialog.kt │ │ │ │ └── SettingsViewModel.kt │ │ │ └── res/ │ │ │ └── values/ │ │ │ └── strings.xml │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── settings/ │ │ └── impl/ │ │ └── SettingsViewModelTest.kt │ └── topic/ │ ├── api/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── feature/ │ │ │ └── topic/ │ │ │ └── api/ │ │ │ └── navigation/ │ │ │ └── TopicNavKey.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── impl/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── topic/ │ │ └── impl/ │ │ └── TopicScreenTest.kt │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── feature/ │ │ └── topic/ │ │ └── impl/ │ │ ├── TopicScreen.kt │ │ ├── TopicViewModel.kt │ │ └── navigation/ │ │ └── TopicEntryProvider.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── google/ │ └── samples/ │ └── apps/ │ └── nowinandroid/ │ └── feature/ │ └── topic/ │ └── impl/ │ └── TopicViewModelTest.kt ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── kokoro/ │ ├── build.sh │ ├── continuous.cfg │ ├── nightly.cfg │ ├── nightly.sh │ └── presubmit.cfg ├── lint/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── lint/ │ │ │ ├── NiaIssueRegistry.kt │ │ │ ├── TestMethodNameDetector.kt │ │ │ └── designsystem/ │ │ │ └── DesignSystemDetector.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── com.android.tools.lint.client.api.IssueRegistry │ └── test/ │ └── kotlin/ │ └── com/ │ └── google/ │ └── samples/ │ └── apps/ │ └── nowinandroid/ │ └── lint/ │ ├── TestMethodNameDetectorTest.kt │ └── designsystem/ │ └── DesignSystemDetectorTest.kt ├── settings.gradle.kts ├── spotless/ │ ├── copyright.kt │ ├── copyright.kts │ └── copyright.xml ├── sync/ │ ├── sync-test/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── core/ │ │ └── sync/ │ │ └── test/ │ │ ├── NeverSyncingSyncManager.kt │ │ └── TestSyncModule.kt │ └── work/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── androidTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── sync/ │ │ └── workers/ │ │ └── SyncWorkerTest.kt │ ├── demo/ │ │ └── kotlin/ │ │ └── com/ │ │ └── google/ │ │ └── samples/ │ │ └── apps/ │ │ └── nowinandroid/ │ │ └── sync/ │ │ └── di/ │ │ └── SyncModule.kt │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── samples/ │ │ │ └── apps/ │ │ │ └── nowinandroid/ │ │ │ └── sync/ │ │ │ ├── initializers/ │ │ │ │ ├── SyncInitializer.kt │ │ │ │ └── SyncWorkHelpers.kt │ │ │ ├── status/ │ │ │ │ ├── StubSyncSubscriber.kt │ │ │ │ ├── SyncSubscriber.kt │ │ │ │ └── WorkManagerSyncManager.kt │ │ │ └── workers/ │ │ │ ├── AnalyticsExtensions.kt │ │ │ ├── DelegatingWorker.kt │ │ │ └── SyncWorker.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── prod/ │ ├── AndroidManifest.xml │ └── kotlin/ │ └── com/ │ └── google/ │ └── samples/ │ └── apps/ │ └── nowinandroid/ │ └── sync/ │ ├── di/ │ │ └── SyncModule.kt │ ├── services/ │ │ └── SyncNotificationsService.kt │ └── status/ │ └── FirebaseSyncSubscriber.kt ├── tools/ │ ├── nowinandroid-codestyle.xml │ ├── pre-push │ └── setup.sh └── ui-test-hilt-manifest/ ├── .gitignore ├── README.md ├── build.gradle.kts └── src/ └── main/ ├── AndroidManifest.xml └── kotlin/ └── com/ └── google/ └── samples/ └── apps/ └── nowinandroid/ └── uitesthiltmanifest/ └── HiltComponentActivity.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ # This configuration is used by ktlint when spotless invokes it [*.{kt,kts}] ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true ktlint_function_naming_ignore_when_annotated_with=Composable, Test ktlint_standard_backing-property-naming = disabled ktlint_standard_binary-expression-wrapping = disabled ktlint_standard_chain-method-continuation = disabled ktlint_standard_class-signature = disabled ktlint_standard_condition-wrapping = disabled ktlint_standard_function-expression-body = disabled ktlint_standard_function-literal = disabled ktlint_standard_function-type-modifier-spacing = disabled ktlint_standard_multiline-loop = disabled ktlint_standard_function-signature = disabled ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: File a bug report title: "[Bug]: " labels: ["bug", "triage me"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: checkboxes attributes: label: Is there a StackOverflow question about this issue? description: Please search [StackOverflow](https://stackoverflow.com/questions/tagged/android-jetpack) if an issue with an answer already exists for the bug you encountered. options: - label: I have searched StackOverflow required: true - type: textarea id: what-happened attributes: label: What happened? description: Also tell us, what did you expect to happen? placeholder: Tell us what you see! value: "A bug happened!" validations: required: true - type: textarea id: logs attributes: label: Relevant logcat output description: Please copy and paste any relevant logcat output. This will be automatically formatted into code, so no need for backticks. render: shell - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ISSUE_TEMPLATE/docs_issue.yml ================================================ name: Documentation issue description: File an issue or make a suggestion for the project documentation title: "[Documentation]: " labels: ["documentation"] body: - type: markdown attributes: value: | Thanks for taking the time to improve our documentation! - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the documentation issue you encountered. options: - label: I have searched the existing issues required: true - type: input id: page-url attributes: label: Page URL (type "NEW" for a new page suggestion) validations: required: true - type: textarea id: what-needs-improving attributes: label: What's the documentation problem or suggestion? placeholder: Tell us what should be improved! value: "Docs need improving!" validations: required: true - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature request description: File a feature request title: "[FR]: " labels: ["enhancement", "triage me"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for this feature request. options: - label: I have searched the existing issues required: true - type: textarea id: describe-problem attributes: label: Describe the problem description: Is your feature request related to a problem? Please describe. placeholder: I'm always frustrated when... validations: required: true - type: textarea id: solution attributes: label: Describe the solution description: Please describe the solution you'd like. A clear and concise description of what you want to happen. validations: required: true - type: textarea id: context attributes: label: Additional context description: Add any other context or screenshots about the feature request here. validations: required: false - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ci-gradle.properties ================================================ # # Copyright 2020 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. # org.gradle.daemon=false org.gradle.parallel=true org.gradle.workers.max=2 org.gradle.configuration-cache=true org.gradle.configuration-cache.parallel=true kotlin.incremental=false # Controls KotlinOptions.allWarningsAsErrors. # This value used in CI and is currently set to false. # If you want to treat warnings as errors locally, set this property to true # in your ~/.gradle/gradle.properties file. warningsAsErrors=false ================================================ FILE: .github/pull_request_template.md ================================================ **DO NOT CREATE A PULL REQUEST WITHOUT READING THESE INSTRUCTIONS** ## Instructions Thanks for submitting a pull request. To accept your pull request we need you do a few things: **If this is your first pull request** - [Sign the contributors license agreement](https://cla.developers.google.com/) **Ensure tests pass and code is formatted correctly** - Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug` - Fix code formatting: `./gradlew spotlessApply` **Add a description** We need to know what you've done and why you've done it. Include a summary of what your pull request contains, and why you have made these changes. Include links to any relevant issues which it fixes. [Here's an example](https://github.com/android/nowinandroid/pull/1257). **NOW DELETE THIS LINE AND EVERYTHING ABOVE IT** **What I have done and why** \ ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "local>android/.github:renovate-config" ], "baseBranches": [ "main" ], "gitIgnoredAuthors": [ "renovate[bot]@users.noreply.github.com", "github-actions[bot]@users.noreply.github.com", "41898282+github-actions[bot]@users.noreply.github.com" ] } ================================================ FILE: .github/workflows/Build.yaml ================================================ name: Build on: workflow_dispatch: push: branches: - main pull_request: concurrency: group: build-${{ github.ref }} cancel-in-progress: true jobs: test_and_apk: name: "Local tests and APKs" runs-on: ubuntu-latest permissions: contents: write pull-requests: write security-events: write timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} build-scan-publish: true build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - name: Check build-logic run: ./gradlew :build-logic:convention:check - name: Check spotless run: ./gradlew spotlessCheck - name: Check Dependency Guard id: dependencyguard_verify continue-on-error: true run: ./gradlew dependencyGuard - name: Prevent updating Dependency Guard baselines if this is a fork id: checkfork_dependencyguard continue-on-error: false if: steps.dependencyguard_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository run: | echo "::error::Dependency Guard failed, please update baselines with: ./gradlew dependencyGuardBaseline" && exit 1 # Runs if previous job failed - name: Generate new Dependency Guard baselines if verification failed and it's a PR id: dependencyguard_baseline if: steps.dependencyguard_verify.outcome == 'failure' && github.event_name == 'pull_request' run: | ./gradlew dependencyGuardBaseline - name: Push new Dependency Guard baselines if available uses: stefanzweifel/git-auto-commit-action@v5 if: steps.dependencyguard_baseline.outcome == 'success' with: file_pattern: '**/dependencies/*.txt' disable_globbing: true commit_message: "🤖 Updates baselines for Dependency Guard" - name: Update Graphs run: ./gradlew graphUpdate continue-on-error: true - name: Check Graphs id: graphs_verify run: git add -- "**/README.md" && git diff --cached --quiet --exit-code -- "**/README.md" - name: Prevent updating graphs if this is a fork id: checkfork_graphs continue-on-error: false if: steps.graphs_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository run: | echo "::error::Check Graphs failed, please update graphs with: ./gradlew graphUpdate" && exit 1 - name: Push new graphs if available if: steps.graphs_verify.outcome == 'failure' && github.event_name == 'pull_request' uses: stefanzweifel/git-auto-commit-action@v5 with: file_pattern: '**/README.md' disable_globbing: true commit_message: "🤖 Updates graphs" - name: Run all local screenshot tests (Roborazzi) id: screenshotsverify continue-on-error: true run: ./gradlew verifyRoborazziDemoDebug - name: Prevent pushing new screenshots if this is a fork id: checkfork_screenshots continue-on-error: false if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository run: | echo "::error::Screenshot tests failed, please create a PR in your fork first." echo "Your fork's CI will take screenshots for your fork." exit 1 # Runs if previous job failed - name: Generate new screenshots if verification failed and it's a PR id: screenshotsrecord if: steps.screenshotsverify.outcome == 'failure' && github.event_name == 'pull_request' run: | ./gradlew recordRoborazziDemoDebug - name: Push new screenshots if available uses: stefanzweifel/git-auto-commit-action@v5 if: steps.screenshotsrecord.outcome == 'success' with: file_pattern: '*/*.png' disable_globbing: true commit_message: "🤖 Updates screenshots" # Run local tests after screenshot tests to avoid wrong UP-TO-DATE. TODO: Ignore screenshots. - name: Run local tests run: ./gradlew testDemoDebug :lint:test - name: Build all build type and flavor permutations run: ./gradlew :app:assemble -PminifyWithR8=false - name: Upload build outputs (APKs) uses: actions/upload-artifact@v4 with: name: APKs path: '**/build/outputs/apk/**/*.apk' - name: Upload JVM local results (XML) if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: local-test-results path: '**/build/test-results/test*UnitTest/**.xml' - name: Upload screenshot results (PNG) if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: screenshot-test-results path: '**/build/outputs/roborazzi/*_compare.png' - name: Check lint run: ./gradlew :app:lintProdRelease :app-nia-catalog:lintRelease :lint:lint - name: Upload lint reports (HTML) if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: lint-reports path: '**/build/reports/lint-results-*.html' - name: Upload lint reports (SARIF) for app module if: ${{ !cancelled() && hashFiles('app/**/*.sarif') != '' }} uses: github/codeql-action/upload-sarif@v3 with: sarif_file: './app/' category: app - name: Upload lint reports (SARIF) for app-nia-catalog module if: ${{ !cancelled() && hashFiles('app-nia-catalog/**/*.sarif') != '' }} uses: github/codeql-action/upload-sarif@v3 with: sarif_file: './app-nia-catalog/' category: app-nia-catalog - name: Upload lint reports (SARIF) for lint module if: ${{ !cancelled() && hashFiles('lint/**/*.sarif') != '' }} uses: github/codeql-action/upload-sarif@v3 with: sarif_file: './lint/' category: lint - name: Check badging run: ./gradlew :app:checkProdReleaseBadging androidTest: runs-on: ubuntu-latest timeout-minutes: 55 strategy: matrix: api-level: [26, 34] steps: - name: Delete unnecessary tools 🔧 uses: jlumbroso/free-disk-space@v1.3.1 with: android: false # Don't remove Android tools tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" dotnet: true # rm -rf /usr/share/dotnet haskell: true # rm -rf /opt/ghc... swap-storage: true # rm -f /mnt/swapfile (4GiB) docker-images: false # Takes 16s, enable if needed in the future large-packages: false # includes google-cloud-sdk and it's slow - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm ls /dev/kvm - name: Checkout uses: actions/checkout@v4 - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} build-scan-publish: true build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - name: Build projects and run instrumentation tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} arch: x86_64 disable-animations: true disk-size: 6000M heap-size: 600M script: ./gradlew connectedDemoDebugAndroidTest --daemon - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) if: matrix.api-level == 30 # There is no need to verify Roborazzi tests to generate coverage. run: ./gradlew testDemoDebugUnitTest -Proborazzi.test.verify=false # Add Prod if we ever add JVM tests for prod # Add `createProdDebugUnitTestCoverageReport` if we ever add JVM tests for prod - name: Generate coverage reports for Debug variants (only API 30) if: matrix.api-level == 30 run: ./gradlew createDemoDebugCombinedCoverageReport - name: Upload test reports if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.api-level }} path: '**/build/reports/androidTests' - name: Display local test coverage (only API 30) if: matrix.api-level == 30 id: jacoco uses: madrapps/jacoco-report@v1.7.1 with: title: Combined test coverage report min-coverage-overall: 40 min-coverage-changed-files: 60 paths: | ${{ github.workspace }}/**/build/reports/jacoco/**/*Report.xml token: ${{ secrets.GITHUB_TOKEN }} - name: Upload local coverage reports (XML + HTML) (only API 30) if: matrix.api-level == 30 uses: actions/upload-artifact@v4 with: name: coverage-reports if-no-files-found: error compression-level: 1 overwrite: false path: '**/build/reports/jacoco/' ================================================ FILE: .github/workflows/NightlyBaselineProfiles.yaml ================================================ name: NightlyBaselineProfiles on: workflow_dispatch: schedule: - cron: '42 4 * * *' jobs: baseline_profiles: name: "Generate Baseline Profiles" if: github.repository == 'android/nowinandroid' runs-on: ubuntu-latest permissions: contents: write timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm ls /dev/kvm - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} build-scan-publish: true build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Accept licenses run: yes | sdkmanager --licenses || true - name: Check build-logic run: ./gradlew :build-logic:convention:check - name: Setup GMD run: ./gradlew :benchmarks:pixel6Api33Setup --info -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" - name: Build all build type and flavor permutations including baseline profiles run: ./gradlew :app:assemble -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=baselineprofile -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true ================================================ FILE: .github/workflows/Release.yml ================================================ name: GitHub Release with APKs on: workflow_dispatch: push: tags: - 'v*' jobs: build: if: github.repository == 'android/nowinandroid' runs-on: ubuntu-latest timeout-minutes: 120 steps: - name: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm ls /dev/kvm - name: Checkout uses: actions/checkout@v4 - name: Copy CI gradle.properties run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} build-scan-publish: true build-scan-terms-of-use-url: "https://gradle.com/terms-of-service" build-scan-terms-of-use-agree: "yes" - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Accept licenses run: yes | sdkmanager --licenses || true - name: Setup GMD run: ./gradlew :benchmarks:pixel6Api33Setup --info -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" - name: Build release variant including baseline profile generation run: ./gradlew :app:assembleDemoRelease -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true -Pandroid.experimental.androidTest.numManagedDeviceShards=1 -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1 - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: true prerelease: false - name: Upload app uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: app/build/outputs/apk/demo/release/app-demo-release.apk asset_name: app-demo-release.apk asset_content_type: application/vnd.android.package-archive ================================================ FILE: .gitignore ================================================ # built application files *.apk *.ap_ # files for the dex VM *.dex # Java class files *.class # generated files bin/ gen/ out/ build/ generated/ # Local configuration file (sdk path, etc) local.properties # Eclipse project files .classpath .project # Windows thumbnail db .DS_Store # IDEA/Android Studio project files, because # the project can be imported from settings.gradle.kts *.iml .idea/* !.idea/copyright # Keep the code styles. !/.idea/codeStyles /.idea/codeStyles/* !/.idea/codeStyles/Project.xml !/.idea/codeStyles/codeStyleConfig.xml # Gradle cache .gradle # Sandbox stuff _sandbox # Android Studio captures folder captures/ # Kotlin .kotlin ================================================ FILE: .google/BUILDME ================================================ # This file can be used to trigger an internal build by changing the number below 2 ================================================ FILE: .google/packaging.yaml ================================================ # Copyright (C) 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. # # GOOGLE SAMPLE PACKAGING DATA # # This file is used by Google as part of our samples packaging process. # End users may safely ignore this file. It has no relevance to other systems. --- status: PUBLISHED technologies: [Android, JetpackCompose, Coroutines] categories: - AndroidTesting - AndroidArchitecture - AndroidArchitectureUILayer - AndroidArchitectureDomainLayer - AndroidArchitectureDataLayer - AndroidArchitectureStateProduction - AndroidArchitectureStateHolder - JetpackComposeTesting - JetpackComposeA11y - JetpackComposeArchitectureAndState - JetpackComposeDesignSystems - JetpackComposeNavigation - JetpackComposeAnimation solutions: - Mobile - Flow - JetpackHilt - JetpackDataStore - JetpackRoom - JetpackNavigation - JetpackWorkManager - JetpackLifecycle languages: [Kotlin] github: android/nowinandroid level: ADVANCED license: apache2 ================================================ FILE: .idea/codeStyles/Project.xml ================================================ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/copyright/The_Android_Open_Source_Project.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .run/Generate Demo Baseline Profile.run.xml ================================================ true true false ================================================ FILE: .run/spotlessApply.run.xml ================================================ true true false ================================================ FILE: AGENTS.md ================================================ # Now in Android Project Now in Android is a native Android mobile application written in Kotlin. It provides regular news about Android development. Users can choose to follow topics, be notified when new content is available, and bookmark items. ## Architecture This project is a modern Android application that follows the official architecture guidance from Google. It is a reactive, single-activity app that uses the following: - **UI:** Built entirely with Jetpack Compose, including Material 3 components and adaptive layouts for different screen sizes. - **State Management:** Unidirectional Data Flow (UDF) is implemented using Kotlin Coroutines and `Flow`s. `ViewModel`s act as state holders, exposing UI state as streams of data. - **Dependency Injection:** Hilt is used for dependency injection throughout the app, simplifying the management of dependencies and improving testability. - **Navigation:** Navigation is handled by Jetpack Navigation 2 for Compose, allowing for a declarative and type-safe way to navigate between screens. - **Data:** The data layer is implemented using the repository pattern. - **Local Data:** Room and DataStore are used for local data persistence. - **Remote Data:** Retrofit and OkHttp are used for fetching data from the network. - **Background Processing:** WorkManager is used for deferrable background tasks. ## Modules The main Android app lives in the `app/` folder. Feature modules live in `feature/` and core and shared modules in `core/`. ## Commands to Build & Test The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`. - Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`. - Fix linting/formatting: `./gradlew spotlessApply` - Run local tests: `./gradlew {variant}Test` - Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"` - Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug` ### Instrumented tests - Gradle-managed devices to run on device tests: `./gradlew pixel6api31aospDebugAndroidTest`. Also `pixel4api30aospatdDebugAndroidTest` and `pixelcapi30aospatdDebugAndroidTest`. ### Creating tests #### Instrumented tests - Tests for UI features should only use `ComposeTestRule` with a `ComponentActivity`. - Bigger tests live in the `:app` module and they can start activities like `MainActivity`. #### Local tests - [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) for most assertions - [cashapp/turbine](https://github.com/cashapp/turbine) for complex coroutine tests - [google/truth](https://github.com/google/truth) for assertions ## Continuous integration - The workflows are defined in `.github/workflows/*.yaml` and they contain various checks. - Screenshot tests are generated by CI, so they shouldn't be checked into the repo from a workstation. ## Version control and code location - The project uses git and is hosted in https://github.com/android/nowinandroid. ================================================ FILE: CODEOWNERS ================================================ * @dturner ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Google Open Source Community Guidelines At Google, we recognize and celebrate the creativity and collaboration of open source contributors and the diversity of skills, experiences, cultures, and opinions they bring to the projects and communities they participate in. Every one of Google's open source projects and communities are inclusive environments, based on treating all individuals respectfully, regardless of gender identity and expression, sexual orientation, disabilities, neurodiversity, physical appearance, body size, ethnicity, nationality, race, age, religion, or similar personal characteristic. We value diverse opinions, but we value respectful behavior more. Respectful behavior includes: * Being considerate, kind, constructive, and helpful. * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or physically threatening behavior, speech, and imagery. * Not engaging in unwanted physical contact. Some Google open source projects [may adopt][] an explicit project code of conduct, which may have additional detailed expectations for participants. Most of those projects will use our [modified Contributor Covenant][]. [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ ## Resolve peacefully We do not believe that all conflict is necessarily bad; healthy debate and disagreement often yields positive results. However, it is never okay to be disrespectful. If you see someone behaving disrespectfully, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. ## Reporting problems Some Google open source projects may adopt a project-specific code of conduct. In those cases, a Google employee will be identified as the Project Steward, who will receive and handle reports of code of conduct violations. In the event that a project hasn’t identified a Project Steward, you can report problems by emailing opensource@google.com. We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice. *This document was adapted from the [IndieWeb Code of Conduct][] and can also be found at .* [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct ================================================ FILE: CONTRIBUTING.md ================================================ # How to become a contributor and submit your own code ## Contributor License Agreements We'd love to accept your sample apps and patches! Before we can take them, we have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement (CLA). * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. ## Contributing A Patch 1. Submit an issue describing your proposed change to the repo in question. 1. The repo owner will respond to your issue promptly. 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 1. Fork the desired repo, develop and test your code changes. 1. Ensure that your code adheres to the existing style in the sample to which you are contributing. Refer to the [Google Cloud Platform Samples Style Guide](https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the recommended coding standards for this organization. 1. Ensure that your code has an appropriate set of unit tests which all pass. 1. Submit a pull request. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: README.md ================================================ ![Now in Android](docs/images/nia-splash.jpg "Now in Android") Now in Android App ================== **Learn how this app was designed and built in the [design case study](https://goo.gle/nia-figma), [architecture learning journey](docs/ArchitectureLearningJourney.md) and [modularization learning journey](docs/ModularizationLearningJourney.md).** This is the repository for the [Now in Android](https://developer.android.com/series/now-in-android) app. It is a **work in progress** 🚧. **Now in Android** is a fully functional Android app built entirely with Kotlin and Jetpack Compose. It follows Android design and development best practices and is intended to be a useful reference for developers. As a running app, it's intended to help developers keep up-to-date with the world of Android development by providing regular news updates. The app is currently in development. The `prodRelease` variant is [available on the Play Store](https://play.google.com/store/apps/details?id=com.google.samples.apps.nowinandroid). # Features **Now in Android** displays content from the [Now in Android](https://developer.android.com/series/now-in-android) series. Users can browse for links to recent videos, articles and other content. Users can also follow topics they are interested in, and be notified when new content is published which matches interests they are following. ## Screenshots ![Screenshot showing For You screen, Interests screen and Topic detail screen](docs/images/screenshots.png "Screenshot showing For You screen, Interests screen and Topic detail screen") # Development Environment **Now in Android** uses the Gradle build system and can be imported directly into Android Studio (make sure you are using the latest stable version available [here](https://developer.android.com/studio)). Change the run configuration to `app`. ![image](https://user-images.githubusercontent.com/873212/210559920-ef4a40c5-c8e0-478b-bb00-4879a8cf184a.png) The `demoDebug` and `demoRelease` build variants can be built and run (the `prod` variants use a backend server which is not currently publicly available). ![image](https://user-images.githubusercontent.com/873212/210560507-44045dc5-b6d5-41ca-9746-f0f7acf22f8e.png) Once you're up and running, you can refer to the learning journeys below to get a better understanding of which libraries and tools are being used, the reasoning behind the approaches to UI, testing, architecture and more, and how all of these different pieces of the project fit together to create a complete app. # Architecture The **Now in Android** app follows the [official architecture guidance](https://developer.android.com/topic/architecture) and is described in detail in the [architecture learning journey](docs/ArchitectureLearningJourney.md). # Modularization The **Now in Android** app has been fully modularized and you can find the detailed guidance and description of the modularization strategy used in [modularization learning journey](docs/ModularizationLearningJourney.md). # Build The app contains the usual `debug` and `release` build variants. In addition, the `benchmark` variant of `app` is used to test startup performance and generate a baseline profile (see below for more information). `app-nia-catalog` is a standalone app that displays the list of components that are stylized for **Now in Android**. The app also uses [product flavors](https://developer.android.com/studio/build/build-variants#product-flavors) to control where content for the app should be loaded from. The `demo` flavor uses static local data to allow immediate building and exploring of the UI. The `prod` flavor makes real network calls to a backend server, providing up-to-date content. At this time, there is not a public backend available. For normal development use the `demoDebug` variant. For UI performance testing use the `demoRelease` variant. # Testing To facilitate testing of components, **Now in Android** uses dependency injection with [Hilt](https://developer.android.com/training/dependency-injection/hilt-android). Most data layer components are defined as interfaces. Then, concrete implementations (with various dependencies) are bound to provide those interfaces to other components in the app. In tests, **Now in Android** notably does _not_ use any mocking libraries. Instead, the production implementations can be replaced with test doubles using Hilt's testing APIs (or via manual constructor injection for `ViewModel` tests). These test doubles implement the same interface as the production implementations and generally provide a simplified (but still realistic) implementation with additional testing hooks. This results in less brittle tests that may exercise more production code, instead of just verifying specific calls against mocks. Examples: - In instrumentation tests, a temporary folder is used to store the user's preferences, which is wiped after each test. This allows using the real `DataStore` and exercising all related code, instead of mocking the flow of data updates. - There are `Test` implementations of each repository, which implement the normal, full repository interface and also provide test-only hooks. `ViewModel` tests use these `Test` repositories, and thus can use the test-only hooks to manipulate the state of the `Test` repository and verify the resulting behavior, instead of checking that specific repository methods were called. To run the tests execute the following gradle tasks: - `testDemoDebug` run all local tests against the `demoDebug` variant. Screenshot tests will fail (see below for explanation). To avoid this, run `recordRoborazziDemoDebug` prior to running unit tests. - `connectedDemoDebugAndroidTest` run all instrumented tests against the `demoDebug` variant. > [!NOTE] > You should not run `./gradlew test` or `./gradlew connectedAndroidTest` as this will execute tests against _all_ build variants which is both unnecessary and will result in failures as only the `demoDebug` variant is supported. No other variants have any tests (although this might change in future). ## Screenshot tests A screenshot test takes a screenshot of a screen or a UI component within the app, and compares it with a previously recorded screenshot which is known to be rendered correctly. For example, Now in Android has [screenshot tests](https://github.com/android/nowinandroid/blob/main/app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt) to verify that the navigation is displayed correctly on different screen sizes ([known correct screenshots](https://github.com/android/nowinandroid/tree/main/app/src/testDemo/screenshots)). Now In Android uses [Roborazzi](https://github.com/takahirom/roborazzi) to run screenshot tests of certain screens and UI components. When working with screenshot tests the following gradle tasks are useful: - `verifyRoborazziDemoDebug` run all screenshot tests, verifying the screenshots against the known correct screenshots. - `recordRoborazziDemoDebug` record new "known correct" screenshots. Use this command when you have made changes to the UI and manually verified that they are rendered correctly. Screenshots will be stored in `modulename/src/test/screenshots`. - `compareRoborazziDemoDebug` create comparison images between failed tests and the known correct images. These can also be found in `modulename/src/test/screenshots`. > [!NOTE] > **Note on failing screenshot tests** > The known correct screenshots stored in this repository are recorded on CI using Linux. Other platforms may (and probably will) generate slightly different images, making the screenshot tests fail. When working on a non-Linux platform, a workaround to this is to run `recordRoborazziDemoDebug` on the `main` branch before starting work. After making changes, `verifyRoborazziDemoDebug` will identify only legitimate changes. For more information about screenshot testing [check out this talk](https://www.droidcon.com/2023/11/15/easy-screenshot-testing-with-compose/). # UI The app was designed using [Material 3 guidelines](https://m3.material.io/). Learn more about the design process and obtain the design files in the [Now in Android Material 3 Case Study](https://goo.gle/nia-figma) (design assets [also available as a PDF](docs/Now-In-Android-Design-File.pdf)). The Screens and UI elements are built entirely using [Jetpack Compose](https://developer.android.com/jetpack/compose). The app has two themes: - Dynamic color - uses colors based on the [user's current color theme](https://material.io/blog/announcing-material-you) (if supported) - Default theme - uses predefined colors when dynamic color is not supported Each theme also supports dark mode. The app uses adaptive layouts to [support different screen sizes](https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes). Find out more about the [UI architecture here](docs/ArchitectureLearningJourney.md#ui-layer). # Performance ## Benchmarks Find all tests written using [`Macrobenchmark`](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) in the `benchmarks` module. This module also contains the test to generate the Baseline profile. ## Baseline profiles The baseline profile for this app is located at [`app/src/main/baseline-prof.txt`](app/src/main/baseline-prof.txt). It contains rules that enable AOT compilation of the critical user path taken during app launch. For more information on baseline profiles, read [this document](https://developer.android.com/studio/profile/baselineprofiles). > [!NOTE] > The baseline profile needs to be re-generated for release builds that touch code which changes app startup. To generate the baseline profile, select the `benchmark` build variant and run the `BaselineProfileGenerator` benchmark test on an AOSP Android Emulator. Then copy the resulting baseline profile from the emulator to [`app/src/main/baseline-prof.txt`](app/src/main/baseline-prof.txt). ## Compose compiler metrics Run the following command to get and analyze compose compiler metrics: ```bash ./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true ``` The reports files will be added to [build/compose-reports](build/compose-reports). The metrics files will also be added to [build/compose-metrics](build/compose-metrics). For more information on Compose compiler metrics, see [this blog post](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8). # License **Now in Android** is distributed under the terms of the Apache License (Version 2.0). See the [license](LICENSE) for more information. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/README.md ================================================ # `:app` ## Module dependency graph ```mermaid --- config: layout: elk elk: nodePlacementStrategy: SIMPLE --- graph TB subgraph :feature direction TB subgraph :feature:settings direction TB :feature:settings:impl[impl]:::android-library end subgraph :feature:foryou direction TB :feature:foryou:api[api]:::android-library :feature:foryou:impl[impl]:::android-library end subgraph :feature:bookmarks direction TB :feature:bookmarks:api[api]:::android-library :feature:bookmarks:impl[impl]:::android-library end subgraph :feature:search direction TB :feature:search:api[api]:::android-library :feature:search:impl[impl]:::android-library end subgraph :feature:interests direction TB :feature:interests:api[api]:::android-library :feature:interests:impl[impl]:::android-library end subgraph :feature:topic direction TB :feature:topic:api[api]:::android-library :feature:topic:impl[impl]:::android-library end end subgraph :sync direction TB :sync:work[work]:::android-library end subgraph :core direction TB :core:analytics[analytics]:::android-library :core:common[common]:::jvm-library :core:data[data]:::android-library :core:database[database]:::android-library :core:datastore[datastore]:::android-library :core:datastore-proto[datastore-proto]:::jvm-library :core:designsystem[designsystem]:::android-library :core:domain[domain]:::android-library :core:model[model]:::jvm-library :core:navigation[navigation]:::android-library :core:network[network]:::android-library :core:notifications[notifications]:::android-library :core:ui[ui]:::android-library end :benchmarks[benchmarks]:::android-test :app[app]:::android-application :app -.->|baselineProfile| :benchmarks :app -.-> :core:analytics :app -.-> :core:common :app -.-> :core:data :app -.-> :core:designsystem :app -.-> :core:model :app -.-> :core:ui :app -.-> :feature:bookmarks:api :app -.-> :feature:bookmarks:impl :app -.-> :feature:foryou:api :app -.-> :feature:foryou:impl :app -.-> :feature:interests:api :app -.-> :feature:interests:impl :app -.-> :feature:search:api :app -.-> :feature:search:impl :app -.-> :feature:settings:impl :app -.-> :feature:topic:api :app -.-> :feature:topic:impl :app -.-> :sync:work :benchmarks -.->|testedApks| :app :core:data -.-> :core:analytics :core:data --> :core:common :core:data --> :core:database :core:data --> :core:datastore :core:data --> :core:network :core:data -.-> :core:notifications :core:database --> :core:model :core:datastore -.-> :core:common :core:datastore --> :core:datastore-proto :core:datastore --> :core:model :core:domain --> :core:data :core:domain --> :core:model :core:network --> :core:common :core:network --> :core:model :core:notifications -.-> :core:common :core:notifications --> :core:model :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model :feature:bookmarks:api --> :core:navigation :feature:bookmarks:impl -.-> :core:data :feature:bookmarks:impl -.-> :core:designsystem :feature:bookmarks:impl -.-> :core:ui :feature:bookmarks:impl -.-> :feature:bookmarks:api :feature:bookmarks:impl -.-> :feature:topic:api :feature:foryou:api --> :core:navigation :feature:foryou:impl -.-> :core:designsystem :feature:foryou:impl -.-> :core:domain :feature:foryou:impl -.-> :core:notifications :feature:foryou:impl -.-> :core:ui :feature:foryou:impl -.-> :feature:foryou:api :feature:foryou:impl -.-> :feature:topic:api :feature:interests:api --> :core:navigation :feature:interests:impl -.-> :core:designsystem :feature:interests:impl -.-> :core:domain :feature:interests:impl -.-> :core:ui :feature:interests:impl -.-> :feature:interests:api :feature:interests:impl -.-> :feature:topic:api :feature:search:api -.-> :core:domain :feature:search:api --> :core:navigation :feature:search:impl -.-> :core:designsystem :feature:search:impl -.-> :core:domain :feature:search:impl -.-> :core:ui :feature:search:impl -.-> :feature:interests:api :feature:search:impl -.-> :feature:search:api :feature:search:impl -.-> :feature:topic:api :feature:settings:impl -.-> :core:data :feature:settings:impl -.-> :core:designsystem :feature:settings:impl -.-> :core:ui :feature:topic:api -.-> :core:designsystem :feature:topic:api --> :core:navigation :feature:topic:api -.-> :core:ui :feature:topic:impl -.-> :core:data :feature:topic:impl -.-> :core:designsystem :feature:topic:impl -.-> :core:ui :feature:topic:impl -.-> :feature:topic:api :sync:work -.-> :core:analytics :sync:work -.-> :core:data :sync:work -.-> :core:notifications classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; ```
📋 Graph legend ```mermaid graph TB application[application]:::android-application feature[feature]:::android-feature library[library]:::android-library jvm[jvm]:::jvm-library application -.-> feature library --> jvm classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; ```
================================================ FILE: app/benchmark-rules.pro ================================================ # Proguard rules for the `benchmark` build type. # # Obsfuscation must be disabled for the build variant that generates Baseline Profile, otherwise # wrong symbols would be generated. The generated Baseline Profile will be properly applied when generated # without obfuscation and your app is being obfuscated. -dontobfuscate # Please add these rules to your existing keep rules in order to suppress warnings. # This is generated automatically by the Android Gradle plugin. -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.conscrypt.Conscrypt$Version -dontwarn org.conscrypt.Conscrypt -dontwarn org.conscrypt.ConscryptHostnameVerifier -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE ================================================ FILE: app/build.gradle.kts ================================================ /* * 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.samples.apps.nowinandroid.NiaBuildType plugins { alias(libs.plugins.nowinandroid.android.application) alias(libs.plugins.nowinandroid.android.application.compose) alias(libs.plugins.nowinandroid.android.application.flavors) alias(libs.plugins.nowinandroid.android.application.jacoco) alias(libs.plugins.nowinandroid.android.application.firebase) alias(libs.plugins.nowinandroid.hilt) alias(libs.plugins.google.osslicenses) alias(libs.plugins.baselineprofile) alias(libs.plugins.roborazzi) alias(libs.plugins.kotlin.serialization) } android { defaultConfig { applicationId = "com.google.samples.apps.nowinandroid" versionCode = 8 versionName = "0.1.2" // X.Y.Z; X = Major, Y = minor, Z = Patch level // Custom test runner to set up Hilt dependency graph testInstrumentationRunner = "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" } buildTypes { debug { applicationIdSuffix = NiaBuildType.DEBUG.applicationIdSuffix } release { isMinifyEnabled = providers.gradleProperty("minifyWithR8") .map(String::toBooleanStrict).getOrElse(true) applicationIdSuffix = NiaBuildType.RELEASE.applicationIdSuffix proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") // To publish on the Play store a private signing key is required, but to allow anyone // who clones the code to sign and run the release variant, use the debug signing key. // TODO: Abstract the signing configuration to a separate file to avoid hardcoding this. signingConfig = signingConfigs.named("debug").get() // Ensure Baseline Profile is fresh for release builds. baselineProfile.automaticGenerationDuringBuild = true } } packaging { resources { excludes.add("/META-INF/{AL2.0,LGPL2.1}") } } testOptions.unitTests.isIncludeAndroidResources = true namespace = "com.google.samples.apps.nowinandroid" } dependencies { implementation(projects.feature.interests.api) implementation(projects.feature.interests.impl) implementation(projects.feature.foryou.api) implementation(projects.feature.foryou.impl) implementation(projects.feature.bookmarks.api) implementation(projects.feature.bookmarks.impl) implementation(projects.feature.topic.api) implementation(projects.feature.topic.impl) implementation(projects.feature.search.api) implementation(projects.feature.search.impl) implementation(projects.feature.settings.impl) implementation(projects.core.common) implementation(projects.core.ui) implementation(projects.core.designsystem) implementation(projects.core.data) implementation(projects.core.model) implementation(projects.core.analytics) implementation(projects.sync.work) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) implementation(libs.androidx.compose.material3.adaptive.navigation3) implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.runtime.tracing) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.viewModel.navigation3) implementation(libs.androidx.profileinstaller) implementation(libs.androidx.tracing.ktx) implementation(libs.androidx.window.core) implementation(libs.kotlinx.coroutines.guava) implementation(libs.coil.kt) implementation(libs.kotlinx.serialization.json) ksp(libs.hilt.compiler) debugImplementation(libs.androidx.compose.ui.testManifest) debugImplementation(projects.uiTestHiltManifest) kspTest(libs.hilt.compiler) testImplementation(projects.core.dataTest) testImplementation(projects.core.datastoreTest) testImplementation(libs.hilt.android.testing) testImplementation(projects.sync.syncTest) testImplementation(libs.kotlin.test) testDemoImplementation(libs.androidx.navigation.testing) testDemoImplementation(libs.robolectric) testDemoImplementation(libs.roborazzi) testDemoImplementation(projects.core.screenshotTesting) testDemoImplementation(projects.core.testing) androidTestImplementation(projects.core.testing) androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.kotlin.test) baselineProfile(projects.benchmarks) } baselineProfile { // Don't build on every iteration of a full assemble. // Instead enable generation directly for the release build variant. automaticGenerationDuringBuild = false // Make use of Dex Layout Optimizations via Startup Profiles dexLayoutOptimization = true } dependencyGuard { configuration("prodReleaseRuntimeClasspath") } ================================================ FILE: app/dependencies/prodReleaseRuntimeClasspath.txt ================================================ androidx.activity:activity-compose:1.12.0 androidx.activity:activity-ktx:1.12.0 androidx.activity:activity:1.12.0 androidx.annotation:annotation-experimental:1.5.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 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.8.0 androidx.collection:collection-jvm:1.5.0 androidx.collection:collection-ktx:1.5.0 androidx.collection:collection:1.5.0 androidx.compose.animation:animation-android:1.10.0-beta02 androidx.compose.animation:animation-core-android:1.10.0-beta02 androidx.compose.animation:animation-core:1.10.0-beta02 androidx.compose.animation:animation:1.10.0-beta02 androidx.compose.foundation:foundation-android:1.10.0-beta02 androidx.compose.foundation:foundation-layout-android:1.10.0-beta02 androidx.compose.foundation:foundation-layout:1.10.0-beta02 androidx.compose.foundation:foundation:1.10.0-beta02 androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-navigation3-android:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha04 androidx.compose.material3.adaptive:adaptive:1.3.0-alpha04 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha04 androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha04 androidx.compose.material3:material3-android:1.5.0-alpha04 androidx.compose.material3:material3-window-size-class-android:1.5.0-alpha04 androidx.compose.material3:material3-window-size-class:1.5.0-alpha04 androidx.compose.material3:material3:1.5.0-alpha04 androidx.compose.material:material-icons-core-android:1.7.8 androidx.compose.material:material-icons-core:1.7.8 androidx.compose.material:material-icons-extended-android:1.7.8 androidx.compose.material:material-icons-extended:1.7.8 androidx.compose.material:material-ripple-android:1.10.0-alpha04 androidx.compose.material:material-ripple:1.10.0-alpha04 androidx.compose.runtime:runtime-android:1.10.0-beta02 androidx.compose.runtime:runtime-annotation-android:1.10.0-beta02 androidx.compose.runtime:runtime-annotation:1.10.0-beta02 androidx.compose.runtime:runtime-retain-android:1.10.0-beta02 androidx.compose.runtime:runtime-retain:1.10.0-beta02 androidx.compose.runtime:runtime-saveable-android:1.10.0-beta02 androidx.compose.runtime:runtime-saveable:1.10.0-beta02 androidx.compose.runtime:runtime-tracing:1.10.0-beta02 androidx.compose.runtime:runtime:1.10.0-beta02 androidx.compose.ui:ui-android:1.10.0-beta02 androidx.compose.ui:ui-geometry-android:1.10.0-beta02 androidx.compose.ui:ui-geometry:1.10.0-beta02 androidx.compose.ui:ui-graphics-android:1.10.0-beta02 androidx.compose.ui:ui-graphics:1.10.0-beta02 androidx.compose.ui:ui-text-android:1.10.0-beta02 androidx.compose.ui:ui-text:1.10.0-beta02 androidx.compose.ui:ui-tooling-preview-android:1.10.0-beta02 androidx.compose.ui:ui-tooling-preview:1.10.0-beta02 androidx.compose.ui:ui-unit-android:1.10.0-beta02 androidx.compose.ui:ui-unit:1.10.0-beta02 androidx.compose.ui:ui-util-android:1.10.0-beta02 androidx.compose.ui:ui-util:1.10.0-beta02 androidx.compose.ui:ui:1.10.0-beta02 androidx.compose:compose-bom-alpha:2025.09.01 androidx.concurrent:concurrent-futures-ktx:1.1.0 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.16.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core-viewtree:1.0.0 androidx.core:core:1.16.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 androidx.datastore:datastore-android:1.2.0 androidx.datastore:datastore-core-android:1.2.0 androidx.datastore:datastore-core-okio-jvm:1.2.0 androidx.datastore:datastore-core-okio:1.2.0 androidx.datastore:datastore-core:1.2.0 androidx.datastore:datastore-preferences-android:1.2.0 androidx.datastore:datastore-preferences-core-android:1.2.0 androidx.datastore:datastore-preferences-core:1.2.0 androidx.datastore:datastore-preferences-external-protobuf:1.2.0 androidx.datastore:datastore-preferences-proto:1.2.0 androidx.datastore:datastore-preferences:1.2.0 androidx.datastore:datastore:1.2.0 androidx.documentfile:documentfile:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0 androidx.dynamicanimation:dynamicanimation:1.0.0 androidx.emoji2:emoji2-views-helper:1.4.0 androidx.emoji2:emoji2:1.4.0 androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.4 androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-shapes-android:1.0.1 androidx.graphics:graphics-shapes:1.0.1 androidx.hilt:hilt-common:1.2.0 androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0-alpha02 androidx.hilt:hilt-lifecycle-viewmodel:1.3.0-alpha02 androidx.hilt:hilt-work:1.2.0 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.10.0 androidx.lifecycle:lifecycle-common-jvm:2.10.0 androidx.lifecycle:lifecycle-common:2.10.0 androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0 androidx.lifecycle:lifecycle-livedata-core:2.10.0 androidx.lifecycle:lifecycle-livedata:2.10.0 androidx.lifecycle:lifecycle-process:2.10.0 androidx.lifecycle:lifecycle-runtime-android:2.10.0 androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0 androidx.lifecycle:lifecycle-runtime-compose:2.10.0 androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0 androidx.lifecycle:lifecycle-runtime-ktx:2.10.0 androidx.lifecycle:lifecycle-runtime:2.10.0 androidx.lifecycle:lifecycle-service:2.10.0 androidx.lifecycle:lifecycle-viewmodel-android:2.10.0 androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0 androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0 androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0 androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.10.0 androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0 androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0 androidx.lifecycle:lifecycle-viewmodel:2.10.0 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.navigation3:navigation3-runtime-android:1.0.0 androidx.navigation3:navigation3-runtime:1.0.0 androidx.navigation3:navigation3-ui-android:1.0.0 androidx.navigation3:navigation3-ui:1.0.0 androidx.navigationevent:navigationevent-android:1.0.0 androidx.navigationevent:navigationevent-compose-android:1.0.0 androidx.navigationevent:navigationevent-compose:1.0.0 androidx.navigationevent:navigationevent:1.0.0 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.4.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.room:room-common-jvm:2.8.3 androidx.room:room-common:2.8.3 androidx.room:room-ktx:2.8.3 androidx.room:room-runtime-android:2.8.3 androidx.room:room-runtime:2.8.3 androidx.savedstate:savedstate-android:1.4.0 androidx.savedstate:savedstate-compose-android:1.4.0 androidx.savedstate:savedstate-compose:1.4.0 androidx.savedstate:savedstate-ktx:1.4.0 androidx.savedstate:savedstate:1.4.0 androidx.sqlite:sqlite-android:2.6.1 androidx.sqlite:sqlite-framework-android:2.6.1 androidx.sqlite:sqlite-framework:2.6.1 androidx.sqlite:sqlite:2.6.1 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing-ktx:1.3.0-alpha02 androidx.tracing:tracing-perfetto:1.0.0 androidx.tracing:tracing:1.3.0-alpha02 androidx.transition:transition:1.6.0 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 androidx.window:window-core-android:1.5.0 androidx.window:window-core:1.5.0 androidx.window:window:1.5.0 androidx.work:work-runtime-ktx:2.10.0 androidx.work:work-runtime:2.10.0 com.caverock:androidsvg-aar:1.4 com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.accompanist:accompanist-permissions:0.37.0 com.google.android.datatransport:transport-api:3.2.0 com.google.android.datatransport:transport-backend-cct:3.3.0 com.google.android.datatransport:transport-runtime:3.3.0 com.google.android.gms:play-services-ads-identifier:18.0.0 com.google.android.gms:play-services-base:18.5.0 com.google.android.gms:play-services-basement:18.4.0 com.google.android.gms:play-services-cloud-messaging:17.2.0 com.google.android.gms:play-services-measurement-api:22.1.2 com.google.android.gms:play-services-measurement-base:22.1.2 com.google.android.gms:play-services-measurement-impl:22.1.2 com.google.android.gms:play-services-measurement-sdk-api:22.1.2 com.google.android.gms:play-services-measurement-sdk:22.1.2 com.google.android.gms:play-services-measurement:22.1.2 com.google.android.gms:play-services-oss-licenses:17.1.0 com.google.android.gms:play-services-stats:17.0.2 com.google.android.gms:play-services-tasks:18.2.0 com.google.code.findbugs:jsr305:3.0.2 com.google.dagger:dagger-lint-aar:2.59 com.google.dagger:dagger:2.59 com.google.dagger:hilt-android:2.59 com.google.dagger:hilt-core:2.59 com.google.errorprone:error_prone_annotations:2.26.0 com.google.firebase:firebase-abt:21.1.1 com.google.firebase:firebase-analytics:22.1.2 com.google.firebase:firebase-annotations:16.2.0 com.google.firebase:firebase-bom:33.7.0 com.google.firebase:firebase-common-ktx:21.0.0 com.google.firebase:firebase-common:21.0.0 com.google.firebase:firebase-components:18.0.0 com.google.firebase:firebase-config-interop:16.0.1 com.google.firebase:firebase-config:22.0.1 com.google.firebase:firebase-crashlytics:19.3.0 com.google.firebase:firebase-datatransport:19.0.0 com.google.firebase:firebase-encoders-json:18.0.1 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.2.0 com.google.firebase:firebase-installations:18.0.0 com.google.firebase:firebase-measurement-connector:20.0.1 com.google.firebase:firebase-messaging:24.1.0 com.google.firebase:firebase-perf:21.0.3 com.google.firebase:firebase-sessions:2.0.7 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:4.29.2 com.google.protobuf:protobuf-kotlin-lite:4.29.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.9.1 com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-kotlinx-serialization:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 io.coil-kt:coil-base:2.7.0 io.coil-kt:coil-compose-base:2.7.0 io.coil-kt:coil-compose:2.7.0 io.coil-kt:coil-svg:2.7.0 io.coil-kt:coil:2.7.0 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 org.checkerframework:checker-qual:3.12.0 org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib:2.3.0 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.1 org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.1 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0 org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0 org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0 org.jetbrains:annotations:23.0.0 org.jspecify:jspecify:1.0.0 ================================================ FILE: app/google-services.json ================================================ { "project_info": { "project_number": "YourProjectId", "project_id": "abc", "storage_bucket": "abc" }, "client": [ { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } }, { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid.demo.debug" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } }, { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid.demo.benchmark" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } }, { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid.benchmark" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } }, { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid.debug" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } }, { "client_info": { "mobilesdk_app_id": "Your:App:Id", "android_client_info": { "package_name": "com.google.samples.apps.nowinandroid.demo" } }, "oauth_client": [], "api_key": [ { "current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } } ], "configuration_version": "1" } ================================================ FILE: app/prodRelease-badging.txt ================================================ package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' platformBuildVersionCode='36' compileSdkVersion='36' compileSdkVersionCodename='16' minSdkVersion:'23' targetSdkVersion:'36' uses-permission: name='android.permission.INTERNET' uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.POST_NOTIFICATIONS' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.google.android.c2dm.permission.RECEIVE' uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE' uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED' uses-permission: name='android.permission.FOREGROUND_SERVICE' uses-permission: name='com.google.samples.apps.nowinandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' application-label:'Now in Android' application-label-af:'Now in Android' application-label-am:'Now in Android' application-label-ar:'Now in Android' application-label-as:'Now in Android' application-label-az:'Now in Android' application-label-be:'Now in Android' application-label-bg:'Now in Android' application-label-bn:'Now in Android' application-label-bs:'Now in Android' application-label-ca:'Now in Android' application-label-cs:'Now in Android' application-label-da:'Now in Android' application-label-de:'Now in Android' application-label-el:'Now in Android' application-label-en-AU:'Now in Android' application-label-en-CA:'Now in Android' application-label-en-GB:'Now in Android' application-label-en-IN:'Now in Android' application-label-en-XC:'Now in Android' application-label-es:'Now in Android' application-label-es-US:'Now in Android' application-label-et:'Now in Android' application-label-eu:'Now in Android' application-label-fa:'Now in Android' application-label-fi:'Now in Android' application-label-fr:'Now in Android' application-label-fr-CA:'Now in Android' application-label-gl:'Now in Android' application-label-gu:'Now in Android' application-label-hi:'Now in Android' application-label-hr:'Now in Android' application-label-hu:'Now in Android' application-label-hy:'Now in Android' application-label-in:'Now in Android' application-label-is:'Now in Android' application-label-it:'Now in Android' application-label-iw:'Now in Android' application-label-ja:'Now in Android' application-label-ka:'Now in Android' application-label-kk:'Now in Android' application-label-km:'Now in Android' application-label-kn:'Now in Android' application-label-ko:'Now in Android' application-label-ky:'Now in Android' application-label-lo:'Now in Android' application-label-lt:'Now in Android' application-label-lv:'Now in Android' application-label-mk:'Now in Android' application-label-ml:'Now in Android' application-label-mn:'Now in Android' application-label-mr:'Now in Android' application-label-ms:'Now in Android' application-label-my:'Now in Android' application-label-nb:'Now in Android' application-label-ne:'Now in Android' application-label-nl:'Now in Android' application-label-or:'Now in Android' application-label-pa:'Now in Android' application-label-pl:'Now in Android' application-label-pt:'Now in Android' application-label-pt-BR:'Now in Android' application-label-pt-PT:'Now in Android' application-label-ro:'Now in Android' application-label-ru:'Now in Android' application-label-si:'Now in Android' application-label-sk:'Now in Android' application-label-sl:'Now in Android' application-label-sq:'Now in Android' application-label-sr:'Now in Android' application-label-sr-Latn:'Now in Android' application-label-sv:'Now in Android' application-label-sw:'Now in Android' application-label-ta:'Now in Android' application-label-te:'Now in Android' application-label-th:'Now in Android' application-label-tl:'Now in Android' application-label-tr:'Now in Android' application-label-uk:'Now in Android' application-label-ur:'Now in Android' application-label-uz:'Now in Android' application-label-vi:'Now in Android' application-label-zh-CN:'Now in Android' application-label-zh-HK:'Now in Android' application-label-zh-TW:'Now in Android' application-label-zu:'Now in Android' application-icon-120:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-160:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-240:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-320:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-480:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml' application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' uses-library-not-required:'android.ext.adservices' uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.sidecar' feature-group: label='' uses-feature: name='android.hardware.faketouch' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' main other-activities other-receivers other-services supports-screens: 'small' 'normal' 'large' 'xlarge' supports-any-density: 'true' locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si' 'sk' 'sl' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' densities: '120' '160' '240' '320' '480' '640' '65534' native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' ================================================ FILE: app/proguard-rules.pro ================================================ # Repackage classes into the default package to reduce the size of descriptors. -repackageclasses ================================================ FILE: app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt ================================================ /* * 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.ui import androidx.compose.ui.semantics.SemanticsActions.ScrollBy import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode import androidx.test.espresso.Espresso import androidx.test.espresso.NoActivityResumedException import com.google.samples.apps.nowinandroid.MainActivity import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule import com.google.samples.apps.nowinandroid.feature.interests.impl.LIST_PANE_TEST_TAG import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import javax.inject.Inject import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as BookmarksR import com.google.samples.apps.nowinandroid.feature.foryou.api.R as FeatureForyouR import com.google.samples.apps.nowinandroid.feature.search.api.R as FeatureSearchR import com.google.samples.apps.nowinandroid.feature.settings.impl.R as SettingsR /** * Tests all the navigation flows that are handled by the navigation library. */ @HiltAndroidTest class NavigationTest { /** * Manages the components' state and is used to perform injection on your test */ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) /** * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission. */ @get:Rule(order = 1) val postNotificationsPermission = GrantPostNotificationsPermissionRule() /** * Use the primary activity to initialize the app normally. */ @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() @Inject lateinit var topicsRepository: TopicsRepository @Inject lateinit var newsRepository: NewsRepository // The strings used for matching in these tests private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_navigate_up) private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_api_title) private val interests by composeTestRule.stringResource(FeatureSearchR.string.feature_search_api_interests) private val sampleTopic = "Headlines" private val appName by composeTestRule.stringResource(R.string.app_name) private val saved by composeTestRule.stringResource(BookmarksR.string.feature_bookmarks_api_title) private val settings by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_top_app_bar_action_icon_description) private val brand by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_brand_android) private val ok by composeTestRule.stringResource(SettingsR.string.feature_settings_impl_dismiss_dialog_button_text) @Before fun setup() = hiltRule.inject() @Test fun firstScreen_isForYou() { composeTestRule.apply { // VERIFY for you is selected onNodeWithText(forYou).assertIsSelected() } } // TODO: implement tests related to navigation & resetting of destinations (b/213307564) // Restoring content should be tested with another tab than the For You one, as that will // still succeed even when restoring state is turned off. /** * When navigating between the different top level destinations, we should restore the state * of previously visited destinations. */ @Test fun navigationBar_navigateToPreviouslySelectedTab_restoresContent() { composeTestRule.apply { // GIVEN the user follows a topic onNodeWithText(sampleTopic).performClick() // WHEN the user navigates to the Interests destination onNodeWithText(interests).performClick() // AND the user navigates to the For You destination onNodeWithText(forYou).performClick() // THEN the state of the For You destination is restored onNodeWithContentDescription(sampleTopic).assertIsOn() } } /** * When reselecting a tab, it should show that tab's start destination and restore its state. */ @Test fun navigationBar_reselectTab_keepsState() { composeTestRule.apply { // GIVEN the user follows a topic onNodeWithText(sampleTopic).performClick() // WHEN the user taps the For You navigation bar item onNodeWithText(forYou).performClick() // THEN the state of the For You destination is restored onNodeWithContentDescription(sampleTopic).assertIsOn() } } // @Test // fun navigationBar_reselectTab_resetsToStartDestination() { // // GIVEN the user is on the Topics destination and scrolls // // and navigates to the Topic Detail destination // // WHEN the user taps the Topics navigation bar item // // THEN the Topics destination shows in the same scrolled state // } /* * Top level destinations should never show an up affordance. */ @Test fun topLevelDestinations_doNotShowUpArrow() { composeTestRule.apply { // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithText(saved).performClick() onNodeWithContentDescription(navigateUp).assertDoesNotExist() onNodeWithText(interests).performClick() onNodeWithContentDescription(navigateUp).assertDoesNotExist() } } @Test fun topLevelDestinations_showTopBarWithTitle() { composeTestRule.apply { // Verify that the top bar contains the app name on the first screen. onNodeWithText(appName).assertExists() // Go to the saved tab, verify that the top bar contains "saved". This means // we'll have 2 elements with the text "saved" on screen. One in the top bar, and // one in the bottom navigation. onNodeWithText(saved).performClick() onAllNodesWithText(saved).assertCountEquals(2) // As above but for the interests tab. onNodeWithText(interests).performClick() onAllNodesWithText(interests).assertCountEquals(2) } } @Test fun topLevelDestinations_showSettingsIcon() { composeTestRule.apply { onNodeWithContentDescription(settings).assertExists() onNodeWithText(saved).performClick() onNodeWithContentDescription(settings).assertExists() onNodeWithText(interests).performClick() onNodeWithContentDescription(settings).assertExists() } } @Test fun whenSettingsIconIsClicked_settingsDialogIsShown() { composeTestRule.apply { onNodeWithContentDescription(settings).performClick() // Check that one of the settings is actually displayed. onNodeWithText(brand).assertExists() } } @Test fun whenSettingsDialogDismissed_previousScreenIsDisplayed() { composeTestRule.apply { // Navigate to the saved screen, open the settings dialog, then close it. onNodeWithText(saved).performClick() onNodeWithContentDescription(settings).performClick() onNodeWithText(ok).performClick() // Check that the saved screen is still visible and selected. onNode(hasText(saved) and hasTestTag("NiaNavItem")).assertIsSelected() } } /* * There should always be at most one instance of a top-level destination at the same time. */ @Test(expected = NoActivityResumedException::class) fun homeDestination_back_quitsApp() { composeTestRule.apply { // GIVEN the user navigates to the Interests destination onNodeWithText(interests).performClick() // and then navigates to the For you destination onNodeWithText(forYou).performClick() // WHEN the user uses the system button/gesture to go back Espresso.pressBack() // THEN the app quits } } /* * When pressing back from any top level destination except "For you", the app navigates back * to the "For you" destination, no matter which destinations you visited in between. */ @Test fun navigationBar_backFromAnyDestination_returnsToForYou() { composeTestRule.apply { // GIVEN the user navigated to the Interests destination onNodeWithText(interests).performClick() // TODO: Add another destination here to increase test coverage, see b/226357686. // WHEN the user uses the system button/gesture to go back, Espresso.pressBack() // THEN the app shows the For You destination onNodeWithText(forYou).assertExists() } } // TODO decide if backStack should preserve previous stacks when navigating back to home tab (ForYou) // https://github.com/android/nowinandroid/issues/1937 @Ignore @Test fun navigationBar_multipleBackStackInterests() { composeTestRule.apply { onNodeWithText(interests).performClick() // Select the last topic val topic = runBlocking { topicsRepository.getTopics().first().sortedBy(Topic::name).last() } onNodeWithTag(LIST_PANE_TEST_TAG).performScrollToNode(hasText(topic.name)) onNodeWithText(topic.name).performClick() // Verify the topic is still shown onNodeWithTag("topic:${topic.id}").assertIsDisplayed() // Switch tab onNodeWithText(forYou).performClick() // Come back to Interests onNodeWithText(interests).performClick() // Verify the topic is still shown onNodeWithTag("topic:${topic.id}").assertExists() } } @Test fun navigatingToTopicFromForYou_showsTopicDetails() { composeTestRule.apply { // Get the first news resource val newsResource = runBlocking { newsRepository.getNewsResources().first().first() } // Get its first topic and follow it val topic = newsResource.topics.first() onNodeWithText(topic.name).performClick() // Get the news feed and scroll to the news resource // Note: Possible flakiness. If the content of the news resource is long then the topic // tag might not be visible meaning it cannot be clicked onNodeWithTag("forYou:feed") .performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}")) .fetchSemanticsNode() .apply { val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}") .fetchSemanticsNode() config[ScrollBy].action?.invoke( 0f, // to ensure the bottom of the card is visible, // manually scroll the difference between the height of // the scrolling node and the height of the card (newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(), ) } // Click the first topic tag onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true) .onFirst() .performClick() // Verify that we're on the correct topic details screen onNodeWithTag("topic:${topic.id}").assertExists() } } } ================================================ FILE: app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/UiTestExtensions.kt ================================================ /* * 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 import androidx.annotation.StringRes import androidx.compose.ui.test.junit4.AndroidComposeTestRule import kotlin.properties.ReadOnlyProperty fun AndroidComposeTestRule<*, *>.stringResource( @StringRes resId: Int, ): ReadOnlyProperty = ReadOnlyProperty { _, _ -> activity.getString(resId) } ================================================ FILE: app/src/benchmark/res/values/colors.xml ================================================ #000000 #FF006780 ================================================ FILE: app/src/benchmark/res/values-night/colors.xml ================================================ #FFFFFF #FF006780 ================================================ FILE: app/src/debug/res/values/colors.xml ================================================ #000000 #FFA23F16 ================================================ FILE: app/src/debug/res/values-night/colors.xml ================================================ #FFFFFF #FFA23F16 ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt ================================================ /* * 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 import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats import androidx.tracing.trace import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository 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.ui.LocalTimeZone import com.google.samples.apps.nowinandroid.ui.NiaApp import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { /** * Lazily inject [JankStats], which is used to track jank throughout the app. */ @Inject lateinit var lazyStats: dagger.Lazy @Inject lateinit var networkMonitor: NetworkMonitor @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @Inject lateinit var analyticsHelper: AnalyticsHelper @Inject lateinit var userNewsResourceRepository: UserNewsResourceRepository private val viewModel: MainActivityViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) // We keep this as a mutable state, so that we can track changes inside the composition. // This allows us to react to dark/light mode changes. var themeSettings by mutableStateOf( ThemeSettings( darkTheme = resources.configuration.isSystemInDarkTheme, androidTheme = Loading.shouldUseAndroidTheme, disableDynamicTheming = Loading.shouldDisableDynamicTheming, ), ) // Update the uiState lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { combine( isSystemInDarkTheme(), viewModel.uiState, ) { systemDark, uiState -> ThemeSettings( darkTheme = uiState.shouldUseDarkTheme(systemDark), androidTheme = uiState.shouldUseAndroidTheme, disableDynamicTheming = uiState.shouldDisableDynamicTheming, ) } .onEach { themeSettings = it } .map { it.darkTheme } .distinctUntilChanged() .collect { darkTheme -> trace("niaEdgeToEdge") { // Turn off the decor fitting system windows, which allows us to handle insets, // including IME animations, and go edge-to-edge. // This is the same parameters as the default enableEdgeToEdge call, but we manually // resolve whether or not to show dark theme using uiState, since it can be different // than the configuration's dark theme value based on the user preference. enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( lightScrim = android.graphics.Color.TRANSPARENT, darkScrim = android.graphics.Color.TRANSPARENT, ) { darkTheme }, navigationBarStyle = SystemBarStyle.auto( lightScrim = lightScrim, darkScrim = darkScrim, ) { darkTheme }, ) } } } } // Keep the splash screen on-screen until the UI state is loaded. This condition is // evaluated each time the app needs to be redrawn so it should be fast to avoid blocking // the UI. splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() } setContent { val appState = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle() CompositionLocalProvider( LocalAnalyticsHelper provides analyticsHelper, LocalTimeZone provides currentTimeZone, ) { NiaTheme( darkTheme = themeSettings.darkTheme, androidTheme = themeSettings.androidTheme, disableDynamicTheming = themeSettings.disableDynamicTheming, ) { NiaApp(appState) } } } } override fun onResume() { super.onResume() lazyStats.get().isTrackingEnabled = true } override fun onPause() { super.onPause() lazyStats.get().isTrackingEnabled = false } } /** * The default light scrim, as defined by androidx and the platform: * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598 */ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF) /** * The default dark scrim, as defined by androidx and the platform: * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598 */ private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b) /** * Class for the system theme settings. * This wrapping class allows us to combine all the changes and prevent unnecessary recompositions. */ data class ThemeSettings( val darkTheme: Boolean, val androidTheme: Boolean, val disableDynamicTheming: Boolean, ) ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt ================================================ /* * 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 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading import com.google.samples.apps.nowinandroid.MainActivityUiState.Success import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand import com.google.samples.apps.nowinandroid.core.model.data.UserData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( userDataRepository: UserDataRepository, ) : ViewModel() { val uiState: StateFlow = userDataRepository.userData.map { Success(it) }.stateIn( scope = viewModelScope, initialValue = Loading, started = SharingStarted.WhileSubscribed(5_000), ) } sealed interface MainActivityUiState { data object Loading : MainActivityUiState data class Success(val userData: UserData) : MainActivityUiState { override val shouldDisableDynamicTheming = !userData.useDynamicColor override val shouldUseAndroidTheme: Boolean = when (userData.themeBrand) { ThemeBrand.DEFAULT -> false ThemeBrand.ANDROID -> true } override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = when (userData.darkThemeConfig) { DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkTheme DarkThemeConfig.LIGHT -> false DarkThemeConfig.DARK -> true } } /** * Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen. */ fun shouldKeepSplashScreen() = this is Loading /** * Returns `true` if the dynamic color is disabled. */ val shouldDisableDynamicTheming: Boolean get() = true /** * Returns `true` if the Android theme should be used. */ val shouldUseAndroidTheme: Boolean get() = false /** * Returns `true` if dark theme should be used. */ fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt ================================================ /* * 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 import android.app.Application import android.content.pm.ApplicationInfo import android.os.StrictMode import android.os.StrictMode.ThreadPolicy.Builder import coil.ImageLoader import coil.ImageLoaderFactory import com.google.samples.apps.nowinandroid.sync.initializers.Sync import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject /** * [Application] class for NiA */ @HiltAndroidApp class NiaApplication : Application(), ImageLoaderFactory { @Inject lateinit var imageLoader: dagger.Lazy @Inject lateinit var profileVerifierLogger: ProfileVerifierLogger override fun onCreate() { super.onCreate() setStrictModePolicy() // Initialize Sync; the system responsible for keeping data in the app up to date. Sync.initialize(context = this) profileVerifierLogger() } override fun newImageLoader(): ImageLoader = imageLoader.get() /** * Return true if the application is debuggable. */ private fun isDebuggable(): Boolean { return 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE } /** * Set a thread policy that detects all potential problems on the main thread, such as network * and disk access. * * If a problem is found, the offending call will be logged and the application will be killed. */ private fun setStrictModePolicy() { if (isDebuggable()) { StrictMode.setThreadPolicy( Builder().detectAll().penaltyLog().build(), ) } } } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt ================================================ /* * 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.di import android.app.Activity import android.util.Log import android.view.Window import androidx.metrics.performance.JankStats import androidx.metrics.performance.JankStats.OnFrameListener import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityComponent @Module @InstallIn(ActivityComponent::class) object JankStatsModule { @Provides fun providesOnFrameListener(): OnFrameListener = OnFrameListener { frameData -> // Make sure to only log janky frames. if (frameData.isJank) { // We're currently logging this but would better report it to a backend. Log.v("NiA Jank", frameData.toString()) } } @Provides fun providesWindow(activity: Activity): Window = activity.window @Provides fun providesJankStats( window: Window, frameListener: OnFrameListener, ): JankStats = JankStats.createAndTrack(window, frameListener) } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt ================================================ /* * Copyright 2025 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.navigation import androidx.annotation.StringRes import androidx.compose.ui.graphics.vector.ImageVector import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey import com.google.samples.apps.nowinandroid.feature.bookmarks.api.R as bookmarksR import com.google.samples.apps.nowinandroid.feature.foryou.api.R as forYouR import com.google.samples.apps.nowinandroid.feature.search.api.R as searchR /** * Type for the top level navigation items in the application. Contains UI information about the * current route that is used in the top app bar and common navigation UI. * * @param selectedIcon The icon to be displayed in the navigation UI when this destination is * selected. * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is * not selected. * @param iconTextId Text that to be displayed in the navigation UI. * @param titleTextId Text that is displayed on the top app bar. */ data class TopLevelNavItem( val selectedIcon: ImageVector, val unselectedIcon: ImageVector, @StringRes val iconTextId: Int, @StringRes val titleTextId: Int, ) val FOR_YOU = TopLevelNavItem( selectedIcon = NiaIcons.Upcoming, unselectedIcon = NiaIcons.UpcomingBorder, iconTextId = forYouR.string.feature_foryou_api_title, titleTextId = R.string.app_name, ) val BOOKMARKS = TopLevelNavItem( selectedIcon = NiaIcons.Bookmarks, unselectedIcon = NiaIcons.BookmarksBorder, iconTextId = bookmarksR.string.feature_bookmarks_api_title, titleTextId = bookmarksR.string.feature_bookmarks_api_title, ) val INTERESTS = TopLevelNavItem( selectedIcon = NiaIcons.Grid3x3, unselectedIcon = NiaIcons.Grid3x3, iconTextId = searchR.string.feature_search_api_interests, titleTextId = searchR.string.feature_search_api_interests, ) val TOP_LEVEL_NAV_ITEMS = mapOf( ForYouNavKey to FOR_YOU, BookmarksNavKey to BOOKMARKS, InterestsNavKey(null) to INTERESTS, ) ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt ================================================ /* * 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.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.ui.NavDisplay import com.google.samples.apps.nowinandroid.R import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaGradientBackground import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationSuiteScaffold import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAppBar import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.navigation.toEntries import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.navigation.bookmarksEntry import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey import com.google.samples.apps.nowinandroid.feature.foryou.impl.navigation.forYouEntry import com.google.samples.apps.nowinandroid.feature.interests.impl.navigation.interestsEntry import com.google.samples.apps.nowinandroid.feature.search.api.navigation.SearchNavKey import com.google.samples.apps.nowinandroid.feature.search.impl.navigation.searchEntry import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsDialog import com.google.samples.apps.nowinandroid.feature.topic.impl.navigation.topicEntry import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import com.google.samples.apps.nowinandroid.feature.settings.impl.R as settingsR @Composable fun NiaApp( appState: NiaAppState, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { val shouldShowGradientBackground = appState.navigationState.currentTopLevelKey == ForYouNavKey var showSettingsDialog by rememberSaveable { mutableStateOf(false) } NiaBackground(modifier = modifier) { NiaGradientBackground( gradientColors = if (shouldShowGradientBackground) { LocalGradientColors.current } else { GradientColors() }, ) { val snackbarHostState = remember { SnackbarHostState() } val isOffline by appState.isOffline.collectAsStateWithLifecycle() // If user is not connected to the internet show a snack bar to inform them. val notConnectedMessage = stringResource(R.string.not_connected) LaunchedEffect(isOffline) { if (isOffline) { snackbarHostState.showSnackbar( message = notConnectedMessage, duration = Indefinite, ) } } CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) { NiaApp( appState = appState, // TODO: Settings should be a dialog screen showSettingsDialog = showSettingsDialog, onSettingsDismissed = { showSettingsDialog = false }, onTopAppBarActionClick = { showSettingsDialog = true }, windowAdaptiveInfo = windowAdaptiveInfo, ) } } } } @Composable @OptIn( ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3AdaptiveApi::class, ) internal fun NiaApp( appState: NiaAppState, showSettingsDialog: Boolean, onSettingsDismissed: () -> Unit, onTopAppBarActionClick: () -> Unit, modifier: Modifier = Modifier, windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), ) { val unreadNavKeys by appState.topLevelNavKeysWithUnreadResources .collectAsStateWithLifecycle() if (showSettingsDialog) { SettingsDialog( onDismiss = { onSettingsDismissed() }, ) } val snackbarHostState = LocalSnackbarHostState.current val navigator = remember { Navigator(appState.navigationState) } NiaNavigationSuiteScaffold( navigationSuiteItems = { TOP_LEVEL_NAV_ITEMS.forEach { (navKey, navItem) -> val hasUnread = unreadNavKeys.contains(navKey) val selected = navKey == appState.navigationState.currentTopLevelKey item( selected = selected, onClick = { navigator.navigate(navKey) }, icon = { Icon( imageVector = navItem.unselectedIcon, contentDescription = null, ) }, selectedIcon = { Icon( imageVector = navItem.selectedIcon, contentDescription = null, ) }, label = { Text(stringResource(navItem.iconTextId)) }, modifier = Modifier .testTag("NiaNavItem") .then(if (hasUnread) Modifier.notificationDot() else Modifier), ) } }, windowAdaptiveInfo = windowAdaptiveInfo, ) { Scaffold( modifier = modifier.semantics { testTagsAsResourceId = true }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onBackground, contentWindowInsets = WindowInsets(0, 0, 0, 0), snackbarHost = { SnackbarHost( snackbarHostState, modifier = Modifier.windowInsetsPadding( WindowInsets.safeDrawing.exclude( WindowInsets.ime, ), ), ) }, ) { padding -> Column( Modifier .fillMaxSize() .padding(padding) .consumeWindowInsets(padding) .windowInsetsPadding( WindowInsets.safeDrawing.only( WindowInsetsSides.Horizontal, ), ), ) { // Only show the top app bar on top level destinations. var shouldShowTopAppBar = false if (appState.navigationState.currentKey in appState.navigationState.topLevelKeys) { shouldShowTopAppBar = true val destination = TOP_LEVEL_NAV_ITEMS[appState.navigationState.currentTopLevelKey] ?: error("Top level nav item not found for ${appState.navigationState.currentTopLevelKey}") NiaTopAppBar( titleRes = destination.titleTextId, navigationIcon = NiaIcons.Search, navigationIconContentDescription = stringResource( id = settingsR.string.feature_settings_impl_top_app_bar_navigation_icon_description, ), actionIcon = NiaIcons.Settings, actionIconContentDescription = stringResource( id = settingsR.string.feature_settings_impl_top_app_bar_action_icon_description, ), colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, ), onActionClick = { onTopAppBarActionClick() }, onNavigationClick = { navigator.navigate(SearchNavKey) }, ) } Box( // Workaround for https://issuetracker.google.com/338478720 modifier = Modifier.consumeWindowInsets( if (shouldShowTopAppBar) { WindowInsets.safeDrawing.only(WindowInsetsSides.Top) } else { WindowInsets(0, 0, 0, 0) }, ), ) { val listDetailStrategy = rememberListDetailSceneStrategy() val entryProvider = entryProvider { forYouEntry(navigator) bookmarksEntry(navigator) interestsEntry(navigator) topicEntry(navigator) searchEntry(navigator) } NavDisplay( entries = appState.navigationState.toEntries(entryProvider), sceneStrategy = listDetailStrategy, onBack = { navigator.goBack() }, ) } // TODO: We may want to add padding or spacer when the snackbar is shown so that // content doesn't display behind it. } } } } private fun Modifier.notificationDot(): Modifier = composed { val tertiaryColor = MaterialTheme.colorScheme.tertiary drawWithContent { drawContent() drawCircle( tertiaryColor, radius = 5.dp.toPx(), // This is based on the dimensions of the NavigationBar's "indicator pill"; // however, its parameters are private, so we must depend on them implicitly // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp) center = center + Offset( 64.dp.toPx() * .45f, 32.dp.toPx() * -.45f - 6.dp.toPx(), ), ) } } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt ================================================ /* * 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.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation3.runtime.NavKey import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository 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.navigation.NavigationState import com.google.samples.apps.nowinandroid.core.navigation.rememberNavigationState import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey import com.google.samples.apps.nowinandroid.navigation.TOP_LEVEL_NAV_ITEMS import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.TimeZone @Composable fun rememberNiaAppState( networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, coroutineScope: CoroutineScope = rememberCoroutineScope(), ): NiaAppState { val navigationState = rememberNavigationState(ForYouNavKey, TOP_LEVEL_NAV_ITEMS.keys) NavigationTrackingSideEffect(navigationState) return remember( navigationState, coroutineScope, networkMonitor, userNewsResourceRepository, timeZoneMonitor, ) { NiaAppState( navigationState = navigationState, coroutineScope = coroutineScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) } } @Stable class NiaAppState( val navigationState: NavigationState, coroutineScope: CoroutineScope, networkMonitor: NetworkMonitor, userNewsResourceRepository: UserNewsResourceRepository, timeZoneMonitor: TimeZoneMonitor, ) { val isOffline = networkMonitor.isOnline .map(Boolean::not) .stateIn( scope = coroutineScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = false, ) /** * The top level nav keys that have unread news resources. */ val topLevelNavKeysWithUnreadResources: StateFlow> = userNewsResourceRepository.observeAllForFollowedTopics() .combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources -> setOfNotNull( ForYouNavKey.takeIf { forYouNewsResources.any { !it.hasBeenViewed } }, BookmarksNavKey.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } }, ) } .stateIn( coroutineScope, SharingStarted.WhileSubscribed(5_000), initialValue = emptySet(), ) val currentTimeZone = timeZoneMonitor.currentTimeZone .stateIn( coroutineScope, SharingStarted.WhileSubscribed(5_000), TimeZone.currentSystemDefault(), ) } /** * Stores information about navigation events to be used with JankStats */ @Composable private fun NavigationTrackingSideEffect(navigationState: NavigationState) { TrackDisposableJank(navigationState.currentKey) { metricsHolder -> metricsHolder.state?.putState("Navigation", navigationState.currentKey.toString()) onDispose {} } } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt ================================================ /* * 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.common.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" }, ) } } ================================================ FILE: app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/UiExtensions.kt ================================================ /* * 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.util import android.content.res.Configuration import androidx.activity.ComponentActivity import androidx.core.util.Consumer import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged /** * Convenience wrapper for dark mode checking */ val Configuration.isSystemInDarkTheme get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES /** * Registers listener for configuration changes to retrieve whether system is in dark theme or not. * Immediately upon subscribing, it sends the current value and then registers listener for changes. */ fun ComponentActivity.isSystemInDarkTheme() = callbackFlow { channel.trySend(resources.configuration.isSystemInDarkTheme) val listener = Consumer { channel.trySend(it.isSystemInDarkTheme) } addOnConfigurationChangedListener(listener) awaitClose { removeOnConfigurationChangedListener(listener) } } .distinctUntilChanged() .conflate() ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_splash.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #000000 #FCFCFC ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Now in Android ⚠️ You aren’t connected to the internet ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ #FCFCFC #000000 ================================================ FILE: app/src/main/res/values-night/themes.xml ================================================ ================================================ FILE: app/src/prod/AndroidManifest.xml ================================================ ================================================ FILE: app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/DeviceConfigurationOverrideWindowInsets.kt ================================================ /* * 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 import android.view.WindowInsets import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowInsetsCompat import androidx.core.view.children /** * A [DeviceConfigurationOverride] that overrides the window insets for the contained content. */ @Suppress("ktlint:standard:function-naming") fun DeviceConfigurationOverride.Companion.WindowInsets( windowInsets: WindowInsetsCompat, ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest -> val currentContentUnderTest by rememberUpdatedState(contentUnderTest) val currentWindowInsets by rememberUpdatedState(windowInsets) AndroidView( factory = { context -> object : AbstractComposeView(context) { @Composable override fun Content() { currentContentUnderTest() } override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { children.forEach { it.dispatchApplyWindowInsets( WindowInsets(currentWindowInsets.toWindowInsets()), ) } return WindowInsetsCompat.CONSUMED.toWindowInsets()!! } /** * Deprecated, but intercept the `requestApplyInsets` call via the deprecated * method. */ @Deprecated("Deprecated in Java") override fun requestFitSystemWindows() { dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!)) } } }, update = { with(currentWindowInsets) { it.requestApplyInsets() } }, ) } ================================================ FILE: app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt ================================================ /* * 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.material3.adaptive.Posture import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.runtime.CompositionLocalProvider 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 androidx.window.core.layout.WindowSizeClass 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.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository 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.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule import org.junit.Test 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 navigation UI is rendered correctly on different screen sizes. */ @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 NiaAppScreenSizesScreenshotTests { /** * Manages the components' state and is used to perform injection on your test */ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) /** * Use a test activity to set the content on. */ @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @Inject lateinit var networkMonitor: NetworkMonitor @Inject lateinit var timeZoneMonitor: TimeZoneMonitor @Inject lateinit var userDataRepository: UserDataRepository @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")) } private fun testNiaAppScreenshotWithSize(width: Dp, height: Dp, screenshotName: String) { composeTestRule.setContent { CompositionLocalProvider( LocalInspectionMode provides true, ) { DeviceConfigurationOverride( override = DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), ) { NiaTheme { val fakeAppState = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) NiaApp( fakeAppState, windowAdaptiveInfo = WindowAdaptiveInfo( windowSizeClass = WindowSizeClass.compute( width.value, height.value, ), windowPosture = Posture(), ), ) } } } } composeTestRule.onRoot() .captureRoboImage( "src/testDemo/screenshots/$screenshotName.png", roborazziOptions = DefaultRoborazziOptions, ) } @Test fun compactWidth_compactHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 400.dp, 400.dp, "compactWidth_compactHeight_showsNavigationBar", ) } @Test fun mediumWidth_compactHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 610.dp, 400.dp, "mediumWidth_compactHeight_showsNavigationBar", ) } @Test fun expandedWidth_compactHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 900.dp, 400.dp, "expandedWidth_compactHeight_showsNavigationBar", ) } @Test fun compactWidth_mediumHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 400.dp, 500.dp, "compactWidth_mediumHeight_showsNavigationBar", ) } @Test fun mediumWidth_mediumHeight_showsNavigationRail() { testNiaAppScreenshotWithSize( 610.dp, 500.dp, "mediumWidth_mediumHeight_showsNavigationRail", ) } @Test fun expandedWidth_mediumHeight_showsNavigationRail() { testNiaAppScreenshotWithSize( 900.dp, 500.dp, "expandedWidth_mediumHeight_showsNavigationRail", ) } @Test fun compactWidth_expandedHeight_showsNavigationBar() { testNiaAppScreenshotWithSize( 400.dp, 1000.dp, "compactWidth_expandedHeight_showsNavigationBar", ) } @Test fun mediumWidth_expandedHeight_showsNavigationRail() { testNiaAppScreenshotWithSize( 610.dp, 1000.dp, "mediumWidth_expandedHeight_showsNavigationRail", ) } @Test fun expandedWidth_expandedHeight_showsNavigationRail() { testNiaAppScreenshotWithSize( 900.dp, 1000.dp, "expandedWidth_expandedHeight_showsNavigationRail", ) } } ================================================ FILE: app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt ================================================ /* * 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.ui import androidx.compose.runtime.remember import androidx.compose.ui.test.junit4.createComposeRule import androidx.navigation3.runtime.NavBackStack import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.navigation.NavigationState import com.google.samples.apps.nowinandroid.core.navigation.Navigator import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor import com.google.samples.apps.nowinandroid.feature.bookmarks.api.navigation.BookmarksNavKey import com.google.samples.apps.nowinandroid.feature.foryou.api.navigation.ForYouNavKey import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltTestApplication import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.datetime.TimeZone import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import kotlin.test.assertEquals /** * Tests [NiaAppState]. */ @RunWith(RobolectricTestRunner::class) @Config(application = HiltTestApplication::class) @HiltAndroidTest class NiaAppStateTest { @get:Rule val composeTestRule = createComposeRule() // Create the test dependencies. private val networkMonitor = TestNetworkMonitor() private val timeZoneMonitor = TestTimeZoneMonitor() private val userNewsResourceRepository = CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository()) // Subject under test. private lateinit var state: NiaAppState private fun testNavigationState() = NavigationState( startKey = ForYouNavKey, topLevelStack = NavBackStack(ForYouNavKey), subStacks = mapOf( ForYouNavKey to NavBackStack(ForYouNavKey), BookmarksNavKey to NavBackStack(BookmarksNavKey), ), ) @Test fun niaAppState_currentDestination() = runTest { val navigationState = testNavigationState() val navigator = Navigator(navigationState) composeTestRule.setContent { state = remember(navigationState) { NiaAppState( coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, navigationState = navigationState, ) } } assertEquals(ForYouNavKey, state.navigationState.currentTopLevelKey) assertEquals(ForYouNavKey, state.navigationState.currentKey) // Navigate to another destination once navigator.navigate(BookmarksNavKey) composeTestRule.waitForIdle() assertEquals(BookmarksNavKey, state.navigationState.currentTopLevelKey) assertEquals(BookmarksNavKey, state.navigationState.currentKey) } @Test fun niaAppState_destinations() = runTest { composeTestRule.setContent { state = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) } val navigationState = state.navigationState assertEquals(3, navigationState.topLevelKeys.size) assertEquals( setOf(ForYouNavKey, BookmarksNavKey, InterestsNavKey(null)), navigationState.topLevelKeys, ) } @Test fun niaAppState_whenNetworkMonitorIsOffline_StateIsOffline() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, navigationState = testNavigationState(), ) } backgroundScope.launch { state.isOffline.collect() } networkMonitor.setConnected(false) assertEquals( true, state.isOffline.value, ) } @Test fun niaAppState_differentTZ_withTimeZoneMonitorChange() = runTest(UnconfinedTestDispatcher()) { composeTestRule.setContent { state = NiaAppState( coroutineScope = backgroundScope, networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, navigationState = testNavigationState(), ) } val changedTz = TimeZone.of("Europe/Prague") backgroundScope.launch { state.currentTimeZone.collect() } timeZoneMonitor.setTimeZone(changedTz) assertEquals( changedTz, state.currentTimeZone.value, ) } } ================================================ FILE: app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt ================================================ /* * 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.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsEndWidth import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsStartWidth import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.adaptive.Posture import androidx.compose.material3.adaptive.WindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.DeviceConfigurationOverride import androidx.compose.ui.test.ForcedSize import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import androidx.core.graphics.Insets import androidx.core.view.WindowInsetsCompat import androidx.window.core.layout.WindowSizeClass 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.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity 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.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. */ @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 SnackbarInsetsScreenshotTests { /** * Manages the components' state and is used to perform injection on your test */ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) /** * Use a test activity to set the content on. */ @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @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() { testSnackbarScreenshotWithSize( 400.dp, 500.dp, "insets_snackbar_compact_medium_noSnackbar", action = { }, ) } @Test fun snackbarShown_phone() { testSnackbarScreenshotWithSize( 400.dp, 500.dp, "insets_snackbar_compact_medium", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } @Test fun snackbarShown_foldable() { testSnackbarScreenshotWithSize( 600.dp, 600.dp, "insets_snackbar_medium_medium", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } @Test fun snackbarShown_tablet() { testSnackbarScreenshotWithSize( 900.dp, 900.dp, "insets_snackbar_expanded_expanded", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } private fun testSnackbarScreenshotWithSize( width: Dp, height: Dp, screenshotName: String, action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, LocalSnackbarHostState provides snackbarHostState, ) { scope = rememberCoroutineScope() DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), ) { DeviceConfigurationOverride( DeviceConfigurationOverride.WindowInsets( WindowInsetsCompat.Builder() .setInsets( WindowInsetsCompat.Type.statusBars(), DpRect( left = 0.dp, top = 64.dp, right = 0.dp, bottom = 0.dp, ).toInsets(), ) .setInsets( WindowInsetsCompat.Type.navigationBars(), DpRect( left = 64.dp, top = 0.dp, right = 64.dp, bottom = 64.dp, ).toInsets(), ) .build(), ), ) { BoxWithConstraints(Modifier.testTag("root")) { NiaTheme { val appState = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) NiaApp( appState = appState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, windowAdaptiveInfo = WindowAdaptiveInfo( windowSizeClass = WindowSizeClass.compute( maxWidth.value, maxHeight.value, ), windowPosture = Posture(), ), ) DebugVisibleWindowInsets() } } } } } } scope.launch { action(snackbarHostState) } composeTestRule.onNodeWithTag("root") .captureRoboImage( "src/testDemo/screenshots/$screenshotName.png", roborazziOptions = DefaultRoborazziOptions, ) } } @Composable fun DebugVisibleWindowInsets( modifier: Modifier = Modifier, debugColor: Color = Color.Magenta.copy(alpha = 0.5f), ) { Box(modifier = modifier.fillMaxSize()) { Spacer( modifier = Modifier .align(Alignment.CenterStart) .fillMaxHeight() .windowInsetsStartWidth(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)) .background(debugColor), ) Spacer( modifier = Modifier .align(Alignment.CenterEnd) .fillMaxHeight() .windowInsetsEndWidth(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)) .background(debugColor), ) Spacer( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .windowInsetsTopHeight(WindowInsets.safeDrawing) .background(debugColor), ) Spacer( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .windowInsetsBottomHeight(WindowInsets.safeDrawing) .background(debugColor), ) } } @Composable private fun DpRect.toInsets() = toInsets(LocalDensity.current) private fun DpRect.toInsets(density: Density) = Insets.of(with(density) { toRect() }.roundToIntRect().toAndroidRect()) ================================================ FILE: app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt ================================================ /* * 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.adaptive.Posture import androidx.compose.material3.adaptive.WindowAdaptiveInfo 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 androidx.window.core.layout.WindowSizeClass 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.feature.bookmarks.impl.navigation.LocalSnackbarHostState import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity 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.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. */ @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) /** * Use a test activity to set the content on. */ @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @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() { testSnackbarScreenshotWithSize( 400.dp, 500.dp, "snackbar_compact_medium_noSnackbar", action = { }, ) } @Test fun snackbarShown_phone() { testSnackbarScreenshotWithSize( 400.dp, 500.dp, "snackbar_compact_medium", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } @Test fun snackbarShown_foldable() { testSnackbarScreenshotWithSize( 600.dp, 600.dp, "snackbar_medium_medium", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } @Test fun snackbarShown_tablet() { testSnackbarScreenshotWithSize( 900.dp, 900.dp, "snackbar_expanded_expanded", ) { snackbarHostState -> snackbarHostState.showSnackbar( "This is a test snackbar message", actionLabel = "Action Label", duration = Indefinite, ) } } private fun testSnackbarScreenshotWithSize( width: Dp, height: Dp, screenshotName: String, action: suspend (snackbarHostState: SnackbarHostState) -> Unit, ) { lateinit var scope: CoroutineScope val snackbarHostState = SnackbarHostState() composeTestRule.setContent { CompositionLocalProvider( // Replaces images with placeholders LocalInspectionMode provides true, LocalSnackbarHostState provides snackbarHostState, ) { scope = rememberCoroutineScope() DeviceConfigurationOverride( DeviceConfigurationOverride.ForcedSize(DpSize(width, height)), ) { BoxWithConstraints { NiaTheme { val appState = rememberNiaAppState( networkMonitor = networkMonitor, userNewsResourceRepository = userNewsResourceRepository, timeZoneMonitor = timeZoneMonitor, ) NiaApp( appState = appState, showSettingsDialog = false, onSettingsDismissed = {}, onTopAppBarActionClick = {}, windowAdaptiveInfo = WindowAdaptiveInfo( windowSizeClass = WindowSizeClass.compute( maxWidth.value, maxHeight.value, ), windowPosture = Posture(), ), ) } } } } } scope.launch { action(snackbarHostState) } composeTestRule.onRoot() .captureRoboImage( "src/testDemo/screenshots/$screenshotName.png", roborazziOptions = DefaultRoborazziOptions, ) } } ================================================ FILE: app/src/testDemo/resources/robolectric.properties ================================================ # # Copyright 2025 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. # sdk = 35 ================================================ FILE: app-nia-catalog/.gitignore ================================================ /build ================================================ FILE: app-nia-catalog/README.md ================================================ # `:app-nia-catalog` ## Module dependency graph ```mermaid --- config: layout: elk elk: nodePlacementStrategy: SIMPLE --- graph TB subgraph :core direction TB :core:analytics[analytics]:::android-library :core:designsystem[designsystem]:::android-library :core:model[model]:::jvm-library :core:ui[ui]:::android-library end :app-nia-catalog[app-nia-catalog]:::android-application :app-nia-catalog -.-> :core:designsystem :app-nia-catalog -.-> :core:ui :core:ui --> :core:analytics :core:ui --> :core:designsystem :core:ui --> :core:model classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; ```
📋 Graph legend ```mermaid graph TB application[application]:::android-application feature[feature]:::android-feature library[library]:::android-library jvm[jvm]:::jvm-library application -.-> feature library --> jvm classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; ```
================================================ FILE: app-nia-catalog/build.gradle.kts ================================================ /* * 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.samples.apps.nowinandroid.FlavorDimension import com.google.samples.apps.nowinandroid.NiaFlavor /* * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ plugins { alias(libs.plugins.nowinandroid.android.application) alias(libs.plugins.nowinandroid.android.application.compose) } android { defaultConfig { applicationId = "com.google.samples.apps.niacatalog" versionCode = 1 versionName = "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level // The UI catalog does not depend on content from the app, however, it depends on modules // which do, so we must specify a default value for the contentType dimension. missingDimensionStrategy(FlavorDimension.contentType.name, NiaFlavor.demo.name) } packaging { resources { excludes.add("/META-INF/{AL2.0,LGPL2.1}") } } namespace = "com.google.samples.apps.niacatalog" buildTypes { release { // To publish on the Play store a private signing key is required, but to allow anyone // who clones the code to sign and run the release variant, use the debug signing key. // TODO: Abstract the signing configuration to a separate file to avoid hardcoding this. signingConfig = signingConfigs.named("debug").get() } } } dependencies { implementation(libs.androidx.activity.compose) implementation(projects.core.designsystem) implementation(projects.core.ui) } dependencyGuard { configuration("releaseRuntimeClasspath") } ================================================ FILE: app-nia-catalog/dependencies/releaseRuntimeClasspath.txt ================================================ androidx.activity:activity-compose:1.9.3 androidx.activity:activity-ktx:1.9.3 androidx.activity:activity:1.9.3 androidx.annotation:annotation-experimental:1.5.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.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.8.0 androidx.collection:collection-jvm:1.5.0 androidx.collection:collection-ktx:1.5.0 androidx.collection:collection:1.5.0 androidx.compose.animation:animation-android:1.10.0-alpha04 androidx.compose.animation:animation-core-android:1.10.0-alpha04 androidx.compose.animation:animation-core:1.10.0-alpha04 androidx.compose.animation:animation:1.10.0-alpha04 androidx.compose.foundation:foundation-android:1.10.0-alpha04 androidx.compose.foundation:foundation-layout-android:1.10.0-alpha04 androidx.compose.foundation:foundation-layout:1.10.0-alpha04 androidx.compose.foundation:foundation:1.10.0-alpha04 androidx.compose.material3.adaptive:adaptive-android:1.2.0-beta03 androidx.compose.material3.adaptive:adaptive:1.2.0-beta03 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.5.0-alpha04 androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-alpha04 androidx.compose.material3:material3-android:1.5.0-alpha04 androidx.compose.material3:material3:1.5.0-alpha04 androidx.compose.material:material-icons-core-android:1.7.8 androidx.compose.material:material-icons-core:1.7.8 androidx.compose.material:material-icons-extended-android:1.7.8 androidx.compose.material:material-icons-extended:1.7.8 androidx.compose.material:material-ripple-android:1.10.0-alpha04 androidx.compose.material:material-ripple:1.10.0-alpha04 androidx.compose.runtime:runtime-android:1.10.0-alpha04 androidx.compose.runtime:runtime-annotation-android:1.10.0-alpha04 androidx.compose.runtime:runtime-annotation:1.10.0-alpha04 androidx.compose.runtime:runtime-retain-android:1.10.0-alpha04 androidx.compose.runtime:runtime-retain:1.10.0-alpha04 androidx.compose.runtime:runtime-saveable-android:1.10.0-alpha04 androidx.compose.runtime:runtime-saveable:1.10.0-alpha04 androidx.compose.runtime:runtime:1.10.0-alpha04 androidx.compose.ui:ui-android:1.10.0-alpha04 androidx.compose.ui:ui-geometry-android:1.10.0-alpha04 androidx.compose.ui:ui-geometry:1.10.0-alpha04 androidx.compose.ui:ui-graphics-android:1.10.0-alpha04 androidx.compose.ui:ui-graphics:1.10.0-alpha04 androidx.compose.ui:ui-text-android:1.10.0-alpha04 androidx.compose.ui:ui-text:1.10.0-alpha04 androidx.compose.ui:ui-tooling-preview-android:1.10.0-alpha04 androidx.compose.ui:ui-tooling-preview:1.10.0-alpha04 androidx.compose.ui:ui-unit-android:1.10.0-alpha04 androidx.compose.ui:ui-unit:1.10.0-alpha04 androidx.compose.ui:ui-util-android:1.10.0-alpha04 androidx.compose.ui:ui-util:1.10.0-alpha04 androidx.compose.ui:ui:1.10.0-alpha04 androidx.compose:compose-bom-alpha:2025.09.01 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.16.0 androidx.core:core-viewtree:1.0.0 androidx.core:core:1.16.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 androidx.documentfile:documentfile:1.0.0 androidx.dynamicanimation:dynamicanimation:1.0.0 androidx.emoji2:emoji2:1.4.0 androidx.exifinterface:exifinterface:1.3.7 androidx.fragment:fragment:1.5.1 androidx.graphics:graphics-path:1.0.1 androidx.graphics:graphics-shapes-android:1.0.1 androidx.graphics:graphics-shapes:1.0.1 androidx.interpolator:interpolator:1.0.0 androidx.legacy:legacy-support-core-utils:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.9.4 androidx.lifecycle:lifecycle-common-jvm:2.9.4 androidx.lifecycle:lifecycle-common:2.9.4 androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.4 androidx.lifecycle:lifecycle-livedata-core:2.9.4 androidx.lifecycle:lifecycle-livedata:2.9.4 androidx.lifecycle:lifecycle-process:2.9.4 androidx.lifecycle:lifecycle-runtime-android:2.9.4 androidx.lifecycle:lifecycle-runtime-compose-android:2.9.4 androidx.lifecycle:lifecycle-runtime-compose:2.9.4 androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.4 androidx.lifecycle:lifecycle-runtime-ktx:2.9.4 androidx.lifecycle:lifecycle-runtime:2.9.4 androidx.lifecycle:lifecycle-viewmodel-android:2.9.4 androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4 androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.4 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4 androidx.lifecycle:lifecycle-viewmodel:2.9.4 androidx.loader:loader:1.0.0 androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.print:print:1.0.0 androidx.profileinstaller:profileinstaller:1.4.0 androidx.savedstate:savedstate-android:1.3.2 androidx.savedstate:savedstate-compose-android:1.3.2 androidx.savedstate:savedstate-compose:1.3.2 androidx.savedstate:savedstate-ktx:1.3.2 androidx.savedstate:savedstate:1.3.2 androidx.startup:startup-runtime:1.1.1 androidx.tracing:tracing-ktx:1.3.0-alpha02 androidx.tracing:tracing:1.3.0-alpha02 androidx.transition:transition:1.6.0 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 androidx.window:window-core-android:1.4.0 androidx.window:window-core:1.4.0 androidx.window:window:1.4.0 com.google.accompanist:accompanist-drawablepainter:0.32.0 com.google.code.findbugs:jsr305:3.0.2 com.google.dagger:dagger-lint-aar:2.59 com.google.dagger:dagger:2.59 com.google.dagger:hilt-android:2.59 com.google.dagger:hilt-core:2.59 com.google.guava:listenablefuture:1.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.9.0 com.squareup.okio:okio:3.9.0 io.coil-kt:coil-base:2.7.0 io.coil-kt:coil-compose-base:2.7.0 io.coil-kt:coil-compose:2.7.0 io.coil-kt:coil:2.7.0 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 org.jetbrains.kotlin:kotlin-stdlib-common:2.3.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 org.jetbrains.kotlin:kotlin-stdlib:2.3.0 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3 org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3 org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3 org.jetbrains:annotations:23.0.0 org.jspecify:jspecify:1.0.0 ================================================ FILE: app-nia-catalog/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app-nia-catalog/src/main/kotlin/com/google/samples/apps/niacatalog/NiaCatalogActivity.kt ================================================ /* * 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.niacatalog import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.view.WindowCompat import com.google.samples.apps.niacatalog.ui.NiaCatalog class NiaCatalogActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { NiaCatalog() } } } ================================================ FILE: app-nia-catalog/src/main/kotlin/com/google/samples/apps/niacatalog/ui/Catalog.kt ================================================ /* * 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.niacatalog.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton 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.NiaNavigationBar import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOutlinedButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTab import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRow import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaViewToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme /** * Now in Android component catalog. */ @OptIn(ExperimentalLayoutApi::class) @Composable fun NiaCatalog() { NiaTheme { Surface { val contentPadding = WindowInsets .systemBars .add(WindowInsets(left = 16.dp, top = 16.dp, right = 16.dp, bottom = 16.dp)) .asPaddingValues() LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = contentPadding, verticalArrangement = Arrangement.spacedBy(16.dp), ) { item { Text( text = "NiA Catalog", style = MaterialTheme.typography.headlineSmall, ) } item { Text("Buttons", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { NiaButton(onClick = {}) { Text(text = "Enabled") } NiaOutlinedButton(onClick = {}) { Text(text = "Enabled") } NiaTextButton(onClick = {}) { Text(text = "Enabled") } } } item { Text("Disabled buttons", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { NiaButton( onClick = {}, enabled = false, ) { Text(text = "Disabled") } NiaOutlinedButton( onClick = {}, enabled = false, ) { Text(text = "Disabled") } NiaTextButton( onClick = {}, enabled = false, ) { Text(text = "Disabled") } } } item { Text("Buttons with leading icons", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { NiaButton( onClick = {}, text = { Text(text = "Enabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) NiaOutlinedButton( onClick = {}, text = { Text(text = "Enabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) NiaTextButton( onClick = {}, text = { Text(text = "Enabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) } } item { Text("Disabled buttons with leading icons", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { NiaButton( onClick = {}, enabled = false, text = { Text(text = "Disabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) NiaOutlinedButton( onClick = {}, enabled = false, text = { Text(text = "Disabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) NiaTextButton( onClick = {}, enabled = false, text = { Text(text = "Disabled") }, leadingIcon = { Icon(imageVector = NiaIcons.Add, contentDescription = null) }, ) } } item { Text("Dropdown menus", Modifier.padding(top = 16.dp)) } item { Text("Chips", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { var firstChecked by rememberSaveable { mutableStateOf(false) } NiaFilterChip( selected = firstChecked, onSelectedChange = { checked -> firstChecked = checked }, label = { Text(text = "Enabled") }, ) var secondChecked by rememberSaveable { mutableStateOf(true) } NiaFilterChip( selected = secondChecked, onSelectedChange = { checked -> secondChecked = checked }, label = { Text(text = "Enabled") }, ) NiaFilterChip( selected = false, onSelectedChange = {}, enabled = false, label = { Text(text = "Disabled") }, ) NiaFilterChip( selected = true, onSelectedChange = {}, enabled = false, label = { Text(text = "Disabled") }, ) } } item { Text("Icon buttons", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { var firstChecked by rememberSaveable { mutableStateOf(false) } NiaIconToggleButton( checked = firstChecked, onCheckedChange = { checked -> firstChecked = checked }, icon = { Icon( imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, ) var secondChecked by rememberSaveable { mutableStateOf(true) } NiaIconToggleButton( checked = secondChecked, onCheckedChange = { checked -> secondChecked = checked }, icon = { Icon( imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, ) NiaIconToggleButton( checked = false, onCheckedChange = {}, icon = { Icon( imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, enabled = false, ) NiaIconToggleButton( checked = true, onCheckedChange = {}, icon = { Icon( imageVector = NiaIcons.BookmarkBorder, contentDescription = null, ) }, checkedIcon = { Icon( imageVector = NiaIcons.Bookmark, contentDescription = null, ) }, enabled = false, ) } } item { Text("View toggle", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { var firstExpanded by rememberSaveable { mutableStateOf(false) } NiaViewToggleButton( expanded = firstExpanded, onExpandedChange = { expanded -> firstExpanded = expanded }, compactText = { Text(text = "Compact view") }, expandedText = { Text(text = "Expanded view") }, ) var secondExpanded by rememberSaveable { mutableStateOf(true) } NiaViewToggleButton( expanded = secondExpanded, onExpandedChange = { expanded -> secondExpanded = expanded }, compactText = { Text(text = "Compact view") }, expandedText = { Text(text = "Expanded view") }, ) NiaViewToggleButton( expanded = false, onExpandedChange = {}, compactText = { Text(text = "Disabled") }, expandedText = { Text(text = "Disabled") }, enabled = false, ) } } item { Text("Tags", Modifier.padding(top = 16.dp)) } item { FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { NiaTopicTag( followed = true, onClick = {}, text = { Text(text = "Topic 1".uppercase()) }, ) NiaTopicTag( followed = false, onClick = {}, text = { Text(text = "Topic 2".uppercase()) }, ) NiaTopicTag( followed = false, onClick = {}, text = { Text(text = "Disabled".uppercase()) }, enabled = false, ) } } item { Text("Tabs", Modifier.padding(top = 16.dp)) } item { var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } val titles = listOf("Topics", "People") NiaTabRow(selectedTabIndex = selectedTabIndex) { titles.forEachIndexed { index, title -> NiaTab( selected = selectedTabIndex == index, onClick = { selectedTabIndex = index }, text = { Text(text = title) }, ) } } } item { Text("Navigation", Modifier.padding(top = 16.dp)) } item { var selectedItem by rememberSaveable { mutableIntStateOf(0) } val items = listOf("For you", "Saved", "Interests") val icons = listOf( NiaIcons.UpcomingBorder, NiaIcons.BookmarksBorder, NiaIcons.Grid3x3, ) val selectedIcons = listOf( NiaIcons.Upcoming, NiaIcons.Bookmarks, NiaIcons.Grid3x3, ) NiaNavigationBar { items.forEachIndexed { index, item -> NiaNavigationBarItem( icon = { Icon( imageVector = icons[index], contentDescription = item, ) }, selectedIcon = { Icon( imageVector = selectedIcons[index], contentDescription = item, ) }, label = { Text(item) }, selected = selectedItem == index, onClick = { selectedItem = index }, ) } } } } } } } ================================================ FILE: app-nia-catalog/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app-nia-catalog/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app-nia-catalog/src/main/res/values/strings.xml ================================================ NiA Catalog ================================================ FILE: app-nia-catalog/src/main/res/values/themes.xml ================================================