├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── ci-gradle.properties ├── scripts │ └── gradlew_recursive.sh └── workflows │ ├── android.yml │ ├── copy-branch.yml │ └── update_deps.yml ├── .gitignore ├── .google └── packaging.yaml ├── .idea └── copyright │ ├── google.xml │ └── profiles_settings.xml ├── ASSETS_LICENSE ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules-benchmark.pro ├── proguard-rules.pro └── src │ ├── androidTest │ ├── assets │ │ └── plants.json │ ├── java │ │ └── com │ │ │ └── google │ │ │ └── samples │ │ │ └── apps │ │ │ └── sunflower │ │ │ ├── GardenActivityTest.kt │ │ │ ├── MainCoroutineRule.kt │ │ │ ├── compose │ │ │ ├── garden │ │ │ │ └── GardenTest.kt │ │ │ ├── plantdetail │ │ │ │ └── PlantDetailComposeTest.kt │ │ │ └── plantlist │ │ │ │ └── PlantListTest.kt │ │ │ ├── data │ │ │ ├── GardenPlantingDaoTest.kt │ │ │ └── PlantDaoTest.kt │ │ │ ├── utilities │ │ │ ├── LiveDataTestUtil.kt │ │ │ ├── MainTestRunner.kt │ │ │ └── TestUtils.kt │ │ │ ├── viewmodels │ │ │ └── PlantDetailViewModelTest.kt │ │ │ └── worker │ │ │ └── SeedDatabaseWorkerTest.kt │ └── res │ │ └── raw │ │ └── apple.jpg │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── plants.json │ ├── baseline-prof.txt │ ├── java │ │ └── com │ │ │ └── google │ │ │ └── samples │ │ │ └── apps │ │ │ └── sunflower │ │ │ ├── GardenActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── api │ │ │ └── UnsplashService.kt │ │ │ ├── compose │ │ │ ├── Dimens.kt │ │ │ ├── Modifiers.kt │ │ │ ├── Screen.kt │ │ │ ├── SunflowerApp.kt │ │ │ ├── gallery │ │ │ │ └── GalleryScreen.kt │ │ │ ├── garden │ │ │ │ └── GardenScreen.kt │ │ │ ├── home │ │ │ │ └── HomeScreen.kt │ │ │ ├── plantdetail │ │ │ │ ├── PlantDetailScroller.kt │ │ │ │ └── PlantDetailView.kt │ │ │ ├── plantlist │ │ │ │ ├── PlantListItemView.kt │ │ │ │ └── PlantListScreen.kt │ │ │ └── utils │ │ │ │ └── TextSnackbarContainer.kt │ │ │ ├── data │ │ │ ├── AppDatabase.kt │ │ │ ├── Converters.kt │ │ │ ├── GardenPlanting.kt │ │ │ ├── GardenPlantingDao.kt │ │ │ ├── GardenPlantingRepository.kt │ │ │ ├── Plant.kt │ │ │ ├── PlantAndGardenPlantings.kt │ │ │ ├── PlantDao.kt │ │ │ ├── PlantRepository.kt │ │ │ ├── UnsplashPagingSource.kt │ │ │ ├── UnsplashPhoto.kt │ │ │ ├── UnsplashPhotoUrls.kt │ │ │ ├── UnsplashRepository.kt │ │ │ ├── UnsplashSearchResponse.kt │ │ │ └── UnsplashUser.kt │ │ │ ├── di │ │ │ ├── DatabaseModule.kt │ │ │ └── NetworkModule.kt │ │ │ ├── ui │ │ │ ├── Color.kt │ │ │ ├── Shapes.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ ├── utilities │ │ │ ├── Constants.kt │ │ │ └── GrowZoneUtil.kt │ │ │ ├── viewmodels │ │ │ ├── GalleryViewModel.kt │ │ │ ├── GardenPlantingListViewModel.kt │ │ │ ├── PlantAndGardenPlantingsViewModel.kt │ │ │ ├── PlantDetailViewModel.kt │ │ │ └── PlantListViewModel.kt │ │ │ └── workers │ │ │ └── SeedDatabaseWorker.kt │ └── res │ │ ├── drawable │ │ ├── ic_filter_list_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_my_garden_active.xml │ │ ├── ic_photo_library.xml │ │ └── ic_plant_list_active.xml │ │ ├── layout │ │ └── item_plant_description.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-bn │ │ └── strings.xml │ │ ├── values-ca │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-pt │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-sv-rSE │ │ └── strings.xml │ │ ├── values-tr-rTR │ │ └── strings.xml │ │ ├── values-vi │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── google │ └── samples │ └── apps │ └── sunflower │ ├── data │ ├── ConvertersTest.kt │ ├── GardenPlantingTest.kt │ └── PlantTest.kt │ ├── test │ └── CalendarMatcher.kt │ └── utilities │ └── GrowZoneUtilTest.kt ├── build.gradle.kts ├── buildscripts ├── init.gradle.kts └── toml-updater-config.gradle ├── docs └── MigrationJourney.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── macrobenchmark ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── google │ └── samples │ └── apps │ └── sunflower │ └── macrobenchmark │ ├── BaselineProfileGenerator.kt │ ├── PlantDetailBenchmarks.kt │ ├── PlantListBenchmarks.kt │ ├── StartupBenchmarks.kt │ └── Utils.kt ├── screenshots ├── SunflowerM3Screenshots.png ├── ic_launcher-web.png ├── icon_background.png ├── icon_foreground.png ├── jetpack_donut.png ├── phone_my_garden.png ├── phone_plant_detail.png ├── phone_plant_list.png ├── screenshots.png └── sunflower.gif └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @android/compose-devrel 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage me"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: checkboxes 11 | attributes: 12 | label: Is there an existing issue for this? 13 | description: Please search to see if an issue already exists for the bug you encountered. 14 | options: 15 | - label: I have searched the existing issues 16 | required: true 17 | - type: checkboxes 18 | attributes: 19 | label: Is there a StackOverflow question about this issue? 20 | description: Please search [StackOverflow](https://stackoverflow.com/questions/tagged/android-jetpack-compose) if an issue with an answer already exists for the bug you encountered. 21 | options: 22 | - label: I have searched StackOverflow 23 | required: true 24 | - type: checkboxes 25 | attributes: 26 | label: Is this an issue related to the sample app? 27 | description: Please confirm that this is an issue related to this sample repo. If this is a bug related to Compose, file an issue on the Compose [issue tracker](https://issuetracker.google.com/issues/new?component=612128) instead. 28 | options: 29 | - label: Yes, this is a specific issue related to this samples repo. 30 | required: true 31 | - type: textarea 32 | id: what-happened 33 | attributes: 34 | label: What happened? 35 | description: Also tell us, what did you expect to happen? 36 | placeholder: Tell us what you see! 37 | value: "A bug happened!" 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: logs 42 | attributes: 43 | label: Relevant logcat output 44 | description: Please copy and paste any relevant logcat output. This will be automatically formatted into code, so no need for backticks. 45 | render: shell 46 | - type: checkboxes 47 | id: terms 48 | attributes: 49 | label: Code of Conduct 50 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 51 | options: 52 | - label: I agree to follow this project's Code of Conduct 53 | required: true 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: File a feature request 3 | title: "[FR]: " 4 | labels: ["enhancement", "triage me"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: checkboxes 11 | attributes: 12 | label: Is there an existing issue for this? 13 | description: Please search to see if an issue already exists for this feature request. 14 | options: 15 | - label: I have searched the existing issues 16 | required: true 17 | - type: checkboxes 18 | attributes: 19 | label: Is this a feature request for the samples? 20 | description: Please confirm that this is a feature request related to this samples repo. If this is a request related to Compose, file a feature request on the Compose [issue tracker](https://issuetracker.google.com/issues/new?component=612128) instead. 21 | options: 22 | - label: Yes, this is a specific request related to this samples repo. 23 | required: true 24 | - type: textarea 25 | id: describe-problem 26 | attributes: 27 | label: Describe the problem 28 | description: Is your feature request related to a problem? Please describe. 29 | placeholder: I'm always frustrated when... 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: solution 34 | attributes: 35 | label: Describe the solution 36 | description: Please describe the solution you'd like. A clear and concise description of what you want to happen. 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: context 41 | attributes: 42 | label: Additional context 43 | description: Add any other context or screenshots about the feature request here. 44 | validations: 45 | required: false 46 | - type: checkboxes 47 | id: terms 48 | attributes: 49 | label: Code of Conduct 50 | description: By submitting this issue, you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md) 51 | options: 52 | - label: I agree to follow this project's Code of Conduct 53 | required: true 54 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Create a pull request 4 | label: 'triage me' 5 | --- 6 | 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: 7 | - [ ] 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 8 | - [ ] Ensure the tests and linter pass 9 | - [ ] Appropriate docs were updated (if necessary) 10 | 11 | Fixes # 🦕 12 | -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2024 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | org.gradle.daemon=false 18 | org.gradle.parallel=true 19 | org.gradle.jvmargs=-Xmx5120m 20 | org.gradle.workers.max=2 21 | 22 | kotlin.incremental=false 23 | kotlin.compiler.execution.strategy=in-process 24 | 25 | # Controls KotlinOptions.allWarningsAsErrors. This is used in CI and can be set in local properties. 26 | warningsAsErrors=true 27 | -------------------------------------------------------------------------------- /.github/scripts/gradlew_recursive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2020 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -xe 18 | 19 | # Default Gradle settings are not optimal for Android builds, override them 20 | # here to make the most out of the GitHub Actions build servers 21 | GRADLE_OPTS="$GRADLE_OPTS -Xms4g -Xmx4g" 22 | GRADLE_OPTS="$GRADLE_OPTS -XX:+HeapDumpOnOutOfMemoryError" 23 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.daemon=false" 24 | GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.workers.max=2" 25 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.incremental=false" 26 | GRADLE_OPTS="$GRADLE_OPTS -Dkotlin.compiler.execution.strategy=in-process" 27 | GRADLE_OPTS="$GRADLE_OPTS -Dfile.encoding=UTF-8" 28 | export GRADLE_OPTS 29 | 30 | # Crawl all gradlew files which indicate an Android project 31 | # You may edit this if your repo has a different project structure 32 | for GRADLEW in `find . -name "gradlew"` ; do 33 | SAMPLE=$(dirname "${GRADLEW}") 34 | # Tell Gradle that this is a CI environment and disable parallel compilation 35 | bash "$GRADLEW" -p "$SAMPLE" -Pci --no-parallel --stacktrace $@ 36 | done 37 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Android CI 16 | 17 | on: 18 | push: 19 | branches: [ main ] 20 | pull_request: 21 | branches: [ main ] 22 | 23 | jobs: 24 | 25 | build: 26 | name: Build 27 | runs-on: ubuntu-20.04 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: set up JDK 17 32 | uses: actions/setup-java@v3 33 | with: 34 | java-version: '17' 35 | distribution: 'temurin' 36 | - name: Build project 37 | run: .github/scripts/gradlew_recursive.sh assembleDebug 38 | - name: Zip artifacts 39 | run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' 40 | - name: Upload artifacts 41 | uses: actions/upload-artifact@v1 42 | with: 43 | name: assemble 44 | path: assemble.zip 45 | # 46 | # Disabling androidTest temporarily due to the stability issue b/251319989 47 | # androidTest: 48 | # needs: build 49 | # runs-on: macOS-latest 50 | # timeout-minutes: 45 51 | # 52 | # steps: 53 | # - uses: actions/setup-java@v3 54 | # with: 55 | # distribution: 'temurin' 56 | # java-version: '11' 57 | # - uses: actions/checkout@v3 58 | # 59 | # - name: Setup Android SDK 60 | # uses: android-actions/setup-android@v2 61 | # 62 | # - name: Checkout 63 | # uses: actions/checkout@v3 64 | # 65 | # - name: Run instrumented tests with GMD 66 | # run: ./gradlew pixel2api27DebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=20 -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true --info 67 | # 68 | # - name: Upload test reports 69 | # if: always() 70 | # uses: actions/upload-artifact@v3 71 | # with: 72 | # name: test-reports 73 | # path: | 74 | # '*/build/outputs/androidTest-results/' 75 | # '!**/*"*' # Couldn't exclude a file with double quotation. Revisit the path once b/242988834 is fixed 76 | -------------------------------------------------------------------------------- /.github/workflows/copy-branch.yml: -------------------------------------------------------------------------------- 1 | # Duplicates default main branch to the old master branch 2 | 3 | name: Duplicates main to old master branch 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | push: 9 | branches: [ main ] 10 | 11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 12 | jobs: 13 | # This workflow contains a single job called "copy-branch" 14 | copy-branch: 15 | # The type of runner that the job will run on 16 | runs-on: ubuntu-latest 17 | 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it, 21 | # but specifies master branch (old default). 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 0 25 | ref: master 26 | 27 | - run: | 28 | git config user.name github-actions 29 | git config user.email github-actions@github.com 30 | git merge origin/main 31 | git push 32 | -------------------------------------------------------------------------------- /.github/workflows/update_deps.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2024 The Android Open Source Project 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | name: Update Versions / Dependencies 18 | 19 | on: 20 | schedule: 21 | - cron: '9 0 1 * *' 22 | workflow_dispatch: 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Copy CI gradle.properties 30 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 31 | - name: set up JDK 17 32 | uses: actions/setup-java@v3 33 | with: 34 | java-version: 17 35 | distribution: 'zulu' 36 | cache: gradle 37 | 38 | - name: Update dependencies 39 | run: ./gradlew versionCatalogUpdate 40 | - name: Create pull request 41 | id: cpr 42 | uses: peter-evans/create-pull-request@v4 43 | with: 44 | token: ${{ secrets.PAT }} 45 | commit-message: 🤖 Update Dependencies 46 | committer: compose-devrel-github-bot 47 | author: compose-devrel-github-bot 48 | signoff: false 49 | branch: bot-update-deps 50 | delete-branch: true 51 | title: '🤖 Update Dependencies' 52 | body: Updated depedencies 53 | reviewers: ${{ github.actor }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/* 5 | !.idea/copyright 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | ktlint -------------------------------------------------------------------------------- /.google/packaging.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # GOOGLE SAMPLE PACKAGING DATA 16 | # 17 | # This file is used by Google as part of our samples packaging process. 18 | # End users may safely ignore this file. It has no relevance to other systems. 19 | --- 20 | status: PUBLISHED 21 | technologies: [Android, JetpackCompose, Coroutines] 22 | categories: 23 | - Getting Started 24 | - Jetpack 25 | - AndroidTesting 26 | - AndroidArchitecture 27 | - AndroidArchitectureUILayer 28 | - AndroidArchitectureDataLayer 29 | - AndroidArchitectureStateProduction 30 | - AndroidArchitectureStateHolder 31 | - AndroidArchitectureUIEvents 32 | - JetpackComposeArchitectureAndState 33 | - JetpackComposeMigrationAndInterop 34 | - JetpackComposeDesignSystems 35 | - JetpackComposeNavigation 36 | - JetpackComposeAnimation 37 | - JetpackComposeTesting 38 | languages: [Kotlin] 39 | solutions: 40 | - Mobile 41 | - Flow 42 | - JetpackHilt 43 | - JetpackRoom 44 | - JetpackWorkManager 45 | - JetpackNavigation 46 | - JetpackLifecycle 47 | github: android/sunflower 48 | level: INTERMEDIATE 49 | icon: screenshots/ic_launcher-web.png 50 | apiRefs: 51 | - android:android.support.constraint.ConstraintLayout 52 | license: apache2 53 | -------------------------------------------------------------------------------- /.idea/copyright/google.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/proguard-rules-benchmark.pro: -------------------------------------------------------------------------------- 1 | # Not obfuscating benchmark builds to be readable with profilers and compatible with baseline profiles 2 | -dontobfuscate 3 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/google/home/tiem/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle.kts. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | 27 | # ServiceLoader support 28 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} 29 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} 30 | 31 | # Most of volatile fields are updated with AFU and should not be mangled 32 | -keepclassmembernames class kotlinx.** { 33 | volatile ; 34 | } 35 | -keepclassmembers class com.google.samples.apps.sunflower.** { ; } 36 | 37 | 38 | # Keep annotation default values (e.g., retrofit2.http.Field.encoded). 39 | -keepattributes AnnotationDefault 40 | 41 | # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). 42 | -keep,allowobfuscation,allowshrinking interface retrofit2.Call 43 | -keep,allowobfuscation,allowshrinking class retrofit2.Response 44 | 45 | # With R8 full mode generic signatures are stripped for classes that are not 46 | # kept. Suspend functions are wrapped in continuations where the type argument 47 | # is used. 48 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 49 | # -keep class hilt_aggregated_deps.** { *; } 50 | 51 | 52 | ##---------------Begin: proguard configuration for Gson ---------- 53 | # Gson uses generic type information stored in a class file when working with fields. Proguard 54 | # removes such information by default, so configure it to keep all of it. 55 | -keep class com.google.gson.** { *; } 56 | -keepattributes Signature 57 | # For using GSON @Expose annotation 58 | -keepattributes *Annotation* 59 | # Gson specific classes 60 | -keep class sun.misc.Unsafe { *; } 61 | #-keep class com.google.gson.stream.** { *; } 62 | # Application classes that will be serialized/deserialized over Gson 63 | -keep class com.google.gson.examples.android.model.** { *; } 64 | ##---------------End: proguard configuration for Gson ---------- 65 | 66 | ##---------------Begin: proguard configuration for OkHttp ---------- 67 | # Don't warn on unused classes. 68 | # See: https://github.com/square/okhttp/issues/6258 69 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 70 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 71 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 72 | -dontwarn org.conscrypt.* 73 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 74 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 75 | -dontwarn org.openjsse.net.ssl.OpenJSSE 76 | ##---------------End: proguard configuration for OkHttp ---------- 77 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/GardenActivityTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower 18 | 19 | import android.util.Log 20 | import androidx.compose.ui.test.assertIsDisplayed 21 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 22 | import androidx.compose.ui.test.onNodeWithTag 23 | import androidx.compose.ui.test.onNodeWithText 24 | import androidx.compose.ui.test.onRoot 25 | import androidx.compose.ui.test.performClick 26 | import androidx.compose.ui.test.printToLog 27 | import androidx.test.platform.app.InstrumentationRegistry 28 | import androidx.work.Configuration 29 | import androidx.work.testing.SynchronousExecutor 30 | import androidx.work.testing.WorkManagerTestInitHelper 31 | import dagger.hilt.android.testing.HiltAndroidRule 32 | import dagger.hilt.android.testing.HiltAndroidTest 33 | import org.junit.Before 34 | import org.junit.Rule 35 | import org.junit.Test 36 | import org.junit.rules.RuleChain 37 | 38 | @HiltAndroidTest 39 | class GardenActivityTest { 40 | 41 | private val hiltRule = HiltAndroidRule(this) 42 | private val composeTestRule = createAndroidComposeRule() 43 | 44 | @get:Rule 45 | val rule: RuleChain = RuleChain 46 | .outerRule(hiltRule) 47 | .around(composeTestRule) 48 | 49 | @Before 50 | fun setup() { 51 | val context = InstrumentationRegistry.getInstrumentation().targetContext 52 | val config = Configuration.Builder() 53 | .setMinimumLoggingLevel(Log.DEBUG) 54 | .setExecutor(SynchronousExecutor()) 55 | .build() 56 | 57 | WorkManagerTestInitHelper.initializeTestWorkManager(context, config) 58 | } 59 | 60 | @Test fun clickAddPlant_OpensPlantList() { 61 | // Given that no Plants are added to the user's garden 62 | 63 | // When the "Add Plant" button is clicked 64 | with(composeTestRule.onNodeWithText("Add plant")) { 65 | assertExists() 66 | assertIsDisplayed() 67 | performClick() 68 | } 69 | 70 | composeTestRule.waitForIdle() 71 | 72 | // Then the pager should change to the Plant List page 73 | with(composeTestRule.onNodeWithTag("plant_list")) { 74 | assertExists() 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/MainCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower 18 | 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.test.* 21 | import org.junit.rules.TestWatcher 22 | import org.junit.runner.Description 23 | 24 | class MainCoroutineRule( 25 | val testDispatcher: TestDispatcher = StandardTestDispatcher() 26 | ) : TestWatcher() { 27 | 28 | override fun starting(description: Description) { 29 | super.starting(description) 30 | Dispatchers.setMain(testDispatcher) 31 | } 32 | 33 | override fun finished(description: Description) { 34 | super.finished(description) 35 | Dispatchers.resetMain() 36 | } 37 | } 38 | 39 | fun MainCoroutineRule.runBlockingTest(block: suspend () -> Unit) = runTest(this.testDispatcher) { 40 | block() 41 | } 42 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/compose/garden/GardenTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.garden 18 | 19 | import androidx.compose.ui.test.assertIsDisplayed 20 | import androidx.compose.ui.test.junit4.createComposeRule 21 | import androidx.compose.ui.test.onNodeWithText 22 | import androidx.test.ext.junit.runners.AndroidJUnit4 23 | import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings 24 | import com.google.samples.apps.sunflower.utilities.testPlantAndGardenPlanting 25 | import org.junit.Rule 26 | import org.junit.Test 27 | import org.junit.runner.RunWith 28 | 29 | @RunWith(AndroidJUnit4::class) 30 | class GardenTest { 31 | 32 | @get:Rule 33 | val composeTestRule = createComposeRule() 34 | 35 | @Test 36 | fun garden_emptyGarden() { 37 | startGarden(emptyList()) 38 | composeTestRule.onNodeWithText("Add plant").assertIsDisplayed() 39 | } 40 | 41 | @Test 42 | fun garden_notEmptyGarden() { 43 | startGarden(listOf(testPlantAndGardenPlanting)) 44 | composeTestRule.onNodeWithText("Add plant").assertDoesNotExist() 45 | composeTestRule.onNodeWithText(testPlantAndGardenPlanting.plant.name).assertIsDisplayed() 46 | } 47 | 48 | private fun startGarden(gardenPlantings: List) { 49 | composeTestRule.setContent { 50 | GardenScreen(gardenPlants = gardenPlantings) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/compose/plantlist/PlantListTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.plantlist 18 | 19 | import androidx.compose.ui.test.assertIsDisplayed 20 | import androidx.compose.ui.test.junit4.createComposeRule 21 | import androidx.compose.ui.test.onNodeWithText 22 | import androidx.test.ext.junit.runners.AndroidJUnit4 23 | import com.google.samples.apps.sunflower.compose.plantdetail.plantForTesting 24 | import com.google.samples.apps.sunflower.data.Plant 25 | import org.junit.Rule 26 | import org.junit.Test 27 | import org.junit.runner.RunWith 28 | 29 | @RunWith(AndroidJUnit4::class) 30 | class PlantListTest { 31 | @get:Rule 32 | val composeTestRule = createComposeRule() 33 | 34 | @Test 35 | fun plantList_itemShown() { 36 | startPlantList() 37 | composeTestRule.onNodeWithText("Apple").assertIsDisplayed() 38 | } 39 | 40 | private fun startPlantList(onPlantClick: (Plant) -> Unit = {}) { 41 | composeTestRule.setContent { 42 | PlantListScreen(plants = listOf(plantForTesting()), onPlantClick = onPlantClick) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/data/PlantDaoTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 20 | import androidx.room.Room 21 | import androidx.test.ext.junit.runners.AndroidJUnit4 22 | import androidx.test.platform.app.InstrumentationRegistry 23 | import kotlinx.coroutines.flow.first 24 | import kotlinx.coroutines.runBlocking 25 | import org.hamcrest.MatcherAssert.assertThat 26 | import org.hamcrest.Matchers.equalTo 27 | import org.junit.After 28 | import org.junit.Before 29 | import org.junit.Rule 30 | import org.junit.Test 31 | import org.junit.runner.RunWith 32 | 33 | @RunWith(AndroidJUnit4::class) 34 | class PlantDaoTest { 35 | private lateinit var database: AppDatabase 36 | private lateinit var plantDao: PlantDao 37 | private val plantA = Plant("1", "A", "", 1, 1, "") 38 | private val plantB = Plant("2", "B", "", 1, 1, "") 39 | private val plantC = Plant("3", "C", "", 2, 2, "") 40 | 41 | @get:Rule 42 | var instantTaskExecutorRule = InstantTaskExecutorRule() 43 | 44 | @Before fun createDb() = runBlocking { 45 | val context = InstrumentationRegistry.getInstrumentation().targetContext 46 | database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() 47 | plantDao = database.plantDao() 48 | 49 | // Insert plants in non-alphabetical order to test that results are sorted by name 50 | plantDao.upsertAll(listOf(plantB, plantC, plantA)) 51 | } 52 | 53 | @After fun closeDb() { 54 | database.close() 55 | } 56 | 57 | @Test fun testGetPlants() = runBlocking { 58 | val plantList = plantDao.getPlants().first() 59 | assertThat(plantList.size, equalTo(3)) 60 | 61 | // Ensure plant list is sorted by name 62 | assertThat(plantList[0], equalTo(plantA)) 63 | assertThat(plantList[1], equalTo(plantB)) 64 | assertThat(plantList[2], equalTo(plantC)) 65 | } 66 | 67 | @Test fun testGetPlantsWithGrowZoneNumber() = runBlocking { 68 | val plantList = plantDao.getPlantsWithGrowZoneNumber(1).first() 69 | assertThat(plantList.size, equalTo(2)) 70 | assertThat(plantDao.getPlantsWithGrowZoneNumber(2).first().size, equalTo(1)) 71 | assertThat(plantDao.getPlantsWithGrowZoneNumber(3).first().size, equalTo(0)) 72 | 73 | // Ensure plant list is sorted by name 74 | assertThat(plantList[0], equalTo(plantA)) 75 | assertThat(plantList[1], equalTo(plantB)) 76 | } 77 | 78 | @Test fun testGetPlant() = runBlocking { 79 | assertThat(plantDao.getPlant(plantA.plantId).first(), equalTo(plantA)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/LiveDataTestUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | import androidx.lifecycle.LiveData 20 | import java.util.concurrent.CountDownLatch 21 | import java.util.concurrent.TimeUnit 22 | 23 | /** 24 | * Helper method for testing LiveData objects, from 25 | * https://github.com/googlesamples/android-architecture-components. 26 | * 27 | * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds. 28 | * Once we got a notification via onChanged, we stop observing. 29 | */ 30 | @Throws(InterruptedException::class) 31 | fun getValue(liveData: LiveData): T { 32 | val data = arrayOfNulls(1) 33 | val latch = CountDownLatch(1) 34 | liveData.observeForever { o -> 35 | data[0] = o 36 | latch.countDown() 37 | } 38 | latch.await(2, TimeUnit.SECONDS) 39 | 40 | @Suppress("UNCHECKED_CAST") 41 | return data[0] as T 42 | } 43 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/MainTestRunner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | import android.app.Application 20 | import android.content.Context 21 | import androidx.test.runner.AndroidJUnitRunner 22 | import dagger.hilt.android.testing.HiltTestApplication 23 | 24 | // A custom runner to set up the instrumented application class for tests. 25 | class MainTestRunner : AndroidJUnitRunner() { 26 | 27 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { 28 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | import android.content.Intent 20 | import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction 21 | import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra 22 | import com.google.samples.apps.sunflower.data.GardenPlanting 23 | import com.google.samples.apps.sunflower.data.Plant 24 | import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings 25 | import org.hamcrest.Matcher 26 | import org.hamcrest.Matchers.`is` 27 | import org.hamcrest.Matchers.allOf 28 | import java.util.Calendar 29 | 30 | /** 31 | * [Plant] objects used for tests. 32 | */ 33 | val testPlants = arrayListOf( 34 | Plant("1", "Apple", "A red fruit", 1), 35 | Plant("2", "B", "Description B", 1), 36 | Plant("3", "C", "Description C", 2) 37 | ) 38 | val testPlant = testPlants[0] 39 | 40 | /** 41 | * [Calendar] object used for tests. 42 | */ 43 | val testCalendar: Calendar = Calendar.getInstance().apply { 44 | set(Calendar.YEAR, 1998) 45 | set(Calendar.MONTH, Calendar.SEPTEMBER) 46 | set(Calendar.DAY_OF_MONTH, 4) 47 | } 48 | 49 | /** 50 | * [GardenPlanting] object used for tests. 51 | */ 52 | val testGardenPlanting = GardenPlanting(testPlant.plantId, testCalendar, testCalendar) 53 | 54 | /** 55 | * [PlantAndGardenPlantings] object used for tests. 56 | */ 57 | val testPlantAndGardenPlanting = PlantAndGardenPlantings(testPlant, listOf(testGardenPlanting)) 58 | 59 | /** 60 | * Simplify testing Intents with Chooser 61 | * 62 | * @param matcher the actual intent before wrapped by Chooser Intent 63 | */ 64 | fun chooser(matcher: Matcher): Matcher = allOf( 65 | hasAction(Intent.ACTION_CHOOSER), 66 | hasExtra(`is`(Intent.EXTRA_INTENT), matcher) 67 | ) 68 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.viewmodels 18 | 19 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 20 | import androidx.lifecycle.SavedStateHandle 21 | import androidx.room.Room 22 | import androidx.test.platform.app.InstrumentationRegistry 23 | import com.google.samples.apps.sunflower.MainCoroutineRule 24 | import com.google.samples.apps.sunflower.data.AppDatabase 25 | import com.google.samples.apps.sunflower.data.GardenPlantingRepository 26 | import com.google.samples.apps.sunflower.data.PlantRepository 27 | import com.google.samples.apps.sunflower.runBlockingTest 28 | import com.google.samples.apps.sunflower.utilities.getValue 29 | import com.google.samples.apps.sunflower.utilities.testPlant 30 | import dagger.hilt.android.testing.HiltAndroidRule 31 | import dagger.hilt.android.testing.HiltAndroidTest 32 | import kotlinx.coroutines.flow.first 33 | import org.junit.After 34 | import org.junit.Assert.assertFalse 35 | import org.junit.Before 36 | import org.junit.Rule 37 | import org.junit.Test 38 | import org.junit.rules.RuleChain 39 | import javax.inject.Inject 40 | import kotlin.jvm.Throws 41 | 42 | @HiltAndroidTest 43 | class PlantDetailViewModelTest { 44 | 45 | private lateinit var appDatabase: AppDatabase 46 | private lateinit var viewModel: PlantDetailViewModel 47 | private val hiltRule = HiltAndroidRule(this) 48 | private val instantTaskExecutorRule = InstantTaskExecutorRule() 49 | private val coroutineRule = MainCoroutineRule() 50 | 51 | @get:Rule 52 | val rule: RuleChain = RuleChain 53 | .outerRule(hiltRule) 54 | .around(instantTaskExecutorRule) 55 | .around(coroutineRule) 56 | 57 | @Inject 58 | lateinit var plantRepository: PlantRepository 59 | 60 | @Inject 61 | lateinit var gardenPlantingRepository: GardenPlantingRepository 62 | 63 | @Before 64 | fun setUp() { 65 | hiltRule.inject() 66 | 67 | val context = InstrumentationRegistry.getInstrumentation().targetContext 68 | appDatabase = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() 69 | 70 | val savedStateHandle: SavedStateHandle = SavedStateHandle().apply { 71 | set("plantId", testPlant.plantId) 72 | } 73 | viewModel = PlantDetailViewModel(savedStateHandle, plantRepository, gardenPlantingRepository) 74 | } 75 | 76 | @After 77 | fun tearDown() { 78 | appDatabase.close() 79 | } 80 | 81 | @Suppress("BlockingMethodInNonBlockingContext") 82 | @Test 83 | @Throws(InterruptedException::class) 84 | fun testDefaultValues() = coroutineRule.runBlockingTest { 85 | assertFalse(viewModel.isPlanted.first()) 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/samples/apps/sunflower/worker/SeedDatabaseWorkerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.worker 18 | 19 | import android.content.Context 20 | import android.util.Log 21 | import androidx.test.platform.app.InstrumentationRegistry 22 | import androidx.work.Configuration 23 | import androidx.work.ListenableWorker.Result 24 | import androidx.work.WorkManager 25 | import androidx.work.testing.SynchronousExecutor 26 | import androidx.work.testing.TestListenableWorkerBuilder 27 | import androidx.work.testing.WorkManagerTestInitHelper 28 | import androidx.work.workDataOf 29 | import com.google.samples.apps.sunflower.utilities.PLANT_DATA_FILENAME 30 | import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker 31 | import org.hamcrest.CoreMatchers.`is` 32 | import org.junit.Assert.assertThat 33 | import org.junit.Before 34 | import org.junit.Test 35 | import org.junit.runner.RunWith 36 | import org.junit.runners.JUnit4 37 | 38 | @RunWith(JUnit4::class) 39 | class RefreshMainDataWorkTest { 40 | private lateinit var workManager: WorkManager 41 | private lateinit var context: Context 42 | private lateinit var configuration: Configuration 43 | 44 | @Before 45 | fun setup() { 46 | // Configure WorkManager 47 | configuration = Configuration.Builder() 48 | // Set log level to Log.DEBUG to make it easier to debug 49 | .setMinimumLoggingLevel(Log.DEBUG) 50 | // Use a SynchronousExecutor here to make it easier to write tests 51 | .setExecutor(SynchronousExecutor()) 52 | .build() 53 | 54 | // Initialize WorkManager for instrumentation tests. 55 | context = InstrumentationRegistry.getInstrumentation().targetContext 56 | WorkManagerTestInitHelper.initializeTestWorkManager(context, configuration) 57 | workManager = WorkManager.getInstance(context) 58 | } 59 | 60 | @Test 61 | fun testRefreshMainDataWork() { 62 | // Get the ListenableWorker 63 | val worker = TestListenableWorkerBuilder( 64 | context = context, 65 | inputData = workDataOf(SeedDatabaseWorker.KEY_FILENAME to PLANT_DATA_FILENAME) 66 | ).build() 67 | 68 | // Start the work synchronously 69 | val future = worker.startWork() 70 | val result = future.get() 71 | 72 | assertThat(result, `is`(Result.Success())) 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/androidTest/res/raw/apple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/androidTest/res/raw/apple.jpg -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/GardenActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import androidx.activity.enableEdgeToEdge 23 | import com.google.samples.apps.sunflower.compose.SunflowerApp 24 | import com.google.samples.apps.sunflower.ui.SunflowerTheme 25 | import dagger.hilt.android.AndroidEntryPoint 26 | 27 | @AndroidEntryPoint 28 | class GardenActivity : ComponentActivity() { 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | // Displaying edge-to-edge 34 | enableEdgeToEdge() 35 | setContent { 36 | SunflowerTheme { 37 | SunflowerApp() 38 | } 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/MainApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower 18 | 19 | import android.app.Application 20 | import androidx.work.Configuration 21 | import dagger.hilt.android.HiltAndroidApp 22 | 23 | @HiltAndroidApp 24 | class MainApplication : Application(), Configuration.Provider { 25 | override val workManagerConfiguration: Configuration 26 | get() = Configuration.Builder() 27 | .setMinimumLoggingLevel(if (BuildConfig.DEBUG) android.util.Log.DEBUG else android.util.Log.ERROR) 28 | .build() 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/api/UnsplashService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.api 18 | 19 | import com.google.samples.apps.sunflower.BuildConfig 20 | import com.google.samples.apps.sunflower.data.UnsplashSearchResponse 21 | import okhttp3.OkHttpClient 22 | import okhttp3.logging.HttpLoggingInterceptor 23 | import okhttp3.logging.HttpLoggingInterceptor.Level 24 | import retrofit2.Retrofit 25 | import retrofit2.converter.gson.GsonConverterFactory 26 | import retrofit2.http.GET 27 | import retrofit2.http.Query 28 | 29 | /** 30 | * Used to connect to the Unsplash API to fetch photos 31 | */ 32 | interface UnsplashService { 33 | 34 | @GET("search/photos") 35 | suspend fun searchPhotos( 36 | @Query("query") query: String, 37 | @Query("page") page: Int, 38 | @Query("per_page") perPage: Int, 39 | @Query("client_id") clientId: String = BuildConfig.UNSPLASH_ACCESS_KEY 40 | ): UnsplashSearchResponse 41 | 42 | companion object { 43 | private const val BASE_URL = "https://api.unsplash.com/" 44 | 45 | fun create(): UnsplashService { 46 | val logger = HttpLoggingInterceptor().apply { level = Level.BASIC } 47 | 48 | val client = OkHttpClient.Builder() 49 | .addInterceptor(logger) 50 | .build() 51 | 52 | return Retrofit.Builder() 53 | .baseUrl(BASE_URL) 54 | .client(client) 55 | .addConverterFactory(GsonConverterFactory.create()) 56 | .build() 57 | .create(UnsplashService::class.java) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/Dimens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.res.dimensionResource 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | import com.google.samples.apps.sunflower.R 24 | 25 | /** 26 | * Class that captures dimens used in Compose code. The dimens that need to be consistent with the 27 | * View system use [dimensionResource] and are marked as composable. 28 | * 29 | * Disclaimer: 30 | * This approach doesn't consider multiple configurations. For that, an Ambient should be created. 31 | */ 32 | object Dimens { 33 | 34 | val PaddingSmall: Dp 35 | @Composable get() = dimensionResource(R.dimen.margin_small) 36 | 37 | val PaddingNormal: Dp 38 | @Composable get() = dimensionResource(R.dimen.margin_normal) 39 | 40 | val PaddingLarge: Dp = 24.dp 41 | 42 | val PlantDetailAppBarHeight: Dp 43 | @Composable get() = dimensionResource(R.dimen.plant_detail_app_bar_height) 44 | 45 | val ToolbarIconPadding = 12.dp 46 | 47 | val ToolbarIconSize = 32.dp 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/Modifiers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose 18 | 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.layout.LayoutModifier 21 | import androidx.compose.ui.layout.Measurable 22 | import androidx.compose.ui.layout.MeasureResult 23 | import androidx.compose.ui.layout.MeasureScope 24 | import androidx.compose.ui.unit.Constraints 25 | 26 | /** 27 | * Hides an element on the screen leaving its space occupied. 28 | * This should be replaced with the real visible modifier in the future: 29 | * https://issuetracker.google.com/issues/158837937 30 | * 31 | * isVisible is of type () -> Boolean because if the calling composable doesn't own the 32 | * state boolean of that Boolean, a read (recompose) will be avoided. 33 | */ 34 | fun Modifier.visible(isVisible: () -> Boolean) = this.then(VisibleModifier(isVisible)) 35 | 36 | private data class VisibleModifier( 37 | private val isVisible: () -> Boolean 38 | ) : LayoutModifier { 39 | override fun MeasureScope.measure( 40 | measurable: Measurable, 41 | constraints: Constraints 42 | ): MeasureResult { 43 | val placeable = measurable.measure(constraints) 44 | return layout(placeable.width, placeable.height) { 45 | if (isVisible()) { 46 | placeable.place(0, 0) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/Screen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose 18 | 19 | import androidx.navigation.NamedNavArgument 20 | import androidx.navigation.NavType 21 | import androidx.navigation.navArgument 22 | 23 | sealed class Screen( 24 | val route: String, 25 | val navArguments: List = emptyList() 26 | ) { 27 | data object Home : Screen("home") 28 | 29 | data object PlantDetail : Screen( 30 | route = "plantDetail/{plantId}", 31 | navArguments = listOf(navArgument("plantId") { 32 | type = NavType.StringType 33 | }) 34 | ) { 35 | fun createRoute(plantId: String) = "plantDetail/${plantId}" 36 | } 37 | 38 | data object Gallery : Screen( 39 | route = "gallery/{plantName}", 40 | navArguments = listOf(navArgument("plantName") { 41 | type = NavType.StringType 42 | }) 43 | ) { 44 | fun createRoute(plantName: String) = "gallery/${plantName}" 45 | 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/plantdetail/PlantDetailScroller.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.plantdetail 18 | 19 | import androidx.compose.animation.core.MutableTransitionState 20 | import androidx.compose.foundation.ScrollState 21 | import androidx.compose.ui.unit.Density 22 | import androidx.compose.ui.unit.dp 23 | 24 | // Value obtained empirically so that the header buttons don't surpass the header container 25 | private val HeaderTransitionOffset = 190.dp 26 | 27 | /** 28 | * Class that contains derived state for when the toolbar should be shown 29 | */ 30 | data class PlantDetailsScroller( 31 | val scrollState: ScrollState, 32 | val namePosition: Float 33 | ) { 34 | val toolbarTransitionState = MutableTransitionState(ToolbarState.HIDDEN) 35 | 36 | fun getToolbarState(density: Density): ToolbarState { 37 | // When the namePosition is placed correctly on the screen (position > 1f) and it's 38 | // position is close to the header, then show the toolbar. 39 | return if (namePosition > 1f && 40 | scrollState.value > (namePosition - getTransitionOffset(density)) 41 | ) { 42 | toolbarTransitionState.targetState = ToolbarState.SHOWN 43 | ToolbarState.SHOWN 44 | } else { 45 | toolbarTransitionState.targetState = ToolbarState.HIDDEN 46 | ToolbarState.HIDDEN 47 | } 48 | } 49 | 50 | private fun getTransitionOffset(density: Density): Float = with(density) { 51 | HeaderTransitionOffset.toPx() 52 | } 53 | } 54 | 55 | // Toolbar state related classes and functions to achieve the CollapsingToolbarLayout animation 56 | enum class ToolbarState { HIDDEN, SHOWN } 57 | 58 | val ToolbarState.isShown 59 | get() = this == ToolbarState.SHOWN 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/plantlist/PlantListItemView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.plantlist 18 | 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.fillMaxWidth 21 | import androidx.compose.foundation.layout.height 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.foundation.layout.wrapContentWidth 24 | import androidx.compose.material3.Card 25 | import androidx.compose.material3.CardDefaults 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.layout.ContentScale 32 | import androidx.compose.ui.res.dimensionResource 33 | import androidx.compose.ui.res.stringResource 34 | import androidx.compose.ui.text.style.TextAlign 35 | import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi 36 | import com.bumptech.glide.integration.compose.GlideImage 37 | import com.google.samples.apps.sunflower.R 38 | import com.google.samples.apps.sunflower.data.Plant 39 | import com.google.samples.apps.sunflower.data.UnsplashPhoto 40 | 41 | @Composable 42 | fun PlantListItem(plant: Plant, onClick: () -> Unit) { 43 | ImageListItem(name = plant.name, imageUrl = plant.imageUrl, onClick = onClick) 44 | } 45 | 46 | @Composable 47 | fun PhotoListItem(photo: UnsplashPhoto, onClick: () -> Unit) { 48 | ImageListItem(name = photo.user.name, imageUrl = photo.urls.small, onClick = onClick) 49 | } 50 | 51 | @OptIn(ExperimentalGlideComposeApi::class) 52 | @Composable 53 | fun ImageListItem(name: String, imageUrl: String, onClick: () -> Unit) { 54 | Card( 55 | onClick = onClick, 56 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), 57 | modifier = Modifier 58 | .padding(horizontal = dimensionResource(id = R.dimen.card_side_margin)) 59 | .padding(bottom = dimensionResource(id = R.dimen.card_bottom_margin)) 60 | ) { 61 | Column(Modifier.fillMaxWidth()) { 62 | GlideImage( 63 | model = imageUrl, 64 | contentDescription = stringResource(R.string.a11y_plant_item_image), 65 | Modifier 66 | .fillMaxWidth() 67 | .height(dimensionResource(id = R.dimen.plant_item_image_height)), 68 | contentScale = ContentScale.Crop 69 | ) 70 | Text( 71 | text = name, 72 | textAlign = TextAlign.Center, 73 | maxLines = 1, 74 | style = MaterialTheme.typography.titleMedium, 75 | modifier = Modifier 76 | .fillMaxWidth() 77 | .padding(vertical = dimensionResource(id = R.dimen.margin_normal)) 78 | .wrapContentWidth(Alignment.CenterHorizontally) 79 | ) 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/plantlist/PlantListScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.plantlist 18 | 19 | import androidx.compose.foundation.layout.PaddingValues 20 | import androidx.compose.foundation.layout.consumeWindowInsets 21 | import androidx.compose.foundation.layout.imePadding 22 | import androidx.compose.foundation.layout.navigationBarsPadding 23 | import androidx.compose.foundation.lazy.grid.GridCells 24 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 25 | import androidx.compose.foundation.lazy.grid.items 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.livedata.observeAsState 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.testTag 31 | import androidx.compose.ui.res.dimensionResource 32 | import androidx.compose.ui.tooling.preview.Preview 33 | import androidx.compose.ui.tooling.preview.PreviewParameter 34 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 35 | import androidx.hilt.navigation.compose.hiltViewModel 36 | import com.google.samples.apps.sunflower.viewmodels.PlantListViewModel 37 | import com.google.samples.apps.sunflower.R 38 | import com.google.samples.apps.sunflower.data.Plant 39 | 40 | @Composable 41 | fun PlantListScreen( 42 | onPlantClick: (Plant) -> Unit, 43 | 44 | modifier: Modifier = Modifier, 45 | viewModel: PlantListViewModel = hiltViewModel(), 46 | ) { 47 | val plants by viewModel.plants.observeAsState(initial = emptyList()) 48 | PlantListScreen(plants = plants, modifier, onPlantClick = onPlantClick) 49 | } 50 | 51 | @Composable 52 | fun PlantListScreen( 53 | plants: List, 54 | modifier: Modifier = Modifier, 55 | onPlantClick: (Plant) -> Unit = {}, 56 | ) { 57 | LazyVerticalGrid( 58 | columns = GridCells.Fixed(2), 59 | modifier = modifier.testTag("plant_list") 60 | .imePadding(), 61 | contentPadding = PaddingValues( 62 | horizontal = dimensionResource(id = R.dimen.card_side_margin), 63 | vertical = dimensionResource(id = R.dimen.header_margin) 64 | ) 65 | ) { 66 | items( 67 | items = plants, 68 | key = { it.plantId } 69 | ) { plant -> 70 | PlantListItem(plant = plant) { 71 | onPlantClick(plant) 72 | } 73 | } 74 | } 75 | } 76 | 77 | @Preview 78 | @Composable 79 | private fun PlantListScreenPreview( 80 | @PreviewParameter(PlantListPreviewParamProvider::class) plants: List 81 | ) { 82 | PlantListScreen(plants = plants) 83 | } 84 | 85 | private class PlantListPreviewParamProvider : PreviewParameterProvider> { 86 | override val values: Sequence> = 87 | sequenceOf( 88 | emptyList(), 89 | listOf( 90 | Plant("1", "Apple", "Apple", growZoneNumber = 1), 91 | Plant("2", "Banana", "Banana", growZoneNumber = 2), 92 | Plant("3", "Carrot", "Carrot", growZoneNumber = 3), 93 | Plant("4", "Dill", "Dill", growZoneNumber = 3), 94 | ) 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/compose/utils/TextSnackbarContainer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.compose.utils 18 | 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.layout.systemBarsPadding 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Shapes 24 | import androidx.compose.material3.Snackbar 25 | import androidx.compose.material3.SnackbarDuration 26 | import androidx.compose.material3.SnackbarHost 27 | import androidx.compose.material3.SnackbarHostState 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.LaunchedEffect 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.rememberUpdatedState 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.unit.dp 36 | 37 | /** 38 | * Simple API to display a Snackbar with text on the screen 39 | */ 40 | @Composable 41 | fun TextSnackbarContainer( 42 | snackbarText: String, 43 | showSnackbar: Boolean, 44 | onDismissSnackbar: () -> Unit, 45 | modifier: Modifier = Modifier, 46 | snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, 47 | content: @Composable () -> Unit 48 | ) { 49 | Box(modifier) { 50 | content() 51 | 52 | val onDismissState by rememberUpdatedState(onDismissSnackbar) 53 | LaunchedEffect(showSnackbar, snackbarText) { 54 | if (showSnackbar) { 55 | try { 56 | snackbarHostState.showSnackbar( 57 | message = snackbarText, 58 | duration = SnackbarDuration.Short 59 | ) 60 | } finally { 61 | onDismissState() 62 | } 63 | } 64 | } 65 | 66 | // Override shapes to not use the ones coming from the MdcTheme 67 | MaterialTheme(shapes = Shapes()) { 68 | SnackbarHost( 69 | hostState = snackbarHostState, 70 | modifier = modifier 71 | .align(Alignment.BottomCenter) 72 | .systemBarsPadding() 73 | .padding(all = 8.dp), 74 | ) { 75 | Snackbar(it) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import android.content.Context 20 | import androidx.room.Database 21 | import androidx.room.Room 22 | import androidx.room.RoomDatabase 23 | import androidx.room.TypeConverters 24 | import androidx.sqlite.db.SupportSQLiteDatabase 25 | import androidx.work.OneTimeWorkRequestBuilder 26 | import androidx.work.WorkManager 27 | import androidx.work.workDataOf 28 | import com.google.samples.apps.sunflower.utilities.DATABASE_NAME 29 | import com.google.samples.apps.sunflower.utilities.PLANT_DATA_FILENAME 30 | import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker 31 | import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker.Companion.KEY_FILENAME 32 | 33 | /** 34 | * The Room database for this app 35 | */ 36 | @Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false) 37 | @TypeConverters(Converters::class) 38 | abstract class AppDatabase : RoomDatabase() { 39 | abstract fun gardenPlantingDao(): GardenPlantingDao 40 | abstract fun plantDao(): PlantDao 41 | 42 | companion object { 43 | 44 | // For Singleton instantiation 45 | @Volatile private var instance: AppDatabase? = null 46 | 47 | fun getInstance(context: Context): AppDatabase { 48 | return instance ?: synchronized(this) { 49 | instance ?: buildDatabase(context).also { instance = it } 50 | } 51 | } 52 | 53 | // Create and pre-populate the database. See this article for more details: 54 | // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785 55 | private fun buildDatabase(context: Context): AppDatabase { 56 | return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) 57 | .addCallback( 58 | object : RoomDatabase.Callback() { 59 | override fun onCreate(db: SupportSQLiteDatabase) { 60 | super.onCreate(db) 61 | val request = OneTimeWorkRequestBuilder() 62 | .setInputData(workDataOf(KEY_FILENAME to PLANT_DATA_FILENAME)) 63 | .build() 64 | WorkManager.getInstance(context).enqueue(request) 65 | } 66 | } 67 | ) 68 | .build() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/Converters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.TypeConverter 20 | import java.util.Calendar 21 | 22 | /** 23 | * Type converters to allow Room to reference complex data types. 24 | */ 25 | class Converters { 26 | @TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis 27 | 28 | @TypeConverter fun datestampToCalendar(value: Long): Calendar = 29 | Calendar.getInstance().apply { timeInMillis = value } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlanting.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.ColumnInfo 20 | import androidx.room.Entity 21 | import androidx.room.ForeignKey 22 | import androidx.room.Index 23 | import androidx.room.PrimaryKey 24 | import java.util.Calendar 25 | 26 | /** 27 | * [GardenPlanting] represents when a user adds a [Plant] to their garden, with useful metadata. 28 | * Properties such as [lastWateringDate] are used for notifications (such as when to water the 29 | * plant). 30 | * 31 | * Declaring the column info allows for the renaming of variables without implementing a 32 | * database migration, as the column name would not change. 33 | */ 34 | @Entity( 35 | tableName = "garden_plantings", 36 | foreignKeys = [ 37 | ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"]) 38 | ], 39 | indices = [Index("plant_id")] 40 | ) 41 | data class GardenPlanting( 42 | @ColumnInfo(name = "plant_id") val plantId: String, 43 | 44 | /** 45 | * Indicates when the [Plant] was planted. Used for showing notification when it's time 46 | * to harvest the plant. 47 | */ 48 | @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(), 49 | 50 | /** 51 | * Indicates when the [Plant] was last watered. Used for showing notification when it's 52 | * time to water the plant. 53 | */ 54 | @ColumnInfo(name = "last_watering_date") 55 | val lastWateringDate: Calendar = Calendar.getInstance() 56 | ) { 57 | @PrimaryKey(autoGenerate = true) 58 | @ColumnInfo(name = "id") 59 | var gardenPlantingId: Long = 0 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.Dao 20 | import androidx.room.Delete 21 | import androidx.room.Insert 22 | import androidx.room.Query 23 | import androidx.room.Transaction 24 | import kotlinx.coroutines.flow.Flow 25 | 26 | /** 27 | * The Data Access Object for the [GardenPlanting] class. 28 | */ 29 | @Dao 30 | interface GardenPlantingDao { 31 | @Query("SELECT * FROM garden_plantings") 32 | fun getGardenPlantings(): Flow> 33 | 34 | @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)") 35 | fun isPlanted(plantId: String): Flow 36 | 37 | /** 38 | * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle 39 | * the object mapping. 40 | */ 41 | @Transaction 42 | @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)") 43 | fun getPlantedGardens(): Flow> 44 | 45 | @Insert 46 | suspend fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long 47 | 48 | @Delete 49 | suspend fun deleteGardenPlanting(gardenPlanting: GardenPlanting) 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import javax.inject.Inject 20 | import javax.inject.Singleton 21 | 22 | @Singleton 23 | class GardenPlantingRepository @Inject constructor( 24 | private val gardenPlantingDao: GardenPlantingDao 25 | ) { 26 | 27 | suspend fun createGardenPlanting(plantId: String) { 28 | val gardenPlanting = GardenPlanting(plantId) 29 | gardenPlantingDao.insertGardenPlanting(gardenPlanting) 30 | } 31 | 32 | suspend fun removeGardenPlanting(gardenPlanting: GardenPlanting) { 33 | gardenPlantingDao.deleteGardenPlanting(gardenPlanting) 34 | } 35 | 36 | fun isPlanted(plantId: String) = 37 | gardenPlantingDao.isPlanted(plantId) 38 | 39 | fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens() 40 | 41 | companion object { 42 | 43 | // For Singleton instantiation 44 | @Volatile private var instance: GardenPlantingRepository? = null 45 | 46 | fun getInstance(gardenPlantingDao: GardenPlantingDao) = 47 | instance ?: synchronized(this) { 48 | instance ?: GardenPlantingRepository(gardenPlantingDao).also { instance = it } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/Plant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.ColumnInfo 20 | import androidx.room.Entity 21 | import androidx.room.PrimaryKey 22 | import java.util.Calendar 23 | import java.util.Calendar.DAY_OF_YEAR 24 | 25 | @Entity(tableName = "plants") 26 | data class Plant( 27 | @PrimaryKey @ColumnInfo(name = "id") val plantId: String, 28 | val name: String, 29 | val description: String, 30 | val growZoneNumber: Int, 31 | val wateringInterval: Int = 7, // how often the plant should be watered, in days 32 | val imageUrl: String = "" 33 | ) { 34 | 35 | /** 36 | * Determines if the plant should be watered. Returns true if [since]'s date > date of last 37 | * watering + watering Interval; false otherwise. 38 | */ 39 | fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) = 40 | since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) } 41 | 42 | override fun toString() = name 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/PlantAndGardenPlantings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.Embedded 20 | import androidx.room.Relation 21 | 22 | /** 23 | * This class captures the relationship between a [Plant] and a user's [GardenPlanting], which is 24 | * used by Room to fetch the related entities. 25 | */ 26 | data class PlantAndGardenPlantings( 27 | @Embedded 28 | val plant: Plant, 29 | 30 | @Relation(parentColumn = "id", entityColumn = "plant_id") 31 | val gardenPlantings: List = emptyList() 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/PlantDao.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.room.Dao 20 | import androidx.room.Query 21 | import androidx.room.Upsert 22 | import kotlinx.coroutines.flow.Flow 23 | 24 | /** 25 | * The Data Access Object for the Plant class. 26 | */ 27 | @Dao 28 | interface PlantDao { 29 | @Query("SELECT * FROM plants ORDER BY name") 30 | fun getPlants(): Flow> 31 | 32 | @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name") 33 | fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): Flow> 34 | 35 | @Query("SELECT * FROM plants WHERE id = :plantId") 36 | fun getPlant(plantId: String): Flow 37 | 38 | @Upsert 39 | suspend fun upsertAll(plants: List) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/PlantRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import javax.inject.Inject 20 | import javax.inject.Singleton 21 | 22 | /** 23 | * Repository module for handling data operations. 24 | * 25 | * Collecting from the Flows in [PlantDao] is main-safe. Room supports Coroutines and moves the 26 | * query execution off of the main thread. 27 | */ 28 | @Singleton 29 | class PlantRepository @Inject constructor(private val plantDao: PlantDao) { 30 | 31 | fun getPlants() = plantDao.getPlants() 32 | 33 | fun getPlant(plantId: String) = plantDao.getPlant(plantId) 34 | 35 | fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) = 36 | plantDao.getPlantsWithGrowZoneNumber(growZoneNumber) 37 | 38 | companion object { 39 | 40 | // For Singleton instantiation 41 | @Volatile private var instance: PlantRepository? = null 42 | 43 | fun getInstance(plantDao: PlantDao) = 44 | instance ?: synchronized(this) { 45 | instance ?: PlantRepository(plantDao).also { instance = it } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPagingSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.paging.PagingSource 20 | import androidx.paging.PagingState 21 | import com.google.samples.apps.sunflower.api.UnsplashService 22 | 23 | private const val UNSPLASH_STARTING_PAGE_INDEX = 1 24 | 25 | class UnsplashPagingSource( 26 | private val service: UnsplashService, 27 | private val query: String 28 | ) : PagingSource() { 29 | 30 | override suspend fun load(params: LoadParams): LoadResult { 31 | val page = params.key ?: UNSPLASH_STARTING_PAGE_INDEX 32 | return try { 33 | val response = service.searchPhotos(query, page, params.loadSize) 34 | val photos = response.results 35 | LoadResult.Page( 36 | data = photos, 37 | prevKey = if (page == UNSPLASH_STARTING_PAGE_INDEX) null else page - 1, 38 | nextKey = if (page == response.totalPages) null else page + 1 39 | ) 40 | } catch (exception: Exception) { 41 | LoadResult.Error(exception) 42 | } 43 | } 44 | 45 | override fun getRefreshKey(state: PagingState): Int? { 46 | return state.anchorPosition?.let { anchorPosition -> 47 | // This loads starting from previous page, but since PagingConfig.initialLoadSize spans 48 | // multiple pages, the initial load will still load items centered around 49 | // anchorPosition. This also prevents needing to immediately launch prepend due to 50 | // prefetchDistance. 51 | state.closestPageToPosition(anchorPosition)?.prevKey 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPhoto.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import com.google.gson.annotations.SerializedName 20 | 21 | /** 22 | * Data class that represents a photo from Unsplash. 23 | * 24 | * Not all of the fields returned from the API are represented here; only the ones used in this 25 | * project are listed below. For a full list of fields, consult the API documentation 26 | * [here](https://unsplash.com/documentation#get-a-photo). 27 | */ 28 | data class UnsplashPhoto( 29 | @field:SerializedName("id") val id: String, 30 | @field:SerializedName("urls") val urls: UnsplashPhotoUrls, 31 | @field:SerializedName("user") val user: UnsplashUser 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashPhotoUrls.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import com.google.gson.annotations.SerializedName 20 | 21 | /** 22 | * Data class that represents URLs available for a Unsplash photo. 23 | * 24 | * Although several photo sizes are available, this project uses only uses the `small` sized photo. 25 | * For more details, consult the API documentation 26 | * [here](https://unsplash.com/documentation#example-image-use). 27 | */ 28 | data class UnsplashPhotoUrls( 29 | @field:SerializedName("small") val small: String 30 | ) 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import androidx.paging.Pager 20 | import androidx.paging.PagingConfig 21 | import androidx.paging.PagingData 22 | import com.google.samples.apps.sunflower.api.UnsplashService 23 | import kotlinx.coroutines.flow.Flow 24 | import javax.inject.Inject 25 | 26 | class UnsplashRepository @Inject constructor(private val service: UnsplashService) { 27 | 28 | fun getSearchResultStream(query: String): Flow> { 29 | return Pager( 30 | config = PagingConfig(enablePlaceholders = false, pageSize = NETWORK_PAGE_SIZE), 31 | pagingSourceFactory = { UnsplashPagingSource(service, query) } 32 | ).flow 33 | } 34 | 35 | companion object { 36 | private const val NETWORK_PAGE_SIZE = 25 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashSearchResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import com.google.gson.annotations.SerializedName 20 | 21 | /** 22 | * Data class that represents a photo search response from Unsplash. 23 | * 24 | * Not all of the fields returned from the API are represented here; only the ones used in this 25 | * project are listed below. For a full list of fields, consult the API documentation 26 | * [here](https://unsplash.com/documentation#search-photos). 27 | */ 28 | data class UnsplashSearchResponse( 29 | @field:SerializedName("results") val results: List, 30 | @field:SerializedName("total_pages") val totalPages: Int 31 | ) 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/data/UnsplashUser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import com.google.gson.annotations.SerializedName 20 | 21 | /** 22 | * Data class that represents a user from Unsplash. 23 | * 24 | * Not all of the fields returned from the API are represented here; only the ones used in this 25 | * project are listed below. For a full list of fields, consult the API documentation 26 | * [here](https://unsplash.com/documentation#get-a-users-public-profile). 27 | */ 28 | data class UnsplashUser( 29 | @field:SerializedName("name") val name: String, 30 | @field:SerializedName("username") val username: String 31 | ) { 32 | val attributionUrl: String 33 | get() { 34 | return "https://unsplash.com/$username?utm_source=sunflower&utm_medium=referral" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.di 18 | 19 | import android.content.Context 20 | import com.google.samples.apps.sunflower.data.AppDatabase 21 | import com.google.samples.apps.sunflower.data.GardenPlantingDao 22 | import com.google.samples.apps.sunflower.data.PlantDao 23 | import dagger.Module 24 | import dagger.Provides 25 | import dagger.hilt.InstallIn 26 | import dagger.hilt.android.qualifiers.ApplicationContext 27 | import dagger.hilt.components.SingletonComponent 28 | import javax.inject.Singleton 29 | 30 | @InstallIn(SingletonComponent::class) 31 | @Module 32 | class DatabaseModule { 33 | 34 | @Singleton 35 | @Provides 36 | fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase { 37 | return AppDatabase.getInstance(context) 38 | } 39 | 40 | @Provides 41 | fun providePlantDao(appDatabase: AppDatabase): PlantDao { 42 | return appDatabase.plantDao() 43 | } 44 | 45 | @Provides 46 | fun provideGardenPlantingDao(appDatabase: AppDatabase): GardenPlantingDao { 47 | return appDatabase.gardenPlantingDao() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.di 18 | 19 | import com.google.samples.apps.sunflower.api.UnsplashService 20 | import dagger.Module 21 | import dagger.Provides 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.components.SingletonComponent 24 | import javax.inject.Singleton 25 | 26 | @InstallIn(SingletonComponent::class) 27 | @Module 28 | class NetworkModule { 29 | 30 | @Singleton 31 | @Provides 32 | fun provideUnsplashService(): UnsplashService { 33 | return UnsplashService.create() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/ui/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.ui 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val md_theme_light_primary = Color(0xFF246D00) 22 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 23 | val md_theme_light_primaryContainer = Color(0xFFA6F780) 24 | val md_theme_light_onPrimaryContainer = Color(0xFF062100) 25 | val md_theme_light_secondary = Color(0xFF55624C) 26 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 27 | val md_theme_light_secondaryContainer = Color(0xFFD8E7CB) 28 | val md_theme_light_onSecondaryContainer = Color(0xFF131F0D) 29 | val md_theme_light_tertiary = Color(0xFF386667) 30 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 31 | val md_theme_light_tertiaryContainer = Color(0xFFBBEBEC) 32 | val md_theme_light_onTertiaryContainer = Color(0xFF002021) 33 | val md_theme_light_error = Color(0xFFBA1A1A) 34 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 35 | val md_theme_light_onError = Color(0xFFFFFFFF) 36 | val md_theme_light_onErrorContainer = Color(0xFF410002) 37 | val md_theme_light_background = Color(0xFFFDFDF6) 38 | val md_theme_light_onBackground = Color(0xFF1A1C18) 39 | val md_theme_light_surface = Color(0xFFFDFDF6) 40 | val md_theme_light_onSurface = Color(0xFF1A1C18) 41 | val md_theme_light_surfaceVariant = Color(0xFFDFE4D7) 42 | val md_theme_light_onSurfaceVariant = Color(0xFF43483E) 43 | val md_theme_light_outline = Color(0xFF73796D) 44 | val md_theme_light_inverseOnSurface = Color(0xFFF1F1EA) 45 | val md_theme_light_inverseSurface = Color(0xFF2F312D) 46 | val md_theme_light_inversePrimary = Color(0xFF8BDA67) 47 | val md_theme_light_shadow = Color(0xFF000000) 48 | val md_theme_light_surfaceTint = Color(0xFF246D00) 49 | val md_theme_light_outlineVariant = Color(0xFFC3C8BB) 50 | val md_theme_light_scrim = Color(0xFF000000) 51 | 52 | val md_theme_dark_primary = Color(0xFF8BDA67) 53 | val md_theme_dark_onPrimary = Color(0xFF0F3900) 54 | val md_theme_dark_primaryContainer = Color(0xFF195200) 55 | val md_theme_dark_onPrimaryContainer = Color(0xFFA6F780) 56 | val md_theme_dark_secondary = Color(0xFFBCCBB0) 57 | val md_theme_dark_onSecondary = Color(0xFF273421) 58 | val md_theme_dark_secondaryContainer = Color(0xFF3E4A36) 59 | val md_theme_dark_onSecondaryContainer = Color(0xFFD8E7CB) 60 | val md_theme_dark_tertiary = Color(0xFFA0CFD0) 61 | val md_theme_dark_onTertiary = Color(0xFF003738) 62 | val md_theme_dark_tertiaryContainer = Color(0xFF1E4E4F) 63 | val md_theme_dark_onTertiaryContainer = Color(0xFFBBEBEC) 64 | val md_theme_dark_error = Color(0xFFFFB4AB) 65 | val md_theme_dark_errorContainer = Color(0xFF93000A) 66 | val md_theme_dark_onError = Color(0xFF690005) 67 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 68 | val md_theme_dark_background = Color(0xFF1A1C18) 69 | val md_theme_dark_onBackground = Color(0xFFE3E3DC) 70 | val md_theme_dark_surface = Color(0xFF1A1C18) 71 | val md_theme_dark_onSurface = Color(0xFFE3E3DC) 72 | val md_theme_dark_surfaceVariant = Color(0xFF43483E) 73 | val md_theme_dark_onSurfaceVariant = Color(0xFFC3C8BB) 74 | val md_theme_dark_outline = Color(0xFF8D9287) 75 | val md_theme_dark_inverseOnSurface = Color(0xFF1A1C18) 76 | val md_theme_dark_inverseSurface = Color(0xFFE3E3DC) 77 | val md_theme_dark_inversePrimary = Color(0xFF246D00) 78 | val md_theme_dark_shadow = Color(0xFF000000) 79 | val md_theme_dark_surfaceTint = Color(0xFF8BDA67) 80 | val md_theme_dark_outlineVariant = Color(0xFF43483E) 81 | val md_theme_dark_scrim = Color(0xFF000000) 82 | 83 | val seed = Color(0xFF256F00) 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/ui/Shapes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.ui 18 | 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.material3.Shapes 21 | import androidx.compose.ui.unit.dp 22 | 23 | val Shapes = Shapes( 24 | small = RoundedCornerShape( 25 | topStart = 0.dp, 26 | topEnd = 12.dp, 27 | bottomStart = 12.dp, 28 | bottomEnd = 0.dp 29 | ), 30 | medium = RoundedCornerShape( 31 | topStart = 0.dp, 32 | topEnd = 12.dp, 33 | bottomStart = 12.dp, 34 | bottomEnd = 0.dp 35 | ) 36 | ) 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/ui/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.ui 18 | 19 | import androidx.compose.material3.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.sp 23 | 24 | 25 | val Typography = Typography( 26 | displaySmall = TextStyle( 27 | fontWeight = FontWeight.Normal, 28 | fontSize = 36.sp 29 | ), 30 | headlineSmall = TextStyle( 31 | fontWeight = FontWeight.Normal, 32 | fontSize = 30.sp 33 | ), 34 | labelSmall = TextStyle( 35 | fontWeight = FontWeight.Normal, 36 | fontSize = 13.sp 37 | ), 38 | titleSmall = TextStyle( 39 | fontWeight = FontWeight.Bold, 40 | fontSize = 14.sp 41 | ), 42 | titleMedium = TextStyle( 43 | fontWeight = FontWeight.SemiBold, 44 | letterSpacing = (.5).sp, 45 | fontSize = 18.sp 46 | ), 47 | titleLarge = TextStyle( 48 | fontWeight = FontWeight.Normal, 49 | fontSize = 24.sp 50 | ), 51 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/utilities/Constants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | /** 20 | * Constants used throughout the app. 21 | */ 22 | const val DATABASE_NAME = "sunflower-db" 23 | const val PLANT_DATA_FILENAME = "plants.json" 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/utilities/GrowZoneUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | import kotlin.math.abs 20 | 21 | /** 22 | * A helper function to determine a Plant's growing zone for a given latitude. 23 | * 24 | * The numbers listed here are roughly based on the United States Department of Agriculture's 25 | * Plant Hardiness Zone Map (http://planthardiness.ars.usda.gov/), which helps determine which 26 | * plants are most likely to thrive at a location. 27 | * 28 | * If a given latitude falls on the border between two zone ranges, the larger zone range is chosen 29 | * (e.g. latitude 14.0 => zone 12). 30 | * 31 | * Negative latitude values are converted to positive with [Math.abs]. 32 | * 33 | * For latitude values greater than max (90.0), zone 1 is returned. 34 | */ 35 | fun getZoneForLatitude(latitude: Double) = when (abs(latitude)) { 36 | in 0.0..7.0 -> 13 37 | in 7.0..14.0 -> 12 38 | in 14.0..21.0 -> 11 39 | in 21.0..28.0 -> 10 40 | in 28.0..35.0 -> 9 41 | in 35.0..42.0 -> 8 42 | in 42.0..49.0 -> 7 43 | in 49.0..56.0 -> 6 44 | in 56.0..63.0 -> 5 45 | in 63.0..70.0 -> 4 46 | in 70.0..77.0 -> 3 47 | in 77.0..84.0 -> 2 48 | else -> 1 // Remaining latitudes are assigned to zone 1. 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GalleryViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.viewmodels 18 | 19 | import androidx.lifecycle.SavedStateHandle 20 | import androidx.lifecycle.ViewModel 21 | import androidx.lifecycle.viewModelScope 22 | import androidx.paging.PagingData 23 | import androidx.paging.cachedIn 24 | import com.google.samples.apps.sunflower.data.UnsplashPhoto 25 | import com.google.samples.apps.sunflower.data.UnsplashRepository 26 | import dagger.hilt.android.lifecycle.HiltViewModel 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.MutableStateFlow 29 | import kotlinx.coroutines.flow.filterNotNull 30 | import kotlinx.coroutines.flow.first 31 | import kotlinx.coroutines.launch 32 | import javax.inject.Inject 33 | 34 | @HiltViewModel 35 | class GalleryViewModel @Inject constructor( 36 | savedStateHandle: SavedStateHandle, 37 | private val repository: UnsplashRepository 38 | ) : ViewModel() { 39 | 40 | private var queryString: String? = savedStateHandle["plantName"] 41 | 42 | 43 | private val _plantPictures = MutableStateFlow?>(null) 44 | val plantPictures: Flow> get() = _plantPictures.filterNotNull() 45 | 46 | init { 47 | refreshData() 48 | } 49 | 50 | 51 | fun refreshData() { 52 | 53 | viewModelScope.launch { 54 | try { 55 | _plantPictures.value = repository.getSearchResultStream(queryString ?: "").cachedIn(viewModelScope).first() 56 | } catch (e: Exception) { 57 | e.printStackTrace() 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.viewmodels 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import com.google.samples.apps.sunflower.data.GardenPlantingRepository 22 | import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings 23 | import dagger.hilt.android.lifecycle.HiltViewModel 24 | import kotlinx.coroutines.flow.SharingStarted 25 | import kotlinx.coroutines.flow.StateFlow 26 | import kotlinx.coroutines.flow.stateIn 27 | import javax.inject.Inject 28 | 29 | @HiltViewModel 30 | class GardenPlantingListViewModel @Inject internal constructor( 31 | gardenPlantingRepository: GardenPlantingRepository 32 | ) : ViewModel() { 33 | val plantAndGardenPlantings: StateFlow> = 34 | gardenPlantingRepository 35 | .getPlantedGardens() 36 | .stateIn( 37 | viewModelScope, 38 | SharingStarted.WhileSubscribed(5000), 39 | emptyList() 40 | ) 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantAndGardenPlantingsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.viewmodels 18 | 19 | import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings 20 | import java.text.SimpleDateFormat 21 | import java.util.Locale 22 | 23 | class PlantAndGardenPlantingsViewModel(plantings: PlantAndGardenPlantings) { 24 | private val plant = checkNotNull(plantings.plant) 25 | private val gardenPlanting = plantings.gardenPlantings[0] 26 | 27 | val waterDateString: String = dateFormat.format(gardenPlanting.lastWateringDate.time) 28 | val wateringInterval 29 | get() = plant.wateringInterval 30 | val imageUrl 31 | get() = plant.imageUrl 32 | val plantName 33 | get() = plant.name 34 | val plantDateString: String = dateFormat.format(gardenPlanting.plantDate.time) 35 | val plantId 36 | get() = plant.plantId 37 | 38 | companion object { 39 | private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.viewmodels 18 | 19 | import androidx.lifecycle.LiveData 20 | import androidx.lifecycle.MutableLiveData 21 | import androidx.lifecycle.SavedStateHandle 22 | import androidx.lifecycle.ViewModel 23 | import androidx.lifecycle.asLiveData 24 | import androidx.lifecycle.viewModelScope 25 | import com.google.samples.apps.sunflower.BuildConfig 26 | import com.google.samples.apps.sunflower.data.GardenPlantingRepository 27 | import com.google.samples.apps.sunflower.data.PlantRepository 28 | import dagger.hilt.android.lifecycle.HiltViewModel 29 | import kotlinx.coroutines.flow.SharingStarted 30 | import kotlinx.coroutines.flow.stateIn 31 | import kotlinx.coroutines.launch 32 | import javax.inject.Inject 33 | 34 | /** 35 | * The ViewModel used in [PlantDetailsScreen]. 36 | */ 37 | @HiltViewModel 38 | class PlantDetailViewModel @Inject constructor( 39 | savedStateHandle: SavedStateHandle, 40 | plantRepository: PlantRepository, 41 | private val gardenPlantingRepository: GardenPlantingRepository, 42 | ) : ViewModel() { 43 | 44 | val plantId: String = savedStateHandle.get(PLANT_ID_SAVED_STATE_KEY)!! 45 | 46 | val isPlanted = gardenPlantingRepository.isPlanted(plantId) 47 | .stateIn( 48 | viewModelScope, 49 | SharingStarted.WhileSubscribed(5000), 50 | false 51 | ) 52 | val plant = plantRepository.getPlant(plantId).asLiveData() 53 | 54 | private val _showSnackbar = MutableLiveData(false) 55 | val showSnackbar: LiveData 56 | get() = _showSnackbar 57 | 58 | fun addPlantToGarden() { 59 | viewModelScope.launch { 60 | gardenPlantingRepository.createGardenPlanting(plantId) 61 | _showSnackbar.value = true 62 | } 63 | } 64 | 65 | fun dismissSnackbar() { 66 | _showSnackbar.value = false 67 | } 68 | 69 | fun hasValidUnsplashKey() = (BuildConfig.UNSPLASH_ACCESS_KEY != "null") 70 | 71 | companion object { 72 | private const val PLANT_ID_SAVED_STATE_KEY = "plantId" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/sunflower/workers/SeedDatabaseWorker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.workers 18 | 19 | import android.content.Context 20 | import android.util.Log 21 | import androidx.work.CoroutineWorker 22 | import androidx.work.WorkerParameters 23 | import com.google.gson.Gson 24 | import com.google.gson.reflect.TypeToken 25 | import com.google.gson.stream.JsonReader 26 | import com.google.samples.apps.sunflower.data.AppDatabase 27 | import com.google.samples.apps.sunflower.data.Plant 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.withContext 30 | 31 | class SeedDatabaseWorker( 32 | context: Context, 33 | workerParams: WorkerParameters 34 | ) : CoroutineWorker(context, workerParams) { 35 | override suspend fun doWork(): Result = withContext(Dispatchers.IO) { 36 | try { 37 | val filename = inputData.getString(KEY_FILENAME) 38 | if (filename != null) { 39 | applicationContext.assets.open(filename).use { inputStream -> 40 | JsonReader(inputStream.reader()).use { jsonReader -> 41 | val plantType = object : TypeToken>() {}.type 42 | val plantList: List = Gson().fromJson(jsonReader, plantType) 43 | 44 | val database = AppDatabase.getInstance(applicationContext) 45 | database.plantDao().upsertAll(plantList) 46 | 47 | Result.success() 48 | } 49 | } 50 | } else { 51 | Log.e(TAG, "Error seeding database - no valid filename") 52 | Result.failure() 53 | } 54 | } catch (ex: Exception) { 55 | Log.e(TAG, "Error seeding database", ex) 56 | Result.failure() 57 | } 58 | } 59 | 60 | companion object { 61 | private const val TAG = "SeedDatabaseWorker" 62 | const val KEY_FILENAME = "PLANT_DATA_FILENAME" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_list_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_my_garden_active.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_library.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_plant_list_active.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_plant_description.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | গাছ বৃদ্ধি এলাকা বাছাই 20 | গাছের ছবি 21 | আমার বাগান 22 | গাছের তালিকা 23 | মজুদ চারাগাছ 24 | গাছের বিস্তারিত 25 | গাছ লাগান 26 | বাগানে এই গাছ টি লাগানো হয়েছে 27 | আপনার বাগানে কোনো গাছ নেই 28 | বপন সম্পন্ন হয়েছে 29 | সর্বশেষ জল দেত্তয়া 30 | শেয়ার 31 | অ্যানড্রয়েড সানফ্লাওয়ার অ্যাপে আমার %s গাছ টি দেখুন 32 | 33 | 34 | জল দেয়া প্রয়োজন 35 | 36 | প্রতি দিন 37 | প্রতি %d দিন 38 | 39 | 40 | 41 | আগামী কাল জল দিন। 42 | জল দিন %d দিন পর। 43 | 44 | 45 | 46 | গাছের ছবি 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/values-ca/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Filtra per zona de creixement 19 | Imatge de la planta 20 | El meu jardí 21 | Llista de plantes 22 | Plantes disponibles 23 | Detalls de la planta 24 | Afegeix una planta 25 | La planta s\'ha afegit al jardí 26 | El jardí és buit 27 | Plantada 28 | Regada per darrera vegada 29 | Comparteix 30 | Mira la planta %s a l\'aplicació Android Sunflower 31 | 32 | 33 | Cal regar-la 34 | 35 | diàriament 36 | cada %d dies 37 | 38 | 39 | 40 | cal regar-la demà. 41 | cal regar-la d\'aquí a %d dies. 42 | 43 | 44 | 45 | Imatge de la planta 46 | (translate me) Photos by Unsplash 47 | (translate me) Navigate to gallery screen 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Nach Wachstumszone filtern 19 | Bild der Pflanze 20 | Mein Garten 21 | Pflanzenliste 22 | Verfügbare Pflanzen 23 | Details zur Pflanze 24 | Pflanze hinzufügen 25 | Pflanze wurde zum Garten hinzugefügt 26 | Ihr Garten ist unbepflanzt 27 | Gepflanzt 28 | Zuletzt gegossen 29 | Teilen 30 | Schau dir die %s Pflanze in der App "Sunflower" an 31 | 32 | 33 | Wasserbedarf 34 | 35 | jeden Tag 36 | alle %d Tage 37 | 38 | 39 | 40 | Morgen gießen 41 | In %d Tagen gießen 42 | 43 | 44 | 45 | Bild der Pflanze 46 | (translate me) Photos by Unsplash 47 | (translate me) Navigate to gallery screen 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Filtrar por zona de crecimiento 19 | Imagen de la planta 20 | Mi jardín 21 | Lista de plantas 22 | Plantas disponibles 23 | Detalles de la planta 24 | Añadir planta 25 | Planta añadida al jardín 26 | Tu jardín esta vacío 27 | Plantada 28 | Última vez regada 29 | Compartir 30 | Revisa la planta %s en la aplicación Android Sunflower 31 | 32 | 33 | Necesita ser regada 34 | 35 | diariamente. 36 | cada %d días. 37 | 38 | 39 | 40 | regar mañana. 41 | regar en %d días. 42 | 43 | 44 | 45 | Imagen de la planta 46 | (translate me) Photos by Unsplash 47 | (translate me) Navigate to gallery screen 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Filtrer par zone de croissance 19 | Mon jardin 20 | Image de plante 21 | Liste des plantes 22 | Plantes disponibles 23 | Détails sur la plante 24 | Ajouter une plante 25 | Plante ajoutée à votre jardin 26 | Votre jardin est vide 27 | Plantée 28 | Dernier arrosage 29 | Partager 30 | Regardez la plante %s sur l\'application Sunflower 31 | 32 | 33 | Arrosage nécessaire 34 | 35 | %d fois par jour 36 | Tous les %d jours 37 | 38 | 39 | 40 | à arroser dans %d jour. 41 | à arroser dans %d jours. 42 | 43 | 44 | 45 | Image de plante 46 | (translate me) Photos by Unsplash 47 | (translate me) Navigate to gallery screen 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Filtra per zona di crescita 19 | Immagine della pianta 20 | Il mio giardino 21 | Lista delle piante 22 | (translate me) Available Plants 23 | Dettagli della pianta 24 | Condividi 25 | (translate me) Planted 26 | (translate me) Last Watered 27 | Il tuo giardino è vuoto 28 | (translate me) Add plant 29 | Pianta aggiunta al giardino 30 | Controlla la pianta %s nell\'app Sunflower 31 | 32 | 33 | Esigenze di irrigazione 34 | 35 | ogni giorno 36 | ogni %d giorni 37 | 38 | 39 | 40 | annaffia domani. 41 | annaffia tra %d giorni. 42 | 43 | 44 | 45 | Immagine della pianta 46 | (translate me) Photos by Unsplash 47 | (translate me) Navigate to gallery screen 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 成長度合いによって分ける 19 | 植物のイメージ 20 | 私の庭 21 | 植物のリスト 22 | 選択できる植物 23 | 植物の詳細 24 | 植物を追加する 25 | 植物が庭に追加されました 26 | あなたの庭に植物はありません 27 | 植えた日 28 | 最後に水をあげた日 29 | 共有 30 | %s という植物を「Sunflower」アプリで見てみてください 31 | 32 | 33 | 34 | 水の必要量 35 | 36 | %d日ごと 37 | 38 | 39 | 40 | %d日後に水をあげてください 41 | 42 | 43 | 44 | 植物の写真 45 | (translate me) Photos by Unsplash 46 | (translate me) Navigate to gallery screen 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | Filtrar por zona de cultivo 21 | Meu jardim 22 | Lista de plantas 23 | Plantas disponíveis 24 | Detalhes da planta 25 | Adicionar planta 26 | Adicionou planta ao jardim 27 | Seu jardim está vazio 28 | Plantada 29 | Regado pela última vez 30 | Partilhar 31 | Confira o %s da planta na aplicação Sunflower 32 | Imagens tiradas do Unsplash 33 | 34 | 35 | Necessidades de rega 36 | 37 | todos dias 38 | a cada %d dias 39 | 40 | 41 | 42 | regar amanhã. 43 | regar dentro de %d dias. 44 | 45 | 46 | 47 | Foto da planta 48 | Navegue para a tela da galeria 49 | Imagem da planta 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Отфильтровать по зоне выращивания 19 | Изображение растения 20 | Мои сад 21 | Растения 22 | Доступные растения 23 | Добавить растение 24 | Ваш сад пуст 25 | Поделиться 26 | О растении 27 | Растение добавлено в ваш сад 28 | Посажен 29 | Последний полив 30 | Попробуйте %s в приложении Android Sunflower 31 | 32 | 33 | Необходим полив 34 | 35 | раз в %d день 36 | каждый %d дня 37 | каждые %d дней 38 | каждые %d дней 39 | 40 | 41 | 42 | полить через %d день 43 | полить через %d дня 44 | полить через %d дней 45 | полить через %d дней 46 | 47 | 48 | 49 | Изображение растения 50 | (translate me) Photos by Unsplash 51 | (translate me) Navigate to gallery screen 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/values-sv-rSE/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | Filtrera efter odlingszon 20 | Foto på växten 21 | Min trädgård 22 | Växtlista 23 | Tillgängliga växter 24 | Växtdetaljer 25 | Lägg till växt 26 | Växt tillagd i trädgård 27 | Din trädgård är tom 28 | Planterad 29 | Senast vattnad 30 | Dela 31 | Titta på %s-växten i Android Sunflower-appen 32 | 33 | 34 | Vattningsbehov 35 | 36 | varje dag. 37 | efter %d dagar 38 | 39 | 40 | 41 | vattna imorgon. 42 | vattna om %d dagar. 43 | 44 | 45 | 46 | Bild på växt 47 | Foton från Unsplash 48 | Navigera till galleriet 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Yetişme bölgesine göre filtrele 19 | Bitkinin görüntüsü 20 | Bahçem 21 | Bitki listesi 22 | Mevcut Bitkiler 23 | Bitki detayları 24 | Bitki Ekle 25 | Bitki bahçeye eklendi 26 | Bahçen boş 27 | Ekiliş Tarihi 28 | Son Sulama Tarihi 29 | Paylaş 30 | %s bitkisini Android Sunflower uygulamasında inceleyin 31 | 32 | 33 | Sulama gereksinimi: 34 | 35 | her gün 36 | %d günde bir 37 | 38 | 39 | 40 | yarın sulayın. 41 | %d gün içinde sulayın. 42 | 43 | 44 | 45 | Bitkinin görüntüsü 46 | Unsplash Fotoğrafları 47 | Galeri ekranına git 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-vi/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | Lọc theo vùng phát triển 19 | Vườn của tôi 20 | Danh sách cây 21 | Cây Hiện Có 22 | Chi tiết cây 23 | Thêm cây 24 | Đã thêm cây vào vườn 25 | Vườn của bạn đang trống 26 | Đã trồng 27 | Lần cuối tưới 28 | Chia sẻ 29 | Xem cây %s tại ứng dụng Android Sunflower 30 | Hình ảnh từ Unsplash 31 | 32 | 33 | Cần tưới 34 | 35 | hàng ngày 36 | mỗi %d ngày 37 | 38 | 39 | 40 | tưới vào ngày mai. 41 | tưới trong %d ngày. 42 | 43 | 44 | 45 | Hình ảnh của cây 46 | Trở lại 47 | Đi đến thư viện 48 | Hình của cây 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 按生长区筛选 19 | 植物图片 20 | 我的花园 21 | 植物目录 22 | 植物品种 23 | 植物介绍 24 | 添加植物 25 | 添加了新植物 26 | 花园里还没有植物 27 | 种下日期 28 | 最后浇水 29 | 分享 30 | 在安卓 Sunflower APP 上看看这 %s 31 | 32 | 33 | 34 | 浇水指南 35 | 36 | 每隔 %d 天 37 | 38 | 39 | 40 | %d 天后浇水 41 | 42 | 43 | 44 | 植物图片 45 | 图片来源:Unsplash 46 | 跳转到图库 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 按生長區篩選 19 | 植物圖片 20 | 我的花園 21 | 植物目錄 22 | 植物品種 23 | 植物介紹 24 | 添加植物 25 | 添加了新植物 26 | 花園裡還沒有植物 27 | 種下日期 28 | 最後澆水 29 | 分享 30 | 在安卓 Sunflower APP 上看看這 %s 31 | 32 | 33 | 34 | 澆水指南 35 | 36 | 每隔 %d 天 37 | 38 | 39 | 40 | %d 天後澆水 41 | 42 | 43 | 44 | 植物圖片 45 | 圖片來源:Unsplash 46 | 跳轉到圖庫 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 16dp 23 | 24 | 25 | 72dp 26 | 27 | 278dp 28 | 29 | 16dp 30 | 8dp 31 | 4dp 32 | 33 | 48dp 34 | 35 | 95dp 36 | 37 | 12dp 38 | 26dp 39 | 2dp 40 | 12dp 41 | 42 | 43 | 5dp 44 | 45 | 46 | 24dp 47 | 48 | 72dp 49 | 50 | 51 | 555dp 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/test/java/com/google/samples/apps/sunflower/data/ConvertersTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import org.junit.Assert.assertEquals 20 | import org.junit.Test 21 | import java.util.Calendar 22 | import java.util.Calendar.DAY_OF_MONTH 23 | import java.util.Calendar.MONTH 24 | import java.util.Calendar.SEPTEMBER 25 | import java.util.Calendar.YEAR 26 | 27 | internal class ConvertersTest { 28 | 29 | private val cal = Calendar.getInstance().apply { 30 | set(YEAR, 1998) 31 | set(MONTH, SEPTEMBER) 32 | set(DAY_OF_MONTH, 4) 33 | } 34 | 35 | @Test fun calendarToDatestamp() { 36 | assertEquals(cal.timeInMillis, Converters().calendarToDatestamp(cal)) 37 | } 38 | 39 | @Test fun datestampToCalendar() { 40 | assertEquals(Converters().datestampToCalendar(cal.timeInMillis), cal) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/test/java/com/google/samples/apps/sunflower/data/GardenPlantingTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import com.google.samples.apps.sunflower.test.CalendarMatcher.Companion.equalTo 20 | import org.hamcrest.CoreMatchers.`is` 21 | import org.hamcrest.MatcherAssert.assertThat 22 | import org.junit.Test 23 | import java.util.Calendar 24 | 25 | internal class GardenPlantingTest { 26 | 27 | @Test 28 | fun testDefaultValues() { 29 | val gardenPlanting = GardenPlanting("1") 30 | val calendar = Calendar.getInstance() 31 | assertThat(gardenPlanting.plantDate, equalTo(calendar)) 32 | assertThat(gardenPlanting.lastWateringDate, equalTo(calendar)) 33 | assertThat(gardenPlanting.gardenPlantingId, `is`(0L)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/com/google/samples/apps/sunflower/data/PlantTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.data 18 | 19 | import org.junit.Assert.assertEquals 20 | import org.junit.Assert.assertFalse 21 | import org.junit.Assert.assertTrue 22 | import org.junit.Before 23 | import org.junit.Test 24 | import java.util.Calendar 25 | import java.util.Calendar.DAY_OF_YEAR 26 | 27 | internal class PlantTest { 28 | 29 | private lateinit var plant: Plant 30 | 31 | @Before fun setUp() { 32 | plant = Plant("1", "Tomato", "A red vegetable", 1, 2, "") 33 | } 34 | 35 | @Test fun test_default_values() { 36 | val defaultPlant = Plant("2", "Apple", "Description", 1) 37 | assertEquals(7, defaultPlant.wateringInterval) 38 | assertEquals("", defaultPlant.imageUrl) 39 | } 40 | 41 | @Test fun test_shouldBeWatered() { 42 | Calendar.getInstance().let { now -> 43 | // Generate lastWateringDate from being as copy of now. 44 | val lastWateringDate = Calendar.getInstance() 45 | 46 | // Test for lastWateringDate is today. 47 | lastWateringDate.time = now.time 48 | assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -0) })) 49 | 50 | // Test for lastWateringDate is yesterday. 51 | lastWateringDate.time = now.time 52 | assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -1) })) 53 | 54 | // Test for lastWateringDate is the day before yesterday. 55 | lastWateringDate.time = now.time 56 | assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -2) })) 57 | 58 | // Test for lastWateringDate is some days ago, three days ago, four days ago etc. 59 | lastWateringDate.time = now.time 60 | assertTrue(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -3) })) 61 | } 62 | } 63 | 64 | @Test fun test_toString() { 65 | assertEquals("Tomato", plant.toString()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/test/java/com/google/samples/apps/sunflower/test/CalendarMatcher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.test 18 | 19 | import org.hamcrest.Description 20 | import org.hamcrest.Factory 21 | import org.hamcrest.Matcher 22 | import org.hamcrest.TypeSafeDiagnosingMatcher 23 | import java.text.SimpleDateFormat 24 | import java.util.Calendar 25 | import java.util.Calendar.DAY_OF_MONTH 26 | import java.util.Calendar.MONTH 27 | import java.util.Calendar.YEAR 28 | 29 | /** 30 | * Calendar matcher. 31 | * Only Year/Month/Day precision is needed for comparing GardenPlanting Calendar entries 32 | */ 33 | internal class CalendarMatcher( 34 | private val expected: Calendar 35 | ) : TypeSafeDiagnosingMatcher() { 36 | private val formatter = SimpleDateFormat("dd.MM.yyyy") 37 | 38 | override fun describeTo(description: Description?) { 39 | description?.appendText(formatter.format(expected.time)) 40 | } 41 | 42 | override fun matchesSafely(actual: Calendar?, mismatchDescription: Description?): Boolean { 43 | if (actual == null) { 44 | mismatchDescription?.appendText("was null") 45 | return false 46 | } 47 | if (actual.get(YEAR) == expected.get(YEAR) && 48 | actual.get(MONTH) == expected.get(MONTH) && 49 | actual.get(DAY_OF_MONTH) == expected.get(DAY_OF_MONTH) 50 | ) 51 | return true 52 | mismatchDescription?.appendText("was ")?.appendText(formatter.format(actual.time)) 53 | return false 54 | } 55 | 56 | companion object { 57 | /** 58 | * Creates a matcher for [Calendar]s that only matches when year, month and day of 59 | * actual calendar are equal to year, month and day of expected calendar. 60 | * 61 | * For example: 62 | * assertThat(someDate, hasSameDateWith(Calendar.getInstance())) 63 | * 64 | * @param expected calendar that has expected year, month and day [Calendar] 65 | */ 66 | @Factory 67 | fun equalTo(expected: Calendar): Matcher = CalendarMatcher(expected) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/test/java/com/google/samples/apps/sunflower/utilities/GrowZoneUtilTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.utilities 18 | 19 | import org.junit.Assert.assertEquals 20 | import org.junit.Test 21 | 22 | internal class GrowZoneUtilTest { 23 | 24 | @Test fun getZoneForLatitude() { 25 | assertEquals(13, getZoneForLatitude(0.0)) 26 | assertEquals(13, getZoneForLatitude(7.0)) 27 | assertEquals(12, getZoneForLatitude(7.1)) 28 | assertEquals(1, getZoneForLatitude(84.1)) 29 | assertEquals(1, getZoneForLatitude(90.0)) 30 | } 31 | 32 | @Test fun getZoneForLatitude_negativeLatitudes() { 33 | assertEquals(13, getZoneForLatitude(-7.0)) 34 | assertEquals(12, getZoneForLatitude(-7.1)) 35 | assertEquals(1, getZoneForLatitude(-84.1)) 36 | assertEquals(1, getZoneForLatitude(-90.0)) 37 | } 38 | 39 | // Bugfix test for https://github.com/android/sunflower/issues/8 40 | @Test fun getZoneForLatitude_GitHub_issue8() { 41 | assertEquals(9, getZoneForLatitude(35.0)) 42 | assertEquals(8, getZoneForLatitude(42.0)) 43 | assertEquals(7, getZoneForLatitude(49.0)) 44 | assertEquals(6, getZoneForLatitude(56.0)) 45 | assertEquals(5, getZoneForLatitude(63.0)) 46 | assertEquals(4, getZoneForLatitude(70.0)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | buildscript { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | plugins { 25 | alias(libs.plugins.android.application) apply false 26 | alias(libs.plugins.kotlin.android) apply false 27 | alias(libs.plugins.hilt) apply false 28 | alias(libs.plugins.spotless) 29 | alias(libs.plugins.ksp) apply false 30 | alias(libs.plugins.android.test) apply false 31 | alias(libs.plugins.gradle.versions) 32 | alias(libs.plugins.version.catalog.update) 33 | alias(libs.plugins.compose.compiler) 34 | } 35 | 36 | apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") 37 | -------------------------------------------------------------------------------- /buildscripts/init.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | val ktlintVersion = "0.46.1" 18 | 19 | initscript { 20 | val spotlessVersion = "6.10.0" 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | dependencies { 27 | classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion") 28 | } 29 | } 30 | 31 | allprojects { 32 | if (this == rootProject) { 33 | return@allprojects 34 | } 35 | apply() 36 | extensions.configure { 37 | kotlin { 38 | target("**/*.kt") 39 | targetExclude("**/build/**/*.kt") 40 | ktlint(ktlintVersion).editorConfigOverride( 41 | mapOf( 42 | "ktlint_code_style" to "android", 43 | "ij_kotlin_allow_trailing_comma" to true, 44 | // These rules were introduced in ktlint 0.46.0 and should not be 45 | // enabled without further discussion. They are disabled for now. 46 | // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 47 | "disabled_rules" to 48 | "filename," + 49 | "annotation,annotation-spacing," + 50 | "argument-list-wrapping," + 51 | "double-colon-spacing," + 52 | "enum-entry-name-case," + 53 | "multiline-if-else," + 54 | "no-empty-first-line-in-method-block," + 55 | "package-name," + 56 | "trailing-comma," + 57 | "spacing-around-angle-brackets," + 58 | "spacing-between-declarations-with-annotations," + 59 | "spacing-between-declarations-with-comments," + 60 | "unary-op-spacing" 61 | ) 62 | ) 63 | licenseHeaderFile(rootProject.file("spotless/copyright.kt")) 64 | } 65 | format("kts") { 66 | target("**/*.kts") 67 | targetExclude("**/build/**/*.kts") 68 | // Look for the first line that doesn't have a block comment (assumed to be the license) 69 | licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /buildscripts/toml-updater-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | versionCatalogUpdate { 18 | sortByKey.set(true) 19 | 20 | keep { 21 | // keep versions without any library or plugin reference 22 | keepUnusedVersions.set(true) 23 | // keep all libraries that aren't used in the project 24 | keepUnusedLibraries.set(true) 25 | // keep all plugins that aren't used in the project 26 | keepUnusedPlugins.set(true) 27 | } 28 | } 29 | 30 | def isNonStable = { String version -> 31 | def regex = /^[0-9,.v-]+(-r)?$/ 32 | return !(version ==~ regex) 33 | } 34 | 35 | tasks.named("dependencyUpdates").configure { 36 | resolutionStrategy { 37 | componentSelection { 38 | all { 39 | if (isNonStable(it.candidate.version) && !isNonStable(it.currentVersion)) { 40 | reject('Release candidate') 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/MigrationJourney.md: -------------------------------------------------------------------------------- 1 | # Migrating Sunflower to Compose 2 | 3 | Originally, Sunflower was meant to showcase best practices for several Jetpack libraries such as Jetpack Navigation, Room, ViewPager2, and more. Given this starting point, Sunflower now demonstrates how you can migrate a View-based app to Compose so that you can take a similar approach to migrate your app to Compose. 4 | 5 | As of 2022, Sunflower demonstrates a partially migrated View-based app. The intent is to fully migrate Sunflower so that the UI layer is only in Compose. 6 | 7 | This document captures the high-level strategy, as well as links to issues/pull requests for migration steps, taken to migrate the rest of the app to be Compose-only. 8 | 9 | The general steps followed to migrate Sunflower to Compose are: 10 | 11 | 1. Planning the migration approach 12 | 2. Migrate existing screens to Compose one by one 13 | 3. Migrate Navigation Component to Compose and remove Fragments 14 | 15 | ## #1 Planning the migration approach 16 | 17 | Status: Done ✅ 18 | 19 | Before starting the migration process, it's best to think about the overall strategy to follow to incrementally migrate the entire app to Compose. The recommended [migration strategy](https://developer.android.com/jetpack/compose/interop/migration-strategy) is to start introducing Compose by using it for new features you build. In Sunflower's case, we won't be adding new features, instead, each screen needs to be migrated to Compose one by one followed by replacing Fragment-based navigation with Navigation Compose. This approach is also called the *bottom-up* approach to migration. 20 | 21 | See [migration strategy](https://developer.android.com/jetpack/compose/interop/migration-strategy) to learn more. 22 | 23 | ## #2 Migrate existing screens one by one 24 | 25 | Status: Done ✅ 26 | 27 | The Sunflower app has 5 distinct screens. Each screen needs to be migrated to Compose before moving to step 3. You can see the linked issues/pull requests below to follow progress or learn more about how each screen was migrated. 28 | 29 | | Screen | Status | 30 | |-----------------------|------------------------------------------------------------| 31 | | GalleryFragment | Done [#819](https://github.com/android/sunflower/pull/819) | 32 | | GardenFragment | Done [#819](https://github.com/android/sunflower/pull/819) | 33 | | HomeViewPagerFragment | Done [#823](https://github.com/android/sunflower/pull/823) | 34 | | PlantListFragment | Done [#822](https://github.com/android/sunflower/pull/822) | 35 | | PlantDetailFragment | Done [#638](https://github.com/android/sunflower/pull/638) | 36 | 37 | ## #3 Migrate Navigation Component to Compose and remove Fragments 38 | 39 | Status: Done ✅ 40 | 41 | PR: [#827](https://github.com/android/sunflower/pull/827) 42 | 43 | The last step in the migration process is to replace Fragment-based navigation with Jetpack Navigation, to use [Navigation Compose](https://developer.android.com/jetpack/compose/navigation). 44 | Upon completing this step, all Fragments can be removed. -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Project-wide Gradle settings. 18 | 19 | # IDE (e.g. Android Studio) users: 20 | # Gradle settings configured through the IDE *will override* 21 | # any settings specified in this file. 22 | 23 | # For more details on how to configure your build environment visit 24 | # http://www.gradle.org/docs/current/userguide/build_environment.html 25 | 26 | # Specifies the JVM arguments used for the daemon process. 27 | # The setting is particularly useful for tweaking memory settings. 28 | android.useAndroidX=true 29 | org.gradle.jvmargs=-Xmx1536m 30 | # When configured, Gradle will run in incubating parallel mode. 31 | # This option should only be used with decoupled projects. More details, visit 32 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 33 | # org.gradle.parallel=true 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 03 11:14:53 PST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /macrobenchmark/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /macrobenchmark/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | alias(libs.plugins.android.test) 19 | alias(libs.plugins.kotlin.android) 20 | } 21 | 22 | android { 23 | compileSdk = libs.versions.compileSdk.get().toInt() 24 | 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_17 27 | targetCompatibility = JavaVersion.VERSION_17 28 | } 29 | 30 | kotlinOptions { 31 | jvmTarget = JavaVersion.VERSION_17.toString() 32 | } 33 | 34 | defaultConfig { 35 | minSdk = libs.versions.minSdk.get().toInt() 36 | targetSdk = libs.versions.targetSdk.get().toInt() 37 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 38 | } 39 | 40 | buildTypes { 41 | // This benchmark buildType is used for benchmarking, and should function like your 42 | // release build (for example, with minification on). It's signed with a debug key 43 | // for easy local/CI testing. 44 | create("benchmark") { 45 | isDebuggable = true 46 | signingConfig = getByName("debug").signingConfig 47 | } 48 | } 49 | 50 | targetProjectPath = ":app" 51 | namespace = "com.google.samples.apps.sunflower.macrobenchmark" 52 | experimentalProperties["android.experimental.self-instrumenting"] = true 53 | } 54 | 55 | dependencies { 56 | implementation(libs.androidx.test.ext.junit) 57 | implementation(libs.androidx.espresso.core) 58 | implementation(libs.androidx.test.uiautomator) 59 | implementation(libs.androidx.benchmark.macro.junit4) 60 | } 61 | 62 | androidComponents { 63 | beforeVariants(selector().all()) { 64 | it.enabled = it.buildType == "benchmark" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/java/com/google/samples/apps/sunflower/macrobenchmark/BaselineProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.macrobenchmark 18 | 19 | import androidx.benchmark.macro.junit4.BaselineProfileRule 20 | import androidx.test.ext.junit.runners.AndroidJUnit4 21 | import androidx.test.uiautomator.By 22 | import androidx.test.uiautomator.Until 23 | import org.junit.Rule 24 | import org.junit.Test 25 | import org.junit.runner.RunWith 26 | 27 | @RunWith(AndroidJUnit4::class) 28 | class BaselineProfileGenerator { 29 | 30 | @get:Rule 31 | val rule = BaselineProfileRule() 32 | 33 | @Test 34 | fun startPlantListPlantDetail() { 35 | rule.collect(PACKAGE_NAME) { 36 | // start the app flow 37 | pressHome() 38 | startActivityAndWait() 39 | 40 | // go to plant list flow 41 | val plantListTab = device.findObject(By.descContains("Plant list")) 42 | plantListTab.click() 43 | device.waitForIdle() 44 | // sleep for animations to settle 45 | Thread.sleep(500) 46 | 47 | // go to plant detail flow 48 | val plantList = device.findObject(By.res(packageName, "plant_list")) 49 | val listItem = plantList.children[0] 50 | listItem.click() 51 | device.wait(Until.gone(By.res(packageName, "plant_list")), 5_000) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/java/com/google/samples/apps/sunflower/macrobenchmark/PlantDetailBenchmarks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.macrobenchmark 18 | 19 | import androidx.benchmark.macro.CompilationMode 20 | import androidx.benchmark.macro.FrameTimingMetric 21 | import androidx.benchmark.macro.MacrobenchmarkScope 22 | import androidx.benchmark.macro.StartupMode 23 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 24 | import androidx.test.ext.junit.runners.AndroidJUnit4 25 | import androidx.test.uiautomator.By 26 | import androidx.test.uiautomator.Until 27 | import org.junit.Rule 28 | import org.junit.Test 29 | import org.junit.runner.RunWith 30 | 31 | @RunWith(AndroidJUnit4::class) 32 | class PlantDetailBenchmarks { 33 | @get:Rule 34 | val benchmarkRule = MacrobenchmarkRule() 35 | 36 | @Test 37 | fun plantDetailCompilationNone() = benchmarkPlantDetail(CompilationMode.None()) 38 | 39 | @Test 40 | fun plantDetailCompilationPartial() = benchmarkPlantDetail(CompilationMode.Partial()) 41 | 42 | @Test 43 | fun plantDetailCompilationFull() = benchmarkPlantDetail(CompilationMode.Full()) 44 | 45 | private fun benchmarkPlantDetail(compilationMode: CompilationMode) = 46 | benchmarkRule.measureRepeated( 47 | packageName = PACKAGE_NAME, 48 | metrics = listOf(FrameTimingMetric()), 49 | compilationMode = compilationMode, 50 | iterations = 10, 51 | startupMode = StartupMode.COLD, 52 | setupBlock = { 53 | startActivityAndWait() 54 | goToPlantListTab() 55 | } 56 | ) { 57 | goToPlantDetail() 58 | } 59 | } 60 | 61 | fun MacrobenchmarkScope.goToPlantDetail(index: Int? = null) { 62 | val plantListSelector = By.res(packageName, "plant_list") 63 | val recycler = device.findObject(plantListSelector) 64 | 65 | // select different item each iteration, but only from the visible ones 66 | val currentChildIndex = index ?: ((iteration ?: 0) % recycler.childCount) 67 | 68 | val child = recycler.children[currentChildIndex] 69 | child.click() 70 | // wait until plant list is gone 71 | device.wait(Until.gone(plantListSelector), 5_000) 72 | } 73 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/java/com/google/samples/apps/sunflower/macrobenchmark/PlantListBenchmarks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.macrobenchmark 18 | 19 | import androidx.benchmark.macro.CompilationMode 20 | import androidx.benchmark.macro.FrameTimingMetric 21 | import androidx.benchmark.macro.MacrobenchmarkScope 22 | import androidx.benchmark.macro.StartupMode 23 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 24 | import androidx.test.ext.junit.runners.AndroidJUnit4 25 | import androidx.test.uiautomator.By 26 | import androidx.test.uiautomator.Until 27 | import org.junit.Rule 28 | import org.junit.Test 29 | import org.junit.runner.RunWith 30 | 31 | @RunWith(AndroidJUnit4::class) 32 | class PlantListBenchmarks { 33 | @get:Rule 34 | val benchmarkRule = MacrobenchmarkRule() 35 | 36 | @Test 37 | fun openPlantList() = openPlantList(CompilationMode.None()) 38 | 39 | @Test 40 | fun plantListCompilationPartial() = openPlantList(CompilationMode.Partial()) 41 | 42 | @Test 43 | fun plantListCompilationFull() = openPlantList(CompilationMode.Full()) 44 | 45 | private fun openPlantList(compilationMode: CompilationMode) = 46 | benchmarkRule.measureRepeated( 47 | packageName = PACKAGE_NAME, 48 | metrics = listOf(FrameTimingMetric()), 49 | compilationMode = compilationMode, 50 | iterations = 5, 51 | startupMode = StartupMode.COLD, 52 | setupBlock = { 53 | pressHome() 54 | // Start the default activity, but don't measure the frames yet 55 | startActivityAndWait() 56 | } 57 | ) { 58 | goToPlantListTab() 59 | } 60 | } 61 | 62 | fun MacrobenchmarkScope.goToPlantListTab() { 63 | // Find the tab with plants list 64 | val plantListTab = device.findObject(By.descContains("Plant list")) 65 | plantListTab.click() 66 | 67 | // Wait until plant list has children 68 | val recyclerHasChild = By.hasChild(By.res(packageName, "plant_list")) 69 | device.wait(Until.hasObject(recyclerHasChild), 5_000) 70 | 71 | // Wait until idle 72 | device.waitForIdle() 73 | } 74 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/java/com/google/samples/apps/sunflower/macrobenchmark/StartupBenchmarks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.macrobenchmark 18 | 19 | import androidx.benchmark.macro.BaselineProfileMode 20 | import androidx.benchmark.macro.CompilationMode 21 | import androidx.benchmark.macro.StartupMode 22 | import androidx.benchmark.macro.StartupTimingMetric 23 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 24 | import androidx.test.ext.junit.runners.AndroidJUnit4 25 | import androidx.test.uiautomator.By 26 | import androidx.test.uiautomator.Until 27 | import org.junit.Rule 28 | import org.junit.Test 29 | import org.junit.runner.RunWith 30 | 31 | /** 32 | * This is an example startup benchmark. 33 | * 34 | * It navigates to the device's home screen, and launches the default activity. 35 | * 36 | * Before running this benchmark: 37 | * 1) switch your app's active build variant in the Studio (affects Studio runs only) 38 | * 2) add `` to your app's manifest, within the `` tag 39 | * 40 | * Run this benchmark from Studio to see startup measurements, and captured system traces 41 | * for investigating your app's performance. 42 | */ 43 | @RunWith(AndroidJUnit4::class) 44 | class StartupBenchmarks { 45 | @get:Rule 46 | val benchmarkRule = MacrobenchmarkRule() 47 | 48 | @Test 49 | fun startupCompilationNone() = startup(CompilationMode.None()) 50 | 51 | @Test 52 | fun startupCompilationPartial() = startup(CompilationMode.Partial()) 53 | 54 | @Test 55 | fun startupCompilationWarmup() = 56 | startup(CompilationMode.Partial(BaselineProfileMode.Disable, 2)) 57 | 58 | private fun startup(compilationMode: CompilationMode) = 59 | benchmarkRule.measureRepeated( 60 | packageName = PACKAGE_NAME, 61 | metrics = listOf(StartupTimingMetric()), 62 | iterations = 5, 63 | compilationMode = compilationMode, 64 | startupMode = StartupMode.COLD, 65 | setupBlock = { 66 | pressHome() 67 | } 68 | ) { 69 | startActivityAndWait() 70 | 71 | // wait for the content called by reportFullyDrawn is visible 72 | val recyclerHasChild = By.hasChild(By.res(packageName, "garden_list")) 73 | device.wait(Until.hasObject(recyclerHasChild), 5_000) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /macrobenchmark/src/main/java/com/google/samples/apps/sunflower/macrobenchmark/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.sunflower.macrobenchmark 18 | 19 | const val PACKAGE_NAME = "com.google.samples.apps.sunflower" 20 | -------------------------------------------------------------------------------- /screenshots/SunflowerM3Screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/SunflowerM3Screenshots.png -------------------------------------------------------------------------------- /screenshots/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/ic_launcher-web.png -------------------------------------------------------------------------------- /screenshots/icon_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/icon_background.png -------------------------------------------------------------------------------- /screenshots/icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/icon_foreground.png -------------------------------------------------------------------------------- /screenshots/jetpack_donut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/jetpack_donut.png -------------------------------------------------------------------------------- /screenshots/phone_my_garden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/phone_my_garden.png -------------------------------------------------------------------------------- /screenshots/phone_plant_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/phone_plant_detail.png -------------------------------------------------------------------------------- /screenshots/phone_plant_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/phone_plant_list.png -------------------------------------------------------------------------------- /screenshots/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/screenshots.png -------------------------------------------------------------------------------- /screenshots/sunflower.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/android/sunflower/2a357a31551bb53f3fe80382a9ce6d30bcc8b960/screenshots/sunflower.gif -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | pluginManagement { 18 | repositories { 19 | gradlePluginPortal() 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | dependencyResolutionManagement { 26 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 27 | repositories { 28 | google() 29 | mavenCentral() 30 | } 31 | } 32 | 33 | include(":app") 34 | include(":macrobenchmark") 35 | --------------------------------------------------------------------------------