Showing preview only (2,058K chars total). Download the full file or copy to clipboard to get everything.
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**
\<add your PR description here\>
================================================
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
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="5" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="3" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value>
<package name="java.awt" withSubpackages="false" static="false" />
<package name="javax.swing" withSubpackages="false" static="false" />
</value>
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
<option name="JD_P_AT_EMPTY_LINES" value="false" />
<option name="JD_DO_NOT_WRAP_ONE_LINE_COMMENTS" value="true" />
<option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
<option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
<option name="JD_KEEP_EMPTY_RETURN" value="false" />
<option name="JD_PRESERVE_LINE_FEEDS" value="true" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
<option name="IMPORT_NESTED_CLASSES" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Properties>
<option name="KEEP_BLANK_LINES" value="true" />
</Properties>
<XML>
<option name="XML_ATTRIBUTE_WRAP" value="2" />
</XML>
<ADDITIONAL_INDENT_OPTIONS fileType="java">
<option name="TAB_SIZE" value="8" />
</ADDITIONAL_INDENT_OPTIONS>
<ADDITIONAL_INDENT_OPTIONS fileType="js">
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</ADDITIONAL_INDENT_OPTIONS>
<codeStyleSettings language="JAVA">
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="PREFER_PARAMETERS_WRAP" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="RESOURCE_LIST_WRAP" value="1" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="THROWS_LIST_WRAP" value="1" />
<option name="EXTENDS_KEYWORD_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="WRAP_LONG_LINES" value="true" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:viewportWidth</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:viewportHeight</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="FIELD_ANNOTATION_WRAP" value="1" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="5" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
================================================
FILE: .idea/copyright/The_Android_Open_Source_Project.xml
================================================
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &#36;today.year 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." />
<option name="myName" value="The Android Open Source Project" />
</copyright>
</component>
================================================
FILE: .idea/copyright/profiles_settings.xml
================================================
<component name="CopyrightManager">
<settings default="The Android Open Source Project" />
</component>
================================================
FILE: .run/Generate Demo Baseline Profile.run.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<component name="ProjectRunConfigurationManager">
<!--
Baseline Profiles improve code execution speed by around 30% from the first launch by avoiding
interpretation and just-in-time (JIT) compilation steps for included code paths.
More information at http://d.android.com/baseline-profiles.
In this run configuration we leverage rerun parameter that always reruns the requested task regardless of cache.
We also leverage enable-display parameter to be able to verify the generator works as intended.
-->
<configuration default="false" name="Generate Demo Baseline Profile" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":benchmark:pixel6Api31atdDemoBenchmarkAndroidTest" />
<option value="--rerun" />
<option value="--enable-display" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>
================================================
FILE: .run/spotlessApply.run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="spotlessApply" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="spotlessApply" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>
================================================
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 <https://opensource.google/conduct/>.*
[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
================================================

<a href="https://play.google.com/store/apps/details?id=com.google.samples.apps.nowinandroid"><img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="70"></a>
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

# 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`.

The `demoDebug` and `demoRelease` build variants can be built and run (the `prod` variants use a backend server which is not currently publicly available).

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
<!--region 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;
```
<details><summary>📋 Graph legend</summary>
```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;
```
</details>
<!--endregion-->
================================================
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<MainActivity>()
@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<Any, String> =
ReadOnlyProperty { _, _ -> activity.getString(resId) }
================================================
FILE: app/src/benchmark/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FF006780</color>
</resources>
================================================
FILE: app/src/benchmark/res/values-night/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FF006780</color>
</resources>
================================================
FILE: app/src/debug/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources>
================================================
FILE: app/src/debug/res/values-night/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Allow users to distinguish between build variants by having a different background color
for the launcher icon. See https://github.com/android/nowinandroid/pull/989. -->
<color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources>
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<!--
Firebase automatically adds these AD_ID and ADSERVICES permissions, even though we don't use them.
If you use these permissions you must declare how you're using them to Google Play, otherwise the
app will be rejected when publishing it. To avoid this we remove the permissions entirely.
-->
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" tools:node="remove"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" tools:node="remove"/>
<application
android:name=".NiaApplication"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nia.Splash">
<profileable android:shell="true" tools:targetApi="q" />
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="www.nowinandroid.apps.samples.google.com" />
</intent-filter>
</activity>
<!-- Disable Firebase analytics by default. This setting is overwritten for the `prod`
flavor -->
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<!-- Disable collection of AD_ID for all build variants -->
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<!-- Firebase automatically adds the following property which we don't use so remove it -->
<property
android:name="android.adservices.AD_SERVICES_CONFIG"
tools:node="remove" />
</application>
</manifest>
================================================
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<JankStats>
@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<MainActivityUiState> = 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<ImageLoader>
@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<NavKey>()
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<Set<NavKey>> =
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<Configuration> {
channel.trySend(it.isSystemInDarkTheme)
}
addOnConfigurationChangedListener(listener)
awaitClose { removeOnConfigurationChangedListener(listener) }
}
.distinctUntilChanged()
.conflate()
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="@color/ic_launcher_background_tint"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M65.08,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM43.6,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM65.77,72.44 L69.66,65.73a0.81,0.81 0,0 0,-0.3 -1.1,0.82 0.82,0 0,0 -1.11,0.3l-3.93,6.8a24,24 0,0 0,-9.99 -2.14c-3.6,0 -6.98,0.77 -9.99,2.14l-3.93,-6.8a0.8,0.8 0,1 0,-1.4 0.8l3.88,6.71A22.91,22.91 0,0 0,31 90.77h46.67a22.9,22.9 0,0 0,-11.9 -18.33Z"
android:fillColor="@color/ic_launcher_foreground_tint"/>
<path
android:pathData="M46.57,35a0.85,0.85 0,0 0,-0.85 0.85v7.3h-1.53a1.52,1.52 0,0 0,0 3.05h1.53v-3.05h1.7c0.75,0 1.36,-0.61 1.36,-1.36v-4.07h1.19c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.85h-3.4ZM46.57,54.35h3.4c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.84h-1.19v-4.08c0,-0.75 -0.61,-1.36 -1.36,-1.36h-1.7v7.3c0,0.47 0.38,0.85 0.85,0.85ZM61.54,35c0.47,0 0.85,0.38 0.85,0.85v7.3h1.53a1.52,1.52 0,0 1,0 3.05h-1.53v-3.05h-1.7c-0.75,0 -1.36,-0.61 -1.36,-1.36v-4.07h-1.18a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.47 0.38,-0.85 0.85,-0.85h3.39ZM61.54,54.35h-3.39a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.46 0.38,-0.84 0.85,-0.84h1.18v-4.08c0,-0.75 0.61,-1.36 1.36,-1.36h1.7v7.3c0,0.47 -0.38,0.85 -0.85,0.85Z"
android:fillColor="@color/ic_launcher_foreground_tint"
android:fillType="evenOdd"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_splash.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M0,0h108v108h-108z"
android:fillColor="@color/ic_launcher_background_tint"/>
<path
android:pathData="M65.08,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM43.6,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM65.77,72.44 L69.66,65.73a0.81,0.81 0,0 0,-0.3 -1.1,0.82 0.82,0 0,0 -1.11,0.3l-3.93,6.8a24,24 0,0 0,-9.99 -2.14c-3.6,0 -6.98,0.77 -9.99,2.14l-3.93,-6.8a0.8,0.8 0,1 0,-1.4 0.8l3.88,6.71A22.91,22.91 0,0 0,31 90.77h46.67a22.9,22.9 0,0 0,-11.9 -18.33Z"
android:fillColor="@color/ic_launcher_foreground_tint"/>
<path
android:pathData="M46.57,35a0.85,0.85 0,0 0,-0.85 0.85v7.3h-1.53a1.52,1.52 0,0 0,0 3.05h1.53v-3.05h1.7c0.75,0 1.36,-0.61 1.36,-1.36v-4.07h1.19c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.85h-3.4ZM46.57,54.35h3.4c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.84h-1.19v-4.08c0,-0.75 -0.61,-1.36 -1.36,-1.36h-1.7v7.3c0,0.47 0.38,0.85 0.85,0.85ZM61.54,35c0.47,0 0.85,0.38 0.85,0.85v7.3h1.53a1.52,1.52 0,0 1,0 3.05h-1.53v-3.05h-1.7c-0.75,0 -1.36,-0.61 -1.36,-1.36v-4.07h-1.18a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.47 0.38,-0.85 0.85,-0.85h3.39ZM61.54,54.35h-3.39a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.46 0.38,-0.84 0.85,-0.84h1.18v-4.08c0,-0.75 0.61,-1.36 1.36,-1.36h1.7v7.3c0,0.47 -0.38,0.85 -0.85,0.85Z"
android:fillColor="@color/ic_launcher_foreground_tint"
android:fillType="evenOdd"/>
</vector>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FCFCFC</color>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">Now in Android</string>
<string name="not_connected">⚠️ You aren’t connected to the internet</string>
</resources>
================================================
FILE: app/src/main/res/values/themes.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Allows us to override night specific attributes in the
values-night folder. -->
<style name="NightAdjusted.Theme.Nia" parent="android:Theme.Material.Light.NoActionBar" />
<!-- The final theme we use -->
<style name="Theme.Nia" parent="NightAdjusted.Theme.Nia">
<item name="android:forceDarkAllowed" tools:targetApi="29">false</item>
</style>
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar" tools:targetApi="23">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
</style>
<style name="Theme.Nia.Splash" parent="NightAdjusted.Theme.Splash">
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash</item>
<item name="postSplashScreenTheme">@style/Theme.Nia</item>
</style>
</resources>
================================================
FILE: app/src/main/res/values-night/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#FCFCFC</color>
<color name="ic_launcher_foreground_tint">#000000</color>
</resources>
================================================
FILE: app/src/main/res/values-night/themes.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="NightAdjusted.Theme.Nia" parent="android:Theme.Material.NoActionBar" />
<style name="NightAdjusted.Theme.Splash" parent="Theme.SplashScreen">
<item name="android:windowLightStatusBar" tools:targetApi="23">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
</style>
</resources>
================================================
FILE: app/src/prod/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<!-- Enable Firebase analytics for `prod` builds -->
<meta-data
tools:replace="android:value"
android:name="firebase_analytics_collection_deactivated"
android:value="false" />
</application>
</manifest>
================================================
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<HiltComponentActivity>()
@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_sho
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
Condensed preview — 554 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,031K chars).
[
{
"path": ".editorconfig",
"chars": 747,
"preview": "# https://editorconfig.org/\n# This configuration is used by ktlint when spotless invokes it\n\n[*.{kt,kts}]\nij_kotlin_allo"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1650,
"preview": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage me\"]\nbody:\n - type: markdown\n "
},
{
"path": ".github/ISSUE_TEMPLATE/docs_issue.yml",
"chars": 1258,
"preview": "name: Documentation issue\ndescription: File an issue or make a suggestion for the project documentation\ntitle: \"[Documen"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1511,
"preview": "name: Feature request\ndescription: File a feature request\ntitle: \"[FR]: \"\nlabels: [\"enhancement\", \"triage me\"]\nbody:\n -"
},
{
"path": ".github/ci-gradle.properties",
"chars": 1028,
"preview": "#\n# Copyright 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
},
{
"path": ".github/pull_request_template.md",
"chars": 902,
"preview": "**DO NOT CREATE A PULL REQUEST WITHOUT READING THESE INSTRUCTIONS**\n\n## Instructions\nThanks for submitting a pull reques"
},
{
"path": ".github/renovate.json",
"chars": 357,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"local>android/.github:renovate-c"
},
{
"path": ".github/workflows/Build.yaml",
"chars": 10241,
"preview": "name: Build\n\non:\n workflow_dispatch:\n push:\n branches:\n - main\n pull_request:\n\nconcurrency:\n group: build-${"
},
{
"path": ".github/workflows/NightlyBaselineProfiles.yaml",
"chars": 2188,
"preview": "name: NightlyBaselineProfiles\n\non:\n workflow_dispatch:\n schedule:\n - cron: '42 4 * * *'\n\njobs:\n baseline_profiles"
},
{
"path": ".github/workflows/Release.yml",
"chars": 2877,
"preview": "name: GitHub Release with APKs\n\non:\n workflow_dispatch:\n push:\n tags:\n - 'v*'\n\njobs:\n build:\n if: github.rep"
},
{
"path": ".gitignore",
"chars": 665,
"preview": "# built application files\n*.apk\n*.ap_\n\n# files for the dex VM\n*.dex\n\n# Java class files\n*.class\n\n# generated files\nbin/\n"
},
{
"path": ".google/BUILDME",
"chars": 84,
"preview": "# This file can be used to trigger an internal build by changing the number below\n2\n"
},
{
"path": ".google/packaging.yaml",
"chars": 1519,
"preview": "# Copyright (C) 2022 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");"
},
{
"path": ".idea/codeStyles/Project.xml",
"chars": 11145,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <JavaCodeStyleSettings"
},
{
"path": ".idea/codeStyles/codeStyleConfig.xml",
"chars": 142,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
},
{
"path": ".idea/copyright/The_Android_Open_Source_Project.xml",
"chars": 840,
"preview": "<component name=\"CopyrightManager\">\n <copyright>\n <option name=\"notice\" value=\"Copyright &#36;today.year The And"
},
{
"path": ".idea/copyright/profiles_settings.xml",
"chars": 105,
"preview": "<component name=\"CopyrightManager\">\n <settings default=\"The Android Open Source Project\" />\n</component>"
},
{
"path": ".run/Generate Demo Baseline Profile.run.xml",
"chars": 2285,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": ".run/spotlessApply.run.xml",
"chars": 866,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"spotlessApply\" type=\"GradleRunC"
},
{
"path": "AGENTS.md",
"chars": 3021,
"preview": "# Now in Android Project\n\nNow in Android is a native Android mobile application written in Kotlin. It provides regular n"
},
{
"path": "CODEOWNERS",
"chars": 11,
"preview": "* @dturner\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3167,
"preview": "# Google Open Source Community Guidelines\n\nAt Google, we recognize and celebrate the creativity and collaboration of ope"
},
{
"path": "CONTRIBUTING.md",
"chars": 1629,
"preview": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your samp"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 10995,
"preview": "\n\n<a href=\"https://play.google.com/store/apps/details?id=c"
},
{
"path": "app/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app/README.md",
"chars": 6132,
"preview": "# `:app`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nodePlacementS"
},
{
"path": "app/benchmark-rules.pro",
"chars": 896,
"preview": "# Proguard rules for the `benchmark` build type.\n#\n# Obsfuscation must be disabled for the build variant that generates "
},
{
"path": "app/build.gradle.kts",
"chars": 6168,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/dependencies/prodReleaseRuntimeClasspath.txt",
"chars": 12365,
"preview": "androidx.activity:activity-compose:1.12.0\nandroidx.activity:activity-ktx:1.12.0\nandroidx.activity:activity:1.12.0\nandroi"
},
{
"path": "app/google-services.json",
"chars": 2917,
"preview": "{\n \"project_info\": {\n \"project_number\": \"YourProjectId\",\n \"project_id\": \"abc\",\n \"storage_bucket\": \"abc\"\n },\n "
},
{
"path": "app/prodRelease-badging.txt",
"chars": 5833,
"preview": "package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='16' p"
},
{
"path": "app/proguard-rules.pro",
"chars": 98,
"preview": "# Repackage classes into the default package to reduce the size of descriptors.\n-repackageclasses\n"
},
{
"path": "app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt",
"chars": 13442,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/UiTestExtensions.kt",
"chars": 978,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/benchmark/res/values/colors.xml",
"chars": 1009,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/benchmark/res/values-night/colors.xml",
"chars": 1009,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/debug/res/values/colors.xml",
"chars": 1005,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/debug/res/values-night/colors.xml",
"chars": 1005,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 3322,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt",
"chars": 8041,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt",
"chars": 3075,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaApplication.kt",
"chars": 2304,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/di/JankStatsModule.kt",
"chars": 1642,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelNavItem.kt",
"chars": 2948,
"preview": "/*\n * Copyright 2025 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt",
"chars": 13134,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt",
"chars": 4539,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/ProfileVerifierLogger.kt",
"chars": 2685,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/kotlin/com/google/samples/apps/nowinandroid/util/UiExtensions.kt",
"chars": 1753,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 982,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 2183,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/drawable/ic_splash.xml",
"chars": 2320,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 974,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 974,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 820,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 830,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 1620,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values-night/colors.xml",
"chars": 820,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/main/res/values-night/themes.xml",
"chars": 1094,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2021 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/prod/AndroidManifest.xml",
"chars": 1069,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/DeviceConfigurationOverrideWindowInsets.kt",
"chars": 2685,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppScreenSizesScreenshotTests.kt",
"chars": 7714,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppStateTest.kt",
"chars": 6159,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarInsetsScreenshotTests.kt",
"chars": 12624,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/testDemo/kotlin/com/google/samples/apps/nowinandroid/ui/SnackbarScreenshotTests.kt",
"chars": 8276,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app/src/testDemo/resources/robolectric.properties",
"chars": 609,
"preview": "#\n# Copyright 2025 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
},
{
"path": "app-nia-catalog/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app-nia-catalog/README.md",
"chars": 1828,
"preview": "# `:app-nia-catalog`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n no"
},
{
"path": "app-nia-catalog/build.gradle.kts",
"chars": 2752,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app-nia-catalog/dependencies/releaseRuntimeClasspath.txt",
"chars": 6664,
"preview": "androidx.activity:activity-compose:1.9.3\nandroidx.activity:activity-ktx:1.9.3\nandroidx.activity:activity:1.9.3\nandroidx."
},
{
"path": "app-nia-catalog/src/main/AndroidManifest.xml",
"chars": 1505,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/kotlin/com/google/samples/apps/niacatalog/NiaCatalogActivity.kt",
"chars": 1126,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app-nia-catalog/src/main/kotlin/com/google/samples/apps/niacatalog/ui/Catalog.kt",
"chars": 17635,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "app-nia-catalog/src/main/res/drawable/ic_launcher_background.xml",
"chars": 955,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 2676,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 974,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 974,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/res/values/strings.xml",
"chars": 745,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "app-nia-catalog/src/main/res/values/themes.xml",
"chars": 784,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "benchmarks/README.md",
"chars": 6139,
"preview": "# `:benchmarks`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nodePla"
},
{
"path": "benchmarks/build.gradle.kts",
"chars": 2508,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/AndroidManifest.xml",
"chars": 683,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "benchmarks/src/main/kotlin/androidx/test/uiautomator/UiAutomatorHelpers.kt",
"chars": 1519,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/BaselineProfileMetrics.kt",
"chars": 1876,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/GeneralActions.kt",
"chars": 2392,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/Utils.kt",
"chars": 2127,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/BookmarksBaselineProfile.kt",
"chars": 1362,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/ForYouBaselineProfile.kt",
"chars": 1587,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/InterestsBaselineProfile.kt",
"chars": 1490,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/baselineprofile/StartupBaselineProfile.kt",
"chars": 1518,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/bookmarks/BookmarksActions.kt",
"chars": 1120,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ForYouActions.kt",
"chars": 4011,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/foryou/ScrollForYouFeedBenchmark.kt",
"chars": 2084,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/interests/InterestsActions.kt",
"chars": 1881,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/interests/ScrollTopicListBenchmark.kt",
"chars": 2198,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/interests/ScrollTopicListPowerMetricsBenchmark.kt",
"chars": 3212,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/interests/TopicsScreenRecompositionBenchmark.kt",
"chars": 2206,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "benchmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/startup/StartupBenchmark.kt",
"chars": 2886,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/README.md",
"chars": 2063,
"preview": "# Convention Plugins\n\nThe `build-logic` folder defines project-specific convention plugins, used to keep a single\nsource"
},
{
"path": "build-logic/convention/build.gradle.kts",
"chars": 4887,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt",
"chars": 1265,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt",
"chars": 2035,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt",
"chars": 2808,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt",
"chars": 1092,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt",
"chars": 1409,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidFeatureApiConventionPlugin.kt",
"chars": 1121,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidFeatureImplConventionPlugin.kt",
"chars": 2158,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt",
"chars": 1249,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt",
"chars": 2990,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt",
"chars": 1389,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt",
"chars": 1646,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt",
"chars": 1923,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt",
"chars": 1330,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt",
"chars": 1834,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt",
"chars": 1379,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/RootPlugin.kt",
"chars": 1379,
"preview": "/*\n * Copyright 2025 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt",
"chars": 2555,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidInstrumentedTests.kt",
"chars": 1376,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt",
"chars": 5313,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt",
"chars": 2351,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Graph.kt",
"chars": 13150,
"preview": "/*\n * Copyright 2025 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt",
"chars": 5263,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt",
"chars": 4139,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaBuildType.kt",
"chars": 867,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt",
"chars": 2338,
"preview": "/*\n * Copyright 2026 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/PrintTestApks.kt",
"chars": 4004,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/ProjectExtensions.kt",
"chars": 945,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Spotless.kt",
"chars": 2940,
"preview": "/*\n * Copyright 2026 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build-logic/gradle.properties",
"chars": 263,
"preview": "# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534\norg.gradle.parallel=t"
},
{
"path": "build-logic/settings.gradle.kts",
"chars": 1191,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build.gradle.kts",
"chars": 1938,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "build_android_release.sh",
"chars": 1746,
"preview": "#!/usr/bin/env bash\n\n#\n# Copyright 2022 The Android Open Source Project\n#\n# Licensed under the Apache License, Version"
},
{
"path": "compose_compiler_config.conf",
"chars": 606,
"preview": "// This file contains classes (with possible wildcards) that the Compose Compiler will treat as stable.\n// It allows us "
},
{
"path": "core/analytics/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "core/analytics/README.md",
"chars": 1473,
"preview": "# `:core:analytics`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nod"
},
{
"path": "core/analytics/build.gradle.kts",
"chars": 1036,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/demo/kotlin/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt",
"chars": 996,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/main/AndroidManifest.xml",
"chars": 684,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2023 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "core/analytics/src/main/kotlin/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsEvent.kt",
"chars": 2065,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/main/kotlin/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsHelper.kt",
"chars": 879,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/main/kotlin/com/google/samples/apps/nowinandroid/core/analytics/NoOpAnalyticsHelper.kt",
"chars": 878,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/main/kotlin/com/google/samples/apps/nowinandroid/core/analytics/StubAnalyticsHelper.kt",
"chars": 1164,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/main/kotlin/com/google/samples/apps/nowinandroid/core/analytics/UiHelpers.kt",
"chars": 1117,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/prod/kotlin/com/google/samples/apps/nowinandroid/core/analytics/AnalyticsModule.kt",
"chars": 1337,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/analytics/src/prod/kotlin/com/google/samples/apps/nowinandroid/core/analytics/FirebaseAnalyticsHelper.kt",
"chars": 1443,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "core/common/README.md",
"chars": 1460,
"preview": "# `:core:common`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nodePl"
},
{
"path": "core/common/build.gradle.kts",
"chars": 876,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/common/network/NiaDispatchers.kt",
"chars": 913,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/common/network/di/CoroutineScopesModule.kt",
"chars": 1546,
"preview": "/*\n * Copyright 2026 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/common/network/di/DispatchersModule.kt",
"chars": 1434,
"preview": "/*\n * Copyright 2026 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/src/main/kotlin/com/google/samples/apps/nowinandroid/core/common/result/Result.kt",
"chars": 1187,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/common/src/test/kotlin/com/google/samples/apps/nowinandroid/core/common/result/ResultKtTest.kt",
"chars": 1684,
"preview": "/*\n * Copyright 2026 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "core/data/README.md",
"chars": 2327,
"preview": "# `:core:data`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nodePlac"
},
{
"path": "core/data/build.gradle.kts",
"chars": 1397,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/AndroidManifest.xml",
"chars": 832,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/SyncUtilities.kt",
"chars": 4148,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/DataModule.kt",
"chars": 3087,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/di/UserNewsResourceRepositoryModule.kt",
"chars": 1242,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt",
"chars": 2512,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/model/RecentSearchQuery.kt",
"chars": 1056,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/model/Topic.kt",
"chars": 1027,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/AnalyticsExtensions.kt",
"chars": 3014,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/CompositeUserNewsResourceRepository.kt",
"chars": 2816,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultRecentSearchRepository.kt",
"chars": 1926,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt",
"chars": 3966,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/NewsRepository.kt",
"chars": 1594,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt",
"chars": 6875,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepository.kt",
"chars": 2837,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepository.kt",
"chars": 3293,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/RecentSearchRepository.kt",
"chars": 1317,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/SearchContentsRepository.kt",
"chars": 1224,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/TopicsRepository.kt",
"chars": 1089,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserDataRepository.kt",
"chars": 2167,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/UserNewsResourceRepository.kt",
"chars": 1470,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt",
"chars": 3736,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/NetworkMonitor.kt",
"chars": 833,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/SyncManager.kt",
"chars": 852,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/TimeZoneMonitor.kt",
"chars": 4621,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/CompositeUserNewsResourceRepositoryTest.kt",
"chars": 7646,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/UserNewsResourceTest.kt",
"chars": 4284,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt",
"chars": 4788,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepositoryTest.kt",
"chars": 14583,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstTopicsRepositoryTest.kt",
"chars": 6211,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstUserDataRepositoryTest.kt",
"chars": 8640,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/TestSynchronizer.kt",
"chars": 1377,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNewsResourceDao.kt",
"chars": 5142,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestNiaNetworkDataSource.kt",
"chars": 4471,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/data/testdoubles/TestTopicDao.kt",
"chars": 2320,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data/src/test/kotlin/com/google/samples/apps/nowinandroid/core/database/model/PopulatedNewsResourceKtTest.kt",
"chars": 2600,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "core/data-test/README.md",
"chars": 2414,
"preview": "# `:core:data-test`\n\n## Module dependency graph\n\n<!--region graph-->\n```mermaid\n---\nconfig:\n layout: elk\n elk:\n nod"
},
{
"path": "core/data-test/build.gradle.kts",
"chars": 899,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/src/main/AndroidManifest.xml",
"chars": 684,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n Copyright 2022 The Android Open Source Project\n\n Licensed under the"
},
{
"path": "core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/AlwaysOnlineNetworkMonitor.kt",
"chars": 986,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/DefaultZoneIdTimeZoneMonitor.kt",
"chars": 1055,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/TestDataModule.kt",
"chars": 2905,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt",
"chars": 3121,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "core/data-test/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeRecentSearchRepository.kt",
"chars": 1363,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
}
]
// ... and 354 more files (download for full content)
About this extraction
This page contains the full source code of the android/nowinandroid GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 554 files (1.8 MB), approximately 463.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.