diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties
new file mode 100644
index 000000000..c349fe239
--- /dev/null
+++ b/.github/ci-gradle.properties
@@ -0,0 +1,23 @@
+#
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.gradle.daemon=false
+org.gradle.parallel=true
+org.gradle.jvmargs=-Xmx5120m
+org.gradle.workers.max=2
+
+kotlin.incremental=false
+kotlin.compiler.execution.strategy=in-process
\ No newline at end of file
diff --git a/.github/workflows/AccessibilityCodelab.yaml b/.github/workflows/AccessibilityCodelab.yaml
new file mode 100644
index 000000000..0c645e81e
--- /dev/null
+++ b/.github/workflows/AccessibilityCodelab.yaml
@@ -0,0 +1,61 @@
+name: AccessibilityCodelab
+
+on:
+ push:
+ branches:
+ - main
+ - end
+ paths:
+ - 'AccessibilityCodelab/**'
+ pull_request:
+ paths:
+ - 'AccessibilityCodelab/**'
+
+env:
+ SAMPLE_PATH: AccessibilityCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/AdvancedStateAndSideEffectsCodelab.yaml b/.github/workflows/AdvancedStateAndSideEffectsCodelab.yaml
new file mode 100644
index 000000000..dd0a9c3ae
--- /dev/null
+++ b/.github/workflows/AdvancedStateAndSideEffectsCodelab.yaml
@@ -0,0 +1,117 @@
+name: AdvancedStateAndSideEffectsCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'AdvancedStateAndSideEffectsCodelab/**'
+ pull_request:
+ paths:
+ - 'AdvancedStateAndSideEffectsCodelab/**'
+
+env:
+ SAMPLE_PATH: AdvancedStateAndSideEffectsCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
+
+ test:
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ matrix:
+ api-level: [26, 29]
+
+ steps:
+ # https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners
+ - name: Enable KVM group perms
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+ ls /dev/kvm
+
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Run instrumentation tests
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: google_apis
+ arch: x86
+ disable-animations: true
+ script: ./gradlew connectedCheck --stacktrace
+ working-directory: ${{ env.SAMPLE_PATH }}
+
+ - name: Upload test reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: test-reports-state-${{ matrix.api-level }}
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/AnimationCodelab.yaml b/.github/workflows/AnimationCodelab.yaml
new file mode 100644
index 000000000..15f729030
--- /dev/null
+++ b/.github/workflows/AnimationCodelab.yaml
@@ -0,0 +1,60 @@
+name: AnimationCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'AnimationCodelab/**'
+ pull_request:
+ paths:
+ - 'AnimationCodelab/**'
+
+env:
+ SAMPLE_PATH: AnimationCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/BasicLayoutsCodelab.yaml b/.github/workflows/BasicLayoutsCodelab.yaml
new file mode 100644
index 000000000..158806511
--- /dev/null
+++ b/.github/workflows/BasicLayoutsCodelab.yaml
@@ -0,0 +1,60 @@
+name: BasicLayoutsCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'BasicLayoutsCodelab/**'
+ pull_request:
+ paths:
+ - 'BasicLayoutsCodelab/**'
+
+env:
+ SAMPLE_PATH: BasicLayoutsCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
\ No newline at end of file
diff --git a/.github/workflows/BasicStateCodelab.yaml b/.github/workflows/BasicStateCodelab.yaml
new file mode 100644
index 000000000..dd70acae7
--- /dev/null
+++ b/.github/workflows/BasicStateCodelab.yaml
@@ -0,0 +1,60 @@
+name: BasicStateCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'BasicStateCodelab/**'
+ pull_request:
+ paths:
+ - 'BasicStateCodelab/**'
+
+env:
+ SAMPLE_PATH: BasicStateCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
\ No newline at end of file
diff --git a/.github/workflows/BasicsCodelab.yaml b/.github/workflows/BasicsCodelab.yaml
new file mode 100644
index 000000000..8b9ffdecc
--- /dev/null
+++ b/.github/workflows/BasicsCodelab.yaml
@@ -0,0 +1,60 @@
+name: BasicsCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'BasicsCodelab/**'
+ pull_request:
+ paths:
+ - 'BasicsCodelab/**'
+
+env:
+ SAMPLE_PATH: BasicsCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
\ No newline at end of file
diff --git a/.github/workflows/MigrationCodelab.yaml b/.github/workflows/MigrationCodelab.yaml
new file mode 100644
index 000000000..a47e8a8f4
--- /dev/null
+++ b/.github/workflows/MigrationCodelab.yaml
@@ -0,0 +1,108 @@
+name: MigrationCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'MigrationCodelab/**'
+ pull_request:
+ paths:
+ - 'MigrationCodelab/**'
+
+env:
+ SAMPLE_PATH: MigrationCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
+
+ test:
+ needs: build
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ matrix:
+ api-level: [26, 29]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Run instrumentation tests
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ arch: x86
+ disable-animations: true
+ script: ./gradlew connectedCheck --stacktrace
+ working-directory: ${{ env.SAMPLE_PATH }}
+
+ - name: Upload test reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: test-reports-migration-${{ matrix.api-level }}
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/NavigationCodelab.yaml b/.github/workflows/NavigationCodelab.yaml
new file mode 100644
index 000000000..f17d32c48
--- /dev/null
+++ b/.github/workflows/NavigationCodelab.yaml
@@ -0,0 +1,61 @@
+name: NavigationCodelab
+
+on:
+ push:
+ branches:
+ - main
+ - end
+ paths:
+ - 'NavigationCodelab/**'
+ pull_request:
+ paths:
+ - 'NavigationCodelab/**'
+
+env:
+ SAMPLE_PATH: NavigationCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/TestingCodelab.yaml b/.github/workflows/TestingCodelab.yaml
new file mode 100644
index 000000000..94ad8111a
--- /dev/null
+++ b/.github/workflows/TestingCodelab.yaml
@@ -0,0 +1,61 @@
+name: TestingCodelab
+
+on:
+ push:
+ branches:
+ - main
+ - end
+ paths:
+ - 'TestingCodelab/**'
+ pull_request:
+ paths:
+ - 'TestingCodelab/**'
+
+env:
+ SAMPLE_PATH: TestingCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
diff --git a/.github/workflows/ThemingCodelab.yaml b/.github/workflows/ThemingCodelab.yaml
new file mode 100644
index 000000000..55bbef1db
--- /dev/null
+++ b/.github/workflows/ThemingCodelab.yaml
@@ -0,0 +1,60 @@
+name: ThemingCodelab
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'ThemingCodelab/**'
+ pull_request:
+ paths:
+ - 'ThemingCodelab/**'
+
+env:
+ SAMPLE_PATH: ThemingCodelab
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - name: Generate cache key
+ run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
+
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.gradle/caches/modules-*
+ ~/.gradle/caches/jars-*
+ ~/.gradle/caches/build-cache-*
+ key: gradle-${{ hashFiles('checksum.txt') }}
+
+ - name: Build project
+ working-directory: ${{ env.SAMPLE_PATH }}
+ run: ./gradlew assembleDebug lintDebug --stacktrace
+
+ - name: Upload build outputs (APKs)
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-outputs
+ path: ${{ env.SAMPLE_PATH }}/app/build/outputs
+
+ - name: Upload build reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: build-reports
+ path: ${{ env.SAMPLE_PATH }}/app/build/reports
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..d134aa5bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local
+/.idea/*
+.DS_Store
+build/
+/captures
+.externalNativeBuild
+local.properties
+.kotlin/
diff --git a/AccessibilityCodelab/.gitignore b/AccessibilityCodelab/.gitignore
new file mode 100644
index 000000000..4a708a497
--- /dev/null
+++ b/AccessibilityCodelab/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/buildSrc/.gradle/*
+.kotlin/
diff --git a/AccessibilityCodelab/ASSETS_LICENSE b/AccessibilityCodelab/ASSETS_LICENSE
new file mode 100644
index 000000000..e7fc95866
--- /dev/null
+++ b/AccessibilityCodelab/ASSETS_LICENSE
@@ -0,0 +1,88 @@
+All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 license.
+
+
+SIL OPEN FONT LICENSE
+Version 1.1 - 26 February 2007
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/AccessibilityCodelab/README.md b/AccessibilityCodelab/README.md
new file mode 100644
index 000000000..4ead5bee5
--- /dev/null
+++ b/AccessibilityCodelab/README.md
@@ -0,0 +1,22 @@
+# Accessibility in Jetpack Compose Codelab
+
+This folder contains the source code for
+the [Accessibility in Jetpack Compose Codelab](https://developer.android.com/codelabs/jetpack-compose-accessibility)
+
+## License
+
+```
+Copyright 2021 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
diff --git a/AccessibilityCodelab/app/.gitignore b/AccessibilityCodelab/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/AccessibilityCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/AccessibilityCodelab/app/build.gradle b/AccessibilityCodelab/app/build.gradle
new file mode 100644
index 000000000..f24e9a5d9
--- /dev/null
+++ b/AccessibilityCodelab/app/build.gradle
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.plugin.compose'
+}
+
+android {
+ compileSdkVersion 37
+ namespace 'com.example.jetnews'
+ defaultConfig {
+ applicationId 'com.example.jetnews'
+ minSdkVersion 23
+ targetSdkVersion 33
+ versionCode 1
+ versionName '1.0'
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ signingConfigs {
+ // We use a bundled debug keystore, to allow debug builds from CI to be upgradable
+ debug {
+ storeFile rootProject.file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ packagingOptions {
+ excludes += "/META-INF/AL2.0"
+ excludes += "/META-INF/LGPL2.1"
+ }
+}
+
+dependencies {
+ def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
+ implementation(composeBom)
+ testImplementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation "androidx.compose.runtime:runtime"
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.foundation:foundation-layout"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.material:material-icons-extended"
+ implementation "androidx.compose.foundation:foundation"
+ implementation "androidx.compose.animation:animation"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation "androidx.compose.runtime:runtime-livedata"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+ debugImplementation "androidx.compose.ui:ui-test-manifest"
+ testImplementation "androidx.compose.ui:ui-test-junit4"
+ androidTestImplementation "androidx.compose.ui:ui-test"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+
+ def accompanist_version = '0.36.0'
+ implementation "com.google.accompanist:accompanist-swiperefresh:$accompanist_version"
+ implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
+
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
+
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'androidx.activity:activity-ktx:1.13.0'
+ implementation 'androidx.core:core-ktx:1.19.0'
+ implementation "androidx.activity:activity-compose:1.13.0"
+
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.11.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0"
+
+ implementation 'androidx.navigation:navigation-compose:2.9.8'
+
+ androidTestImplementation 'androidx.test:rules:1.7.0'
+ androidTestImplementation 'androidx.test:runner:1.7.0'
+
+ // TODO: Bump to latest after Espresso 3.5.0 goes stable
+ // (due to https://github.com/robolectric/robolectric/issues/6593)
+ testImplementation 'org.robolectric:robolectric:4.16.1'
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ // Treat all Kotlin warnings as errors (disabled by default)
+ allWarningsAsErrors = project.hasProperty("warningsAsErrors") ? project.warningsAsErrors : false
+
+ freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
+ // Enable experimental coroutines APIs, including Flow
+ freeCompilerArgs += '-opt-in=kotlin.Experimental'
+
+ // Set JVM target to 1.8
+ jvmTarget = "1.8"
+ }
+}
diff --git a/AccessibilityCodelab/app/proguard-rules.pro b/AccessibilityCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..4cb94585a
--- /dev/null
+++ b/AccessibilityCodelab/app/proguard-rules.pro
@@ -0,0 +1,24 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+-renamesourcefileattribute SourceFile
+
+# Repackage classes into the top-level.
+-repackageclasses
diff --git a/AccessibilityCodelab/app/src/main/AndroidManifest.xml b/AccessibilityCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..2ef4ce5c2
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/JetnewsApplication.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/JetnewsApplication.kt
new file mode 100644
index 000000000..086db2a61
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/JetnewsApplication.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews
+
+import android.app.Application
+import com.example.jetnews.data.AppContainer
+import com.example.jetnews.data.AppContainerImpl
+
+class JetnewsApplication : Application() {
+
+ // AppContainer instance used by the rest of classes to obtain dependencies
+ lateinit var container: AppContainer
+
+ override fun onCreate() {
+ super.onCreate()
+ container = AppContainerImpl(this)
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt
new file mode 100644
index 000000000..b60382cf1
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/AppContainerImpl.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.data
+
+import android.content.Context
+import com.example.jetnews.data.interests.InterestsRepository
+import com.example.jetnews.data.posts.PostsRepository
+
+/**
+ * Dependency Injection container at the application level.
+ */
+interface AppContainer {
+ val postsRepository: PostsRepository
+ val interestsRepository: InterestsRepository
+}
+
+/**
+ * Implementation for the Dependency Injection container at the application level.
+ *
+ * Variables are initialized lazily and the same instance is shared across the whole app.
+ */
+class AppContainerImpl(private val applicationContext: Context) : AppContainer {
+
+ override val postsRepository: PostsRepository by lazy {
+ PostsRepository()
+ }
+
+ override val interestsRepository: InterestsRepository by lazy {
+ InterestsRepository()
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt
new file mode 100644
index 000000000..c272cf28d
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/interests/InterestsRepository.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.data.interests
+
+import com.example.jetnews.utils.addOrRemove
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+typealias TopicsMap = Map>
+
+/**
+ * Implementation of InterestRepository that returns a hardcoded list of
+ * topics, people and publications synchronously.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class InterestsRepository {
+
+ /**
+ * Get relevant topics to the user.
+ */
+ val topics by lazy {
+ mapOf(
+ "Android" to listOf("Jetpack Compose", "Kotlin", "Jetpack"),
+ "Programming" to listOf("Kotlin", "Declarative UIs", "Java"),
+ "Technology" to listOf("Pixel", "Google")
+ )
+ }
+
+ // for now, keep the selections in memory
+ private val selectedTopics = MutableStateFlow(setOf())
+
+ // Used to make suspend functions that read and update state safe to call from any thread
+ private val mutex = Mutex()
+
+ /**
+ * Toggle between selected and unselected
+ */
+ suspend fun toggleTopicSelection(topic: TopicSelection) {
+ mutex.withLock {
+ val set = selectedTopics.value.toMutableSet()
+ set.addOrRemove(topic)
+ selectedTopics.value = set
+ }
+ }
+
+ /**
+ * Currently selected topics
+ */
+ fun observeTopicsSelected(): Flow> = selectedTopics
+}
+
+data class TopicSelection(val section: String, val topic: String)
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsData.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsData.kt
new file mode 100644
index 000000000..86e970f20
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsData.kt
@@ -0,0 +1,1027 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.data.posts.impl
+
+import com.example.jetnews.R
+import com.example.jetnews.model.Markup
+import com.example.jetnews.model.MarkupType
+import com.example.jetnews.model.Metadata
+import com.example.jetnews.model.Paragraph
+import com.example.jetnews.model.ParagraphType
+import com.example.jetnews.model.Post
+import com.example.jetnews.model.PostAuthor
+import com.example.jetnews.model.Publication
+
+/**
+ * Define hardcoded posts to avoid handling any non-ui operations.
+ */
+
+val pietro = PostAuthor("Pietro Maggi", "https://medium.com/@pmaggi")
+val manuel = PostAuthor("Manuel Vivo", "https://medium.com/@manuelvicnt")
+val florina = PostAuthor(
+ "Florina Muntenescu",
+ "https://medium.com/@florina.muntenescu"
+)
+val jose =
+ PostAuthor("Jose Alcérreca", "https://medium.com/@JoseAlcerreca")
+
+val publication = Publication(
+ "Android Developers",
+ "https://cdn-images-1.medium.com/max/258/1*u7oZc2_5mrkcFaxkXEyfYA@2x.png"
+)
+val paragraphsPost1 = listOf(
+ Paragraph(
+ ParagraphType.Text,
+ "Working to make our Android application more modular, I ended up with a sample that included a set of on-demand features grouped inside a folder:"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Pretty standard setup, all the on-demand modules, inside a “features” folder; clean."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "These modules are included in the settings.gradle file as:"
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "include ':app'\n" +
+ "include ':features:module1'\n" +
+ "include ':features:module2'\n" +
+ "include ':features:module3'\n" +
+ "include ':features:module4'"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "These setup works nicely with a single “minor” issue: an empty module named features in the Android view in Android Studio:"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "I can live with that, but I would much prefer to remove that empty module from my project!"
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "If you cannot remove it, just rename it!"
+ ),
+
+ Paragraph(
+ ParagraphType.Text,
+ "At I/O I was lucky enough to attend the “Android Studio: Tips and Tricks” talk where Ivan Gravilovic, from Google, shared some amazing tips. One of these was a possible solution for my problem: setting a custom path for my modules.",
+ listOf(
+ Markup(
+ MarkupType.Italic,
+ 41,
+ 72
+ )
+ )
+ ),
+
+ Paragraph(
+ ParagraphType.Text,
+ "In this particular case our settings.gradle becomes:",
+ listOf(Markup(MarkupType.Code, 28, 43))
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ """
+ include ':app'
+ include ':module1'
+ include ':module1'
+ include ':module1'
+ include ':module1'
+ """.trimIndent()
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ """
+ // Set a custom path for the four features modules.
+ // This avoid to have an empty "features" module in Android Studio.
+ project(":module1").projectDir=new File(rootDir, "features/module1")
+ project(":module2").projectDir=new File(rootDir, "features/module2")
+ project(":module3").projectDir=new File(rootDir, "features/module3")
+ project(":module4").projectDir=new File(rootDir, "features/module4")
+ """.trimIndent()
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "And the layout in Android Studio is now:"
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Conclusion"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "As the title says, this is really a small thing, but it helps keep my project in order and it shows how a small Gradle configuration can help keep your project tidy."
+ ),
+ Paragraph(
+ ParagraphType.Quote,
+ "You can find this update in the latest version of the on-demand modules codelab.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 54,
+ 79,
+ "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Resources"
+ ),
+ Paragraph(
+ ParagraphType.Bullet,
+ "Android Studio: Tips and Tricks (Google I/O’19)",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 0,
+ 47,
+ "https://www.youtube.com/watch?v=ihF-PwDfRZ4&list=PLWz5rJ2EKKc9FfSQIRXEWyWpHD6TtwxMM&index=32&t=0s"
+ )
+ )
+ ),
+
+ Paragraph(
+ ParagraphType.Bullet,
+ "On Demand module codelab",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 0,
+ 24,
+ "https://codelabs.developers.google.com/codelabs/on-demand-dynamic-delivery/index.html"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Bullet,
+ "Patchwork Plaid — A modularization story",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 0,
+ 40,
+ "https://medium.com/androiddevelopers/a-patchwork-plaid-monolith-to-modularized-app-60235d9f212e"
+ )
+ )
+ )
+)
+
+val paragraphsPost2 = listOf(
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger is a popular Dependency Injection framework commonly used in Android. It provides fully static and compile-time dependencies addressing many of the development and performance issues that have reflection-based solutions.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 0,
+ 6,
+ "https://dagger.dev/"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "This month, a new tutorial was released to help you better understand how it works. This article focuses on using Dagger with Kotlin, including best practices to optimize your build time and gotchas you might encounter.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 14,
+ 26,
+ "https://dagger.dev/tutorial/"
+ ),
+ Markup(MarkupType.Bold, 114, 132),
+ Markup(MarkupType.Bold, 144, 159),
+ Markup(MarkupType.Bold, 191, 198)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger is implemented using Java’s annotations model and annotations in Kotlin are not always directly parallel with how equivalent Java code would be written. This post will highlight areas where they differ and how you can use Dagger with Kotlin without having a headache."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "This post was inspired by some of the suggestions in this Dagger issue that goes through best practices and pain points of Dagger in Kotlin. Thanks to all of the contributors that commented there!",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 58,
+ 70,
+ "https://github.com/google/dagger/issues/900"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "kapt build improvements"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "To improve your build time, Dagger added support for gradle’s incremental annotation processing in v2.18! This is enabled by default in Dagger v2.24. In case you’re using a lower version, you need to add a few lines of code (as shown below) if you want to benefit from it.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 99,
+ 104,
+ "https://github.com/google/dagger/releases/tag/dagger-2.18"
+ ),
+ Markup(
+ MarkupType.Link,
+ 143,
+ 148,
+ "https://github.com/google/dagger/releases/tag/dagger-2.24"
+ ),
+ Markup(MarkupType.Bold, 53, 95)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Also, you can tell Dagger not to format the generated code. This option was added in Dagger v2.18 and it’s the default behavior (doesn’t generate formatted code) in v2.23. If you’re using a lower version, disable code formatting to improve your build time (see code below).",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 92,
+ 97,
+ "https://github.com/google/dagger/releases/tag/dagger-2.18"
+ ),
+ Markup(
+ MarkupType.Link,
+ 165,
+ 170,
+ "https://github.com/google/dagger/releases/tag/dagger-2.23"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Include these compiler arguments in your build.gradle file to make Dagger more performant at build time:",
+ listOf(Markup(MarkupType.Code, 41, 53))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Alternatively, if you use Kotlin DSL script files, include them like this in the build.gradle.kts file of the modules that use Dagger:",
+ listOf(Markup(MarkupType.Code, 81, 97))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Qualifiers for field attributes"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "",
+ listOf(Markup(MarkupType.Link, 0, 0))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "When an annotation is placed on a property in Kotlin, it’s not clear whether Java will see that annotation on the field of the property or the method for that property. Setting the field: prefix on the annotation ensures that the qualifier ends up in the right place (See documentation for more details).",
+ listOf(
+ Markup(MarkupType.Code, 181, 187),
+ Markup(
+ MarkupType.Link,
+ 268,
+ 285,
+ "http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/"
+ ),
+ Markup(MarkupType.Italic, 114, 119),
+ Markup(MarkupType.Italic, 143, 149)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "✅ The way to apply qualifiers on an injected field is:"
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "@Inject @field:MinimumBalance lateinit var minimumBalance: BigDecimal",
+ listOf(Markup(MarkupType.Bold, 8, 29))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "❌ As opposed to:"
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ """
+ @Inject @MinimumBalance lateinit var minimumBalance: BigDecimal
+ // @MinimumBalance is ignored!
+ """.trimIndent(),
+ listOf(Markup(MarkupType.Bold, 65, 95))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Forgetting to add field: could lead to injecting the wrong object if there’s an unqualified instance of that type available in the Dagger graph.",
+ listOf(Markup(MarkupType.Code, 18, 24))
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Static @Provides functions optimization"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger’s generated code will be more performant if @Provides methods are static. To achieve this in Kotlin, use a Kotlin object instead of a class and annotate your methods with @JvmStatic. This is a best practice that you should follow as much as possible.",
+ listOf(
+ Markup(MarkupType.Code, 51, 60),
+ Markup(MarkupType.Code, 73, 79),
+ Markup(MarkupType.Code, 121, 127),
+ Markup(MarkupType.Code, 141, 146),
+ Markup(MarkupType.Code, 178, 188),
+ Markup(MarkupType.Bold, 200, 213),
+ Markup(MarkupType.Italic, 200, 213)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "In case you need an abstract method, you’ll need to add the @JvmStatic method to a companion object and annotate it with @Module too.",
+ listOf(
+ Markup(MarkupType.Code, 60, 70),
+ Markup(MarkupType.Code, 121, 128)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Alternatively, you can extract the object module out and include it in the abstract one:"
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Injecting Generics"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Kotlin compiles generics with wildcards to make Kotlin APIs work with Java. These are generated when a type appears as a parameter (more info here) or as fields. For example, a Kotlin List parameter shows up as List super Foo> in Java.",
+ listOf(
+ Markup(MarkupType.Code, 184, 193),
+ Markup(MarkupType.Code, 216, 233),
+ Markup(
+ MarkupType.Link,
+ 132,
+ 146,
+ "https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "This causes problems with Dagger because it expects an exact (aka invariant) type match. Using @JvmSuppressWildcards will ensure that Dagger sees the type without wildcards.",
+ listOf(
+ Markup(MarkupType.Code, 95, 116),
+ Markup(
+ MarkupType.Link,
+ 66,
+ 75,
+ "https://en.wikipedia.org/wiki/Class_invariant"
+ ),
+ Markup(
+ MarkupType.Link,
+ 96,
+ 116,
+ "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-suppress-wildcards/index.html"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "This is a common issue when you inject collections using Dagger’s multibinding feature, for example:",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 57,
+ 86,
+ "https://dagger.dev/multibindings.html"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ """
+ class MyVMFactory @Inject constructor(
+ private val vmMap: Map>
+ ) {
+ ...
+ }
+ """.trimIndent(),
+ listOf(Markup(MarkupType.Bold, 72, 93))
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Inline method bodies"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger determines the types that are configured by @Provides methods by inspecting the return type. Specifying the return type in Kotlin functions is optional and even the IDE sometimes encourages you to refactor your code to have inline method bodies that hide the return type declaration.",
+ listOf(Markup(MarkupType.Code, 51, 60))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "This can lead to bugs if the inferred type is different from the one you meant. Let’s see some examples:"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "If you want to add a specific type to the graph, inlining works as expected. See the different ways to do the same in Kotlin:"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "If you want to provide an implementation of an interface, then you must explicitly specify the return type. Not doing it can lead to problems and bugs:"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger mostly works with Kotlin out of the box. However, you have to watch out for a few things just to make sure you’re doing what you really mean to do: @field: for qualifiers on field attributes, inline method bodies, and @JvmSuppressWildcards when injecting collections.",
+ listOf(
+ Markup(MarkupType.Code, 155, 162),
+ Markup(MarkupType.Code, 225, 246)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Dagger optimizations come with no cost, add them and follow best practices to improve your build time: enabling incremental annotation processing, disabling formatting and using static @Provides methods in your Dagger modules.",
+ listOf(
+ Markup(
+ MarkupType.Code,
+ 185,
+ 194
+ )
+ )
+ )
+)
+
+val paragraphsPost3 = listOf(
+ Paragraph(
+ ParagraphType.Text,
+ "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by following our new Refactoring to Kotlin codelab, available in English \uD83C\uDDEC\uD83C\uDDE7, Chinese \uD83C\uDDE8\uD83C\uDDF3 and Brazilian Portuguese \uD83C\uDDE7\uD83C\uDDF7.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 151,
+ 172,
+ "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0"
+ ),
+ Markup(
+ MarkupType.Link,
+ 209,
+ 216,
+ "https://clmirror.storage.googleapis.com/codelabs/java-to-kotlin-zh/index.html#0"
+ ),
+ Markup(
+ MarkupType.Link,
+ 226,
+ 246,
+ "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "When you first get started writing Kotlin code, you tend to follow Java Programming Language idioms. The automatic converter, part of both Android Studio and Intellij IDEA, can do a pretty good job of automatically refactoring your code, but sometimes, it needs a little help. This is where our new Refactoring to Kotlin codelab comes in.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 105,
+ 124,
+ "https://www.jetbrains.com/help/idea/converting-a-java-file-to-kotlin-file.html"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "We’ll take two classes (a User and a Repository) in Java Programming Language and convert them to Kotlin, check out what the automatic converter did and why. Then we go to the next level — make it idiomatic, teaching best practices and useful tips along the way.",
+ listOf(
+ Markup(MarkupType.Code, 26, 30),
+ Markup(MarkupType.Code, 37, 47)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "The Refactoring to Kotlin codelab starts with basic topics — understand how nullability is declared in Kotlin, what types of equality are defined or how to best handle classes whose role is just to hold data. We then continue with how to handle static fields and functions in Kotlin and how to apply the Singleton pattern, with the help of one handy keyword: object. We’ll see how Kotlin helps us model our classes better, how it differentiates between a property of a class and an action the class can do. Finally, we’ll learn how to execute code only in the context of a specific object with the scope functions.",
+ listOf(
+ Markup(MarkupType.Code, 245, 251),
+ Markup(MarkupType.Code, 359, 365),
+ Markup(
+ MarkupType.Link,
+ 4,
+ 25,
+ "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Thanks to Walmyr Carvalho and Nelson Glauber for translating the codelab in Brazilian Portuguese!",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 21,
+ 42,
+ "https://codelabs.developers.google.com/codelabs/java-to-kotlin/#0"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 76,
+ 96,
+ "https://codelabs.developers.google.com/codelabs/java-to-kotlin-pt-br/#0"
+ )
+ )
+ )
+)
+
+val paragraphsPost4 = listOf(
+ Paragraph(
+ ParagraphType.Text,
+ "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "In a ViewModel, if you’re exposing data coming from resources (strings, drawables, colors…), you have to take into account that ViewModel objects ignore configuration changes such as locale changes. When the user changes their locale, activities are recreated but the ViewModel objects are not.",
+ listOf(
+ Markup(
+ MarkupType.Bold,
+ 183,
+ 197
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "AndroidViewModel is a subclass of ViewModel that is aware of the Application context. However, having access to a context can be dangerous if you’re not observing or reacting to the lifecycle of that context. The recommended practice is to avoid dealing with objects that have a lifecycle in ViewModels.",
+ listOf(
+ Markup(MarkupType.Code, 0, 16),
+ Markup(MarkupType.Code, 34, 43),
+ Markup(MarkupType.Bold, 209, 303)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Let’s look at an example based on this issue in the tracker: Updating ViewModel on system locale change.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 61,
+ 103,
+ "https://issuetracker.google.com/issues/111961971"
+ ),
+ Markup(MarkupType.Italic, 61, 104)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "The problem is that the string is resolved in the constructor only once. If there’s a locale change, the ViewModel won’t be recreated. This will result in our app showing obsolete data and therefore being only partially localized.",
+ listOf(Markup(MarkupType.Bold, 73, 133))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "As Sergey points out in the comments to the issue, the recommended approach is to expose the ID of the resource you want to load and do so in the view. As the view (activity, fragment, etc.) is lifecycle-aware it will be recreated after a configuration change so the resource will be reloaded correctly.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 3,
+ 9,
+ "https://twitter.com/ZelenetS"
+ ),
+ Markup(
+ MarkupType.Link,
+ 28,
+ 36,
+ "https://issuetracker.google.com/issues/111961971#comment2"
+ ),
+ Markup(MarkupType.Bold, 82, 150)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Even if you don’t plan to localize your app, it makes testing much easier and cleans up your ViewModel objects so there’s no reason not to future-proof."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "We fixed this issue in the android-architecture repository in the Java and Kotlin branches and we offloaded resource loading to the Data Binding layout.",
+ listOf(
+ Markup(
+ MarkupType.Link,
+ 66,
+ 70,
+ "https://github.com/googlesamples/android-architecture/pull/631"
+ ),
+ Markup(
+ MarkupType.Link,
+ 75,
+ 81,
+ "https://github.com/googlesamples/android-architecture/pull/635"
+ ),
+ Markup(
+ MarkupType.Link,
+ 128,
+ 151,
+ "https://github.com/googlesamples/android-architecture/pull/635/files#diff-7eb5d85ec3ea4e05ecddb7dc8ae20aa1R62"
+ )
+ )
+ )
+)
+
+val paragraphsPost5 = listOf(
+ Paragraph(
+ ParagraphType.Text,
+ "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of working with collections based on how they’re evaluated: eagerly — with Collections, and lazily — with Sequences. Continue reading to find out what’s the difference between the two, which one you should use and when, and what the performance implications of each are.",
+ listOf(
+ Markup(MarkupType.Code, 210, 220),
+ Markup(MarkupType.Code, 241, 249),
+ Markup(
+ MarkupType.Link,
+ 210,
+ 221,
+ "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html"
+ ),
+ Markup(
+ MarkupType.Link,
+ 241,
+ 250,
+ "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/index.html"
+ ),
+ Markup(MarkupType.Bold, 130, 134),
+ Markup(MarkupType.Bold, 195, 202),
+ Markup(MarkupType.Bold, 227, 233),
+ Markup(MarkupType.Italic, 130, 134)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Collections vs sequences"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "The difference between eager and lazy evaluation lies in when each transformation on the collection is performed.",
+ listOf(
+ Markup(
+ MarkupType.Italic,
+ 57,
+ 61
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Collections are eagerly evaluated — each operation is performed when it’s called and the result of the operation is stored in a new collection. The transformations on collections are inline functions. For example, looking at how map is implemented, we can see that it’s an inline function, that creates a new ArrayList:",
+ listOf(
+ Markup(MarkupType.Code, 229, 232),
+ Markup(MarkupType.Code, 273, 279),
+ Markup(MarkupType.Code, 309, 318),
+ Markup(
+ MarkupType.Link,
+ 183,
+ 199,
+ "https://kotlinlang.org/docs/reference/inline-functions.html"
+ ),
+ Markup(
+ MarkupType.Link,
+ 229,
+ 232,
+ "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Collections.kt#L1312"
+ ),
+ Markup(MarkupType.Bold, 0, 12),
+ Markup(MarkupType.Italic, 16, 23)
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "public inline fun Iterable.map(transform: (T) -> R): List {\n" +
+ " return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)\n" +
+ "}",
+ listOf(
+ Markup(MarkupType.Bold, 7, 13),
+ Markup(MarkupType.Bold, 88, 97)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Sequences are lazily evaluated. They have two types of operations: intermediate and terminal. Intermediate operations are not performed on the spot; they’re just stored. Only when a terminal operation is called, the intermediate operations are triggered on each element in a row and finally, the terminal operation is applied. Intermediate operations (like map, distinct, groupBy etc) return another sequence whereas terminal operations (like first, toList, count etc) don’t.",
+ listOf(
+ Markup(MarkupType.Code, 357, 360),
+ Markup(MarkupType.Code, 362, 370),
+ Markup(MarkupType.Code, 372, 379),
+ Markup(MarkupType.Code, 443, 448),
+ Markup(MarkupType.Code, 450, 456),
+ Markup(MarkupType.Code, 458, 463),
+ Markup(MarkupType.Bold, 0, 9),
+ Markup(MarkupType.Bold, 67, 79),
+ Markup(MarkupType.Bold, 84, 92),
+ Markup(MarkupType.Bold, 254, 269),
+ Markup(MarkupType.Italic, 14, 20)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Sequences don’t hold a reference to the items of the collection. They’re created based on the iterator of the original collection and keep a reference to all the intermediate operations that need to be performed."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Unlike transformations on collections, intermediate transformations on sequences are not inline functions — inline functions cannot be stored and sequences need to store them. Looking at how an intermediate operation like map is implemented, we can see that the transform function is kept in a new instance of a Sequence:",
+ listOf(
+ Markup(MarkupType.Code, 222, 225),
+ Markup(MarkupType.Code, 312, 320),
+ Markup(
+ MarkupType.Link,
+ 222,
+ 225,
+ "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L860"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "public fun Sequence.map(transform: (T) -> R): Sequence{ \n" +
+ " return TransformingSequence(this, transform)\n" +
+ "}",
+ listOf(Markup(MarkupType.Bold, 85, 105))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "A terminal operation, like first, iterates through the elements of the sequence until the predicate condition is matched.",
+ listOf(
+ Markup(MarkupType.Code, 27, 32),
+ Markup(
+ MarkupType.Link,
+ 27,
+ 32,
+ "https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/common/src/generated/_Sequences.kt#L117"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "public inline fun Sequence.first(predicate: (T) -> Boolean): T {\n" +
+ " for (element in this) if (predicate(element)) return element\n" +
+ " throw NoSuchElementException(“Sequence contains no element matching the predicate.”)\n" +
+ "}"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "If we look at how a sequence like TransformingSequence (used in the map above) is implemented, we’ll see that when next is called on the sequence iterator, the transformation stored is also applied.",
+ listOf(
+ Markup(MarkupType.Code, 34, 54),
+ Markup(MarkupType.Code, 68, 71)
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "internal class TransformingIndexedSequence \n" +
+ "constructor(private val sequence: Sequence, private val transformer: (Int, T) -> R) : Sequence {",
+ listOf(
+ Markup(
+ MarkupType.Bold,
+ 109,
+ 120
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.CodeBlock,
+ "override fun iterator(): Iterator = object : Iterator {\n" +
+ " …\n" +
+ " override fun next(): R {\n" +
+ " return transformer(checkIndexOverflow(index++), iterator.next())\n" +
+ " }\n" +
+ " …\n" +
+ "}",
+ listOf(
+ Markup(MarkupType.Bold, 83, 89),
+ Markup(MarkupType.Bold, 107, 118)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Independent on whether you’re using collections or sequences, the Kotlin Standard Library offers quite a wide range of operations for both, like find, filter, groupBy and others. Make sure you check them out, before implementing your own version of these.",
+ listOf(
+ Markup(MarkupType.Code, 145, 149),
+ Markup(MarkupType.Code, 151, 157),
+ Markup(MarkupType.Code, 159, 166),
+ Markup(
+ MarkupType.Link,
+ 193,
+ 207,
+ "https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/#functions"
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Collections and sequences"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Let’s say that we have a list of objects of different shapes. We want to make the shapes yellow and then take the first square shape."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Let’s see how and when each operation is applied for collections and when for sequences"
+ ),
+ Paragraph(
+ ParagraphType.Subhead,
+ "Collections"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "map is called — a new ArrayList is created. We iterate through all items of the initial collection, transform it by copying the original object and changing the color, then add it to the new list.",
+ listOf(Markup(MarkupType.Code, 0, 3))
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "first is called — we iterate through each item until the first square is found",
+ listOf(Markup(MarkupType.Code, 0, 5))
+ ),
+ Paragraph(
+ ParagraphType.Subhead,
+ "Sequences"
+ ),
+ Paragraph(
+ ParagraphType.Bullet,
+ "asSequence — a sequence is created based on the Iterator of the original collection",
+ listOf(Markup(MarkupType.Code, 0, 10))
+ ),
+ Paragraph(
+ ParagraphType.Bullet,
+ "map is called — the transformation is added to the list of operations needed to be performed by the sequence but the operation is NOT performed",
+ listOf(
+ Markup(MarkupType.Code, 0, 3),
+ Markup(MarkupType.Bold, 130, 133)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Bullet,
+ "first is called — this is a terminal operation, so, all the intermediate operations are triggered, on each element of the collection. We iterate through the initial collection applying map and then first on each of them. Since the condition from first is satisfied by the 2nd element, then we no longer apply the map on the rest of the collection.",
+ listOf(Markup(MarkupType.Code, 0, 5))
+ ),
+
+ Paragraph(
+ ParagraphType.Text,
+ "When working with sequences no intermediate collection is created and since items are evaluated one by one, map is only performed on some of the inputs."
+ ),
+ Paragraph(
+ ParagraphType.Header,
+ "Performance"
+ ),
+ Paragraph(
+ ParagraphType.Subhead,
+ "Order of transformations"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Independent of whether you’re using collections or sequences, the order of transformations matters. In the example above, first doesn’t need to happen after map since it’s not a consequence of the map transformation. If we reverse the order of our business logic and call first on the collection and then transform the result, then we only create one new object — the yellow square. When using sequences — we avoid creating 2 new objects, when using collections, we avoid creating an entire new list.",
+ listOf(
+ Markup(MarkupType.Code, 122, 127),
+ Markup(MarkupType.Code, 157, 160),
+ Markup(MarkupType.Code, 197, 200)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Because terminal operations can finish processing early, and intermediate operations are evaluated lazily, sequences can, in some cases, help you avoid doing unnecessary work compared to collections. Make sure you always check the order of the transformations and the dependencies between them!"
+ ),
+ Paragraph(
+ ParagraphType.Subhead,
+ "Inlining and large data sets consequences"
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Collection operations use inline functions, so the bytecode of the operation, together with the bytecode of the lambda passed to it will be inlined. Sequences don’t use inline functions, therefore, new Function objects are created for each operation.",
+ listOf(
+ Markup(
+ MarkupType.Code,
+ 202,
+ 210
+ )
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "On the other hand, collections create a new list for every transformation while sequences just keep a reference to the transformation function."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "When working with small collections, with 1–2 operators, these differences don’t have big implications so working with collections should be ok. But, when working with large lists the intermediate collection creation can become expensive; in such cases, use sequences.",
+ listOf(
+ Markup(MarkupType.Bold, 18, 35),
+ Markup(MarkupType.Bold, 119, 130),
+ Markup(MarkupType.Bold, 168, 179),
+ Markup(MarkupType.Bold, 258, 267)
+ )
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Unfortunately, I’m not aware of any benchmarking study done that would help us get a better understanding on how the performance of collections vs sequences is affected with different sizes of collections or operation chains."
+ ),
+ Paragraph(
+ ParagraphType.Text,
+ "Collections eagerly evaluate your data while sequences do so lazily. Depending on the size of your data, pick the one that fits best: collections — for small lists or sequences — for larger ones, and pay attention to the order of the transformations."
+ )
+)
+
+val post1 = Post(
+ id = "dc523f0ed25c",
+ title = "A Little Thing about Android Module Paths",
+ subtitle = "How to configure your module paths, instead of using Gradle’s default.",
+ url = "https://medium.com/androiddevelopers/gradle-path-configuration-dc523f0ed25c",
+ publication = publication,
+ metadata = Metadata(
+ author = pietro,
+ date = "August 02",
+ readTimeMinutes = 1
+ ),
+ paragraphs = paragraphsPost1,
+ imageId = R.drawable.post_1,
+ imageThumbId = R.drawable.post_1_thumb
+)
+
+val post2 = Post(
+ id = "7446d8dfd7dc",
+ title = "Dagger in Kotlin: Gotchas and Optimizations",
+ subtitle = "Use Dagger in Kotlin! This article includes best practices to optimize your build time and gotchas you might encounter.",
+ url = "https://medium.com/androiddevelopers/dagger-in-kotlin-gotchas-and-optimizations-7446d8dfd7dc",
+ publication = publication,
+ metadata = Metadata(
+ author = manuel,
+ date = "July 30",
+ readTimeMinutes = 3
+ ),
+ paragraphs = paragraphsPost2,
+ imageId = R.drawable.post_2,
+ imageThumbId = R.drawable.post_2_thumb
+)
+
+val post3 = Post(
+ id = "ac552dcc1741",
+ title = "From Java Programming Language to Kotlin — the idiomatic way",
+ subtitle = "Learn how to get started converting Java Programming Language code to Kotlin, making it more idiomatic and avoid common pitfalls, by…",
+ url = "https://medium.com/androiddevelopers/from-java-programming-language-to-kotlin-the-idiomatic-way-ac552dcc1741",
+ publication = publication,
+ metadata = Metadata(
+ author = florina,
+ date = "July 09",
+ readTimeMinutes = 1
+ ),
+ paragraphs = paragraphsPost3,
+ imageId = R.drawable.post_3,
+ imageThumbId = R.drawable.post_3_thumb
+)
+
+val post4 = Post(
+ id = "84eb677660d9",
+ title = "Locale changes and the AndroidViewModel antipattern",
+ subtitle = "TL;DR: Expose resource IDs from ViewModels to avoid showing obsolete data.",
+ url = "https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9",
+ publication = publication,
+ metadata = Metadata(
+ author = jose,
+ date = "April 02",
+ readTimeMinutes = 1
+ ),
+ paragraphs = paragraphsPost4,
+ imageId = R.drawable.post_4,
+ imageThumbId = R.drawable.post_4_thumb
+)
+
+val post5 = Post(
+ id = "55db18283aca",
+ title = "Collections and sequences in Kotlin",
+ subtitle = "Working with collections is a common task and the Kotlin Standard Library offers many great utility functions. It also offers two ways of…",
+ url = "https://medium.com/androiddevelopers/collections-and-sequences-in-kotlin-55db18283aca",
+ publication = publication,
+ metadata = Metadata(
+ author = florina,
+ date = "July 24",
+ readTimeMinutes = 4
+ ),
+ paragraphs = paragraphsPost5,
+ imageId = R.drawable.post_5,
+ imageThumbId = R.drawable.post_5_thumb
+)
+
+val posts: List =
+ listOf(
+ post1,
+ post2,
+ post3,
+ post4,
+ post5,
+ post1.copy(id = "post6"),
+ post2.copy(id = "post7"),
+ post3.copy(id = "post8"),
+ post4.copy(id = "post9"),
+ post5.copy(id = "post10")
+ )
\ No newline at end of file
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt
new file mode 100644
index 000000000..cb51202dc
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/data/posts/PostsRepository.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.data.posts
+
+import com.example.jetnews.data.posts.impl.posts
+import com.example.jetnews.model.Post
+import com.example.jetnews.utils.addOrRemove
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Simplified implementation of PostsRepository that returns a hardcoded list of
+ * posts with resources synchronously.
+ */
+class PostsRepository {
+ // for now, keep the favorites in memory
+ private val favorites = MutableStateFlow>(setOf())
+
+ /**
+ * Get a specific JetNews post.
+ */
+ fun getPost(postId: String?): Post? {
+ return posts.find { it.id == postId }
+ }
+
+ /**
+ * Get JetNews posts.
+ */
+ fun getPosts(): List {
+ return posts
+ }
+
+ /**
+ * Observe the current favorites
+ */
+ fun observeFavorites(): Flow> = favorites
+
+ /**
+ * Toggle a postId to be a favorite or not.
+ */
+ fun toggleFavorite(postId: String) {
+ val set = favorites.value.toMutableSet()
+ set.addOrRemove(postId)
+ favorites.value = set
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/model/Post.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/model/Post.kt
new file mode 100644
index 000000000..40e8977e7
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/model/Post.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.model
+
+import androidx.annotation.DrawableRes
+
+data class Post(
+ val id: String,
+ val title: String,
+ val subtitle: String? = null,
+ val url: String,
+ val publication: Publication? = null,
+ val metadata: Metadata,
+ val paragraphs: List = emptyList(),
+ @DrawableRes val imageId: Int,
+ @DrawableRes val imageThumbId: Int
+)
+
+data class Metadata(
+ val author: PostAuthor,
+ val date: String,
+ val readTimeMinutes: Int
+)
+
+data class PostAuthor(
+ val name: String,
+ val url: String? = null
+)
+
+data class Publication(
+ val name: String,
+ val logoUrl: String
+)
+
+data class Paragraph(
+ val type: ParagraphType,
+ val text: String,
+ val markups: List = emptyList()
+)
+
+data class Markup(
+ val type: MarkupType,
+ val start: Int,
+ val end: Int,
+ val href: String? = null
+)
+
+enum class MarkupType {
+ Link,
+ Code,
+ Italic,
+ Bold,
+}
+
+enum class ParagraphType {
+ Title,
+ Caption,
+ Header,
+ Subhead,
+ Text,
+ CodeBlock,
+ Quote,
+ Bullet,
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt
new file mode 100644
index 000000000..b5682d1d3
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.ListAlt
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.jetnews.R
+import com.example.jetnews.ui.theme.JetnewsTheme
+
+@Composable
+fun AppDrawer(
+ currentRoute: String,
+ navigateToHome: () -> Unit,
+ navigateToInterests: () -> Unit,
+ closeDrawer: () -> Unit
+) {
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ Spacer(Modifier.height(24.dp))
+ JetNewsLogo(Modifier.padding(16.dp))
+ Divider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = .2f))
+ DrawerButton(
+ icon = Icons.Filled.Home,
+ label = "Home",
+ isSelected = currentRoute == MainDestinations.HOME_ROUTE,
+ action = {
+ navigateToHome()
+ closeDrawer()
+ }
+ )
+
+ DrawerButton(
+ icon = Icons.Filled.ListAlt,
+ label = "Interests",
+ isSelected = currentRoute == MainDestinations.INTERESTS_ROUTE,
+ action = {
+ navigateToInterests()
+ closeDrawer()
+ }
+ )
+ }
+}
+
+@Composable
+private fun JetNewsLogo(modifier: Modifier = Modifier) {
+ Row(modifier = modifier) {
+ Image(
+ painter = painterResource(R.drawable.ic_jetnews_logo),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
+ )
+ Spacer(Modifier.width(8.dp))
+ Image(
+ painter = painterResource(R.drawable.ic_jetnews_wordmark),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+ )
+ }
+}
+
+@Composable
+private fun DrawerButton(
+ icon: ImageVector,
+ label: String,
+ isSelected: Boolean,
+ action: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val colors = MaterialTheme.colorScheme
+ val imageAlpha = if (isSelected) {
+ 1f
+ } else {
+ 0.6f
+ }
+ val textIconColor = if (isSelected) {
+ colors.primary
+ } else {
+ colors.onSurface.copy(alpha = 0.6f)
+ }
+ val backgroundColor = if (isSelected) {
+ colors.primary.copy(alpha = 0.12f)
+ } else {
+ Color.Transparent
+ }
+
+ val surfaceModifier = modifier
+ .padding(start = 8.dp, top = 8.dp, end = 8.dp)
+ .fillMaxWidth()
+ Surface(
+ modifier = surfaceModifier,
+ color = backgroundColor,
+ shape = MaterialTheme.shapes.small
+ ) {
+ TextButton(
+ onClick = action,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Image(
+ imageVector = icon,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(textIconColor),
+ alpha = imageAlpha
+ )
+ Spacer(Modifier.width(16.dp))
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ color = textIconColor
+ )
+ }
+ }
+ }
+}
+
+@Preview("Drawer contents")
+@Preview("Drawer contents (dark)", uiMode = UI_MODE_NIGHT_YES)
+@Composable
+fun PreviewAppDrawer() {
+ JetnewsTheme {
+ Surface {
+ AppDrawer(
+ currentRoute = MainDestinations.HOME_ROUTE,
+ navigateToHome = {},
+ navigateToInterests = {},
+ closeDrawer = { }
+ )
+ }
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt
new file mode 100644
index 000000000..c8e3e2e11
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsApp.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.example.jetnews.data.AppContainer
+import com.example.jetnews.ui.theme.JetnewsTheme
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import kotlinx.coroutines.launch
+
+@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "UnusedMaterial3ScaffoldPaddingParameter")
+@Composable
+fun JetnewsApp(
+ appContainer: AppContainer
+) {
+ JetnewsTheme {
+ val systemUiController = rememberSystemUiController()
+ SideEffect {
+ systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = false)
+ }
+
+ val navController = rememberNavController()
+ val coroutineScope = rememberCoroutineScope()
+
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route ?: MainDestinations.HOME_ROUTE
+
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet(modifier = Modifier.width(300.dp), windowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) {
+ AppDrawer(
+ currentRoute = currentRoute,
+ navigateToHome = { navController.navigate(MainDestinations.HOME_ROUTE) },
+ navigateToInterests = { navController.navigate(MainDestinations.INTERESTS_ROUTE) },
+ closeDrawer = { coroutineScope.launch { drawerState.close() } }
+ )
+ }
+ },
+ content = {
+ Scaffold {
+ JetnewsNavGraph(
+ appContainer = appContainer,
+ navController = navController,
+ drawerState = drawerState
+ )
+ }
+ }
+ )
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt
new file mode 100644
index 000000000..525920efe
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/JetnewsNavGraph.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui
+
+import androidx.compose.material3.DrawerState
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.example.jetnews.data.AppContainer
+import com.example.jetnews.ui.MainDestinations.ARTICLE_ID_KEY
+import com.example.jetnews.ui.article.ArticleScreen
+import com.example.jetnews.ui.home.HomeScreen
+import com.example.jetnews.ui.interests.InterestsScreen
+import kotlinx.coroutines.launch
+
+/**
+ * Destinations used in the ([JetnewsApp]).
+ */
+object MainDestinations {
+ const val HOME_ROUTE = "home"
+ const val INTERESTS_ROUTE = "interests"
+ const val ARTICLE_ROUTE = "post"
+ const val ARTICLE_ID_KEY = "postId"
+}
+
+@Composable
+fun JetnewsNavGraph(
+ appContainer: AppContainer,
+ navController: NavHostController = rememberNavController(),
+ drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
+ startDestination: String = MainDestinations.HOME_ROUTE
+) {
+ val actions = remember(navController) { MainActions(navController) }
+ val coroutineScope = rememberCoroutineScope()
+ val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } }
+
+ NavHost(
+ navController = navController,
+ startDestination = startDestination
+ ) {
+ composable(MainDestinations.HOME_ROUTE) {
+ HomeScreen(
+ postsRepository = appContainer.postsRepository,
+ navigateToArticle = actions.navigateToArticle,
+ openDrawer = openDrawer
+ )
+ }
+ composable(MainDestinations.INTERESTS_ROUTE) {
+ InterestsScreen(
+ interestsRepository = appContainer.interestsRepository,
+ openDrawer = openDrawer
+ )
+ }
+ composable("${MainDestinations.ARTICLE_ROUTE}/{$ARTICLE_ID_KEY}") { backStackEntry ->
+ ArticleScreen(
+ postId = backStackEntry.arguments?.getString(ARTICLE_ID_KEY),
+ onBack = actions.upPress,
+ postsRepository = appContainer.postsRepository
+ )
+ }
+ }
+}
+
+/**
+ * Models the navigation actions in the app.
+ */
+class MainActions(navController: NavHostController) {
+ val navigateToArticle: (String) -> Unit = { postId: String ->
+ navController.navigate("${MainDestinations.ARTICLE_ROUTE}/$postId")
+ }
+ val upPress: () -> Unit = {
+ navController.navigateUp()
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/MainActivity.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/MainActivity.kt
new file mode 100644
index 000000000..8d379188b
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/MainActivity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.core.view.WindowCompat
+import com.example.jetnews.JetnewsApplication
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ val appContainer = (application as JetnewsApplication).container
+ setContent {
+ JetnewsApp(appContainer)
+ }
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt
new file mode 100644
index 000000000..45d401d57
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.article
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import com.example.jetnews.R
+import com.example.jetnews.data.posts.PostsRepository
+import com.example.jetnews.data.posts.impl.post3
+import com.example.jetnews.model.Post
+import com.example.jetnews.ui.components.InsetAwareTopAppBar
+import com.example.jetnews.ui.theme.JetnewsTheme
+import com.example.jetnews.utils.supportWideScreen
+
+/**
+ * Stateful Article Screen that manages state using [produceUiState]
+ *
+ * @param postId (state) the post to show
+ * @param postsRepository data source for this screen
+ * @param onBack (event) request back navigation
+ */
+@Suppress("DEPRECATION") // allow ViewModelLifecycleScope call
+@Composable
+fun ArticleScreen(
+ postId: String?,
+ postsRepository: PostsRepository,
+ onBack: () -> Unit
+) {
+ val postData = postsRepository.getPost(postId)!!
+
+ ArticleScreen(
+ post = postData,
+ onBack = onBack
+ )
+}
+
+/**
+ * Stateless Article Screen that displays a single post.
+ *
+ * @param post (state) item to display
+ * @param onBack (event) request navigate back
+ */
+@Composable
+fun ArticleScreen(
+ post: Post,
+ onBack: () -> Unit
+) {
+
+ var showDialog by rememberSaveable { mutableStateOf(false) }
+ if (showDialog) {
+ FunctionalityNotAvailablePopup { showDialog = false }
+ }
+
+ Scaffold(
+ topBar = {
+ InsetAwareTopAppBar(
+ title = {
+ Text(
+ text = "Published in: ${post.publication?.name}",
+ style = MaterialTheme.typography.titleSmall,
+ color = LocalContentColor.current
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ // Step 4: Content descriptions
+ contentDescription = stringResource(
+ R.string.cd_navigate_up
+ )
+ )
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ PostContent(
+ post = post,
+ modifier = Modifier
+ // innerPadding takes into account the top and bottom bar
+ .padding(innerPadding)
+ // center content in landscape mode
+ .supportWideScreen()
+ )
+ }
+}
+
+/**
+ * Display a popup explaining functionality not available.
+ *
+ * @param onDismiss (event) request the popup be dismissed
+ */
+@Composable
+private fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ text = {
+ Text(
+ text = "Functionality not available \uD83D\uDE48",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = "CLOSE")
+ }
+ }
+ )
+}
+
+@Preview("Article screen")
+@Preview("Article screen (dark)", uiMode = UI_MODE_NIGHT_YES)
+@Preview("Article screen (big font)", fontScale = 1.5f)
+@Preview("Article screen (large screen)", device = Devices.PIXEL_C)
+@Composable
+fun PreviewArticle() {
+ JetnewsTheme {
+ ArticleScreen(PostsRepository().getPost(post3.id)!!, {})
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt
new file mode 100644
index 000000000..ff74a7166
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.article
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.Typography
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.FirstBaseline
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ParagraphStyle
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.jetnews.data.posts.impl.post3
+import com.example.jetnews.model.Markup
+import com.example.jetnews.model.MarkupType
+import com.example.jetnews.model.Metadata
+import com.example.jetnews.model.Paragraph
+import com.example.jetnews.model.ParagraphType
+import com.example.jetnews.model.Post
+import com.example.jetnews.ui.theme.JetnewsTheme
+
+private val defaultSpacerSize = 16.dp
+
+@Composable
+fun PostContent(post: Post, modifier: Modifier = Modifier) {
+ LazyColumn(
+ modifier = modifier.padding(horizontal = defaultSpacerSize)
+ ) {
+ item {
+ Spacer(Modifier.height(defaultSpacerSize))
+ PostHeaderImage(post)
+ }
+ item {
+ Text(text = post.title, style = MaterialTheme.typography.headlineMedium)
+ Spacer(Modifier.height(8.dp))
+ }
+ post.subtitle?.let { subtitle ->
+ item {
+ CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ lineHeight = 20.sp
+ )
+ }
+ Spacer(Modifier.height(defaultSpacerSize))
+ }
+ }
+ item {
+ PostMetadata(post.metadata)
+ Spacer(Modifier.height(24.dp))
+ }
+ items(post.paragraphs) {
+ Paragraph(paragraph = it)
+ }
+ item {
+ Spacer(Modifier.height(48.dp))
+ }
+ }
+}
+
+@Composable
+private fun PostHeaderImage(post: Post) {
+ val imageModifier = Modifier
+ .heightIn(min = 180.dp)
+ .fillMaxWidth()
+ .clip(shape = MaterialTheme.shapes.medium)
+ Image(
+ painter = painterResource(post.imageId),
+ contentDescription = null,
+ modifier = imageModifier,
+ contentScale = ContentScale.Crop
+ )
+ Spacer(Modifier.height(defaultSpacerSize))
+}
+
+@Composable
+private fun PostMetadata(metadata: Metadata) {
+ val typography = MaterialTheme.typography
+ // Step 6: Custom merging
+ Row(Modifier.semantics(mergeDescendants = true) {}) {
+ Image(
+ imageVector = Icons.Filled.AccountCircle,
+ contentDescription = null,
+ modifier = Modifier.size(40.dp),
+ colorFilter = ColorFilter.tint(LocalContentColor.current),
+ contentScale = ContentScale.Fit
+ )
+ Spacer(Modifier.width(8.dp))
+ Column {
+ Text(
+ text = metadata.author.name,
+ style = typography.bodySmall,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+
+ CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
+ Text(
+ text = "${metadata.date} • ${metadata.readTimeMinutes} min read",
+ style = typography.bodySmall
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Paragraph(paragraph: Paragraph) {
+ val (textStyle, paragraphStyle, trailingPadding) = paragraph.type.getTextAndParagraphStyle()
+
+ val annotatedString = paragraphToAnnotatedString(
+ paragraph,
+ MaterialTheme.typography,
+ MaterialTheme.colorScheme.codeBlockBackground
+ )
+ Box(modifier = Modifier.padding(bottom = trailingPadding)) {
+ when (paragraph.type) {
+ ParagraphType.Bullet -> BulletParagraph(
+ text = annotatedString,
+ textStyle = textStyle,
+ paragraphStyle = paragraphStyle
+ )
+ ParagraphType.CodeBlock -> CodeBlockParagraph(
+ text = annotatedString,
+ textStyle = textStyle,
+ paragraphStyle = paragraphStyle
+ )
+ ParagraphType.Header -> {
+ Text(
+ modifier = Modifier.padding(4.dp)
+ // Step 5: Headings
+ .semantics { heading() },
+ text = annotatedString,
+ style = textStyle.merge(paragraphStyle)
+ )
+ }
+ else -> Text(
+ modifier = Modifier.padding(4.dp),
+ text = annotatedString,
+ style = textStyle
+ )
+ }
+ }
+}
+
+@Composable
+private fun CodeBlockParagraph(
+ text: AnnotatedString,
+ textStyle: TextStyle,
+ paragraphStyle: ParagraphStyle
+) {
+ Surface(
+ color = MaterialTheme.colorScheme.codeBlockBackground,
+ shape = MaterialTheme.shapes.small,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = text,
+ style = textStyle.merge(paragraphStyle)
+ )
+ }
+}
+
+@Composable
+private fun BulletParagraph(
+ text: AnnotatedString,
+ textStyle: TextStyle,
+ paragraphStyle: ParagraphStyle
+) {
+ Row {
+ with(LocalDensity.current) {
+ // this box is acting as a character, so it's sized with font scaling (sp)
+ Box(
+ modifier = Modifier
+ .size(8.sp.toDp(), 8.sp.toDp())
+ .alignBy {
+ // Add an alignment "baseline" 1sp below the bottom of the circle
+ 9.sp.roundToPx()
+ }
+ .background(LocalContentColor.current, CircleShape),
+ ) { /* no content */ }
+ }
+ Text(
+ modifier = Modifier
+ .weight(1f)
+ .alignBy(FirstBaseline),
+ text = text,
+ style = textStyle.merge(paragraphStyle)
+ )
+ }
+}
+
+private data class ParagraphStyling(
+ val textStyle: TextStyle,
+ val paragraphStyle: ParagraphStyle,
+ val trailingPadding: Dp
+)
+
+@Composable
+private fun ParagraphType.getTextAndParagraphStyle(): ParagraphStyling {
+ val typography = MaterialTheme.typography
+ var textStyle: TextStyle = typography.bodyLarge
+ var paragraphStyle = ParagraphStyle()
+ var trailingPadding = 24.dp
+
+ when (this) {
+ ParagraphType.Caption -> textStyle = typography.bodyLarge
+ ParagraphType.Title -> textStyle = typography.headlineMedium
+ ParagraphType.Subhead -> {
+ textStyle = typography.titleLarge
+ trailingPadding = 16.dp
+ }
+ ParagraphType.Text -> {
+ textStyle = typography.bodyLarge.copy(lineHeight = 28.sp)
+ paragraphStyle = paragraphStyle.copy(lineHeight = 28.sp)
+ }
+ ParagraphType.Header -> {
+ textStyle = typography.headlineSmall
+ trailingPadding = 16.dp
+ }
+ ParagraphType.CodeBlock -> textStyle = typography.bodyLarge.copy(
+ fontFamily = FontFamily.Monospace
+ )
+ ParagraphType.Quote -> textStyle = typography.bodyLarge
+ ParagraphType.Bullet -> {
+ paragraphStyle = ParagraphStyle(textIndent = TextIndent(firstLine = 8.sp))
+ }
+ }
+ return ParagraphStyling(
+ textStyle,
+ paragraphStyle,
+ trailingPadding
+ )
+}
+
+private fun paragraphToAnnotatedString(
+ paragraph: Paragraph,
+ typography: Typography,
+ codeBlockBackground: Color
+): AnnotatedString {
+ val styles: List> = paragraph.markups
+ .map { it.toAnnotatedStringItem(typography, codeBlockBackground) }
+ return AnnotatedString(text = paragraph.text, spanStyles = styles)
+}
+
+fun Markup.toAnnotatedStringItem(
+ typography: Typography,
+ codeBlockBackground: Color
+): AnnotatedString.Range {
+ return when (this.type) {
+ MarkupType.Italic -> {
+ AnnotatedString.Range(
+ typography.bodyLarge.copy(fontStyle = FontStyle.Italic).toSpanStyle(),
+ start,
+ end
+ )
+ }
+ MarkupType.Link -> {
+ AnnotatedString.Range(
+ typography.bodyLarge.copy(textDecoration = TextDecoration.Underline).toSpanStyle(),
+ start,
+ end
+ )
+ }
+ MarkupType.Bold -> {
+ AnnotatedString.Range(
+ typography.bodyLarge.copy(fontWeight = FontWeight.Bold).toSpanStyle(),
+ start,
+ end
+ )
+ }
+ MarkupType.Code -> {
+ AnnotatedString.Range(
+ typography.bodyLarge
+ .copy(
+ background = codeBlockBackground,
+ fontFamily = FontFamily.Monospace
+ ).toSpanStyle(),
+ start,
+ end
+ )
+ }
+ }
+}
+
+private val ColorScheme.codeBlockBackground: Color
+ get() = onSurface.copy(alpha = .15f)
+
+@Preview("Post content")
+@Preview("Post content (dark)", uiMode = UI_MODE_NIGHT_YES)
+@Composable
+fun PreviewPost() {
+ JetnewsTheme {
+ Surface {
+ PostContent(post = post3)
+ }
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/components/InsetAwareTopAppBar.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/components/InsetAwareTopAppBar.kt
new file mode 100644
index 000000000..24c27928b
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/components/InsetAwareTopAppBar.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.components
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * A wrapper around [TopAppBar] which uses [Modifier.statusBarsPadding] to shift the app bar's
+ * contents down, but still draws the background behind the status bar too.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InsetAwareTopAppBar(
+ title: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable (() -> Unit),
+ actions: @Composable RowScope.() -> Unit = {},
+ backgroundColor: Color = MaterialTheme.colorScheme.surface,
+ elevation: Dp = 4.dp
+) {
+ Surface(
+ color = backgroundColor,
+ shadowElevation = elevation,
+ tonalElevation = elevation,
+ modifier = modifier
+ ) {
+ TopAppBar(
+ title = title,
+ navigationIcon = navigationIcon,
+ actions = actions,
+ colors = TopAppBarDefaults.topAppBarColors().copy(
+ containerColor = Color.Transparent,
+ titleContentColor = MaterialTheme.colorScheme.onPrimary,
+ navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
+ ),
+ modifier = Modifier.statusBarsPadding()
+ )
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt
new file mode 100644
index 000000000..6ac9a112b
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.home
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.add
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.example.jetnews.R
+import com.example.jetnews.data.posts.PostsRepository
+import com.example.jetnews.model.Post
+import com.example.jetnews.ui.components.InsetAwareTopAppBar
+import com.example.jetnews.ui.theme.JetnewsTheme
+import kotlinx.coroutines.launch
+
+/**
+ * Stateful HomeScreen which manages state using [produceUiState]
+ *
+ * @param postsRepository data source for this screen
+ * @param navigateToArticle (event) request navigation to Article screen
+ * @param openDrawer (event) request opening the app drawer
+ */
+@Composable
+fun HomeScreen(
+ postsRepository: PostsRepository,
+ navigateToArticle: (String) -> Unit,
+ openDrawer: () -> Unit,
+) {
+ HomeScreen(
+ posts = postsRepository.getPosts(),
+ navigateToArticle = navigateToArticle,
+ openDrawer = openDrawer,
+ )
+}
+
+/**
+ * Responsible for displaying the Home Screen of this application.
+ *
+ * Stateless composable is not coupled to any specific state management.
+ *
+ * @param posts (state) the data to show on the screen
+ * @param favorites (state) favorite posts
+ * @param onToggleFavorite (event) toggles favorite for a post
+ * @param navigateToArticle (event) request navigation to Article screen
+ * @param openDrawer (event) request opening the app drawer
+ */
+@Composable
+fun HomeScreen(
+ posts: List,
+ navigateToArticle: (String) -> Unit,
+ openDrawer: () -> Unit,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ Scaffold(
+ topBar = {
+ val title = stringResource(id = R.string.app_name)
+ InsetAwareTopAppBar(
+ title = { Text(text = title) },
+ navigationIcon = {
+ IconButton(onClick = { coroutineScope.launch { openDrawer() } }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_jetnews_logo),
+ contentDescription = stringResource(R.string.cd_open_navigation_drawer)
+ )
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ val modifier = Modifier.padding(innerPadding)
+ PostList(posts, navigateToArticle, modifier)
+ }
+}
+
+/**
+ * Display a list of posts.
+ *
+ * When a post is clicked on, [navigateToArticle] will be called to navigate to the detail screen
+ * for that post.
+ *
+ * @param posts (state) the list to display
+ * @param navigateToArticle (event) request navigation to Article screen
+ * @param modifier modifier for the root element
+ */
+@Composable
+private fun PostList(
+ posts: List,
+ navigateToArticle: (postId: String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val postsHistory = posts.subList(0, 3)
+ val postsPopular = posts.subList(3, 5)
+ val contentPadding = rememberContentPaddingForScreen(additionalTop = 8.dp)
+
+ LazyColumn(
+ modifier = modifier,
+ contentPadding = contentPadding
+ ) {
+ items(postsHistory) { post ->
+ PostCardHistory(post, navigateToArticle)
+ PostListDivider()
+ }
+ item {
+ PostListPopularSection(postsPopular, navigateToArticle)
+ }
+ }
+}
+
+/**
+ * Horizontal scrolling cards for [PostList]
+ *
+ * @param posts (state) to display
+ * @param navigateToArticle (event) request navigation to Article screen
+ */
+@Composable
+private fun PostListPopularSection(
+ posts: List,
+ navigateToArticle: (String) -> Unit
+) {
+ Column {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(id = R.string.home_popular_section_title),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ LazyRow(contentPadding = PaddingValues(end = 16.dp)) {
+ items(posts) { post ->
+ PostCardPopular(
+ post,
+ navigateToArticle,
+ Modifier.padding(start = 16.dp, bottom = 16.dp)
+ )
+ }
+ }
+ PostListDivider()
+ }
+}
+
+/**
+ * Full-width divider with padding for [PostList]
+ */
+@Composable
+private fun PostListDivider() {
+ Divider(
+ modifier = Modifier.padding(horizontal = 14.dp),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
+ )
+}
+
+/**
+ * Determine the content padding to apply to the different screens of the app
+ */
+@Composable
+fun rememberContentPaddingForScreen(additionalTop: Dp = 0.dp) =
+ WindowInsets.systemBars
+ .only(WindowInsetsSides.Bottom)
+ .add(WindowInsets(top = additionalTop))
+ .asPaddingValues()
+@Preview("Home screen")
+@Preview("Home screen (dark)", uiMode = UI_MODE_NIGHT_YES)
+@Preview("Home screen (big font)", fontScale = 1.5f)
+@Preview("Home screen (large screen)", device = Devices.PIXEL_C)
+@Composable
+fun PreviewHomeScreen() {
+ JetnewsTheme {
+ HomeScreen(
+ posts = PostsRepository().getPosts(),
+ navigateToArticle = { /*TODO*/ },
+ openDrawer = { /*TODO*/ },
+ )
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt
new file mode 100644
index 000000000..520d25ddd
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.home
+
+import android.content.res.Configuration
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.CustomAccessibilityAction
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.customActions
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.jetnews.R
+import com.example.jetnews.data.posts.impl.post1
+import com.example.jetnews.data.posts.impl.post3
+import com.example.jetnews.model.Post
+import com.example.jetnews.ui.theme.JetnewsTheme
+
+@Composable
+fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
+ var openDialog by remember { mutableStateOf(false) }
+ // Step 3: Custom actions
+ val showFewerLabel = stringResource(R.string.cd_show_fewer)
+ Row(
+ // Step 2: Click labels
+ Modifier
+ .clickable(
+ // R.string.action_read_article = "read article"
+ onClickLabel = stringResource(R.string.action_read_article)
+ ) {
+ navigateToArticle(post.id)
+ }
+ // Step 3: Custom actions
+ .semantics {
+ customActions = listOf(
+ CustomAccessibilityAction(
+ label = showFewerLabel,
+ action = { openDialog = true; true }
+ )
+ )
+ }
+ ) {
+ Image(
+ painter = painterResource(post.imageThumbId),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 16.dp, start = 16.dp, end = 16.dp)
+ .size(40.dp, 40.dp)
+ .clip(MaterialTheme.shapes.small)
+ )
+ Column(
+ Modifier
+ .weight(1f)
+ .padding(top = 16.dp, bottom = 16.dp)
+ ) {
+ Text(post.title, style = MaterialTheme.typography.titleMedium)
+ Row(Modifier.padding(top = 4.dp)) {
+ CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
+ val textStyle = MaterialTheme.typography.bodyMedium
+ Text(
+ text = post.metadata.author.name,
+ style = textStyle
+ )
+ Text(
+ text = " - ${post.metadata.readTimeMinutes} min read",
+ style = textStyle
+ )
+ }
+ }
+ }
+ CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
+ // Step 1: Touch target size
+ IconButton(
+ // Step 3: Custom actions
+ modifier = Modifier.clearAndSetSemantics { },
+ onClick = { openDialog = true }
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = showFewerLabel
+ )
+ }
+ }
+ }
+ if (openDialog) {
+ AlertDialog(
+ modifier = Modifier.padding(20.dp),
+ onDismissRequest = { openDialog = false },
+ title = {
+ Text(
+ text = stringResource(id = R.string.fewer_stories),
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ text = {
+ Text(
+ text = stringResource(id = R.string.fewer_stories_content),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ },
+ confirmButton = {
+ Text(
+ text = stringResource(id = R.string.agree),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .padding(15.dp)
+ .clickable { openDialog = false }
+ )
+ }
+ )
+ }
+}
+
+@Composable
+fun PostCardPopular(
+ post: Post,
+ navigateToArticle: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ // Step 2: Click labels
+ val readArticleLabel = stringResource(id = R.string.action_read_article)
+ Card(
+ colors = CardDefaults.cardColors(),
+ shape = MaterialTheme.shapes.medium,
+ modifier = modifier
+ .size(280.dp, 240.dp)
+ .semantics { onClick(label = readArticleLabel, action = null) },
+ onClick = { navigateToArticle(post.id) },
+ elevation = CardDefaults.elevatedCardElevation()
+ ) {
+ Column {
+
+ Image(
+ painter = painterResource(post.imageId),
+ contentDescription = null, // decorative
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .height(100.dp)
+ .fillMaxWidth()
+ )
+
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = post.title,
+ style = MaterialTheme.typography.titleLarge,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = post.metadata.author.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ Text(
+ text = stringResource(
+ id = R.string.home_post_min_read,
+ post.metadata.date,
+ post.metadata.readTimeMinutes
+ ),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+}
+
+@Preview("Regular colors")
+@Preview("Dark colors", uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+fun PreviewPostCardPopular() {
+ JetnewsTheme {
+ Surface {
+ PostCardPopular(post1, {})
+ }
+ }
+}
+
+@Preview("Post History card")
+@Composable
+fun HistoryPostPreview() {
+ JetnewsTheme {
+ Surface {
+ PostCardHistory(post3, {})
+ }
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt
new file mode 100644
index 000000000..2e5c454f1
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.interests
+
+import android.annotation.SuppressLint
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.jetnews.R
+import com.example.jetnews.data.interests.InterestsRepository
+import com.example.jetnews.data.interests.TopicSelection
+import com.example.jetnews.data.interests.TopicsMap
+import com.example.jetnews.ui.components.InsetAwareTopAppBar
+import com.example.jetnews.ui.theme.JetnewsTheme
+import kotlinx.coroutines.launch
+
+/**
+ * Stateful InterestsScreen that handles the interaction with the repository
+ *
+ * @param interestsRepository data source for this screen
+ * @param openDrawer (event) request opening the app drawer
+ */
+@Composable
+fun InterestsScreen(
+ interestsRepository: InterestsRepository,
+ openDrawer: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ // Returns a [CoroutineScope] that is scoped to the lifecycle of [InterestsScreen]. When this
+ // screen is removed from composition, the scope will be cancelled.
+ val coroutineScope = rememberCoroutineScope()
+
+ // collectAsState will read a [Flow] in Compose
+ val selectedTopics by interestsRepository.observeTopicsSelected().collectAsState(setOf())
+ val onTopicSelect: (TopicSelection) -> Unit = {
+ coroutineScope.launch { interestsRepository.toggleTopicSelection(it) }
+ }
+ InterestsScreen(
+ topics = interestsRepository.topics,
+ selectedTopics = selectedTopics,
+ onTopicSelect = onTopicSelect,
+ openDrawer = openDrawer,
+ modifier = modifier,
+ )
+}
+
+/**
+ * Stateless interest screen displays the topics the user can subscribe to
+ *
+ * @param topics (state) topics to display, mapped by section
+ * @param selectedTopics (state) currently selected topics
+ * @param onTopicSelect (event) request a topic selection be changed
+ * @param openDrawer (event) request opening the app drawer
+ */
+@Composable
+fun InterestsScreen(
+ topics: TopicsMap,
+ selectedTopics: Set,
+ onTopicSelect: (TopicSelection) -> Unit,
+ openDrawer: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Scaffold(
+ topBar = {
+ InsetAwareTopAppBar(
+ title = { Text("Interests") },
+ navigationIcon = {
+ IconButton(onClick = openDrawer) {
+ Icon(
+ painter = painterResource(R.drawable.ic_jetnews_logo),
+ contentDescription = stringResource(R.string.cd_open_navigation_drawer)
+ )
+ }
+ }
+ )
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = modifier.padding(padding)
+ ) {
+ topics.forEach { (section, topics) ->
+ item {
+ Text(
+ text = section,
+ modifier = Modifier
+ .padding(16.dp),
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+ items(topics) { topic ->
+ TopicItem(
+ itemTitle = topic,
+ selected = selectedTopics.contains(TopicSelection(section, topic))
+ ) { onTopicSelect(TopicSelection(section, topic)) }
+ TopicDivider()
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Display a full-width topic item
+ *
+ * @param itemTitle (state) topic title
+ * @param selected (state) is topic currently selected
+ * @param onToggle (event) toggle selection for topic
+ */
+@Composable
+private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
+ val image = painterResource(R.drawable.placeholder_1_1)
+ // Step 8: State descriptions
+ val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
+ val stateSubscribed = stringResource(R.string.state_subscribed)
+ Row(
+ modifier = Modifier
+ // Step 8: State descriptions
+ .semantics {
+ stateDescription = if (selected) {
+ stateSubscribed
+ } else {
+ stateNotSubscribed
+ }
+ }
+ // Step 7: Switches and Checkboxes
+ .toggleable(
+ value = selected,
+ onValueChange = { _ -> onToggle() },
+ role = Role.Checkbox
+ )
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ Image(
+ painter = image,
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(56.dp, 56.dp)
+ .clip(RoundedCornerShape(4.dp))
+ )
+ Text(
+ text = itemTitle,
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .padding(16.dp),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(Modifier.weight(1f))
+ Checkbox(
+ checked = selected,
+ // Step 7: Switches and Checkboxes
+ onCheckedChange = null,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+}
+
+/**
+ * Full-width divider for topics
+ */
+@Composable
+private fun TopicDivider() {
+ Divider(
+ modifier = Modifier.padding(start = 90.dp),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
+ )
+}
+
+@Preview("Interests screen", "Interests")
+@Preview("Interests screen (dark)", "Interests", uiMode = UI_MODE_NIGHT_YES)
+@Preview("Interests screen (big font)", "Interests", fontScale = 1.5f)
+@Preview("Interests screen (large screen)", "Interests", device = Devices.PIXEL_C)
+@Composable
+fun PreviewInterestsScreen() {
+ JetnewsTheme {
+ InterestsScreen(
+ interestsRepository = InterestsRepository(),
+ openDrawer = {}
+ )
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Color.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Color.kt
new file mode 100644
index 000000000..34825b8b9
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Color.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Red200 = Color(0xfff297a2)
+val Red300 = Color(0xffea6d7e)
+val Red700 = Color(0xffdd0d3c)
+val Red800 = Color(0xffd00036)
+val Red900 = Color(0xffc20029)
+
+val DarkGray200 = Color(0xFF2A2A2A)
\ No newline at end of file
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt
new file mode 100644
index 000000000..10c00e956
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Shape.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+val JetnewsShapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(8.dp)
+)
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt
new file mode 100644
index 000000000..7391789f9
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+private val LightThemeColors = lightColorScheme(
+ primary = Red700,
+ primaryContainer = Red900,
+ surface = Red700,
+ onPrimary = Color.White,
+ secondary = Red700,
+ secondaryContainer = Red900,
+ onSecondary = Color.White,
+ error = Red800
+)
+
+private val DarkThemeColors = darkColorScheme(
+ primary = Red300,
+ primaryContainer = Red700,
+ surface = DarkGray200,
+ onPrimary = Color.White,
+ secondary = Red300,
+ onSecondary = Color.Black,
+ error = Red200
+)
+
+@Composable
+fun JetnewsTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ MaterialTheme(
+ colorScheme = if (darkTheme) DarkThemeColors else LightThemeColors,
+ typography = JetnewsTypography,
+ shapes = JetnewsShapes,
+ content = content
+ )
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Type.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Type.kt
new file mode 100644
index 000000000..155788514
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/ui/theme/Type.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.example.jetnews.R
+
+private val Montserrat = FontFamily(
+ Font(R.font.montserrat_regular),
+ Font(R.font.montserrat_medium, FontWeight.W500),
+ Font(R.font.montserrat_semibold, FontWeight.W600)
+)
+
+private val Domine = FontFamily(
+ Font(R.font.domine_regular),
+ Font(R.font.domine_bold, FontWeight.Bold)
+)
+
+val JetnewsTypography = Typography(
+ headlineMedium = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 30.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 24.sp,
+ letterSpacing = 0.sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 20.sp,
+ letterSpacing = 0.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
+ letterSpacing = 0.15.sp
+ ),
+ titleSmall = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ letterSpacing = 0.1.sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = Domine,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ letterSpacing = 0.25.sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ letterSpacing = 1.25.sp
+ ),
+ bodySmall = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ letterSpacing = 0.4.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = Montserrat,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp,
+ letterSpacing = 1.sp
+ )
+)
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt
new file mode 100644
index 000000000..7c8608e19
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/MapExtensions.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.utils
+
+internal fun MutableSet.addOrRemove(element: E) {
+ if (!add(element)) {
+ remove(element)
+ }
+}
diff --git a/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/Modifiers.kt b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/Modifiers.kt
new file mode 100644
index 000000000..248f6f52b
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/java/com/example/jetnews/utils/Modifiers.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.jetnews.utils
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/**
+ * Support wide screen by making the content width max 840dp, centered horizontally.
+ */
+fun Modifier.supportWideScreen() = this
+ .fillMaxWidth()
+ .wrapContentWidth(align = Alignment.CenterHorizontally)
+ .widthIn(max = 840.dp)
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml b/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml
new file mode 100644
index 000000000..899bbb7e1
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml b/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml
new file mode 100644
index 000000000..10fc024e4
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/drawable-anydpi-v26/ic_launcher_foreground.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_1_1.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_1_1.png
new file mode 100644
index 000000000..55fb3c2ac
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_1_1.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_4_3.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_4_3.png
new file mode 100644
index 000000000..ebe05a16b
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/placeholder_4_3.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1.png
new file mode 100644
index 000000000..f2efcdc27
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1_thumb.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1_thumb.png
new file mode 100644
index 000000000..6af4fcc71
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_1_thumb.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2.png
new file mode 100644
index 000000000..acc31574c
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2_thumb.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2_thumb.png
new file mode 100644
index 000000000..e5c598ec4
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_2_thumb.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3.png
new file mode 100644
index 000000000..f8922fbe9
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3_thumb.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3_thumb.png
new file mode 100644
index 000000000..6e6a82d8b
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_3_thumb.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4.png
new file mode 100644
index 000000000..14b14d977
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4_thumb.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4_thumb.png
new file mode 100644
index 000000000..d772e0019
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_4_thumb.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5.png
new file mode 100644
index 000000000..31eb8183b
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5_thumb.png b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5_thumb.png
new file mode 100644
index 000000000..5c42ce2a8
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/drawable-nodpi/post_5_thumb.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_logo.xml b/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_logo.xml
new file mode 100644
index 000000000..af97fc324
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_logo.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_wordmark.xml b/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_wordmark.xml
new file mode 100644
index 000000000..c0f889316
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/drawable/ic_jetnews_wordmark.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/drawable/ic_text_settings.xml b/AccessibilityCodelab/app/src/main/res/drawable/ic_text_settings.xml
new file mode 100644
index 000000000..ee73d9bbd
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/drawable/ic_text_settings.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/font/domine_bold.ttf b/AccessibilityCodelab/app/src/main/res/font/domine_bold.ttf
new file mode 100755
index 000000000..329288c6b
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/font/domine_bold.ttf differ
diff --git a/AccessibilityCodelab/app/src/main/res/font/domine_regular.ttf b/AccessibilityCodelab/app/src/main/res/font/domine_regular.ttf
new file mode 100755
index 000000000..c387575c2
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/font/domine_regular.ttf differ
diff --git a/AccessibilityCodelab/app/src/main/res/font/montserrat_medium.ttf b/AccessibilityCodelab/app/src/main/res/font/montserrat_medium.ttf
new file mode 100755
index 000000000..6e079f698
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/font/montserrat_medium.ttf differ
diff --git a/AccessibilityCodelab/app/src/main/res/font/montserrat_regular.ttf b/AccessibilityCodelab/app/src/main/res/font/montserrat_regular.ttf
new file mode 100755
index 000000000..8d443d5d5
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/font/montserrat_regular.ttf differ
diff --git a/AccessibilityCodelab/app/src/main/res/font/montserrat_semibold.ttf b/AccessibilityCodelab/app/src/main/res/font/montserrat_semibold.ttf
new file mode 100755
index 000000000..f8a43f2b2
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/font/montserrat_semibold.ttf differ
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AccessibilityCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..df4ac82c2
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png b/AccessibilityCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..068920789
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png b/AccessibilityCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..41189c3fc
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/AccessibilityCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..9934af168
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/AccessibilityCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4fc37c0dd
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/AccessibilityCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..645a6e37b
Binary files /dev/null and b/AccessibilityCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/AccessibilityCodelab/app/src/main/res/values-night/colors.xml b/AccessibilityCodelab/app/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..7c8601b60
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,17 @@
+
+
+
+ #0e0e0e
+
diff --git a/AccessibilityCodelab/app/src/main/res/values/colors.xml b/AccessibilityCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..234289875
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+
+
+
+ #dd0d3e
+ #c20029
+ @color/red900
+
diff --git a/AccessibilityCodelab/app/src/main/res/values/strings.xml b/AccessibilityCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..89e7225de
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,36 @@
+
+
+
+ Jetnews
+ Can\'t update latest news
+ Retry
+ Popular on Jetnews
+ %1$s - %2$d min read
+ Navigate up
+ Add to favorites
+ Share
+ Text settings
+ Open navigation drawer
+ Show fewer like this
+ Bookmark
+ unbookmark
+ bookmark
+ Show fewer stories like this?
+ This feature is not yet implemented
+ AGREE
+ read article
+ not subscribed
+ subscribed
+
diff --git a/AccessibilityCodelab/app/src/main/res/values/styles.xml b/AccessibilityCodelab/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..b895f2796
--- /dev/null
+++ b/AccessibilityCodelab/app/src/main/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/AccessibilityCodelab/build.gradle b/AccessibilityCodelab/build.gradle
new file mode 100644
index 000000000..fe6f2fa30
--- /dev/null
+++ b/AccessibilityCodelab/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:9.2.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10"
+ }
+}
+
+plugins {
+ id 'com.diffplug.spotless' version '8.7.0'
+ id 'org.jetbrains.kotlin.plugin.compose' version "2.3.10" apply false
+}
+
+subprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ apply plugin: 'com.diffplug.spotless'
+ spotless {
+ kotlin {
+ target '**/*.kt'
+ targetExclude("$buildDir/**/*.kt")
+ targetExclude('bin/**/*.kt')
+
+ ktlint("0.45.2").userData([android: "true"])
+ licenseHeaderFile rootProject.file('spotless/copyright.kt')
+ }
+ }
+}
diff --git a/AccessibilityCodelab/debug.keystore b/AccessibilityCodelab/debug.keystore
new file mode 100644
index 000000000..6024334a4
Binary files /dev/null and b/AccessibilityCodelab/debug.keystore differ
diff --git a/AccessibilityCodelab/gradle.properties b/AccessibilityCodelab/gradle.properties
new file mode 100644
index 000000000..961905d2a
--- /dev/null
+++ b/AccessibilityCodelab/gradle.properties
@@ -0,0 +1,42 @@
+#
+# Copyright 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+
+# Turn on parallel compilation, caching and on-demand configuration
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Enable R8 full mode.
+android.enableR8.fullMode=true
diff --git a/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.jar b/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.properties b/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/AccessibilityCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/AccessibilityCodelab/gradlew b/AccessibilityCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/AccessibilityCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/AccessibilityCodelab/gradlew.bat b/AccessibilityCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/AccessibilityCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/AccessibilityCodelab/settings.gradle b/AccessibilityCodelab/settings.gradle
new file mode 100644
index 000000000..b2268f99d
--- /dev/null
+++ b/AccessibilityCodelab/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='JetNews'
diff --git a/AccessibilityCodelab/spotless/copyright.kt b/AccessibilityCodelab/spotless/copyright.kt
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/AccessibilityCodelab/spotless/copyright.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
diff --git a/AdaptiveUiCodelab/.gitignore b/AdaptiveUiCodelab/.gitignore
new file mode 100644
index 000000000..4a708a497
--- /dev/null
+++ b/AdaptiveUiCodelab/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/buildSrc/.gradle/*
+.kotlin/
diff --git a/AdaptiveUiCodelab/README.md b/AdaptiveUiCodelab/README.md
new file mode 100644
index 000000000..dceabc27a
--- /dev/null
+++ b/AdaptiveUiCodelab/README.md
@@ -0,0 +1,22 @@
+# Build adaptive apps with Jetpack Compose codelab
+
+This folder contains the solution source code for
+the [Build adaptive apps with Jetpack Compose codelab](https://codelabs.developers.google.com/jetpack-compose-adaptability)
+
+## License
+
+```
+Copyright 2022 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/.gitignore b/AdaptiveUiCodelab/app/.gitignore
new file mode 100644
index 000000000..d4cb42576
--- /dev/null
+++ b/AdaptiveUiCodelab/app/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/buildSrc/.gradle/*
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/build.gradle.kts b/AdaptiveUiCodelab/app/build.gradle.kts
new file mode 100644
index 000000000..bf820bb91
--- /dev/null
+++ b/AdaptiveUiCodelab/app/build.gradle.kts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.example.reply"
+ compileSdk = 37
+
+ defaultConfig {
+ applicationId = "com.example.reply"
+ minSdk = 23
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/AL2.0"
+ excludes += "/META-INF/LGPL2.1"
+ }
+ }
+}
+
+dependencies {
+ val composeBom = platform(libs.androidx.compose.bom)
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.material3.adaptive)
+ implementation(libs.androidx.material3.adaptive.layout)
+ implementation(libs.androidx.material3.adaptive.nav.suite)
+ implementation(libs.androidx.material3.adaptive.navigation)
+ implementation(libs.androidx.material.icons.extended)
+ implementation(libs.androidx.ui.tooling.preview)
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.window)
+ implementation(libs.kotlinx.coroutines.android)
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
diff --git a/AdaptiveUiCodelab/app/proguard-rules.pro b/AdaptiveUiCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..2f9dc5a47
--- /dev/null
+++ b/AdaptiveUiCodelab/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/AdaptiveUiCodelab/app/src/main/AndroidManifest.xml b/AdaptiveUiCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..822022764
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Account.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Account.kt
new file mode 100644
index 000000000..f624c5741
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Account.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+import androidx.annotation.DrawableRes
+
+/**
+ * An object which represents an account which can belong to a user. A single user can have
+ * multiple accounts.
+ */
+data class Account(
+ val id: Long,
+ val uid: Long,
+ val firstName: String,
+ val lastName: String,
+ val email: String,
+ val altEmail: String,
+ @DrawableRes val avatar: Int,
+ var isCurrentAccount: Boolean = false
+) {
+ val fullName: String = "$firstName $lastName"
+
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Email.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Email.kt
new file mode 100644
index 000000000..9da590e5d
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/Email.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+import com.example.reply.data.local.LocalAccountsDataProvider
+
+
+/**
+ * A simple data class to represent an Email.
+ */
+data class Email(
+ val id: Long,
+ val sender: Account,
+ val recipients: List = emptyList(),
+ val subject: String = "",
+ val body: String = "",
+ val attachments: List = emptyList(),
+ var isImportant: Boolean = false,
+ var isStarred: Boolean = false,
+ var mailbox: MailboxType = MailboxType.INBOX,
+ var createAt: String,
+ val replies: List = emptyList()
+) {
+ val senderPreview: String = "${sender.fullName} - 4 hrs ago"
+ val hasBody: Boolean = body.isNotBlank()
+ val hasAttachments: Boolean = attachments.isNotEmpty()
+ val recipientsPreview: String = recipients
+ .map { it.firstName }
+ .fold("") { name, acc -> "$acc, $name" }
+ val nonUserAccountRecipients = recipients
+ .filterNot { LocalAccountsDataProvider.isUserAccount(it.uid) }
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailAttachment.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailAttachment.kt
new file mode 100644
index 000000000..80ffa96ad
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailAttachment.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+import androidx.annotation.DrawableRes
+
+/**
+ * An object class to define an attachment to email object.
+ */
+data class EmailAttachment(
+ @DrawableRes val resId: Int,
+ val contentDesc: String
+)
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepository.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepository.kt
new file mode 100644
index 000000000..f2e42bf4c
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * An Interface contract to get all enails info for a User.
+ */
+interface EmailsRepository {
+ fun getAllEmails(): Flow>
+ fun getCategoryEmails(category: MailboxType): Flow>
+ fun getAllFolders(): List
+ fun getEmailFromId(id: Long): Flow
+}
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt
new file mode 100644
index 000000000..d6fcf81c4
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/EmailsRepositoryImpl.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+import com.example.reply.data.local.LocalEmailsDataProvider
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+class EmailsRepositoryImpl : EmailsRepository {
+
+ override fun getAllEmails(): Flow> = flow {
+ emit(LocalEmailsDataProvider.allEmails)
+ }
+
+ override fun getCategoryEmails(category: MailboxType): Flow> = flow {
+ val categoryEmails = LocalEmailsDataProvider.allEmails.filter { it.mailbox == category }
+ emit(categoryEmails)
+ }
+
+ override fun getAllFolders(): List {
+ return LocalEmailsDataProvider.getAllFolders()
+ }
+
+ override fun getEmailFromId(id: Long): Flow = flow {
+ val categoryEmails = LocalEmailsDataProvider.allEmails.firstOrNull { it.id == id }
+ }
+}
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/MailboxType.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/MailboxType.kt
new file mode 100644
index 000000000..9fa3043a3
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/MailboxType.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data
+
+/**
+ * An enum class to define different types of email folders or categories.
+ */
+enum class MailboxType {
+ INBOX, DRAFTS, SENT, SPAM, TRASH
+}
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt
new file mode 100644
index 000000000..75e58197e
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalAccountsDataProvider.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data.local
+
+import com.example.reply.R
+import com.example.reply.data.Account
+
+/**
+ * An static data store of [Account]s. This includes both [Account]s owned by the current user and
+ * all [Account]s of the current user's contacts.
+ */
+object LocalAccountsDataProvider {
+
+ val allUserAccounts = mutableListOf(
+ Account(
+ 1L,
+ 0L,
+ "Jeff",
+ "Hansen",
+ "hikingfan@gmail.com",
+ "hkngfan@outside.com",
+ R.drawable.avatar_10,
+ true
+ ),
+ Account(
+ 2L,
+ 0L,
+ "Jeff",
+ "H",
+ "jeffersonloveshiking@gmail.com",
+ "jeffersonloveshiking@work.com",
+ R.drawable.avatar_2
+ ),
+ Account(
+ 3L,
+ 0L,
+ "Jeff",
+ "Hansen",
+ "jeffersonc@google.com",
+ "jeffersonc@gmail.com",
+ R.drawable.avatar_9
+ )
+ )
+
+ private val allUserContactAccounts = listOf(
+ Account(
+ 4L,
+ 1L,
+ "Tracy",
+ "Alvarez",
+ "tracealvie@gmail.com",
+ "tracealvie@gravity.com",
+ R.drawable.avatar_1
+ ),
+ Account(
+ 5L,
+ 2L,
+ "Allison",
+ "Trabucco",
+ "atrabucco222@gmail.com",
+ "atrabucco222@work.com",
+ R.drawable.avatar_3
+ ),
+ Account(
+ 6L,
+ 3L,
+ "Ali",
+ "Connors",
+ "aliconnors@gmail.com",
+ "aliconnors@android.com",
+ R.drawable.avatar_5
+ ),
+ Account(
+ 7L,
+ 4L,
+ "Alberto",
+ "Williams",
+ "albertowilliams124@gmail.com",
+ "albertowilliams124@chromeos.com",
+ R.drawable.avatar_0
+ ),
+ Account(
+ 8L,
+ 5L,
+ "Kim",
+ "Alen",
+ "alen13@gmail.com",
+ "alen13@mountainview.gov",
+ R.drawable.avatar_7
+ ),
+ Account(
+ 9L,
+ 6L,
+ "Google",
+ "Express",
+ "express@google.com",
+ "express@gmail.com",
+ R.drawable.avatar_express
+ ),
+ Account(
+ 10L,
+ 7L,
+ "Sandra",
+ "Adams",
+ "sandraadams@gmail.com",
+ "sandraadams@textera.com",
+ R.drawable.avatar_2
+ ),
+ Account(
+ 11L,
+ 8L,
+ "Trevor",
+ "Hansen",
+ "trevorhandsen@gmail.com",
+ "trevorhandsen@express.com",
+ R.drawable.avatar_8
+ ),
+ Account(
+ 12L,
+ 9L,
+ "Sean",
+ "Holt",
+ "sholt@gmail.com",
+ "sholt@art.com",
+ R.drawable.avatar_6
+ ),
+ Account(
+ 13L,
+ 10L,
+ "Frank",
+ "Hawkins",
+ "fhawkank@gmail.com",
+ "fhawkank@thisisme.com",
+ R.drawable.avatar_4
+ )
+ )
+
+ /**
+ * Get the current user's default account.
+ */
+ fun getDefaultUserAccount() = allUserAccounts.first()
+
+ /**
+ * Whether or not the given [Account.id] uid is an account owned by the current user.
+ */
+ fun isUserAccount(uid: Long): Boolean = allUserAccounts.any { it.uid == uid }
+
+
+ /**
+ * Get the contact of the current user with the given [accountId].
+ */
+ fun getContactAccountByUid(accountId: Long): Account {
+ return allUserContactAccounts.firstOrNull { it.id == accountId }
+ ?: allUserContactAccounts.first()
+ }
+}
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt
new file mode 100644
index 000000000..4ce1615b7
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/data/local/LocalEmailsDataProvider.kt
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.data.local
+
+import com.example.reply.R
+import com.example.reply.data.Email
+import com.example.reply.data.EmailAttachment
+import com.example.reply.data.MailboxType
+
+/**
+ * A static data store of [Email]s.
+ */
+
+object LocalEmailsDataProvider {
+
+ private val replies = listOf(
+ Email(
+ 4L,
+ LocalAccountsDataProvider.getContactAccountByUid(11L),
+ listOf(
+ LocalAccountsDataProvider.getDefaultUserAccount(),
+ LocalAccountsDataProvider.getContactAccountByUid(8L),
+ LocalAccountsDataProvider.getContactAccountByUid(5L)
+ ),
+ "Brazil trip",
+ """
+ Thought we might be able to go over some details about our upcoming vacation.
+
+ I've been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. One, the north has some of the most predictable wind on the planet. I'd love to get out on the ocean and kitesurf for a couple of days if we're going to be anywhere near or around Taiba. I hear it's beautiful there and if you're up for it, I'd love to go. Other than that, I haven't spent too much time looking into places along our road trip route. I'm assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you're more of a planner, so if you have ideas or places in mind, lets jot some ideas down!
+
+ Maybe we can jump on the phone later today if you have a second.
+ """.trimIndent(),
+ createAt = "2 hours ago",
+ isStarred = true
+ ),
+ Email(
+ 5L,
+ LocalAccountsDataProvider.getContactAccountByUid(13L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Update to Your Itinerary",
+ "",
+ createAt = "2 hours ago",
+ ),
+ Email(
+ 6L,
+ LocalAccountsDataProvider.getContactAccountByUid(10L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Recipe to try",
+ "Raspberry Pie: We should make this pie recipe tonight! The filling is " +
+ "very quick to put together.",
+ createAt = "2 hours ago",
+ mailbox = MailboxType.SENT
+ ),
+ Email(
+ 7L,
+ LocalAccountsDataProvider.getContactAccountByUid(9L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Delivered",
+ "Your shoes should be waiting for you at home!",
+ createAt = "2 hours ago",
+ ),
+ Email(
+ 8L,
+ LocalAccountsDataProvider.getContactAccountByUid(13L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Your update on Google Play Store is live!",
+ """
+ Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing.
+
+ Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link.
+ """.trimIndent(),
+ mailbox = MailboxType.TRASH,
+ createAt = "3 hours ago",
+ ),
+ Email(
+ 9L,
+ LocalAccountsDataProvider.getContactAccountByUid(10L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "(No subject)",
+ """
+ Hey,
+
+ Wanted to email and see what you thought of
+ """.trimIndent(),
+ createAt = "3 hours ago",
+ mailbox = MailboxType.DRAFTS
+ )
+ )
+
+ val allEmails = mutableListOf(
+ Email(
+ 0L,
+ LocalAccountsDataProvider.getContactAccountByUid(9L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Package shipped!",
+ """
+ Cucumber Mask Facial has shipped.
+
+ Keep an eye out for a package to arrive between this Thursday and next Tuesday. If for any reason you don't receive your package before the end of next week, please reach out to us for details on your shipment.
+
+ As always, thank you for shopping with us and we hope you love our specially formulated Cucumber Mask!
+ """.trimIndent(),
+ createAt = "20 mins ago",
+ isStarred = true,
+ replies = replies,
+ ),
+ Email(
+ 1L,
+ LocalAccountsDataProvider.getContactAccountByUid(6L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Brunch this weekend?",
+ """
+ I'll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don't have anything scheduled, it would be great to see you! It feels like its been forever.
+
+ If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico.
+
+ Talk to you soon,
+
+ Ali
+ """.trimIndent(),
+ createAt = "40 mins ago",
+ replies = replies,
+ ),
+ Email(
+ 2L,
+ LocalAccountsDataProvider.getContactAccountByUid(5L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Bonjour from Paris",
+ "Here are some great shots from my trip...",
+ listOf(
+ EmailAttachment(R.drawable.paris_1, "Bridge in Paris"),
+ EmailAttachment(R.drawable.paris_2, "Bridge in Paris at night"),
+ EmailAttachment(R.drawable.paris_3, "City street in Paris"),
+ EmailAttachment(R.drawable.paris_4, "Street with bike in Paris")
+ ),
+ true,
+ createAt = "1 hour ago",
+ replies = replies,
+ ),
+ Email(
+ 3L,
+ LocalAccountsDataProvider.getContactAccountByUid(8L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "High school reunion?",
+ """
+ Hi friends,
+
+ I was at the grocery store on Sunday night.. when I ran into Genie Williams! I almost didn't recognize her afer 20 years!
+
+ Anyway, it turns out she is on the organizing committee for the high school reunion this fall. I don't know if you were planning on going or not, but she could definitely use our help in trying to track down lots of missing alums. If you can make it, we're doing a little phone-tree party at her place next Saturday, hoping that if we can find one person, thee more will...
+ """.trimIndent(),
+ createAt = "2 hours ago",
+ mailbox = MailboxType.SENT
+ ),
+ Email(
+ 4L,
+ LocalAccountsDataProvider.getContactAccountByUid(11L),
+ listOf(
+ LocalAccountsDataProvider.getDefaultUserAccount(),
+ LocalAccountsDataProvider.getContactAccountByUid(8L),
+ LocalAccountsDataProvider.getContactAccountByUid(5L)
+ ),
+ "Brazil trip",
+ """
+ Thought we might be able to go over some details about our upcoming vacation.
+
+ I've been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. One, the north has some of the most predictable wind on the planet. I'd love to get out on the ocean and kitesurf for a couple of days if we're going to be anywhere near or around Taiba. I hear it's beautiful there and if you're up for it, I'd love to go. Other than that, I haven't spent too much time looking into places along our road trip route. I'm assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you're more of a planner, so if you have ideas or places in mind, lets jot some ideas down!
+
+ Maybe we can jump on the phone later today if you have a second.
+ """.trimIndent(),
+ createAt = "2 hours ago",
+ isStarred = true
+ ),
+ Email(
+ 5L,
+ LocalAccountsDataProvider.getContactAccountByUid(13L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Update to Your Itinerary",
+ "",
+ createAt = "2 hours ago",
+ ),
+ Email(
+ 6L,
+ LocalAccountsDataProvider.getContactAccountByUid(10L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Recipe to try",
+ "Raspberry Pie: We should make this pie recipe tonight! The filling is " +
+ "very quick to put together.",
+ createAt = "2 hours ago",
+ mailbox = MailboxType.SENT
+ ),
+ Email(
+ 7L,
+ LocalAccountsDataProvider.getContactAccountByUid(9L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Delivered",
+ "Your shoes should be waiting for you at home!",
+ createAt = "2 hours ago",
+ ),
+ Email(
+ 8L,
+ LocalAccountsDataProvider.getContactAccountByUid(13L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Your update on Google Play Store is live!",
+ """
+ Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing.
+
+ Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link.
+ """.trimIndent(),
+ mailbox = MailboxType.TRASH,
+ createAt = "3 hours ago",
+ ),
+ Email(
+ 9L,
+ LocalAccountsDataProvider.getContactAccountByUid(10L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "(No subject)",
+ """
+ Hey,
+
+ Wanted to email and see what you thought of
+ """.trimIndent(),
+ createAt = "3 hours ago",
+ mailbox = MailboxType.DRAFTS
+ ),
+ Email(
+ 10L,
+ LocalAccountsDataProvider.getContactAccountByUid(5L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Try a free TrailGo account",
+ """
+ Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich.
+
+ Whether you're an experienced hiker or just looking to get outside for the afternoon, there's a segment that suits you.
+ """.trimIndent(),
+ createAt = "3 hours ago",
+ mailbox = MailboxType.TRASH
+ ),
+ Email(
+ 11L,
+ LocalAccountsDataProvider.getContactAccountByUid(5L),
+ listOf(LocalAccountsDataProvider.getDefaultUserAccount()),
+ "Free money",
+ """
+ You've been selected as a winner in our latest raffle! To claim your prize, click on the link.
+ """.trimIndent(),
+ createAt = "3 hours ago",
+ mailbox = MailboxType.SPAM
+ )
+ )
+
+ /**
+ * Get an [Email] with the given [id].
+ */
+ fun get(id: Long): Email? {
+ return allEmails.firstOrNull { it.id == id }
+ }
+
+ /**
+ * Create a new, blank [Email].
+ */
+ fun create(): Email {
+ return Email(
+ System.nanoTime(), // Unique ID generation.
+ LocalAccountsDataProvider.getDefaultUserAccount(),
+ createAt = "Just now",
+ )
+ }
+
+ /**
+ * Create a new [Email] that is a reply to the email with the given [replyToId].
+ */
+ fun createReplyTo(replyToId: Long): Email {
+ val replyTo = get(replyToId) ?: return create()
+ return Email(
+ id = System.nanoTime(),
+ sender = replyTo.recipients.firstOrNull()
+ ?: LocalAccountsDataProvider.getDefaultUserAccount(),
+ recipients = listOf(replyTo.sender) + replyTo.recipients,
+ subject = replyTo.subject,
+ isStarred = replyTo.isStarred,
+ isImportant = replyTo.isImportant,
+ createAt = "Just now",
+ )
+ }
+
+
+ /**
+ * Get a list of [EmailFolder]s by which [Email]s can be categorized.
+ */
+ fun getAllFolders() = listOf(
+ "Receipts",
+ "Pine Elementary",
+ "Taxes",
+ "Vacation",
+ "Mortgage",
+ "Grocery coupons"
+ )
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/MainActivity.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/MainActivity.kt
new file mode 100644
index 000000000..6b706ffc1
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/MainActivity.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.reply.data.local.LocalEmailsDataProvider
+import com.example.reply.ui.theme.ReplyTheme
+
+class MainActivity : ComponentActivity() {
+
+ private val viewModel: ReplyHomeViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ ReplyTheme {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ ReplyApp(
+ replyHomeUIState = uiState,
+ onEmailClick = viewModel::setSelectedEmail
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun ReplyAppPreview() {
+ ReplyTheme {
+ ReplyApp(
+ replyHomeUIState = ReplyHomeUIState(
+ emails = LocalEmailsDataProvider.allEmails
+ ),
+ onEmailClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true, widthDp = 700)
+@Composable
+fun ReplyAppPreviewTablet() {
+ ReplyTheme {
+ ReplyApp(
+ replyHomeUIState = ReplyHomeUIState(
+ emails = LocalEmailsDataProvider.allEmails
+ ),
+ onEmailClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true, widthDp = 1000)
+@Composable
+fun ReplyAppPreviewDesktop() {
+ ReplyTheme {
+ ReplyApp(
+ replyHomeUIState = ReplyHomeUIState(
+ emails = LocalEmailsDataProvider.allEmails
+ ),
+ onEmailClick = {}
+ )
+ }
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyApp.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyApp.kt
new file mode 100644
index 000000000..c72692f67
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyApp.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
+import androidx.compose.material3.adaptive.currentWindowSize
+import androidx.compose.material3.adaptive.layout.AnimatedPane
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
+import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import com.example.reply.data.Email
+import kotlinx.coroutines.launch
+
+private val WINDOW_WIDTH_LARGE = 1200.dp
+
+@Composable
+fun ReplyApp(
+ replyHomeUIState: ReplyHomeUIState,
+ onEmailClick: (Email) -> Unit,
+) {
+ ReplyNavigationWrapperUI {
+ ReplyAppContent(
+ replyHomeUIState = replyHomeUIState,
+ onEmailClick = onEmailClick
+ )
+ }
+}
+
+@Composable
+private fun ReplyNavigationWrapperUI(
+ content: @Composable () -> Unit = {}
+) {
+ var selectedDestination: ReplyDestination by remember {
+ mutableStateOf(ReplyDestination.Inbox)
+ }
+
+ val windowSize = with(LocalDensity.current) {
+ currentWindowSize().toSize().toDpSize()
+ }
+ val navLayoutType = if (windowSize.width >= WINDOW_WIDTH_LARGE) {
+ // Show a permanent drawer when window width is large.
+ NavigationSuiteType.NavigationDrawer
+ } else {
+ // Otherwise use the default from NavigationSuiteScaffold.
+ NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfo())
+ }
+
+ NavigationSuiteScaffold(
+ navigationSuiteItems = {
+ ReplyDestination.entries.forEach {
+ item(
+ label = { Text(stringResource(it.labelRes)) },
+ icon = { Icon(it.icon, stringResource(it.labelRes)) },
+ selected = it == selectedDestination,
+ onClick = { /*TODO update selection*/ },
+ )
+ }
+ },
+ layoutType = navLayoutType
+ ) {
+ content()
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+fun ReplyAppContent(
+ replyHomeUIState: ReplyHomeUIState,
+ onEmailClick: (Email) -> Unit,
+) {
+ val selectedEmail = replyHomeUIState.selectedEmail
+ val navigator = rememberListDetailPaneScaffoldNavigator()
+ val coroutineScope = rememberCoroutineScope()
+
+ BackHandler(navigator.canNavigateBack()) {
+ coroutineScope.launch { navigator.navigateBack() }
+ }
+
+ ListDetailPaneScaffold(
+ directive = navigator.scaffoldDirective,
+ value = navigator.scaffoldValue,
+ listPane = {
+ AnimatedPane {
+ ReplyListPane(
+ replyHomeUIState = replyHomeUIState,
+ onEmailClick = { email ->
+ onEmailClick(email)
+ coroutineScope.launch {
+ navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
+ }
+ }
+ )
+ }
+ },
+ detailPane = {
+ AnimatedPane {
+ if (selectedEmail != null) {
+ ReplyDetailPane(selectedEmail)
+ }
+ }
+ }
+ )
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt
new file mode 100644
index 000000000..13c0e6423
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyHomeViewModel.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.reply.data.Email
+import com.example.reply.data.EmailsRepository
+import com.example.reply.data.EmailsRepositoryImpl
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class ReplyHomeViewModel(
+ private val emailsRepository: EmailsRepository = EmailsRepositoryImpl()
+): ViewModel() {
+
+ // UI state exposed to the UI
+ private val _uiState = MutableStateFlow(ReplyHomeUIState(loading = true))
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ observeEmails()
+ }
+
+ private fun observeEmails() {
+ viewModelScope.launch {
+ emailsRepository.getAllEmails()
+ .catch { ex ->
+ _uiState.value = ReplyHomeUIState(error = ex.message)
+ }
+ .collect { emails ->
+ // If nothing is selected, initially select the first element.
+ val currentSelected = _uiState.value.selectedEmail
+ _uiState.value = ReplyHomeUIState(
+ emails = emails,
+ selectedEmail = currentSelected ?: emails.firstOrNull()
+ )
+ }
+ }
+ }
+
+ fun setSelectedEmail(email: Email) {
+ _uiState.update {
+ it.copy(selectedEmail = email)
+ }
+ }
+}
+
+data class ReplyHomeUIState(
+ val emails : List = emptyList(),
+ val selectedEmail: Email? = null,
+ val loading: Boolean = false,
+ val error: String? = null
+)
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyListContent.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyListContent.kt
new file mode 100644
index 000000000..e6ba1792c
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyListContent.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.StarBorder
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.example.reply.R
+import com.example.reply.data.Email
+
+@Composable
+fun ReplyListPane(
+ replyHomeUIState: ReplyHomeUIState,
+ onEmailClick: (Email) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(modifier = modifier.fillMaxWidth()) {
+ item {
+ ReplySearchBar(modifier = Modifier.fillMaxWidth())
+ }
+ items(replyHomeUIState.emails) { email ->
+ ReplyEmailListItem(
+ email = email,
+ onEmailClick = onEmailClick
+ )
+ }
+ }
+}
+
+@Composable
+fun ReplyDetailPane(
+ email: Email,
+ modifier: Modifier = Modifier
+) {
+ LazyColumn(modifier = modifier.fillMaxWidth()) {
+ item {
+ ReplyEmailThreadItem(email)
+ }
+ items(email.replies) { reply ->
+ ReplyEmailThreadItem(reply)
+ }
+ }
+}
+
+@Composable
+fun ReplyEmailListItem(
+ email: Email,
+ onEmailClick: (Email) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ onClick = { onEmailClick(email) },
+ modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ ReplyProfileImage(
+ drawableResource = email.sender.avatar,
+ description = email.sender.fullName,
+ )
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = email.sender.firstName,
+ style = MaterialTheme.typography.labelMedium
+ )
+ Text(
+ text = email.createAt,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.outline
+ )
+ }
+ IconButton(
+ onClick = { /*TODO*/ },
+ modifier = Modifier
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surface)
+ ) {
+ Icon(
+ imageVector = Icons.Default.StarBorder,
+ contentDescription = "Favorite",
+ tint = MaterialTheme.colorScheme.outline
+ )
+ }
+ }
+
+ Text(
+ text = email.subject,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
+ )
+ Text(
+ text = email.body,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 2,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+}
+
+@Composable
+fun ReplyEmailThreadItem(
+ email: Email,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ ReplyProfileImage(
+ drawableResource = email.sender.avatar,
+ description = email.sender.fullName,
+ )
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 12.dp, vertical = 4.dp),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = email.sender.firstName,
+ style = MaterialTheme.typography.labelMedium
+ )
+ Text(
+ text = "20 mins ago",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.outline
+ )
+ }
+ IconButton(
+ onClick = { /*TODO*/ },
+ modifier = Modifier
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.surface)
+ ) {
+ Icon(
+ imageVector = Icons.Default.StarBorder,
+ contentDescription = "Favorite",
+ tint = MaterialTheme.colorScheme.outline
+ )
+ }
+ }
+
+ Text(
+ text = email.subject,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.outline,
+ modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
+ )
+
+ Text(
+ text = email.body,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Button(
+ onClick = { /*TODO*/ },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface)
+ ) {
+ Text(
+ text = stringResource(id = R.string.reply),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Button(
+ onClick = { /*TODO*/ },
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface)
+ ) {
+ Text(
+ text = stringResource(id = R.string.reply_all),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+}
+
+
+@Composable
+fun ReplyProfileImage(
+ drawableResource: Int,
+ description: String,
+ modifier: Modifier = Modifier,
+) {
+ Image(
+ modifier = modifier
+ .size(40.dp)
+ .clip(CircleShape),
+ painter = painterResource(id = drawableResource),
+ contentDescription = description,
+ )
+}
+
+@Composable
+fun ReplySearchBar(modifier: Modifier = Modifier) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .background(MaterialTheme.colorScheme.surface, CircleShape),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = stringResource(id = R.string.search),
+ modifier = Modifier.padding(start = 16.dp),
+ tint = MaterialTheme.colorScheme.outline
+ )
+ Text(
+ text = stringResource(id = R.string.search_replies),
+ modifier = Modifier
+ .weight(1f)
+ .padding(16.dp),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.outline
+ )
+ ReplyProfileImage(
+ drawableResource = R.drawable.avatar_6,
+ description = stringResource(id = R.string.profile),
+ modifier = Modifier
+ .padding(12.dp)
+ .size(32.dp)
+ )
+ }
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyNavigation.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyNavigation.kt
new file mode 100644
index 000000000..8dafef379
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/ReplyNavigation.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui
+
+import androidx.annotation.StringRes
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Article
+import androidx.compose.material.icons.filled.Inbox
+import androidx.compose.material.icons.outlined.Chat
+import androidx.compose.material.icons.outlined.People
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.example.reply.R
+
+/** Navigation destinations in the app. */
+enum class ReplyDestination(
+ @StringRes val labelRes: Int,
+ val icon: ImageVector,
+) {
+ Inbox(R.string.tab_inbox, Icons.Default.Inbox),
+
+ Articles(R.string.tab_article, Icons.Default.Article),
+
+ Messages(R.string.tab_dm, Icons.Outlined.Chat),
+
+ Groups(R.string.tab_groups, Icons.Outlined.People),
+}
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Color.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Color.kt
new file mode 100644
index 000000000..a73291a98
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Color.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+// Generate them via theme builder
+// https://material-foundation.github.io/material-theme-builder/#/custom
+
+val replyLightPrimary = Color(0xFF825500)
+val replyLightOnPrimary = Color(0xFFFFFFFF)
+val replyLightPrimaryContainer = Color(0xFFFFDDAE)
+val replyLightOnPrimaryContainer = Color(0xFF2A1800)
+val replyLightSecondary = Color(0xFF6F5B40)
+val replyLightOnSecondary = Color(0xFFFFFFFF)
+val replyLightSecondaryContainer = Color(0xFFFADEBC)
+val replyLightOnSecondaryContainer = Color(0xFF271904)
+val replyLightTertiary = Color(0xFF516440)
+val replyLightOnTertiary = Color(0xFFFFFFFF)
+val replyLightTertiaryContainer = Color(0xFFD3EABC)
+val replyLightOnTertiaryContainer = Color(0xFF102004)
+val replyLightError = Color(0xFFBA1B1B)
+val replyLightErrorContainer = Color(0xFFFFDAD4)
+val replyLightOnError = Color(0xFFFFFFFF)
+val replyLightOnErrorContainer = Color(0xFF410001)
+val replyLightBackground = Color(0xFFFCFCFC)
+val replyLightOnBackground = Color(0xFF1F1B16)
+val replyLightSurface = Color(0xFFFCFCFC)
+val replyLightOnSurface = Color(0xFF1F1B16)
+val replyLightSurfaceVariant = Color(0xFFF0E0CF)
+val replyLightOnSurfaceVariant = Color(0xFF4F4539)
+val replyLightOutline = Color(0xFF817567)
+val replyLightInverseOnSurface = Color(0xFFF9EFE6)
+val replyLightInverseSurface = Color(0xFF34302A)
+val replyLightPrimaryInverse = Color(0xFFFFB945)
+
+
+val replyDarkPrimary = Color(0xFFFFB945)
+val replyDarkOnPrimary = Color(0xFF452B00)
+val replyDarkPrimaryContainer = Color(0xFF624000)
+val replyDarkOnPrimaryContainer = Color(0xFFFFDDAE)
+val replyDarkSecondary = Color(0xFFDDC3A2)
+val replyDarkOnSecondary = Color(0xFF3E2E16)
+val replyDarkSecondaryContainer = Color(0xFF56442B)
+val replyDarkOnSecondaryContainer = Color(0xFFFADEBC)
+val replyDarkTertiary = Color(0xFFB8CEA2)
+val replyDarkOnTertiary = Color(0xFF243516)
+val replyDarkTertiaryContainer = Color(0xFF3A4C2B)
+val replyDarkOnTertiaryContainer = Color(0xFFD3EABC)
+val replyDarkError = Color(0xFFFFB4A9)
+val replyDarkErrorContainer = Color(0xFF930006)
+val replyDarkOnError = Color(0xFF680003)
+val replyDarkOnErrorContainer = Color(0xFFFFDAD4)
+val replyDarkBackground = Color(0xFF1F1B16)
+val replyDarkOnBackground = Color(0xFFEAE1D9)
+val replyDarkSurface = Color(0xFF1F1B16)
+val replyDarkOnSurface = Color(0xFFEAE1D9)
+val replyDarkSurfaceVariant = Color(0xFF4F4539)
+val replyDarkOnSurfaceVariant = Color(0xFFD3C4B4)
+val replyDarkOutline = Color(0xFF9C8F80)
+val replyDarkInverseOnSurface = Color(0xFF1F1B16)
+val replyDarkInverseSurface = Color(0xFFEAE1D9)
+val replyDarkPrimaryInverse = Color(0xFF825500)
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Theme.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Theme.kt
new file mode 100644
index 000000000..958d25b97
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Theme.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+
+// Material 3 color schemes
+private val replyDarkColorScheme = darkColorScheme(
+ primary = replyDarkPrimary,
+ onPrimary = replyDarkOnPrimary,
+ primaryContainer = replyDarkPrimaryContainer,
+ onPrimaryContainer = replyDarkOnPrimaryContainer,
+ inversePrimary = replyDarkPrimaryInverse,
+ secondary = replyDarkSecondary,
+ onSecondary = replyDarkOnSecondary,
+ secondaryContainer = replyDarkSecondaryContainer,
+ onSecondaryContainer = replyDarkOnSecondaryContainer,
+ tertiary = replyDarkTertiary,
+ onTertiary = replyDarkOnTertiary,
+ tertiaryContainer = replyDarkTertiaryContainer,
+ onTertiaryContainer = replyDarkOnTertiaryContainer,
+ error = replyDarkError,
+ onError = replyDarkOnError,
+ errorContainer = replyDarkErrorContainer,
+ onErrorContainer = replyDarkOnErrorContainer,
+ background = replyDarkBackground,
+ onBackground = replyDarkOnBackground,
+ surface = replyDarkSurface,
+ onSurface = replyDarkOnSurface,
+ inverseSurface = replyDarkInverseSurface,
+ inverseOnSurface = replyDarkInverseOnSurface,
+ surfaceVariant = replyDarkSurfaceVariant,
+ onSurfaceVariant = replyDarkOnSurfaceVariant,
+ outline = replyDarkOutline
+)
+
+private val replyLightColorScheme = lightColorScheme(
+ primary = replyLightPrimary,
+ onPrimary = replyLightOnPrimary,
+ primaryContainer = replyLightPrimaryContainer,
+ onPrimaryContainer = replyLightOnPrimaryContainer,
+ inversePrimary = replyLightPrimaryInverse,
+ secondary = replyLightSecondary,
+ onSecondary = replyLightOnSecondary,
+ secondaryContainer = replyLightSecondaryContainer,
+ onSecondaryContainer = replyLightOnSecondaryContainer,
+ tertiary = replyLightTertiary,
+ onTertiary = replyLightOnTertiary,
+ tertiaryContainer = replyLightTertiaryContainer,
+ onTertiaryContainer = replyLightOnTertiaryContainer,
+ error = replyLightError,
+ onError = replyLightOnError,
+ errorContainer = replyLightErrorContainer,
+ onErrorContainer = replyLightOnErrorContainer,
+ background = replyLightBackground,
+ onBackground = replyLightOnBackground,
+ surface = replyLightSurface,
+ onSurface = replyLightOnSurface,
+ inverseSurface = replyLightInverseSurface,
+ inverseOnSurface = replyLightInverseOnSurface,
+ surfaceVariant = replyLightSurfaceVariant,
+ onSurfaceVariant = replyLightOnSurfaceVariant,
+ outline = replyLightOutline
+)
+
+@Composable
+fun ReplyTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val replyColorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> replyDarkColorScheme
+ else -> replyLightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ (view.context as Activity).window.statusBarColor = replyColorScheme.primary.toArgb()
+ ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = replyColorScheme,
+ typography = replyTypography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Type.kt b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Type.kt
new file mode 100644
index 000000000..ba0f907a5
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/java/com/example/reply/ui/theme/Type.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.reply.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Material 3 typography
+val replyTypography = Typography(
+ headlineLarge = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineMedium = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineSmall = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp
+ ),
+ titleLarge = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ titleMedium = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ ),
+ titleSmall = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ ),
+ bodyLarge = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ ),
+ bodyMedium = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ ),
+ bodySmall = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp
+ ),
+ labelLarge = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ ),
+ labelMedium = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelSmall = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+)
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/AdaptiveUiCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_0.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_0.jpg
new file mode 100644
index 000000000..dcf2608a8
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_0.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_1.jpeg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_1.jpeg
new file mode 100644
index 000000000..9dc59cfb4
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_1.jpeg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_10.jpeg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_10.jpeg
new file mode 100644
index 000000000..27b8dc615
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_10.jpeg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_2.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_2.jpg
new file mode 100644
index 000000000..54c74a888
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_2.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_3.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_3.jpg
new file mode 100644
index 000000000..a63f8ce57
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_3.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_4.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_4.jpg
new file mode 100644
index 000000000..279b70def
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_4.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_5.jpeg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_5.jpeg
new file mode 100644
index 000000000..d665a3d00
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_5.jpeg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_6.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_6.jpg
new file mode 100644
index 000000000..0b3226775
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_6.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_7.jpeg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_7.jpeg
new file mode 100644
index 000000000..1cccbb911
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_7.jpeg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_8.jpeg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_8.jpeg
new file mode 100644
index 000000000..5b387afa2
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_8.jpeg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_9.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_9.jpg
new file mode 100644
index 000000000..087bf93af
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_9.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_express.png b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_express.png
new file mode 100644
index 000000000..f05790fb9
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/avatar_express.png differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/ic_launcher_background.xml b/AdaptiveUiCodelab/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..440c0d140
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/paris_1.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_1.jpg
new file mode 100644
index 000000000..eef03ef3d
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_1.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/paris_2.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_2.jpg
new file mode 100644
index 000000000..c2547daf4
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_2.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/paris_3.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_3.jpg
new file mode 100644
index 000000000..13e4841db
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_3.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/drawable/paris_4.jpg b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_4.jpg
new file mode 100644
index 000000000..52a6524d1
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/drawable/paris_4.jpg differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/AdaptiveUiCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/AdaptiveUiCodelab/app/src/main/res/values/strings.xml b/AdaptiveUiCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..defa329ac
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+ Reply
+ Navigation Drawer
+ Inbox
+ Articles
+ Messages
+ Groups
+
+ Profile
+ Search
+ search replies
+ Reply
+ Reply All
+
diff --git a/AdaptiveUiCodelab/app/src/main/res/values/themes.xml b/AdaptiveUiCodelab/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..b6e8bfe98
--- /dev/null
+++ b/AdaptiveUiCodelab/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/build.gradle.kts b/AdaptiveUiCodelab/build.gradle.kts
new file mode 100644
index 000000000..aab7e396a
--- /dev/null
+++ b/AdaptiveUiCodelab/build.gradle.kts
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.compose.compiler) apply false
+}
diff --git a/AdaptiveUiCodelab/gradle.properties b/AdaptiveUiCodelab/gradle.properties
new file mode 100644
index 000000000..84d849c5b
--- /dev/null
+++ b/AdaptiveUiCodelab/gradle.properties
@@ -0,0 +1,37 @@
+# Copyright 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/AdaptiveUiCodelab/gradle/libs.versions.toml b/AdaptiveUiCodelab/gradle/libs.versions.toml
new file mode 100644
index 000000000..fbf309ebf
--- /dev/null
+++ b/AdaptiveUiCodelab/gradle/libs.versions.toml
@@ -0,0 +1,41 @@
+[versions]
+androidGradlePlugin = "9.2.1"
+composeBom = "2026.06.00"
+coreKtx = "1.19.0"
+activityCompose = "1.13.0"
+espressoCore = "3.7.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+kotlin = "2.3.10"
+kotlinxCoroutinesAndroid = "1.10.2"
+lifecycle = "2.11.0"
+material3Adaptive = "1.2.0"
+material3AdaptiveNavSuite = "1.4.0"
+window = "1.5.1"
+
+[libraries]
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
+androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
+androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
+androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+androidx-material3 = { module = "androidx.compose.material3:material3" }
+androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
+androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
+androidx-material3-adaptive-nav-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }
+androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }
+androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+androidx-window = { module = "androidx.window:window", version.ref = "window" }
+junit = { module = "junit:junit", version.ref = "junit" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.jar b/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.properties b/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/AdaptiveUiCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/AdaptiveUiCodelab/gradlew b/AdaptiveUiCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/AdaptiveUiCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/AdaptiveUiCodelab/gradlew.bat b/AdaptiveUiCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/AdaptiveUiCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/AdaptiveUiCodelab/settings.gradle.kts b/AdaptiveUiCodelab/settings.gradle.kts
new file mode 100644
index 000000000..91364c1c2
--- /dev/null
+++ b/AdaptiveUiCodelab/settings.gradle.kts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Reply"
+include(":app")
diff --git a/AdvancedStateAndSideEffectsCodelab/.gitignore b/AdvancedStateAndSideEffectsCodelab/.gitignore
new file mode 100644
index 000000000..4a708a497
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/buildSrc/.gradle/*
+.kotlin/
diff --git a/AdvancedStateAndSideEffectsCodelab/README.md b/AdvancedStateAndSideEffectsCodelab/README.md
new file mode 100644
index 000000000..2268aa274
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/README.md
@@ -0,0 +1,40 @@
+# Advanced State in Jetpack Compose Codelab
+
+This folder contains the source code for the
+[Advanced State in Jetpack Compose Codelab](https://developer.android.com/codelabs/jetpack-compose-advanced-state-side-effects)
+codelab.
+
+The project is built in multiple git branches:
+* `main` – the starter code for this project, you will make changes to this to complete the codelab
+* `end` – contains the solution to this codelab
+
+## [Optional] Google Maps SDK setup
+
+Seeing the city on the MapView is not necessary to complete the codelab. However, if you want
+to get the MapView to render on the screen, you need to get an API key as
+the [documentation says](https://developers.google.com/maps/documentation/android-sdk/get-api-key),
+and include it in the `local.properties` file as follows:
+
+```
+MAPS_API_KEY={insert_your_api_key_here}
+```
+
+When restricting the Key to Android apps, use `androidx.compose.samples.crane` as package name, and
+`A0:BD:B3:B6:F0:C4:BE:90:C6:9D:5F:4C:1D:F0:90:80:7F:D7:FE:1F` as SHA-1 certificate fingerprint.
+
+## License
+```
+Copyright 2021 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
diff --git a/AdvancedStateAndSideEffectsCodelab/app/.gitignore b/AdvancedStateAndSideEffectsCodelab/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/AdvancedStateAndSideEffectsCodelab/app/build.gradle b/AdvancedStateAndSideEffectsCodelab/app/build.gradle
new file mode 100644
index 000000000..5f49ca0e8
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/build.gradle
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'com.google.devtools.ksp'
+ id 'org.jetbrains.kotlin.plugin.compose'
+ id 'dagger.hilt.android.plugin'
+}
+
+// Reads the Google maps key that is used in the AndroidManifest
+Properties properties = new Properties()
+if (rootProject.file("local.properties").exists()) {
+ properties.load(rootProject.file("local.properties").newDataInputStream())
+}
+
+android {
+ namespace "androidx.compose.samples.crane"
+ compileSdkVersion 37
+ namespace "androidx.compose.samples.crane"
+ defaultConfig {
+ applicationId "androidx.compose.samples.crane"
+ minSdkVersion 23
+ targetSdkVersion 33
+ versionCode 1
+ versionName "1.0"
+ vectorDrawables.useSupportLibrary = true
+ testInstrumentationRunner "androidx.compose.samples.crane.CustomTestRunner"
+
+ manifestPlaceholders = [MAPS_API_KEY: properties.getProperty("MAPS_API_KEY", "")]
+ }
+
+ signingConfigs {
+ // We use a bundled debug keystore, to allow debug builds from CI to be upgradable
+ debug {
+ storeFile rootProject.file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ buildFeatures {
+ compose true
+
+ // Disable unused AGP features
+ buildConfig false
+ aidl false
+ renderScript false
+ resValues false
+ shaders false
+ }
+
+ packagingOptions {
+ // Multiple dependency bring these files in. Exclude them to enable
+ // our test APK to build (has no effect on our AARs)
+ excludes += "/META-INF/AL2.0"
+ excludes += "/META-INF/LGPL2.1"
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
+ implementation "com.google.android.libraries.maps:maps:3.1.0-beta"
+ implementation "com.google.maps.android:maps-v3-ktx:3.4.0"
+ constraints {
+ // Volley is a transitive dependency of maps
+ implementation("com.android.volley:volley:1.2.1") {
+ because("Only volley 1.2.0 or newer are available on maven.google.com")
+ }
+ }
+
+ implementation "androidx.activity:activity-compose:1.13.0"
+ implementation "androidx.appcompat:appcompat:1.7.1"
+ implementation "androidx.tracing:tracing:1.3.0"
+
+ def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+ implementation "androidx.compose.runtime:runtime"
+ implementation "androidx.compose.material:material"
+ implementation "androidx.compose.foundation:foundation"
+ implementation "androidx.compose.foundation:foundation-layout"
+ implementation "androidx.compose.animation:animation"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+ debugImplementation "androidx.compose.ui:ui-test-manifest"
+
+ def lifecycle_version = "2.11.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
+
+ implementation "com.google.dagger:hilt-android:2.59.2"
+ ksp "com.google.dagger:hilt-compiler:2.59.2"
+ ksp "org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10"
+
+ implementation "io.coil-kt:coil-compose:2.7.0"
+
+ androidTestImplementation "junit:junit:4.13.2"
+ androidTestImplementation "androidx.test:core:1.7.0"
+ androidTestImplementation "androidx.test:runner:1.7.0"
+ androidTestImplementation "androidx.test:rules:1.7.0"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0"
+ androidTestImplementation "androidx.test.ext:junit-ktx:1.3.0"
+ androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
+ androidTestImplementation "com.google.dagger:hilt-android:2.59.2"
+ androidTestImplementation "com.google.dagger:hilt-android-testing:2.59.2"
+ kspAndroidTest "com.google.dagger:hilt-compiler:2.59.2"
+ kspAndroidTest "org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10"
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/proguard-rules.pro b/AdvancedStateAndSideEffectsCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..4cb94585a
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/proguard-rules.pro
@@ -0,0 +1,24 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+-renamesourcefileattribute SourceFile
+
+# Repackage classes into the top-level.
+-repackageclasses
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/CustomTestRunner.kt b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/CustomTestRunner.kt
new file mode 100644
index 000000000..d81394f07
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/CustomTestRunner.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class CustomTestRunner : AndroidJUnitRunner() {
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
+ return super.newApplication(cl, HiltTestApplication::class.java.name, context)
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt
new file mode 100644
index 000000000..b34279d41
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.details
+
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.data.DestinationsRepository
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.data.MADRID
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.android.libraries.maps.MapView
+import com.google.android.libraries.maps.model.CameraPosition
+import com.google.android.libraries.maps.model.LatLng
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import javax.inject.Inject
+import kotlin.math.pow
+import kotlin.math.round
+
+@HiltAndroidTest
+class DetailsActivityTest {
+
+ @Inject
+ lateinit var destinationsRepository: DestinationsRepository
+ private lateinit var cityDetails: ExploreModel
+
+ private val city = MADRID
+ private val testExploreModel = ExploreModel(city, "description", "imageUrl")
+
+ @get:Rule(order = 0)
+ var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule(order = 1)
+ val composeTestRule = AndroidComposeTestRule(
+ activityRule = ActivityScenarioRule(
+ createDetailsActivityIntent(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ testExploreModel
+ )
+ ),
+ // Needed for now, discussed in https://issuetracker.google.com/issues/174472899
+ activityProvider = { rule ->
+ var activity: DetailsActivity? = null
+ rule.scenario.onActivity { activity = it }
+ if (activity == null) {
+ throw IllegalStateException("Activity was not set in the ActivityScenarioRule!")
+ }
+ activity!!
+ }
+ )
+
+ @Before
+ fun setUp() {
+ hiltRule.inject()
+ cityDetails = destinationsRepository.getDestination(MADRID.name)!!
+ }
+
+ @Test
+ fun mapView_cameraPositioned() {
+ composeTestRule.onNodeWithText(cityDetails.city.nameToDisplay).assertIsDisplayed()
+ composeTestRule.onNodeWithText(cityDetails.description).assertIsDisplayed()
+ onView(withId(R.id.map)).check(matches(isDisplayed()))
+
+ var cameraPosition: CameraPosition? = null
+ waitForMap(onCameraPosition = { cameraPosition = it })
+
+ val expected = LatLng(
+ testExploreModel.city.latitude.toDouble(),
+ testExploreModel.city.longitude.toDouble()
+ )
+ assert(expected.latitude == cameraPosition?.target?.latitude?.round(6))
+ assert(expected.longitude == cameraPosition?.target?.longitude?.round(6))
+ }
+
+ /**
+ * As the MapView is included using the AndroidView API, it cannot be referenced using Compose
+ * testing APIs. Therefore, we use the activityRule to get an instance of the DetailsActivity
+ * an findViewById using MapView's id.
+ *
+ * As obtaining the map is an asynchronous call, we use a CountDownLatch to make this
+ * call synchronous in the test.
+ */
+ private fun waitForMap(onCameraPosition: (CameraPosition) -> Unit) {
+ val countDownLatch = CountDownLatch(1)
+ composeTestRule.activityRule.scenario.onActivity {
+ it.findViewById(R.id.map).getMapAsync { map ->
+ onCameraPosition(map.cameraPosition)
+ countDownLatch.countDown()
+ }
+ }
+ countDownLatch.await()
+ }
+}
+
+private fun Double.round(decimals: Int = 2): Double =
+ round(this * 10f.pow(decimals)) / 10f.pow(decimals)
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt
new file mode 100644
index 000000000..3e3721586
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("DEPRECATION")
+
+package androidx.compose.samples.crane.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module
+@TestInstallIn(
+ components = [SingletonComponent::class],
+ replaces = [DispatchersModule::class]
+)
+class TestDispatchersModule {
+
+ @Provides
+ @DefaultDispatcher
+ fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Unconfined
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt
new file mode 100644
index 000000000..5f3b6e116
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@HiltAndroidTest
+class HomeTest {
+
+ @get:Rule(order = 0)
+ var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule()
+
+ @Test
+ fun home_navigatesToAllScreens() {
+ // Waits until loading is finished
+ composeTestRule.waitUntil(timeoutMillis = 10_000L) {
+ composeTestRule
+ .onAllNodesWithText("Explore Flights by Destination")
+ .fetchSemanticsNodes().size == 1
+ }
+
+ composeTestRule.onNodeWithText("Explore Flights by Destination").assertIsDisplayed()
+ composeTestRule.onNodeWithText("SLEEP").performClick()
+ composeTestRule.onNodeWithText("Explore Properties by Destination").assertIsDisplayed()
+ composeTestRule.onNodeWithText("EAT").performClick()
+ composeTestRule.onNodeWithText("Explore Restaurants by Destination").assertIsDisplayed()
+ composeTestRule.onNodeWithText("FLY").performClick()
+ composeTestRule.onNodeWithText("Explore Flights by Destination").assertIsDisplayed()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/AndroidManifest.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..e5c4acf63
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/ic_launcher-playstore.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 000000000..acbe14b08
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/ic_launcher-playstore.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/CraneApplication.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/CraneApplication.kt
new file mode 100644
index 000000000..0de08cf5f
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/CraneApplication.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane
+
+import android.app.Application
+import androidx.compose.samples.crane.util.UnsplashSizingInterceptor
+import coil.ImageLoader
+import coil.ImageLoaderFactory
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class CraneApplication : Application(), ImageLoaderFactory {
+
+ /**
+ * Create the singleton [ImageLoader].
+ * This is used by [rememberImagePainter] to load images in the app.
+ */
+ override fun newImageLoader(): ImageLoader {
+ return ImageLoader.Builder(this)
+ .components {
+ add(UnsplashSizingInterceptor)
+ }
+ .build()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt
new file mode 100644
index 000000000..078cfc3d7
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.compose.samples.crane.ui.captionTextStyle
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SimpleUserInput(
+ text: String? = null,
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null
+) {
+ CraneUserInput(
+ caption = if (text == null) caption else null,
+ text = text ?: "",
+ vectorImageId = vectorImageId
+ )
+}
+
+@Composable
+fun CraneUserInput(
+ text: String,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = { },
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null,
+ tint: Color = LocalContentColor.current
+) {
+ CraneBaseUserInput(
+ modifier = modifier,
+ onClick = onClick,
+ caption = caption,
+ vectorImageId = vectorImageId,
+ tintIcon = { text.isNotEmpty() },
+ tint = tint
+ ) {
+ Text(text = text, style = MaterialTheme.typography.body1.copy(color = tint))
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun CraneBaseUserInput(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = { },
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null,
+ showCaption: () -> Boolean = { true },
+ tintIcon: () -> Boolean,
+ tint: Color = LocalContentColor.current,
+ content: @Composable () -> Unit
+) {
+ Surface(
+ modifier = modifier,
+ onClick = onClick,
+ color = MaterialTheme.colors.primaryVariant
+ ) {
+ Row(Modifier.padding(all = 12.dp)) {
+ if (vectorImageId != null) {
+ Icon(
+ modifier = Modifier.size(24.dp, 24.dp),
+ painter = painterResource(id = vectorImageId),
+ tint = if (tintIcon()) tint else Color(0x80FFFFFF),
+ contentDescription = null
+ )
+ Spacer(Modifier.width(8.dp))
+ }
+ if (caption != null && showCaption()) {
+ Text(
+ modifier = Modifier.align(Alignment.CenterVertically),
+ text = caption,
+ style = (captionTextStyle).copy(color = tint)
+ )
+ Spacer(Modifier.width(8.dp))
+ }
+ Row(
+ Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ ) {
+ content()
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewInput() {
+ CraneTheme {
+ Surface {
+ CraneBaseUserInput(
+ tintIcon = { true },
+ vectorImageId = R.drawable.ic_plane,
+ caption = "Caption",
+ showCaption = { true }
+ ) {
+ Text(text = "text", style = MaterialTheme.typography.body1)
+ }
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt
new file mode 100644
index 000000000..494636dd9
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+private val screens = listOf("Find Trips", "My Trips", "Saved Trips", "Price Alerts", "My Account")
+
+@Composable
+fun CraneDrawer(modifier: Modifier = Modifier) {
+ Column(
+ modifier
+ .fillMaxSize()
+ .padding(start = 24.dp, top = 48.dp)
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_crane_drawer),
+ contentDescription = stringResource(R.string.cd_drawer)
+ )
+ for (screen in screens) {
+ Spacer(Modifier.height(24.dp))
+ Text(text = screen, style = MaterialTheme.typography.h4)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun CraneDrawerPreview() {
+ CraneTheme {
+ CraneDrawer()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt
new file mode 100644
index 000000000..0851cecdc
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Tab
+import androidx.compose.material.TabRow
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.home.CraneScreen
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.core.os.ConfigurationCompat
+
+@Composable
+fun CraneTabBar(
+ modifier: Modifier = Modifier,
+ onMenuClicked: () -> Unit,
+ children: @Composable (Modifier) -> Unit
+) {
+ Row(modifier) {
+ // Separate Row as the children shouldn't have the padding
+ Row(Modifier.padding(top = 8.dp)) {
+ Image(
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .clickable(onClick = onMenuClicked),
+ painter = painterResource(id = R.drawable.ic_menu),
+ contentDescription = stringResource(id = R.string.cd_menu)
+ )
+ Spacer(Modifier.width(8.dp))
+ Image(
+ painter = painterResource(id = R.drawable.ic_crane_logo),
+ contentDescription = null
+ )
+ }
+ children(
+ Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ )
+ }
+}
+
+@Composable
+fun CraneTabs(
+ modifier: Modifier = Modifier,
+ titles: List,
+ tabSelected: CraneScreen,
+ onTabSelected: (CraneScreen) -> Unit
+) {
+ TabRow(
+ selectedTabIndex = tabSelected.ordinal,
+ modifier = modifier,
+ contentColor = MaterialTheme.colors.onSurface,
+ indicator = { },
+ divider = { }
+ ) {
+ titles.forEachIndexed { index, title ->
+ val selected = index == tabSelected.ordinal
+
+ var textModifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
+ if (selected) {
+ textModifier =
+ Modifier
+ .border(BorderStroke(2.dp, Color.White), RoundedCornerShape(16.dp))
+ .then(textModifier)
+ }
+
+ Tab(
+ selected = selected,
+ onClick = { onTabSelected(CraneScreen.values()[index]) }
+ ) {
+ Text(
+ modifier = textModifier,
+ text = title.uppercase(
+ ConfigurationCompat.getLocales(LocalConfiguration.current)[0]!!
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/EditableUserInput.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/EditableUserInput.kt
new file mode 100644
index 000000000..836f79c00
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/EditableUserInput.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.samples.crane.ui.captionTextStyle
+import androidx.compose.ui.graphics.SolidColor
+
+@Composable
+fun rememberEditableUserInputState(hint: String): EditableUserInputState =
+ rememberSaveable(hint, saver = EditableUserInputState.Saver) {
+ EditableUserInputState(hint, hint)
+ }
+
+class EditableUserInputState(private val hint: String, initialText: String) {
+
+ var text by mutableStateOf(initialText)
+ private set
+
+ fun updateText(newText: String) {
+ text = newText
+ }
+
+ val isHint: Boolean
+ get() = text == hint
+
+ companion object {
+ val Saver: Saver = listSaver(
+ save = { listOf(it.hint, it.text) },
+ restore = {
+ EditableUserInputState(
+ hint = it[0],
+ initialText = it[1],
+ )
+ }
+ )
+ }
+}
+
+@Composable
+fun CraneEditableUserInput(
+ state: EditableUserInputState = rememberEditableUserInputState(""),
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null
+) {
+ CraneBaseUserInput(
+ caption = caption,
+ tintIcon = { !state.isHint },
+ showCaption = { !state.isHint },
+ vectorImageId = vectorImageId
+ ) {
+ BasicTextField(
+ value = state.text,
+ onValueChange = { state.updateText(it) },
+ textStyle = if (state.isHint) {
+ captionTextStyle.copy(color = LocalContentColor.current)
+ } else {
+ MaterialTheme.typography.body1.copy(color = LocalContentColor.current)
+ },
+ cursorBrush = SolidColor(LocalContentColor.current)
+ )
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt
new file mode 100644
index 000000000..732ed6719
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Divider
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.home.OnExploreItemClicked
+import androidx.compose.samples.crane.ui.BottomSheetShape
+import androidx.compose.samples.crane.ui.crane_caption
+import androidx.compose.samples.crane.ui.crane_divider_color
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImagePainter
+import coil.compose.rememberAsyncImagePainter
+import coil.request.ImageRequest.Builder
+import kotlinx.coroutines.launch
+
+@Composable
+fun ExploreSection(
+ modifier: Modifier = Modifier,
+ title: String,
+ exploreList: List,
+ onItemClicked: OnExploreItemClicked
+) {
+ Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
+ Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.caption.copy(color = crane_caption)
+ )
+ Spacer(Modifier.height(8.dp))
+ Box(Modifier.weight(1f)) {
+ val listState = rememberLazyListState()
+ ExploreList(exploreList, onItemClicked, listState = listState)
+
+ // Show the button if the first visible item is past
+ // the first item. We use a remembered derived state to
+ // minimize unnecessary compositions
+ val showButton by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0
+ }
+ }
+ if (showButton) {
+ val coroutineScope = rememberCoroutineScope()
+ FloatingActionButton(
+ backgroundColor = MaterialTheme.colors.primary,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .navigationBarsPadding()
+ .padding(bottom = 8.dp),
+ onClick = {
+ coroutineScope.launch {
+ listState.scrollToItem(0)
+ }
+ }
+ ) {
+ Text("Up!")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ExploreList(
+ exploreList: List,
+ onItemClicked: OnExploreItemClicked,
+ modifier: Modifier = Modifier,
+ listState: LazyListState = rememberLazyListState()
+) {
+ LazyColumn(
+ modifier = modifier,
+ contentPadding = WindowInsets.navigationBars.asPaddingValues(),
+ state = listState
+ ) {
+ items(exploreList) { exploreItem ->
+ Column(Modifier.fillParentMaxWidth()) {
+ ExploreItem(
+ modifier = Modifier.fillParentMaxWidth(),
+ item = exploreItem,
+ onItemClicked = onItemClicked
+ )
+ Divider(color = crane_divider_color)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ExploreItem(
+ modifier: Modifier = Modifier,
+ item: ExploreModel,
+ onItemClicked: OnExploreItemClicked
+) {
+ Row(
+ modifier = modifier
+ .clickable { onItemClicked(item) }
+ .padding(top = 12.dp, bottom = 12.dp)
+ ) {
+ ExploreImageContainer {
+ Box {
+ val painter = rememberAsyncImagePainter(
+ Builder(LocalContext.current).data(data = item.imageUrl)
+ .apply(block = fun Builder.() {
+ crossfade(true)
+ }).build()
+ )
+ Image(
+ painter = painter,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.fillMaxSize(),
+ )
+
+ if (painter.state is AsyncImagePainter.State.Loading) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_crane_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .size(36.dp)
+ .align(Alignment.Center),
+ )
+ }
+ }
+ }
+ Spacer(Modifier.width(24.dp))
+ Column {
+ Text(
+ text = item.city.nameToDisplay,
+ style = MaterialTheme.typography.h6
+ )
+ Spacer(Modifier.height(8.dp))
+ Text(
+ text = item.description,
+ style = MaterialTheme.typography.caption.copy(color = crane_caption)
+ )
+ }
+ }
+}
+
+@Composable
+private fun ExploreImageContainer(content: @Composable () -> Unit) {
+ Surface(Modifier.size(width = 60.dp, height = 60.dp), RoundedCornerShape(4.dp)) {
+ content()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/Result.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/Result.kt
new file mode 100644
index 000000000..5dad2368e
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/base/Result.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+/**
+ * A generic class that holds a value.
+ * @param
+ */
+sealed class Result {
+ data class Success(val data: T) : Result()
+ data class Error(val exception: Exception) : Result()
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt
new file mode 100644
index 000000000..4304fc3f6
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+val MADRID = City(
+ name = "Madrid",
+ country = "Spain",
+ latitude = "40.416775",
+ longitude = "-3.703790"
+)
+
+val NAPLES = City(
+ name = "Naples",
+ country = "Italy",
+ latitude = "40.853294",
+ longitude = "14.305573"
+)
+
+val DALLAS = City(
+ name = "Dallas",
+ country = "US",
+ latitude = "32.779167",
+ longitude = "-96.808891"
+)
+
+val CORDOBA = City(
+ name = "Cordoba",
+ country = "Argentina",
+ latitude = "-31.416668",
+ longitude = "-64.183334"
+)
+
+val MALDIVAS = City(
+ name = "Maldivas",
+ country = "South Asia",
+ latitude = "1.924992",
+ longitude = "73.399658"
+)
+
+val ASPEN = City(
+ name = "Aspen",
+ country = "Colorado",
+ latitude = "39.191097",
+ longitude = "-106.817535"
+)
+
+val BALI = City(
+ name = "Bali",
+ country = "Indonesia",
+ latitude = "-8.3405",
+ longitude = "115.0920"
+)
+
+val BIGSUR = City(
+ name = "Big Sur",
+ country = "California",
+ latitude = "36.2704",
+ longitude = "-121.8081"
+)
+
+val KHUMBUVALLEY = City(
+ name = "Khumbu Valley",
+ country = "Nepal",
+ latitude = "27.9320",
+ longitude = "86.8050"
+)
+
+val ROME = City(
+ name = "Rome",
+ country = "Italy",
+ latitude = "41.902782",
+ longitude = "12.496366"
+)
+
+val GRANADA = City(
+ name = "Granada",
+ country = "Spain",
+ latitude = "37.18817",
+ longitude = "-3.60667"
+)
+
+val WASHINGTONDC = City(
+ name = "Washington DC",
+ country = "USA",
+ latitude = "38.9072",
+ longitude = "-77.0369"
+)
+
+val BARCELONA = City(
+ name = "Barcelona",
+ country = "Spain",
+ latitude = "41.390205",
+ longitude = "2.154007"
+)
+
+val CRETE = City(
+ name = "Crete",
+ country = "Greece",
+ latitude = "35.2401",
+ longitude = "24.8093"
+)
+
+val LONDON = City(
+ name = "London",
+ country = "United Kingdom",
+ latitude = "51.509865",
+ longitude = "-0.118092"
+)
+
+val PARIS = City(
+ name = "Paris",
+ country = "France",
+ latitude = "48.864716",
+ longitude = "2.349014"
+)
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsLocalDataSource.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsLocalDataSource.kt
new file mode 100644
index 000000000..28e3cd627
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsLocalDataSource.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val DEFAULT_IMAGE_WIDTH = "250"
+
+/**
+ * Annotated with Singleton as the class created a lot of objects.
+ */
+@Singleton
+class DestinationsLocalDataSource @Inject constructor() {
+
+ val craneRestaurants = listOf(
+ ExploreModel(
+ city = NAPLES,
+ description = "1286 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = DALLAS,
+ description = "2241 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1495749388945-9d6e4e5b67b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CORDOBA,
+ description = "876 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1562625964-ffe9b2f617fc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MADRID,
+ description = "5610 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1515443961218-a51367888e4b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MALDIVAS,
+ description = "1286 Restaurants",
+ imageUrl = "https://images.unsplash.com/flagged/photo-1556202256-af2687079e51?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ASPEN,
+ description = "2241 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1542384557-0824d90731ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "876 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1567337710282-00832b415979?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ )
+ )
+
+ val craneHotels = listOf(
+ ExploreModel(
+ city = MALDIVAS,
+ description = "1286 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ASPEN,
+ description = "2241 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1445019980597-93fa8acb246c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "876 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1570213489059-0aac6626cade?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BIGSUR,
+ description = "5610 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1561409037-c7be81613c1f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = NAPLES,
+ description = "1286 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1455587734955-081b22074882?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = DALLAS,
+ description = "2241 Available Properties",
+ imageUrl = "https://images.unsplash.com/46/sh3y2u5PSaKq8c4LxB3B_submission-photo-4.jpg?ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CORDOBA,
+ description = "876 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1570214476695-19bd467e6f7a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ )
+ )
+
+ val craneDestinations = listOf(
+ ExploreModel(
+ city = KHUMBUVALLEY,
+ description = "Nonstop - 5h 16m+",
+ imageUrl = "https://images.unsplash.com/photo-1544735716-392fe2489ffa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MADRID,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1539037116277-4db20889f2d4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "Nonstop - 6h 20m+",
+ imageUrl = "https://images.unsplash.com/photo-1518548419970-58e3b4079ab2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ROME,
+ description = "Nonstop - 2h 38m+",
+ imageUrl = "https://images.unsplash.com/photo-1515542622106-78bda8ba0e5b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = GRANADA,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1534423839368-1796a4dd1845?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MALDIVAS,
+ description = "Nonstop - 9h 24m+",
+ imageUrl = "https://images.unsplash.com/photo-1544550581-5f7ceaf7f992?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = WASHINGTONDC,
+ description = "Nonstop - 7h 30m+",
+ imageUrl = "https://images.unsplash.com/photo-1557160854-e1e89fdd3286?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BARCELONA,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1562883676-8c7feb83f09b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CRETE,
+ description = "Nonstop - 1h 50m+",
+ imageUrl = "https://images.unsplash.com/photo-1486575008575-27670acb58db?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = NAPLES,
+ description = "Nonstop - 1h 45m+",
+ imageUrl = "https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = DALLAS,
+ description = "Nonstop - 8h 30m+",
+ imageUrl = "https://images.unsplash.com/photo-1495749388945-9d6e4e5b67b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CORDOBA,
+ description = "1 stop - 11h 30m+",
+ imageUrl = "https://images.unsplash.com/photo-1562625964-ffe9b2f617fc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BIGSUR,
+ description = "Nonstop - 10h 45m+",
+ imageUrl = "https://images.unsplash.com/photo-1561409037-c7be81613c1f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = LONDON,
+ description = "Nonstop - 1h 5m+",
+ imageUrl = "https://images.unsplash.com/photo-1505761671935-60b3a7427bad?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = PARIS,
+ description = "Nonstop - 2h 25m+",
+ imageUrl = "https://images.unsplash.com/photo-1509299349698-dd22323b5963?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ )
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsRepository.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsRepository.kt
new file mode 100644
index 000000000..df419033a
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/DestinationsRepository.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+import javax.inject.Inject
+
+class DestinationsRepository @Inject constructor(
+ private val destinationsLocalDataSource: DestinationsLocalDataSource
+) {
+ val destinations: List = destinationsLocalDataSource.craneDestinations
+ val hotels: List = destinationsLocalDataSource.craneHotels
+ val restaurants: List = destinationsLocalDataSource.craneRestaurants
+
+ fun getDestination(cityName: String): ExploreModel? {
+ return destinationsLocalDataSource.craneDestinations.firstOrNull {
+ it.city.name == cityName
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt
new file mode 100644
index 000000000..9155e0055
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class City(
+ val name: String,
+ val country: String,
+ val latitude: String,
+ val longitude: String
+) {
+ val nameToDisplay = "$name, $country"
+}
+
+@Immutable
+data class ExploreModel(
+ val city: City,
+ val description: String,
+ val imageUrl: String
+)
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt
new file mode 100644
index 000000000..4e36d6004
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.details
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.samples.crane.base.Result
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.android.libraries.maps.CameraUpdateFactory
+import com.google.android.libraries.maps.MapView
+import com.google.android.libraries.maps.model.LatLng
+import com.google.maps.android.ktx.addMarker
+import com.google.maps.android.ktx.awaitMap
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+internal const val KEY_ARG_DETAILS_CITY_NAME = "KEY_ARG_DETAILS_CITY_NAME"
+
+fun launchDetailsActivity(context: Context, item: ExploreModel) {
+ context.startActivity(createDetailsActivityIntent(context, item))
+}
+
+@VisibleForTesting
+fun createDetailsActivityIntent(context: Context, item: ExploreModel): Intent {
+ val intent = Intent(context, DetailsActivity::class.java)
+ intent.putExtra(KEY_ARG_DETAILS_CITY_NAME, item.city.name)
+ return intent
+}
+
+@AndroidEntryPoint
+class DetailsActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ CraneTheme {
+ Surface {
+ DetailsScreen(
+ onErrorLoading = { finish() },
+ modifier = Modifier.systemBarsPadding()
+ )
+ }
+ }
+ }
+ }
+}
+
+data class DetailsUiState(
+ val cityDetails: ExploreModel? = null,
+ val isLoading: Boolean = false,
+ val throwError: Boolean = false
+)
+
+@Composable
+fun DetailsScreen(
+ onErrorLoading: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: DetailsViewModel = viewModel()
+) {
+ val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
+ val cityDetailsResult = viewModel.cityDetails
+ value = if (cityDetailsResult is Result.Success) {
+ DetailsUiState(cityDetailsResult.data)
+ } else {
+ DetailsUiState(throwError = true)
+ }
+ }
+
+ when {
+ uiState.cityDetails != null -> {
+ DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
+ }
+ uiState.isLoading -> {
+ Box(modifier.fillMaxSize()) {
+ CircularProgressIndicator(
+ color = MaterialTheme.colors.onSurface,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+ else -> { onErrorLoading() }
+ }
+}
+
+@Composable
+fun DetailsContent(
+ exploreModel: ExploreModel,
+ modifier: Modifier = Modifier
+) {
+ Column(modifier = modifier, verticalArrangement = Arrangement.Center) {
+ Spacer(Modifier.height(32.dp))
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = exploreModel.city.nameToDisplay,
+ style = MaterialTheme.typography.h4,
+ textAlign = TextAlign.Center
+ )
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = exploreModel.description,
+ style = MaterialTheme.typography.h6,
+ textAlign = TextAlign.Center
+ )
+ Spacer(Modifier.height(16.dp))
+ CityMapView(exploreModel.city.latitude, exploreModel.city.longitude)
+ }
+}
+
+@Composable
+private fun CityMapView(latitude: String, longitude: String) {
+ // The MapView lifecycle is handled by this composable. As the MapView also needs to be updated
+ // with input from Compose UI, those updates are encapsulated into the MapViewContainer
+ // composable. In this way, when an update to the MapView happens, this composable won't
+ // recompose and the MapView won't need to be recreated.
+ val mapView = rememberMapViewWithLifecycle()
+ MapViewContainer(mapView, latitude, longitude)
+}
+
+@Composable
+private fun MapViewContainer(
+ map: MapView,
+ latitude: String,
+ longitude: String
+) {
+ val cameraPosition = remember(latitude, longitude) {
+ LatLng(latitude.toDouble(), longitude.toDouble())
+ }
+
+ LaunchedEffect(map) {
+ val googleMap = map.awaitMap()
+ googleMap.addMarker { position(cameraPosition) }
+ googleMap.moveCamera(CameraUpdateFactory.newLatLng(cameraPosition))
+ }
+
+ var zoom by rememberSaveable(map) { mutableStateOf(InitialZoom) }
+ ZoomControls(zoom) {
+ zoom = it.coerceIn(MinZoom, MaxZoom)
+ }
+
+ val coroutineScope = rememberCoroutineScope()
+ AndroidView({ map }) { mapView ->
+ // Reading zoom so that AndroidView recomposes when it changes. The getMapAsync lambda
+ // is stored for later, Compose doesn't recognize state reads
+ val mapZoom = zoom
+ coroutineScope.launch {
+ val googleMap = mapView.awaitMap()
+ googleMap.setZoom(mapZoom)
+ // Move camera to the same place to trigger the zoom update
+ googleMap.moveCamera(CameraUpdateFactory.newLatLng(cameraPosition))
+ }
+ }
+}
+
+@Composable
+private fun ZoomControls(
+ zoom: Float,
+ onZoomChanged: (Float) -> Unit
+) {
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ ZoomButton("-", onClick = { onZoomChanged(zoom * 0.8f) })
+ ZoomButton("+", onClick = { onZoomChanged(zoom * 1.2f) })
+ }
+}
+
+@Composable
+private fun ZoomButton(text: String, onClick: () -> Unit) {
+ Button(
+ modifier = Modifier.padding(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MaterialTheme.colors.onPrimary,
+ contentColor = MaterialTheme.colors.primary
+ ),
+ onClick = onClick
+ ) {
+ Text(text = text, style = MaterialTheme.typography.h5)
+ }
+}
+
+private const val InitialZoom = 5f
+const val MinZoom = 2f
+const val MaxZoom = 20f
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt
new file mode 100644
index 000000000..1d52b524e
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.details
+
+import androidx.compose.samples.crane.base.Result
+import androidx.compose.samples.crane.data.DestinationsRepository
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class DetailsViewModel @Inject constructor(
+ private val destinationsRepository: DestinationsRepository,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val cityName = savedStateHandle.get(KEY_ARG_DETAILS_CITY_NAME)!!
+
+ val cityDetails: Result
+ get() {
+ val destination = destinationsRepository.getDestination(cityName)
+ return if (destination != null) {
+ Result.Success(destination)
+ } else {
+ Result.Error(IllegalArgumentException("City doesn't exist"))
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt
new file mode 100644
index 000000000..0d6128c10
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.details
+
+import android.os.Bundle
+import androidx.annotation.FloatRange
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.samples.crane.R
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import com.google.android.libraries.maps.GoogleMap
+import com.google.android.libraries.maps.MapView
+
+/**
+ * Remembers a MapView and gives it the lifecycle of the current LifecycleOwner
+ */
+@Composable
+fun rememberMapViewWithLifecycle(): MapView {
+ val context = LocalContext.current
+ val mapView = remember {
+ MapView(context).apply {
+ id = R.id.map
+ }
+ }
+
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+ DisposableEffect(key1 = lifecycle, key2 = mapView) {
+ // Make MapView follow the current lifecycle
+ val lifecycleObserver = getMapLifecycleObserver(mapView)
+ lifecycle.addObserver(lifecycleObserver)
+ onDispose {
+ lifecycle.removeObserver(lifecycleObserver)
+ }
+ }
+
+ return mapView
+}
+
+private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
+ LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
+ Lifecycle.Event.ON_START -> mapView.onStart()
+ Lifecycle.Event.ON_RESUME -> mapView.onResume()
+ Lifecycle.Event.ON_PAUSE -> mapView.onPause()
+ Lifecycle.Event.ON_STOP -> mapView.onStop()
+ Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
+ else -> throw IllegalStateException()
+ }
+ }
+
+fun GoogleMap.setZoom(
+ @FloatRange(from = MinZoom.toDouble(), to = MaxZoom.toDouble()) zoom: Float
+) {
+ resetMinMaxZoomPreference()
+ setMinZoomPreference(zoom)
+ setMaxZoomPreference(zoom)
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/di/DispatchersModule.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/di/DispatchersModule.kt
new file mode 100644
index 000000000..21ec6554f
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/di/DispatchersModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Qualifier
+
+@Module
+@InstallIn(SingletonComponent::class)
+class DispatchersModule {
+
+ @Provides
+ @DefaultDispatcher
+ fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
+}
+
+@Retention(AnnotationRetention.BINARY)
+@Qualifier
+annotation class DefaultDispatcher
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt
new file mode 100644
index 000000000..48bcb9fcb
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material.BackdropScaffold
+import androidx.compose.material.BackdropValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Scaffold
+import androidx.compose.material.rememberBackdropScaffoldState
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.samples.crane.base.CraneDrawer
+import androidx.compose.samples.crane.base.CraneTabBar
+import androidx.compose.samples.crane.base.CraneTabs
+import androidx.compose.samples.crane.base.ExploreSection
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.launch
+
+typealias OnExploreItemClicked = (ExploreModel) -> Unit
+
+enum class CraneScreen {
+ Fly, Sleep, Eat
+}
+
+@Composable
+fun CraneHome(
+ onExploreItemClicked: OnExploreItemClicked,
+ modifier: Modifier = Modifier,
+) {
+ val scaffoldState = rememberScaffoldState()
+ Scaffold(
+ scaffoldState = scaffoldState,
+ modifier = Modifier.statusBarsPadding(),
+ drawerContent = {
+ CraneDrawer()
+ }
+ ) { padding ->
+ val scope = rememberCoroutineScope()
+ CraneHomeContent(
+ modifier = modifier.padding(padding),
+ onExploreItemClicked = onExploreItemClicked,
+ openDrawer = {
+ scope.launch {
+ scaffoldState.drawerState.open()
+ }
+ }
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun CraneHomeContent(
+ onExploreItemClicked: OnExploreItemClicked,
+ openDrawer: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: MainViewModel = viewModel(),
+) {
+ val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle()
+
+ val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
+ var tabSelected by remember { mutableStateOf(CraneScreen.Fly) }
+
+ BackdropScaffold(
+ modifier = modifier,
+ scaffoldState = rememberBackdropScaffoldState(BackdropValue.Revealed),
+ frontLayerScrimColor = Color.Unspecified,
+ appBar = {
+ HomeTabBar(openDrawer, tabSelected, onTabSelected = { tabSelected = it })
+ },
+ backLayerContent = {
+ SearchContent(
+ tabSelected,
+ viewModel,
+ onPeopleChanged
+ )
+ },
+ frontLayerContent = {
+ when (tabSelected) {
+ CraneScreen.Fly -> {
+ ExploreSection(
+ title = "Explore Flights by Destination",
+ exploreList = suggestedDestinations,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ CraneScreen.Sleep -> {
+ ExploreSection(
+ title = "Explore Properties by Destination",
+ exploreList = viewModel.hotels,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ CraneScreen.Eat -> {
+ ExploreSection(
+ title = "Explore Restaurants by Destination",
+ exploreList = viewModel.restaurants,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ }
+ }
+ )
+}
+
+@Composable
+private fun HomeTabBar(
+ openDrawer: () -> Unit,
+ tabSelected: CraneScreen,
+ onTabSelected: (CraneScreen) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ CraneTabBar(
+ modifier = modifier,
+ onMenuClicked = openDrawer
+ ) { tabBarModifier ->
+ CraneTabs(
+ modifier = tabBarModifier,
+ titles = CraneScreen.values().map { it.name },
+ tabSelected = tabSelected,
+ onTabSelected = { newTab -> onTabSelected(CraneScreen.values()[newTab.ordinal]) }
+ )
+ }
+}
+
+@Composable
+private fun SearchContent(
+ tabSelected: CraneScreen,
+ viewModel: MainViewModel,
+ onPeopleChanged: (Int) -> Unit
+) {
+ when (tabSelected) {
+ CraneScreen.Fly -> FlySearchContent(
+ onPeopleChanged = onPeopleChanged,
+ onToDestinationChanged = { viewModel.toDestinationChanged(it) }
+ )
+ CraneScreen.Sleep -> SleepSearchContent(
+ onPeopleChanged = onPeopleChanged
+ )
+ CraneScreen.Eat -> EatSearchContent(
+ onPeopleChanged = onPeopleChanged
+ )
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt
new file mode 100644
index 000000000..ab73dd04f
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.SimpleUserInput
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FlySearchContent(
+ onPeopleChanged: (Int) -> Unit,
+ onToDestinationChanged: (String) -> Unit
+) {
+ CraneSearch {
+ PeopleUserInput(
+ titleSuffix = ", Economy",
+ onPeopleChanged = onPeopleChanged
+ )
+ Spacer(Modifier.height(8.dp))
+ FromDestination()
+ Spacer(Modifier.height(8.dp))
+ ToDestinationUserInput(onToDestinationChanged = onToDestinationChanged)
+ Spacer(Modifier.height(8.dp))
+ DatesUserInput()
+ }
+}
+
+@Composable
+fun SleepSearchContent(onPeopleChanged: (Int) -> Unit) {
+ CraneSearch {
+ PeopleUserInput(onPeopleChanged = onPeopleChanged)
+ Spacer(Modifier.height(8.dp))
+ DatesUserInput()
+ Spacer(Modifier.height(8.dp))
+ SimpleUserInput(caption = "Select Location", vectorImageId = R.drawable.ic_hotel)
+ }
+}
+
+@Composable
+fun EatSearchContent(onPeopleChanged: (Int) -> Unit) {
+ CraneSearch {
+ PeopleUserInput(onPeopleChanged = onPeopleChanged)
+ Spacer(Modifier.height(8.dp))
+ DatesUserInput()
+ Spacer(Modifier.height(8.dp))
+ SimpleUserInput(caption = "Select Time", vectorImageId = R.drawable.ic_time)
+ Spacer(Modifier.height(8.dp))
+ SimpleUserInput(caption = "Select Location", vectorImageId = R.drawable.ic_restaurant)
+ }
+}
+
+@Composable
+private fun CraneSearch(content: @Composable () -> Unit) {
+ Column(Modifier.padding(start = 24.dp, top = 0.dp, end = 24.dp, bottom = 12.dp)) {
+ content()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt
new file mode 100644
index 000000000..d0b2d7913
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.samples.crane.R
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import kotlinx.coroutines.delay
+
+private const val SplashWaitTime: Long = 2000
+
+@Composable
+fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
+ Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ // Adds composition consistency. Use the value when LaunchedEffect is first called
+ val currentOnTimeout by rememberUpdatedState(onTimeout)
+
+ LaunchedEffect(Unit) {
+ delay(SplashWaitTime)
+ currentOnTimeout()
+ }
+ Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt
new file mode 100644
index 000000000..4d3e28ba4
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.samples.crane.details.launchDetailsActivity
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.core.view.WindowCompat
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContent {
+ CraneTheme {
+ MainScreen(onExploreItemClicked = {
+ launchDetailsActivity(
+ context = this,
+ item = it
+ )
+ })
+ }
+ }
+ }
+}
+
+@Composable
+private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
+ Surface(color = MaterialTheme.colors.primary) {
+ var showLandingScreen by remember {
+ mutableStateOf(true)
+ }
+ if (showLandingScreen) {
+ LandingScreen(onTimeout = { showLandingScreen = false })
+ } else {
+ CraneHome(onExploreItemClicked = onExploreItemClicked)
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt
new file mode 100644
index 000000000..7c6128172
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.samples.crane.data.DestinationsRepository
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.di.DefaultDispatcher
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import kotlin.random.Random
+
+const val MAX_PEOPLE = 4
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val destinationsRepository: DestinationsRepository,
+ @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
+) : ViewModel() {
+
+ val hotels: List = destinationsRepository.hotels
+ val restaurants: List = destinationsRepository.restaurants
+
+ private val _suggestedDestinations = MutableStateFlow>(emptyList())
+ val suggestedDestinations: StateFlow> = _suggestedDestinations.asStateFlow()
+
+ init {
+ _suggestedDestinations.value = destinationsRepository.destinations
+ }
+
+ fun updatePeople(people: Int) {
+ viewModelScope.launch {
+ if (people > MAX_PEOPLE) {
+ _suggestedDestinations.value = emptyList()
+ } else {
+ val newDestinations = withContext(defaultDispatcher) {
+ destinationsRepository.destinations
+ .shuffled(Random(people * (1..100).shuffled().first()))
+ }
+ _suggestedDestinations.value = newDestinations
+ }
+ }
+ }
+
+ fun toDestinationChanged(newDestination: String) {
+ viewModelScope.launch {
+ val newDestinations = withContext(defaultDispatcher) {
+ destinationsRepository.destinations
+ .filter { it.city.nameToDisplay.contains(newDestination) }
+ }
+ _suggestedDestinations.value = newDestinations
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt
new file mode 100644
index 000000000..27945f2f4
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.CraneEditableUserInput
+import androidx.compose.samples.crane.base.CraneUserInput
+import androidx.compose.samples.crane.base.rememberEditableUserInputState
+import androidx.compose.samples.crane.home.PeopleUserInputAnimationState.Invalid
+import androidx.compose.samples.crane.home.PeopleUserInputAnimationState.Valid
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import kotlinx.coroutines.flow.filter
+
+enum class PeopleUserInputAnimationState { Valid, Invalid }
+
+class PeopleUserInputState {
+ var people by mutableStateOf(1)
+ private set
+
+ val animationState: MutableTransitionState =
+ MutableTransitionState(Valid)
+
+ fun addPerson() {
+ people = (people % (MAX_PEOPLE + 1)) + 1
+ updateAnimationState()
+ }
+
+ private fun updateAnimationState() {
+ val newState =
+ if (people > MAX_PEOPLE) Invalid
+ else Valid
+
+ if (animationState.currentState != newState) animationState.targetState = newState
+ }
+}
+
+@Composable
+fun PeopleUserInput(
+ titleSuffix: String? = "",
+ onPeopleChanged: (Int) -> Unit,
+ peopleState: PeopleUserInputState = remember { PeopleUserInputState() }
+) {
+ Column {
+ val transitionState = remember { peopleState.animationState }
+ val tint = tintPeopleUserInput(transitionState)
+
+ val people = peopleState.people
+ CraneUserInput(
+ text = if (people == 1) "$people Adult$titleSuffix" else "$people Adults$titleSuffix",
+ vectorImageId = R.drawable.ic_person,
+ tint = tint.value,
+ onClick = {
+ peopleState.addPerson()
+ onPeopleChanged(peopleState.people)
+ }
+ )
+ if (transitionState.targetState == Invalid) {
+ Text(
+ text = "Error: We don't support more than $MAX_PEOPLE people",
+ style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)
+ )
+ }
+ }
+}
+
+@Composable
+fun FromDestination() {
+ CraneUserInput(text = "Seoul, South Korea", vectorImageId = R.drawable.ic_location)
+}
+
+@Composable
+fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
+ val editableUserInputState = rememberEditableUserInputState(hint = "Choose Destination")
+ CraneEditableUserInput(
+ state = editableUserInputState,
+ caption = "To",
+ vectorImageId = R.drawable.ic_plane
+ )
+
+ val currentOnDestinationChanged by rememberUpdatedState(onToDestinationChanged)
+ LaunchedEffect(editableUserInputState) {
+ snapshotFlow { editableUserInputState.text }
+ .filter { !editableUserInputState.isHint }
+ .collect {
+ currentOnDestinationChanged(editableUserInputState.text)
+ }
+ }
+}
+
+@Composable
+fun DatesUserInput() {
+ CraneUserInput(
+ caption = "Select Dates",
+ text = "",
+ vectorImageId = R.drawable.ic_calendar
+ )
+}
+
+@Composable
+private fun tintPeopleUserInput(
+ transitionState: MutableTransitionState
+): State {
+ val validColor = MaterialTheme.colors.onSurface
+ val invalidColor = MaterialTheme.colors.secondary
+
+ val transition = updateTransition(transitionState, label = "")
+ return transition.animateColor(
+ transitionSpec = { tween(durationMillis = 300) }, label = ""
+ ) {
+ if (it == Valid) validColor else invalidColor
+ }
+}
+
+@Preview
+@Composable
+fun PeopleUserInputPreview() {
+ CraneTheme {
+ PeopleUserInput(onPeopleChanged = {})
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt
new file mode 100644
index 000000000..9cbceefd7
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+val crane_caption = Color.DarkGray
+val crane_divider_color = Color.LightGray
+private val crane_red = Color(0xFFE30425)
+private val crane_white = Color.White
+private val crane_purple_700 = Color(0xFF720D5D)
+private val crane_purple_800 = Color(0xFF5D1049)
+private val crane_purple_900 = Color(0xFF4E0D3A)
+
+val craneColors = lightColors(
+ primary = crane_purple_800,
+ secondary = crane_red,
+ surface = crane_purple_900,
+ onSurface = crane_white,
+ primaryVariant = crane_purple_700
+)
+
+val BottomSheetShape = RoundedCornerShape(
+ topStart = 20.dp,
+ topEnd = 20.dp,
+ bottomStart = 0.dp,
+ bottomEnd = 0.dp
+)
+
+@Composable
+fun CraneTheme(content: @Composable () -> Unit) {
+ MaterialTheme(colors = craneColors, typography = craneTypography) {
+ content()
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt
new file mode 100644
index 000000000..4e807b73b
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.compose.material.Typography
+import androidx.compose.samples.crane.R
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+private val light = Font(R.font.raleway_light, FontWeight.W300)
+private val regular = Font(R.font.raleway_regular, FontWeight.W400)
+private val medium = Font(R.font.raleway_medium, FontWeight.W500)
+private val semibold = Font(R.font.raleway_semibold, FontWeight.W600)
+
+private val craneFontFamily = FontFamily(fonts = listOf(light, regular, medium, semibold))
+
+val captionTextStyle = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 16.sp
+)
+
+val craneTypography = Typography(
+ h1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W300,
+ fontSize = 96.sp
+ ),
+ h2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 60.sp
+ ),
+ h3 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 48.sp
+ ),
+ h4 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 34.sp
+ ),
+ h5 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 24.sp
+ ),
+ h6 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 20.sp
+ ),
+ subtitle1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 16.sp
+ ),
+ subtitle2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 14.sp
+ ),
+ body1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 16.sp
+ ),
+ body2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 14.sp
+ ),
+ button = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 12.sp
+ ),
+ overline = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 12.sp
+ )
+)
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/NetworkUtil.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/NetworkUtil.kt
new file mode 100644
index 000000000..ab601d9ef
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/NetworkUtil.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.util
+
+import coil.intercept.Interceptor
+import coil.request.ImageResult
+import okhttp3.HttpUrl.Companion.toHttpUrl
+
+/**
+ * A Coil [Interceptor] which appends query params to Unsplash urls to request sized images.
+ */
+object UnsplashSizingInterceptor : Interceptor {
+ override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
+ val data = chain.request.data
+ val size = chain.size
+ if (data is String &&
+ data.startsWith("https://images.unsplash.com/photo-")
+ ) {
+ val url = data.toHttpUrl()
+ .newBuilder()
+ .addQueryParameter("w", size.width.toString())
+ .addQueryParameter("h", size.height.toString())
+ .build()
+ val request = chain.request.newBuilder().data(url).build()
+ return chain.proceed(request)
+ }
+ return chain.proceed(chain.request)
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt
new file mode 100644
index 000000000..fa4f432f3
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.util
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+
+@Composable
+fun Circle(color: Color) {
+ Canvas(Modifier.fillMaxSize()) {
+ drawCircle(color)
+ }
+}
+
+@Composable
+fun SemiRect(color: Color, lookingLeft: Boolean = true) {
+ val layoutDirection = LocalLayoutDirection.current
+ Canvas(Modifier.fillMaxSize()) {
+ // The SemiRect should face left EITHER the lookingLeft param is true
+ // OR the layoutDirection is Rtl
+ val offset = if (lookingLeft xor (layoutDirection == LayoutDirection.Rtl)) {
+ Offset(0f, 0f)
+ } else {
+ Offset(size.width / 2, 0f)
+ }
+ val size = Size(width = size.width / 2, height = size.height)
+
+ drawRect(size = size, topLeft = offset, color = color)
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_back.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 000000000..960992c0b
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_calendar.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_calendar.xml
new file mode 100644
index 000000000..0e1dd670f
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_drawer.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_drawer.xml
new file mode 100644
index 000000000..882e1b547
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_drawer.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_logo.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_logo.xml
new file mode 100644
index 000000000..57b849978
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_crane_logo.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_hotel.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_hotel.xml
new file mode 100644
index 000000000..85856159d
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_hotel.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..98ac48537
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_location.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_location.xml
new file mode 100644
index 000000000..ce32b44cd
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_location.xml
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_menu.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_menu.xml
new file mode 100644
index 000000000..c76aa3cea
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_menu.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_person.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_person.xml
new file mode 100644
index 000000000..6066f41cb
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_person.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_plane.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_plane.xml
new file mode 100644
index 000000000..9d72412cc
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_plane.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_restaurant.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_restaurant.xml
new file mode 100644
index 000000000..04b64dbe7
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_restaurant.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_time.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_time.xml
new file mode 100644
index 000000000..5f6704760
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/drawable/ic_time.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_light.ttf b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_light.ttf
new file mode 100755
index 000000000..b5ec48606
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_light.ttf differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_medium.ttf b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_medium.ttf
new file mode 100755
index 000000000..070ac7691
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_medium.ttf differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_regular.ttf b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_regular.ttf
new file mode 100755
index 000000000..746c24238
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_regular.ttf differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_semibold.ttf b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_semibold.ttf
new file mode 100755
index 000000000..34db42061
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/font/raleway_semibold.ttf differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..fe8cc4795
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..c3bc12a98
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ac3662d8d
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..d16629b96
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5cb5cfe5b
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..ba761daff
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values-v29/styles.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values-v29/styles.xml
new file mode 100644
index 000000000..52f164e9c
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values-v29/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/colors.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..84c23aeb5
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+ #5D1049
+ #3D0A2C
+ #E30425
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ic_launcher_background.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..bcc5f1271
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,20 @@
+
+
+
+ #5D1049
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ids.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ids.xml
new file mode 100644
index 000000000..c873906cc
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/ids.xml
@@ -0,0 +1,19 @@
+
+
+
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/strings.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..819feadd0
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+
+ Crane
+
+ Menu
+ Back
+ Loading
+ Open drawer
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/styles.xml b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..b67b38424
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/app/src/main/res/values/styles.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AdvancedStateAndSideEffectsCodelab/build.gradle b/AdvancedStateAndSideEffectsCodelab/build.gradle
new file mode 100644
index 000000000..0359705fa
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/build.gradle
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:9.2.1"
+ classpath "com.google.dagger:hilt-android-gradle-plugin:2.59.2"
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10"
+ }
+}
+
+plugins {
+ id 'com.diffplug.spotless' version '8.7.0'
+ id 'com.google.devtools.ksp' version '2.3.9' apply false
+}
+
+subprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ apply plugin: 'com.diffplug.spotless'
+ spotless {
+ kotlin {
+ target '**/*.kt'
+ targetExclude("$buildDir/**/*.kt")
+ targetExclude('bin/**/*.kt')
+ ktlint("0.45.2")
+ licenseHeaderFile rootProject.file('spotless/copyright.kt')
+ }
+ }
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ jvmTarget = "17"
+
+ // Use experimental APIs
+ freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
+ }
+ }
+ // androidx.test and hilt are forcing JUnit, 4.12. This forces them to use 4.13
+ configurations.configureEach {
+ resolutionStrategy {
+ force "junit:junit:4.13.2"
+ }
+ }
+}
diff --git a/AdvancedStateAndSideEffectsCodelab/debug.keystore b/AdvancedStateAndSideEffectsCodelab/debug.keystore
new file mode 100644
index 000000000..6024334a4
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/debug.keystore differ
diff --git a/AdvancedStateAndSideEffectsCodelab/gradle.properties b/AdvancedStateAndSideEffectsCodelab/gradle.properties
new file mode 100644
index 000000000..e2a50a753
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/gradle.properties
@@ -0,0 +1,45 @@
+#
+# Copyright 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+
+# Turn on parallel compilation, caching and on-demand configuration
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+# Needed for com.google.android.libraries.maps:maps
+android.enableJetifier=true
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Enable R8 full mode.
+android.enableR8.fullMode=false
diff --git a/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.jar b/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.properties b/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/AdvancedStateAndSideEffectsCodelab/gradlew b/AdvancedStateAndSideEffectsCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/AdvancedStateAndSideEffectsCodelab/gradlew.bat b/AdvancedStateAndSideEffectsCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/AdvancedStateAndSideEffectsCodelab/screenshots/crane.gif b/AdvancedStateAndSideEffectsCodelab/screenshots/crane.gif
new file mode 100644
index 000000000..c0690e52c
Binary files /dev/null and b/AdvancedStateAndSideEffectsCodelab/screenshots/crane.gif differ
diff --git a/AdvancedStateAndSideEffectsCodelab/settings.gradle b/AdvancedStateAndSideEffectsCodelab/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/AdvancedStateAndSideEffectsCodelab/spotless/copyright.kt b/AdvancedStateAndSideEffectsCodelab/spotless/copyright.kt
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/AdvancedStateAndSideEffectsCodelab/spotless/copyright.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
diff --git a/BasicLayoutsCodelab/.gitignore b/BasicLayoutsCodelab/.gitignore
new file mode 100644
index 000000000..4a708a497
--- /dev/null
+++ b/BasicLayoutsCodelab/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/buildSrc/.gradle/*
+.kotlin/
diff --git a/BasicLayoutsCodelab/ASSETS_LICENSE b/BasicLayoutsCodelab/ASSETS_LICENSE
new file mode 100644
index 000000000..e7fc95866
--- /dev/null
+++ b/BasicLayoutsCodelab/ASSETS_LICENSE
@@ -0,0 +1,88 @@
+All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 license.
+
+
+SIL OPEN FONT LICENSE
+Version 1.1 - 26 February 2007
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/CONTRIBUTING.md b/BasicLayoutsCodelab/CONTRIBUTING.md
new file mode 100644
index 000000000..03f956b11
--- /dev/null
+++ b/BasicLayoutsCodelab/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/LICENSE b/BasicLayoutsCodelab/LICENSE
new file mode 100644
index 000000000..f49a4e16e
--- /dev/null
+++ b/BasicLayoutsCodelab/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/README.md b/BasicLayoutsCodelab/README.md
new file mode 100644
index 000000000..9796ccc76
--- /dev/null
+++ b/BasicLayoutsCodelab/README.md
@@ -0,0 +1,37 @@
+# Basic Layouts in Compose Codelab
+
+This folder contains the source code for
+the [Basic Layouts in Compose Codelab](https://developer.android.com/codelabs/jetpack-compose-layouts)
+
+## License
+
+```
+Copyright 2022 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+## Image asset attributions
+
+[fc1_short_mantras.jpg](https://www.pexels.com/photo/body-of-water-view-1825206/) - Elizaveta Kozorezova
+[fc2_nature_meditations.jpg](https://www.pexels.com/photo/photo-of-green-leaves-3571551/) - Nothing Ahead
+[fc3_stress_and_anxiety.jpg](https://www.pexels.com/photo/aerial-view-of-body-of-water-1557238/) - Jim
+[fc4_self_massage.jpg](https://www.pexels.com/photo/photography-of-stones-1029604/) - Scott Webb
+[fc5_overwhelmed.jpg](https://www.pexels.com/photo/white-clouds-3560044/) - Ruvim
+[fc6_nightly_wind_down.jpg](https://www.pexels.com/photo/time-lapse-photo-of-stars-on-night-924824/) - Jakub Novacek
+[ab1_inversions.jpg](https://www.pexels.com/photo/low-angle-view-of-woman-relaxing-on-beach-against-blue-sky-317157/) - Chevanon Photography
+[ab2_quick_yoga.jpg](https://www.pexels.com/photo/photo-of-woman-stretching-her-body-1812964/) - Agung Pandit Wiguna
+[ab3_stretching.jpg](https://www.pexels.com/photo/photo-of-women-stretching-together-4056723/) - Cliff Booth
+[ab4_tabata.jpg](https://www.pexels.com/photo/fashion-man-people-art-4662438/) - Elly Fairytale
+[ab5_hiit.jpg](https://www.pexels.com/photo/man-wearing-white-pants-under-blue-sky-999309/) - The Lazy Artist Gallery
+[ab6_pre_natal_yoga.jpg](https://www.pexels.com/photo/woman-doing-yoga-396133/) - freestocks.org
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/.gitignore b/BasicLayoutsCodelab/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/BasicLayoutsCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/build.gradle b/BasicLayoutsCodelab/app/build.gradle
new file mode 100644
index 000000000..13990cd6d
--- /dev/null
+++ b/BasicLayoutsCodelab/app/build.gradle
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.plugin.compose'
+}
+
+android {
+ compileSdk 37
+ namespace "com.codelab.basiclayouts"
+
+ defaultConfig {
+ applicationId "com.codelab.basiclayouts"
+ minSdk 23
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ buildFeatures {
+ compose true
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation 'androidx.core:core-ktx:1.19.0'
+ implementation "androidx.compose.ui:ui"
+ implementation 'androidx.compose.material3:material3'
+ implementation 'androidx.compose.material3:material3-window-size-class:1.4.0'
+ implementation "androidx.compose.material:material-icons-extended"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ implementation "com.google.android.material:material:1.14.0"
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.11.0'
+ implementation 'androidx.activity:activity-compose:1.13.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+}
diff --git a/BasicLayoutsCodelab/app/proguard-rules.pro b/BasicLayoutsCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/BasicLayoutsCodelab/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/AndroidManifest.xml b/BasicLayoutsCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..941cd15be
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/MainActivity.kt b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/MainActivity.kt
new file mode 100644
index 000000000..1f953f036
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/MainActivity.kt
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.codelab.basiclayouts
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Spa
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.codelab.basiclayouts.ui.theme.MySootheTheme
+
+class MainActivity : ComponentActivity() {
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val windowSizeClass = calculateWindowSizeClass(this)
+ MySootheApp(windowSizeClass)
+ }
+ }
+}
+
+// Step: Search bar - Modifiers
+@Composable
+fun SearchBar(
+ modifier: Modifier = Modifier
+) {
+ TextField(
+ value = "",
+ onValueChange = {},
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null
+ )
+ },
+ colors = TextFieldDefaults.colors(
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface,
+ focusedContainerColor = MaterialTheme.colorScheme.surface
+ ),
+ placeholder = {
+ Text(stringResource(R.string.placeholder_search))
+ },
+ modifier = modifier
+ .fillMaxWidth()
+ .heightIn(min = 56.dp)
+ )
+}
+
+// Step: Align your body - Alignment
+@Composable
+fun AlignYourBodyElement(
+ @DrawableRes drawable: Int,
+ @StringRes text: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = painterResource(drawable),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .size(88.dp)
+ .clip(CircleShape)
+ )
+ Text(
+ text = stringResource(text),
+ modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
+
+// Step: Favorite collection card - Material Surface
+@Composable
+fun FavoriteCollectionCard(
+ @DrawableRes drawable: Int,
+ @StringRes text: Int,
+ modifier: Modifier = Modifier
+) {
+ Surface(
+ shape = MaterialTheme.shapes.medium,
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ modifier = modifier
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.width(255.dp)
+ ) {
+ Image(
+ painter = painterResource(drawable),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.size(80.dp)
+ )
+ Text(
+ text = stringResource(text),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+ }
+}
+
+// Step: Align your body row - Arrangements
+@Composable
+fun AlignYourBodyRow(
+ modifier: Modifier = Modifier
+) {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ modifier = modifier
+ ) {
+ items(alignYourBodyData) { item ->
+ AlignYourBodyElement(item.drawable, item.text)
+ }
+ }
+}
+
+// Step: Favorite collections grid - LazyGrid
+@Composable
+fun FavoriteCollectionsGrid(
+ modifier: Modifier = Modifier
+) {
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(2),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = modifier.height(168.dp)
+ ) {
+ items(favoriteCollectionsData) { item ->
+ FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
+ }
+ }
+}
+
+// Step: Home section - Slot APIs
+@Composable
+fun HomeSection(
+ @StringRes title: Int,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Column(modifier) {
+ Text(
+ text = stringResource(title),
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
+ )
+ content()
+ }
+}
+
+// Step: Home screen - Scrolling
+@Composable
+fun HomeScreen(modifier: Modifier = Modifier) {
+ Column(
+ modifier
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(Modifier.height(16.dp))
+ SearchBar(Modifier.padding(horizontal = 16.dp))
+ HomeSection(title = R.string.align_your_body) {
+ AlignYourBodyRow()
+ }
+ HomeSection(title = R.string.favorite_collections) {
+ FavoriteCollectionsGrid()
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+}
+
+// Step: Bottom navigation - Material
+@Composable
+private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
+ NavigationBar(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ modifier = modifier
+ ) {
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = Icons.Default.Spa,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_navigation_home))
+ },
+ selected = true,
+ onClick = {}
+ )
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_navigation_profile))
+ },
+ selected = false,
+ onClick = {}
+ )
+ }
+}
+
+// Step: Navigation Rail - Material
+@Composable
+private fun SootheNavigationRail(modifier: Modifier = Modifier) {
+ NavigationRail(
+ modifier = modifier.padding(start = 8.dp, end = 8.dp),
+ containerColor = MaterialTheme.colorScheme.background,
+ ) {
+ Column(
+ modifier = modifier.fillMaxHeight(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ NavigationRailItem(
+ icon = {
+ Icon(
+ imageVector = Icons.Default.Spa,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_navigation_home))
+ },
+ selected = true,
+ onClick = {}
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ NavigationRailItem(
+ icon = {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(stringResource(R.string.bottom_navigation_profile))
+ },
+ selected = false,
+ onClick = {}
+ )
+ }
+ }
+}
+
+// Step: MySoothe App - Scaffold
+@Composable
+fun MySootheApp(windowSize: WindowSizeClass) {
+ when (windowSize.widthSizeClass) {
+ WindowWidthSizeClass.Compact -> {
+ MySootheAppPortrait()
+ }
+ WindowWidthSizeClass.Expanded -> {
+ MySootheAppLandscape()
+ }
+ }
+}
+
+@Composable
+fun MySootheAppPortrait() {
+ MySootheTheme {
+ Scaffold(
+ bottomBar = { SootheBottomNavigation() }
+ ) { padding ->
+ HomeScreen(Modifier.padding(padding))
+ }
+ }
+}
+
+@Composable
+fun MySootheAppLandscape() {
+ MySootheTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ Row {
+ SootheNavigationRail()
+ HomeScreen()
+ }
+ }
+ }
+}
+
+private val alignYourBodyData = listOf(
+ R.drawable.ab1_inversions to R.string.ab1_inversions,
+ R.drawable.ab2_quick_yoga to R.string.ab2_quick_yoga,
+ R.drawable.ab3_stretching to R.string.ab3_stretching,
+ R.drawable.ab4_tabata to R.string.ab4_tabata,
+ R.drawable.ab5_hiit to R.string.ab5_hiit,
+ R.drawable.ab6_pre_natal_yoga to R.string.ab6_pre_natal_yoga
+).map { DrawableStringPair(it.first, it.second) }
+
+private val favoriteCollectionsData = listOf(
+ R.drawable.fc1_short_mantras to R.string.fc1_short_mantras,
+ R.drawable.fc2_nature_meditations to R.string.fc2_nature_meditations,
+ R.drawable.fc3_stress_and_anxiety to R.string.fc3_stress_and_anxiety,
+ R.drawable.fc4_self_massage to R.string.fc4_self_massage,
+ R.drawable.fc5_overwhelmed to R.string.fc5_overwhelmed,
+ R.drawable.fc6_nightly_wind_down to R.string.fc6_nightly_wind_down
+).map { DrawableStringPair(it.first, it.second) }
+
+private data class DrawableStringPair(
+ @DrawableRes val drawable: Int,
+ @StringRes val text: Int
+)
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun SearchBarPreview() {
+ MySootheTheme { SearchBar(Modifier.padding(8.dp)) }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun AlignYourBodyElementPreview() {
+ MySootheTheme {
+ AlignYourBodyElement(
+ text = R.string.ab1_inversions,
+ drawable = R.drawable.ab1_inversions,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun FavoriteCollectionCardPreview() {
+ MySootheTheme {
+ FavoriteCollectionCard(
+ text = R.string.fc2_nature_meditations,
+ drawable = R.drawable.fc2_nature_meditations,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun FavoriteCollectionsGridPreview() {
+ MySootheTheme { FavoriteCollectionsGrid() }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun AlignYourBodyRowPreview() {
+ MySootheTheme { AlignYourBodyRow() }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun HomeSectionPreview() {
+ MySootheTheme {
+ HomeSection(R.string.align_your_body) {
+ AlignYourBodyRow()
+ }
+ }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
+@Composable
+fun ScreenContentPreview() {
+ MySootheTheme { HomeScreen() }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun BottomNavigationPreview() {
+ MySootheTheme { SootheBottomNavigation(Modifier.padding(top = 24.dp)) }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
+@Composable
+fun NavigationRailPreview() {
+ MySootheTheme { SootheNavigationRail() }
+}
+
+@Preview(widthDp = 360, heightDp = 640)
+@Composable
+fun MySoothePortraitPreview() {
+ MySootheAppPortrait()
+}
+
+@Preview(widthDp = 640, heightDp = 360)
+@Composable
+fun MySootheLandscapePreview() {
+ MySootheAppLandscape()
+}
diff --git a/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Color.kt b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Color.kt
new file mode 100644
index 000000000..6046f6790
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Color.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.codelab.basiclayouts.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val md_theme_light_primary = Color(0xFF6B5C4D)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_primaryContainer = Color(0xFFF4DFCD)
+val md_theme_light_onPrimaryContainer = Color(0xFF241A0E)
+val md_theme_light_secondary = Color(0xFF635D59)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFEAE1DB)
+val md_theme_light_onSecondaryContainer = Color(0xFF1F1B17)
+val md_theme_light_tertiary = Color(0xFF5E5F58)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFE3E3DA)
+val md_theme_light_onTertiaryContainer = Color(0xFF1B1C17)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFF5F0EE)
+val md_theme_light_onBackground = Color(0xFF1D1B1A)
+val md_theme_light_surface = Color(0xFFFFFBFF)
+val md_theme_light_onSurface = Color(0xFF1D1B1A)
+val md_theme_light_surfaceVariant = Color(0xFFE7E1DE)
+val md_theme_light_onSurfaceVariant = Color(0xFF494644)
+val md_theme_light_outline = Color(0xFF7A7674)
+val md_theme_light_inverseOnSurface = Color(0xFFF5F0EE)
+
+val md_theme_dark_primary = Color(0xFFD7C3B1)
+val md_theme_dark_onPrimary = Color(0xFF3A2E22)
+val md_theme_dark_primaryContainer = Color(0xFF524437)
+val md_theme_dark_onPrimaryContainer = Color(0xFFF4DFCD)
+val md_theme_dark_secondary = Color(0xFFCDC5BF)
+val md_theme_dark_onSecondary = Color(0xFF34302C)
+val md_theme_dark_secondaryContainer = Color(0xFF4B4642)
+val md_theme_dark_onSecondaryContainer = Color(0xFFEAE1DB)
+val md_theme_dark_tertiary = Color(0xFFC7C7BE)
+val md_theme_dark_onTertiary = Color(0xFF30312B)
+val md_theme_dark_tertiaryContainer = Color(0xFF464741)
+val md_theme_dark_onTertiaryContainer = Color(0xFFE3E3DA)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFB4AB)
+val md_theme_dark_background = Color(0xFF32302F)
+val md_theme_dark_onBackground = Color(0xFFE6E1E0)
+val md_theme_dark_surface = Color(0xFF1D1B1A)
+val md_theme_dark_onSurface = Color(0xFFE6E1E0)
+val md_theme_dark_surfaceVariant = Color(0xFF494644)
+val md_theme_dark_onSurfaceVariant = Color(0xFFE6E1E0)
+val md_theme_dark_outline = Color(0xFF94908D)
diff --git a/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Shape.kt b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Shape.kt
new file mode 100644
index 000000000..395e7ae10
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Shape.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.codelab.basiclayouts.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+val shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(12.dp)
+)
diff --git a/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Theme.kt b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Theme.kt
new file mode 100644
index 000000000..96db70787
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Theme.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.codelab.basiclayouts.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+
+private val LightColors = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline
+)
+
+
+private val DarkColors = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline
+)
+
+@Composable
+fun MySootheTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable() () -> Unit
+) {
+ val colors = if (darkTheme) {
+ DarkColors
+ } else {
+ LightColors
+ }
+
+ MaterialTheme(
+ colorScheme = colors,
+ typography = typography,
+ shapes = shapes,
+ content = content
+ )
+}
diff --git a/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Type.kt b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Type.kt
new file mode 100644
index 000000000..2cb1dcbd3
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/java/com/codelab/basiclayouts/ui/theme/Type.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.codelab.basiclayouts.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.sp
+import com.codelab.basiclayouts.R
+
+private val fontFamilyKulim = FontFamily(
+ listOf(
+ Font(
+ resId = R.font.kulim_park_regular
+ ),
+ Font(
+ resId = R.font.kulim_park_light,
+ weight = FontWeight.Light
+ )
+ )
+)
+
+private val fontFamilyLato = FontFamily(
+ listOf(
+ Font(
+ resId = R.font.lato_regular
+ ),
+ Font(
+ resId = R.font.lato_bold,
+ weight = FontWeight.Bold
+ )
+ )
+)
+
+val typography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = fontFamilyKulim,
+ fontWeight = FontWeight.Light,
+ fontSize = 57.sp,
+ lineHeight = 64.sp,
+ letterSpacing = (-0.25).sp
+ ),
+ displayMedium = TextStyle(
+ fontFamily = fontFamilyKulim,
+ fontSize = 45.sp,
+ lineHeight = 52.sp
+ ),
+ displaySmall = TextStyle(
+ fontFamily = fontFamilyKulim,
+ fontSize = 36.sp,
+ lineHeight = 44.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontWeight = FontWeight(500),
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = (0.15).sp
+ ),
+ bodySmall = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = (0.4).sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = (0.25).sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = (0.5).sp
+ ),
+ labelMedium = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontWeight = FontWeight(500),
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = (0.5).sp,
+ textAlign = TextAlign.Center
+ ),
+ labelLarge = TextStyle(
+ fontFamily = fontFamilyLato,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = (0.1).sp
+ )
+)
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/BasicLayoutsCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..e5600f9b6
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab1_inversions.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab1_inversions.jpg
new file mode 100644
index 000000000..73bf26f11
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab1_inversions.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab2_quick_yoga.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab2_quick_yoga.jpg
new file mode 100644
index 000000000..999c124ca
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab2_quick_yoga.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab3_stretching.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab3_stretching.jpg
new file mode 100644
index 000000000..ada9f8f51
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab3_stretching.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab4_tabata.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab4_tabata.jpg
new file mode 100644
index 000000000..8e1db89ba
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab4_tabata.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab5_hiit.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab5_hiit.jpg
new file mode 100644
index 000000000..776d9ea8c
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab5_hiit.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ab6_pre_natal_yoga.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/ab6_pre_natal_yoga.jpg
new file mode 100644
index 000000000..b1cae07fb
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/ab6_pre_natal_yoga.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc1_short_mantras.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc1_short_mantras.jpg
new file mode 100644
index 000000000..332abae22
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc1_short_mantras.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc2_nature_meditations.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc2_nature_meditations.jpg
new file mode 100644
index 000000000..dc6048cd3
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc2_nature_meditations.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc3_stress_and_anxiety.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc3_stress_and_anxiety.jpg
new file mode 100644
index 000000000..c35e01b88
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc3_stress_and_anxiety.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc4_self_massage.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc4_self_massage.jpg
new file mode 100644
index 000000000..d7d59849e
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc4_self_massage.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc5_overwhelmed.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc5_overwhelmed.jpg
new file mode 100644
index 000000000..945e94a49
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc5_overwhelmed.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/fc6_nightly_wind_down.jpg b/BasicLayoutsCodelab/app/src/main/res/drawable/fc6_nightly_wind_down.jpg
new file mode 100644
index 000000000..c682f6ae2
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/drawable/fc6_nightly_wind_down.jpg differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/drawable/ic_launcher_background.xml b/BasicLayoutsCodelab/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..0eace0157
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_light.ttf b/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_light.ttf
new file mode 100644
index 000000000..cfe086ecc
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_light.ttf differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_regular.ttf b/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_regular.ttf
new file mode 100644
index 000000000..1387503e8
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/font/kulim_park_regular.ttf differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/font/lato_bold.ttf b/BasicLayoutsCodelab/app/src/main/res/font/lato_bold.ttf
new file mode 100644
index 000000000..b63a14d6a
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/font/lato_bold.ttf differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/font/lato_regular.ttf b/BasicLayoutsCodelab/app/src/main/res/font/lato_regular.ttf
new file mode 100644
index 000000000..33eba8b19
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/font/lato_regular.ttf differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..41693623d
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..41693623d
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/BasicLayoutsCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/BasicLayoutsCodelab/app/src/main/res/values-night/colors.xml b/BasicLayoutsCodelab/app/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..007841d3b
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,16 @@
+
+
+ #32302F
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/values/colors.xml b/BasicLayoutsCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..b94156844
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,16 @@
+
+
+ #F5F0EE
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/values/strings.xml b/BasicLayoutsCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..5f979c4e4
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,34 @@
+
+
+ MySoothe
+ Favorite Collections
+ Align your Body
+ Inversions
+ Quick yoga
+ Stretching
+ Tabata
+ HIIT
+ Pre-natal yoga
+ Short mantras
+ Nature meditations
+ Stress and anxiety
+ Self massage
+ Overwhelmed
+ Nightly wind down
+ start
+ Home
+ Profile
+ Search
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/app/src/main/res/values/themes.xml b/BasicLayoutsCodelab/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..23c1e6c95
--- /dev/null
+++ b/BasicLayoutsCodelab/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/build.gradle b/BasicLayoutsCodelab/build.gradle
new file mode 100644
index 000000000..5e8de1699
--- /dev/null
+++ b/BasicLayoutsCodelab/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:9.2.1"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10"
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10"
+ }
+}
+
+plugins {
+ id 'com.diffplug.spotless' version '8.7.0'
+}
+
+subprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ apply plugin: 'com.diffplug.spotless'
+ spotless {
+ kotlin {
+ target '**/*.kt'
+ targetExclude("$buildDir/**/*.kt")
+ targetExclude('bin/**/*.kt')
+
+ ktlint("0.45.2").userData([android: "true"])
+ licenseHeaderFile rootProject.file('spotless/copyright.kt')
+ }
+ }
+}
diff --git a/BasicLayoutsCodelab/gradle.properties b/BasicLayoutsCodelab/gradle.properties
new file mode 100644
index 000000000..46a3404f7
--- /dev/null
+++ b/BasicLayoutsCodelab/gradle.properties
@@ -0,0 +1,29 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.jar b/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.properties b/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/BasicLayoutsCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/BasicLayoutsCodelab/gradlew b/BasicLayoutsCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/BasicLayoutsCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/BasicLayoutsCodelab/gradlew.bat b/BasicLayoutsCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/BasicLayoutsCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/BasicLayoutsCodelab/settings.gradle b/BasicLayoutsCodelab/settings.gradle
new file mode 100644
index 000000000..a4cf5c4fa
--- /dev/null
+++ b/BasicLayoutsCodelab/settings.gradle
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+rootProject.name = "MySoothe"
+include ':app'
diff --git a/BasicLayoutsCodelab/spotless/copyright.kt b/BasicLayoutsCodelab/spotless/copyright.kt
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/BasicLayoutsCodelab/spotless/copyright.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..bb70059a4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..1af981f55
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MigrationCodelab/.gitignore b/MigrationCodelab/.gitignore
new file mode 100644
index 000000000..abb0bb836
--- /dev/null
+++ b/MigrationCodelab/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+ktlint
+.kotlin/
diff --git a/MigrationCodelab/ASSETS_LICENSE b/MigrationCodelab/ASSETS_LICENSE
new file mode 100644
index 000000000..0f2611cdb
--- /dev/null
+++ b/MigrationCodelab/ASSETS_LICENSE
@@ -0,0 +1,62 @@
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE.
+
+License
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
+
+1. Definitions
+
+"Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with one or more other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License.
+"Creative Commons Compatible License" means a license that is listed at https://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of derivatives of works made available under that license under this License or either a Creative Commons unported license or a Creative Commons jurisdiction license with the same License Elements as this License.
+"Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License.
+"License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
+"Licensor" means the individual, individuals, entity or entities that offers the Work under the terms of this License.
+"Original Author" means the individual, individuals, entity or entities who created the Work.
+"Work" means the copyrightable work of authorship offered under the terms of this License.
+"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
+2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
+
+to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works;
+to create and reproduce Derivative Works provided that any such Derivative Work, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified.";
+to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works;
+to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works.
+For the avoidance of doubt, where the Work is a musical composition:
+
+Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or, in the event that Licensor is a member of a performance rights society (e.g. ASCAP, BMI, SESAC), via that society, royalties for the public performance or public digital performance (e.g. webcast) of the Work.
+Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights agency or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions).
+Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions).
+The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
+
+You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of a recipient of the Work to exercise of the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. When You distribute, publicly display, publicly perform, or publicly digitally perform the Work, You may not impose any technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise of the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by Section 4(c), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by Section 4(c), as requested.
+You may distribute, publicly display, publicly perform, or publicly digitally perform a Derivative Work only under: (i) the terms of this License; (ii) a later version of this License with the same License Elements as this License; (iii) either the Creative Commons (Unported) license or a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g. Attribution-ShareAlike 3.0 (Unported)); (iv) a Creative Commons Compatible License. If you license the Derivative Work under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Derivative Work under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and with the following provisions: (I) You must include a copy of, or the Uniform Resource Identifier for, the Applicable License with every copy or phonorecord of each Derivative Work You distribute, publicly display, publicly perform, or publicly digitally perform; (II) You may not offer or impose any terms on the Derivative Works that restrict the terms of the Applicable License or the ability of a recipient of the Work to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties; and, (IV) when You distribute, publicly display, publicly perform, or publicly digitally perform the Work, You may not impose any technological measures on the Derivative Work that restrict the ability of a recipient of the Derivative Work from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Derivative Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Derivative Work itself to be made subject to the terms of the Applicable License.
+If You distribute, publicly display, publicly perform, or publicly digitally perform the Work (as defined in Section 1 above) or any Derivative Works (as defined in Section 1 above) or Collective Works (as defined in Section 1 above), You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and, consistent with Section 3(b) in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear, if a credit for all contributing authors of the Derivative Work or Collective Work appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND ONLY TO THE EXTENT OF ANY RIGHTS HELD IN THE LICENSED WORK BY THE LICENSOR. THE LICENSOR MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MARKETABILITY, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
+Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
+8. Miscellaneous
+
+Each time You distribute or publicly digitally perform the Work (as defined in Section 1 above) or a Collective Work (as defined in Section 1 above), the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
+Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
+If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
+No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
+This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
+
+Creative Commons Notice
+
+Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor.
+
+Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License.
+
+Creative Commons may be contacted at https://creativecommons.org/.
\ No newline at end of file
diff --git a/MigrationCodelab/CONTRIBUTING.md b/MigrationCodelab/CONTRIBUTING.md
new file mode 100644
index 000000000..bb70059a4
--- /dev/null
+++ b/MigrationCodelab/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/MigrationCodelab/LICENSE b/MigrationCodelab/LICENSE
new file mode 100644
index 000000000..1af981f55
--- /dev/null
+++ b/MigrationCodelab/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MigrationCodelab/README.md b/MigrationCodelab/README.md
new file mode 100644
index 000000000..b58f8d76c
--- /dev/null
+++ b/MigrationCodelab/README.md
@@ -0,0 +1,47 @@
+# Migrating to Jetpack Compose
+
+This folder contains the source code for the [Migrating to Jetpack Compose codelab](https://developer.android.com/codelabs/jetpack-compose-migration).
+
+The codelab which migrates parts of [Sunflower](https://github.com/android/sunflower)'s Plant
+details screen to Jetpack Compose is built in multiple GitHub branches:
+
+* `main` is the codelab's starting point.
+* `end` contains the solution to this codelab.
+
+## Pre-requisites
+* Experience with Kotlin syntax, including lambdas.
+* Knowing the [basics of Compose](https://developer.android.com/codelabs/jetpack-compose-basics/).
+
+## Getting Started
+1. Install the latest Android Studio [canary](https://developer.android.com/studio/preview/).
+2. Download the sample.
+3. Import the sample into Android Studio.
+4. Build and run the sample.
+
+
+## Screenshots
+
+
+
+
+
+## License
+
+```
+Copyright (C) 2020 The Android Open Source Project
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+```
diff --git a/MigrationCodelab/app/.gitignore b/MigrationCodelab/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/MigrationCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/MigrationCodelab/app/build.gradle b/MigrationCodelab/app/build.gradle
new file mode 100644
index 000000000..87ccbf5e4
--- /dev/null
+++ b/MigrationCodelab/app/build.gradle
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'androidx.navigation.safeargs.kotlin'
+apply plugin: 'org.jetbrains.kotlin.plugin.compose'
+apply plugin: 'com.android.legacy-kapt'
+
+
+android {
+ compileSdkVersion 37
+ namespace "com.google.samples.apps.sunflower"
+ defaultConfig {
+ applicationId "com.google.samples.apps.sunflower"
+ minSdkVersion 23
+ targetSdkVersion 33
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ versionCode 1
+ versionName "0.1.6"
+ vectorDrawables.useSupportLibrary true
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ buildFeatures {
+ dataBinding true
+ compose true
+ }
+ packagingOptions {
+ // Multiple dependency bring these files in. Exclude them to enable
+ // our test APK to build (has no effect on our AARs)
+ excludes += "/META-INF/AL2.0"
+ excludes += "/META-INF/LGPL2.1"
+ }
+}
+
+dependencies {
+ def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ kapt "androidx.room:room-compiler:2.8.4"
+ kapt "com.github.bumptech.glide:compiler:5.0.7"
+ kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10"
+ implementation "androidx.appcompat:appcompat:1.7.1"
+ implementation "androidx.constraintlayout:constraintlayout:2.2.1"
+ implementation "androidx.core:core-ktx:1.19.0"
+ implementation "androidx.fragment:fragment-ktx:1.8.9"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.11.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0"
+ implementation "androidx.navigation:navigation-fragment-ktx:2.9.8"
+ implementation "androidx.navigation:navigation-ui-ktx:2.9.8"
+ implementation "androidx.recyclerview:recyclerview:1.4.0"
+ implementation "androidx.room:room-runtime:2.8.4"
+ implementation "androidx.room:room-ktx:2.8.4"
+ implementation "androidx.tracing:tracing:1.3.0"
+ implementation "androidx.viewpager2:viewpager2:1.1.0"
+ implementation "androidx.work:work-runtime-ktx:2.11.2"
+ implementation "com.github.bumptech.glide:glide:5.0.7"
+ implementation "com.google.android.material:material:1.14.0"
+ implementation "com.google.code.gson:gson:2.14.0"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
+
+ // Compose
+ implementation "androidx.compose.runtime:runtime"
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.foundation:foundation"
+ implementation "androidx.compose.foundation:foundation-layout"
+ implementation "androidx.compose.material3:material3"
+ implementation "androidx.compose.runtime:runtime-livedata"
+ implementation "androidx.compose.ui:ui-tooling-preview"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+
+ // Testing dependencies
+ androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
+ androidTestImplementation "androidx.test.espresso:espresso-contrib:3.7.0"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0"
+ androidTestImplementation "androidx.test.espresso:espresso-intents:3.7.0"
+ androidTestImplementation "androidx.test.ext:junit:1.3.0"
+ androidTestImplementation "androidx.test.uiautomator:uiautomator:2.3.0"
+ androidTestImplementation "androidx.work:work-testing:2.11.2"
+ androidTestImplementation "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:4.1.1"
+ androidTestImplementation "com.google.truth:truth:1.4.5"
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+ testImplementation "junit:junit:4.13.2"
+}
diff --git a/MigrationCodelab/app/proguard-rules.pro b/MigrationCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..4965c37a4
--- /dev/null
+++ b/MigrationCodelab/app/proguard-rules.pro
@@ -0,0 +1,34 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/google/home/tiem/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# ServiceLoader support
+-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
+-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
+
+# Most of volatile fields are updated with AFU and should not be mangled
+-keepclassmembernames class kotlinx.** {
+ volatile ;
+}
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/GardenActivityTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/GardenActivityTest.kt
new file mode 100644
index 000000000..c2d6c413c
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/GardenActivityTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.rule.ActivityTestRule
+import org.junit.Rule
+import org.junit.Test
+
+class GardenActivityTest {
+
+ @Rule @JvmField
+ var activityTestRule = ActivityTestRule(GardenActivity::class.java)
+
+ @Test fun clickAddPlant_OpensPlantList() {
+ // Given that no Plants are added to the user's garden
+
+ // When the "Add Plant" button is clicked
+ onView(withId(R.id.add_plant)).perform(click())
+
+ // Then the ViewPager should change to the Plant List page
+ onView(withId(R.id.plant_list)).check(matches(isDisplayed()))
+ }
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/PlantDetailFragmentTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/PlantDetailFragmentTest.kt
new file mode 100644
index 000000000..ebeb6c58d
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/PlantDetailFragmentTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import android.accessibilityservice.AccessibilityService
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.navigation.Navigation.findNavController
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.testing.TestListenableWorkerBuilder
+import com.google.samples.apps.sunflower.utilities.chooser
+import com.google.samples.apps.sunflower.utilities.testPlant
+import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
+import kotlinx.coroutines.runBlocking
+import org.hamcrest.CoreMatchers.allOf
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlantDetailFragmentTest {
+
+ @Rule
+ @JvmField
+ val composeTestRule = createAndroidComposeRule()
+
+ // Note that keeping these references is only safe if the activity is not recreated.
+ private lateinit var activity: ComponentActivity
+
+ @Before
+ fun jumpToPlantDetailFragment() {
+ populateDatabase()
+
+ composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
+ activity = gardenActivity
+
+ val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
+ findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
+ }
+ }
+
+ @Test
+ fun testPlantName() {
+ composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
+ }
+
+ @Test
+ fun testShareTextIntent() {
+ val shareText = activity.getString(R.string.share_text_plant, testPlant.name)
+
+ Intents.init()
+ onView(withId(R.id.action_share)).perform(click())
+ intended(
+ chooser(
+ allOf(
+ hasAction(Intent.ACTION_SEND),
+ hasType("text/plain"),
+ hasExtra(Intent.EXTRA_TEXT, shareText)
+ )
+ )
+ )
+ Intents.release()
+
+ // dismiss the Share Dialog
+ InstrumentationRegistry.getInstrumentation()
+ .uiAutomation
+ .performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
+ }
+
+ // TODO: This workaround is needed due to the real database being used in tests.
+ // A fake database created with a Room.inMemoryDatabaseBuilder should be used instead.
+ // That's difficult to do in the current state of the project since there are no
+ // dependency injection best practices in place.
+ private fun populateDatabase() {
+ val request = TestListenableWorkerBuilder(
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ ).build()
+ runBlocking {
+ request.doWork()
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/GardenPlantingDaoTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/GardenPlantingDaoTest.kt
new file mode 100644
index 000000000..57e209d2f
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/GardenPlantingDaoTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.espresso.matcher.ViewMatchers.assertThat
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.samples.apps.sunflower.utilities.getValue
+import com.google.samples.apps.sunflower.utilities.testCalendar
+import com.google.samples.apps.sunflower.utilities.testGardenPlanting
+import com.google.samples.apps.sunflower.utilities.testPlant
+import com.google.samples.apps.sunflower.utilities.testPlants
+import kotlinx.coroutines.runBlocking
+import org.hamcrest.CoreMatchers.equalTo
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class GardenPlantingDaoTest {
+ private lateinit var database: AppDatabase
+ private lateinit var gardenPlantingDao: GardenPlantingDao
+ private var testGardenPlantingId: Long = 0
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before fun createDb() = runBlocking {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
+ gardenPlantingDao = database.gardenPlantingDao()
+
+ database.plantDao().insertAll(testPlants)
+ testGardenPlantingId = gardenPlantingDao.insertGardenPlanting(testGardenPlanting)
+ }
+
+ @After fun closeDb() {
+ database.close()
+ }
+
+ @Test fun testGetGardenPlantings() = runBlocking {
+ val gardenPlanting2 = GardenPlanting(
+ testPlants[1].plantId,
+ testCalendar,
+ testCalendar
+ ).also { it.gardenPlantingId = 2 }
+ gardenPlantingDao.insertGardenPlanting(gardenPlanting2)
+ assertThat(getValue(gardenPlantingDao.getGardenPlantings()).size, equalTo(2))
+ }
+
+ @Test fun testDeleteGardenPlanting() = runBlocking {
+ val gardenPlanting2 = GardenPlanting(
+ testPlants[1].plantId,
+ testCalendar,
+ testCalendar
+ ).also { it.gardenPlantingId = 2 }
+ gardenPlantingDao.insertGardenPlanting(gardenPlanting2)
+ assertThat(getValue(gardenPlantingDao.getGardenPlantings()).size, equalTo(2))
+ gardenPlantingDao.deleteGardenPlanting(gardenPlanting2)
+ assertThat(getValue(gardenPlantingDao.getGardenPlantings()).size, equalTo(1))
+ }
+
+ @Test fun testGetGardenPlantingForPlant() {
+ assertTrue(getValue(gardenPlantingDao.isPlanted(testPlant.plantId)))
+ }
+
+ @Test fun testGetGardenPlantingForPlant_notFound() {
+ assertFalse(getValue(gardenPlantingDao.isPlanted(testPlants[2].plantId)))
+ }
+
+ @Test fun testGetPlantAndGardenPlantings() {
+ val plantAndGardenPlantings = getValue(gardenPlantingDao.getPlantedGardens())
+ assertThat(plantAndGardenPlantings.size, equalTo(1))
+
+ /**
+ * Only the [testPlant] has been planted, and thus has an associated [GardenPlanting]
+ */
+ assertThat(plantAndGardenPlantings[0].plant, equalTo(testPlant))
+ assertThat(plantAndGardenPlantings[0].gardenPlantings.size, equalTo(1))
+ assertThat(plantAndGardenPlantings[0].gardenPlantings[0], equalTo(testGardenPlanting))
+ }
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/PlantDaoTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/PlantDaoTest.kt
new file mode 100644
index 000000000..1843049e2
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/data/PlantDaoTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.samples.apps.sunflower.utilities.getValue
+import kotlinx.coroutines.runBlocking
+import org.hamcrest.Matchers.equalTo
+import org.junit.After
+import org.junit.Assert.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PlantDaoTest {
+ private lateinit var database: AppDatabase
+ private lateinit var plantDao: PlantDao
+ private val plantA = Plant("1", "A", "", 1, 1, "")
+ private val plantB = Plant("2", "B", "", 1, 1, "")
+ private val plantC = Plant("3", "C", "", 2, 2, "")
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before fun createDb() = runBlocking {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
+ plantDao = database.plantDao()
+
+ // Insert plants in non-alphabetical order to test that results are sorted by name
+ plantDao.insertAll(listOf(plantB, plantC, plantA))
+ }
+
+ @After fun closeDb() {
+ database.close()
+ }
+
+ @Test fun testGetPlants() {
+ val plantList = getValue(plantDao.getPlants())
+ assertThat(plantList.size, equalTo(3))
+
+ // Ensure plant list is sorted by name
+ assertThat(plantList[0], equalTo(plantA))
+ assertThat(plantList[1], equalTo(plantB))
+ assertThat(plantList[2], equalTo(plantC))
+ }
+
+ @Test fun testGetPlantsWithGrowZoneNumber() {
+ val plantList = getValue(plantDao.getPlantsWithGrowZoneNumber(1))
+ assertThat(plantList.size, equalTo(2))
+ assertThat(getValue(plantDao.getPlantsWithGrowZoneNumber(2)).size, equalTo(1))
+ assertThat(getValue(plantDao.getPlantsWithGrowZoneNumber(3)).size, equalTo(0))
+
+ // Ensure plant list is sorted by name
+ assertThat(plantList[0], equalTo(plantA))
+ assertThat(plantList[1], equalTo(plantB))
+ }
+
+ @Test fun testGetPlant() {
+ assertThat(getValue(plantDao.getPlant(plantA.plantId)), equalTo(plantA))
+ }
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/TestUtils.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/TestUtils.kt
new file mode 100644
index 000000000..5b0037de2
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/utilities/TestUtils.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.utilities
+
+import android.app.Activity
+import android.content.Intent
+import androidx.appcompat.widget.Toolbar
+import androidx.lifecycle.LiveData
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
+import com.google.samples.apps.sunflower.data.GardenPlanting
+import com.google.samples.apps.sunflower.data.Plant
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.`is`
+import java.util.Calendar
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * [Plant] objects used for tests.
+ */
+val testPlants = arrayListOf(
+ Plant("1", "Apple", "A red fruit", 1),
+ Plant("2", "B", "Description B", 1),
+ Plant("3", "C", "Description C", 2)
+)
+val testPlant = testPlants[0]
+
+/**
+ * [Calendar] object used for tests.
+ */
+val testCalendar: Calendar = Calendar.getInstance().apply {
+ set(Calendar.YEAR, 1998)
+ set(Calendar.MONTH, Calendar.SEPTEMBER)
+ set(Calendar.DAY_OF_MONTH, 4)
+}
+
+/**
+ * [GardenPlanting] object used for tests.
+ */
+val testGardenPlanting = GardenPlanting(testPlant.plantId, testCalendar, testCalendar)
+
+/**
+ * Returns the content description for the navigation button view in the toolbar.
+ */
+fun getToolbarNavigationContentDescription(activity: Activity, toolbarId: Int) =
+ activity.findViewById(toolbarId).navigationContentDescription as String
+
+/**
+ * Simplify testing Intents with Chooser
+ *
+ * @param matcher the actual intent before wrapped by Chooser Intent
+ */
+fun chooser(matcher: Matcher): Matcher = allOf(
+ hasAction(Intent.ACTION_CHOOSER),
+ hasExtra(`is`(Intent.EXTRA_INTENT), matcher)
+)
+
+/**
+ * Helper method for testing LiveData objects, from
+ * https://github.com/googlesamples/android-architecture-components.
+ *
+ * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
+ * Once we got a notification via onChanged, we stop observing.
+ */
+@Throws(InterruptedException::class)
+fun getValue(liveData: LiveData): T {
+ val data = arrayOfNulls(1)
+ val latch = CountDownLatch(1)
+ liveData.observeForever { o ->
+ data[0] = o
+ latch.countDown()
+ }
+ latch.await(2, TimeUnit.SECONDS)
+
+ @Suppress("UNCHECKED_CAST")
+ return data[0] as T
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelTest.kt
new file mode 100644
index 000000000..fd9062efa
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.samples.apps.sunflower.data.AppDatabase
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+import com.google.samples.apps.sunflower.data.PlantRepository
+import com.google.samples.apps.sunflower.utilities.getValue
+import com.google.samples.apps.sunflower.utilities.testPlant
+import kotlinx.coroutines.Dispatchers
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class PlantDetailViewModelTest {
+
+ private lateinit var appDatabase: AppDatabase
+ private lateinit var viewModel: PlantDetailViewModel
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ appDatabase = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
+
+ val plantRepo = PlantRepository.getInstance(appDatabase.plantDao())
+ val gardenPlantingRepo = GardenPlantingRepository.getInstance(
+ appDatabase.gardenPlantingDao(),
+ Dispatchers.IO
+ )
+ viewModel = PlantDetailViewModel(plantRepo, gardenPlantingRepo, testPlant.plantId)
+ }
+
+ @After
+ fun tearDown() {
+ appDatabase.close()
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun testDefaultValues() {
+ assertFalse(getValue(viewModel.isPlanted))
+ }
+}
diff --git a/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/worker/RefreshMainDataWorkTest.kt b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/worker/RefreshMainDataWorkTest.kt
new file mode 100644
index 000000000..fe527030a
--- /dev/null
+++ b/MigrationCodelab/app/src/androidTest/java/com/google/samples/apps/sunflower/worker/RefreshMainDataWorkTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.worker
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.work.ListenableWorker.Result
+import androidx.work.WorkManager
+import androidx.work.testing.TestListenableWorkerBuilder
+import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
+import org.hamcrest.CoreMatchers.`is`
+import org.junit.Assert.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class RefreshMainDataWorkTest {
+ private lateinit var context: Context
+ private lateinit var workManager: WorkManager
+
+ @Before
+ fun setup() {
+ context = ApplicationProvider.getApplicationContext()
+ workManager = WorkManager.getInstance(context)
+ }
+
+ @Test
+ fun testRefreshMainDataWork() {
+ // Get the ListenableWorker
+ val worker = TestListenableWorkerBuilder(context).build()
+
+ // Start the work synchronously
+ val result = worker.startWork().get()
+
+ assertThat(result, `is`(Result.success()))
+ }
+}
diff --git a/MigrationCodelab/app/src/main/AndroidManifest.xml b/MigrationCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9ae69cc8a
--- /dev/null
+++ b/MigrationCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/assets/plants.json b/MigrationCodelab/app/src/main/assets/plants.json
new file mode 100644
index 000000000..764790472
--- /dev/null
+++ b/MigrationCodelab/app/src/main/assets/plants.json
@@ -0,0 +1,138 @@
+[
+ {
+ "plantId": "malus-pumila",
+ "name": "Apple",
+ "description": "An apple is a sweet, edible fruit produced by an apple tree (Malus pumila). Apple trees are cultivated worldwide, and are the most widely grown species in the genus Malus. The tree originated in Central Asia, where its wild ancestor, Malus sieversii, is still found today. Apples have been grown for thousands of years in Asia and Europe, and were brought to North America by European colonists. Apples have religious and mythological significance in many cultures, including Norse, Greek and European Christian traditions.
Apple trees are large if grown from seed. Generally apple cultivars are propagated by grafting onto rootstocks, which control the size of the resulting tree. There are more than 7,500 known cultivars of apples, resulting in a range of desired characteristics. Different cultivars are bred for various tastes and uses, including cooking, eating raw and cider production. Trees and fruit are prone to a number of fungal, bacterial and pest problems, which can be controlled by a number of organic and non-organic means. In 2010, the fruit's genome was sequenced as part of research on disease control and selective breeding in apple production.
Worldwide production of apples in 2014 was 84.6 million tonnes, with China accounting for 48% of the total.
(From Wikipedia)",
+ "growZoneNumber": 3,
+ "wateringInterval": 30,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/5/55/Apple_orchard_in_Tasmania.jpg"
+ },
+ {
+ "plantId": "beta-vulgaris",
+ "name": "Beet",
+ "description": "The beetroot is the taproot portion of the beet plant, usually known in North America as the beet and also known as the table beet, garden beet, red beet, or golden beet. It is one of several of the cultivated varieties of Beta vulgaris grown for their edible taproots and their leaves (called beet greens). These varieties have been classified as B. vulgaris subsp. vulgaris Conditiva Group.
Other than as a food, beets have use as a food colouring and as a medicinal plant. Many beet products are made from other Beta vulgaris varieties, particularly sugar beet.
(From Wikipedia)",
+ "growZoneNumber": 2,
+ "wateringInterval": 7,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/2/29/Beetroot_jm26647.jpg"
+ },
+ {
+ "plantId": "coriandrum-sativum",
+ "name": "Cilantro",
+ "description": "Coriander, also known as cilantro or Chinese parsley, is an annual herb in the family Apiaceae. All parts of the plant are edible, but the fresh leaves and the dried seeds are the parts most traditionally used in cooking.
(From Wikipedia)",
+ "growZoneNumber": 2,
+ "wateringInterval": 2,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/5/51/A_scene_of_Coriander_leaves.JPG"
+ },
+ {
+ "plantId": "solanum-lycopersicum",
+ "name": "Tomato",
+ "description": "The tomato is the edible, often red, berry of the nightshade Solanum lycopersicum, commonly known as a tomato plant. The species originated in western South America. The Nahuatl (Aztec language) word tomatl gave rise to the Spanish word tomate, from which the English word tomato derived. Its use as a cultivated food may have originated with the indigenous peoples of Mexico. The Spanish encountered the tomato from their contact with the Aztec during the Spanish colonization of the Americas and brought it to Europe. From there, the tomato was introduced to other parts of the European-colonized world during the 16th century.
The tomato is consumed in diverse ways, raw or cooked, in many dishes, sauces, salads, and drinks. While tomatoes are fruits – botanically classified as berries – they are commonly used as a vegetable ingredient or side dish.
Numerous varieties of the tomato plant are widely grown in temperate climates across the world, with greenhouses allowing for the production of tomatoes throughout all seasons of the year. Tomato plants typically grow to 1–3 meters (3–10 ft) in height. They are vines that have a weak stem that sprawls and typically needs support. Indeterminate tomato plants are perennials in their native habitat, but are cultivated as annuals. Determinate, or bush, plants are annuals that stop growing at a certain height and produce a crop all at once. The size of the tomato varies according to the cultivar, with a range of 0.5–4 inches (1.3–10.2 cm) in width.
(From Wikipedia)",
+ "growZoneNumber": 9,
+ "wateringInterval": 4,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/1/17/Cherry_tomatoes_red_and_green_2009_16x9.jpg"
+ },
+ {
+ "plantId": "persea-americana",
+ "name": "Avocado",
+ "description": "The avocado (Persea americana) is a tree, long thought to have originated in South Central Mexico, classified as a member of the flowering plant family Lauraceae. The fruit of the plant, also called an avocado (or avocado pear or alligator pear), is botanically a large berry containing a single large seed.
Avocados are commercially valuable and are cultivated in tropical and Mediterranean climates throughout the world. They have a green-skinned, fleshy body that may be pear-shaped, egg-shaped, or spherical. Commercially, they ripen after harvesting. Avocado trees are partially self-pollinating and are often propagated through grafting to maintain a predictable quality and quantity of the fruit.
(From Wikipedia)",
+ "growZoneNumber": 9,
+ "wateringInterval": 3,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/e/e4/Branch_and_fruit_of_the_Maluma_avocado_cultivar.jpg"
+ },
+ {
+ "plantId": "pyrus-communis",
+ "name": "Pear",
+ "description": "The pear tree and shrub are a species of genus Pyrus, in the family Rosaceae, bearing the pomaceous fruit of the same name. Several species of pear are valued for their edible fruit and juices while others are cultivated as trees.
(From Wikipedia)",
+ "growZoneNumber": 3,
+ "wateringInterval": 30,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/1/13/More_pears.jpg"
+ },
+ {
+ "plantId": "solanum-melongena",
+ "name": "Eggplant",
+ "description": "Eggplant (US), aubergine (UK), or brinjal (South Asia and South Africa) is a plant species in the nightshade family Solanaceae, Solanum melongena, grown for its often purple edible fruit.
The spongy, absorbent fruit of the plant is widely used in cooking in many different cuisines, and is often considered a vegetable, even though it is a berry by botanical definition. As a member of the genus Solanum, it is related to the tomato and the potato. Like the tomato, its skin and seeds can be eaten, but, like the potato, it is not advisable to eat it raw. Eggplant supplies low contents of macronutrients and micronutrients. The capability of the fruit to absorb oils and flavors into its flesh through cooking is well known in the culinary arts.
It was originally domesticated from the wild nightshade species thorn or bitter apple, S. incanum, probably with two independent domestications: one in South Asia, and one in East Asia.
(From Wikipedia)",
+ "growZoneNumber": 4,
+ "wateringInterval": 3,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/e/e5/Eggplant_display.JPG"
+ },
+ {
+ "plantId": "vitis-vinifera",
+ "name": "Grape",
+ "description": "A grape is a fruit, botanically a berry, of the deciduous woody vines of the flowering plant genus Vitis.
Grapes can be eaten fresh as table grapes or they can be used for making wine, jam, juice, jelly, grape seed extract, raisins, vinegar, and grape seed oil. Grapes are a non-climacteric type of fruit, generally occurring in clusters.
(From Wikipedia)",
+ "growZoneNumber": 9,
+ "wateringInterval": 9,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/0/03/Grape_Plant_and_grapes9.jpg"
+ },
+ {
+ "plantId": "mangifera-indica",
+ "name": "Mango",
+ "description": "Mangoes are juicy stone fruit (drupe) from numerous species of tropical trees belonging to the flowering plant genus Mangifera, cultivated mostly for their edible fruit.
The majority of these species are found in nature as wild mangoes. The genus belongs to the cashew family Anacardiaceae. Mangoes are native to South Asia, from where the 'common mango' or 'Indian mango', Mangifera indica, has been distributed worldwide to become one of the most widely cultivated fruits in the tropics. Other Mangifera species (e.g. horse mango, Mangifera foetida) are grown on a more localized basis.
It is the national fruit of India, Pakistan, and the Philippines, and the national tree of Bangladesh.
(From Wikipedia)",
+ "growZoneNumber": 11,
+ "wateringInterval": 7,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/6/67/Mangos_criollos_y_pera.JPG"
+ },
+ {
+ "plantId": "citrus-x-sinensis",
+ "name": "Orange",
+ "description": "The orange is the fruit of the citrus species Citrus × sinensis in the family Rutaceae. It is also called sweet orange, to distinguish it from the related Citrus × aurantium, referred to as bitter orange. The sweet orange reproduces asexually (apomixis through nucellar embryony); varieties of sweet orange arise through mutations.
The orange is a hybrid between pomelo (Citrus maxima) and mandarin (Citrus reticulata). The chloroplast genome, and therefore the maternal line, is that of pomelo. The sweet orange has had its full genome sequenced.
Sweet oranges were mentioned in Chinese literature in 314 BC. As of 1987, orange trees were found to be the most cultivated fruit tree in the world. Orange trees are widely grown in tropical and subtropical climates for their sweet fruit. The fruit of the orange tree can be eaten fresh, or processed for its juice or fragrant peel. As of 2012, sweet oranges accounted for approximately 70% of citrus production.
In 2014, 70.9 million tonnes of oranges were grown worldwide, with Brazil producing 24% of the world total followed by China and India.
(From Wikipedia)",
+ "growZoneNumber": 9,
+ "wateringInterval": 30,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/2/22/Apfelsinenbaum--Orange_tree.jpg"
+ },
+ {
+ "plantId": "helianthus-annuus",
+ "name": "Sunflower",
+ "description": "Roses are red
Violets are blue
Sunflowers have seeds
That folks love to chew
- M.G., 2018
Helianthus annuus, the common sunflower, is a large annual forb of the genus Helianthus grown as a crop for its edible oil and edible fruits. This sunflower species is also used as wild bird food, as livestock forage (as a meal or a silage plant), in some industrial applications, and as an ornamental in domestic gardens. The plant was first domesticated in the Americas. Wild Helianthus annuus is a widely branched annual plant with many flower heads. The domestic sunflower, however, often possesses only a single large inflorescence (flower head) atop an unbranched stem. The name sunflower may derive from the flower's head's shape, which resembles the sun, or from the impression that the blooming plant appears to slowly turn its flower towards the sun as the latter moves across the sky on a daily basis.
Sunflower seeds were brought to Europe from the Americas in the 16th century, where, along with sunflower oil, they became a widespread cooking ingredient.
(From Wikipedia)",
+ "growZoneNumber": 8,
+ "wateringInterval": 3,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/a/aa/Sunflowers_in_field_flower.jpg"
+ },
+ {
+ "plantId": "citrullus-lanatus",
+ "name": "Watermelon",
+ "description": "Citrullus lanatus is a plant species in the family Cucurbitaceae, a vine-like (scrambler and trailer) flowering plant originating in West Africa. It is cultivated for its fruit. The subdivision of this species into two varieties, watermelons (Citrullus lanatus (Thunb.) var. lanatus) and citron melons (Citrullus lanatus var. citroides (L. H. Bailey) Mansf.), originated with the erroneous synonymization of Citrullus lanatus (Thunb.) Matsum. & Nakai and Citrullus vulgaris Schrad. by L.H. Bailey in 1930. Molecular data including sequences from the original collection of Thunberg and other relevant type material, show that the sweet watermelon (Citrullus vulgaris Schrad.) and the bitter wooly melon Citrullus lanatus (Thunb.) Matsum. & Nakai are not closely related to each other. Since 1930, thousands of papers have misapplied the name Citrullus lanatus (Thunb.) Matsum. & Nakai for the watermelon, and a proposal to conserve the name with this meaning was accepted by the relevant nomenclatural committee and confirmed at the International Botanical Congress in Shenzhen.
The bitter South African melon first collected by Thunberg has become naturalized in semiarid regions of several continents, and is designated as a 'pest plant' in parts of Western Australia where they are called pig melon.
Watermelon (Citrullus lanatus) is a scrambling and trailing vine in the flowering plant family Cucurbitaceae. The species was long thought to have originated in southern Africa, but this was based on the erroneous synonymization by L. H. Bailey (1930) of a South African species with the cultivated watermelon. The error became apparent with DNA comparison of material of the cultivated watermelon seen and named by Linnaeus and the holotype of the South African species. There is evidence from seeds in Pharao tombs of watermelon cultivation in Ancient Egypt. Watermelon is grown in tropical and subtropical areas worldwide for its large edible fruit, also known as a watermelon, which is a special kind of berry with a hard rind and no internal division, botanically called a pepo. The sweet, juicy flesh is usually deep red to pink, with many black seeds, although seedless varieties have been cultivated. The fruit can be eaten raw or pickled and the rind is edible after cooking.
Considerable breeding effort has been put into disease-resistant varieties. Many cultivars are available that produce mature fruit within 100 days of planting the crop.
(From Wikipedia)",
+ "growZoneNumber": 7,
+ "wateringInterval": 3,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/f/fc/01266jfWatermelons_Philippines_textures_Apolonio_Samson_Market_Quezon_Cityfvf_02.jpg"
+ },
+ {
+ "plantId": "hibiscus-rosa-sinensis",
+ "name": "Hibiscus",
+ "description": "Hibiscus is a genus of flowering plants in the mallow family, Malvaceae. The genus is quite large, comprising several hundred species that are native to warm temperate, subtropical and tropical regions throughout the world. Member species are renowned for their large, showy flowers and those species are commonly known simply as 'hibiscus', or less widely known as rose mallow. There are also names for hibiscus such as hardy hibiscus, rose of sharon, and tropical hibiscus.
The genus includes both annual and perennial herbaceous plants, as well as woody shrubs and small trees. The generic name is derived from the Greek name ἰβίσκος (hibiskos) which Pedanius Dioscorides gave to Althaea officinalis (c. 40–90 AD).
Several species are widely cultivated as ornamental plants, notably Hibiscus syriacus and Hibiscus rosa-sinensis.
A tea made from hibiscus flowers is known by many names around the world and is served both hot and cold. The beverage is known for its red colour, tart flavour, and vitamin C content.
(From Wikipedia)",
+ "growZoneNumber": 10,
+ "wateringInterval": 1,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/8/82/Hibiscus_rosa-sinensis_flower_2.JPG"
+ },
+ {
+ "plantId": "cypripedium-reginae",
+ "name": "Pink & White Lady's Slipper",
+ "description": "Cypripedium reginae, known as the showy lady's slipper, pink-and-white lady's-slipper, or the queen's lady's-slipper, is a rare terrestrial lady's-slipper orchid native to northern North America.
It is the state flower of Minnesota, United States, and the provincial flower of Prince Edward Island, Canada.
Despite producing a large amount of seeds per seed pod, it reproduces largely by vegetative reproduction, and remains restricted to the North East region of the United States and south east regions of Canada. Although never common, this rare plant has vanished from much of its historical range due to habitat loss. It has been a subject of horticultural interest for many years with Charles Darwin who, like many, was unsuccessful in cultivating the plant.
(From Wikipedia)",
+ "growZoneNumber": 4,
+ "wateringInterval": 7,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/a/ab/Cypripedium_reginae_Orchi_004.jpg"
+ },
+ {
+ "plantId": "aquilegia-coerulea",
+ "name": "Rocky Mountain Columbine",
+ "description": "Aquilegia coerulea, the state flower of Colorado, is a species of flowering plant in the buttercup family Ranunculaceae, native to the Rocky Mountains from Montana south to New Mexico and west to Idaho and Arizona. Its common name is Colorado blue columbine; sometimes it is called \"Rocky Mountain columbine,\" but this also refers to Aquilegia saximontana.
(From Wikipedia)",
+ "growZoneNumber": 5,
+ "wateringInterval": 3,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/9/94/Aquilegia_caerulea.jpg"
+ },
+ {
+ "plantId": "magnolia-denudata",
+ "name": "Yulan Magnolia",
+ "description": "Magnolia denudata, known as the lilytree or Yulan magnolia (simplified Chinese: 玉兰花; traditional Chinese: 玉蘭花), is native to central and eastern China. It has been cultivated in Chinese Buddhist temple gardens since 600 AD. Its flowers were regarded as a symbol of purity in the Tang Dynasty and it was planted in the grounds of the Emperor's palace.
It is the official city flower of Shanghai.
(From Wikipedia)",
+ "growZoneNumber": 8,
+ "wateringInterval": 7,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/1/13/Yulan_magnolia_%28Magnolia_denudata%29_%2816953983745%29.jpg"
+ },
+ {
+ "plantId": "bougainvillea-glabra",
+ "name": "Bougainvillea",
+ "description": "Bougainvillea is a genus of thorny ornamental vines, bushes, or trees. The inflorescence consists of large colourful sepallike bracts which surround three simple waxy flowers. The vine species grow anywhere from 1 to 12 m (3 to 40 ft.) tall, scrambling over other plants with their spiky thorns, which are tipped with a black, waxy substance. They are evergreen where rainfall occurs all year, or deciduous if there is a dry season.
Bougainvillea glabra (simplified Chinese: 簕杜鹃; traditional Chinese: 簕杜鵑) is the official city flower of Shenzhen and many other cities around the world.
(From Wikipedia)",
+ "growZoneNumber": 10,
+ "wateringInterval": 21,
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/6/6d/Paperflower_--_Bougainvillea_glabra.jpg"
+ }
+]
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenActivity.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenActivity.kt
new file mode 100644
index 000000000..37fa29a5a
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenActivity.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.databinding.DataBindingUtil.setContentView
+import com.google.samples.apps.sunflower.databinding.ActivityGardenBinding
+
+class GardenActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Displaying edge-to-edge
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ setContentView(this, R.layout.activity_garden)
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenFragment.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenFragment.kt
new file mode 100644
index 000000000..2b1197016
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/GardenFragment.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.viewpager2.widget.ViewPager2
+import com.google.samples.apps.sunflower.adapters.GardenPlantingAdapter
+import com.google.samples.apps.sunflower.adapters.PLANT_LIST_PAGE_INDEX
+import com.google.samples.apps.sunflower.databinding.FragmentGardenBinding
+import com.google.samples.apps.sunflower.utilities.InjectorUtils
+import com.google.samples.apps.sunflower.viewmodels.GardenPlantingListViewModel
+
+class GardenFragment : Fragment() {
+
+ private lateinit var binding: FragmentGardenBinding
+
+ private val viewModel: GardenPlantingListViewModel by viewModels {
+ InjectorUtils.provideGardenPlantingListViewModelFactory(requireContext())
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = FragmentGardenBinding.inflate(inflater, container, false)
+ val adapter = GardenPlantingAdapter()
+ binding.gardenList.adapter = adapter
+
+ binding.addPlant.setOnClickListener {
+ navigateToPlantListPage()
+ }
+
+ subscribeUi(adapter, binding)
+ return binding.root
+ }
+
+ private fun subscribeUi(adapter: GardenPlantingAdapter, binding: FragmentGardenBinding) {
+ viewModel.plantAndGardenPlantings.observe(viewLifecycleOwner) { result ->
+ binding.hasPlantings = !result.isNullOrEmpty()
+ adapter.submitList(result)
+ }
+ }
+
+ // TODO: convert to data binding if applicable
+ private fun navigateToPlantListPage() {
+ requireActivity().findViewById(R.id.view_pager).currentItem =
+ PLANT_LIST_PAGE_INDEX
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/HomeViewPagerFragment.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/HomeViewPagerFragment.kt
new file mode 100644
index 000000000..76c0c1cb6
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/HomeViewPagerFragment.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import com.google.android.material.tabs.TabLayoutMediator
+import com.google.samples.apps.sunflower.adapters.MY_GARDEN_PAGE_INDEX
+import com.google.samples.apps.sunflower.adapters.PLANT_LIST_PAGE_INDEX
+import com.google.samples.apps.sunflower.adapters.SunflowerPagerAdapter
+import com.google.samples.apps.sunflower.databinding.FragmentViewPagerBinding
+
+class HomeViewPagerFragment : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val binding = FragmentViewPagerBinding.inflate(inflater, container, false)
+ val tabLayout = binding.tabs
+ val viewPager = binding.viewPager
+
+ viewPager.adapter = SunflowerPagerAdapter(this)
+
+ // Set the icon and text for each tab
+ TabLayoutMediator(tabLayout, viewPager) { tab, position ->
+ tab.setIcon(getTabIcon(position))
+ tab.text = getTabTitle(position)
+ }.attach()
+
+ (activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
+
+ return binding.root
+ }
+
+ private fun getTabIcon(position: Int): Int {
+ return when (position) {
+ MY_GARDEN_PAGE_INDEX -> R.drawable.garden_tab_selector
+ PLANT_LIST_PAGE_INDEX -> R.drawable.plant_list_tab_selector
+ else -> throw IndexOutOfBoundsException()
+ }
+ }
+
+ private fun getTabTitle(position: Int): String? {
+ return when (position) {
+ MY_GARDEN_PAGE_INDEX -> getString(R.string.my_garden_title)
+ PLANT_LIST_PAGE_INDEX -> getString(R.string.plant_list_title)
+ else -> null
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt
new file mode 100644
index 000000000..89b9adb42
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/PlantListFragment.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import com.google.samples.apps.sunflower.adapters.PlantAdapter
+import com.google.samples.apps.sunflower.databinding.FragmentPlantListBinding
+import com.google.samples.apps.sunflower.utilities.InjectorUtils
+import com.google.samples.apps.sunflower.viewmodels.PlantListViewModel
+
+class PlantListFragment : Fragment() {
+
+ private val viewModel: PlantListViewModel by viewModels {
+ InjectorUtils.providePlantListViewModelFactory(this)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val binding = FragmentPlantListBinding.inflate(inflater, container, false)
+ context ?: return binding.root
+
+ val adapter = PlantAdapter()
+ binding.plantList.adapter = adapter
+ subscribeUi(adapter)
+
+ setHasOptionsMenu(true)
+ return binding.root
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.menu_plant_list, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.filter_zone -> {
+ updateData()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun subscribeUi(adapter: PlantAdapter) {
+ viewModel.plants.observe(viewLifecycleOwner) { plants ->
+ adapter.submitList(plants)
+ }
+ }
+
+ private fun updateData() {
+ with(viewModel) {
+ if (isFiltered()) {
+ clearGrowZoneNumber()
+ } else {
+ setGrowZoneNumber(9)
+ }
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/BindIsGone.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/BindIsGone.kt
new file mode 100644
index 000000000..7cc6703dd
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/BindIsGone.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.adapters
+
+import android.view.View
+import androidx.databinding.BindingAdapter
+
+@BindingAdapter("isGone")
+fun bindIsGone(view: View, isGone: Boolean) {
+ view.visibility = if (isGone) {
+ View.GONE
+ } else {
+ View.VISIBLE
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/GardenPlantingAdapter.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/GardenPlantingAdapter.kt
new file mode 100644
index 000000000..e697df8d4
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/GardenPlantingAdapter.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.google.samples.apps.sunflower.HomeViewPagerFragmentDirections
+import com.google.samples.apps.sunflower.R
+import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings
+import com.google.samples.apps.sunflower.databinding.ListItemGardenPlantingBinding
+import com.google.samples.apps.sunflower.viewmodels.PlantAndGardenPlantingsViewModel
+
+class GardenPlantingAdapter :
+ ListAdapter(
+ GardenPlantDiffCallback()
+ ) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.list_item_garden_planting,
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ class ViewHolder(
+ private val binding: ListItemGardenPlantingBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ init {
+ binding.setClickListener { view ->
+ binding.viewModel?.plantId?.let { plantId ->
+ navigateToPlant(plantId, view)
+ }
+ }
+ }
+
+ private fun navigateToPlant(plantId: String, view: View) {
+ val direction = HomeViewPagerFragmentDirections
+ .actionViewPagerFragmentToPlantDetailFragment(plantId)
+ view.findNavController().navigate(direction)
+ }
+
+ fun bind(plantings: PlantAndGardenPlantings) {
+ with(binding) {
+ viewModel = PlantAndGardenPlantingsViewModel(plantings)
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class GardenPlantDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(
+ oldItem: PlantAndGardenPlantings,
+ newItem: PlantAndGardenPlantings
+ ): Boolean {
+ return oldItem.plant.plantId == newItem.plant.plantId
+ }
+
+ override fun areContentsTheSame(
+ oldItem: PlantAndGardenPlantings,
+ newItem: PlantAndGardenPlantings
+ ): Boolean {
+ return oldItem.plant == newItem.plant
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt
new file mode 100644
index 000000000..76104880b
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantAdapter.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.google.samples.apps.sunflower.HomeViewPagerFragmentDirections
+import com.google.samples.apps.sunflower.PlantListFragment
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.databinding.ListItemPlantBinding
+
+/**
+ * Adapter for the [RecyclerView] in [PlantListFragment].
+ */
+class PlantAdapter : ListAdapter(PlantDiffCallback()) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return PlantViewHolder(
+ ListItemPlantBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val plant = getItem(position)
+ (holder as PlantViewHolder).bind(plant)
+ }
+
+ class PlantViewHolder(
+ private val binding: ListItemPlantBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ init {
+ binding.setClickListener {
+ binding.plant?.let { plant ->
+ navigateToPlant(plant, it)
+ }
+ }
+ }
+
+ private fun navigateToPlant(
+ plant: Plant,
+ view: View
+ ) {
+ val direction =
+ HomeViewPagerFragmentDirections.actionViewPagerFragmentToPlantDetailFragment(
+ plant.plantId
+ )
+ view.findNavController().navigate(direction)
+ }
+
+ fun bind(item: Plant) {
+ binding.apply {
+ plant = item
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class PlantDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: Plant, newItem: Plant): Boolean {
+ return oldItem.plantId == newItem.plantId
+ }
+
+ override fun areContentsTheSame(oldItem: Plant, newItem: Plant): Boolean {
+ return oldItem == newItem
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt
new file mode 100644
index 000000000..e6b5c542f
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/PlantDetailBindingAdapters.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.adapters
+
+import android.text.method.LinkMovementMethod
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.text.HtmlCompat
+import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT
+import androidx.databinding.BindingAdapter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.samples.apps.sunflower.R
+
+@BindingAdapter("imageFromUrl")
+fun bindImageFromUrl(view: ImageView, imageUrl: String?) {
+ if (!imageUrl.isNullOrEmpty()) {
+ Glide.with(view.context)
+ .load(imageUrl)
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(view)
+ }
+}
+
+@BindingAdapter("isGone")
+fun bindIsGone(view: FloatingActionButton, isGone: Boolean?) {
+ if (isGone == null || isGone) {
+ view.hide()
+ } else {
+ view.show()
+ }
+}
+
+@BindingAdapter("renderHtml")
+fun bindRenderHtml(view: TextView, description: String?) {
+ if (description != null) {
+ view.text = HtmlCompat.fromHtml(description, FROM_HTML_MODE_COMPACT)
+ view.movementMethod = LinkMovementMethod.getInstance()
+ } else {
+ view.text = ""
+ }
+}
+
+@BindingAdapter("wateringText")
+fun bindWateringText(textView: TextView, wateringInterval: Int) {
+ val resources = textView.context.resources
+ val quantityString = resources.getQuantityString(
+ R.plurals.watering_needs_suffix,
+ wateringInterval,
+ wateringInterval
+ )
+
+ textView.text = quantityString
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/SunflowerPagerAdapter.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/SunflowerPagerAdapter.kt
new file mode 100644
index 000000000..ebbea27ce
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/adapters/SunflowerPagerAdapter.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.adapters
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import com.google.samples.apps.sunflower.GardenFragment
+import com.google.samples.apps.sunflower.PlantListFragment
+
+const val MY_GARDEN_PAGE_INDEX = 0
+const val PLANT_LIST_PAGE_INDEX = 1
+
+class SunflowerPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
+
+ /**
+ * Mapping of the ViewPager page indexes to their respective Fragments
+ */
+ private val tabFragmentsCreators: Map Fragment> = mapOf(
+ MY_GARDEN_PAGE_INDEX to { GardenFragment() },
+ PLANT_LIST_PAGE_INDEX to { PlantListFragment() }
+ )
+
+ override fun getItemCount() = tabFragmentsCreators.size
+
+ override fun createFragment(position: Int): Fragment {
+ return tabFragmentsCreators[position]?.invoke() ?: throw IndexOutOfBoundsException()
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/AppDatabase.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/AppDatabase.kt
new file mode 100644
index 000000000..f3a517864
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/AppDatabase.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import com.google.samples.apps.sunflower.utilities.DATABASE_NAME
+import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
+
+/**
+ * The Room database for this app
+ */
+@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
+@TypeConverters(Converters::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun gardenPlantingDao(): GardenPlantingDao
+ abstract fun plantDao(): PlantDao
+
+ companion object {
+
+ // For Singleton instantiation
+ @Volatile private var instance: AppDatabase? = null
+
+ fun getInstance(context: Context): AppDatabase {
+ return instance ?: synchronized(this) {
+ instance ?: buildDatabase(context).also { instance = it }
+ }
+ }
+
+ // Create and pre-populate the database. See this article for more details:
+ // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
+ private fun buildDatabase(context: Context): AppDatabase {
+ return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
+ .addCallback(object : RoomDatabase.Callback() {
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ super.onCreate(db)
+ val request = OneTimeWorkRequestBuilder().build()
+ WorkManager.getInstance(context).enqueue(request)
+ }
+ })
+ .build()
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Converters.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Converters.kt
new file mode 100644
index 000000000..c57d2fe62
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Converters.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.room.TypeConverter
+import java.util.Calendar
+
+/**
+ * Type converters to allow Room to reference complex data types.
+ */
+class Converters {
+ @TypeConverter fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis
+
+ @TypeConverter fun datestampToCalendar(value: Long): Calendar =
+ Calendar.getInstance().apply { timeInMillis = value }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlanting.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlanting.kt
new file mode 100644
index 000000000..c133a9fd5
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlanting.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import java.util.Calendar
+
+/**
+ * [GardenPlanting] represents when a user adds a [Plant] to their garden, with useful metadata.
+ * Properties such as [lastWateringDate] are used for notifications (such as when to water the
+ * plant).
+ *
+ * Declaring the column info allows for the renaming of variables without implementing a
+ * database migration, as the column name would not change.
+ */
+@Entity(
+ tableName = "garden_plantings",
+ foreignKeys = [
+ ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
+ ],
+ indices = [Index("plant_id")]
+)
+data class GardenPlanting(
+ @ColumnInfo(name = "plant_id") val plantId: String,
+
+ /**
+ * Indicates when the [Plant] was planted. Used for showing notification when it's time
+ * to harvest the plant.
+ */
+ @ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),
+
+ /**
+ * Indicates when the [Plant] was last watered. Used for showing notification when it's
+ * time to water the plant.
+ */
+ @ColumnInfo(name = "last_watering_date")
+ val lastWateringDate: Calendar = Calendar.getInstance()
+) {
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var gardenPlantingId: Long = 0
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingDao.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingDao.kt
new file mode 100644
index 000000000..de873f228
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingDao.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+
+/**
+ * The Data Access Object for the [GardenPlanting] class.
+ */
+@Dao
+interface GardenPlantingDao {
+ @Query("SELECT * FROM garden_plantings")
+ fun getGardenPlantings(): LiveData>
+
+ @Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
+ fun isPlanted(plantId: String): LiveData
+
+ /**
+ * This query will tell Room to query both the [Plant] and [GardenPlanting] tables and handle
+ * the object mapping.
+ */
+ @Transaction
+ @Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
+ fun getPlantedGardens(): LiveData>
+
+ @Insert
+ fun insertGardenPlanting(gardenPlanting: GardenPlanting): Long
+
+ @Delete
+ fun deleteGardenPlanting(gardenPlanting: GardenPlanting)
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingRepository.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingRepository.kt
new file mode 100644
index 000000000..6e9f1efa5
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/GardenPlantingRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class GardenPlantingRepository private constructor(
+ private val gardenPlantingDao: GardenPlantingDao,
+ private val ioDispatcher: CoroutineDispatcher
+) {
+
+ suspend fun createGardenPlanting(plantId: String) {
+ withContext(ioDispatcher) {
+ val gardenPlanting = GardenPlanting(plantId)
+ gardenPlantingDao.insertGardenPlanting(gardenPlanting)
+ }
+ }
+
+ suspend fun removeGardenPlanting(gardenPlanting: GardenPlanting) {
+ gardenPlantingDao.deleteGardenPlanting(gardenPlanting)
+ }
+
+ fun isPlanted(plantId: String) =
+ gardenPlantingDao.isPlanted(plantId)
+
+ fun getPlantedGardens() = gardenPlantingDao.getPlantedGardens()
+
+ companion object {
+
+ // For Singleton instantiation
+ @Volatile private var instance: GardenPlantingRepository? = null
+
+ fun getInstance(gardenPlantingDao: GardenPlantingDao, ioDispatcher: CoroutineDispatcher) =
+ instance ?: synchronized(this) {
+ instance ?: GardenPlantingRepository(gardenPlantingDao, ioDispatcher).also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Plant.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Plant.kt
new file mode 100644
index 000000000..5dc29c02d
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/Plant.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.util.Calendar
+import java.util.Calendar.DAY_OF_YEAR
+
+@Entity(tableName = "plants")
+data class Plant(
+ @PrimaryKey @ColumnInfo(name = "id")
+ val plantId: String,
+ val name: String,
+ val description: String,
+ val growZoneNumber: Int,
+ val wateringInterval: Int = 7, // how often the plant should be watered, in days
+ val imageUrl: String = ""
+) {
+
+ /**
+ * Determines if the plant should be watered. Returns true if [since]'s date > date of last
+ * watering + watering Interval; false otherwise.
+ */
+ fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) =
+ since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }
+
+ override fun toString() = name
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantAndGardenPlantings.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantAndGardenPlantings.kt
new file mode 100644
index 000000000..9e7497640
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantAndGardenPlantings.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+/**
+ * This class captures the relationship between a [Plant] and a user's [GardenPlanting], which is
+ * used by Room to fetch the related entities.
+ */
+data class PlantAndGardenPlantings(
+ @Embedded
+ val plant: Plant,
+
+ @Relation(parentColumn = "id", entityColumn = "plant_id")
+ val gardenPlantings: List = emptyList()
+)
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantDao.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantDao.kt
new file mode 100644
index 000000000..fba4e5334
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantDao.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+/**
+ * The Data Access Object for the Plant class.
+ */
+@Dao
+interface PlantDao {
+ @Query("SELECT * FROM plants ORDER BY name")
+ fun getPlants(): LiveData>
+
+ @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
+ fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): LiveData>
+
+ @Query("SELECT * FROM plants WHERE id = :plantId")
+ fun getPlant(plantId: String): LiveData
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertAll(plants: List)
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantRepository.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantRepository.kt
new file mode 100644
index 000000000..c40f1063f
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/data/PlantRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+/**
+ * Repository module for handling data operations.
+ */
+class PlantRepository private constructor(private val plantDao: PlantDao) {
+
+ fun getPlants() = plantDao.getPlants()
+
+ fun getPlant(plantId: String) = plantDao.getPlant(plantId)
+
+ fun getPlantsWithGrowZoneNumber(growZoneNumber: Int) =
+ plantDao.getPlantsWithGrowZoneNumber(growZoneNumber)
+
+ companion object {
+
+ // For Singleton instantiation
+ @Volatile private var instance: PlantRepository? = null
+
+ fun getInstance(plantDao: PlantDao) =
+ instance ?: synchronized(this) {
+ instance ?: PlantRepository(plantDao).also { instance = it }
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailDescription.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailDescription.kt
new file mode 100644
index 000000000..688af5df2
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailDescription.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.plantdetail
+
+import android.content.res.Configuration
+import android.text.method.LinkMovementMethod
+import android.widget.TextView
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.text.HtmlCompat
+import com.google.samples.apps.sunflower.R
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.theme.SunflowerTheme
+import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel
+
+@Composable
+fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
+ // Observes values coming from the VM's LiveData field as State
+ val plant by plantDetailViewModel.plant.observeAsState()
+
+ // New emissions from plant will make PlantDetailDescription recompose as the state's read here
+ plant?.let {
+ // If plant is not null, display the content
+ PlantDetailContent(it)
+ }
+}
+
+@Composable
+fun PlantDetailContent(plant: Plant) {
+ Surface {
+ Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
+ PlantName(plant.name)
+ PlantWatering(plant.wateringInterval)
+ PlantDescription(plant.description)
+ }
+ }
+}
+
+@Composable
+private fun PlantName(name: String) {
+ Text(
+ text = name,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = dimensionResource(R.dimen.margin_small))
+ .wrapContentWidth(align = Alignment.CenterHorizontally)
+ )
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun PlantWatering(wateringInterval: Int) {
+ Column(Modifier.fillMaxWidth()) {
+ // Same modifier used by both Texts
+ val centerWithPaddingModifier = Modifier
+ .padding(horizontal = dimensionResource(R.dimen.margin_small))
+ .align(Alignment.CenterHorizontally)
+
+ val normalPadding = dimensionResource(R.dimen.margin_normal)
+
+ Text(
+ text = stringResource(R.string.watering_needs_prefix),
+ color = MaterialTheme.colorScheme.primaryContainer,
+ fontWeight = FontWeight.Bold,
+ modifier = centerWithPaddingModifier.padding(top = normalPadding)
+ )
+
+ val wateringIntervalText = pluralStringResource(
+ R.plurals.watering_needs_suffix,
+ wateringInterval,
+ wateringInterval
+ )
+ Text(
+ text = wateringIntervalText,
+ modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
+ )
+ }
+}
+
+@Composable
+private fun PlantDescription(description: String) {
+ // Remembers the HTML formatted description. Re-executes on a new description
+ val htmlDescription = remember(description) {
+ HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
+ }
+
+ // Displays the TextView on the screen and updates with the HTML description when inflated
+ // Updates to htmlDescription will make AndroidView recompose and update the text
+ AndroidView(
+ factory = { context ->
+ TextView(context).apply {
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+ },
+ update = {
+ it.text = htmlDescription
+ }
+ )
+}
+
+@Preview
+@Composable
+private fun PlantDetailContentPreview() {
+ val plant = Plant("id", "Apple", "HTML
description", 3, 30, "")
+ SunflowerTheme {
+ PlantDetailContent(plant)
+ }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun PlantDetailContentDarkPreview() {
+ val plant = Plant("id", "Apple", "HTML
description", 3, 30, "")
+ SunflowerTheme {
+ PlantDetailContent(plant)
+ }
+}
+
+@Preview
+@Composable
+private fun PlantNamePreview() {
+ SunflowerTheme {
+ PlantName("Apple")
+ }
+}
+
+@Preview
+@Composable
+private fun PlantWateringPreview() {
+ SunflowerTheme {
+ PlantWatering(7)
+ }
+}
+
+@Preview
+@Composable
+private fun PlantDescriptionPreview() {
+ SunflowerTheme {
+ PlantDescription("HTML
description")
+ }
+}
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailFragment.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailFragment.kt
new file mode 100644
index 000000000..526592d59
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/plantdetail/PlantDetailFragment.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.plantdetail
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.app.ShareCompat
+import androidx.core.widget.NestedScrollView
+import androidx.databinding.DataBindingUtil
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.navArgs
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.google.android.material.snackbar.Snackbar
+import com.google.samples.apps.sunflower.R
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.databinding.FragmentPlantDetailBinding
+import com.google.samples.apps.sunflower.theme.SunflowerTheme
+import com.google.samples.apps.sunflower.utilities.InjectorUtils
+import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel
+
+/**
+ * A fragment representing a single Plant detail screen.
+ */
+class PlantDetailFragment : Fragment() {
+
+ private val args: PlantDetailFragmentArgs by navArgs()
+
+ private val plantDetailViewModel: PlantDetailViewModel by viewModels {
+ InjectorUtils.providePlantDetailViewModelFactory(requireActivity(), args.plantId)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val binding = DataBindingUtil.inflate(
+ inflater,
+ R.layout.fragment_plant_detail,
+ container,
+ false
+ ).apply {
+ viewModel = plantDetailViewModel
+ lifecycleOwner = viewLifecycleOwner
+ callback = object : Callback {
+ override fun add(plant: Plant?) {
+ plant?.let {
+ hideAppBarFab(fab)
+ plantDetailViewModel.addPlantToGarden()
+ Snackbar.make(root, R.string.added_plant_to_garden, Snackbar.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+
+ var isToolbarShown = false
+
+ // scroll change listener begins at Y = 0 when image is fully collapsed
+ plantDetailScrollview.setOnScrollChangeListener(
+ NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ ->
+
+ // User scrolled past image to height of toolbar and the title text is
+ // underneath the toolbar, so the toolbar should be shown.
+ val shouldShowToolbar = scrollY > toolbar.height
+
+ // The new state of the toolbar differs from the previous state; update
+ // appbar and toolbar attributes.
+ if (isToolbarShown != shouldShowToolbar) {
+ isToolbarShown = shouldShowToolbar
+
+ // Use shadow animator to add elevation if toolbar is shown
+ appbar.isActivated = shouldShowToolbar
+
+ // Show the plant name if toolbar is shown
+ toolbarLayout.isTitleEnabled = shouldShowToolbar
+ }
+ }
+ )
+
+ toolbar.setNavigationOnClickListener { view ->
+ view.findNavController().navigateUp()
+ }
+
+ toolbar.setOnMenuItemClickListener { item ->
+ when (item.itemId) {
+ R.id.action_share -> {
+ createShareIntent()
+ true
+ }
+ else -> false
+ }
+ }
+
+ composeView.apply {
+ // By default, the Composition is disposed when ComposeView is detached
+ // from the window. This causes problems during transitions as the ComposeView
+ // will still be visible on the screen after it's detached from the window.
+ // Instead, to dispose the Composition when the Fragment view lifecycle is
+ // destroyed, we set the DisposeOnViewTreeLifecycleDestroyed strategy as the
+ // ViewCompositionStrategy for this ComposeView
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+
+ // Add Jetpack Compose content to this View
+ setContent {
+ SunflowerTheme {
+ PlantDetailDescription(plantDetailViewModel)
+ }
+ }
+ }
+ }
+ setHasOptionsMenu(true)
+
+ return binding.root
+ }
+
+ // Helper function for calling a share functionality.
+ // Should be used when user presses a share button/menu item.
+ @Suppress("DEPRECATION")
+ private fun createShareIntent() {
+ val shareText = plantDetailViewModel.plant.value.let { plant ->
+ if (plant == null) {
+ ""
+ } else {
+ getString(R.string.share_text_plant, plant.name)
+ }
+ }
+ val shareIntent = ShareCompat.IntentBuilder.from(requireActivity())
+ .setText(shareText)
+ .setType("text/plain")
+ .createChooserIntent()
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+ startActivity(shareIntent)
+ }
+
+ // FloatingActionButtons anchored to AppBarLayouts have their visibility controlled by the scroll position.
+ // We want to turn this behavior off to hide the FAB when it is clicked.
+ //
+ // This is adapted from Chris Banes' Stack Overflow answer: https://stackoverflow.com/a/41442923
+ private fun hideAppBarFab(fab: FloatingActionButton) {
+ val params = fab.layoutParams as CoordinatorLayout.LayoutParams
+ val behavior = params.behavior as FloatingActionButton.Behavior
+ behavior.isAutoHideEnabled = false
+ fab.hide()
+ }
+
+ interface Callback {
+ fun add(plant: Plant?)
+ }
+}
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/theme/Theme.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/theme/Theme.kt
new file mode 100644
index 000000000..26a43c115
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/theme/Theme.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.theme
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.colorResource
+import com.google.samples.apps.sunflower.R
+
+@SuppressLint("ConflictingOnColor")
+@Composable
+fun SunflowerTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val lightColors = lightColorScheme(
+ primary = colorResource(id = R.color.sunflower_green_500),
+ primaryContainer = colorResource(id = R.color.sunflower_green_700),
+ secondary = colorResource(id = R.color.sunflower_yellow_500),
+ background = colorResource(id = R.color.sunflower_green_500),
+ onPrimary = colorResource(id = R.color.sunflower_black),
+ onSecondary = colorResource(id = R.color.sunflower_black),
+ )
+ val darkColors = darkColorScheme(
+ primary = colorResource(id = R.color.sunflower_green_100),
+ primaryContainer = colorResource(id = R.color.sunflower_green_200),
+ secondary = colorResource(id = R.color.sunflower_yellow_300),
+ onPrimary = colorResource(id = R.color.sunflower_black),
+ onSecondary = colorResource(id = R.color.sunflower_black),
+ onBackground = colorResource(id = R.color.sunflower_black),
+ surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
+ onSurface = colorResource(id = R.color.sunflower_white),
+ )
+ val colors = if (darkTheme) darkColors else lightColors
+ MaterialTheme(
+ colorScheme = colors,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/Constants.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/Constants.kt
new file mode 100644
index 000000000..30eb99477
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.utilities
+
+/**
+ * Constants used throughout the app.
+ */
+const val DATABASE_NAME = "sunflower-db"
+const val PLANT_DATA_FILENAME = "plants.json"
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/GetZoneForLatitude.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/GetZoneForLatitude.kt
new file mode 100644
index 000000000..d433b8562
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/GetZoneForLatitude.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.utilities
+
+import kotlin.math.abs
+
+/**
+ * A helper function to determine a Plant's growing zone for a given latitude.
+ *
+ * The numbers listed here are roughly based on the United States Department of Agriculture's
+ * Plant Hardiness Zone Map (http://planthardiness.ars.usda.gov/), which helps determine which
+ * plants are most likely to thrive at a location.
+ *
+ * If a given latitude falls on the border between two zone ranges, the larger zone range is chosen
+ * (e.g. latitude 14.0 => zone 12).
+ *
+ * Negative latitude values are converted to positive with [Math.abs].
+ *
+ * For latitude values greater than max (90.0), zone 1 is returned.
+ */
+fun getZoneForLatitude(latitude: Double) = when (abs(latitude)) {
+ in 0.0..7.0 -> 13
+ in 7.0..14.0 -> 12
+ in 14.0..21.0 -> 11
+ in 21.0..28.0 -> 10
+ in 28.0..35.0 -> 9
+ in 35.0..42.0 -> 8
+ in 42.0..49.0 -> 7
+ in 49.0..56.0 -> 6
+ in 56.0..63.0 -> 5
+ in 63.0..70.0 -> 4
+ in 70.0..77.0 -> 3
+ in 77.0..84.0 -> 2
+ else -> 1 // Remaining latitudes are assigned to zone 1.
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/InjectorUtils.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/InjectorUtils.kt
new file mode 100644
index 000000000..460689a78
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/utilities/InjectorUtils.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.utilities
+
+import android.content.Context
+import androidx.fragment.app.Fragment
+import com.google.samples.apps.sunflower.data.AppDatabase
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+import com.google.samples.apps.sunflower.data.PlantRepository
+import com.google.samples.apps.sunflower.viewmodels.GardenPlantingListViewModelFactory
+import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModelFactory
+import com.google.samples.apps.sunflower.viewmodels.PlantListViewModelFactory
+import kotlinx.coroutines.Dispatchers
+
+/**
+ * Static methods used to inject classes needed for various Activities and Fragments.
+ */
+object InjectorUtils {
+
+ private val ioDispatcher = Dispatchers.IO
+
+ private fun getPlantRepository(context: Context): PlantRepository {
+ return PlantRepository.getInstance(
+ AppDatabase.getInstance(context.applicationContext).plantDao()
+ )
+ }
+
+ private fun getGardenPlantingRepository(context: Context): GardenPlantingRepository {
+ return GardenPlantingRepository.getInstance(
+ AppDatabase.getInstance(context.applicationContext).gardenPlantingDao(),
+ ioDispatcher
+ )
+ }
+
+ fun provideGardenPlantingListViewModelFactory(
+ context: Context
+ ): GardenPlantingListViewModelFactory {
+ return GardenPlantingListViewModelFactory(getGardenPlantingRepository(context))
+ }
+
+ fun providePlantListViewModelFactory(fragment: Fragment): PlantListViewModelFactory {
+ return PlantListViewModelFactory(getPlantRepository(fragment.requireContext()), fragment)
+ }
+
+ fun providePlantDetailViewModelFactory(
+ context: Context,
+ plantId: String
+ ): PlantDetailViewModelFactory {
+ return PlantDetailViewModelFactory(
+ getPlantRepository(context),
+ getGardenPlantingRepository(context),
+ plantId
+ )
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModel.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModel.kt
new file mode 100644
index 000000000..d925db4df
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings
+
+class GardenPlantingListViewModel internal constructor(
+ gardenPlantingRepository: GardenPlantingRepository
+) : ViewModel() {
+ val plantAndGardenPlantings: LiveData> =
+ gardenPlantingRepository.getPlantedGardens()
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModelFactory.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModelFactory.kt
new file mode 100644
index 000000000..d6d6e173c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/GardenPlantingListViewModelFactory.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+
+/**
+ * Factory for creating a [GardenPlantingListViewModel] with a constructor that takes a
+ * [GardenPlantingRepository].
+ */
+class GardenPlantingListViewModelFactory(
+ private val repository: GardenPlantingRepository
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return GardenPlantingListViewModel(repository) as T
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantAndGardenPlantingsViewModel.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantAndGardenPlantingsViewModel.kt
new file mode 100644
index 000000000..50d6bc3b6
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantAndGardenPlantingsViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import com.google.samples.apps.sunflower.data.PlantAndGardenPlantings
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class PlantAndGardenPlantingsViewModel(plantings: PlantAndGardenPlantings) {
+ private val plant = checkNotNull(plantings.plant)
+ private val gardenPlanting = plantings.gardenPlantings[0]
+
+ val waterDateString: String = dateFormat.format(gardenPlanting.lastWateringDate.time)
+ val wateringInterval
+ get() = plant.wateringInterval
+ val imageUrl
+ get() = plant.imageUrl
+ val plantName
+ get() = plant.name
+ val plantDateString: String = dateFormat.format(gardenPlanting.plantDate.time)
+ val plantId
+ get() = plant.plantId
+
+ companion object {
+ private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US)
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModel.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModel.kt
new file mode 100644
index 000000000..e22cc0979
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+import com.google.samples.apps.sunflower.data.PlantRepository
+import com.google.samples.apps.sunflower.plantdetail.PlantDetailFragment
+import kotlinx.coroutines.launch
+
+/**
+ * The ViewModel used in [PlantDetailFragment].
+ */
+class PlantDetailViewModel(
+ plantRepository: PlantRepository,
+ private val gardenPlantingRepository: GardenPlantingRepository,
+ private val plantId: String
+) : ViewModel() {
+
+ val isPlanted = gardenPlantingRepository.isPlanted(plantId)
+ val plant = plantRepository.getPlant(plantId)
+
+ fun addPlantToGarden() {
+ viewModelScope.launch {
+ gardenPlantingRepository.createGardenPlanting(plantId)
+ }
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelFactory.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelFactory.kt
new file mode 100644
index 000000000..88670a3d2
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantDetailViewModelFactory.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.google.samples.apps.sunflower.data.GardenPlantingRepository
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.data.PlantRepository
+
+/**
+ * Factory for creating a [PlantDetailViewModel] with a constructor that takes a [PlantRepository]
+ * and an ID for the current [Plant].
+ */
+class PlantDetailViewModelFactory(
+ private val plantRepository: PlantRepository,
+ private val gardenPlantingRepository: GardenPlantingRepository,
+ private val plantId: String
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return PlantDetailViewModel(plantRepository, gardenPlantingRepository, plantId) as T
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModel.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModel.kt
new file mode 100644
index 000000000..3c8adeeac
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.switchMap
+import com.google.samples.apps.sunflower.PlantListFragment
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.data.PlantRepository
+
+/**
+ * The ViewModel for [PlantListFragment].
+ */
+class PlantListViewModel internal constructor(
+ plantRepository: PlantRepository,
+ private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ val plants: LiveData> = getSavedGrowZoneNumber().switchMap {
+ if (it == NO_GROW_ZONE) {
+ plantRepository.getPlants()
+ } else {
+ plantRepository.getPlantsWithGrowZoneNumber(it)
+ }
+ }
+
+ fun setGrowZoneNumber(num: Int) {
+ savedStateHandle.set(GROW_ZONE_SAVED_STATE_KEY, num)
+ }
+
+ fun clearGrowZoneNumber() {
+ savedStateHandle.set(GROW_ZONE_SAVED_STATE_KEY, NO_GROW_ZONE)
+ }
+
+ fun isFiltered() = getSavedGrowZoneNumber().value != NO_GROW_ZONE
+
+ private fun getSavedGrowZoneNumber(): MutableLiveData {
+ return savedStateHandle.getLiveData(GROW_ZONE_SAVED_STATE_KEY, NO_GROW_ZONE)
+ }
+
+ companion object {
+ private const val NO_GROW_ZONE = -1
+ private const val GROW_ZONE_SAVED_STATE_KEY = "GROW_ZONE_SAVED_STATE_KEY"
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModelFactory.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModelFactory.kt
new file mode 100644
index 000000000..fca495cfa
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModelFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.viewmodels
+
+import android.os.Bundle
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.savedstate.SavedStateRegistryOwner
+import com.google.samples.apps.sunflower.data.PlantRepository
+
+/**
+ * Factory for creating a [PlantListViewModel] with a constructor that takes a [PlantRepository].
+ */
+class PlantListViewModelFactory(
+ private val repository: PlantRepository,
+ owner: SavedStateRegistryOwner,
+ defaultArgs: Bundle? = null
+) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T {
+ return PlantListViewModel(repository, handle) as T
+ }
+}
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/views/MaskedCardView.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/views/MaskedCardView.kt
new file mode 100644
index 000000000..72e73e783
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/views/MaskedCardView.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Path
+import android.graphics.RectF
+import android.util.AttributeSet
+import com.google.android.material.R
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.shape.ShapeAppearanceModel
+import com.google.android.material.shape.ShapeAppearancePathProvider
+
+/**
+ * A Card view that clips the content of any shape, this should be done upstream in card,
+ * working around it for now.
+ */
+class MaskedCardView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = R.attr.materialCardViewStyle
+) : MaterialCardView(context, attrs, defStyle) {
+ @SuppressLint("RestrictedApi")
+ private val pathProvider = ShapeAppearancePathProvider()
+ private val path: Path = Path()
+ private val shapeAppearance: ShapeAppearanceModel =
+ ShapeAppearanceModel.builder(
+ context,
+ attrs,
+ defStyle,
+ R.style.Widget_MaterialComponents_CardView
+ ).build()
+
+ private val rectF = RectF(0f, 0f, 0f, 0f)
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.clipPath(path)
+ super.onDraw(canvas)
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ rectF.right = w.toFloat()
+ rectF.bottom = h.toFloat()
+ pathProvider.calculatePath(shapeAppearance, 1f, rectF, path)
+ super.onSizeChanged(w, h, oldw, oldh)
+ }
+}
diff --git a/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/workers/SeedDatabaseWorker.kt b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/workers/SeedDatabaseWorker.kt
new file mode 100644
index 000000000..89063b812
--- /dev/null
+++ b/MigrationCodelab/app/src/main/java/com/google/samples/apps/sunflower/workers/SeedDatabaseWorker.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.workers
+
+import android.content.Context
+import android.util.Log
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.samples.apps.sunflower.data.AppDatabase
+import com.google.samples.apps.sunflower.data.Plant
+import com.google.samples.apps.sunflower.utilities.PLANT_DATA_FILENAME
+import kotlinx.coroutines.coroutineScope
+
+class SeedDatabaseWorker(
+ context: Context,
+ workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
+ override suspend fun doWork(): Result = coroutineScope {
+ try {
+ applicationContext.assets.open(PLANT_DATA_FILENAME).use { inputStream ->
+ JsonReader(inputStream.reader()).use { jsonReader ->
+ val plantType = object : TypeToken>() {}.type
+ val plantList: List = Gson().fromJson(jsonReader, plantType)
+
+ val database = AppDatabase.getInstance(applicationContext)
+ database.plantDao().insertAll(plantList)
+
+ Result.success()
+ }
+ }
+ } catch (ex: Exception) {
+ Log.e(TAG, "Error seeding database", ex)
+ Result.failure()
+ }
+ }
+
+ companion object {
+ private const val TAG = "SeedDatabaseWorker"
+ }
+}
diff --git a/MigrationCodelab/app/src/main/res/anim/slide_in_left.xml b/MigrationCodelab/app/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 000000000..2cf890a19
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/anim/slide_in_right.xml b/MigrationCodelab/app/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 000000000..f0d7612f0
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/anim/slide_out_left.xml b/MigrationCodelab/app/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 000000000..fe0cde740
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/anim/slide_out_right.xml b/MigrationCodelab/app/src/main/res/anim/slide_out_right.xml
new file mode 100644
index 000000000..b7ede9b60
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/anim/slide_out_right.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/animator/show_toolbar.xml b/MigrationCodelab/app/src/main/res/animator/show_toolbar.xml
new file mode 100644
index 000000000..7656685b9
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/animator/show_toolbar.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/color/color_on_surface_20.xml b/MigrationCodelab/app/src/main/res/color/color_on_surface_20.xml
new file mode 100644
index 000000000..6316b50cb
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/color/color_on_surface_20.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/color/color_on_surface_50.xml b/MigrationCodelab/app/src/main/res/color/color_on_surface_50.xml
new file mode 100644
index 000000000..a9f697266
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/color/color_on_surface_50.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/drawable/garden_tab_selector.xml b/MigrationCodelab/app/src/main/res/drawable/garden_tab_selector.xml
new file mode 100644
index 000000000..dc7d70fb4
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/garden_tab_selector.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_detail_back.xml b/MigrationCodelab/app/src/main/res/drawable/ic_detail_back.xml
new file mode 100644
index 000000000..f6ce8b03b
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_detail_back.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_detail_share.xml b/MigrationCodelab/app/src/main/res/drawable/ic_detail_share.xml
new file mode 100644
index 000000000..e0fe4875c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_detail_share.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_filter_list_24dp.xml b/MigrationCodelab/app/src/main/res/drawable/ic_filter_list_24dp.xml
new file mode 100644
index 000000000..7f0892f16
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_filter_list_24dp.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_launcher_background.xml b/MigrationCodelab/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..05983e01f
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_active.xml b/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_active.xml
new file mode 100644
index 000000000..51e539212
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_active.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_inactive.xml b/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_inactive.xml
new file mode 100644
index 000000000..07d88fb3e
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_my_garden_inactive.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_active.xml b/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_active.xml
new file mode 100644
index 000000000..a32b83aa4
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_active.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_inactive.xml b/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_inactive.xml
new file mode 100644
index 000000000..dd338703a
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_plant_list_inactive.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/ic_plus.xml b/MigrationCodelab/app/src/main/res/drawable/ic_plus.xml
new file mode 100644
index 000000000..dda947fba
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/ic_plus.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/drawable/plant_list_tab_selector.xml b/MigrationCodelab/app/src/main/res/drawable/plant_list_tab_selector.xml
new file mode 100644
index 000000000..32fabb93c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/plant_list_tab_selector.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/drawable/tab_icon_color_selector.xml b/MigrationCodelab/app/src/main/res/drawable/tab_icon_color_selector.xml
new file mode 100644
index 000000000..b359a1c74
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/drawable/tab_icon_color_selector.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/layout/activity_garden.xml b/MigrationCodelab/app/src/main/res/layout/activity_garden.xml
new file mode 100644
index 000000000..c09f0fc88
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/activity_garden.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/layout/fragment_garden.xml b/MigrationCodelab/app/src/main/res/layout/fragment_garden.xml
new file mode 100644
index 000000000..b8e9bcc25
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/fragment_garden.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/layout/fragment_plant_detail.xml b/MigrationCodelab/app/src/main/res/layout/fragment_plant_detail.xml
new file mode 100644
index 000000000..eb40bcd37
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/fragment_plant_detail.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/layout/fragment_plant_list.xml b/MigrationCodelab/app/src/main/res/layout/fragment_plant_list.xml
new file mode 100644
index 000000000..1f75cd6d8
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/fragment_plant_list.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/layout/fragment_view_pager.xml b/MigrationCodelab/app/src/main/res/layout/fragment_view_pager.xml
new file mode 100644
index 000000000..f17fd8bf2
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/fragment_view_pager.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/layout/list_item_garden_planting.xml b/MigrationCodelab/app/src/main/res/layout/list_item_garden_planting.xml
new file mode 100644
index 000000000..6b6d0b461
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/list_item_garden_planting.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/layout/list_item_plant.xml b/MigrationCodelab/app/src/main/res/layout/list_item_plant.xml
new file mode 100644
index 000000000..eaac01c24
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/layout/list_item_plant.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/menu/menu_plant_detail.xml b/MigrationCodelab/app/src/main/res/menu/menu_plant_detail.xml
new file mode 100644
index 000000000..17978a99c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/menu/menu_plant_detail.xml
@@ -0,0 +1,27 @@
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/menu/menu_plant_list.xml b/MigrationCodelab/app/src/main/res/menu/menu_plant_list.xml
new file mode 100644
index 000000000..46cd7ef25
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/menu/menu_plant_list.xml
@@ -0,0 +1,26 @@
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..8c3cb5037
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..8c3cb5037
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..5fe3a9596
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 000000000..ea58faa9a
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0a1151e88
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..457d3d197
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..2c2cfbd8f
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 000000000..3847f8767
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..c09b2dea3
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..bf6340a18
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..2e3f249da
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 000000000..f63dc13ba
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..854d91661
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..43b03e1da
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c7ab6ee62
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 000000000..6f8418cb1
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..95159c04c
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..5f75b147e
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..32faef718
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 000000000..7696a8a15
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..5572ecaeb
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4d47dbc22
Binary files /dev/null and b/MigrationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/MigrationCodelab/app/src/main/res/navigation/nav_garden.xml b/MigrationCodelab/app/src/main/res/navigation/nav_garden.xml
new file mode 100644
index 000000000..ee0661dcc
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/navigation/nav_garden.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values-ca/strings.xml b/MigrationCodelab/app/src/main/res/values-ca/strings.xml
new file mode 100644
index 000000000..da891ea30
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-ca/strings.xml
@@ -0,0 +1,47 @@
+
+
+
+ Filtra per zona de creixement
+ Imatge de la planta
+ El meu jardí
+ Llista de plantes
+ Plantes disponibles
+ Detalls de la planta
+ Afegeix una planta
+ La planta s\'ha afegit al jardí
+ El jardí és buit
+ Plantada
+ Regada per darrera vegada
+ Comparteix
+ Mira la planta %s a l\'aplicació Android Sunflower
+
+
+ Cal regar-la
+
+ - diàriament
+ - cada %d dies
+
+
+
+ - cal regar-la demà.
+ - cal regar-la d\'aquí a %d dies.
+
+
+
+ Imatge de la planta
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-de/strings.xml b/MigrationCodelab/app/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..1abbd0c0c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,47 @@
+
+
+
+ Nach Wachstumszone filtern
+ Bild der Pflanze
+ Mein Garten
+ Pflanzenliste
+ Verfügbare Pflanzen
+ Details zur Pflanze
+ Pflanze hinzufügen
+ Pflanze wurde zum Garten hinzugefügt
+ Ihr Garten ist unbepflanzt
+ Gepflanzt
+ Zuletzt gegossen
+ Teilen
+ Schau dir die %s Pflanze in der App "Sunflower" an
+
+
+ Wasserbedarf
+
+ - jeden Tag
+ - alle %d Tage
+
+
+
+ - Morgen gießen
+ - In %d Tagen gießen
+
+
+
+ Bild der Pflanze
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-es/strings.xml b/MigrationCodelab/app/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..00ca4a5d7
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,47 @@
+
+
+
+ Filtrar por zona de crecimiento
+ Imagen de la planta
+ Mi jardín
+ Lista de plantas
+ Plantas disponibles
+ Detalles de la planta
+ Añadir planta
+ Planta añadida al jardín
+ Tu jardín esta vacío
+ Plantada
+ Última vez regada
+ Compartir
+ Revisa la planta %s en la aplicación Android Sunflower
+
+
+ Necesita ser regada
+
+ - diariamente.
+ - cada %d días.
+
+
+
+ - regar mañana.
+ - regar en %d días.
+
+
+
+ Imagen de la planta
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-fr/strings.xml b/MigrationCodelab/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..b5932051c
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,49 @@
+
+
+
+ Filtrer par zone de croissance
+ Mon jardin
+ Image de plante
+ Liste des plantes
+ Plantes disponibles
+ Détails sur la plante
+ Ajouter une plante
+ Plante ajoutée à votre jardin
+ Votre jardin est vide
+ Plantée
+ Dernier arrosage
+ Partager
+ Regardez la plante %s sur l\'application Sunflower
+
+
+ Arrosage nécessaire
+
+ - %d fois par jour
+ - Tous les %d jours
+ - Tous les %d jours
+
+
+
+ - à arroser dans %d jour.
+ - à arroser dans %d jours.
+ - à arroser dans %d jours.
+
+
+
+ Image de plante
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-it/strings.xml b/MigrationCodelab/app/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..80afaa82e
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,47 @@
+
+
+
+ Filtra per zona di crescita
+ Immagine della pianta
+ Il mio giardino
+ Lista delle piante
+ (translate me) Available Plants
+ Dettagli della pianta
+ Condividi
+ (translate me) Planted
+ (translate me) Last Watered
+ Il tuo giardino è vuoto
+ (translate me) Add plant
+ Pianta aggiunta al giardino
+ Controlla la pianta %s nell\'app Sunflower
+
+
+ Esigenze di irrigazione
+
+ - ogni giorno
+ - ogni %d giorni
+
+
+
+ - annaffia domani.
+ - annaffia tra %d giorni.
+
+
+
+ Immagine della pianta
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values-ja/strings.xml b/MigrationCodelab/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 000000000..1e1887bd1
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+ 成長度合いによって分ける
+ 植物のイメージ
+ 私の庭
+ 植物のリスト
+ 選択できる植物
+ 植物の詳細
+ 植物を追加する
+ 植物が庭に追加されました
+ あなたの庭に植物はありません
+ 植えた日
+ 最後に水をあげた日
+ 共有
+ %s という植物を「Sunflower」アプリで見てみてください
+
+
+
+ 水の必要量
+
+ - %d日ごと
+
+
+
+ - %d日後に水をあげてください
+
+
+
+ 植物の写真
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-night-v23/styles.xml b/MigrationCodelab/app/src/main/res/values-night-v23/styles.xml
new file mode 100644
index 000000000..e427d4569
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-night-v23/styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values-night/styles.xml b/MigrationCodelab/app/src/main/res/values-night/styles.xml
new file mode 100644
index 000000000..5b3491144
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-ru/strings.xml b/MigrationCodelab/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..707a69df7
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,51 @@
+
+
+
+ Отфильтровать по зоне выращивания
+ Изображение растения
+ Мои сад
+ Растения
+ Доступные растения
+ Добавить растение
+ Ваш сад пуст
+ Поделиться
+ О растении
+ Растение добавлено в ваш сад
+ Посажен
+ Последний полив
+ Попробуйте %s в приложении Android Sunflower
+
+
+ Необходим полив
+
+ - раз в %d день
+ - каждый %d дня
+ - каждые %d дней
+ - каждые %d дней
+
+
+
+ - полить через %d день
+ - полить через %d дня
+ - полить через %d дней
+ - полить через %d дней
+
+
+
+ Изображение растения
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-tr-rTR/strings.xml b/MigrationCodelab/app/src/main/res/values-tr-rTR/strings.xml
new file mode 100644
index 000000000..94e4e38c2
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-tr-rTR/strings.xml
@@ -0,0 +1,47 @@
+
+
+
+ Yetişme bölgesine göre filtrele
+ Bitkinin görüntüsü
+ Bahçem
+ Bitki listesi
+ Mevcut Bitkiler
+ Bitki detayları
+ Bitki Ekle
+ Bitki bahçeye eklendi
+ Bahçen boş
+ Ekiliş Tarihi
+ Son Sulama Tarihi
+ Paylaş
+ %s bitkisini Android Sunflower uygulamasında inceleyin
+
+
+ Sulama gereksinimi:
+
+ - her gün
+ - %d günde bir
+
+
+
+ - yarın sulayın.
+ - %d gün içinde sulayın.
+
+
+
+ Bitkinin görüntüsü
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-v23/styles.xml b/MigrationCodelab/app/src/main/res/values-v23/styles.xml
new file mode 100644
index 000000000..7b6e9173b
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-v23/styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values-v29/shape.xml b/MigrationCodelab/app/src/main/res/values-v29/shape.xml
new file mode 100644
index 000000000..ef6fb49eb
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-v29/shape.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ 30dp
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values-zh-rCN/strings.xml b/MigrationCodelab/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 000000000..0d8a71496
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+ 按生长区筛选
+ 植物图片
+ 我的花园
+ 植物目录
+ 植物品种
+ 植物介绍
+ 添加植物
+ 添加了新植物
+ 花园里还没有植物
+ 种下日期
+ 最后浇水
+ 分享
+ 在安卓 Sunflower APP 上看看这 %s
+
+
+
+ 浇水指南
+
+ - 每隔 %d 天
+
+
+
+ - %d 天后浇水
+
+
+
+ 植物图片
+
+
diff --git a/MigrationCodelab/app/src/main/res/values-zh-rTW/strings.xml b/MigrationCodelab/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 000000000..2b805ca81
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+ 按生長區篩選
+ 植物圖片
+ 我的花園
+ 植物目錄
+ 植物品種
+ 植物介紹
+ 添加植物
+ 添加了新植物
+ 花園裡還沒有植物
+ 種下日期
+ 最後澆水
+ 分享
+ 在安卓 Sunflower APP 上看看這 %s
+
+
+
+ 澆水指南
+
+ - 每隔 %d 天
+
+
+
+ - %d 天後澆水
+
+
+
+ 植物圖片
+
+
diff --git a/MigrationCodelab/app/src/main/res/values/anim.xml b/MigrationCodelab/app/src/main/res/values/anim.xml
new file mode 100644
index 000000000..3515c8ba5
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/anim.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ 200
+
diff --git a/MigrationCodelab/app/src/main/res/values/colors.xml b/MigrationCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..5c03c17e4
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ #de000000
+ #fafafa
+ #99fafafa
+ #eaf6ef
+ #c7e6d3
+ #202221
+ #81ca9d
+ #6dc790
+ #49bb79
+ #005d2b
+ #1a231e
+ #deffffff
+ #f8f99f
+ #ffff63
+
diff --git a/MigrationCodelab/app/src/main/res/values/dimens.xml b/MigrationCodelab/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..fd5cfa81f
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/dimens.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ 16dp
+
+
+ 72dp
+
+ 278dp
+
+ 16dp
+ 8dp
+ 4dp
+
+ 95dp
+
+ 12dp
+ 26dp
+ 2dp
+ 12dp
+
+
+ 5dp
+
+
+ 24dp
+
+
+ 555dp
+
+
diff --git a/MigrationCodelab/app/src/main/res/values/integers.xml b/MigrationCodelab/app/src/main/res/values/integers.xml
new file mode 100644
index 000000000..76c669b44
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/integers.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ 2
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values/shape.xml b/MigrationCodelab/app/src/main/res/values/shape.xml
new file mode 100644
index 000000000..c5ece58c3
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/shape.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 12dp
+
+
\ No newline at end of file
diff --git a/MigrationCodelab/app/src/main/res/values/strings.xml b/MigrationCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..1cbc497b6
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+ Sunflower
+
+
+ Filter by grow zone
+ Image of plant
+ My garden
+ Plant list
+ Available Plants
+ Plant details
+ Add plant
+ Added plant to garden
+ Your garden is empty
+ Planted
+ Last Watered
+ Share
+ Check out the %s plant in the Android Sunflower app
+
+
+ Watering needs
+
+ - every day
+ - every %d days
+
+
+
+ - water tomorrow.
+ - water in %d days.
+
+
+
+ Picture of plant
+
+
diff --git a/MigrationCodelab/app/src/main/res/values/styles.xml b/MigrationCodelab/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..e81b8ea64
--- /dev/null
+++ b/MigrationCodelab/app/src/main/res/values/styles.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/ConvertersTest.kt b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/ConvertersTest.kt
new file mode 100644
index 000000000..1eca92cbb
--- /dev/null
+++ b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/ConvertersTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.Calendar
+import java.util.Calendar.DAY_OF_MONTH
+import java.util.Calendar.MONTH
+import java.util.Calendar.SEPTEMBER
+import java.util.Calendar.YEAR
+
+class ConvertersTest {
+
+ private val cal = Calendar.getInstance().apply {
+ set(YEAR, 1998)
+ set(MONTH, SEPTEMBER)
+ set(DAY_OF_MONTH, 4)
+ }
+
+ @Test fun calendarToDatestamp() {
+ assertEquals(cal.timeInMillis, Converters().calendarToDatestamp(cal))
+ }
+
+ @Test fun datestampToCalendar() {
+ assertEquals(Converters().datestampToCalendar(cal.timeInMillis), cal)
+ }
+}
diff --git a/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/GardenPlantingTest.kt b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/GardenPlantingTest.kt
new file mode 100644
index 000000000..5b1836cc1
--- /dev/null
+++ b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/GardenPlantingTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import org.hamcrest.CoreMatchers.equalTo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThat
+import org.junit.Test
+import java.util.Calendar
+import java.util.Calendar.DAY_OF_MONTH
+import java.util.Calendar.MONTH
+import java.util.Calendar.YEAR
+
+class GardenPlantingTest {
+
+ @Test
+ fun testDefaultValues() {
+ val gardenPlanting = GardenPlanting("1")
+ val cal = Calendar.getInstance()
+ assertYMD(cal, gardenPlanting.plantDate)
+ assertYMD(cal, gardenPlanting.lastWateringDate)
+ assertEquals(0L, gardenPlanting.gardenPlantingId)
+ }
+
+ // Only Year/Month/Day precision is needed for comparing GardenPlanting Calendar entries
+ private fun assertYMD(expectedCal: Calendar, actualCal: Calendar) {
+ assertThat(actualCal.get(YEAR), equalTo(expectedCal.get(YEAR)))
+ assertThat(actualCal.get(MONTH), equalTo(expectedCal.get(MONTH)))
+ assertThat(actualCal.get(DAY_OF_MONTH), equalTo(expectedCal.get(DAY_OF_MONTH)))
+ }
+}
diff --git a/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/PlantTest.kt b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/PlantTest.kt
new file mode 100644
index 000000000..da847f144
--- /dev/null
+++ b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/data/PlantTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.data
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.Calendar
+import java.util.Calendar.DAY_OF_YEAR
+
+class PlantTest {
+
+ private lateinit var plant: Plant
+
+ @Before fun setUp() {
+ plant = Plant("1", "Tomato", "A red vegetable", 1, 2, "")
+ }
+
+ @Test fun test_default_values() {
+ val defaultPlant = Plant("2", "Apple", "Description", 1)
+ assertEquals(7, defaultPlant.wateringInterval)
+ assertEquals("", defaultPlant.imageUrl)
+ }
+
+ @Test fun test_shouldBeWatered() {
+ Calendar.getInstance().let { now ->
+ // Generate lastWateringDate from being as copy of now.
+ val lastWateringDate = Calendar.getInstance()
+
+ // Test for lastWateringDate is today.
+ lastWateringDate.time = now.time
+ assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -0) }))
+
+ // Test for lastWateringDate is yesterday.
+ lastWateringDate.time = now.time
+ assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -1) }))
+
+ // Test for lastWateringDate is the day before yesterday.
+ lastWateringDate.time = now.time
+ assertFalse(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -2) }))
+
+ // Test for lastWateringDate is some days ago, three days ago, four days ago etc.
+ lastWateringDate.time = now.time
+ assertTrue(plant.shouldBeWatered(now, lastWateringDate.apply { add(DAY_OF_YEAR, -3) }))
+ }
+ }
+
+ @Test fun test_toString() {
+ assertEquals("Tomato", plant.toString())
+ }
+}
diff --git a/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/utilities/GrowZoneUtilTest.kt b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/utilities/GrowZoneUtilTest.kt
new file mode 100644
index 000000000..7279389be
--- /dev/null
+++ b/MigrationCodelab/app/src/test/java/com/google/samples/apps/sunflower/utilities/GrowZoneUtilTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.sunflower.utilities
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class GrowZoneUtilTest {
+
+ @Test fun getZoneForLatitude() {
+ assertEquals(13, getZoneForLatitude(0.0))
+ assertEquals(13, getZoneForLatitude(7.0))
+ assertEquals(12, getZoneForLatitude(7.1))
+ assertEquals(1, getZoneForLatitude(84.1))
+ assertEquals(1, getZoneForLatitude(90.0))
+ }
+
+ @Test fun getZoneForLatitude_negativeLatitudes() {
+ assertEquals(13, getZoneForLatitude(-7.0))
+ assertEquals(12, getZoneForLatitude(-7.1))
+ assertEquals(1, getZoneForLatitude(-84.1))
+ assertEquals(1, getZoneForLatitude(-90.0))
+ }
+
+ // Bugfix test for https://github.com/android/sunflower/issues/8
+ @Test fun getZoneForLatitude_GitHub_issue8() {
+ assertEquals(9, getZoneForLatitude(35.0))
+ assertEquals(8, getZoneForLatitude(42.0))
+ assertEquals(7, getZoneForLatitude(49.0))
+ assertEquals(6, getZoneForLatitude(56.0))
+ assertEquals(5, getZoneForLatitude(63.0))
+ assertEquals(4, getZoneForLatitude(70.0))
+ }
+}
diff --git a/MigrationCodelab/build.gradle b/MigrationCodelab/build.gradle
new file mode 100644
index 000000000..c0f064965
--- /dev/null
+++ b/MigrationCodelab/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:9.2.1"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.8"
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10"
+ }
+}
+
+plugins {
+ id 'com.diffplug.spotless' version '8.7.0'
+ id 'com.android.legacy-kapt' version '9.2.1' apply false
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+spotless {
+ kotlin {
+ target "**/*.kt"
+ targetExclude("$buildDir/**/*.kt")
+ targetExclude('bin/**/*.kt')
+ ktlint("0.46.1")
+ }
+}
diff --git a/MigrationCodelab/gradle.properties b/MigrationCodelab/gradle.properties
new file mode 100644
index 000000000..42979c479
--- /dev/null
+++ b/MigrationCodelab/gradle.properties
@@ -0,0 +1,34 @@
+#
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/MigrationCodelab/gradle/wrapper/gradle-wrapper.jar b/MigrationCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/MigrationCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/MigrationCodelab/gradle/wrapper/gradle-wrapper.properties b/MigrationCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/MigrationCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/MigrationCodelab/gradlew b/MigrationCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/MigrationCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/MigrationCodelab/gradlew.bat b/MigrationCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/MigrationCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/MigrationCodelab/screenshots/ic_launcher-web.png b/MigrationCodelab/screenshots/ic_launcher-web.png
new file mode 100644
index 000000000..21fe522b1
Binary files /dev/null and b/MigrationCodelab/screenshots/ic_launcher-web.png differ
diff --git a/MigrationCodelab/screenshots/icon_background.png b/MigrationCodelab/screenshots/icon_background.png
new file mode 100644
index 000000000..6f40b4a19
Binary files /dev/null and b/MigrationCodelab/screenshots/icon_background.png differ
diff --git a/MigrationCodelab/screenshots/icon_foreground.png b/MigrationCodelab/screenshots/icon_foreground.png
new file mode 100644
index 000000000..eb3e5a6de
Binary files /dev/null and b/MigrationCodelab/screenshots/icon_foreground.png differ
diff --git a/MigrationCodelab/screenshots/jetpack_donut.png b/MigrationCodelab/screenshots/jetpack_donut.png
new file mode 100644
index 000000000..2259388b9
Binary files /dev/null and b/MigrationCodelab/screenshots/jetpack_donut.png differ
diff --git a/MigrationCodelab/screenshots/phone_my_garden.png b/MigrationCodelab/screenshots/phone_my_garden.png
new file mode 100644
index 000000000..da73fc1e7
Binary files /dev/null and b/MigrationCodelab/screenshots/phone_my_garden.png differ
diff --git a/MigrationCodelab/screenshots/phone_plant_detail.png b/MigrationCodelab/screenshots/phone_plant_detail.png
new file mode 100644
index 000000000..0cd60cedc
Binary files /dev/null and b/MigrationCodelab/screenshots/phone_plant_detail.png differ
diff --git a/MigrationCodelab/screenshots/phone_plant_list.png b/MigrationCodelab/screenshots/phone_plant_list.png
new file mode 100644
index 000000000..a28a65e7d
Binary files /dev/null and b/MigrationCodelab/screenshots/phone_plant_list.png differ
diff --git a/MigrationCodelab/settings.gradle b/MigrationCodelab/settings.gradle
new file mode 100644
index 000000000..fcb112a59
--- /dev/null
+++ b/MigrationCodelab/settings.gradle
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+include ':app'
diff --git a/NavigationCodelab/.gitignore b/NavigationCodelab/.gitignore
new file mode 100644
index 000000000..fee255bb9
--- /dev/null
+++ b/NavigationCodelab/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.idea/*
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+.kotlin/
diff --git a/NavigationCodelab/ASSETS_LICENSE b/NavigationCodelab/ASSETS_LICENSE
new file mode 100644
index 000000000..e7fc95866
--- /dev/null
+++ b/NavigationCodelab/ASSETS_LICENSE
@@ -0,0 +1,88 @@
+All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 license.
+
+
+SIL OPEN FONT LICENSE
+Version 1.1 - 26 February 2007
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/NavigationCodelab/README.md b/NavigationCodelab/README.md
new file mode 100644
index 000000000..fc80ce26f
--- /dev/null
+++ b/NavigationCodelab/README.md
@@ -0,0 +1,23 @@
+# Navigation in Jetpack Compose Codelab
+
+This folder contains the source code for the
+[Navigation in Jetpack Compose Codelab](https://developer.android.com/codelabs/jetpack-compose-navigation)
+codelab.
+
+## License
+```
+Copyright 2022 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
diff --git a/NavigationCodelab/app/.gitignore b/NavigationCodelab/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/NavigationCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/NavigationCodelab/app/build.gradle b/NavigationCodelab/app/build.gradle
new file mode 100644
index 000000000..c90bd3abe
--- /dev/null
+++ b/NavigationCodelab/app/build.gradle
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.plugin.compose'
+}
+
+android {
+ compileSdkVersion 37
+ namespace "com.example.compose.rally"
+
+ defaultConfig {
+ applicationId "com.example.compose.rally"
+ minSdkVersion 23
+ targetSdkVersion 33
+ versionCode 1
+ versionName '1.0'
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+
+ vectorDrawables.useSupportLibrary = true
+ }
+
+ signingConfigs {
+ // We use a bundled debug keystore, to allow debug builds from CI to be upgradable
+ debug {
+ storeFile rootProject.file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.5.15'
+ }
+
+ packagingOptions {
+ exclude "META-INF/licenses/**"
+ exclude "META-INF/AL2.0"
+ exclude "META-INF/LGPL2.1"
+ }
+}
+
+dependencies {
+ def composeBom = platform('androidx.compose:compose-bom:2026.06.00')
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation "androidx.appcompat:appcompat:1.7.1"
+ implementation "androidx.core:core-ktx:1.19.0"
+ implementation "com.google.android.material:material:1.14.0"
+
+ // Compose
+ implementation "androidx.compose.runtime:runtime"
+ implementation "androidx.compose.ui:ui"
+ implementation "androidx.compose.foundation:foundation"
+ implementation "androidx.compose.material:material"
+ implementation "androidx.compose.material:material-icons-extended"
+ implementation "androidx.activity:activity-compose:1.13.0"
+ implementation "androidx.navigation:navigation-compose:2.9.8"
+ debugImplementation "androidx.compose.ui:ui-tooling"
+
+ // Testing dependencies
+ androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
+ androidTestImplementation "androidx.navigation:navigation-testing:2.9.8"
+ androidTestImplementation "androidx.test.espresso:espresso-contrib:3.7.0"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.7.0"
+
+ // Compose testing dependencies
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4"
+ debugImplementation "androidx.compose.ui:ui-test-manifest"
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ // Treat all Kotlin warnings as errors
+ allWarningsAsErrors = true
+
+ freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
+
+ // Set JVM target to 1.8
+ jvmTarget = "1.8"
+ }
+}
diff --git a/NavigationCodelab/app/proguard-rules.pro b/NavigationCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..4cb94585a
--- /dev/null
+++ b/NavigationCodelab/app/proguard-rules.pro
@@ -0,0 +1,24 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+-renamesourcefileattribute SourceFile
+
+# Repackage classes into the top-level.
+-repackageclasses
diff --git a/NavigationCodelab/app/src/androidTest/java/com/example/compose/rally/NavigationTest.kt b/NavigationCodelab/app/src/androidTest/java/com/example/compose/rally/NavigationTest.kt
new file mode 100644
index 000000000..ea9591f88
--- /dev/null
+++ b/NavigationCodelab/app/src/androidTest/java/com/example/compose/rally/NavigationTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally
+
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import androidx.navigation.compose.ComposeNavigator
+import androidx.navigation.testing.TestNavHostController
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class NavigationTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+ lateinit var navController: TestNavHostController
+
+ @Before
+ fun setupRallyNavHost() {
+ composeTestRule.setContent {
+ // Creates a TestNavHostController
+ navController = TestNavHostController(LocalContext.current)
+ // Sets a ComposeNavigator to the navController so it can navigate through composables
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+ RallyNavHost(navController = navController)
+ }
+ }
+
+ @Test
+ fun rallyNavHost_verifyOverviewStartDestination() {
+ composeTestRule
+ .onNodeWithContentDescription("Overview Screen")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun rallyNavHost_clickAllAccount_navigatesToAccounts() {
+ composeTestRule
+ .onNodeWithContentDescription("All Accounts")
+ .performClick()
+
+ composeTestRule
+ .onNodeWithContentDescription("Accounts Screen")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun rallyNavHost_clickAllBills_navigateToBills() {
+ composeTestRule.onNodeWithContentDescription("All Bills")
+ .performScrollTo()
+ .performClick()
+ val route = navController.currentBackStackEntry?.destination?.route
+ assertEquals(route, "bills")
+ }
+}
diff --git a/NavigationCodelab/app/src/main/AndroidManifest.xml b/NavigationCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..40afec60c
--- /dev/null
+++ b/NavigationCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyActivity.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyActivity.kt
new file mode 100644
index 000000000..84116f333
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyActivity.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.example.compose.rally.ui.components.RallyTabRow
+import com.example.compose.rally.ui.theme.RallyTheme
+
+/**
+ * This Activity recreates part of the Rally Material Study from
+ * https://material.io/design/material-studies/rally.html
+ */
+class RallyActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ RallyApp()
+ }
+ }
+}
+
+@Composable
+fun RallyApp() {
+ RallyTheme {
+ val navController = rememberNavController()
+ val currentBackStack by navController.currentBackStackEntryAsState()
+ val currentDestination = currentBackStack?.destination
+ val currentScreen =
+ rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Accounts
+
+ Scaffold(
+ topBar = {
+ RallyTabRow(
+ allScreens = rallyTabRowScreens,
+ onTabSelected = { newScreen ->
+ navController.navigateSingleTopTo(newScreen.route)
+ },
+ currentScreen = currentScreen
+ )
+ }
+ ) { innerPadding ->
+ RallyNavHost(
+ navController = navController,
+ modifier = Modifier.padding(innerPadding)
+ )
+ }
+ }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyDestinations.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyDestinations.kt
new file mode 100644
index 000000000..1b53193e5
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyDestinations.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.Money
+import androidx.compose.material.icons.filled.MoneyOff
+import androidx.compose.material.icons.filled.PieChart
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.navigation.navDeepLink
+
+/**
+ * Contract for information needed on every Rally navigation destination
+ */
+
+sealed interface RallyDestination {
+ val icon: ImageVector
+ val route: String
+}
+
+/**
+ * Rally app navigation destinations
+ */
+data object Overview : RallyDestination {
+ override val icon = Icons.Filled.PieChart
+ override val route = "overview"
+}
+
+data object Accounts : RallyDestination {
+ override val icon = Icons.Filled.AttachMoney
+ override val route = "accounts"
+}
+
+data object Bills : RallyDestination {
+ override val icon = Icons.Filled.MoneyOff
+ override val route = "bills"
+}
+
+data object SingleAccount : RallyDestination {
+ // Added for simplicity, this icon will not in fact be used, as SingleAccount isn't
+ // part of the RallyTabRow selection
+ override val icon = Icons.Filled.Money
+ override val route = "single_account"
+ const val accountTypeArg = "account_type"
+ val routeWithArgs = "$route/{$accountTypeArg}"
+ val arguments = listOf(
+ navArgument(accountTypeArg) { type = NavType.StringType }
+ )
+ val deepLinks = listOf(
+ navDeepLink { uriPattern = "rally://$route/{$accountTypeArg}" }
+ )
+}
+
+// Screens to be displayed in the top RallyTabRow
+val rallyTabRowScreens = listOf(Overview, Accounts, Bills)
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyNavHost.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyNavHost.kt
new file mode 100644
index 000000000..001ad36d1
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/RallyNavHost.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.example.compose.rally.ui.accounts.AccountsScreen
+import com.example.compose.rally.ui.accounts.SingleAccountScreen
+import com.example.compose.rally.ui.bills.BillsScreen
+import com.example.compose.rally.ui.overview.OverviewScreen
+
+@Composable
+fun RallyNavHost(
+ navController: NavHostController,
+ modifier: Modifier = Modifier
+) {
+ NavHost(
+ navController = navController,
+ startDestination = Overview.route,
+ modifier = modifier
+ ) {
+ composable(route = Overview.route) {
+ OverviewScreen(
+ onClickSeeAllAccounts = {
+ navController.navigateSingleTopTo(Accounts.route)
+ },
+ onClickSeeAllBills = {
+ navController.navigateSingleTopTo(Bills.route)
+ },
+ onAccountClick = { accountType ->
+ navController.navigateToSingleAccount(accountType)
+ }
+ )
+ }
+ composable(route = Accounts.route) {
+ AccountsScreen(
+ onAccountClick = { accountType ->
+ navController.navigateToSingleAccount(accountType)
+ }
+ )
+ }
+ composable(route = Bills.route) {
+ BillsScreen()
+ }
+ composable(
+ route = SingleAccount.routeWithArgs,
+ arguments = SingleAccount.arguments,
+ deepLinks = SingleAccount.deepLinks
+ ) { navBackStackEntry ->
+ val accountType =
+ navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
+ SingleAccountScreen(accountType)
+ }
+ }
+}
+
+fun NavHostController.navigateSingleTopTo(route: String) =
+ this.navigate(route) {
+ // Pop up to the start destination of the graph to
+ // avoid building up a large stack of destinations
+ // on the back stack as users select items
+ popUpTo(
+ this@navigateSingleTopTo.graph.findStartDestination().id
+ ) {
+ saveState = true
+ }
+ // Avoid multiple copies of the same destination when
+ // reselecting the same item
+ launchSingleTop = true
+ // Restore state when reselecting a previously selected item
+ restoreState = true
+ }
+
+private fun NavHostController.navigateToSingleAccount(accountType: String) {
+ this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/data/RallyData.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/data/RallyData.kt
new file mode 100644
index 000000000..6434c17d4
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/data/RallyData.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.data
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+/* Hard-coded data for the Rally sample. */
+
+@Immutable
+data class Account(
+ val name: String,
+ val number: Int,
+ val balance: Float,
+ val color: Color
+)
+
+@Immutable
+data class Bill(
+ val name: String,
+ val due: String,
+ val amount: Float,
+ val color: Color
+)
+
+/**
+ * Pretend repository for user's data.
+ */
+object UserData {
+ val accounts: List = listOf(
+ Account(
+ "Checking",
+ 1234,
+ 2215.13f,
+ Color(0xFF004940)
+ ),
+ Account(
+ "Home Savings",
+ 5678,
+ 8676.88f,
+ Color(0xFF005D57)
+ ),
+ Account(
+ "Car Savings",
+ 9012,
+ 987.48f,
+ Color(0xFF04B97F)
+ ),
+ Account(
+ "Vacation",
+ 3456,
+ 253f,
+ Color(0xFF37EFBA)
+ )
+ )
+ val bills: List = listOf(
+ Bill(
+ "RedPay Credit",
+ "Jan 29",
+ 45.36f,
+ Color(0xFFFFDC78)
+ ),
+ Bill(
+ "Rent",
+ "Feb 9",
+ 1200f,
+ Color(0xFFFF6951)
+ ),
+ Bill(
+ "TabFine Credit",
+ "Feb 22",
+ 87.33f,
+ Color(0xFFFFD7D0)
+ ),
+ Bill(
+ "ABC Loans",
+ "Feb 29",
+ 400f,
+ Color(0xFFFFAC12)
+ ),
+ Bill(
+ "ABC Loans 2",
+ "Feb 29",
+ 77.4f,
+ Color(0xFFFFAC12)
+ )
+ )
+
+ fun getAccount(accountName: String?): Account {
+ return accounts.first { it.name == accountName }
+ }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/accounts/AccountsScreen.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/accounts/AccountsScreen.kt
new file mode 100644
index 000000000..91da58b21
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/accounts/AccountsScreen.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.accounts
+
+import androidx.compose.foundation.clickable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import com.example.compose.rally.R
+import com.example.compose.rally.data.UserData
+import com.example.compose.rally.ui.components.AccountRow
+import com.example.compose.rally.ui.components.StatementBody
+
+/**
+ * The Accounts screen.
+ */
+@Composable
+fun AccountsScreen(
+ onAccountClick: (String) -> Unit = {},
+) {
+ val amountsTotal = remember { UserData.accounts.map { account -> account.balance }.sum() }
+ StatementBody(
+ modifier = Modifier.semantics { contentDescription = "Accounts Screen" },
+ items = UserData.accounts,
+ amounts = { account -> account.balance },
+ colors = { account -> account.color },
+ amountsTotal = amountsTotal,
+ circleLabel = stringResource(R.string.total),
+ rows = { account ->
+ AccountRow(
+ modifier = Modifier.clickable {
+ onAccountClick(account.name)
+ },
+ name = account.name,
+ number = account.number,
+ amount = account.balance,
+ color = account.color
+ )
+ }
+ )
+}
+
+/**
+ * Detail screen for a single account.
+ */
+@Composable
+fun SingleAccountScreen(
+ accountType: String? = UserData.accounts.first().name
+) {
+ val account = remember(accountType) { UserData.getAccount(accountType) }
+ StatementBody(
+ items = listOf(account),
+ colors = { account.color },
+ amounts = { account.balance },
+ amountsTotal = account.balance,
+ circleLabel = account.name,
+ ) { row ->
+ AccountRow(
+ name = row.name,
+ number = row.number,
+ amount = row.balance,
+ color = row.color
+ )
+ }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/bills/BillsScreen.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/bills/BillsScreen.kt
new file mode 100644
index 000000000..14863b01c
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/bills/BillsScreen.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.bills
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import com.example.compose.rally.R
+import com.example.compose.rally.data.Bill
+import com.example.compose.rally.data.UserData
+import com.example.compose.rally.ui.components.BillRow
+import com.example.compose.rally.ui.components.StatementBody
+
+/**
+ * The Bills screen.
+ */
+@Composable
+fun BillsScreen(
+ bills: List = remember { UserData.bills }
+) {
+ StatementBody(
+ modifier = Modifier.clearAndSetSemantics { contentDescription = "Bills" },
+ items = bills,
+ amounts = { bill -> bill.amount },
+ colors = { bill -> bill.color },
+ amountsTotal = bills.map { bill -> bill.amount }.sum(),
+ circleLabel = stringResource(R.string.due),
+ rows = { bill ->
+ BillRow(bill.name, bill.due, bill.amount, bill.color)
+ }
+ )
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt
new file mode 100644
index 000000000..eed4a3178
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Divider
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.unit.dp
+import com.example.compose.rally.R
+import java.text.DecimalFormat
+
+/**
+ * A row representing the basic information of an Account.
+ */
+@Composable
+fun AccountRow(
+ modifier: Modifier = Modifier,
+ name: String,
+ number: Int,
+ amount: Float,
+ color: Color
+) {
+ BaseRow(
+ modifier = modifier,
+ color = color,
+ title = name,
+ subtitle = stringResource(R.string.account_redacted) + AccountDecimalFormat.format(number),
+ amount = amount,
+ negative = false
+ )
+}
+
+/**
+ * A row representing the basic information of a Bill.
+ */
+@Composable
+fun BillRow(name: String, due: String, amount: Float, color: Color) {
+ BaseRow(
+ color = color,
+ title = name,
+ subtitle = "Due $due",
+ amount = amount,
+ negative = true
+ )
+}
+
+@Composable
+private fun BaseRow(
+ modifier: Modifier = Modifier,
+ color: Color,
+ title: String,
+ subtitle: String,
+ amount: Float,
+ negative: Boolean
+) {
+ val dollarSign = if (negative) "–$ " else "$ "
+ val formattedAmount = formatAmount(amount)
+ Row(
+ modifier = modifier
+ .height(68.dp)
+ .clearAndSetSemantics {
+ contentDescription =
+ "$title account ending in ${subtitle.takeLast(4)}, current balance $dollarSign$formattedAmount"
+ },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val typography = MaterialTheme.typography
+ AccountIndicator(
+ color = color,
+ modifier = Modifier
+ )
+ Spacer(Modifier.width(12.dp))
+ Column(Modifier) {
+ Text(text = title, style = typography.body1)
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Text(text = subtitle, style = typography.subtitle1)
+ }
+ }
+ Spacer(Modifier.weight(1f))
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = dollarSign,
+ style = typography.h6,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ Text(
+ text = formattedAmount,
+ style = typography.h6,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+ Spacer(Modifier.width(16.dp))
+
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Icon(
+ imageVector = Icons.Filled.ChevronRight,
+ contentDescription = null,
+ modifier = Modifier
+ .padding(end = 12.dp)
+ .size(24.dp)
+ )
+ }
+ }
+ RallyDivider()
+}
+
+/**
+ * A vertical colored line that is used in a [BaseRow] to differentiate accounts.
+ */
+@Composable
+private fun AccountIndicator(color: Color, modifier: Modifier = Modifier) {
+ Spacer(
+ modifier
+ .size(4.dp, 36.dp)
+ .background(color = color)
+ )
+}
+
+@Composable
+fun RallyDivider(modifier: Modifier = Modifier) {
+ Divider(color = MaterialTheme.colors.background, thickness = 1.dp, modifier = modifier)
+}
+
+fun formatAmount(amount: Float): String {
+ return AmountDecimalFormat.format(amount)
+}
+
+private val AccountDecimalFormat = DecimalFormat("####")
+private val AmountDecimalFormat = DecimalFormat("#,###.##")
+
+/**
+ * Used with accounts and bills to create the animated circle.
+ */
+fun List.extractProportions(selector: (E) -> Float): List {
+ val total = this.sumOf { selector(it).toDouble() }
+ return this.map { (selector(it) / total).toFloat() }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt
new file mode 100644
index 000000000..2445f42bc
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+/**
+ * Generic component used by the accounts and bills screens to show a chart and a list of items.
+ */
+@Composable
+fun StatementBody(
+ modifier: Modifier = Modifier,
+ items: List,
+ colors: (T) -> Color,
+ amounts: (T) -> Float,
+ amountsTotal: Float,
+ circleLabel: String,
+ rows: @Composable (T) -> Unit
+) {
+ Column(modifier = modifier.verticalScroll(rememberScrollState())) {
+ Box(Modifier.padding(16.dp)) {
+ val accountsProportion = items.extractProportions { amounts(it) }
+ val circleColors = items.map { colors(it) }
+ AnimatedCircle(
+ accountsProportion,
+ circleColors,
+ Modifier
+ .height(300.dp)
+ .align(Alignment.Center)
+ .fillMaxWidth()
+ )
+ Column(modifier = Modifier.align(Alignment.Center)) {
+ Text(
+ text = circleLabel,
+ style = MaterialTheme.typography.body1,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+ Text(
+ text = formatAmount(amountsTotal),
+ style = MaterialTheme.typography.h2,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+ }
+ }
+ Spacer(Modifier.height(10.dp))
+ Card {
+ Column(modifier = Modifier.padding(12.dp)) {
+ items.forEach { item ->
+ rows(item)
+ }
+ }
+ }
+ }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAlertDialog.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAlertDialog.kt
new file mode 100644
index 000000000..680ee53cb
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAlertDialog.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.unit.dp
+import com.example.compose.rally.ui.theme.RallyDialogThemeOverlay
+
+@Composable
+fun RallyAlertDialog(
+ onDismiss: () -> Unit,
+ bodyText: String,
+ buttonText: String
+) {
+ RallyDialogThemeOverlay {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ text = { Text(bodyText) },
+ buttons = {
+ Column {
+ Divider(
+ Modifier.padding(horizontal = 12.dp),
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
+ )
+ TextButton(
+ onClick = onDismiss,
+ shape = RectangleShape,
+ contentPadding = PaddingValues(16.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(buttonText)
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt
new file mode 100644
index 000000000..171e15f80
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.components
+
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+
+private const val DividerLengthInDegrees = 1.8f
+
+/**
+ * A donut chart that animates when loaded.
+ */
+@Composable
+fun AnimatedCircle(
+ proportions: List,
+ colors: List,
+ modifier: Modifier = Modifier
+) {
+ val currentState = remember {
+ MutableTransitionState(AnimatedCircleProgress.START)
+ .apply { targetState = AnimatedCircleProgress.END }
+ }
+ val stroke = with(LocalDensity.current) { Stroke(5.dp.toPx()) }
+ val transition = rememberTransition(currentState)
+ val angleOffset by transition.animateFloat(
+ transitionSpec = {
+ tween(
+ delayMillis = 500,
+ durationMillis = 900,
+ easing = LinearOutSlowInEasing
+ )
+ }
+ ) { progress ->
+ if (progress == AnimatedCircleProgress.START) {
+ 0f
+ } else {
+ 360f
+ }
+ }
+ val shift by transition.animateFloat(
+ transitionSpec = {
+ tween(
+ delayMillis = 500,
+ durationMillis = 900,
+ easing = CubicBezierEasing(0f, 0.75f, 0.35f, 0.85f)
+ )
+ }
+ ) { progress ->
+ if (progress == AnimatedCircleProgress.START) {
+ 0f
+ } else {
+ 30f
+ }
+ }
+
+ Canvas(modifier) {
+ val innerRadius = (size.minDimension - stroke.width) / 2
+ val halfSize = size / 2.0f
+ val topLeft = Offset(
+ halfSize.width - innerRadius,
+ halfSize.height - innerRadius
+ )
+ val size = Size(innerRadius * 2, innerRadius * 2)
+ var startAngle = shift - 90f
+ proportions.forEachIndexed { index, proportion ->
+ val sweep = proportion * angleOffset
+ drawArc(
+ color = colors[index],
+ startAngle = startAngle + DividerLengthInDegrees / 2,
+ sweepAngle = sweep - DividerLengthInDegrees,
+ topLeft = topLeft,
+ size = size,
+ useCenter = false,
+ style = stroke
+ )
+ startAngle += sweep
+ }
+ }
+}
+private enum class AnimatedCircleProgress { START, END }
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyTabRow.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyTabRow.kt
new file mode 100644
index 000000000..d9342bf89
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/components/RallyTabRow.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.components
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.ripple
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.example.compose.rally.RallyDestination
+import java.util.Locale
+import androidx.compose.ui.platform.LocalLocale
+
+@Composable
+fun RallyTabRow(
+ allScreens: List,
+ onTabSelected: (RallyDestination) -> Unit,
+ currentScreen: RallyDestination
+) {
+ Surface(
+ Modifier
+ .height(TabHeight)
+ .fillMaxWidth()
+ ) {
+ Row(Modifier.selectableGroup()) {
+ allScreens.forEach { screen ->
+ RallyTab(
+ text = screen.route,
+ icon = screen.icon,
+ onSelected = { onTabSelected(screen) },
+ selected = currentScreen == screen
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun RallyTab(
+ text: String,
+ icon: ImageVector,
+ onSelected: () -> Unit,
+ selected: Boolean
+) {
+ val color = MaterialTheme.colors.onSurface
+ val durationMillis = if (selected) TabFadeInAnimationDuration else TabFadeOutAnimationDuration
+ val animSpec = remember {
+ tween(
+ durationMillis = durationMillis,
+ easing = LinearEasing,
+ delayMillis = TabFadeInAnimationDelay
+ )
+ }
+ val tabTintColor by animateColorAsState(
+ targetValue = if (selected) color else color.copy(alpha = InactiveTabOpacity),
+ animationSpec = animSpec
+ )
+ Row(
+ modifier = Modifier
+ .padding(16.dp)
+ .animateContentSize()
+ .height(TabHeight)
+ .selectable(
+ selected = selected,
+ onClick = onSelected,
+ role = Role.Tab,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = ripple(
+ bounded = false,
+ radius = Dp.Unspecified,
+ color = Color.Unspecified
+ )
+ )
+ .clearAndSetSemantics { contentDescription = text }
+ ) {
+ Icon(imageVector = icon, contentDescription = text, tint = tabTintColor)
+ if (selected) {
+ Spacer(Modifier.width(12.dp))
+ Text(text.uppercase(LocalLocale.current.platformLocale), color = tabTintColor)
+ }
+ }
+}
+
+private val TabHeight = 56.dp
+private const val InactiveTabOpacity = 0.60f
+
+private const val TabFadeInAnimationDuration = 150
+private const val TabFadeInAnimationDelay = 100
+private const val TabFadeOutAnimationDuration = 100
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt
new file mode 100644
index 000000000..a2a0b7e0a
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.overview
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Sort
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import com.example.compose.rally.R
+import com.example.compose.rally.data.UserData
+import com.example.compose.rally.ui.components.AccountRow
+import com.example.compose.rally.ui.components.BillRow
+import com.example.compose.rally.ui.components.RallyAlertDialog
+import com.example.compose.rally.ui.components.RallyDivider
+import com.example.compose.rally.ui.components.formatAmount
+import java.util.Locale
+import androidx.compose.ui.platform.LocalLocale
+
+@Composable
+fun OverviewScreen(
+ onClickSeeAllAccounts: () -> Unit = {},
+ onClickSeeAllBills: () -> Unit = {},
+ onAccountClick: (String) -> Unit = {},
+) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ .semantics { contentDescription = "Overview Screen" }
+ ) {
+ AlertCard()
+ Spacer(Modifier.height(RallyDefaultPadding))
+ AccountsCard(
+ onClickSeeAll = onClickSeeAllAccounts,
+ onAccountClick = onAccountClick
+ )
+ Spacer(Modifier.height(RallyDefaultPadding))
+ BillsCard(
+ onClickSeeAll = onClickSeeAllBills
+ )
+ }
+}
+
+/**
+ * The Alerts card within the Rally Overview screen.
+ */
+@Composable
+private fun AlertCard() {
+ var showDialog by remember { mutableStateOf(false) }
+ val alertMessage = "Heads up, you've used up 90% of your Shopping budget for this month."
+
+ if (showDialog) {
+ RallyAlertDialog(
+ onDismiss = {
+ showDialog = false
+ },
+ bodyText = alertMessage,
+ buttonText = "Dismiss".uppercase(LocalLocale.current.platformLocale)
+ )
+ }
+ Card {
+ Column {
+ AlertHeader {
+ showDialog = true
+ }
+ RallyDivider(
+ modifier = Modifier.padding(start = RallyDefaultPadding, end = RallyDefaultPadding)
+ )
+ AlertItem(alertMessage)
+ }
+ }
+}
+
+@Composable
+private fun AlertHeader(onClickSeeAll: () -> Unit) {
+ Row(
+ modifier = Modifier
+ .padding(RallyDefaultPadding)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = "Alerts",
+ style = MaterialTheme.typography.subtitle2,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ TextButton(
+ onClick = onClickSeeAll,
+ contentPadding = PaddingValues(0.dp),
+ modifier = Modifier.align(Alignment.CenterVertically)
+ ) {
+ Text(
+ text = "SEE ALL",
+ style = MaterialTheme.typography.button,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AlertItem(message: String) {
+ Row(
+ modifier = Modifier
+ .padding(RallyDefaultPadding)
+ // Regard the whole row as one semantics node. This way each row will receive focus as
+ // a whole and the focus bounds will be around the whole row content. The semantics
+ // properties of the descendants will be merged. If we'd use clearAndSetSemantics instead,
+ // we'd have to define the semantics properties explicitly.
+ .semantics(mergeDescendants = true) {},
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier.weight(1f),
+ text = message
+ )
+ IconButton(
+ onClick = {},
+ modifier = Modifier
+ .align(Alignment.Top)
+ .clearAndSetSemantics {}
+ ) {
+ Icon(Icons.AutoMirrored.Filled.Sort, contentDescription = null)
+ }
+ }
+}
+
+/**
+ * Base structure for cards in the Overview screen.
+ */
+@Composable
+private fun OverviewScreenCard(
+ title: String,
+ amount: Float,
+ onClickSeeAll: () -> Unit,
+ values: (T) -> Float,
+ colors: (T) -> Color,
+ data: List,
+ row: @Composable (T) -> Unit
+) {
+ Card {
+ Column {
+ Column(Modifier.padding(RallyDefaultPadding)) {
+ Text(text = title, style = MaterialTheme.typography.subtitle2)
+ val amountText = "$" + formatAmount(
+ amount
+ )
+ Text(text = amountText, style = MaterialTheme.typography.h2)
+ }
+ OverViewDivider(data, values, colors)
+ Column(Modifier.padding(start = 16.dp, top = 4.dp, end = 8.dp)) {
+ data.take(SHOWN_ITEMS).forEach { row(it) }
+ SeeAllButton(
+ modifier = Modifier.clearAndSetSemantics {
+ contentDescription = "All $title"
+ },
+ onClick = onClickSeeAll,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun OverViewDivider(
+ data: List,
+ values: (T) -> Float,
+ colors: (T) -> Color
+) {
+ Row(Modifier.fillMaxWidth()) {
+ data.forEach { item: T ->
+ Spacer(
+ modifier = Modifier
+ .weight(values(item))
+ .height(1.dp)
+ .background(colors(item))
+ )
+ }
+ }
+}
+
+/**
+ * The Accounts card within the Rally Overview screen.
+ */
+@Composable
+private fun AccountsCard(onClickSeeAll: () -> Unit, onAccountClick: (String) -> Unit) {
+ val amount = UserData.accounts.map { account -> account.balance }.sum()
+ OverviewScreenCard(
+ title = stringResource(R.string.accounts),
+ amount = amount,
+ onClickSeeAll = onClickSeeAll,
+ data = UserData.accounts,
+ colors = { it.color },
+ values = { it.balance }
+ ) { account ->
+ AccountRow(
+ modifier = Modifier.clickable { onAccountClick(account.name) },
+ name = account.name,
+ number = account.number,
+ amount = account.balance,
+ color = account.color
+ )
+ }
+}
+
+/**
+ * The Bills card within the Rally Overview screen.
+ */
+@Composable
+private fun BillsCard(onClickSeeAll: () -> Unit) {
+ val amount = UserData.bills.map { bill -> bill.amount }.sum()
+ OverviewScreenCard(
+ title = stringResource(R.string.bills),
+ amount = amount,
+ onClickSeeAll = onClickSeeAll,
+ data = UserData.bills,
+ colors = { it.color },
+ values = { it.amount }
+ ) { bill ->
+ BillRow(
+ name = bill.name,
+ due = bill.due,
+ amount = bill.amount,
+ color = bill.color
+ )
+ }
+}
+
+@Composable
+private fun SeeAllButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
+ TextButton(
+ onClick = onClick,
+ modifier = modifier
+ .height(44.dp)
+ .fillMaxWidth()
+ ) {
+ Text(stringResource(R.string.see_all))
+ }
+}
+
+private val RallyDefaultPadding = 12.dp
+
+private const val SHOWN_ITEMS = 3
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Color.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Color.kt
new file mode 100644
index 000000000..bfa24979a
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Color.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.theme
+
+import androidx.compose.material.darkColors
+import androidx.compose.ui.graphics.Color
+
+val Green500 = Color(0xFF1EB980)
+val DarkBlue900 = Color(0xFF26282F)
+
+// Rally is always dark themed.
+val ColorPalette = darkColors(
+ primary = Green500,
+ surface = DarkBlue900,
+ onSurface = Color.White,
+ background = DarkBlue900,
+ onBackground = Color.White
+)
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt
new file mode 100644
index 000000000..4b690b7ef
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.theme
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Typography
+import androidx.compose.material.darkColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+
+/**
+ * A [MaterialTheme] for Rally.
+ */
+@Composable
+fun RallyTheme(content: @Composable () -> Unit) {
+
+ MaterialTheme(colors = ColorPalette, typography = Typography, content = content)
+}
+
+/**
+ * A theme overlay used for dialogs.
+ */
+@Composable
+fun RallyDialogThemeOverlay(content: @Composable () -> Unit) {
+ // Rally is always dark themed.
+ val dialogColors = darkColors(
+ primary = Color.White,
+ surface = Color.White.copy(alpha = 0.12f).compositeOver(Color.Black),
+ onSurface = Color.White
+ )
+
+ // Copy the current [Typography] and replace some text styles for this theme.
+ val currentTypography = MaterialTheme.typography
+ val dialogTypography = currentTypography.copy(
+ body2 = currentTypography.body1.copy(
+ fontWeight = FontWeight.Normal,
+ fontSize = 20.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 1.sp
+ ),
+ button = currentTypography.button.copy(
+ fontWeight = FontWeight.Bold,
+ letterSpacing = 0.2.em
+ )
+ )
+ MaterialTheme(colors = dialogColors, typography = dialogTypography, content = content)
+}
diff --git a/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Type.kt b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Type.kt
new file mode 100644
index 000000000..45e4f79b1
--- /dev/null
+++ b/NavigationCodelab/app/src/main/java/com/example/compose/rally/ui/theme/Type.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.rally.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import com.example.compose.rally.R
+
+private val EczarFontFamily = FontFamily(
+ Font(R.font.eczar_regular),
+ Font(R.font.eczar_semibold, FontWeight.SemiBold)
+)
+private val RobotoCondensed = FontFamily(
+ Font(R.font.robotocondensed_regular),
+ Font(R.font.robotocondensed_light, FontWeight.Light),
+ Font(R.font.robotocondensed_bold, FontWeight.Bold)
+)
+
+val Typography = Typography(
+ defaultFontFamily = RobotoCondensed,
+ h1 = TextStyle(
+ fontWeight = FontWeight.W100,
+ fontSize = 96.sp,
+ ),
+ h2 = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 44.sp,
+ fontFamily = EczarFontFamily,
+ letterSpacing = 1.5.sp
+ ),
+ h3 = TextStyle(
+ fontWeight = FontWeight.W400,
+ fontSize = 14.sp
+ ),
+ h4 = TextStyle(
+ fontWeight = FontWeight.W700,
+ fontSize = 34.sp
+ ),
+ h5 = TextStyle(
+ fontWeight = FontWeight.W700,
+ fontSize = 24.sp
+ ),
+ h6 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 18.sp,
+ lineHeight = 20.sp,
+ fontFamily = EczarFontFamily,
+ letterSpacing = 3.sp
+ ),
+ subtitle1 = TextStyle(
+ fontWeight = FontWeight.Light,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 3.sp
+ ),
+ subtitle2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ letterSpacing = 0.1.em
+ ),
+ body1 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.1.em
+ ),
+ body2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.em
+ ),
+ button = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.2.em
+ ),
+ caption = TextStyle(
+ fontWeight = FontWeight.W500,
+ fontSize = 12.sp
+ ),
+ overline = TextStyle(
+ fontWeight = FontWeight.W500,
+ fontSize = 10.sp
+ )
+)
diff --git a/NavigationCodelab/app/src/main/res/drawable-v26/ic_launcher_foreground.xml b/NavigationCodelab/app/src/main/res/drawable-v26/ic_launcher_foreground.xml
new file mode 100644
index 000000000..f266f0cfe
--- /dev/null
+++ b/NavigationCodelab/app/src/main/res/drawable-v26/ic_launcher_foreground.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/NavigationCodelab/app/src/main/res/font/eczar_regular.ttf b/NavigationCodelab/app/src/main/res/font/eczar_regular.ttf
new file mode 100644
index 000000000..8eb92d975
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/font/eczar_regular.ttf differ
diff --git a/NavigationCodelab/app/src/main/res/font/eczar_semibold.ttf b/NavigationCodelab/app/src/main/res/font/eczar_semibold.ttf
new file mode 100644
index 000000000..2132b24c0
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/font/eczar_semibold.ttf differ
diff --git a/NavigationCodelab/app/src/main/res/font/robotocondensed_bold.ttf b/NavigationCodelab/app/src/main/res/font/robotocondensed_bold.ttf
new file mode 100644
index 000000000..7fe31289c
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/font/robotocondensed_bold.ttf differ
diff --git a/NavigationCodelab/app/src/main/res/font/robotocondensed_light.ttf b/NavigationCodelab/app/src/main/res/font/robotocondensed_light.ttf
new file mode 100644
index 000000000..43dd8f42b
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/font/robotocondensed_light.ttf differ
diff --git a/NavigationCodelab/app/src/main/res/font/robotocondensed_regular.ttf b/NavigationCodelab/app/src/main/res/font/robotocondensed_regular.ttf
new file mode 100644
index 000000000..62dd61e5d
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/font/robotocondensed_regular.ttf differ
diff --git a/NavigationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/NavigationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..056099c61
--- /dev/null
+++ b/NavigationCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NavigationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/NavigationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..10a5db143
Binary files /dev/null and b/NavigationCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/NavigationCodelab/app/src/main/res/values/colors.xml b/NavigationCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..161f85efe
--- /dev/null
+++ b/NavigationCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+
+
+ #EFEFEF
+ #26282F
+
diff --git a/NavigationCodelab/app/src/main/res/values/strings.xml b/NavigationCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..f5be3bb70
--- /dev/null
+++ b/NavigationCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,26 @@
+
+
+
+ Rally
+ Total
+ Due
+ • • • • •
+ Accounts
+ Bills
+ Sort
+ SEE ALL
+
diff --git a/NavigationCodelab/app/src/main/res/values/styles.xml b/NavigationCodelab/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..8d79c074c
--- /dev/null
+++ b/NavigationCodelab/app/src/main/res/values/styles.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/NavigationCodelab/build.gradle b/NavigationCodelab/build.gradle
new file mode 100644
index 000000000..4478d8cc1
--- /dev/null
+++ b/NavigationCodelab/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:9.2.1"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10"
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.3.10"
+ }
+}
+
+plugins {
+ id 'com.diffplug.spotless' version '8.7.0'
+}
+
+subprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ apply plugin: 'com.diffplug.spotless'
+ spotless {
+ kotlin {
+ target '**/*.kt'
+ targetExclude("$buildDir/**/*.kt")
+ targetExclude('bin/**/*.kt')
+
+ ktlint("0.46.1")
+ licenseHeaderFile rootProject.file('spotless/copyright.kt')
+ }
+ }
+}
diff --git a/NavigationCodelab/debug.keystore b/NavigationCodelab/debug.keystore
new file mode 100644
index 000000000..6024334a4
Binary files /dev/null and b/NavigationCodelab/debug.keystore differ
diff --git a/NavigationCodelab/gradle.properties b/NavigationCodelab/gradle.properties
new file mode 100644
index 000000000..494737fda
--- /dev/null
+++ b/NavigationCodelab/gradle.properties
@@ -0,0 +1,38 @@
+#
+# Copyright 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+
+org.gradle.configureondemand=true
+org.gradle.caching=true
+org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/NavigationCodelab/gradle/wrapper/gradle-wrapper.jar b/NavigationCodelab/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b1b8ef56b
Binary files /dev/null and b/NavigationCodelab/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/NavigationCodelab/gradle/wrapper/gradle-wrapper.properties b/NavigationCodelab/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..eb84db68d
--- /dev/null
+++ b/NavigationCodelab/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+networkTimeout=10000
+retries=0
+retryBackOffMs=500
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/NavigationCodelab/gradlew b/NavigationCodelab/gradlew
new file mode 100755
index 000000000..b9bb139f7
--- /dev/null
+++ b/NavigationCodelab/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/NavigationCodelab/gradlew.bat b/NavigationCodelab/gradlew.bat
new file mode 100644
index 000000000..aa5f10b06
--- /dev/null
+++ b/NavigationCodelab/gradlew.bat
@@ -0,0 +1,82 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+"%COMSPEC%" /c exit 1
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/NavigationCodelab/screenshots/donut.gif b/NavigationCodelab/screenshots/donut.gif
new file mode 100644
index 000000000..81705ede7
Binary files /dev/null and b/NavigationCodelab/screenshots/donut.gif differ
diff --git a/NavigationCodelab/screenshots/rally.gif b/NavigationCodelab/screenshots/rally.gif
new file mode 100644
index 000000000..db55cad76
Binary files /dev/null and b/NavigationCodelab/screenshots/rally.gif differ
diff --git a/NavigationCodelab/settings.gradle b/NavigationCodelab/settings.gradle
new file mode 100644
index 000000000..00489ac87
--- /dev/null
+++ b/NavigationCodelab/settings.gradle
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include ':app'
+rootProject.name = "Rally"
\ No newline at end of file
diff --git a/NavigationCodelab/spotless/copyright.kt b/NavigationCodelab/spotless/copyright.kt
new file mode 100644
index 000000000..806db0fb5
--- /dev/null
+++ b/NavigationCodelab/spotless/copyright.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright $YEAR The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
diff --git a/PerformanceCodelab/.gitignore b/PerformanceCodelab/.gitignore
new file mode 100644
index 000000000..c4ffc6b68
--- /dev/null
+++ b/PerformanceCodelab/.gitignore
@@ -0,0 +1,17 @@
+*.iml
+.gradle
+local.properties
+.idea/*
+!.idea/copyright
+# Keep the code styles.
+!/.idea/codeStyles
+/.idea/codeStyles/*
+!/.idea/codeStyles/Project.xml
+!/.idea/codeStyles/codeStyleConfig.xml
+
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+.kotlin/
diff --git a/PerformanceCodelab/README.md b/PerformanceCodelab/README.md
new file mode 100644
index 000000000..2d915f373
--- /dev/null
+++ b/PerformanceCodelab/README.md
@@ -0,0 +1,26 @@
+# Performance with Jetpack Compose Codelab
+
+The folder contains the source code for the Performance with Jetpack Compose codelab.
+
+## Formatting
+To run spotless use the following command
+```
+./gradlew spotlessApply
+```
+
+## License
+```
+Copyright 2024 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
diff --git a/PerformanceCodelab/app/.gitignore b/PerformanceCodelab/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/PerformanceCodelab/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/PerformanceCodelab/app/build.gradle.kts b/PerformanceCodelab/app/build.gradle.kts
new file mode 100644
index 000000000..471db44e4
--- /dev/null
+++ b/PerformanceCodelab/app/build.gradle.kts
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+ alias(libs.plugins.com.android.application)
+ alias(libs.plugins.androidx.baselineprofile)
+ alias(libs.plugins.compose)
+}
+
+android {
+ namespace = "com.compose.performance"
+ compileSdk = 37
+
+ defaultConfig {
+ applicationId = "com.compose.performance"
+ minSdk = 23
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isProfileable = true
+ isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+composeCompiler {
+ // This settings enables strong-skipping mode for all module in this project.
+ // As an effect, Compose can skip a composable even if it's unstable by comparing it's instance equality (===).
+ // TODO Codelab task: Enable Strong Skipping Mode
+ enableStrongSkippingMode = true
+
+ // TODO Codelab task: Enable Stability Configuration file
+ stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
+}
+
+dependencies {
+ coreLibraryDesugaring(libs.desugar.jdk.libs)
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.core.ktx)
+ implementation(libs.lifecycle.runtime.ktx)
+ implementation(libs.activity.compose)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.ui)
+ implementation(libs.ui.graphics)
+ implementation(libs.ui.tooling.preview)
+ implementation(libs.material3)
+ implementation(libs.androidx.material.icons.core)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.profileinstaller)
+ implementation(libs.androidx.tracing.ktx)
+
+ // TODO Codelab task: Add androidx.runtime-tracing dependency to enable Composition Tracing
+ implementation("androidx.compose.runtime:runtime-tracing:1.11.3")
+
+ implementation(libs.coil.compose)
+ implementation(libs.androidx.media3.exoplayer)
+ implementation(libs.androidx.media3.ui)
+
+ baselineProfile(project(":measure"))
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+ androidTestImplementation(platform(libs.compose.bom))
+ androidTestImplementation(libs.ui.test.junit4)
+
+ debugImplementation(libs.ui.tooling)
+ debugImplementation(libs.ui.test.manifest)
+}
diff --git a/PerformanceCodelab/app/proguard-rules.pro b/PerformanceCodelab/app/proguard-rules.pro
new file mode 100644
index 000000000..68f8882de
--- /dev/null
+++ b/PerformanceCodelab/app/proguard-rules.pro
@@ -0,0 +1,28 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+# Please add these rules to your existing keep rules in order to suppress warnings.
+# This is generated automatically by the Android Gradle plugin.
+-dontwarn kotlinx.serialization.KSerializer
+-dontwarn kotlinx.serialization.Serializable
+
+-dontobfuscate
diff --git a/PerformanceCodelab/app/src/main/AndroidManifest.xml b/PerformanceCodelab/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..4ea455e1b
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/MainActivity.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/MainActivity.kt
new file mode 100644
index 000000000..63a590932
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/MainActivity.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.compose.performance.accelerate.AccelerateHeavyScreen
+import com.compose.performance.phases.PhasesAnimatedShape
+import com.compose.performance.phases.PhasesComposeLogo
+import com.compose.performance.stability.StabilityScreen
+import com.compose.performance.ui.theme.PerformanceWorkshopTheme
+
+class MainActivity : ComponentActivity() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ setContent {
+ val viewModel: PerformanceCodeLabViewModel = viewModel {
+ // Allows us to start the workshop from a various screen
+ // so that we can have a run configuration for each task.
+ val startFromStep = intent.getStringExtra(EXTRA_START_TASK)
+ PerformanceCodeLabViewModel(startFromStep)
+ }
+
+ PerformanceWorkshopTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .semantics { testTagsAsResourceId = true },
+ color = MaterialTheme.colorScheme.background
+ ) {
+ PerformanceCodeLabScreen(
+ selectedPage = viewModel.selectedPage.value,
+ onPageSelected = {
+ viewModel.selectedPage.value = it
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+private class PerformanceCodeLabViewModel(startFromStep: String?) : ViewModel() {
+ val selectedPage = mutableStateOf(TaskScreen.from(startFromStep))
+}
+
+@Composable
+private fun PerformanceCodeLabScreen(
+ selectedPage: TaskScreen,
+ onPageSelected: (selected: TaskScreen) -> Unit
+) {
+ BackHandler(enabled = !selectedPage.isFirst) {
+ onPageSelected(selectedPage.previous())
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .systemBarsPadding()
+ .testTag(selectedPage.id),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ val previousTaskLabel = stringResource(R.string.previous_task)
+ val nextTaskLabel = stringResource(R.string.next_task)
+
+ IconButton(
+ onClick = { onPageSelected(selectedPage.previous()) },
+ modifier = Modifier.semantics {
+ contentDescription = previousTaskLabel
+ }
+ ) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.AutoMirrored.Filled.ArrowBack),
+ contentDescription = null
+ )
+ }
+
+ Text("Task ${selectedPage.ordinal + 1} / ${TaskScreen.entries.lastIndex + 1}")
+
+ IconButton(
+ onClick = { onPageSelected(selectedPage.next()) },
+ modifier = Modifier.semantics { contentDescription = nextTaskLabel }
+ ) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.AutoMirrored.Filled.ArrowForward),
+ contentDescription = null
+ )
+ }
+ }
+ Text(text = selectedPage.label)
+ HorizontalDivider()
+
+ selectedPage.composable()
+ }
+}
+
+const val EXTRA_START_TASK = "EXTRA_START_TASK"
+
+private enum class TaskScreen(
+ val id: String,
+ val label: String,
+ val composable: @Composable () -> Unit
+) {
+ AccelerateHeavyScreen(
+ id = "accelerate_heavy",
+ label = "Accelerate - HeavyScreen",
+ composable = { AccelerateHeavyScreen() }
+ ),
+ PhasesLogo(
+ id = "phases_logo",
+ label = "Phases - Compose Logo",
+ composable = { PhasesComposeLogo() }
+ ),
+ PhasesAnimatedShape(
+ id = "phases_animatedshape",
+ label = "Phases - Animating Shape",
+ composable = { PhasesAnimatedShape() }
+ ),
+ StabilityList(
+ id = "stability_screen",
+ label = "Stability - Stable LazyList",
+ composable = { StabilityScreen() }
+ );
+
+ val isFirst get() = ordinal == 0
+
+ fun previous() = entries[Math.floorMod(ordinal - 1, entries.size)]
+ fun next() = entries[Math.floorMod(ordinal + 1, entries.size)]
+
+ companion object {
+ fun from(extra: String?) = entries.firstOrNull { it.id == extra } ?: entries.first()
+ }
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/RecomposeHighlighter.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/RecomposeHighlighter.kt
new file mode 100644
index 000000000..ed9157b45
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/RecomposeHighlighter.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.compose.recomposehighlighter
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.dp
+import java.util.Objects
+import kotlin.math.min
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * A [Modifier] that draws a border around elements that are recomposing. The border increases in
+ * size and interpolates from red to green as more recompositions occur before a timeout.
+ */
+@Stable
+fun Modifier.recomposeHighlighter(): Modifier = this.then(RecomposeHighlighterElement())
+
+private class RecomposeHighlighterElement : ModifierNodeElement() {
+
+ override fun InspectorInfo.inspectableProperties() {
+ debugInspectorInfo { name = "recomposeHighlighter" }
+ }
+
+ override fun create(): RecomposeHighlighterModifier = RecomposeHighlighterModifier()
+
+ override fun update(node: RecomposeHighlighterModifier) {
+ node.incrementCompositions()
+ }
+
+ // It's never equal, so that every recomposition triggers the update function.
+ override fun equals(other: Any?): Boolean = false
+
+ override fun hashCode(): Int = Objects.hash(this)
+}
+
+private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode {
+
+ private var timerJob: Job? = null
+
+ /**
+ * The total number of compositions that have occurred.
+ */
+ private var totalCompositions: Long = 0
+ set(value) {
+ if (field == value) return
+ restartTimer()
+ field = value
+ invalidateDraw()
+ }
+
+ fun incrementCompositions() {
+ totalCompositions++
+ }
+
+ override fun onAttach() {
+ super.onAttach()
+ restartTimer()
+ }
+
+ override val shouldAutoInvalidate: Boolean = false
+
+ override fun onDetach() {
+ timerJob?.cancel()
+ }
+
+ /**
+ * Start the timeout, and reset everytime there's a recomposition.
+ */
+ private fun restartTimer() {
+ if (!isAttached) return
+
+ timerJob?.cancel()
+ timerJob = coroutineScope.launch {
+ delay(3000)
+ totalCompositions = 0
+ invalidateDraw()
+ }
+ }
+
+ override fun ContentDrawScope.draw() {
+ // Draw actual content.
+ drawContent()
+
+ // Below is to draw the highlight, if necessary. A lot of the logic is copied from Modifier.border
+
+ val hasValidBorderParams = size.minDimension > 0f
+ if (!hasValidBorderParams || totalCompositions <= 0) {
+ return
+ }
+
+ val (color, strokeWidthPx) =
+ when (totalCompositions) {
+ // We need at least one composition to draw, so draw the smallest border
+ // color in blue.
+ 1L -> Color.Blue to 1f
+ // 2 compositions is _probably_ okay.
+ 2L -> Color.Green to 2.dp.toPx()
+ // 3 or more compositions before timeout may indicate an issue. lerp the
+ // color from yellow to red, and continually increase the border size.
+ else -> {
+ lerp(
+ Color.Yellow.copy(alpha = 0.8f),
+ Color.Red.copy(alpha = 0.5f),
+ min(1f, (totalCompositions - 1).toFloat() / 100f)
+ ) to totalCompositions.toInt().dp.toPx()
+ }
+ }
+
+ val halfStroke = strokeWidthPx / 2
+ val topLeft = Offset(halfStroke, halfStroke)
+ val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
+
+ val fillArea = (strokeWidthPx * 2) > size.minDimension
+ val rectTopLeft = if (fillArea) Offset.Zero else topLeft
+ val size = if (fillArea) size else borderSize
+ val style = if (fillArea) Fill else Stroke(strokeWidthPx)
+
+ drawRect(
+ brush = SolidColor(color),
+ topLeft = rectTopLeft,
+ size = size,
+ style = style
+ )
+ }
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/AccelerateHeavyScreen.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/AccelerateHeavyScreen.kt
new file mode 100644
index 000000000..6eb37aad6
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/AccelerateHeavyScreen.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.accelerate
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.tracing.trace
+import coil.compose.AsyncImage
+import com.compose.performance.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+
+@Composable
+fun AccelerateHeavyScreen(
+ modifier: Modifier = Modifier,
+ viewModel: HeavyScreenViewModel = viewModel()
+) {
+ val items by viewModel.items.collectAsState()
+ AccelerateHeavyScreen(items = items, modifier = modifier)
+}
+
+@Composable
+fun AccelerateHeavyScreen(items: List, modifier: Modifier = Modifier) {
+ // TODO: Codelab task: Wrap this with timezone provider
+ ProvideCurrentTimeZone {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ ) {
+ ScreenContent(items = items)
+
+ if (items.isEmpty()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ }
+ }
+}
+
+@Composable
+fun ScreenContent(items: List) {
+ LazyVerticalGrid(
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("list_of_items"),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ columns = GridCells.Fixed(2)
+ ) {
+ items(items) { item -> HeavyItem(item) }
+ }
+}
+
+@Composable
+fun HeavyItem(item: HeavyItem, modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ Text(text = item.description, style = MaterialTheme.typography.headlineMedium)
+ Spacer(modifier = Modifier.height(8.dp))
+ PublishedText(item.published)
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Box {
+ AsyncImage(
+ model = item.url,
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .shadow(8.dp, RoundedCornerShape(12.dp)),
+ contentDescription = stringResource(R.string.performance_dashboard),
+ placeholder = imagePlaceholder(),
+ contentScale = ContentScale.Crop
+ )
+
+ ItemTags(item.tags, Modifier.align(Alignment.BottomCenter))
+ }
+ }
+}
+
+/**
+ * TODO Codelab task: Improve this placeholder_vector.xml loading
+ */
+@Composable
+fun imagePlaceholder() = trace("ImagePlaceholder") {
+ painterResource(R.drawable.placeholder_vector)
+}
+
+/**
+ * TODO Codelab task: Remove the side effect from every item and hoist it to the parent composable
+ */
+@Composable
+fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
+ Text(
+ text = published.format(LocalTimeZone.current),
+ style = MaterialTheme.typography.labelMedium,
+ modifier = modifier
+ )
+}
+
+val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
+
+/**
+ * TODO Codelab task: Write a composition local provider that will always provide current TimeZone
+ */
+@Composable
+fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
+ // TODO Codelab task: move the side effect for TimeZone changes
+ // TODO Codelab task: create a composition local for current TimeZone
+ val context = LocalContext.current
+
+ var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }
+ val scope = rememberCoroutineScope()
+
+ DisposableEffect(Unit) {
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ currentTimeZone = TimeZone.currentSystemDefault()
+ }
+ }
+
+ scope.launch(Dispatchers.IO) {
+ trace("PublishDate.registerReceiver") {
+ context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
+ }
+ }
+
+ onDispose { context.unregisterReceiver(receiver) }
+ }
+
+ CompositionLocalProvider(
+ value = LocalTimeZone provides currentTimeZone,
+ content = content
+ )
+}
+
+/**
+ * TODO Codelab task: remove unnecessary lazy layout
+ */
+@Composable
+fun ItemTags(tags: List, modifier: Modifier = Modifier) {
+ Row(
+ modifier = modifier
+ .padding(4.dp)
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ tags.forEach { ItemTag(it) }
+ }
+}
+
+@Composable
+fun ItemTag(tag: String) = trace("ItemTag") {
+ Text(
+ text = tag,
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onPrimary,
+ fontSize = 10.sp,
+ maxLines = 1,
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp))
+ .padding(2.dp)
+ )
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/HeavyScreenViewModel.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/HeavyScreenViewModel.kt
new file mode 100644
index 000000000..76948f84c
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/accelerate/HeavyScreenViewModel.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.accelerate
+
+import androidx.compose.runtime.Stable
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.tracing.trace
+import kotlin.random.Random
+import kotlin.time.Duration.Companion.minutes
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+class HeavyScreenViewModel : ViewModel() {
+ private var _items = MutableStateFlow(emptyList())
+ val items = _items.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ delay(500)
+ _items.value = generateItems(1000)
+ }
+ }
+}
+
+@Stable
+data class HeavyItem(
+ val id: Int,
+ val published: Instant,
+ val description: String,
+ val url: String,
+ val tags: List
+)
+
+/**
+ * Simple method formatting an [Instant] to a local time with time zone.
+ */
+fun Instant.format(timeZone: TimeZone): String = trace("PublishDate.format") {
+ val dt = toLocalDateTime(timeZone)
+
+ val day = dt.dayOfMonth.toString().padStart(2, '0')
+ val month = dt.monthNumber.toString().padStart(2, '0')
+ val year = dt.year.toString()
+ val hh = dt.hour.toString().padStart(2, '0')
+ val mm = dt.minute.toString().padStart(2, '0')
+
+ "$day.$month.$year - $hh:$mm\n$timeZone"
+}
+
+private fun generateItems(howMany: Int) = List(howMany) { index ->
+ HeavyItem(
+ id = index,
+ published = Clock.System.now() - stableRandom.nextInt(48 * 60).minutes,
+ description = "Item $index",
+ url = poolOfImageUrls[stableRandom.nextInt(poolOfImageUrls.size)],
+ tags = poolOfTags.shuffled(stableRandom).take(4)
+ )
+}
+
+private val poolOfTags =
+ listOf("Lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit")
+
+private val poolOfImageUrls = listOf(
+ "https://picsum.photos/id/57/2448/3264",
+ "https://picsum.photos/id/36/4179/2790",
+ "https://picsum.photos/id/96/4752/3168",
+ "https://picsum.photos/id/180/2400/1600",
+ "https://picsum.photos/id/252/5000/3281"
+)
+
+private val stableRandom = Random(0)
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesAnimatedShape.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesAnimatedShape.kt
new file mode 100644
index 000000000..5310c67c7
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesAnimatedShape.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.phases
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tracing.trace
+import com.compose.performance.ui.theme.Purple80
+
+private val smallSize = 64.dp
+private val bigSize = 200.dp
+
+@Composable
+fun PhasesAnimatedShape() = trace("PhasesAnimatedShape") {
+ var targetSize by remember { mutableStateOf(smallSize) }
+ val size by animateDpAsState(
+ targetValue = targetSize,
+ label = "box_size",
+ animationSpec = spring(Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessVeryLow)
+ )
+
+ Box(Modifier.fillMaxSize()) {
+ MyShape(size = { size }, modifier = Modifier.align(Alignment.Center))
+ Button(
+ onClick = {
+ targetSize = if (targetSize == smallSize) {
+ bigSize
+ } else {
+ smallSize
+ }
+ },
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(horizontal = 32.dp, vertical = 16.dp)
+ ) {
+ Text("Toggle Size")
+ }
+ }
+}
+
+@Composable
+fun MyShape(size: () -> Dp, modifier: Modifier = Modifier) = trace("MyShape") {
+ Box(
+ modifier = modifier
+ .background(color = Purple80, shape = CircleShape)
+ .layout { measurable, _ ->
+ val sizePx = size()
+ .roundToPx()
+ .coerceAtLeast(0)
+
+ val constraints = Constraints.fixed(
+ width = sizePx,
+ height = sizePx,
+ )
+
+ val placeable = measurable.measure(constraints)
+ layout(sizePx, sizePx) {
+ placeable.place(0, 0)
+ }
+ }
+ )
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesComposeLogo.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesComposeLogo.kt
new file mode 100644
index 000000000..c0c478edf
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/phases/PhasesComposeLogo.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.phases
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameMillis
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.util.trace
+import com.compose.performance.R
+
+@Composable
+fun PhasesComposeLogo() = trace("PhasesComposeLogo") {
+ val logo = painterResource(id = R.drawable.compose_logo)
+ var size by remember { mutableStateOf(IntSize.Zero) }
+ val logoPosition by logoPosition_end(size = size, logoSize = logo.intrinsicSize)
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .onPlaced {
+ size = it.size
+ }
+ ) {
+ Image(
+ painter = logo,
+ contentDescription = "logo",
+ modifier = Modifier.offset { logoPosition }
+ )
+ }
+}
+
+@Composable
+fun logoPosition_end(size: IntSize, logoSize: Size): State =
+ produceState(initialValue = IntOffset.Zero, size, logoSize) {
+ if (size == IntSize.Zero) {
+ this.value = IntOffset.Zero
+ return@produceState
+ }
+
+ var xDirection = 1
+ var yDirection = 1
+
+ while (true) {
+ withFrameMillis {
+ value += IntOffset(x = MOVE_SPEED * xDirection, y = MOVE_SPEED * yDirection)
+
+ if (value.x <= 0 || value.x >= size.width - logoSize.width) {
+ xDirection *= -1
+ }
+
+ if (value.y <= 0 || value.y >= size.height - logoSize.height) {
+ yDirection *= -1
+ }
+ }
+ }
+ }
+
+internal const val MOVE_SPEED = 10
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityScreen.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityScreen.kt
new file mode 100644
index 000000000..e22d9fdf6
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityScreen.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.stability
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.tracing.trace
+import com.compose.performance.R
+import com.example.android.compose.recomposehighlighter.recomposeHighlighter
+import java.time.LocalDate
+
+@Composable
+fun StabilityScreen(viewModel: StabilityViewModel = viewModel()) {
+ // TODO Codelab task: Make items stable with strong skipping mode and annotation to prevent recomposing
+ val items by viewModel.items.collectAsState()
+
+ Box {
+ Column {
+ // TODO Codelab task: make LocalDate stable to prevent recomposing with each change
+ LatestChange(viewModel.latestDateChange)
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .recomposeHighlighter(),
+ contentPadding = PaddingValues(bottom = 72.dp)
+ ) {
+ items(items, key = { it.id }) { item ->
+ StabilityItemRow(
+ item = item,
+ onChecked = { viewModel.checkItem(item.id, it) },
+ onRemoveClicked = { viewModel.removeItem(item.id) }
+ )
+ }
+ }
+ }
+
+ FloatingActionButton(
+ onClick = { viewModel.addItem() },
+ modifier = Modifier
+ .padding(16.dp)
+ .align(Alignment.BottomEnd)
+ .testTag("fab")
+ ) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = stringResource(R.string.add_item)
+ )
+ }
+ }
+}
+
+@Composable
+fun LatestChange(today: LocalDate) = trace("latest_change") {
+ Surface(
+ tonalElevation = 4.dp,
+ modifier = Modifier.recomposeHighlighter()
+ ) {
+ Text(
+ text = stringResource(R.string.latest_change_was, today),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+fun StabilityItemRow(
+ item: StabilityItem,
+ modifier: Modifier = Modifier,
+ onChecked: (checked: Boolean) -> Unit,
+ onRemoveClicked: () -> Unit
+) = trace("item_row") {
+ Box(modifier = modifier.recomposeHighlighter()) {
+ val (rowTonalElevation, iconBg) = when (item.type) {
+ StabilityItemType.REFERENCE -> 4.dp to MaterialTheme.colorScheme.primary
+ StabilityItemType.EQUALITY -> 0.dp to MaterialTheme.colorScheme.tertiary
+ }
+
+ ListItem(
+ tonalElevation = rowTonalElevation,
+ headlineContent = { Text(item.name) },
+ leadingContent = {
+ Text(
+ text = item.type.name.take(3),
+ modifier = Modifier
+ .size(40.dp)
+ .background(iconBg, CircleShape)
+ .wrapContentHeight(Alignment.CenterVertically),
+ color = MaterialTheme.colorScheme.onPrimary,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.labelMedium
+ )
+ },
+ overlineContent = {
+ Text("instance ${System.identityHashCode(item)}")
+ },
+ trailingContent = {
+ Row {
+ Checkbox(checked = item.checked, onCheckedChange = onChecked)
+ IconButton(onClick = onRemoveClicked) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.Default.Delete),
+ contentDescription = stringResource(R.string.remove)
+ )
+ }
+ }
+ }
+ )
+
+ if (item.checked) {
+ HorizontalDivider(
+ thickness = 2.dp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.CenterStart)
+ )
+ }
+ }
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityViewModel.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityViewModel.kt
new file mode 100644
index 000000000..76e690192
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/stability/StabilityViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.stability
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import java.time.LocalDate
+import java.time.LocalDateTime
+import kotlin.random.Random
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+// TODO Codelab task: make this class Stable
+@Immutable
+data class StabilityItem(
+ val id: Int,
+ val type: StabilityItemType,
+ val name: String,
+ val checked: Boolean,
+ val created: LocalDateTime
+)
+
+class StabilityViewModel : ViewModel() {
+
+ private var incrementingKey = 0
+
+ // We're using neverEqualPolicy to showcase what the UI logic does for unstable parameters with each time different instance.
+ var latestDateChange by mutableStateOf(LocalDate.now(), neverEqualPolicy())
+ private set
+
+ private val _items = MutableStateFlow>(emptyList())
+
+ val items = _items
+ .map { simulateNewInstances(it) }
+ .onEach { latestDateChange = LocalDate.now() }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
+
+ init {
+ repeat(3) {
+ addItem()
+ }
+ }
+
+ fun addItem() {
+ _items.value += StabilityItem(
+ id = incrementingKey++,
+ type = StabilityItemType.fromId(incrementingKey),
+ name = sampleData.random(stableRandom),
+ checked = false,
+ created = LocalDateTime.now()
+ )
+ }
+
+ fun removeItem(id: Int) {
+ _items.value = _items.value.filterNot { it.id == id }
+ }
+
+ fun checkItem(id: Int, checked: Boolean) {
+ val items = _items.value.toMutableList()
+ val index = items.indexOfFirst { it.id == id }
+ if (index < 0) return
+ items[index] = items[index].copy(checked = checked)
+ _items.value = items
+ }
+}
+
+enum class StabilityItemType {
+ REFERENCE,
+ EQUALITY;
+
+ companion object {
+ fun fromId(id: Int) = if (id % 2 == 0) REFERENCE else EQUALITY
+ }
+}
+
+/**
+ * We have this method to simulate when a data source provides new instance when mapping a class.
+ * This can occur for remote services like Firebase, REST Api, or local services like Room or sqlDelight.
+ */
+private fun simulateNewInstances(items: List): List = items.map {
+ if (it.type == StabilityItemType.REFERENCE) {
+ // For the reference types, we recreate the class to be always a new instance
+ it.copy()
+ } else {
+ it
+ }
+}
+
+private val sampleData = LoremIpsum(500)
+ .values.first()
+ .split(" ")
+ .map { it.trim('.', ',', '\n') }
+
+private val stableRandom = Random(0)
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Color.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Color.kt
new file mode 100644
index 000000000..af75d14b3
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Color.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Theme.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Theme.kt
new file mode 100644
index 000000000..603cd0533
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Theme.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun PerformanceWorkshopTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Type.kt b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Type.kt
new file mode 100644
index 000000000..abc4f438e
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/java/com/compose/performance/ui/theme/Type.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.compose.performance.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
diff --git a/PerformanceCodelab/app/src/main/res/drawable/compose_logo.xml b/PerformanceCodelab/app/src/main/res/drawable/compose_logo.xml
new file mode 100644
index 000000000..b4a6dd44e
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/drawable/compose_logo.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_background.xml b/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml b/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PerformanceCodelab/app/src/main/res/drawable/placeholder.jpg b/PerformanceCodelab/app/src/main/res/drawable/placeholder.jpg
new file mode 100644
index 000000000..609a047b9
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/drawable/placeholder.jpg differ
diff --git a/PerformanceCodelab/app/src/main/res/drawable/placeholder_small.webp b/PerformanceCodelab/app/src/main/res/drawable/placeholder_small.webp
new file mode 100644
index 000000000..b867d3f70
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/drawable/placeholder_small.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/drawable/placeholder_vector.xml b/PerformanceCodelab/app/src/main/res/drawable/placeholder_vector.xml
new file mode 100644
index 000000000..1b2507402
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/drawable/placeholder_vector.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/PerformanceCodelab/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/PerformanceCodelab/app/src/main/res/values/colors.xml b/PerformanceCodelab/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f8c6127d3
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/PerformanceCodelab/app/src/main/res/values/strings.xml b/PerformanceCodelab/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..51cd08b26
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ compose_performance_workshop
+ Add Item
+ Latest change was %1$s
+ Remove
+ Performance dashboard
+ Previous task
+ Next task
+
diff --git a/PerformanceCodelab/app/src/main/res/values/themes.xml b/PerformanceCodelab/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..320a1a51e
--- /dev/null
+++ b/PerformanceCodelab/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/PerformanceCodelab/app/src/release/generated/baselineProfiles/baseline-prof.txt b/PerformanceCodelab/app/src/release/generated/baselineProfiles/baseline-prof.txt
new file mode 100644
index 000000000..b6879f10d
--- /dev/null
+++ b/PerformanceCodelab/app/src/release/generated/baselineProfiles/baseline-prof.txt
@@ -0,0 +1,18553 @@
+Landroidx/activity/Cancellable;
+Landroidx/activity/ComponentActivity;
+HSPLandroidx/activity/ComponentActivity;->()V
+HSPLandroidx/activity/ComponentActivity;->access$100(Landroidx/activity/ComponentActivity;)Landroidx/activity/OnBackPressedDispatcher;
+HSPLandroidx/activity/ComponentActivity;->addOnContextAvailableListener(Landroidx/activity/contextaware/OnContextAvailableListener;)V
+HSPLandroidx/activity/ComponentActivity;->createFullyDrawnExecutor()Landroidx/activity/ComponentActivity$ReportFullyDrawnExecutor;
+HSPLandroidx/activity/ComponentActivity;->ensureViewModelStore()V
+HSPLandroidx/activity/ComponentActivity;->getDefaultViewModelCreationExtras()Landroidx/lifecycle/viewmodel/CreationExtras;
+HSPLandroidx/activity/ComponentActivity;->getDefaultViewModelProviderFactory()Landroidx/lifecycle/ViewModelProvider$Factory;
+HPLandroidx/activity/ComponentActivity;->getLifecycle()Landroidx/lifecycle/Lifecycle;
+HSPLandroidx/activity/ComponentActivity;->getOnBackPressedDispatcher()Landroidx/activity/OnBackPressedDispatcher;
+HSPLandroidx/activity/ComponentActivity;->getSavedStateRegistry()Landroidx/savedstate/SavedStateRegistry;
+HSPLandroidx/activity/ComponentActivity;->getViewModelStore()Landroidx/lifecycle/ViewModelStore;
+HSPLandroidx/activity/ComponentActivity;->initializeViewTreeOwners()V
+HSPLandroidx/activity/ComponentActivity;->lambda$new$2$androidx-activity-ComponentActivity(Landroid/content/Context;)V
+HSPLandroidx/activity/ComponentActivity;->onCreate(Landroid/os/Bundle;)V
+HSPLandroidx/activity/ComponentActivity;->setContentView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;->(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;->(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;->(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;->(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda3;->onContextAvailable(Landroid/content/Context;)V
+Landroidx/activity/ComponentActivity$1;
+HSPLandroidx/activity/ComponentActivity$1;->(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/ComponentActivity$2;
+HSPLandroidx/activity/ComponentActivity$2;->(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$2;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+Landroidx/activity/ComponentActivity$3;
+HSPLandroidx/activity/ComponentActivity$3;->(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$3;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+Landroidx/activity/ComponentActivity$4;
+HSPLandroidx/activity/ComponentActivity$4;->(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$4;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+Landroidx/activity/ComponentActivity$5;
+HSPLandroidx/activity/ComponentActivity$5;->(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/ComponentActivity$6;
+HSPLandroidx/activity/ComponentActivity$6;->(Landroidx/activity/ComponentActivity;)V
+HSPLandroidx/activity/ComponentActivity$6;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+PLandroidx/activity/ComponentActivity$Api19Impl;->cancelPendingInputEvents(Landroid/view/View;)V
+Landroidx/activity/ComponentActivity$Api33Impl;
+HSPLandroidx/activity/ComponentActivity$Api33Impl;->getOnBackInvokedDispatcher(Landroid/app/Activity;)Landroid/window/OnBackInvokedDispatcher;
+Landroidx/activity/ComponentActivity$NonConfigurationInstances;
+Landroidx/activity/ComponentActivity$ReportFullyDrawnExecutor;
+Landroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;
+HSPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->(Landroidx/activity/ComponentActivity;)V
+PLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->activityDestroyed()V
+HPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->onDraw()V
+PLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->run()V
+HSPLandroidx/activity/ComponentActivity$ReportFullyDrawnExecutorApi16Impl;->viewCreated(Landroid/view/View;)V
+Landroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$1()Landroid/graphics/BlendMode;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/graphics/Insets;)I
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/view/Window;Z)V
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$1(Landroid/view/autofill/AutofillManager;Landroid/view/autofill/AutofillManager$AutofillCallback;)V
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$2(Landroid/graphics/Insets;)I
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$3(Landroid/graphics/Insets;)I
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$4()Landroid/graphics/BlendMode;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m$7()Landroid/graphics/BlendMode;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m()Ljava/lang/Class;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/content/Context;Ljava/lang/Class;)Ljava/lang/Object;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/content/res/Resources;ILandroid/content/res/Resources$Theme;)I
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/Insets;)I
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/graphics/drawable/RippleDrawable;I)V
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/os/LocaleList;I)Ljava/util/Locale;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/View;I)V
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/Window;Z)V
+PLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Landroid/view/autofill/AutofillManager;Landroid/view/autofill/AutofillManager$AutofillCallback;)V
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Ljava/lang/Object;)Landroid/view/autofill/AutofillManager$AutofillCallback;
+HSPLandroidx/activity/ComponentDialog$$ExternalSyntheticApiModelOutline0;->m(Ljava/lang/Object;)Landroid/view/autofill/AutofillManager;
+Landroidx/activity/EdgeToEdge;
+HSPLandroidx/activity/EdgeToEdge;->()V
+HSPLandroidx/activity/EdgeToEdge;->enable$default(Landroidx/activity/ComponentActivity;Landroidx/activity/SystemBarStyle;Landroidx/activity/SystemBarStyle;ILjava/lang/Object;)V
+HSPLandroidx/activity/EdgeToEdge;->enable(Landroidx/activity/ComponentActivity;Landroidx/activity/SystemBarStyle;Landroidx/activity/SystemBarStyle;)V
+Landroidx/activity/EdgeToEdgeApi29;
+HSPLandroidx/activity/EdgeToEdgeApi29;->()V
+HSPLandroidx/activity/EdgeToEdgeApi29;->setUp(Landroidx/activity/SystemBarStyle;Landroidx/activity/SystemBarStyle;Landroid/view/Window;Landroid/view/View;ZZ)V
+Landroidx/activity/EdgeToEdgeImpl;
+Landroidx/activity/FullyDrawnReporter;
+HSPLandroidx/activity/FullyDrawnReporter;->(Ljava/util/concurrent/Executor;Lkotlin/jvm/functions/Function0;)V
+Landroidx/activity/FullyDrawnReporter$$ExternalSyntheticLambda0;
+HSPLandroidx/activity/FullyDrawnReporter$$ExternalSyntheticLambda0;->(Landroidx/activity/FullyDrawnReporter;)V
+Landroidx/activity/FullyDrawnReporterOwner;
+Landroidx/activity/OnBackPressedCallback;
+HSPLandroidx/activity/OnBackPressedCallback;->(Z)V
+HSPLandroidx/activity/OnBackPressedCallback;->addCancellable(Landroidx/activity/Cancellable;)V
+PLandroidx/activity/OnBackPressedCallback;->getEnabledChangedCallback$activity_release()Lkotlin/jvm/functions/Function0;
+HSPLandroidx/activity/OnBackPressedCallback;->isEnabled()Z
+PLandroidx/activity/OnBackPressedCallback;->remove()V
+PLandroidx/activity/OnBackPressedCallback;->removeCancellable(Landroidx/activity/Cancellable;)V
+HSPLandroidx/activity/OnBackPressedCallback;->setEnabled(Z)V
+HSPLandroidx/activity/OnBackPressedCallback;->setEnabledChangedCallback$activity_release(Lkotlin/jvm/functions/Function0;)V
+Landroidx/activity/OnBackPressedDispatcher;
+HSPLandroidx/activity/OnBackPressedDispatcher;->(Ljava/lang/Runnable;)V
+HSPLandroidx/activity/OnBackPressedDispatcher;->(Ljava/lang/Runnable;Landroidx/core/util/Consumer;)V
+PLandroidx/activity/OnBackPressedDispatcher;->access$getInProgressCallback$p(Landroidx/activity/OnBackPressedDispatcher;)Landroidx/activity/OnBackPressedCallback;
+PLandroidx/activity/OnBackPressedDispatcher;->access$getOnBackPressedCallbacks$p(Landroidx/activity/OnBackPressedDispatcher;)Lkotlin/collections/ArrayDeque;
+HSPLandroidx/activity/OnBackPressedDispatcher;->access$updateEnabledCallbacks(Landroidx/activity/OnBackPressedDispatcher;)V
+HSPLandroidx/activity/OnBackPressedDispatcher;->addCallback(Landroidx/lifecycle/LifecycleOwner;Landroidx/activity/OnBackPressedCallback;)V
+HSPLandroidx/activity/OnBackPressedDispatcher;->addCancellableCallback$activity_release(Landroidx/activity/OnBackPressedCallback;)Landroidx/activity/Cancellable;
+HSPLandroidx/activity/OnBackPressedDispatcher;->setOnBackInvokedDispatcher(Landroid/window/OnBackInvokedDispatcher;)V
+HSPLandroidx/activity/OnBackPressedDispatcher;->updateBackInvokedCallbackState(Z)V
+HPLandroidx/activity/OnBackPressedDispatcher;->updateEnabledCallbacks()V
+Landroidx/activity/OnBackPressedDispatcher$1;
+HSPLandroidx/activity/OnBackPressedDispatcher$1;->(Landroidx/activity/OnBackPressedDispatcher;)V
+Landroidx/activity/OnBackPressedDispatcher$2;
+HSPLandroidx/activity/OnBackPressedDispatcher$2;->(Landroidx/activity/OnBackPressedDispatcher;)V
+Landroidx/activity/OnBackPressedDispatcher$3;
+HSPLandroidx/activity/OnBackPressedDispatcher$3;->(Landroidx/activity/OnBackPressedDispatcher;)V
+Landroidx/activity/OnBackPressedDispatcher$4;
+HSPLandroidx/activity/OnBackPressedDispatcher$4;->(Landroidx/activity/OnBackPressedDispatcher;)V
+Landroidx/activity/OnBackPressedDispatcher$Api33Impl;
+HSPLandroidx/activity/OnBackPressedDispatcher$Api33Impl;->()V
+HSPLandroidx/activity/OnBackPressedDispatcher$Api33Impl;->()V
+HSPLandroidx/activity/OnBackPressedDispatcher$Api33Impl;->registerOnBackInvokedCallback(Ljava/lang/Object;ILjava/lang/Object;)V
+PLandroidx/activity/OnBackPressedDispatcher$Api33Impl;->unregisterOnBackInvokedCallback(Ljava/lang/Object;Ljava/lang/Object;)V
+Landroidx/activity/OnBackPressedDispatcher$Api34Impl;
+HSPLandroidx/activity/OnBackPressedDispatcher$Api34Impl;->()V
+HSPLandroidx/activity/OnBackPressedDispatcher$Api34Impl;->()V
+HSPLandroidx/activity/OnBackPressedDispatcher$Api34Impl;->createOnBackAnimationCallback(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Landroid/window/OnBackInvokedCallback;
+Landroidx/activity/OnBackPressedDispatcher$Api34Impl$createOnBackAnimationCallback$1;
+HSPLandroidx/activity/OnBackPressedDispatcher$Api34Impl$createOnBackAnimationCallback$1;->(Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V
+Landroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;
+HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->(Landroidx/activity/OnBackPressedDispatcher;Landroidx/lifecycle/Lifecycle;Landroidx/activity/OnBackPressedCallback;)V
+PLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->cancel()V
+HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
+Landroidx/activity/OnBackPressedDispatcher$OnBackPressedCancellable;
+HSPLandroidx/activity/OnBackPressedDispatcher$OnBackPressedCancellable;->(Landroidx/activity/OnBackPressedDispatcher;Landroidx/activity/OnBackPressedCallback;)V
+PLandroidx/activity/OnBackPressedDispatcher$OnBackPressedCancellable;->cancel()V
+Landroidx/activity/OnBackPressedDispatcher$addCallback$1;
+HSPLandroidx/activity/OnBackPressedDispatcher$addCallback$1;->(Ljava/lang/Object;)V
+HSPLandroidx/activity/OnBackPressedDispatcher$addCallback$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/activity/OnBackPressedDispatcher$addCallback$1;->invoke()V
+Landroidx/activity/OnBackPressedDispatcher$addCancellableCallback$1;
+HSPLandroidx/activity/OnBackPressedDispatcher$addCancellableCallback$1;->(Ljava/lang/Object;)V
+HSPLandroidx/activity/OnBackPressedDispatcher$addCancellableCallback$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/activity/OnBackPressedDispatcher$addCancellableCallback$1;->invoke()V
+Landroidx/activity/OnBackPressedDispatcherOwner;
+Landroidx/activity/R$id;
+Landroidx/activity/SystemBarStyle;
+HSPLandroidx/activity/SystemBarStyle;->()V
+HSPLandroidx/activity/SystemBarStyle;->(IIILkotlin/jvm/functions/Function1;)V
+HSPLandroidx/activity/SystemBarStyle;->(IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/activity/SystemBarStyle;->getDetectDarkMode$activity_release()Lkotlin/jvm/functions/Function1;
+HSPLandroidx/activity/SystemBarStyle;->getNightMode$activity_release()I
+HSPLandroidx/activity/SystemBarStyle;->getScrimWithEnforcedContrast$activity_release(Z)I
+Landroidx/activity/SystemBarStyle$Companion;
+HSPLandroidx/activity/SystemBarStyle$Companion;->()V
+HSPLandroidx/activity/SystemBarStyle$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/activity/SystemBarStyle$Companion;->auto$default(Landroidx/activity/SystemBarStyle$Companion;IILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Landroidx/activity/SystemBarStyle;
+HSPLandroidx/activity/SystemBarStyle$Companion;->auto(IILkotlin/jvm/functions/Function1;)Landroidx/activity/SystemBarStyle;
+Landroidx/activity/SystemBarStyle$Companion$auto$1;
+HSPLandroidx/activity/SystemBarStyle$Companion$auto$1;->()V
+HSPLandroidx/activity/SystemBarStyle$Companion$auto$1;->()V
+HSPLandroidx/activity/SystemBarStyle$Companion$auto$1;->invoke(Landroid/content/res/Resources;)Ljava/lang/Boolean;
+HSPLandroidx/activity/SystemBarStyle$Companion$auto$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+Landroidx/activity/ViewTreeFullyDrawnReporterOwner;
+HSPLandroidx/activity/ViewTreeFullyDrawnReporterOwner;->set(Landroid/view/View;Landroidx/activity/FullyDrawnReporterOwner;)V
+Landroidx/activity/ViewTreeOnBackPressedDispatcherOwner;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner;->get(Landroid/view/View;)Landroidx/activity/OnBackPressedDispatcherOwner;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner;->set(Landroid/view/View;Landroidx/activity/OnBackPressedDispatcherOwner;)V
+Landroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$1;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$1;->()V
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$1;->()V
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$1;->invoke(Landroid/view/View;)Landroid/view/View;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+Landroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$2;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$2;->()V
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$2;->()V
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$2;->invoke(Landroid/view/View;)Landroidx/activity/OnBackPressedDispatcherOwner;
+HSPLandroidx/activity/ViewTreeOnBackPressedDispatcherOwner$findViewTreeOnBackPressedDispatcherOwner$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+Landroidx/activity/compose/BackHandlerKt;
+HPLandroidx/activity/compose/BackHandlerKt;->BackHandler(ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V
+Landroidx/activity/compose/BackHandlerKt$BackHandler$1$1;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$1$1;->(Landroidx/activity/compose/BackHandlerKt$BackHandler$backCallback$1$1;Z)V
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$1$1;->invoke()Ljava/lang/Object;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$1$1;->invoke()V
+Landroidx/activity/compose/BackHandlerKt$BackHandler$2;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$2;->(Landroidx/activity/OnBackPressedDispatcher;Landroidx/lifecycle/LifecycleOwner;Landroidx/activity/compose/BackHandlerKt$BackHandler$backCallback$1$1;)V
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$2;->invoke(Landroidx/compose/runtime/DisposableEffectScope;)Landroidx/compose/runtime/DisposableEffectResult;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
+Landroidx/activity/compose/BackHandlerKt$BackHandler$2$invoke$$inlined$onDispose$1;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$2$invoke$$inlined$onDispose$1;->(Landroidx/activity/compose/BackHandlerKt$BackHandler$backCallback$1$1;)V
+PLandroidx/activity/compose/BackHandlerKt$BackHandler$2$invoke$$inlined$onDispose$1;->dispose()V
+Landroidx/activity/compose/BackHandlerKt$BackHandler$backCallback$1$1;
+HSPLandroidx/activity/compose/BackHandlerKt$BackHandler$backCallback$1$1;->(ZLandroidx/compose/runtime/State;)V
+Landroidx/activity/compose/ComponentActivityKt;
+HSPLandroidx/activity/compose/ComponentActivityKt;->()V
+HSPLandroidx/activity/compose/ComponentActivityKt;->setContent$default(Landroidx/activity/ComponentActivity;Landroidx/compose/runtime/CompositionContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
+HSPLandroidx/activity/compose/ComponentActivityKt;->setContent(Landroidx/activity/ComponentActivity;Landroidx/compose/runtime/CompositionContext;Lkotlin/jvm/functions/Function2;)V
+HSPLandroidx/activity/compose/ComponentActivityKt;->setOwners(Landroidx/activity/ComponentActivity;)V
+Landroidx/activity/compose/LocalOnBackPressedDispatcherOwner;
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner;->()V
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner;->()V
+HPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner;->getCurrent(Landroidx/compose/runtime/Composer;I)Landroidx/activity/OnBackPressedDispatcherOwner;
+Landroidx/activity/compose/LocalOnBackPressedDispatcherOwner$LocalOnBackPressedDispatcherOwner$1;
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner$LocalOnBackPressedDispatcherOwner$1;->()V
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner$LocalOnBackPressedDispatcherOwner$1;->()V
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner$LocalOnBackPressedDispatcherOwner$1;->invoke()Landroidx/activity/OnBackPressedDispatcherOwner;
+HSPLandroidx/activity/compose/LocalOnBackPressedDispatcherOwner$LocalOnBackPressedDispatcherOwner$1;->invoke()Ljava/lang/Object;
+Landroidx/activity/contextaware/ContextAware;
+Landroidx/activity/contextaware/ContextAwareHelper;
+HSPLandroidx/activity/contextaware/ContextAwareHelper;->()V
+HSPLandroidx/activity/contextaware/ContextAwareHelper;->addOnContextAvailableListener(Landroidx/activity/contextaware/OnContextAvailableListener;)V
+PLandroidx/activity/contextaware/ContextAwareHelper;->clearAvailableContext()V
+HSPLandroidx/activity/contextaware/ContextAwareHelper;->dispatchOnContextAvailable(Landroid/content/Context;)V
+Landroidx/activity/contextaware/OnContextAvailableListener;
+Landroidx/activity/result/ActivityResultCaller;
+Landroidx/activity/result/ActivityResultRegistry;
+HSPLandroidx/activity/result/ActivityResultRegistry;->()V
+Landroidx/activity/result/ActivityResultRegistryOwner;
+Landroidx/arch/core/executor/ArchTaskExecutor;
+HSPLandroidx/arch/core/executor/ArchTaskExecutor;->()V
+HSPLandroidx/arch/core/executor/ArchTaskExecutor;->()V
+HPLandroidx/arch/core/executor/ArchTaskExecutor;->getInstance()Landroidx/arch/core/executor/ArchTaskExecutor;
+HSPLandroidx/arch/core/executor/ArchTaskExecutor;->isMainThread()Z
+Landroidx/arch/core/executor/ArchTaskExecutor$$ExternalSyntheticLambda0;
+HSPLandroidx/arch/core/executor/ArchTaskExecutor$$ExternalSyntheticLambda0;->()V
+Landroidx/arch/core/executor/ArchTaskExecutor$$ExternalSyntheticLambda1;
+HSPLandroidx/arch/core/executor/ArchTaskExecutor$$ExternalSyntheticLambda1;->()V
+Landroidx/arch/core/executor/DefaultTaskExecutor;
+HSPLandroidx/arch/core/executor/DefaultTaskExecutor;->()V
+HPLandroidx/arch/core/executor/DefaultTaskExecutor;->isMainThread()Z
+Landroidx/arch/core/executor/DefaultTaskExecutor$1;
+HSPLandroidx/arch/core/executor/DefaultTaskExecutor$1;->(Landroidx/arch/core/executor/DefaultTaskExecutor;)V
+Landroidx/arch/core/executor/TaskExecutor;
+HSPLandroidx/arch/core/executor/TaskExecutor;->()V
+Landroidx/arch/core/internal/FastSafeIterableMap;
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->()V
+HPLandroidx/arch/core/internal/FastSafeIterableMap;->ceil(Ljava/lang/Object;)Ljava/util/Map$Entry;
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->contains(Ljava/lang/Object;)Z
+HPLandroidx/arch/core/internal/FastSafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/FastSafeIterableMap;->remove(Ljava/lang/Object;)Ljava/lang/Object;
+Landroidx/arch/core/internal/SafeIterableMap;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->()V
+PLandroidx/arch/core/internal/SafeIterableMap;->descendingIterator()Ljava/util/Iterator;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->eldest()Ljava/util/Map$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->iterator()Ljava/util/Iterator;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->iteratorWithAdditions()Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->newest()Ljava/util/Map$Entry;
+HPLandroidx/arch/core/internal/SafeIterableMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HPLandroidx/arch/core/internal/SafeIterableMap;->remove(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap;->size()I
+Landroidx/arch/core/internal/SafeIterableMap$AscendingIterator;
+HSPLandroidx/arch/core/internal/SafeIterableMap$AscendingIterator;->(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+PLandroidx/arch/core/internal/SafeIterableMap$DescendingIterator;->(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+PLandroidx/arch/core/internal/SafeIterableMap$DescendingIterator;->forward(Landroidx/arch/core/internal/SafeIterableMap$Entry;)Landroidx/arch/core/internal/SafeIterableMap$Entry;
+Landroidx/arch/core/internal/SafeIterableMap$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->(Ljava/lang/Object;Ljava/lang/Object;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getKey()Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getValue()Ljava/lang/Object;
+Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->hasNext()Z
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->next()Ljava/lang/Object;
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->next()Ljava/util/Map$Entry;
+HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+Landroidx/arch/core/internal/SafeIterableMap$ListIterator;
+HSPLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+HSPLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->hasNext()Z
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->next()Ljava/lang/Object;
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->next()Ljava/util/Map$Entry;
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->nextNode()Landroidx/arch/core/internal/SafeIterableMap$Entry;
+PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V
+Landroidx/arch/core/internal/SafeIterableMap$SupportRemove;
+HSPLandroidx/arch/core/internal/SafeIterableMap$SupportRemove;->()V
+Landroidx/collection/ArrayMap;
+HSPLandroidx/collection/ArrayMap;->()V
+Landroidx/collection/ArraySet;
+HSPLandroidx/collection/ArraySet;->()V
+HSPLandroidx/collection/ArraySet;->(I)V
+HSPLandroidx/collection/ArraySet;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/ArraySet;->add(Ljava/lang/Object;)Z
+HSPLandroidx/collection/ArraySet;->clear()V
+HSPLandroidx/collection/ArraySet;->getArray$collection()[Ljava/lang/Object;
+HSPLandroidx/collection/ArraySet;->getHashes$collection()[I
+HSPLandroidx/collection/ArraySet;->get_size$collection()I
+HSPLandroidx/collection/ArraySet;->setArray$collection([Ljava/lang/Object;)V
+HSPLandroidx/collection/ArraySet;->setHashes$collection([I)V
+HSPLandroidx/collection/ArraySet;->set_size$collection(I)V
+HSPLandroidx/collection/ArraySet;->toArray()[Ljava/lang/Object;
+Landroidx/collection/ArraySetKt;
+HSPLandroidx/collection/ArraySetKt;->allocArrays(Landroidx/collection/ArraySet;I)V
+HSPLandroidx/collection/ArraySetKt;->indexOf(Landroidx/collection/ArraySet;Ljava/lang/Object;I)I
+Landroidx/collection/IntIntMap;
+HSPLandroidx/collection/IntIntMap;->()V
+HSPLandroidx/collection/IntIntMap;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/IntIntMap;->getCapacity()I
+Landroidx/collection/IntObjectMap;
+HSPLandroidx/collection/IntObjectMap;->()V
+HSPLandroidx/collection/IntObjectMap;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/IntObjectMap;->getCapacity()I
+Landroidx/collection/IntObjectMapKt;
+HSPLandroidx/collection/IntObjectMapKt;->()V
+HSPLandroidx/collection/IntObjectMapKt;->mutableIntObjectMapOf()Landroidx/collection/MutableIntObjectMap;
+Landroidx/collection/IntSet;
+HSPLandroidx/collection/IntSet;->()V
+HSPLandroidx/collection/IntSet;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/IntSet;->getCapacity()I
+Landroidx/collection/IntSetKt;
+HSPLandroidx/collection/IntSetKt;->()V
+HPLandroidx/collection/IntSetKt;->getEmptyIntArray()[I
+Landroidx/collection/LongSparseArray;
+HSPLandroidx/collection/LongSparseArray;->(I)V
+HSPLandroidx/collection/LongSparseArray;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/LongSparseArray;->clear()V
+HSPLandroidx/collection/LongSparseArray;->containsKey(J)Z
+HSPLandroidx/collection/LongSparseArray;->get(J)Ljava/lang/Object;
+HSPLandroidx/collection/LongSparseArray;->indexOfKey(J)I
+HSPLandroidx/collection/LongSparseArray;->isEmpty()Z
+HSPLandroidx/collection/LongSparseArray;->keyAt(I)J
+HPLandroidx/collection/LongSparseArray;->put(JLjava/lang/Object;)V
+HSPLandroidx/collection/LongSparseArray;->remove(J)V
+HPLandroidx/collection/LongSparseArray;->size()I
+HPLandroidx/collection/LongSparseArray;->valueAt(I)Ljava/lang/Object;
+Landroidx/collection/LongSparseArrayKt;
+HSPLandroidx/collection/LongSparseArrayKt;->()V
+HSPLandroidx/collection/LongSparseArrayKt;->access$getDELETED$p()Ljava/lang/Object;
+Landroidx/collection/LruCache;
+HSPLandroidx/collection/LruCache;->(I)V
+HSPLandroidx/collection/LruCache;->create(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/collection/LruCache;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/collection/LruCache;->maxSize()I
+HSPLandroidx/collection/LruCache;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/collection/LruCache;->safeSizeOf(Ljava/lang/Object;Ljava/lang/Object;)I
+HSPLandroidx/collection/LruCache;->trimToSize(I)V
+Landroidx/collection/MutableIntIntMap;
+HSPLandroidx/collection/MutableIntIntMap;->(I)V
+HSPLandroidx/collection/MutableIntIntMap;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/MutableIntIntMap;->findFirstAvailableSlot(I)I
+HSPLandroidx/collection/MutableIntIntMap;->findInsertIndex(I)I
+HSPLandroidx/collection/MutableIntIntMap;->initializeGrowth()V
+HSPLandroidx/collection/MutableIntIntMap;->initializeMetadata(I)V
+HSPLandroidx/collection/MutableIntIntMap;->initializeStorage(I)V
+HSPLandroidx/collection/MutableIntIntMap;->set(II)V
+Landroidx/collection/MutableIntObjectMap;
+HSPLandroidx/collection/MutableIntObjectMap;->(I)V
+HSPLandroidx/collection/MutableIntObjectMap;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/MutableIntObjectMap;->findAbsoluteInsertIndex(I)I
+HSPLandroidx/collection/MutableIntObjectMap;->findFirstAvailableSlot(I)I
+HSPLandroidx/collection/MutableIntObjectMap;->initializeGrowth()V
+HSPLandroidx/collection/MutableIntObjectMap;->initializeMetadata(I)V
+HSPLandroidx/collection/MutableIntObjectMap;->initializeStorage(I)V
+HSPLandroidx/collection/MutableIntObjectMap;->set(ILjava/lang/Object;)V
+Landroidx/collection/MutableIntSet;
+HSPLandroidx/collection/MutableIntSet;->(I)V
+HSPLandroidx/collection/MutableIntSet;->initializeGrowth()V
+HSPLandroidx/collection/MutableIntSet;->initializeMetadata(I)V
+HSPLandroidx/collection/MutableIntSet;->initializeStorage(I)V
+Landroidx/collection/MutableObjectIntMap;
+HPLandroidx/collection/MutableObjectIntMap;->(I)V
+HPLandroidx/collection/MutableObjectIntMap;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/MutableObjectIntMap;->adjustStorage()V
+HPLandroidx/collection/MutableObjectIntMap;->findFirstAvailableSlot(I)I
+HPLandroidx/collection/MutableObjectIntMap;->findIndex(Ljava/lang/Object;)I
+HPLandroidx/collection/MutableObjectIntMap;->initializeGrowth()V
+HPLandroidx/collection/MutableObjectIntMap;->initializeMetadata(I)V
+HPLandroidx/collection/MutableObjectIntMap;->initializeStorage(I)V
+HPLandroidx/collection/MutableObjectIntMap;->put(Ljava/lang/Object;II)I
+HSPLandroidx/collection/MutableObjectIntMap;->removeValueAt(I)V
+HPLandroidx/collection/MutableObjectIntMap;->resizeStorage(I)V
+HPLandroidx/collection/MutableObjectIntMap;->set(Ljava/lang/Object;I)V
+Landroidx/collection/MutableScatterMap;
+HPLandroidx/collection/MutableScatterMap;->(I)V
+HPLandroidx/collection/MutableScatterMap;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HPLandroidx/collection/MutableScatterMap;->adjustStorage()V
+HSPLandroidx/collection/MutableScatterMap;->clear()V
+HPLandroidx/collection/MutableScatterMap;->findFirstAvailableSlot(I)I
+HPLandroidx/collection/MutableScatterMap;->findInsertIndex(Ljava/lang/Object;)I
+HPLandroidx/collection/MutableScatterMap;->initializeGrowth()V
+HPLandroidx/collection/MutableScatterMap;->initializeMetadata(I)V
+HPLandroidx/collection/MutableScatterMap;->initializeStorage(I)V
+HPLandroidx/collection/MutableScatterMap;->remove(Ljava/lang/Object;)Ljava/lang/Object;
+HPLandroidx/collection/MutableScatterMap;->removeValueAt(I)Ljava/lang/Object;
+HPLandroidx/collection/MutableScatterMap;->resizeStorage(I)V
+HPLandroidx/collection/MutableScatterMap;->set(Ljava/lang/Object;Ljava/lang/Object;)V
+Landroidx/collection/MutableScatterSet;
+HPLandroidx/collection/MutableScatterSet;->(I)V
+HSPLandroidx/collection/MutableScatterSet;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HPLandroidx/collection/MutableScatterSet;->add(Ljava/lang/Object;)Z
+HSPLandroidx/collection/MutableScatterSet;->adjustStorage()V
+HPLandroidx/collection/MutableScatterSet;->clear()V
+HPLandroidx/collection/MutableScatterSet;->findAbsoluteInsertIndex(Ljava/lang/Object;)I
+HPLandroidx/collection/MutableScatterSet;->findFirstAvailableSlot(I)I
+HPLandroidx/collection/MutableScatterSet;->initializeGrowth()V
+HPLandroidx/collection/MutableScatterSet;->initializeMetadata(I)V
+HPLandroidx/collection/MutableScatterSet;->initializeStorage(I)V
+HSPLandroidx/collection/MutableScatterSet;->plusAssign(Ljava/lang/Object;)V
+HSPLandroidx/collection/MutableScatterSet;->remove(Ljava/lang/Object;)Z
+HPLandroidx/collection/MutableScatterSet;->removeElementAt(I)V
+HPLandroidx/collection/MutableScatterSet;->resizeStorage(I)V
+Landroidx/collection/ObjectIntMap;
+HPLandroidx/collection/ObjectIntMap;->()V
+HPLandroidx/collection/ObjectIntMap;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/ObjectIntMap;->findKeyIndex(Ljava/lang/Object;)I
+HPLandroidx/collection/ObjectIntMap;->getCapacity()I
+HSPLandroidx/collection/ObjectIntMap;->getOrDefault(Ljava/lang/Object;I)I
+HSPLandroidx/collection/ObjectIntMap;->isNotEmpty()Z
+Landroidx/collection/ObjectIntMapKt;
+HSPLandroidx/collection/ObjectIntMapKt;->()V
+HPLandroidx/collection/ObjectIntMapKt;->emptyObjectIntMap()Landroidx/collection/ObjectIntMap;
+Landroidx/collection/ScatterMap;
+HPLandroidx/collection/ScatterMap;->()V
+HPLandroidx/collection/ScatterMap;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HPLandroidx/collection/ScatterMap;->containsKey(Ljava/lang/Object;)Z
+HPLandroidx/collection/ScatterMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HPLandroidx/collection/ScatterMap;->getCapacity()I
+HSPLandroidx/collection/ScatterMap;->isEmpty()Z
+HSPLandroidx/collection/ScatterMap;->isNotEmpty()Z
+Landroidx/collection/ScatterMapKt;
+HSPLandroidx/collection/ScatterMapKt;->()V
+HPLandroidx/collection/ScatterMapKt;->loadedCapacity(I)I
+HPLandroidx/collection/ScatterMapKt;->mutableScatterMapOf()Landroidx/collection/MutableScatterMap;
+HSPLandroidx/collection/ScatterMapKt;->nextCapacity(I)I
+HPLandroidx/collection/ScatterMapKt;->normalizeCapacity(I)I
+HSPLandroidx/collection/ScatterMapKt;->unloadedCapacity(I)I
+Landroidx/collection/ScatterSet;
+HPLandroidx/collection/ScatterSet;->()V
+HSPLandroidx/collection/ScatterSet;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+HPLandroidx/collection/ScatterSet;->contains(Ljava/lang/Object;)Z
+HPLandroidx/collection/ScatterSet;->getCapacity()I
+HPLandroidx/collection/ScatterSet;->getSize()I
+HPLandroidx/collection/ScatterSet;->isEmpty()Z
+Landroidx/collection/ScatterSetKt;
+HSPLandroidx/collection/ScatterSetKt;->()V
+HSPLandroidx/collection/ScatterSetKt;->mutableScatterSetOf()Landroidx/collection/MutableScatterSet;
+Landroidx/collection/SimpleArrayMap;
+HSPLandroidx/collection/SimpleArrayMap;->()V
+HSPLandroidx/collection/SimpleArrayMap;->(I)V
+HSPLandroidx/collection/SimpleArrayMap;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+Landroidx/collection/SparseArrayCompat;
+HSPLandroidx/collection/SparseArrayCompat;->(I)V
+HSPLandroidx/collection/SparseArrayCompat;->(IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+HSPLandroidx/collection/SparseArrayCompat;->keyAt(I)I
+HSPLandroidx/collection/SparseArrayCompat;->put(ILjava/lang/Object;)V
+Landroidx/collection/internal/ContainerHelpersKt;
+HSPLandroidx/collection/internal/ContainerHelpersKt;->()V
+HSPLandroidx/collection/internal/ContainerHelpersKt;->binarySearch([III)I
+HSPLandroidx/collection/internal/ContainerHelpersKt;->binarySearch([JIJ)I
+HSPLandroidx/collection/internal/ContainerHelpersKt;->idealByteArraySize(I)I
+HSPLandroidx/collection/internal/ContainerHelpersKt;->idealIntArraySize(I)I
+HSPLandroidx/collection/internal/ContainerHelpersKt;->idealLongArraySize(I)I
+Landroidx/collection/internal/Lock;
+HSPLandroidx/collection/internal/Lock;->()V
+Landroidx/collection/internal/LruHashMap;
+HSPLandroidx/collection/internal/LruHashMap;->(IF)V
+HSPLandroidx/collection/internal/LruHashMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
+HSPLandroidx/collection/internal/LruHashMap;->isEmpty()Z
+HSPLandroidx/collection/internal/LruHashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
+PLandroidx/compose/animation/ChangeSize;->()V
+PLandroidx/compose/animation/ChangeSize;->(Landroidx/compose/ui/Alignment;Lkotlin/jvm/functions/Function1;Landroidx/compose/animation/core/FiniteAnimationSpec;Z)V
+Landroidx/compose/animation/ColorVectorConverterKt;
+HSPLandroidx/compose/animation/ColorVectorConverterKt;->