parent
2ac13e821e
commit
77356ebc63
@ -1,6 +0,0 @@
|
||||
# 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
|
@ -1,46 +0,0 @@
|
||||
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
|
@ -1,38 +0,0 @@
|
||||
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
|
@ -1,46 +0,0 @@
|
||||
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
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Create a pull request
|
||||
label: 'triage me'
|
||||
---
|
||||
Thank you for opening a Pull Request!
|
||||
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
|
||||
- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
|
||||
- [ ] Ensure the tests and linter pass (`./gradlew --init-script gradle/init.gradle.kts spotlessApply` to automatically apply formatting)
|
||||
- [ ] Appropriate docs were updated (if necessary)
|
||||
|
||||
Is this your first Pull Request?
|
||||
- [ ] Run `./tools/setup.sh`
|
||||
- [ ] Import the code formatting style as explained in [the setup script](/tools/setup.sh#L40).
|
||||
|
||||
Fixes #<issue_number_goes_here> 🦕
|
@ -1,28 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
kotlin.incremental=false
|
||||
kotlin.compiler.execution.strategy=in-process
|
||||
|
||||
# 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
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base", "group:all", ":dependencyDashboard", "schedule:daily"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["org.objenesis:objenesis"],
|
||||
"allowedVersions": "<=2.6"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["com.google.protobuf"],
|
||||
"allowedVersions": "<=0.8.19"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Check spotless
|
||||
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
|
||||
|
||||
- name: Check lint
|
||||
run: ./gradlew lintDemoDebug
|
||||
|
||||
- name: Build all build type and flavor permutations
|
||||
run: ./gradlew assemble
|
||||
|
||||
- name: Run local tests
|
||||
run: ./gradlew testDemoDebug testProdDebug
|
||||
|
||||
- name: Upload build outputs (APKs)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: APKs
|
||||
path: '**/build/outputs/apk/**/*.apk'
|
||||
|
||||
- name: Upload lint reports (HTML)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lint-reports
|
||||
path: '**/build/reports/lint-results-*.html'
|
||||
|
||||
- name: Upload test results (XML)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-results
|
||||
path: '**/build/test-results/test*UnitTest/**.xml'
|
||||
|
||||
androidTest:
|
||||
needs: build
|
||||
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
|
||||
timeout-minutes: 55
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [26, 30]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Build AndroidTest apps
|
||||
run: ./gradlew packageDemoDebug packageDemoDebugAndroidTest --daemon
|
||||
|
||||
- name: 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: Upload test reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-reports-${{ matrix.api-level }}
|
||||
path: '**/build/reports/androidTests'
|
||||
|
||||
androidTest-GMD:
|
||||
needs: build
|
||||
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
|
||||
timeout-minutes: 55
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Accept Android licenses
|
||||
run: yes | "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses || true
|
||||
|
||||
- name: Build AndroidTest apps
|
||||
run: ./gradlew packageDemoDebug packageDemoDebugAndroidTest
|
||||
|
||||
- name: Run instrumented tests with GMD
|
||||
run: ./gradlew cleanManagedDevices --unused-only &&
|
||||
./gradlew ciDemoDebugAndroidTest -Dorg.gradle.workers.max=1
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||
|
||||
- name: Upload test reports
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-reports
|
||||
path: '**/build/reports/androidTests'
|
@ -1,51 +0,0 @@
|
||||
name: GitHub Release with APKs
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 17
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew :app:assembleDemoRelease
|
||||
|
||||
- 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
|
@ -1,2 +0,0 @@
|
||||
# This file can be used to trigger an internal build by changing the number below
|
||||
3
|
@ -1,48 +0,0 @@
|
||||
# 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
|
@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<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>
|
@ -1,33 +0,0 @@
|
||||
# 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.
|
||||
|
@ -1,202 +0,0 @@
|
||||
|
||||
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.
|
@ -1,165 +0,0 @@
|
||||
![Now in Android](docs/images/nia-splash.jpg "Now in Android")
|
||||
|
||||
<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 `demoRelease` variant is [available on the Play Store in open beta](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.
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Screenshot showing For You screen, Interests screen and Topic detail screen](docs/images/screenshots.png "Screenshot showing For You screen, Interests screen and Topic detail screen")
|
||||
|
||||
# Development Environment
|
||||
|
||||
**Now in Android** uses the Gradle build system and can be imported directly into Android Studio (make sure you are using the latest stable version available [here](https://developer.android.com/studio)).
|
||||
|
||||
Change the run configuration to `app`.
|
||||
|
||||
![image](https://user-images.githubusercontent.com/873212/210559920-ef4a40c5-c8e0-478b-bb00-4879a8cf184a.png)
|
||||
|
||||
The `demoDebug` and `demoRelease` build variants can be built and run (the `prod` variants use a backend server which is not currently publicly available).
|
||||
|
||||
![image](https://user-images.githubusercontent.com/873212/210560507-44045dc5-b6d5-41ca-9746-f0f7acf22f8e.png)
|
||||
|
||||
Once you're up and running, you can refer to the learning journeys below to get a better
|
||||
understanding of which libraries and tools are being used, the reasoning behind the approaches to
|
||||
UI, testing, architecture and more, and how all of these different pieces of the project fit
|
||||
together to create a complete app.
|
||||
|
||||
# Architecture
|
||||
|
||||
The **Now in Android** app follows the
|
||||
[official architecture guidance](https://developer.android.com/topic/architecture)
|
||||
and is described in detail in the
|
||||
[architecture learning journey](docs/ArchitectureLearningJourney.md).
|
||||
|
||||
# Modularization
|
||||
|
||||
The **Now in Android** app has been fully modularized and you can find the detailed guidance and
|
||||
description of the modularization strategy used in
|
||||
[modularization learning journey](docs/ModularizationLearningJourney.md).
|
||||
|
||||
# Build
|
||||
|
||||
The app contains the usual `debug` and `release` build variants.
|
||||
|
||||
In addition, the `benchmark` variant of `app` is used to test startup performance and generate a
|
||||
baseline profile (see below for more information).
|
||||
|
||||
`app-nia-catalog` is a standalone app that displays the list of components that are stylized for
|
||||
**Now in Android**.
|
||||
|
||||
The app also uses
|
||||
[product flavors](https://developer.android.com/studio/build/build-variants#product-flavors) to
|
||||
control where content for the app should be loaded from.
|
||||
|
||||
The `demo` flavor uses static local data to allow immediate building and exploring of the UI.
|
||||
|
||||
The `prod` flavor makes real network calls to a backend server, providing up-to-date content. At
|
||||
this time, there is not a public backend available.
|
||||
|
||||
For normal development use the `demoDebug` variant. For UI performance testing use the
|
||||
`demoRelease` variant.
|
||||
|
||||
# Testing
|
||||
|
||||
To facilitate testing of components, **Now in Android** uses dependency injection with
|
||||
[Hilt](https://developer.android.com/training/dependency-injection/hilt-android).
|
||||
|
||||
Most data layer components are defined as interfaces.
|
||||
Then, concrete implementations (with various dependencies) are bound to provide those interfaces to
|
||||
other components in the app.
|
||||
In tests, **Now in Android** notably does _not_ use any mocking libraries.
|
||||
Instead, the production implementations can be replaced with test doubles using Hilt's testing APIs
|
||||
(or via manual constructor injection for `ViewModel` tests).
|
||||
|
||||
These test doubles implement the same interface as the production implementations and generally
|
||||
provide a simplified (but still realistic) implementation with additional testing hooks.
|
||||
This results in less brittle tests that may exercise more production code, instead of just verifying
|
||||
specific calls against mocks.
|
||||
|
||||
Examples:
|
||||
- In instrumentation tests, a temporary folder is used to store the user's preferences, which is
|
||||
wiped after each test.
|
||||
This allows using the real `DataStore` and exercising all related code, instead of mocking the
|
||||
flow of data updates.
|
||||
|
||||
- There are `Test` implementations of each repository, which implement the normal, full repository
|
||||
interface and also provide test-only hooks.
|
||||
`ViewModel` tests use these `Test` repositories, and thus can use the test-only hooks to
|
||||
manipulate the state of the `Test` repository and verify the resulting behavior, instead of
|
||||
checking that specific repository methods were called.
|
||||
|
||||
# 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 analyse compose compiler metrics:
|
||||
|
||||
```
|
||||
./gradlew assembleRelease -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true
|
||||
```
|
||||
|
||||
The reports files will be added to build/compose-reports in each module. The metrics files will be
|
||||
added to build/compose-metrics in each module.
|
||||
|
||||
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.
|
@ -1 +0,0 @@
|
||||
/build
|
@ -1,3 +0,0 @@
|
||||
# :app-nia-catalog module
|
||||
|
||||
![Dependency graph](../docs/images/graphs/dep_graph_app_nia_catalog.png)
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import com.google.samples.apps.nowinandroid.FlavorDimension
|
||||
import com.google.samples.apps.nowinandroid.NiaFlavor
|
||||
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("nowinandroid.android.application")
|
||||
id("nowinandroid.android.application.compose")
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "com.google.samples.apps.niacatalog"
|
||||
versionCode = 1
|
||||
versionName = "0.0.1" // X.Y.Z; X = Major, Y = minor, Z = Patch level
|
||||
|
||||
// The UI catalog does not depend on content from the app, however, it depends on modules
|
||||
// which do, so we must specify a default value for the contentType dimension.
|
||||
missingDimensionStrategy(FlavorDimension.contentType.name, NiaFlavor.demo.name)
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
|
||||
}
|
||||
}
|
||||
namespace = "com.google.samples.apps.niacatalog"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// To publish on the Play store a private signing key is required, but to allow anyone
|
||||
// who clones the code to sign and run the release variant, use the debug signing key.
|
||||
// TODO: Abstract the signing configuration to a separate file to avoid hardcoding this.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:designsystem"))
|
||||
implementation(project(":core:ui"))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.NiaCatalog">
|
||||
<activity
|
||||
android:name="com.google.samples.apps.niacatalog.NiaCatalogActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.NiaCatalog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.niacatalog
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.samples.apps.niacatalog.ui.NiaCatalog
|
||||
|
||||
class NiaCatalogActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent { NiaCatalog() }
|
||||
}
|
||||
}
|
@ -1,372 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.niacatalog.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilterChip
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBar
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaNavigationBarItem
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOutlinedButton
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTab
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTabRow
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTextButton
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaViewToggleButton
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||
|
||||
/**
|
||||
* Now in Android component catalog.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun NiaCatalog() {
|
||||
NiaTheme {
|
||||
Surface {
|
||||
val contentPadding = WindowInsets
|
||||
.systemBars
|
||||
.add(WindowInsets(left = 16.dp, top = 16.dp, right = 16.dp, bottom = 16.dp))
|
||||
.asPaddingValues()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "NiA Catalog",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
item { Text("Buttons", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NiaButton(onClick = {}) {
|
||||
Text(text = "Enabled")
|
||||
}
|
||||
NiaOutlinedButton(onClick = {}) {
|
||||
Text(text = "Enabled")
|
||||
}
|
||||
NiaTextButton(onClick = {}) {
|
||||
Text(text = "Enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Text("Disabled buttons", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NiaButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text(text = "Disabled")
|
||||
}
|
||||
NiaOutlinedButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text(text = "Disabled")
|
||||
}
|
||||
NiaTextButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text(text = "Disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Text("Buttons with leading icons", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NiaButton(
|
||||
onClick = {},
|
||||
text = { Text(text = "Enabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
NiaOutlinedButton(
|
||||
onClick = {},
|
||||
text = { Text(text = "Enabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
NiaTextButton(
|
||||
onClick = {},
|
||||
text = { Text(text = "Enabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("Disabled buttons with leading icons", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NiaButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
text = { Text(text = "Disabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
NiaOutlinedButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
text = { Text(text = "Disabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
NiaTextButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
text = { Text(text = "Disabled") },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = NiaIcons.Add, contentDescription = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("Dropdown menus", Modifier.padding(top = 16.dp)) }
|
||||
item { Text("Chips", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
var firstChecked by remember { mutableStateOf(false) }
|
||||
NiaFilterChip(
|
||||
selected = firstChecked,
|
||||
onSelectedChange = { checked -> firstChecked = checked },
|
||||
label = { Text(text = "Enabled") },
|
||||
)
|
||||
var secondChecked by remember { mutableStateOf(true) }
|
||||
NiaFilterChip(
|
||||
selected = secondChecked,
|
||||
onSelectedChange = { checked -> secondChecked = checked },
|
||||
label = { Text(text = "Enabled") },
|
||||
)
|
||||
NiaFilterChip(
|
||||
selected = false,
|
||||
onSelectedChange = {},
|
||||
enabled = false,
|
||||
label = { Text(text = "Disabled") },
|
||||
)
|
||||
NiaFilterChip(
|
||||
selected = true,
|
||||
onSelectedChange = {},
|
||||
enabled = false,
|
||||
label = { Text(text = "Disabled") },
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("Icon buttons", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
var firstChecked by remember { mutableStateOf(false) }
|
||||
NiaIconToggleButton(
|
||||
checked = firstChecked,
|
||||
onCheckedChange = { checked -> firstChecked = checked },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
checkedIcon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.Bookmark,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
var secondChecked by remember { mutableStateOf(true) }
|
||||
NiaIconToggleButton(
|
||||
checked = secondChecked,
|
||||
onCheckedChange = { checked -> secondChecked = checked },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
checkedIcon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.Bookmark,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
NiaIconToggleButton(
|
||||
checked = false,
|
||||
onCheckedChange = {},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
checkedIcon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.Bookmark,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
)
|
||||
NiaIconToggleButton(
|
||||
checked = true,
|
||||
onCheckedChange = {},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.BookmarkBorder,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
checkedIcon = {
|
||||
Icon(
|
||||
imageVector = NiaIcons.Bookmark,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("View toggle", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
var firstExpanded by remember { mutableStateOf(false) }
|
||||
NiaViewToggleButton(
|
||||
expanded = firstExpanded,
|
||||
onExpandedChange = { expanded -> firstExpanded = expanded },
|
||||
compactText = { Text(text = "Compact view") },
|
||||
expandedText = { Text(text = "Expanded view") },
|
||||
)
|
||||
var secondExpanded by remember { mutableStateOf(true) }
|
||||
NiaViewToggleButton(
|
||||
expanded = secondExpanded,
|
||||
onExpandedChange = { expanded -> secondExpanded = expanded },
|
||||
compactText = { Text(text = "Compact view") },
|
||||
expandedText = { Text(text = "Expanded view") },
|
||||
)
|
||||
NiaViewToggleButton(
|
||||
expanded = false,
|
||||
onExpandedChange = {},
|
||||
compactText = { Text(text = "Disabled") },
|
||||
expandedText = { Text(text = "Disabled") },
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("Tags", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
NiaTopicTag(
|
||||
followed = true,
|
||||
onClick = {},
|
||||
text = { Text(text = "Topic 1".uppercase()) },
|
||||
)
|
||||
NiaTopicTag(
|
||||
followed = false,
|
||||
onClick = {},
|
||||
text = { Text(text = "Topic 2".uppercase()) },
|
||||
)
|
||||
NiaTopicTag(
|
||||
followed = false,
|
||||
onClick = {},
|
||||
text = { Text(text = "Disabled".uppercase()) },
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Text("Tabs", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||
val titles = listOf("Topics", "People")
|
||||
NiaTabRow(selectedTabIndex = selectedTabIndex) {
|
||||
titles.forEachIndexed { index, title ->
|
||||
NiaTab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { selectedTabIndex = index },
|
||||
text = { Text(text = title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Text("Navigation", Modifier.padding(top = 16.dp)) }
|
||||
item {
|
||||
var selectedItem by remember { mutableStateOf(0) }
|
||||
val items = listOf("For you", "Saved", "Interests")
|
||||
val icons = listOf(
|
||||
NiaIcons.UpcomingBorder,
|
||||
NiaIcons.BookmarksBorder,
|
||||
NiaIcons.Grid3x3,
|
||||
)
|
||||
val selectedIcons = listOf(
|
||||
NiaIcons.Upcoming,
|
||||
NiaIcons.Bookmarks,
|
||||
NiaIcons.Grid3x3,
|
||||
)
|
||||
NiaNavigationBar {
|
||||
items.forEachIndexed { index, item ->
|
||||
NiaNavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = icons[index],
|
||||
contentDescription = item,
|
||||
)
|
||||
},
|
||||
selectedIcon = {
|
||||
Icon(
|
||||
imageVector = selectedIcons[index],
|
||||
contentDescription = item,
|
||||
)
|
||||
},
|
||||
label = { Text(item) },
|
||||
selected = selectedItem == index,
|
||||
onClick = { selectedItem = index },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<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="#FFFFFF"/>
|
||||
</vector>
|
@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<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.13C64.01,84.13 63.13,83.26 63.13,82.18C63.13,81.11 64,80.24 65.08,80.24C66.15,80.24 67.02,81.11 67.02,82.18C67.02,83.26 66.15,84.13 65.08,84.13ZM43.6,84.13C42.53,84.13 41.65,83.26 41.65,82.18C41.65,81.11 42.52,80.24 43.6,80.24C44.66,80.24 45.54,81.11 45.54,82.18C45.54,83.26 44.67,84.13 43.6,84.13ZM65.77,72.44L69.66,65.73C69.88,65.35 69.74,64.85 69.36,64.63C68.97,64.41 68.48,64.54 68.25,64.93L64.32,71.73C61.31,70.36 57.94,69.59 54.33,69.59C50.73,69.59 47.35,70.36 44.34,71.73L40.41,64.93C40.19,64.54 39.69,64.41 39.31,64.63C38.92,64.85 38.79,65.35 39.01,65.73L42.89,72.44C36.22,76.07 31.67,82.81 31,90.77H77.67C77,82.8 72.44,76.06 65.77,72.44Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M46.57,35H46.57C46.1,35 45.72,35.38 45.72,35.85L45.72,43.15H44.19C43.35,43.15 42.67,43.83 42.67,44.68C42.67,45.52 43.35,46.2 44.19,46.2H45.72V43.15H47.42C48.17,43.15 48.78,42.54 48.78,41.79L48.78,37.72H49.97C50.43,37.72 50.81,37.34 50.81,36.87V35.85C50.81,35.38 50.43,35 49.97,35H47.42H46.57ZM46.57,54.35H46.57H47.42H49.97C50.43,54.35 50.81,53.97 50.81,53.5V52.48C50.81,52.02 50.43,51.64 49.97,51.64H48.78L48.78,47.56C48.78,46.81 48.17,46.2 47.42,46.2H45.72L45.72,53.5C45.72,53.97 46.1,54.35 46.57,54.35ZM61.54,35H61.54C62.01,35 62.39,35.38 62.39,35.85V43.15H63.92C64.76,43.15 65.44,43.83 65.44,44.68C65.44,45.52 64.76,46.2 63.92,46.2H62.39V43.15H60.69C59.94,43.15 59.33,42.54 59.33,41.79V37.72H58.15C57.68,37.72 57.3,37.34 57.3,36.87V35.85C57.3,35.38 57.68,35 58.15,35H60.69H61.54ZM61.54,54.35H61.54H60.69H58.15C57.68,54.35 57.3,53.97 57.3,53.5V52.48C57.3,52.02 57.68,51.64 58.15,51.64H59.33V47.56C59.33,46.81 59.94,46.2 60.69,46.2H62.39V53.5C62.39,53.97 62.01,54.35 61.54,54.35Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<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>
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<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>
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">NiA Catalog</string>
|
||||
</resources>
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<style name="Theme.NiaCatalog" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
@ -1,3 +0,0 @@
|
||||
# :app module
|
||||
|
||||
![Dependency graph](../docs/images/graphs/dep_graph_app.png)
|
@ -1,125 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -1,270 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertIsSelected
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.NoActivityResumedException
|
||||
import com.google.samples.apps.nowinandroid.MainActivity
|
||||
import com.google.samples.apps.nowinandroid.R
|
||||
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.R as BookmarksR
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.R as FeatureForyouR
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.R as FeatureInterestsR
|
||||
import com.google.samples.apps.nowinandroid.feature.settings.R as SettingsR
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
* Create a temporary folder used to create a Data Store file. This guarantees that
|
||||
* the file is removed in between each test, preventing a crash.
|
||||
*/
|
||||
@BindValue
|
||||
@get:Rule(order = 1)
|
||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
||||
|
||||
/**
|
||||
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
|
||||
*/
|
||||
@get:Rule(order = 2)
|
||||
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
|
||||
|
||||
/**
|
||||
* Use the primary activity to initialize the app normally.
|
||||
*/
|
||||
@get:Rule(order = 3)
|
||||
val composeTestRule = createAndroidComposeRule<MainActivity>()
|
||||
|
||||
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) =
|
||||
ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) }
|
||||
|
||||
// The strings used for matching in these tests
|
||||
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.navigate_up)
|
||||
private val forYou by composeTestRule.stringResource(FeatureForyouR.string.for_you)
|
||||
private val interests by composeTestRule.stringResource(FeatureInterestsR.string.interests)
|
||||
private val sampleTopic = "Headlines"
|
||||
private val appName by composeTestRule.stringResource(R.string.app_name)
|
||||
private val saved by composeTestRule.stringResource(BookmarksR.string.saved)
|
||||
private val settings by composeTestRule.stringResource(SettingsR.string.top_app_bar_action_icon_description)
|
||||
private val brand by composeTestRule.stringResource(SettingsR.string.brand_android)
|
||||
private val ok by composeTestRule.stringResource(SettingsR.string.dismiss_dialog_button_text)
|
||||
|
||||
@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()
|
||||
// TODO: Add top level destinations here, see b/226357686.
|
||||
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
|
||||
hasAnyAncestor(
|
||||
hasTestTag("NiaBottomBar") or hasTestTag("NiaNavRail"),
|
||||
),
|
||||
).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()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun navigationBar_multipleBackStackInterests() {
|
||||
composeTestRule.apply {
|
||||
onNodeWithText(interests).performClick()
|
||||
// TODO: Grab string from fake data
|
||||
onNodeWithText("Android Studio & Tools").performClick()
|
||||
|
||||
// Switch tab
|
||||
onNodeWithText(forYou).performClick()
|
||||
|
||||
// Come back to Interests
|
||||
onNodeWithText(interests).performClick()
|
||||
|
||||
// Verify we're not in the list of interests
|
||||
onNodeWithText("Android Auto").assertDoesNotExist() // TODO: Grab string from fake data
|
||||
}
|
||||
}
|
||||
}
|
@ -1,268 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.ui
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.testharness.TestHarness
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||
import dagger.hilt.android.testing.BindValue
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Tests that the navigation UI is rendered correctly on different screen sizes.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
@HiltAndroidTest
|
||||
class NavigationUiTest {
|
||||
|
||||
/**
|
||||
* Manages the components' state and is used to perform injection on your test
|
||||
*/
|
||||
@get:Rule(order = 0)
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
/**
|
||||
* Create a temporary folder used to create a Data Store file. This guarantees that
|
||||
* the file is removed in between each test, preventing a crash.
|
||||
*/
|
||||
@BindValue
|
||||
@get:Rule(order = 1)
|
||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
||||
|
||||
/**
|
||||
* Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission.
|
||||
*/
|
||||
@get:Rule(order = 2)
|
||||
val postNotificationsPermission = GrantPostNotificationsPermissionRule()
|
||||
|
||||
/**
|
||||
* Use a test activity to set the content on.
|
||||
*/
|
||||
@get:Rule(order = 3)
|
||||
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||
|
||||
val userNewsResourceRepository = CompositeUserNewsResourceRepository(
|
||||
newsRepository = TestNewsRepository(),
|
||||
userDataRepository = TestUserDataRepository(),
|
||||
)
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_compactHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(400.dp, 400.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_compactHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(610.dp, 400.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_compactHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(900.dp, 400.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_mediumHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(400.dp, 500.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_mediumHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(610.dp, 500.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_mediumHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(900.dp, 500.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactWidth_expandedHeight_showsNavigationBar() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(400.dp, 1000.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediumWidth_expandedHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(610.dp, 1000.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun expandedWidth_expandedHeight_showsNavigationRail() {
|
||||
composeTestRule.setContent {
|
||||
TestHarness(size = DpSize(900.dp, 1000.dp)) {
|
||||
BoxWithConstraints {
|
||||
NiaApp(
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||
DpSize(maxWidth, maxHeight),
|
||||
),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||
}
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.ui
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.ComposeNavigator
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.createGraph
|
||||
import androidx.navigation.testing.TestNavHostController
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Tests [NiaAppState].
|
||||
*
|
||||
* Note: This could become an unit test if Robolectric is added to the project and the Context
|
||||
* is faked.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||
class NiaAppStateTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
// Create the test dependencies.
|
||||
private val networkMonitor = TestNetworkMonitor()
|
||||
|
||||
private val userNewsResourceRepository =
|
||||
CompositeUserNewsResourceRepository(TestNewsRepository(), TestUserDataRepository())
|
||||
|
||||
// Subject under test.
|
||||
private lateinit var state: NiaAppState
|
||||
|
||||
@Test
|
||||
fun niaAppState_currentDestination() = runTest {
|
||||
var currentDestination: String? = null
|
||||
|
||||
composeTestRule.setContent {
|
||||
val navController = rememberTestNavController()
|
||||
state = remember(navController) {
|
||||
NiaAppState(
|
||||
navController = navController,
|
||||
coroutineScope = backgroundScope,
|
||||
windowSizeClass = getCompactWindowClass(),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
// Update currentDestination whenever it changes
|
||||
currentDestination = state.currentDestination?.route
|
||||
|
||||
// Navigate to destination b once
|
||||
LaunchedEffect(Unit) {
|
||||
navController.setCurrentDestination("b")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("b", currentDestination)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun niaAppState_destinations() = runTest {
|
||||
composeTestRule.setContent {
|
||||
state = rememberNiaAppState(
|
||||
windowSizeClass = getCompactWindowClass(),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(3, state.topLevelDestinations.size)
|
||||
assertTrue(state.topLevelDestinations[0].name.contains("for_you", true))
|
||||
assertTrue(state.topLevelDestinations[1].name.contains("bookmarks", true))
|
||||
assertTrue(state.topLevelDestinations[2].name.contains("interests", true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun niaAppState_showBottomBar_compact() = runTest {
|
||||
composeTestRule.setContent {
|
||||
state = NiaAppState(
|
||||
navController = NavHostController(LocalContext.current),
|
||||
coroutineScope = backgroundScope,
|
||||
windowSizeClass = getCompactWindowClass(),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(state.shouldShowBottomBar)
|
||||
assertFalse(state.shouldShowNavRail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun niaAppState_showNavRail_medium() = runTest {
|
||||
composeTestRule.setContent {
|
||||
state = NiaAppState(
|
||||
navController = NavHostController(LocalContext.current),
|
||||
coroutineScope = backgroundScope,
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp)),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(state.shouldShowNavRail)
|
||||
assertFalse(state.shouldShowBottomBar)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun niaAppState_showNavRail_large() = runTest {
|
||||
composeTestRule.setContent {
|
||||
state = NiaAppState(
|
||||
navController = NavHostController(LocalContext.current),
|
||||
coroutineScope = backgroundScope,
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(state.shouldShowNavRail)
|
||||
assertFalse(state.shouldShowBottomBar)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) {
|
||||
composeTestRule.setContent {
|
||||
state = NiaAppState(
|
||||
navController = NavHostController(LocalContext.current),
|
||||
coroutineScope = backgroundScope,
|
||||
windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)),
|
||||
networkMonitor = networkMonitor,
|
||||
userNewsResourceRepository = userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
|
||||
backgroundScope.launch { state.isOffline.collect() }
|
||||
networkMonitor.setConnected(false)
|
||||
assertEquals(
|
||||
true,
|
||||
state.isOffline.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberTestNavController(): TestNavHostController {
|
||||
val context = LocalContext.current
|
||||
val navController = remember {
|
||||
TestNavHostController(context).apply {
|
||||
navigatorProvider.addNavigator(ComposeNavigator())
|
||||
graph = createGraph(startDestination = "a") {
|
||||
composable("a") { }
|
||||
composable("b") { }
|
||||
composable("c") { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return navController
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.di
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.view.Window
|
||||
import androidx.metrics.performance.JankStats
|
||||
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(): JankStats.OnFrameListener {
|
||||
return JankStats.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 {
|
||||
return activity.window
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesJankStats(
|
||||
window: Window,
|
||||
frameListener: JankStats.OnFrameListener,
|
||||
): JankStats {
|
||||
return JankStats.createAndTrack(window, frameListener)
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph
|
||||
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
|
||||
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
|
||||
import com.google.samples.apps.nowinandroid.ui.NiaAppState
|
||||
|
||||
/**
|
||||
* Top-level navigation graph. Navigation is organized as explained at
|
||||
* https://d.android.com/jetpack/compose/nav-adaptive
|
||||
*
|
||||
* The navigation graph defined in this file defines the different top level routes. Navigation
|
||||
* within each route is handled using state and Back Handlers.
|
||||
*/
|
||||
@Composable
|
||||
fun NiaNavHost(
|
||||
appState: NiaAppState,
|
||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
startDestination: String = forYouNavigationRoute,
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
modifier = modifier,
|
||||
) {
|
||||
// TODO: handle topic clicks from each top level destination
|
||||
forYouScreen(onTopicClick = {})
|
||||
bookmarksScreen(
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
)
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
|
||||
onTopicClick = navController::navigateToTopic,
|
||||
)
|
||||
interestsGraph(
|
||||
onTopicClick = { topicId ->
|
||||
navController.navigateToTopic(topicId)
|
||||
},
|
||||
nestedGraphs = {
|
||||
topicScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onTopicClick = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.navigation
|
||||
|
||||
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.R as bookmarksR
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.R as forYouR
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
|
||||
|
||||
/**
|
||||
* Type for the top level destinations in the application. Each of these destinations
|
||||
* can contain one or more screens (based on the window size). Navigation from one screen to the
|
||||
* next within a single destination will be handled directly in composables.
|
||||
*/
|
||||
enum class TopLevelDestination(
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector,
|
||||
val iconTextId: Int,
|
||||
val titleTextId: Int,
|
||||
) {
|
||||
FOR_YOU(
|
||||
selectedIcon = NiaIcons.Upcoming,
|
||||
unselectedIcon = NiaIcons.UpcomingBorder,
|
||||
iconTextId = forYouR.string.for_you,
|
||||
titleTextId = R.string.app_name,
|
||||
),
|
||||
BOOKMARKS(
|
||||
selectedIcon = NiaIcons.Bookmarks,
|
||||
unselectedIcon = NiaIcons.BookmarksBorder,
|
||||
iconTextId = bookmarksR.string.saved,
|
||||
titleTextId = bookmarksR.string.saved,
|
||||
),
|
||||
INTERESTS(
|
||||
selectedIcon = NiaIcons.Grid3x3,
|
||||
unselectedIcon = NiaIcons.Grid3x3,
|
||||
iconTextId = interestsR.string.interests,
|
||||
titleTextId = interestsR.string.interests,
|
||||
),
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.ui
|
||||
|
||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navOptions
|
||||
import androidx.tracing.trace
|
||||
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.ui.TrackDisposableJank
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsRoute
|
||||
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph
|
||||
import com.google.samples.apps.nowinandroid.feature.search.navigation.navigateToSearch
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKMARKS
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
|
||||
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun rememberNiaAppState(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
userNewsResourceRepository: UserNewsResourceRepository,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
): NiaAppState {
|
||||
NavigationTrackingSideEffect(navController)
|
||||
return remember(
|
||||
navController,
|
||||
coroutineScope,
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
userNewsResourceRepository,
|
||||
) {
|
||||
NiaAppState(
|
||||
navController,
|
||||
coroutineScope,
|
||||
windowSizeClass,
|
||||
networkMonitor,
|
||||
userNewsResourceRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class NiaAppState(
|
||||
val navController: NavHostController,
|
||||
val coroutineScope: CoroutineScope,
|
||||
val windowSizeClass: WindowSizeClass,
|
||||
networkMonitor: NetworkMonitor,
|
||||
userNewsResourceRepository: UserNewsResourceRepository,
|
||||
) {
|
||||
val currentDestination: NavDestination?
|
||||
@Composable get() = navController
|
||||
.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val currentTopLevelDestination: TopLevelDestination?
|
||||
@Composable get() = when (currentDestination?.route) {
|
||||
forYouNavigationRoute -> FOR_YOU
|
||||
bookmarksRoute -> BOOKMARKS
|
||||
interestsRoute -> INTERESTS
|
||||
else -> null
|
||||
}
|
||||
|
||||
val shouldShowBottomBar: Boolean
|
||||
get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact
|
||||
|
||||
val shouldShowNavRail: Boolean
|
||||
get() = !shouldShowBottomBar
|
||||
|
||||
val isOffline = networkMonitor.isOnline
|
||||
.map(Boolean::not)
|
||||
.stateIn(
|
||||
scope = coroutineScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
|
||||
* route.
|
||||
*/
|
||||
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.values().asList()
|
||||
|
||||
/**
|
||||
* The top level destinations that have unread news resources.
|
||||
*/
|
||||
val topLevelDestinationsWithUnreadResources: StateFlow<Set<TopLevelDestination>> =
|
||||
userNewsResourceRepository.observeAllForFollowedTopics()
|
||||
.combine(userNewsResourceRepository.observeAllBookmarked()) { forYouNewsResources, bookmarkedNewsResources ->
|
||||
setOfNotNull(
|
||||
FOR_YOU.takeIf { forYouNewsResources.any { !it.hasBeenViewed } },
|
||||
BOOKMARKS.takeIf { bookmarkedNewsResources.any { !it.hasBeenViewed } },
|
||||
)
|
||||
}.stateIn(
|
||||
coroutineScope,
|
||||
SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptySet(),
|
||||
)
|
||||
|
||||
/**
|
||||
* UI logic for navigating to a top level destination in the app. Top level destinations have
|
||||
* only one copy of the destination of the back stack, and save and restore state whenever you
|
||||
* navigate to and from it.
|
||||
*
|
||||
* @param topLevelDestination: The destination the app needs to navigate to.
|
||||
*/
|
||||
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
|
||||
trace("Navigation: ${topLevelDestination.name}") {
|
||||
val topLevelNavOptions = navOptions {
|
||||
// Pop up to the start destination of the graph to
|
||||
// avoid building up a large stack of destinations
|
||||
// on the back stack as users select items
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
// Avoid multiple copies of the same destination when
|
||||
// reselecting the same item
|
||||
launchSingleTop = true
|
||||
// Restore state when reselecting a previously selected item
|
||||
restoreState = true
|
||||
}
|
||||
|
||||
when (topLevelDestination) {
|
||||
FOR_YOU -> navController.navigateToForYou(topLevelNavOptions)
|
||||
BOOKMARKS -> navController.navigateToBookmarks(topLevelNavOptions)
|
||||
INTERESTS -> navController.navigateToInterestsGraph(topLevelNavOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToSearch() {
|
||||
navController.navigateToSearch()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information about navigation events to be used with JankStats
|
||||
*/
|
||||
@Composable
|
||||
private fun NavigationTrackingSideEffect(navController: NavHostController) {
|
||||
TrackDisposableJank(navController) { metricsHolder ->
|
||||
val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
|
||||
metricsHolder.state?.putState("Navigation", destination.route.toString())
|
||||
}
|
||||
|
||||
navController.addOnDestinationChangedListener(listener)
|
||||
|
||||
onDispose {
|
||||
navController.removeOnDestinationChangedListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?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">
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="23">false</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
|
||||
</style>
|
||||
|
||||
<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>
|
@ -1,26 +0,0 @@
|
||||
<?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>
|
@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import com.google.samples.apps.nowinandroid.NiaBuildType
|
||||
import com.google.samples.apps.nowinandroid.configureFlavors
|
||||
|
||||
plugins {
|
||||
id("nowinandroid.android.test")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.benchmarks"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField("String", "APP_BUILD_TYPE_SUFFIX", "\"\"")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// This benchmark buildType is used for benchmarking, and should function like your
|
||||
// release build (for example, with minification on). It's signed with a debug key
|
||||
// for easy local/CI testing.
|
||||
create("benchmark") {
|
||||
// Keep the build type debuggable so we can attach a debugger if needed.
|
||||
isDebuggable = true
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
buildConfigField(
|
||||
"String",
|
||||
"APP_BUILD_TYPE_SUFFIX",
|
||||
"\"${NiaBuildType.BENCHMARK.applicationIdSuffix ?: ""}\""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same flavor dimensions as the application to allow generating Baseline Profiles on prod,
|
||||
// which is more close to what will be shipped to users (no fake data), but has ability to run the
|
||||
// benchmarks on demo, so we benchmark on stable data.
|
||||
configureFlavors(this) { flavor ->
|
||||
buildConfigField(
|
||||
"String",
|
||||
"APP_FLAVOR_SUFFIX",
|
||||
"\"${flavor.applicationIdSuffix ?: ""}\""
|
||||
)
|
||||
}
|
||||
|
||||
targetProjectPath = ":app"
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.benchmark.macro)
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.androidx.test.espresso.core)
|
||||
implementation(libs.androidx.test.ext)
|
||||
implementation(libs.androidx.test.rules)
|
||||
implementation(libs.androidx.test.runner)
|
||||
implementation(libs.androidx.test.uiautomator)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants {
|
||||
it.enable = it.buildType == "benchmark"
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.test.uiautomator
|
||||
|
||||
import androidx.test.uiautomator.HasChildrenOp.AT_LEAST
|
||||
import androidx.test.uiautomator.HasChildrenOp.AT_MOST
|
||||
import androidx.test.uiautomator.HasChildrenOp.EXACTLY
|
||||
|
||||
// These helpers need to be in the androidx.test.uiautomator package,
|
||||
// because the abstract class has package local method that needs to be implemented.
|
||||
|
||||
/**
|
||||
* Condition will be satisfied if given element has specified count of children
|
||||
*/
|
||||
fun untilHasChildren(
|
||||
childCount: Int = 1,
|
||||
op: HasChildrenOp = AT_LEAST,
|
||||
): UiObject2Condition<Boolean> {
|
||||
return object : UiObject2Condition<Boolean>() {
|
||||
override fun apply(element: UiObject2): Boolean {
|
||||
return when (op) {
|
||||
AT_LEAST -> element.childCount >= childCount
|
||||
EXACTLY -> element.childCount == childCount
|
||||
AT_MOST -> element.childCount <= childCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class HasChildrenOp {
|
||||
AT_LEAST,
|
||||
EXACTLY,
|
||||
AT_MOST,
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Build.VERSION_CODES.TIRAMISU
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
|
||||
/**
|
||||
* Because the app under test is different from the one running the instrumentation test,
|
||||
* the permission has to be granted manually by either:
|
||||
*
|
||||
* - tapping the Allow button
|
||||
* ```kotlin
|
||||
* val obj = By.text("Allow")
|
||||
* val dialog = device.wait(Until.findObject(obj), TIMEOUT)
|
||||
* dialog?.let {
|
||||
* it.click()
|
||||
* device.wait(Until.gone(obj), 5_000)
|
||||
* }
|
||||
* ```
|
||||
* - or (preferred) executing the grant command on the target package.
|
||||
*/
|
||||
fun MacrobenchmarkScope.allowNotifications() {
|
||||
if (SDK_INT >= TIRAMISU) {
|
||||
val command = "pm grant $packageName ${permission.POST_NOTIFICATIONS}"
|
||||
device.executeShellCommand(command)
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid
|
||||
|
||||
import androidx.test.uiautomator.Direction
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject2
|
||||
import com.google.samples.apps.nowinandroid.benchmarks.BuildConfig
|
||||
|
||||
/**
|
||||
* Convenience parameter to use proper package name with regards to build type and build flavor.
|
||||
*/
|
||||
val PACKAGE_NAME = buildString {
|
||||
append("com.google.samples.apps.nowinandroid")
|
||||
append(BuildConfig.APP_FLAVOR_SUFFIX)
|
||||
append(BuildConfig.APP_BUILD_TYPE_SUFFIX)
|
||||
}
|
||||
|
||||
fun UiDevice.flingElementDownUp(element: UiObject2) {
|
||||
// Set some margin from the sides to prevent triggering system navigation
|
||||
element.setGestureMargin(displayWidth / 5)
|
||||
|
||||
element.fling(Direction.DOWN)
|
||||
waitForIdle()
|
||||
element.fling(Direction.UP)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.baselineprofile
|
||||
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||
import com.google.samples.apps.nowinandroid.bookmarks.goToBookmarksScreen
|
||||
import com.google.samples.apps.nowinandroid.foryou.forYouScrollFeedDownUp
|
||||
import com.google.samples.apps.nowinandroid.foryou.forYouSelectTopics
|
||||
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
|
||||
import com.google.samples.apps.nowinandroid.interests.goToInterestsScreen
|
||||
import com.google.samples.apps.nowinandroid.interests.interestsScrollTopicsDownUp
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Generates a baseline profile which can be copied to `app/src/main/baseline-prof.txt`.
|
||||
*/
|
||||
class BaselineProfileGenerator {
|
||||
@get:Rule val baselineProfileRule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun generate() =
|
||||
baselineProfileRule.collect(PACKAGE_NAME) {
|
||||
// This block defines the app's critical user journey. Here we are interested in
|
||||
// optimizing for app startup. But you can also navigate and scroll
|
||||
// through your most important UI.
|
||||
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
|
||||
// Scroll the feed critical user journey
|
||||
forYouWaitForContent()
|
||||
forYouSelectTopics(true)
|
||||
forYouScrollFeedDownUp()
|
||||
|
||||
// Navigate to saved screen
|
||||
goToBookmarksScreen()
|
||||
|
||||
// Navigate to interests screen
|
||||
goToInterestsScreen()
|
||||
interestsScrollTopicsDownUp()
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.bookmarks
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
fun MacrobenchmarkScope.goToBookmarksScreen() {
|
||||
device.findObject(By.text("Saved")).click()
|
||||
device.waitForIdle()
|
||||
// Wait until saved title are shown on screen
|
||||
device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000)
|
||||
val topAppBar = device.findObject(By.res("niaTopAppBar"))
|
||||
topAppBar.wait(Until.hasObject(By.text("Saved")), 2_000)
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.foryou
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import androidx.test.uiautomator.untilHasChildren
|
||||
import com.google.samples.apps.nowinandroid.flingElementDownUp
|
||||
|
||||
fun MacrobenchmarkScope.forYouWaitForContent() {
|
||||
// Wait until content is loaded by checking if topics are loaded
|
||||
device.wait(Until.gone(By.res("loadingWheel")), 5_000)
|
||||
// Sometimes, the loading wheel is gone, but the content is not loaded yet
|
||||
// So we'll wait here for topics to be sure
|
||||
val obj = device.findObject(By.res("forYou:topicSelection"))
|
||||
// Timeout here is quite big, because sometimes data loading takes a long time!
|
||||
obj.wait(untilHasChildren(), 60_000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects some topics, which will show the feed content for them.
|
||||
* [recheckTopicsIfChecked] Topics may be already checked from the previous iteration.
|
||||
*/
|
||||
fun MacrobenchmarkScope.forYouSelectTopics(recheckTopicsIfChecked: Boolean = false) {
|
||||
val topics = device.findObject(By.res("forYou:topicSelection"))
|
||||
|
||||
// Set gesture margin from sides not to trigger system gesture navigation
|
||||
val horizontalMargin = 10 * topics.visibleBounds.width() / 100
|
||||
topics.setGestureMargins(horizontalMargin, 0, horizontalMargin, 0)
|
||||
|
||||
// Select some topics to show some feed content
|
||||
var index = 0
|
||||
var visited = 0
|
||||
|
||||
while (visited < 3) {
|
||||
// Selecting some topics, which will populate items in the feed.
|
||||
val topic = topics.children[index % topics.childCount]
|
||||
// Find the checkable element to figure out whether it's checked or not
|
||||
val topicCheckIcon = topic.findObject(By.checkable(true))
|
||||
// Topic icon may not be visible if it's out of the screen boundaries
|
||||
// If that's the case, let's try another index
|
||||
if (topicCheckIcon == null) {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
when {
|
||||
// Topic wasn't checked, so just do that
|
||||
!topicCheckIcon.isChecked -> {
|
||||
topic.click()
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
// Topic was checked already and we want to recheck it, so just do it twice
|
||||
recheckTopicsIfChecked -> {
|
||||
repeat(2) {
|
||||
topic.click()
|
||||
device.waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Topic is checked, but we don't recheck it
|
||||
}
|
||||
}
|
||||
|
||||
index++
|
||||
visited++
|
||||
}
|
||||
}
|
||||
|
||||
fun MacrobenchmarkScope.forYouScrollFeedDownUp() {
|
||||
val feedList = device.findObject(By.res("forYou:feed"))
|
||||
device.flingElementDownUp(feedList)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.foryou
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||
import com.google.samples.apps.nowinandroid.allowNotifications
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class ScrollForYouFeedBenchmark {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun scrollFeedCompilationNone() = scrollFeed(CompilationMode.None())
|
||||
|
||||
@Test
|
||||
fun scrollFeedCompilationBaselineProfile() = scrollFeed(CompilationMode.Partial())
|
||||
|
||||
private fun scrollFeed(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
|
||||
packageName = PACKAGE_NAME,
|
||||
metrics = listOf(FrameTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
iterations = 10,
|
||||
startupMode = StartupMode.COLD,
|
||||
setupBlock = {
|
||||
// Start the app
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
allowNotifications()
|
||||
},
|
||||
) {
|
||||
forYouWaitForContent()
|
||||
forYouSelectTopics()
|
||||
forYouScrollFeedDownUp()
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.interests
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import com.google.samples.apps.nowinandroid.flingElementDownUp
|
||||
|
||||
fun MacrobenchmarkScope.goToInterestsScreen() {
|
||||
device.findObject(By.text("Interests")).click()
|
||||
device.waitForIdle()
|
||||
// Wait until interests are shown on screen
|
||||
device.wait(Until.hasObject(By.res("niaTopAppBar")), 2_000)
|
||||
val topAppBar = device.findObject(By.res("niaTopAppBar"))
|
||||
topAppBar.wait(Until.hasObject(By.text("Interests")), 2_000)
|
||||
|
||||
// Wait until content is loaded by checking if interests are loaded
|
||||
device.wait(Until.gone(By.res("loadingWheel")), 5_000)
|
||||
}
|
||||
|
||||
fun MacrobenchmarkScope.interestsScrollTopicsDownUp() {
|
||||
val topicsList = device.findObject(By.res("interests:topics"))
|
||||
device.flingElementDownUp(topicsList)
|
||||
}
|
||||
|
||||
fun MacrobenchmarkScope.interestsWaitForTopics() {
|
||||
device.wait(Until.hasObject(By.text("Accessibility")), 30_000)
|
||||
}
|
||||
|
||||
fun MacrobenchmarkScope.interestsToggleBookmarked() {
|
||||
val topicsList = device.findObject(By.res("interests:topics"))
|
||||
val checkable = topicsList.findObject(By.checkable(true))
|
||||
checkable.click()
|
||||
device.waitForIdle()
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* 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.interests
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.uiautomator.By
|
||||
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||
import com.google.samples.apps.nowinandroid.allowNotifications
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ScrollTopicListBenchmark {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun benchmarkStateChangeCompilationBaselineProfile() =
|
||||
benchmarkStateChange(CompilationMode.Partial())
|
||||
|
||||
private fun benchmarkStateChange(compilationMode: CompilationMode) =
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = PACKAGE_NAME,
|
||||
metrics = listOf(FrameTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
iterations = 10,
|
||||
startupMode = StartupMode.WARM,
|
||||
setupBlock = {
|
||||
// Start the app
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
allowNotifications()
|
||||
// Navigate to interests screen
|
||||
device.findObject(By.text("Interests")).click()
|
||||
device.waitForIdle()
|
||||
},
|
||||
) {
|
||||
interestsWaitForTopics()
|
||||
repeat(3) {
|
||||
interestsScrollTopicsDownUp()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.interests
|
||||
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.uiautomator.By
|
||||
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||
import com.google.samples.apps.nowinandroid.allowNotifications
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TopicsScreenRecompositionBenchmark {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun benchmarkStateChangeCompilationBaselineProfile() =
|
||||
benchmarkStateChange(CompilationMode.Partial())
|
||||
|
||||
private fun benchmarkStateChange(compilationMode: CompilationMode) =
|
||||
benchmarkRule.measureRepeated(
|
||||
packageName = PACKAGE_NAME,
|
||||
metrics = listOf(FrameTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
iterations = 10,
|
||||
startupMode = StartupMode.WARM,
|
||||
setupBlock = {
|
||||
// Start the app
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
allowNotifications()
|
||||
// Navigate to interests screen
|
||||
device.findObject(By.text("Interests")).click()
|
||||
device.waitForIdle()
|
||||
},
|
||||
) {
|
||||
interestsWaitForTopics()
|
||||
repeat(3) {
|
||||
interestsToggleBookmarked()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.startup
|
||||
|
||||
import androidx.benchmark.macro.BaselineProfileMode.Disable
|
||||
import androidx.benchmark.macro.BaselineProfileMode.Require
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupMode.COLD
|
||||
import androidx.benchmark.macro.StartupMode.HOT
|
||||
import androidx.benchmark.macro.StartupMode.WARM
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
|
||||
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
|
||||
import com.google.samples.apps.nowinandroid.foryou.forYouWaitForContent
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a cold state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class ColdStartupBenchmark : AbstractStartupBenchmark(COLD)
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a warm state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class WarmStartupBenchmark : AbstractStartupBenchmark(WARM)
|
||||
|
||||
/**
|
||||
* Run this benchmark from Studio to see startup measurements, and captured system traces
|
||||
* for investigating your app's performance from a hot state.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4ClassRunner::class)
|
||||
class HotStartupBenchmark : AbstractStartupBenchmark(HOT)
|
||||
|
||||
/**
|
||||
* Base class for benchmarks with different startup modes.
|
||||
* Enables app startups from various states of baseline profile or [CompilationMode]s.
|
||||
*/
|
||||
abstract class AbstractStartupBenchmark(private val startupMode: StartupMode) {
|
||||
@get:Rule
|
||||
val benchmarkRule = MacrobenchmarkRule()
|
||||
|
||||
@Test
|
||||
fun startupNoCompilation() = startup(CompilationMode.None())
|
||||
|
||||
@Test
|
||||
fun startupBaselineProfileDisabled() = startup(
|
||||
CompilationMode.Partial(baselineProfileMode = Disable, warmupIterations = 1),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun startupBaselineProfile() = startup(CompilationMode.Partial(baselineProfileMode = Require))
|
||||
|
||||
@Test
|
||||
fun startupFullCompilation() = startup(CompilationMode.Full())
|
||||
|
||||
private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated(
|
||||
packageName = PACKAGE_NAME,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
iterations = 10,
|
||||
startupMode = startupMode,
|
||||
setupBlock = {
|
||||
pressHome()
|
||||
},
|
||||
) {
|
||||
startActivityAndWait()
|
||||
// Waits until the content is ready to capture Time To Full Display
|
||||
forYouWaitForContent()
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
# Convention Plugins
|
||||
|
||||
The `build-logic` folder defines project-specific convention plugins, used to keep a single
|
||||
source of truth for common module configurations.
|
||||
|
||||
This approach is heavily based on
|
||||
[https://developer.squareup.com/blog/herding-elephants/](https://developer.squareup.com/blog/herding-elephants/)
|
||||
and
|
||||
[https://github.com/jjohannes/idiomatic-gradle](https://github.com/jjohannes/idiomatic-gradle).
|
||||
|
||||
By setting up convention plugins in `build-logic`, we can avoid duplicated build script setup,
|
||||
messy `subproject` configurations, without the pitfalls of the `buildSrc` directory.
|
||||
|
||||
`build-logic` is an included build, as configured in the root
|
||||
[`settings.gradle.kts`](../settings.gradle.kts).
|
||||
|
||||
Inside `build-logic` is a `convention` module, which defines a set of plugins that all normal
|
||||
modules can use to configure themselves.
|
||||
|
||||
`build-logic` also includes a set of `Kotlin` files used to share logic between plugins themselves,
|
||||
which is most useful for configuring Android components (libraries vs applications) with shared
|
||||
code.
|
||||
|
||||
These plugins are *additive* and *composable*, and try to only accomplish a single responsibility.
|
||||
Modules can then pick and choose the configurations they need.
|
||||
If there is one-off logic for a module without shared code, it's preferable to define that directly
|
||||
in the module's `build.gradle`, as opposed to creating a convention plugin with module-specific
|
||||
setup.
|
||||
|
||||
Current list of convention plugins:
|
||||
|
||||
- [`nowinandroid.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),
|
||||
[`nowinandroid.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),
|
||||
[`nowinandroid.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):
|
||||
Configures common Android and Kotlin options.
|
||||
- [`nowinandroid.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),
|
||||
[`nowinandroid.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):
|
||||
Configures Jetpack Compose options
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||
import com.google.samples.apps.nowinandroid.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.google.gms.google-services")
|
||||
apply("com.google.firebase.firebase-perf")
|
||||
apply("com.google.firebase.crashlytics")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val bom = libs.findLibrary("firebase-bom").get()
|
||||
add("implementation", platform(bom))
|
||||
"implementation"(libs.findLibrary("firebase.analytics").get())
|
||||
"implementation"(libs.findLibrary("firebase.performance").get())
|
||||
"implementation"(libs.findLibrary("firebase.crashlytics").get())
|
||||
}
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
buildTypes.configureEach {
|
||||
// Disable the Crashlytics mapping file upload. This feature should only be
|
||||
// enabled if a Firebase backend is available and configured in
|
||||
// google-services.json.
|
||||
configure<CrashlyticsExtension> {
|
||||
mappingFileUploadEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# IGNORE this file, it's only used in the internal Google release process
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
APP_OUT=$DIR/app/build/outputs
|
||||
|
||||
export JAVA_HOME="$(cd $DIR/../nowinandroid-prebuilts/jdk17/linux && pwd )"
|
||||
echo "JAVA_HOME=$JAVA_HOME"
|
||||
|
||||
export ANDROID_HOME="$(cd $DIR/../../../prebuilts/fullsdk/linux && pwd )"
|
||||
echo "ANDROID_HOME=$ANDROID_HOME"
|
||||
|
||||
echo "Copying google-services.json"
|
||||
cp $DIR/../nowinandroid-prebuilts/google-services.json $DIR/app
|
||||
|
||||
echo "Copying local.properties"
|
||||
cp $DIR/../nowinandroid-prebuilts/local.properties $DIR
|
||||
|
||||
cd $DIR
|
||||
|
||||
# Build the prodRelease variant
|
||||
GRADLE_PARAMS=" --stacktrace -Puse-google-services"
|
||||
$DIR/gradlew :app:clean :app:assembleProdRelease :app:bundleProdRelease ${GRADLE_PARAMS}
|
||||
BUILD_RESULT=$?
|
||||
|
||||
# Prod release apk
|
||||
cp $APP_OUT/apk/prod/release/app-prod-release.apk $DIST_DIR/app-prod-release.apk
|
||||
# Prod release bundle
|
||||
cp $APP_OUT/bundle/prodRelease/app-prod-release.aab $DIST_DIR/app-prod-release.aab
|
||||
# Prod release bundle mapping
|
||||
cp $APP_OUT/mapping/prodRelease/mapping.txt $DIST_DIR/mobile-release-aab-mapping.txt
|
||||
|
||||
exit $BUILD_RESULT
|
@ -1 +0,0 @@
|
||||
/build
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
plugins {
|
||||
id("nowinandroid.android.library")
|
||||
id("nowinandroid.android.library.compose")
|
||||
id("nowinandroid.android.hilt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.core.analytics"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class AnalyticsModule {
|
||||
@Binds
|
||||
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: StubAnalyticsHelper): AnalyticsHelper
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?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 />
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
/**
|
||||
* Represents an analytics event.
|
||||
*
|
||||
* @param type - the event type. Wherever possible use one of the standard
|
||||
* event `Types`, however, if there is no suitable event type already defined, a custom event can be
|
||||
* defined as long as it is configured in your backend analytics system (for example, by creating a
|
||||
* Firebase Analytics custom event).
|
||||
*
|
||||
* @param extras - list of parameters which supply additional context to the event. See `Param`.
|
||||
*/
|
||||
data class AnalyticsEvent(
|
||||
val type: String,
|
||||
val extras: List<Param> = emptyList(),
|
||||
) {
|
||||
// Standard analytics types.
|
||||
class Types {
|
||||
companion object {
|
||||
const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A key-value pair used to supply extra context to an analytics event.
|
||||
*
|
||||
* @param key - the parameter key. Wherever possible use one of the standard `ParamKeys`,
|
||||
* however, if no suitable key is available you can define your own as long as it is configured
|
||||
* in your backend analytics system (for example, by creating a Firebase Analytics custom
|
||||
* parameter).
|
||||
*
|
||||
* @param value - the parameter value.
|
||||
*/
|
||||
data class Param(val key: String, val value: String)
|
||||
|
||||
// Standard parameter keys.
|
||||
class ParamKeys {
|
||||
companion object {
|
||||
const val SCREEN_NAME = "screen_name"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
/**
|
||||
* Interface for logging analytics events. See `FirebaseAnalyticsHelper` and
|
||||
* `StubAnalyticsHelper` for implementations.
|
||||
*/
|
||||
interface AnalyticsHelper {
|
||||
fun logEvent(event: AnalyticsEvent)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
/**
|
||||
* Implementation of AnalyticsHelper which does nothing. Useful for tests and previews.
|
||||
*/
|
||||
class NoOpAnalyticsHelper : AnalyticsHelper {
|
||||
override fun logEvent(event: AnalyticsEvent) = Unit
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
import android.util.Log
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val TAG = "StubAnalyticsHelper"
|
||||
|
||||
/**
|
||||
* An implementation of AnalyticsHelper just writes the events to logcat. Used in builds where no
|
||||
* analytics events should be sent to a backend.
|
||||
*/
|
||||
@Singleton
|
||||
class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
|
||||
override fun logEvent(event: AnalyticsEvent) {
|
||||
Log.d(TAG, "Received analytics event: $event")
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/**
|
||||
* Global key used to obtain access to the AnalyticsHelper through a CompositionLocal.
|
||||
*/
|
||||
val LocalAnalyticsHelper = staticCompositionLocalOf<AnalyticsHelper> {
|
||||
// Provide a default AnalyticsHelper which does nothing. This is so that tests and previews
|
||||
// do not have to provide one. For real app builds provide a different implementation.
|
||||
NoOpAnalyticsHelper()
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class AnalyticsModule {
|
||||
@Binds
|
||||
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFirebaseAnalytics(): FirebaseAnalytics { return Firebase.analytics }
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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.core.analytics
|
||||
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.ktx.logEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of `AnalyticsHelper` which logs events to a Firebase backend.
|
||||
*/
|
||||
class FirebaseAnalyticsHelper @Inject constructor(
|
||||
private val firebaseAnalytics: FirebaseAnalytics,
|
||||
) : AnalyticsHelper {
|
||||
|
||||
override fun logEvent(event: AnalyticsEvent) {
|
||||
firebaseAnalytics.logEvent(event.type) {
|
||||
for (extra in event.extras) {
|
||||
// Truncate parameter keys and values according to firebase maximum length values.
|
||||
param(
|
||||
key = extra.key.take(40),
|
||||
value = extra.value.take(100),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# :core:common module
|
||||
|
||||
![Dependency graph](../../docs/images/graphs/dep_graph_core_common.png)
|
@ -1 +0,0 @@
|
||||
/build
|
@ -1,3 +0,0 @@
|
||||
# :core:data-test module
|
||||
|
||||
![Dependency graph](../../docs/images/graphs/dep_graph_core_data_test.png)
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("nowinandroid.android.library")
|
||||
id("nowinandroid.android.hilt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.samples.apps.nowinandroid.core.data.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":core:data"))
|
||||
implementation(project(":core:testing"))
|
||||
implementation(project(":core:common"))
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.test
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import javax.inject.Inject
|
||||
|
||||
class AlwaysOnlineNetworkMonitor @Inject constructor() : NetworkMonitor {
|
||||
override val isOnline: Flow<Boolean> = flowOf(true)
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.test
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.di.DataModule
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||
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.fake.FakeNewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeRecentSearchRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeSearchContentsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeTopicsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [DataModule::class],
|
||||
)
|
||||
interface TestDataModule {
|
||||
@Binds
|
||||
fun bindsTopicRepository(
|
||||
fakeTopicsRepository: FakeTopicsRepository,
|
||||
): TopicsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsNewsResourceRepository(
|
||||
fakeNewsRepository: FakeNewsRepository,
|
||||
): NewsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsUserDataRepository(
|
||||
userDataRepository: FakeUserDataRepository,
|
||||
): UserDataRepository
|
||||
|
||||
@Binds
|
||||
fun bindsRecentSearchRepository(
|
||||
recentSearchRepository: FakeRecentSearchRepository,
|
||||
): RecentSearchRepository
|
||||
|
||||
@Binds
|
||||
fun bindsSearchContentsRepository(
|
||||
searchContentsRepository: FakeSearchContentsRepository,
|
||||
): SearchContentsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsNetworkMonitor(
|
||||
networkMonitor: AlwaysOnlineNetworkMonitor,
|
||||
): NetworkMonitor
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# :core:data module
|
||||
|
||||
![Dependency graph](../../docs/images/graphs/dep_graph_core_data.png)
|
@ -1,132 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data
|
||||
|
||||
import android.util.Log
|
||||
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
|
||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Interface marker for a class that manages synchronization between local data and a remote
|
||||
* source for a [Syncable].
|
||||
*/
|
||||
interface Synchronizer {
|
||||
suspend fun getChangeListVersions(): ChangeListVersions
|
||||
|
||||
suspend fun updateChangeListVersions(update: ChangeListVersions.() -> ChangeListVersions)
|
||||
|
||||
/**
|
||||
* Syntactic sugar to call [Syncable.syncWith] while omitting the synchronizer argument
|
||||
*/
|
||||
suspend fun Syncable.sync() = this@sync.syncWith(this@Synchronizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface marker for a class that is synchronized with a remote source. Syncing must not be
|
||||
* performed concurrently and it is the [Synchronizer]'s responsibility to ensure this.
|
||||
*/
|
||||
interface Syncable {
|
||||
/**
|
||||
* Synchronizes the local database backing the repository with the network.
|
||||
* Returns if the sync was successful or not.
|
||||
*/
|
||||
suspend fun syncWith(synchronizer: Synchronizer): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts [block], returning a successful [Result] if it succeeds, otherwise a [Result.Failure]
|
||||
* taking care not to break structured concurrency
|
||||
*/
|
||||
private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
|
||||
Result.success(block())
|
||||
} catch (cancellationException: CancellationException) {
|
||||
throw cancellationException
|
||||
} catch (exception: Exception) {
|
||||
Log.i(
|
||||
"suspendRunCatching",
|
||||
"Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
|
||||
exception,
|
||||
)
|
||||
Result.failure(exception)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for syncing a repository with the network.
|
||||
* [versionReader] Reads the current version of the model that needs to be synced
|
||||
* [changeListFetcher] Fetches the change list for the model
|
||||
* [versionUpdater] Updates the [ChangeListVersions] after a successful sync
|
||||
* [modelDeleter] Deletes models by consuming the ids of the models that have been deleted.
|
||||
* [modelUpdater] Updates models by consuming the ids of the models that have changed.
|
||||
*
|
||||
* Note that the blocks defined above are never run concurrently, and the [Synchronizer]
|
||||
* implementation must guarantee this.
|
||||
*/
|
||||
suspend fun Synchronizer.changeListSync(
|
||||
versionReader: (ChangeListVersions) -> Int,
|
||||
changeListFetcher: suspend (Int) -> List<NetworkChangeList>,
|
||||
versionUpdater: ChangeListVersions.(Int) -> ChangeListVersions,
|
||||
modelDeleter: suspend (List<String>) -> Unit,
|
||||
modelUpdater: suspend (List<String>) -> Unit,
|
||||
) = suspendRunCatching {
|
||||
// Fetch the change list since last sync (akin to a git fetch)
|
||||
val currentVersion = versionReader(getChangeListVersions())
|
||||
val changeList = changeListFetcher(currentVersion)
|
||||
if (changeList.isEmpty()) return@suspendRunCatching true
|
||||
|
||||
val (deleted, updated) = changeList.partition(NetworkChangeList::isDelete)
|
||||
|
||||
// Delete models that have been deleted server-side
|
||||
modelDeleter(deleted.map(NetworkChangeList::id))
|
||||
|
||||
// Using the change list, pull down and save the changes (akin to a git pull)
|
||||
modelUpdater(updated.map(NetworkChangeList::id))
|
||||
|
||||
// Update the last synced version (akin to updating local git HEAD)
|
||||
val latestVersion = changeList.last().changeListVersion
|
||||
updateChangeListVersions {
|
||||
versionUpdater(latestVersion)
|
||||
}
|
||||
}.isSuccess
|
||||
|
||||
/**
|
||||
* Returns a [Flow] whose values are generated by [transform] function that process the most
|
||||
* recently emitted values by each flow.
|
||||
*/
|
||||
fun <T1, T2, T3, T4, T5, T6, R> combine(
|
||||
flow: Flow<T1>,
|
||||
flow2: Flow<T2>,
|
||||
flow3: Flow<T3>,
|
||||
flow4: Flow<T4>,
|
||||
flow5: Flow<T5>,
|
||||
flow6: Flow<T6>,
|
||||
transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
|
||||
): Flow<R> = combine(
|
||||
combine(flow, flow2, flow3, ::Triple),
|
||||
combine(flow4, flow5, flow6, ::Triple),
|
||||
) { t1, t2 ->
|
||||
transform(
|
||||
t1.first,
|
||||
t1.second,
|
||||
t1.third,
|
||||
t2.first,
|
||||
t2.second,
|
||||
t2.third,
|
||||
)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.di
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.DefaultRecentSearchRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.DefaultSearchContentsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstNewsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstTopicsRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstUserDataRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||
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.util.ConnectivityManagerNetworkMonitor
|
||||
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DataModule {
|
||||
|
||||
@Binds
|
||||
fun bindsTopicRepository(
|
||||
topicsRepository: OfflineFirstTopicsRepository,
|
||||
): TopicsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsNewsResourceRepository(
|
||||
newsRepository: OfflineFirstNewsRepository,
|
||||
): NewsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsUserDataRepository(
|
||||
userDataRepository: OfflineFirstUserDataRepository,
|
||||
): UserDataRepository
|
||||
|
||||
@Binds
|
||||
fun bindsRecentSearchRepository(
|
||||
recentSearchRepository: DefaultRecentSearchRepository,
|
||||
): RecentSearchRepository
|
||||
|
||||
@Binds
|
||||
fun bindsSearchContentsRepository(
|
||||
searchContentsRepository: DefaultSearchContentsRepository,
|
||||
): SearchContentsRepository
|
||||
|
||||
@Binds
|
||||
fun bindsNetworkMonitor(
|
||||
networkMonitor: ConnectivityManagerNetworkMonitor,
|
||||
): NetworkMonitor
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.di
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
||||
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface UserNewsResourceRepositoryModule {
|
||||
@Binds
|
||||
fun bindsUserNewsResourceRepository(
|
||||
userDataRepository: CompositeUserNewsResourceRepository,
|
||||
): UserNewsResourceRepository
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.model
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
data class RecentSearchQuery(
|
||||
val query: String,
|
||||
val queriedDate: Instant = Clock.System.now(),
|
||||
)
|
||||
|
||||
fun RecentSearchQueryEntity.asExternalModel() = RecentSearchQuery(
|
||||
query = query,
|
||||
queriedDate = queriedDate,
|
||||
)
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.model
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
|
||||
|
||||
fun NetworkTopic.asEntity() = TopicEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
shortDescription = shortDescription,
|
||||
longDescription = longDescription,
|
||||
url = url,
|
||||
imageUrl = imageUrl,
|
||||
)
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent
|
||||
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Param
|
||||
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
|
||||
|
||||
fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
|
||||
val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
|
||||
val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
|
||||
logEvent(
|
||||
AnalyticsEvent(
|
||||
type = eventType,
|
||||
extras = listOf(
|
||||
Param(key = paramKey, value = newsResourceId),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: Boolean) {
|
||||
val eventType = if (isFollowed) "topic_followed" else "topic_unfollowed"
|
||||
val paramKey = if (isFollowed) "followed_topic_id" else "unfollowed_topic_id"
|
||||
logEvent(
|
||||
AnalyticsEvent(
|
||||
type = eventType,
|
||||
extras = listOf(
|
||||
Param(key = paramKey, value = followedTopicId),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun AnalyticsHelper.logThemeChanged(themeName: String) =
|
||||
logEvent(
|
||||
AnalyticsEvent(
|
||||
type = "theme_changed",
|
||||
extras = listOf(
|
||||
Param(key = "theme_name", value = themeName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) =
|
||||
logEvent(
|
||||
AnalyticsEvent(
|
||||
type = "dark_theme_config_changed",
|
||||
extras = listOf(
|
||||
Param(key = "dark_theme_config", value = darkThemeConfigName),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) =
|
||||
logEvent(
|
||||
AnalyticsEvent(
|
||||
type = "dynamic_color_preference_changed",
|
||||
extras = listOf(
|
||||
Param(key = "dynamic_color_preference", value = useDynamicColor.toString()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun AnalyticsHelper.logOnboardingStateChanged(shouldHideOnboarding: Boolean) {
|
||||
val eventType = if (shouldHideOnboarding) "onboarding_complete" else "onboarding_reset"
|
||||
logEvent(
|
||||
AnalyticsEvent(type = eventType),
|
||||
)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implements a [UserNewsResourceRepository] by combining a [NewsRepository] with a
|
||||
* [UserDataRepository].
|
||||
*/
|
||||
class CompositeUserNewsResourceRepository @Inject constructor(
|
||||
val newsRepository: NewsRepository,
|
||||
val userDataRepository: UserDataRepository,
|
||||
) : UserNewsResourceRepository {
|
||||
|
||||
/**
|
||||
* Returns available news resources (joined with user data) matching the given query.
|
||||
*/
|
||||
override fun observeAll(
|
||||
query: NewsResourceQuery,
|
||||
): Flow<List<UserNewsResource>> =
|
||||
newsRepository.getNewsResources(query)
|
||||
.combine(userDataRepository.userData) { newsResources, userData ->
|
||||
newsResources.mapToUserNewsResources(userData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns available news resources (joined with user data) for the followed topics.
|
||||
*/
|
||||
override fun observeAllForFollowedTopics(): Flow<List<UserNewsResource>> =
|
||||
userDataRepository.userData.map { it.followedTopics }.distinctUntilChanged()
|
||||
.flatMapLatest { followedTopics ->
|
||||
when {
|
||||
followedTopics.isEmpty() -> flowOf(emptyList())
|
||||
else -> observeAll(NewsResourceQuery(filterTopicIds = followedTopics))
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeAllBookmarked(): Flow<List<UserNewsResource>> =
|
||||
userDataRepository.userData.map { it.bookmarkedNewsResources }.distinctUntilChanged()
|
||||
.flatMapLatest { bookmarkedNewsResources ->
|
||||
when {
|
||||
bookmarkedNewsResources.isEmpty() -> flowOf(emptyList())
|
||||
else -> observeAll(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
||||
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultRecentSearchRepository @Inject constructor(
|
||||
private val recentSearchQueryDao: RecentSearchQueryDao,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
) : RecentSearchRepository {
|
||||
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) {
|
||||
withContext(ioDispatcher) {
|
||||
recentSearchQueryDao.insertOrReplaceRecentSearchQuery(
|
||||
RecentSearchQueryEntity(
|
||||
query = searchQuery,
|
||||
queriedDate = Clock.System.now(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
|
||||
recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries ->
|
||||
searchQueries.map {
|
||||
it.asExternalModel()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries()
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
|
||||
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
||||
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultSearchContentsRepository @Inject constructor(
|
||||
private val newsResourceDao: NewsResourceDao,
|
||||
private val newsResourceFtsDao: NewsResourceFtsDao,
|
||||
private val topicDao: TopicDao,
|
||||
private val topicFtsDao: TopicFtsDao,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
) : SearchContentsRepository {
|
||||
|
||||
override suspend fun populateFtsData() {
|
||||
withContext(ioDispatcher) {
|
||||
newsResourceFtsDao.insertAll(
|
||||
newsResourceDao.getNewsResources(
|
||||
useFilterTopicIds = false,
|
||||
useFilterNewsIds = false,
|
||||
)
|
||||
.first()
|
||||
.map(PopulatedNewsResource::asFtsEntity),
|
||||
)
|
||||
topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() })
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||
// Surround the query by asterisks to match the query when it's in the middle of
|
||||
// a word
|
||||
val newsResourceIds = newsResourceFtsDao.searchAllNewsResources("*$searchQuery*")
|
||||
val topicIds = topicFtsDao.searchAllTopics("*$searchQuery*")
|
||||
|
||||
val newsResourcesFlow = newsResourceIds
|
||||
.mapLatest { it.toSet() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
newsResourceDao.getNewsResources(useFilterNewsIds = true, filterNewsIds = it)
|
||||
}
|
||||
val topicsFlow = topicIds
|
||||
.mapLatest { it.toSet() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest(topicDao::getTopicEntities)
|
||||
return combine(newsResourcesFlow, topicsFlow) { newsResources, topics ->
|
||||
SearchResult(
|
||||
topics = topics.map { it.asExternalModel() },
|
||||
newsResources = newsResources.map { it.asExternalModel() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSearchContentsCount(): Flow<Int> =
|
||||
combine(
|
||||
newsResourceFtsDao.getCount(),
|
||||
topicFtsDao.getCount(),
|
||||
) { newsResourceCount, topicsCount ->
|
||||
newsResourceCount + topicsCount
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
|
||||
import com.google.samples.apps.nowinandroid.core.data.changeListSync
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.asEntity
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.topicCrossReferences
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.topicEntityShells
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
||||
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
|
||||
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
|
||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
|
||||
import com.google.samples.apps.nowinandroid.core.notifications.Notifier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
// Heuristic value to optimize for serialization and deserialization cost on client and server
|
||||
// for each news resource batch.
|
||||
private const val SYNC_BATCH_SIZE = 40
|
||||
|
||||
/**
|
||||
* Disk storage backed implementation of the [NewsRepository].
|
||||
* Reads are exclusively from local storage to support offline access.
|
||||
*/
|
||||
class OfflineFirstNewsRepository @Inject constructor(
|
||||
private val niaPreferencesDataSource: NiaPreferencesDataSource,
|
||||
private val newsResourceDao: NewsResourceDao,
|
||||
private val topicDao: TopicDao,
|
||||
private val network: NiaNetworkDataSource,
|
||||
private val notifier: Notifier,
|
||||
) : NewsRepository {
|
||||
|
||||
override fun getNewsResources(
|
||||
query: NewsResourceQuery,
|
||||
): Flow<List<NewsResource>> = newsResourceDao.getNewsResources(
|
||||
useFilterTopicIds = query.filterTopicIds != null,
|
||||
filterTopicIds = query.filterTopicIds ?: emptySet(),
|
||||
useFilterNewsIds = query.filterNewsIds != null,
|
||||
filterNewsIds = query.filterNewsIds ?: emptySet(),
|
||||
)
|
||||
.map { it.map(PopulatedNewsResource::asExternalModel) }
|
||||
|
||||
override suspend fun syncWith(synchronizer: Synchronizer): Boolean {
|
||||
var isFirstSync = false
|
||||
return synchronizer.changeListSync(
|
||||
versionReader = ChangeListVersions::newsResourceVersion,
|
||||
changeListFetcher = { currentVersion ->
|
||||
isFirstSync = currentVersion <= 0
|
||||
network.getNewsResourceChangeList(after = currentVersion)
|
||||
},
|
||||
versionUpdater = { latestVersion ->
|
||||
copy(newsResourceVersion = latestVersion)
|
||||
},
|
||||
modelDeleter = newsResourceDao::deleteNewsResources,
|
||||
modelUpdater = { changedIds ->
|
||||
val userData = niaPreferencesDataSource.userData.first()
|
||||
val hasOnboarded = userData.shouldHideOnboarding
|
||||
val followedTopicIds = userData.followedTopics
|
||||
|
||||
// TODO: Make this more efficient, there is no need to retrieve populated
|
||||
// news resources when all that's needed are the ids
|
||||
val existingNewsResourceIdsThatHaveChanged = when {
|
||||
hasOnboarded -> newsResourceDao.getNewsResources(
|
||||
useFilterTopicIds = true,
|
||||
filterTopicIds = followedTopicIds,
|
||||
useFilterNewsIds = true,
|
||||
filterNewsIds = changedIds.toSet(),
|
||||
)
|
||||
.first()
|
||||
.map { it.entity.id }
|
||||
.toSet()
|
||||
// No need to retrieve anything if notifications won't be sent
|
||||
else -> emptySet()
|
||||
}
|
||||
|
||||
if (isFirstSync) {
|
||||
// When we first retrieve news, mark everything viewed, so that we aren't
|
||||
// overwhelmed with all historical news.
|
||||
niaPreferencesDataSource.setNewsResourcesViewed(changedIds, true)
|
||||
}
|
||||
|
||||
// Obtain the news resources which have changed from the network and upsert them locally
|
||||
changedIds.chunked(SYNC_BATCH_SIZE).forEach { chunkedIds ->
|
||||
val networkNewsResources = network.getNewsResources(ids = chunkedIds)
|
||||
|
||||
// Order of invocation matters to satisfy id and foreign key constraints!
|
||||
|
||||
topicDao.insertOrIgnoreTopics(
|
||||
topicEntities = networkNewsResources
|
||||
.map(NetworkNewsResource::topicEntityShells)
|
||||
.flatten()
|
||||
.distinctBy(TopicEntity::id),
|
||||
)
|
||||
newsResourceDao.upsertNewsResources(
|
||||
newsResourceEntities = networkNewsResources.map(
|
||||
NetworkNewsResource::asEntity,
|
||||
),
|
||||
)
|
||||
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
|
||||
newsResourceTopicCrossReferences = networkNewsResources
|
||||
.map(NetworkNewsResource::topicCrossReferences)
|
||||
.distinct()
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
|
||||
if (hasOnboarded) {
|
||||
val addedNewsResources = newsResourceDao.getNewsResources(
|
||||
useFilterTopicIds = true,
|
||||
filterTopicIds = followedTopicIds,
|
||||
useFilterNewsIds = true,
|
||||
filterNewsIds = changedIds.toSet() - existingNewsResourceIdsThatHaveChanged,
|
||||
)
|
||||
.first()
|
||||
.map(PopulatedNewsResource::asExternalModel)
|
||||
|
||||
if (addedNewsResources.isNotEmpty()) {
|
||||
notifier.postNewsNotifications(
|
||||
newsResources = addedNewsResources,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
|
||||
import com.google.samples.apps.nowinandroid.core.data.changeListSync
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.asEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
|
||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
||||
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
|
||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Disk storage backed implementation of the [TopicsRepository].
|
||||
* Reads are exclusively from local storage to support offline access.
|
||||
*/
|
||||
class OfflineFirstTopicsRepository @Inject constructor(
|
||||
private val topicDao: TopicDao,
|
||||
private val network: NiaNetworkDataSource,
|
||||
) : TopicsRepository {
|
||||
|
||||
override fun getTopics(): Flow<List<Topic>> =
|
||||
topicDao.getTopicEntities()
|
||||
.map { it.map(TopicEntity::asExternalModel) }
|
||||
|
||||
override fun getTopic(id: String): Flow<Topic> =
|
||||
topicDao.getTopicEntity(id).map { it.asExternalModel() }
|
||||
|
||||
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
|
||||
synchronizer.changeListSync(
|
||||
versionReader = ChangeListVersions::topicVersion,
|
||||
changeListFetcher = { currentVersion ->
|
||||
network.getTopicChangeList(after = currentVersion)
|
||||
},
|
||||
versionUpdater = { latestVersion ->
|
||||
copy(topicVersion = latestVersion)
|
||||
},
|
||||
modelDeleter = topicDao::deleteTopics,
|
||||
modelUpdater = { changedIds ->
|
||||
val networkTopics = network.getTopics(ids = changedIds)
|
||||
topicDao.upsertTopics(
|
||||
entities = networkTopics.map(NetworkTopic::asEntity),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.samples.apps.nowinandroid.core.data.repository
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
|
||||
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class OfflineFirstUserDataRepository @Inject constructor(
|
||||
private val niaPreferencesDataSource: NiaPreferencesDataSource,
|
||||
private val analyticsHelper: AnalyticsHelper,
|
||||
) : UserDataRepository {
|
||||
|
||||
override val userData: Flow<UserData> =
|
||||
niaPreferencesDataSource.userData
|
||||
|
||||
@VisibleForTesting
|
||||
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
|
||||
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
|
||||
|
||||
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
|
||||
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
|
||||
analyticsHelper.logTopicFollowToggled(followedTopicId, followed)
|
||||
}
|
||||
|
||||
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
|
||||
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
|
||||
analyticsHelper.logNewsResourceBookmarkToggled(
|
||||
newsResourceId = newsResourceId,
|
||||
isBookmarked = bookmarked,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) =
|
||||
niaPreferencesDataSource.setNewsResourceViewed(newsResourceId, viewed)
|
||||
|
||||
override suspend fun setThemeBrand(themeBrand: ThemeBrand) {
|
||||
niaPreferencesDataSource.setThemeBrand(themeBrand)
|
||||
analyticsHelper.logThemeChanged(themeBrand.name)
|
||||
}
|
||||
|
||||
override suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
|
||||
niaPreferencesDataSource.setDarkThemeConfig(darkThemeConfig)
|
||||
analyticsHelper.logDarkThemeConfigChanged(darkThemeConfig.name)
|
||||
}
|
||||
|
||||
override suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
|
||||
niaPreferencesDataSource.setDynamicColorPreference(useDynamicColor)
|
||||
analyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor)
|
||||
}
|
||||
|
||||
override suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
|
||||
niaPreferencesDataSource.setShouldHideOnboarding(shouldHideOnboarding)
|
||||
analyticsHelper.logOnboardingStateChanged(shouldHideOnboarding)
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Data layer interface for the recent searches.
|
||||
*/
|
||||
interface RecentSearchRepository {
|
||||
|
||||
/**
|
||||
* Get the recent search queries up to the number of queries specified as [limit].
|
||||
*/
|
||||
fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>>
|
||||
|
||||
/**
|
||||
* Insert or replace the [searchQuery] as part of the recent searches.
|
||||
*/
|
||||
suspend fun insertOrReplaceRecentSearch(searchQuery: String)
|
||||
|
||||
/**
|
||||
* Clear the recent searches.
|
||||
*/
|
||||
suspend fun clearRecentSearches()
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Data layer interface for the search feature.
|
||||
*/
|
||||
interface SearchContentsRepository {
|
||||
|
||||
/**
|
||||
* Populate the fts tables for the search contents.
|
||||
*/
|
||||
suspend fun populateFtsData()
|
||||
|
||||
/**
|
||||
* Query the contents matched with the [searchQuery] and returns it as a [Flow] of [SearchResult]
|
||||
*/
|
||||
fun searchContents(searchQuery: String): Flow<SearchResult>
|
||||
|
||||
fun getSearchContentsCount(): Flow<Int>
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue