├── .github
├── ci-gradle.properties
└── workflows
│ ├── BasicSample.yaml
│ ├── TwoWaySample.yaml
│ └── copy-branch.yml
├── BasicSample
├── .gitignore
├── .google
│ └── packaging.yaml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── android
│ │ │ └── databinding
│ │ │ └── basicsample
│ │ │ └── BasicUsageTest.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── android
│ │ │ │ └── databinding
│ │ │ │ └── basicsample
│ │ │ │ ├── data
│ │ │ │ ├── ObservableFieldProfile.kt
│ │ │ │ └── ProfileObservableViewModel.kt
│ │ │ │ ├── ui
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── ObservableFieldActivity.kt
│ │ │ │ └── ViewModelActivity.kt
│ │ │ │ └── util
│ │ │ │ ├── BindingAdapters.kt
│ │ │ │ ├── BindingConverters.kt
│ │ │ │ ├── BindingMethods.kt
│ │ │ │ └── ObservableViewModel.kt
│ │ └── res
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_person_black_96dp.xml
│ │ │ └── ic_whatshot_black_96dp.xml
│ │ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── observable_field_profile.xml
│ │ │ └── viewmodel_profile.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── android
│ │ └── databinding
│ │ └── basicsample
│ │ └── data
│ │ └── ProfileObservableViewModelTest.kt
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
│ └── screenshotbasic.png
└── settings.gradle
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TwoWaySample
├── .gitignore
├── .google
│ └── packaging.yaml
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── android
│ │ │ └── databinding
│ │ │ └── twowaysample
│ │ │ └── BasicUsageTest.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── android
│ │ │ │ └── databinding
│ │ │ │ └── twowaysample
│ │ │ │ ├── data
│ │ │ │ ├── IntervalTimerViewModel.kt
│ │ │ │ └── IntervalTimerViewModelFactory.kt
│ │ │ │ ├── ui
│ │ │ │ ├── AnimationBindingAdapters.kt
│ │ │ │ ├── BindingAdapters.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── NumberOfSetsBindingAdapters.kt
│ │ │ │ └── util
│ │ │ │ ├── Converter.kt
│ │ │ │ ├── ObservableViewModel.kt
│ │ │ │ └── TimerWrapper.kt
│ │ └── res
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ │ ├── layout
│ │ │ └── interval_timer.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ └── tags.xml
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── android
│ │ └── databinding
│ │ └── twowaysample
│ │ └── data
│ │ └── IntervalTimerViewModelTest.kt
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
│ └── screenshot2way.png
└── settings.gradle
└── scripts
└── checksum.sh
/.github/ci-gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | org.gradle.daemon=false
18 | org.gradle.parallel=true
19 | org.gradle.jvmargs=-Xmx5120m
20 | org.gradle.workers.max=2
21 |
22 | kotlin.incremental=false
23 | kotlin.compiler.execution.strategy=in-process
24 |
--------------------------------------------------------------------------------
/.github/workflows/BasicSample.yaml:
--------------------------------------------------------------------------------
1 | name: BasicSample
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '.github/workflows/BasicSample.yaml'
9 | - 'BasicSample/**'
10 | pull_request:
11 | paths:
12 | - '.github/workflows/BasicSample.yaml'
13 | - 'BasicSample/**'
14 |
15 | env:
16 | SAMPLE_PATH: BasicSample
17 |
18 | jobs:
19 | build_and_test:
20 | runs-on: macOS-latest
21 | timeout-minutes: 30
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v3
26 |
27 | - name: Copy CI gradle.properties
28 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
29 |
30 | - name: Set up JDK 17
31 | uses: actions/setup-java@v4
32 | with:
33 | distribution: 'zulu'
34 | java-version: 17
35 |
36 | - name: Generate cache key
37 | run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
38 |
39 | - uses: actions/cache@v3
40 | with:
41 | path: |
42 | ~/.gradle/caches/modules-*
43 | ~/.gradle/caches/jars-*
44 | ~/.gradle/caches/build-cache-*
45 | key: gradle-${{ hashFiles('checksum.txt') }}
46 |
47 | - name: Build project
48 | working-directory: ${{ env.SAMPLE_PATH }}
49 | run: ./gradlew assembleDebug lintDebug --stacktrace
50 |
51 | - name: Run instrumentation tests
52 | uses: reactivecircus/android-emulator-runner@v2
53 | with:
54 | api-level: 26
55 | arch: x86
56 | disable-animations: true
57 | script: ./gradlew connectedCheck --stacktrace
58 | working-directory: ${{ env.SAMPLE_PATH }}
59 |
60 | - name: Upload build outputs (APKs)
61 | uses: actions/upload-artifact@v3
62 | with:
63 | name: build-outputs-BasicSample
64 | path: ${{ env.SAMPLE_PATH }}/app/build/outputs
65 |
66 | - name: Upload build reports
67 | if: always()
68 | uses: actions/upload-artifact@v3
69 | with:
70 | name: build-reports-BasicSample
71 | path: ${{ env.SAMPLE_PATH }}/app/build/reports
72 |
--------------------------------------------------------------------------------
/.github/workflows/TwoWaySample.yaml:
--------------------------------------------------------------------------------
1 | name: TwoWaySample
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '.github/workflows/TwoWaySample.yaml'
9 | - 'TwoWaySample/**'
10 | pull_request:
11 | paths:
12 | - '.github/workflows/TwoWaySample.yaml'
13 | - 'TwoWaySample/**'
14 |
15 | env:
16 | SAMPLE_PATH: TwoWaySample
17 |
18 | jobs:
19 | build_and_test:
20 | runs-on: macOS-latest
21 | timeout-minutes: 30
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v3
26 |
27 | - name: Copy CI gradle.properties
28 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
29 |
30 | - name: Set up JDK 11
31 | uses: actions/setup-java@v3
32 | with:
33 | distribution: 'zulu'
34 | java-version: 11
35 |
36 | - name: Generate cache key
37 | run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt
38 |
39 | - uses: actions/cache@v3
40 | with:
41 | path: |
42 | ~/.gradle/caches/modules-*
43 | ~/.gradle/caches/jars-*
44 | ~/.gradle/caches/build-cache-*
45 | key: gradle-${{ hashFiles('checksum.txt') }}
46 |
47 | - name: Build project
48 | working-directory: ${{ env.SAMPLE_PATH }}
49 | run: ./gradlew assembleDebug lintDebug --stacktrace
50 |
51 | - name: Run instrumentation tests
52 | uses: reactivecircus/android-emulator-runner@v2
53 | with:
54 | api-level: 26
55 | arch: x86
56 | disable-animations: true
57 | script: ./gradlew connectedCheck --stacktrace
58 | working-directory: ${{ env.SAMPLE_PATH }}
59 |
60 | - name: Upload build outputs (APKs)
61 | uses: actions/upload-artifact@v3
62 | with:
63 | name: build-outputs-TwoWaySample
64 | path: ${{ env.SAMPLE_PATH }}/app/build/outputs
65 |
66 | - name: Upload build reports
67 | if: always()
68 | uses: actions/upload-artifact@v3
69 | with:
70 | name: build-reports-TwoWaySample
71 | path: ${{ env.SAMPLE_PATH }}/app/build/reports
--------------------------------------------------------------------------------
/.github/workflows/copy-branch.yml:
--------------------------------------------------------------------------------
1 | # Duplicates default main branch to the old master branch
2 |
3 | name: Duplicates main to old master branch
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the main branch
7 | on:
8 | push:
9 | branches: [ main ]
10 |
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 | # This workflow contains a single job called "copy-branch"
14 | copy-branch:
15 | # The type of runner that the job will run on
16 | runs-on: ubuntu-latest
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it,
21 | # but specifies master branch (old default).
22 | - uses: actions/checkout@v2
23 | with:
24 | fetch-depth: 0
25 | ref: master
26 |
27 | - run: |
28 | git config user.name github-actions
29 | git config user.email github-actions@github.com
30 | git merge origin/main
31 | git push
32 |
--------------------------------------------------------------------------------
/BasicSample/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/BasicSample/.google/packaging.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2018 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | # GOOGLE SAMPLE PACKAGING DATA
17 | #
18 | # This file is used by Google as part of our samples packaging process.
19 | # End users may safely ignore this file. It has no relevance to other systems.
20 | ---
21 | # Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
22 | status: PUBLISHED
23 |
24 | # Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
25 | # statusNote:
26 |
27 | # See http://go/sample-categories
28 | technologies: [Android]
29 | categories: [Data Binding]
30 | languages: [Kotlin]
31 | solutions: [Mobile]
32 |
33 | # May be omitted if unpublished
34 | github: googlesamples/android-databinding
35 |
36 | # Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
37 | level: INTERMEDIATE
38 |
39 | # Default: apache2. May be omitted for most samples.
40 | # Alternatives: apache2-android (for AOSP)
41 | license: apache2
42 |
--------------------------------------------------------------------------------
/BasicSample/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your sample apps and patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | Please fill out either the individual or corporate Contributor License Agreement (CLA).
9 |
10 | * If you are an individual writing original source code and you're sure you
11 | own the intellectual property, then you'll need to sign an [individual CLA]
12 | (https://cla.developers.google.com).
13 | * If you work for a company that wants to allow you to contribute your work,
14 | then you'll need to sign a [corporate CLA]
15 | (https://cla.developers.google.com).
16 | * Please make sure you sign both, Android and Google CLA
17 |
18 | Follow either of the two links above to access the appropriate CLA and
19 | instructions for how to sign and return it. Once we receive it, we'll be able to
20 | accept your pull requests.
21 |
22 | ## Contributing A Patch
23 |
24 | 1. Submit an issue describing your proposed change to the repo in question.
25 | 1. The repo owner will respond to your issue promptly.
26 | 1. If your proposed change is accepted, and you haven't already done so, sign a
27 | Contributor License Agreement (see details above).
28 | 1. Fork the desired repo, develop and test your code changes.
29 | 1. Ensure that your code adheres to the existing style in the sample to which
30 | you are contributing. Refer to the
31 | [Android Code Style Guide]
32 | (https://source.android.com/source/code-style.html) for the
33 | recommended coding standards for this organization.
34 | 1. Ensure that your code has an appropriate set of unit tests which all pass.
35 | 1. Submit a pull request.
36 |
--------------------------------------------------------------------------------
/BasicSample/README.md:
--------------------------------------------------------------------------------
1 | Android Data Binding Basic Sample
2 | =============================================
3 |
4 | This sample showcases the following features of the
5 | [Data Binding library](https://developer.android.com/topic/libraries/data-binding/index.html):
6 |
7 | * Layout variables and expressions
8 | * Observability through Observable Fields, LiveData and Observable classes
9 | * Binding Adapters, Binding Methods and Binding Converters
10 | * Seamless integration with ViewModels
11 |
12 | It shows common bad practices and their solutions in two different screens.
13 |
14 | Features
15 | --------
16 |
17 | 
18 |
19 | ### Layout variables and expressions
20 |
21 | With Data Binding you can write less boilerplate and repetitive code. It moves UI operations out
22 | of the activities and fragments to the XML layout.
23 |
24 | For example, instead of setting text on a TextView in an activity:
25 |
26 | ```java
27 | TextView textView = findViewById(R.id.name);
28 | textView.setText(user.name);
29 | ```
30 |
31 | You assign the attribute to a variable, in the XML layout:
32 |
33 | ```xml
34 |
38 | ```
39 |
40 | See `ObservableFieldActivity.kt`, `ObservableFieldProfile.kt` and `observable_field_profile.xml`
41 | for a simple example.
42 |
43 | ### Observability
44 |
45 | In order to update the UI automatically when the data changes, Data Binding lets you bind attributes
46 | with observable objects. You can choose between three mechanisms to achieve this: Observable fields,
47 | LiveData and Observable classes.
48 |
49 | #### Observable fields
50 |
51 | Types like ObservableBoolean, ObservableInt and the generic ObservableField replace the
52 | corresponding primitives to make them observable. Setting a new value on one of the Observable
53 | fields will update the layout automatically.
54 |
55 | ```kotlin
56 | class ProfileObservableFieldsViewModel : ViewModel() {
57 |
58 | val likes = ObservableInt(0)
59 |
60 | fun onLike() {
61 | likes.increment() // Equivalent to set(likes.get() + 1)
62 | }
63 | }
64 | ```
65 |
66 | In this example, when `onLike` is called, the number of likes is incremented
67 | and the UI is updated. There is no need to notify that the property changed.
68 |
69 | #### LiveData
70 |
71 | LiveData is an observable from
72 | [Android Architecture Components](https://developer.android.com/topic/libraries/architecture)
73 | that is lifecycle-aware.
74 |
75 | The advantages over Observable Fields are that LiveData supports
76 | [Transformations](https://developer.android.com/reference/android/arch/lifecycle/Transformations)
77 | and it's compatible with other components and libraries, like Room and WorkManager.
78 |
79 | ```kotlin
80 | class ProfileLiveDataViewModel : ViewModel() {
81 | private val _likes = MutableLiveData(0)
82 | val likes: LiveData = _likes // Expose an immutable LiveData
83 |
84 | fun onLike() {
85 | _likes.value = (_likes.value ?: 0) + 1
86 | }
87 | }
88 | ```
89 |
90 | It requires an extra step done on the binding:
91 |
92 |
93 | ```kotlin
94 |
95 | binding.lifecycleOwner = this // use viewLifecycleOwner when assigning a fragment
96 | ```
97 |
98 | #### Observable classes
99 |
100 | For maximum flexibility and control, you can implement a fully observable class and decide when
101 | to update certain properties. This lets you create dependencies between properties and it's
102 | useful to dispatch partial UI updates, for example avoiding
103 | potential glitches (UI elements updating almost at the same time).
104 |
105 | ```kotlin
106 | class ProfileObservableViewModel : ObservableViewModel() {
107 | val likes = ObservableInt(0)
108 |
109 | fun onLike() {
110 | likes.increment()
111 | notifyPropertyChanged(BR.popularity)
112 | }
113 |
114 | @Bindable
115 | fun getPopularity(): Popularity {
116 | return likes.get().let {
117 | when {
118 | it > 9 -> Popularity.STAR
119 | it > 4 -> Popularity.POPULAR
120 | else -> Popularity.NORMAL
121 | }
122 | }
123 | }
124 | }
125 | ```
126 |
127 | In this example, when `onLike` is called, the number of likes is incremented and the
128 | `popularity` property is notified of a potential change (`popularity` depends on `likes`).
129 | `getPopularity` is called by the library, returning a possible new value.
130 |
131 | See `ProfileObservableFieldsViewModel.kt` for a complete example.
132 |
133 | #### Binding adapters
134 |
135 | Binding adapters let you customize or create layout attributes. For example, you can create
136 | an `app:progressTint` attribute for progress bars where you change the color of the
137 | progress indicator depending on an external value.
138 |
139 | ```kotlin
140 | @BindingAdapter("app:progressTint")
141 | @JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {
142 |
143 | val color = getAssociatedColor(popularity, view.context)
144 | view.progressTintList = ColorStateList.valueOf(color)
145 | }
146 | ```
147 |
148 | The binding is created in the XML layout with:
149 |
150 | ```xml
151 |
153 | ```
154 |
155 | Using binding adapters lets you move UI calls from the activity to static methods, improving
156 | encapsulation.
157 |
158 | You can also use multiple attributes in a Binding Adapter, see `viewmodel_profile.xml` for a complete
159 | example.
160 |
161 | #### Binding methods and binding converters
162 |
163 | Binding methods and binding converters let you reduce code if your binding adapters
164 | are very simple. You can read about them in the
165 | [official guide](https://developer.android.com/topic/libraries/data-binding/index.html#attribute_setters).
166 |
167 | For example, if an attribute's value needs to be passed to a method in the class:
168 |
169 | ```kotlin
170 | @BindingAdapter("app:srcCompat")
171 | @JvmStatic fun srcCompat(view: ImageView, @DrawableRes drawableId: Int) {
172 | view.setImageResource(drawable)
173 | }
174 | ```
175 |
176 | You can replace this with a Binding Method which can be added to any class in the project:
177 |
178 | ```kotlin
179 | @BindingMethods(
180 | BindingMethod(type = ImageView::class,
181 | attribute = "app:srcCompat",
182 | method = "setImageDrawable"))
183 | ```
184 |
185 | #### Binding Converters (use with caution)
186 |
187 | In this sample we show a View depending on whether a number is zero. There are many options to do
188 | this. We're showing two, one in the `observable_field_profile.xml`
189 | and, the recommended way, in `viewmodel_profile.xml`.
190 |
191 | The goal is to bind the view's visibility to the number of likes, but this won't work:
192 |
193 | ```xml
194 | android:visibility="@{viewmodel.likes}"
195 | ```
196 |
197 | The number of likes is an integer and the visibility attribute takes an integer
198 | (`VISIBLE`, `GONE` and `INVISIBLE` are 0, 4 and 8 respectively), so doing this would build,
199 | but the result would not be the expected.
200 |
201 | A possible solution is:
202 |
203 | ```xml
204 | android:visibility="@{viewmodel.likes == 0 ? View.GONE : View.VISIBLE}"/
205 | ```
206 |
207 | But it adds a relatively complex expression to the layout.
208 |
209 | Instead, you can create and import a utils class:
210 |
211 | ```xml
212 |
213 |
214 | ...
215 |
216 | ```
217 |
218 | and use it from the View like so:
219 |
220 | ```xml
221 | android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"
222 | ```
223 |
224 | `isZero` returns a boolean and `visibility` takes an integer so in order to convert
225 | from boolean we can also define a BindingConversion:
226 |
227 | ```kotlin
228 | @BindingConversion
229 | @JvmStatic fun booleanToVisibility(isVisible: Boolean): Int { // Risky! applies everywhere
230 | return if (isVisible) View.VISIBLE else View.GONE
231 | }
232 | ```
233 |
234 | This conversion is unsafe because this binding conversion is not restricted to our case: it will
235 | convert all booleans to visibility integers when the attribute takes an integer.
236 |
237 | Solution: As with every BindingConversion and BindingMethod, you can replace it with a Binding
238 | Adapter, which normally is much simpler:
239 |
240 | ```kotlin
241 | @BindingAdapter("app:hideIfZero") // Recommended solution
242 | @JvmStatic fun hideIfZero(view: View, number: Int) {
243 | view.visibility = if (number == 0) View.GONE else View.VISIBLE
244 | }
245 | ```
246 |
247 | and as shown in in `viewmodel_profile.xml`:
248 |
249 | ```xml
250 | app:hideIfZero="@{viewmodel.likes}"
251 | ```
252 |
253 | This defines a new custom attribute `hideIfZero` that can't be used accidentally.
254 |
255 | As a rule of thumb it's preferable to create your our custom attributes using Data Binding adapters
256 | instead of adding logic to your binding expressions.
257 |
258 | Sample app
259 | ----------
260 |
261 | This app shows a user's profile using two different screens to showcase different Data Binding
262 | features:
263 |
264 | * Main activity: Shows how a Data Binding layout lets you access Views without `findViewById`.
265 |
266 | * Observable field activity: In this screen the user can give "likes" to the profile and the UI
267 | reacts automatically to changes. However, the activity holds the logic that receives the user
268 | click and the actual profile data, which is not testable.
269 | Also, likes are reset when the user rotates the device and the layout contains documented common
270 | bad practices.
271 |
272 | * ViewModel activity: Using a ViewModel from the
273 | [Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html)
274 | fixes the rotation problem and moves logic out of the activity. Also, the use of binding
275 | adapters changes the responsibility of the activity which is no longer the "view" and
276 | solely responsible for dealing with the lifecycle. Two ViewModels are suggested in
277 | `ProfileObservableViewModel.kt`: one based on observable fields and another implementing the
278 | observable interface.
279 |
280 |
281 | License
282 | --------
283 |
284 | Copyright 2018 The Android Open Source Project, Inc.
285 |
286 | Licensed to the Apache Software Foundation (ASF) under one or more contributor
287 | license agreements. See the NOTICE file distributed with this work for
288 | additional information regarding copyright ownership. The ASF licenses this
289 | file to you under the Apache License, Version 2.0 (the "License"); you may not
290 | use this file except in compliance with the License. You may obtain a copy of
291 | the License at
292 |
293 | http://www.apache.org/licenses/LICENSE-2.0
294 |
295 | Unless required by applicable law or agreed to in writing, software
296 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
297 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
298 | License for the specific language governing permissions and limitations under
299 | the License.
300 |
--------------------------------------------------------------------------------
/BasicSample/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/BasicSample/app/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'com.android.application'
18 |
19 | apply plugin: 'kotlin-android'
20 |
21 | apply plugin: 'kotlin-kapt'
22 |
23 | android {
24 | namespace 'com.example.android.databinding.basicsample'
25 | compileSdkVersion rootProject.compileSdkVersion
26 | defaultConfig {
27 | applicationId "com.example.android.databinding.basicsample"
28 | minSdkVersion rootProject.minSdkVersion
29 | targetSdkVersion rootProject.targetSdkVersion
30 | versionCode 1
31 | versionName "1.0"
32 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
33 | vectorDrawables.useSupportLibrary = true
34 | }
35 | buildTypes {
36 | release {
37 | minifyEnabled false
38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
39 | }
40 | }
41 | buildFeatures {
42 | dataBinding true
43 | }
44 | compileOptions {
45 | sourceCompatibility JavaVersion.VERSION_17
46 | targetCompatibility JavaVersion.VERSION_17
47 | }
48 | }
49 |
50 | dependencies {
51 | implementation fileTree(dir: 'libs', include: ['*.jar'])
52 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
53 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
54 | implementation("androidx.activity:activity-ktx:$activityVersion")
55 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
56 |
57 | testImplementation "junit:junit:$junitVersion"
58 | testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
59 |
60 | androidTestImplementation "androidx.test:runner:$androidXTestRunnerVersion"
61 | androidTestImplementation "androidx.test.ext:junit:$androidXTestExtVersion"
62 | androidTestImplementation "androidx.test:rules:$androidXTestRulesVersion"
63 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/BasicSample/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/BasicSample/app/src/androidTest/java/com/example/android/databinding/basicsample/BasicUsageTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample
18 |
19 | import androidx.test.espresso.Espresso.onView
20 | import androidx.test.espresso.action.ViewActions.click
21 | import androidx.test.espresso.assertion.ViewAssertions.matches
22 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
23 | import androidx.test.espresso.matcher.ViewMatchers.withId
24 | import androidx.test.espresso.matcher.ViewMatchers.withText
25 | import androidx.test.ext.junit.runners.AndroidJUnit4
26 | import androidx.test.rule.ActivityTestRule
27 | import com.example.android.databinding.basicsample.ui.MainActivity
28 | import org.junit.Rule
29 | import org.junit.Test
30 | import org.junit.runner.RunWith
31 |
32 |
33 | /**
34 | * Instrumented test, which will execute on an Android device.
35 | *
36 | * See [testing documentation](http://d.android.com/tools/testing).
37 | */
38 | @RunWith(AndroidJUnit4::class)
39 | class BasicUsageTest {
40 |
41 | @get:Rule
42 | var activityRule = ActivityTestRule(MainActivity::class.java)
43 |
44 | @Test
45 | fun observableFieldsActivity_likes() {
46 | // Click on button to open activity
47 | onView(withId(R.id.observable_fields_activity_button)).perform(click())
48 |
49 | // Click Like 5 times
50 | repeat(5) {
51 | onView(withId(R.id.like_button)).perform(click())
52 | }
53 |
54 | // Check that the number of likes is displayed
55 | onView(withId(R.id.likes)).check(matches(withText("5")))
56 | }
57 |
58 | @Test
59 | fun viewmodelActivity_likes() {
60 | // Click on button to open activity
61 | onView(withId(R.id.viewmodel_activity_button)).perform(click())
62 |
63 | // Click Like 5 times
64 | repeat(5) {
65 | onView(withId(R.id.like_button)).perform(click())
66 | }
67 |
68 | // Check that the number of likes is displayed and the progressBar appeared
69 | onView(withId(R.id.likes)).check(matches(withText("5")))
70 | onView(withId(R.id.progressBar)).check(matches(isDisplayed()))
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/data/ObservableFieldProfile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.data
18 |
19 | import androidx.databinding.ObservableInt
20 |
21 | /**
22 | * Used as a layout variable to provide static properties (name and lastName) and an observable
23 | * one (likes).
24 | */
25 | data class ObservableFieldProfile(
26 | val name: String,
27 | val lastName: String,
28 | val likes: ObservableInt
29 | )
30 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/data/ProfileObservableViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.data
18 |
19 | import androidx.databinding.Bindable
20 | import androidx.databinding.ObservableField
21 | import androidx.databinding.ObservableInt
22 | import androidx.lifecycle.LiveData
23 | import androidx.lifecycle.map
24 | import androidx.lifecycle.MutableLiveData
25 | import androidx.lifecycle.ViewModel
26 | import com.example.android.databinding.basicsample.BR
27 | import com.example.android.databinding.basicsample.util.ObservableViewModel
28 |
29 |
30 | /**
31 | * This class is used as a variable in the XML layout and it's fully observable, meaning that
32 | * changes to any of the exposed observables automatically refresh the UI. *
33 | */
34 | class ProfileLiveDataViewModel : ViewModel() {
35 | private val _name = MutableLiveData("Ada")
36 | private val _lastName = MutableLiveData("Lovelace")
37 | private val _likes = MutableLiveData(0)
38 |
39 | val name: LiveData = _name
40 | val lastName: LiveData = _lastName
41 | val likes: LiveData = _likes
42 |
43 | // popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
44 | val popularity: LiveData = _likes.map {
45 | when {
46 | it > 9 -> Popularity.STAR
47 | it > 4 -> Popularity.POPULAR
48 | else -> Popularity.NORMAL
49 | }
50 | }
51 |
52 | fun onLike() {
53 | _likes.value = (_likes.value ?: 0) + 1
54 | }
55 | }
56 |
57 | /**
58 | * As an alternative to LiveData, you can use Observable Fields and binding properties.
59 | *
60 | * `Popularity` is exposed here as a `@Bindable` property so it's necessary to call
61 | * `notifyPropertyChanged` when any of the dependent properties change (`likes` in this case).
62 | */
63 | class ProfileObservableViewModel : ObservableViewModel() {
64 | val name = ObservableField("Ada")
65 | val lastName = ObservableField("Lovelace")
66 | val likes = ObservableInt(0)
67 |
68 | fun onLike() {
69 | likes.increment()
70 | // You control when the @Bindable properties are updated using `notifyPropertyChanged()`.
71 | notifyPropertyChanged(BR.popularity)
72 | }
73 |
74 | @Bindable
75 | fun getPopularity(): Popularity {
76 | return likes.get().let {
77 | when {
78 | it > 9 -> Popularity.STAR
79 | it > 4 -> Popularity.POPULAR
80 | else -> Popularity.NORMAL
81 | }
82 | }
83 | }
84 | }
85 |
86 | enum class Popularity {
87 | NORMAL,
88 | POPULAR,
89 | STAR
90 | }
91 |
92 | private fun ObservableInt.increment() {
93 | set(get() + 1)
94 | }
95 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.ui
18 |
19 | import android.content.Intent
20 | import androidx.databinding.DataBindingUtil
21 | import android.os.Bundle
22 | import androidx.appcompat.app.AppCompatActivity
23 | import com.example.android.databinding.basicsample.R
24 | import com.example.android.databinding.basicsample.databinding.ActivityMainBinding
25 |
26 | /**
27 | * Shows a menu.
28 | */
29 | class MainActivity : AppCompatActivity() {
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 |
34 | // The layout for this activity is a Data Binding layout so it needs to be inflated using
35 | // DataBindingUtil.
36 | val binding: ActivityMainBinding = DataBindingUtil.setContentView(
37 | this, R.layout.activity_main)
38 |
39 | // The returned binding has references to all the Views with an ID.
40 | binding.observableFieldsActivityButton.setOnClickListener {
41 | startActivity(Intent(this, ObservableFieldActivity::class.java))
42 | }
43 | binding.viewmodelActivityButton.setOnClickListener {
44 | startActivity(Intent(this, ViewModelActivity::class.java))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/ui/ObservableFieldActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.ui
18 |
19 | import android.os.Bundle
20 | import android.view.View
21 | import android.widget.ImageView
22 | import androidx.annotation.ColorInt
23 | import androidx.appcompat.app.AppCompatActivity
24 | import androidx.databinding.BindingAdapter
25 | import androidx.databinding.DataBindingUtil
26 | import androidx.databinding.ObservableInt
27 | import com.example.android.databinding.basicsample.R
28 | import com.example.android.databinding.basicsample.data.ObservableFieldProfile
29 | import com.example.android.databinding.basicsample.databinding.ObservableFieldProfileBinding
30 |
31 | /**
32 | * This activity shows shows static data and lets the user increment the
33 | * number of likes by clicking a button. See [ViewModelActivity] for a better implementation.
34 | */
35 | class ObservableFieldActivity : AppCompatActivity() {
36 |
37 | private val observableFieldProfile = ObservableFieldProfile("Ada", "Lovelace", ObservableInt(0))
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 |
42 | val binding: ObservableFieldProfileBinding =
43 | DataBindingUtil.setContentView(this, R.layout.observable_field_profile)
44 | binding.user = observableFieldProfile
45 | }
46 |
47 | /**
48 | * This method is triggered by the `android:onclick` attribute in the layout. It puts business
49 | * logic in the activity, which is not ideal. See {@link ViewModelActivity} for a better
50 | * solution.
51 | */
52 | fun onLike(view: View) {
53 | observableFieldProfile.likes.set(observableFieldProfile.likes.get() + 1)
54 | }
55 | }
56 |
57 | /**
58 | * Adds support for binding tints to AppCompatImageView.
59 | * See related [bug](https://b.corp.google.com/issues/152953070?pli=1)
60 | */
61 | @BindingAdapter("app:tint")
62 | fun ImageView.setImageTint(@ColorInt color: Int) {
63 | setColorFilter(color)
64 | }
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/ui/ViewModelActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.ui
18 |
19 | import android.os.Bundle
20 | import androidx.activity.viewModels
21 | import androidx.appcompat.app.AppCompatActivity
22 | import androidx.databinding.DataBindingUtil
23 | import com.example.android.databinding.basicsample.R
24 | import com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel
25 | import com.example.android.databinding.basicsample.databinding.ViewmodelProfileBinding
26 |
27 | /**
28 | * This activity uses a [androidx.lifecycle.ViewModel] to hold the data and respond to user
29 | * actions. Also, the layout uses [androidx.databinding.BindingAdapter]s instead of expressions
30 | * which are much more powerful.
31 | *
32 | * @see com.example.android.databinding.basicsample.util.BindingAdapters
33 | */
34 | class ViewModelActivity : AppCompatActivity() {
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 |
38 | // Obtain ViewModel from ViewModelProviders
39 | val viewModel by viewModels()
40 |
41 | // An alternative ViewModel using Observable fields and @Bindable properties can be used:
42 | // val viewModel = ViewModelProviders.of(this).get(ProfileObservableViewModel::class.java)
43 |
44 | // Obtain binding
45 | val binding: ViewmodelProfileBinding =
46 | DataBindingUtil.setContentView(this, R.layout.viewmodel_profile)
47 |
48 | // Bind layout with ViewModel
49 | binding.viewmodel = viewModel
50 |
51 | // LiveData needs the lifecycle owner
52 | binding.lifecycleOwner = this
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/util/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.util
18 |
19 | import android.content.Context
20 | import android.content.res.ColorStateList
21 | import androidx.databinding.BindingAdapter
22 | import android.graphics.drawable.Drawable
23 | import android.os.Build
24 | import androidx.core.content.ContextCompat
25 | import androidx.core.widget.ImageViewCompat
26 | import android.view.View
27 | import android.widget.ImageView
28 | import android.widget.ProgressBar
29 | import com.example.android.databinding.basicsample.R
30 | import com.example.android.databinding.basicsample.data.Popularity
31 |
32 |
33 | object BindingAdapters {
34 | /**
35 | * A Binding Adapter that is called whenever the value of the attribute `app:popularityIcon`
36 | * changes. Receives a popularity level that determines the icon and tint color to use.
37 | */
38 | @BindingAdapter("app:popularityIcon")
39 | @JvmStatic fun popularityIcon(view: ImageView, popularity: Popularity) {
40 |
41 | val color = getAssociatedColor(popularity, view.context)
42 |
43 | ImageViewCompat.setImageTintList(view, ColorStateList.valueOf(color))
44 |
45 | view.setImageDrawable(getDrawablePopularity(popularity, view.context))
46 | }
47 |
48 | /**
49 | * A Binding Adapter that is called whenever the value of the attribute `android:progressTint`
50 | * changes. Depending on the value it determines the color of the progress bar.
51 | */
52 | @BindingAdapter("app:progressTint")
53 | @JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {
54 |
55 | val color = getAssociatedColor(popularity, view.context)
56 |
57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
58 | view.progressTintList = ColorStateList.valueOf(color)
59 | }
60 | }
61 |
62 | /**
63 | * Sets the value of the progress bar so that 5 likes will fill it up.
64 | *
65 | * Showcases Binding Adapters with multiple attributes. Note that this adapter is called
66 | * whenever any of the attribute changes.
67 | */
68 | @BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
69 | @JvmStatic fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
70 | progressBar.progress = (likes * max / 5).coerceAtMost(max)
71 | }
72 |
73 | /**
74 | * Unused Binding Adapter to replace the Binding Converter that hides a view if the number
75 | * of likes is zero.
76 | */
77 | @BindingAdapter("app:hideIfZero")
78 | @JvmStatic fun hideIfZero(view: View, number: Int) {
79 | view.visibility = if (number == 0) View.GONE else View.VISIBLE
80 | }
81 |
82 | private fun getAssociatedColor(popularity: Popularity, context: Context): Int {
83 | return when (popularity) {
84 | Popularity.NORMAL -> context.theme.obtainStyledAttributes(
85 | intArrayOf(android.R.attr.colorForeground)).getColor(0, 0x000000)
86 | Popularity.POPULAR -> ContextCompat.getColor(context, R.color.popular)
87 | Popularity.STAR -> ContextCompat.getColor(context, R.color.star)
88 | }
89 | }
90 |
91 | private fun getDrawablePopularity(popularity: Popularity, context: Context): Drawable? {
92 | return when (popularity) {
93 | Popularity.NORMAL -> {
94 | ContextCompat.getDrawable(context, R.drawable.ic_person_black_96dp)
95 | }
96 | Popularity.POPULAR -> {
97 | ContextCompat.getDrawable(context, R.drawable.ic_whatshot_black_96dp)
98 | }
99 | Popularity.STAR -> {
100 | ContextCompat.getDrawable(context, R.drawable.ic_whatshot_black_96dp)
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/util/BindingConverters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.util
18 |
19 | import androidx.databinding.BindingConversion
20 | import android.view.View
21 |
22 | /**
23 | * In order to show a View only when it has more than 0 likes, we pass this expression to its
24 | * visibilty property:
25 | *
26 | * `android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"`
27 | *
28 | * This converts "likes" (an Int) into a Boolean. See [BindingConverters] for the conversion
29 | * from Boolean to a visibility integer.
30 | */
31 | object ConverterUtil {
32 | @JvmStatic fun isZero(number: Int): Boolean {
33 | return number == 0
34 | }
35 | }
36 |
37 | /**
38 | * The number of likes is an integer and the visibility attribute takes an integer
39 | * (VISIBLE, GONE and INVISIBLE are 0, 4 and 8 respectively), so we use this converter.
40 | *
41 | * There is no need to specify that this converter should be used. [BindingConversion]s are
42 | * applied automatically.
43 | */
44 | object BindingConverters{
45 |
46 | @BindingConversion
47 | @JvmStatic fun booleanToVisibility(isNotVisible: Boolean): Int {
48 | return if (isNotVisible) View.GONE else View.VISIBLE
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/util/BindingMethods.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.util
18 |
19 | import androidx.databinding.BindingMethod
20 | import androidx.databinding.BindingMethods
21 | import android.widget.ImageView
22 |
23 |
24 | /**
25 | * `app:srcCompat` is an attribute used by the support library to integrate vector drawables. This
26 | * BindingMethod binds the attribute to the setImageDrawable method in the ImageView class.
27 | *
28 | * Binding methods have to be applied to any class in your project. Even an empty one.
29 | *
30 | * This is equivalent to:
31 | * ```
32 | *
33 | * @BindingAdapter("app:srcCompat")
34 | * @JvmStatic fun srcCompat(view: ImageView, @DrawableRes drawableId: Int) {
35 | * view.setImageResource(drawable)
36 | * }
37 | * ```
38 | */
39 | @BindingMethods(
40 | BindingMethod(type = ImageView::class,
41 | attribute = "app:srcCompat",
42 | method = "setImageResource"))
43 | class MyBindingMethods
44 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/java/com/example/android/databinding/basicsample/util/ObservableViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.util
18 |
19 | import androidx.lifecycle.ViewModel
20 | import androidx.databinding.Bindable
21 | import androidx.databinding.Observable
22 | import androidx.databinding.PropertyChangeRegistry
23 |
24 |
25 | /**
26 | * A ViewModel that is also an Observable, to be used with Data Binding.
27 | */
28 | open class ObservableViewModel : ViewModel(), Observable {
29 |
30 | private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
31 |
32 | override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
33 | callbacks.add(callback)
34 | }
35 |
36 | override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
37 | callbacks.remove(callback)
38 | }
39 |
40 | /**
41 | * Notifies listeners that all properties of this instance have changed.
42 | */
43 | fun notifyChange() {
44 | callbacks.notifyCallbacks(this, 0, null)
45 | }
46 |
47 | /**
48 | * Notifies listeners that a specific property has changed. The getter for the property
49 | * that changes should be marked with [Bindable] to generate a field in
50 | * `BR` to be used as `fieldId`.
51 | *
52 | * @param fieldId The generated BR id for the Bindable field.
53 | */
54 | fun notifyPropertyChanged(fieldId: Int) {
55 | callbacks.notifyCallbacks(this, fieldId, null)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
23 |
28 |
29 |
35 |
38 |
41 |
42 |
43 |
44 |
50 |
51 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
27 |
32 |
37 |
42 |
47 |
52 |
57 |
62 |
67 |
72 |
77 |
82 |
87 |
92 |
97 |
102 |
107 |
112 |
117 |
122 |
127 |
132 |
137 |
142 |
147 |
152 |
157 |
162 |
167 |
172 |
177 |
182 |
187 |
188 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/drawable/ic_person_black_96dp.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/drawable/ic_whatshot_black_96dp.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 |
22 |
23 |
24 |
28 |
29 |
39 |
40 |
50 |
51 |
64 |
65 |
78 |
79 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/layout/observable_field_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
36 |
37 |
49 |
50 |
62 |
63 |
68 |
84 |
85 |
87 |
98 |
99 |
102 |
115 |
116 |
125 |
126 |
135 |
136 |
149 |
150 |
155 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/layout/viewmodel_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
33 |
34 |
35 |
47 |
48 |
49 |
61 |
62 |
65 |
80 |
81 |
83 |
94 |
95 |
97 |
109 |
110 |
119 |
120 |
129 |
130 |
143 |
144 |
145 |
147 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 | #3f51b5
20 | #303F9F
21 | #ff4081
22 | #ff76a5
23 | #ff0057
24 |
25 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | BasicSample
19 | Likes
20 | TextView
21 | NAME
22 | LAST NAME
23 | Last name
24 | Name
25 | Profile\'s avatar
26 | Like
27 | Observable Fields activity
28 | ViewModel activity
29 | Uses ObservableFields to automatically refresh the UI and showcases common bad practices
30 | Uses a ViewModel to hold data and handle user actions, using better alternatives
31 | See README.md for details
32 |
33 |
--------------------------------------------------------------------------------
/BasicSample/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/BasicSample/app/src/test/java/com/example/android/databinding/basicsample/data/ProfileObservableViewModelTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.basicsample.data
18 |
19 | import org.junit.Assert.assertEquals
20 | import org.junit.Test
21 |
22 |
23 | class ProfileObservableViewModelTest {
24 |
25 | private val viewmodel = ProfileObservableViewModel()
26 |
27 | @Test
28 | fun popularityIsStarAfter10Likes() {
29 | callOnLikeTimes(9)
30 | assertEquals(viewmodel.getPopularity(), Popularity.POPULAR)
31 | callOnLikeTimes(1)
32 | assertEquals(viewmodel.getPopularity(), Popularity.STAR)
33 | }
34 |
35 | @Test
36 | fun popularityIsPopularAfter5Likes() {
37 | callOnLikeTimes(4)
38 | assertEquals(viewmodel.getPopularity(), Popularity.NORMAL)
39 | callOnLikeTimes(1)
40 | assertEquals(viewmodel.getPopularity(), Popularity.POPULAR)
41 | }
42 |
43 | private fun callOnLikeTimes(times: Int) {
44 | (0 until times).forEach {
45 | viewmodel.onLike()
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/BasicSample/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
18 |
19 | buildscript {
20 | ext {
21 | // SDK and tools
22 | compileSdkVersion = 33
23 | minSdkVersion = 21
24 | targetSdkVersion = 33
25 |
26 | // App dependencies
27 | activityVersion = '1.6.1'
28 | constraintLayoutVersion = '2.1.4'
29 | espressoVersion = '3.5.1'
30 | gradleVersion = '8.3.0'
31 | junitVersion = '4.13.2'
32 | hamcrestVersion = '1.3'
33 | kotlinVersion = '1.8.0'
34 | androidXTestRunnerVersion = '1.5.2'
35 | androidXTestExtVersion = '1.1.5'
36 | androidXTestRulesVersion = '1.5.0'
37 | appCompatVersion = '1.6.0'
38 | archLifecycleVersion = '2.5.1'
39 | }
40 | repositories {
41 | google()
42 | mavenCentral()
43 | }
44 | dependencies {
45 | classpath "com.android.tools.build:gradle:$gradleVersion"
46 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
47 |
48 | // NOTE: Do not place your application dependencies here; they belong
49 | // in the individual module build.gradle files
50 | }
51 | }
52 |
53 | allprojects {
54 | repositories {
55 | google()
56 | mavenCentral()
57 | }
58 | }
59 |
60 | task clean(type: Delete) {
61 | delete rootProject.buildDir
62 | }
63 |
--------------------------------------------------------------------------------
/BasicSample/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2018 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | # Project-wide Gradle settings.
18 |
19 | # IDE (e.g. Android Studio) users:
20 | # Gradle settings configured through the IDE *will override*
21 | # any settings specified in this file.
22 |
23 | # For more details on how to configure your build environment visit
24 | # http://www.gradle.org/docs/current/userguide/build_environment.html
25 |
26 | # Specifies the JVM arguments used for the daemon process.
27 | # The setting is particularly useful for tweaking memory settings.
28 | android.enableJetifier=true
29 | android.useAndroidX=true
30 | org.gradle.jvmargs=-Xmx1536m
31 |
32 | # When configured, Gradle will run in incubating parallel mode.
33 | # This option should only be used with decoupled projects. More details, visit
34 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
35 | # org.gradle.parallel=true
36 |
--------------------------------------------------------------------------------
/BasicSample/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/BasicSample/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/BasicSample/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/BasicSample/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/BasicSample/screenshots/screenshotbasic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/BasicSample/screenshots/screenshotbasic.png
--------------------------------------------------------------------------------
/BasicSample/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | include ':app'
18 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to become a contributor and submit your own code
2 |
3 | ## Contributor License Agreements
4 |
5 | We'd love to accept your sample apps and patches! Before we can take them, we
6 | have to jump a couple of legal hurdles.
7 |
8 | Please fill out either the individual or corporate Contributor License Agreement (CLA).
9 |
10 | * If you are an individual writing original source code and you're sure you
11 | own the intellectual property, then you'll need to sign an [individual CLA]
12 | (https://cla.developers.google.com).
13 | * If you work for a company that wants to allow you to contribute your work,
14 | then you'll need to sign a [corporate CLA]
15 | (https://cla.developers.google.com).
16 | * Please make sure you sign both, Android and Google CLA
17 |
18 | Follow either of the two links above to access the appropriate CLA and
19 | instructions for how to sign and return it. Once we receive it, we'll be able to
20 | accept your pull requests.
21 |
22 | ## Contributing A Patch
23 |
24 | 1. Submit an issue describing your proposed change to the repo in question.
25 | 1. The repo owner will respond to your issue promptly.
26 | 1. If your proposed change is accepted, and you haven't already done so, sign a
27 | Contributor License Agreement (see details above).
28 | 1. Fork the desired repo, develop and test your code changes.
29 | 1. Ensure that your code adheres to the existing style in the sample to which
30 | you are contributing. Refer to the
31 | [Android Code Style Guide]
32 | (https://source.android.com/source/code-style.html) for the
33 | recommended coding standards for this organization.
34 | 1. Ensure that your code has an appropriate set of unit tests which all pass.
35 | 1. Submit a pull request.
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Android Data Binding Library samples
2 | ===================================
3 |
4 | A collection of samples using the [Android Data Binding Library](https://developer.android.com/topic/libraries/data-binding/index.html):
5 |
6 | ### Samples
7 |
8 | * **[BasicSample](https://github.com/googlesamples/android-databinding/blob/master/BasicSample)** - (Kotlin) Shows basic usage of layout expressions, binding adapters, and integration with ViewModels.
9 | * **[TwoWaySample](https://github.com/googlesamples/android-databinding/blob/master/TwoWaySample)** - (Kotlin) Shows usage of two-way data binding, advanced Binding Adapters, animations, converters and inverse converters.
10 |
11 | ### Other official samples using Data Binding
12 |
13 | * **[Android Architecture Blueprints (todo-mvvm-live-kotlin branch)](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-live-kotlin/)** - Shows basic Data Binding usage with architecture best practices and Architecture Components, in Kotlin.
14 | * **[Android Architecture Components Samples](https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample)** - An advanced sample that uses the Architecture components, Dagger and the Github API, in Kotlin.
15 | * **[Android Sunflower](https://github.com/googlesamples/android-sunflower)** - A gardening app illustrating Android development best practices with Android Jetpack, including Data Binding.
16 |
17 | ### Reporting Issues
18 |
19 | You can report an [Issue](https://github.com/googlesamples/android-databinding/issues) on the samples using this repository. If you find an issue with the library related to build, report it using the [Gradle Plugin issue tracker](https://b.corp.google.com/issues/new?component=192709&template=842921). If it's related to the Android Studio integration, report it using the [Android Studio issue tracker](https://b.corp.google.com/issues/new?component=192708&template=840533).
20 |
21 | License
22 | -------
23 |
24 | Copyright 2018 The Android Open Source Project, Inc.
25 |
26 | Licensed to the Apache Software Foundation (ASF) under one or more contributor
27 | license agreements. See the NOTICE file distributed with this work for
28 | additional information regarding copyright ownership. The ASF licenses this
29 | file to you under the Apache License, Version 2.0 (the "License"); you may not
30 | use this file except in compliance with the License. You may obtain a copy of
31 | the License at
32 |
33 | http://www.apache.org/licenses/LICENSE-2.0
34 |
35 | Unless required by applicable law or agreed to in writing, software
36 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
37 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
38 | License for the specific language governing permissions and limitations under
39 | the License.
40 |
--------------------------------------------------------------------------------
/TwoWaySample/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/TwoWaySample/.google/packaging.yaml:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2018 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | # GOOGLE SAMPLE PACKAGING DATA
17 | #
18 | # This file is used by Google as part of our samples packaging process.
19 | # End users may safely ignore this file. It has no relevance to other systems.
20 | ---
21 | # Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED}
22 | status: PUBLISHED
23 |
24 | # Optional, put additional explanation here for DEPRECATED or SUPERCEDED.
25 | # statusNote:
26 |
27 | technologies: [Android]
28 | categories: [Data Binding]
29 | languages: [Kotlin]
30 | solutions: [Mobile]
31 |
32 | # May be omitted if unpublished
33 | github: googlesamples/android-databinding
34 |
35 | # Values: BEGINNER | INTERMEDIATE | ADVANCED | EXPERT
36 | level: INTERMEDIATE
37 |
38 | # Default: apache2. May be omitted for most samples.
39 | # Alternatives: apache2-android (for AOSP)
40 | license: apache2
41 |
--------------------------------------------------------------------------------
/TwoWaySample/README.md:
--------------------------------------------------------------------------------
1 | Android Data Binding Advanced Sample
2 | =============================================
3 |
4 | This sample showcases the following features
5 | of the [Data Binding library](https://developer.android.com/topic/libraries/data-binding/index.html)
6 | with an app that shows a workout timer.
7 |
8 | * Two-way Data Binding
9 | * Alternatives to Two-way Data Binding
10 | * Binding adapters with multiple parameters
11 | * Animations with Binding Adapters
12 | * Binding converters and inverse converters
13 | * Data Binding with ViewModels and Kotlin
14 | * No UI framework calls in activity
15 | * Testing
16 |
17 | Features
18 | --------
19 |
20 | 
21 |
22 | ## Two-way Data Binding
23 |
24 | Two-way data binding is used twice in this sample: with a simple case (the toggle start/pause
25 | button) and with a more complex feature (the number of sets input).
26 |
27 | ### Simple two-way case
28 |
29 | In the layout, two-way is indicated with the `@={}` syntax:
30 |
31 | ```xml
32 |
34 | ```
35 |
36 | Note the difference with one-way syntax which doesn't have the equals sign (`@{}`).
37 |
38 | This layout expression binds the `checked` attribute to `timerRunning` in the ViewModel.
39 | This means that if the property in the ViewModel changes (for example, when the timer finishing),
40 | the button will automatically show the new status.
41 |
42 | At the same time, if a user action modifies the `checked` attribute on the button, the ViewModel
43 | will receive this signal, dispatching the event accordingly.
44 |
45 | Two Way Data Binding requires the ViewModel property to be decorated with a [`@Bindable`](https://developer.android.com/reference/android/databinding/Bindable.html) annotation:
46 |
47 | ```kotlin
48 | var timerRunning: Boolean
49 | @Bindable get() {
50 | return state == TimerStates.STARTED
51 | }
52 | set(value) {
53 | // These methods take care of calling notifyPropertyChanged()
54 | if (value) startButtonClicked() else pauseButtonClicked()
55 | }
56 | ```
57 |
58 | **Getter**: When the `state` property changes in the ViewModel, `notifyPropertyChanged(BR.timerRunning)` must be
59 | called to indicate that the UI should be updated with the new value obtained in the getter.
60 |
61 | **Setter**: User actions invoke the setter. The corresponding methods will only call `notifyPropertyChanged`
62 | when the state changes, to avoid infinite loops.
63 |
64 | ### Complex two-way case
65 |
66 | The text attribute of an `EditText` is much more complicated to manage than the checked/unchecked
67 | nature of a toggle button. On top of that, the requirement for this input view is to show the text
68 | in a particular format (`Sets: %d/%d`) so the two-way syntax is similar but it includes a converter:
69 |
70 | ```xml
71 |
73 | ```
74 |
75 | `EditText` doesn't have a `numberOfSets` attribute so this implies there's a corresponding binding adapter.
76 |
77 | In `NumberOfSetsBindingAdapters.kt`:
78 |
79 | ```kotlin
80 | @BindingAdapter("numberOfSets")
81 | @JvmStatic fun setNumberOfSets(view: EditText, value: String) {
82 | view.setText(value)
83 | }
84 | ```
85 |
86 | This sets the value from the ViewModel in the view.
87 |
88 | To complete the two-way syntax "@={}" handling, it's also required to define a Binding Adapter
89 | for a corresponding synthetic attribute with the "AttrChanged" suffix:
90 |
91 | ```kotlin
92 | @BindingAdapter(value = ["numberOfSetsAttrChanged"])
93 | @JvmStatic fun setListener(view: EditText, listener: InverseBindingListener?) {
94 | view.onFocusChangeListener = View.OnFocusChangeListener { focusedView, hasFocus ->
95 | val textView = focusedView as TextView
96 | if (hasFocus) {
97 | // Delete contents of the EditText if the focus entered.
98 | textView.text = ""
99 | } else {
100 | // If the focus left, update the listener
101 | listener?.onChange()
102 | }
103 | }
104 | }
105 | ```
106 |
107 | In this adapter you normally set the listeners that will detect changes in the view. Note that it
108 | includes an InverseBindingListener which needs to be called when we want to tell the data binding
109 | system that there's been a change. This, in turn, triggers calls to the InverseBindingAdapter:
110 |
111 | ```kotlin
112 | @InverseBindingAdapter(attribute = "numberOfSets")
113 | @JvmStatic fun getNumberOfSets(editText: EditText): String {
114 | return editText.text.toString()
115 | }
116 | ```
117 |
118 | See `NumberOfSetsBindingAdapters.kt` for alternatives and an example on how to use converters
119 | with two-way data binding.
120 |
121 | ## Alternatives two-way data binding
122 |
123 | Two-way data binding is an advanced feature that can be complicated. Instead of using the library's
124 | component to automate and remove boilerplate, beginners can opt for a more verbose but easier to
125 | understand approach, using one-way data binding.
126 |
127 | The `EditText` that manages the initial timer value has the following attributes:
128 |
129 | ```xml
130 |
134 | ```
135 |
136 | Similarly to the previous section, the backing property in the ViewModel needs to be converted
137 | before displaying and changes are sent to the ViewModel using a custom listener.
138 |
139 | See `BindingAdapters.kt` for the multiple binding adapters applied in this view and `Converter.kt`
140 | for the logic that converts between formats.
141 |
142 | ## Binding adapters with multiple parameters
143 |
144 | The progress bars in the sample need to be updated whenever either `progress` or the `max` property
145 | changes. The Binding Adapter for this case looks like:
146 |
147 | ```kotlin
148 | @BindingAdapter(value=["android:max", "android:progress"], requireAll = true)
149 | @JvmStatic fun updateProgress(progressBar: ProgressBar, max: Int, progress: Int) ...
150 |
151 | ```
152 |
153 | See `BindingAdapters.kt` for this example and `AnimationBindingAdapters.kt` for more.
154 |
155 | ## Animations with binding adapters
156 |
157 | Animators are also elements that can be bound to data and they usually involve verbose code for
158 | setup and execution. Data Binding lets you move this code out of the activities and fragments to
159 | a more convenient and isolated location: a binding adapter.
160 |
161 | For animations, two binding adapters are created in `AnimationBindingAdapters.kt`. They control
162 | the background color and some Constraint Layout parameters, directly bound to properties in the
163 | ViewModel.
164 |
165 | ## Binding converters and inverse converters
166 |
167 | Binding converters allow you to convert data to a format required by the binding adapter.
168 |
169 | See `Converter.kt` for one-way converters and `NumberOfSetsBindingAdapters.kt` for two-way
170 | converters.
171 |
172 | ## Data Binding with ViewModels and Kotlin
173 |
174 | ViewModels are a perfect fit for data binding because they expose data and state to the view and
175 | they survive configuration changes such as rotations.
176 |
177 | Common mistakes when using Kotlin with the Data Binding Library include:
178 | * Forgetting the `@JvmStatic` annotation in Binding Adapters inside an object or class.
179 | Alternatively you can place the functions in the top level of a file so it generates a public static
180 | method.
181 | * Annotation parameters syntax
182 |
183 | ## No UI framework calls in activity
184 |
185 | One of the important features of data binding is that is frees activities and fragments from making
186 | the UI calls, moving them to the layouts and Binding Adapters. However, not all
187 | framework calls need to be moved.
188 | In this sample, the activity is responsible for ViewModel creation, binding and
189 | managing the Shared Preferences, but there are no UI calls.
190 |
191 | This sample uses a relatively complex `IntervalTimerViewModel` that is exposing the data, receiving
192 | events and holding state for a relatively complex screen. There are multiple advantages to this:
193 | it makes very clear where a piece of code belongs to, it prevents activities from holding state,
194 | and it generates very reusable code (binding adapters).
195 |
196 | ## Testing
197 |
198 | There are no special considerations necessary regarding testing and the Data Binding Library.
199 | There is a UI test class that checks part of the interaction and a unit test class that
200 | verifies some logic in the ViewModel.
201 |
202 | License
203 | --------
204 |
205 | Copyright 2018 The Android Open Source Project, Inc.
206 |
207 | Licensed to the Apache Software Foundation (ASF) under one or more contributor
208 | license agreements. See the NOTICE file distributed with this work for
209 | additional information regarding copyright ownership. The ASF licenses this
210 | file to you under the Apache License, Version 2.0 (the "License"); you may not
211 | use this file except in compliance with the License. You may obtain a copy of
212 | the License at
213 |
214 | http://www.apache.org/licenses/LICENSE-2.0
215 |
216 | Unless required by applicable law or agreed to in writing, software
217 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
218 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
219 | License for the specific language governing permissions and limitations under
220 | the License.
221 |
--------------------------------------------------------------------------------
/TwoWaySample/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/TwoWaySample/app/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'com.android.application'
18 |
19 | apply plugin: 'kotlin-android'
20 |
21 | apply plugin: 'kotlin-kapt'
22 |
23 | android {
24 | compileSdkVersion rootProject.compileSdkVersion
25 | defaultConfig {
26 | applicationId "com.example.android.databinding.twowaysample"
27 | minSdkVersion rootProject.minSdkVersion
28 | targetSdkVersion rootProject.targetSdkVersion
29 | versionCode 1
30 | versionName "1.0"
31 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
32 | vectorDrawables.useSupportLibrary = true
33 | }
34 | buildTypes {
35 | release {
36 | minifyEnabled false
37 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
38 | }
39 | }
40 | buildFeatures {
41 | dataBinding true
42 | }
43 | }
44 |
45 | dependencies {
46 | implementation fileTree(dir: 'libs', include: ['*.jar'])
47 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
48 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
49 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
50 | implementation("androidx.activity:activity-ktx:$activityVersion")
51 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
52 |
53 | testImplementation "junit:junit:$junitVersion"
54 | testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
55 |
56 | androidTestImplementation "androidx.test:runner:$androidXTestRunnerVersion"
57 | androidTestImplementation "androidx.test.ext:junit:$androidXTestExtVersion"
58 | androidTestImplementation "androidx.test:rules:$androidXTestRulesVersion"
59 | androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/TwoWaySample/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/androidTest/java/com/example/android/databinding/twowaysample/BasicUsageTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample
18 |
19 | import androidx.test.espresso.Espresso.onView
20 | import androidx.test.espresso.action.ViewActions.click
21 | import androidx.test.espresso.action.ViewActions.pressImeActionButton
22 | import androidx.test.espresso.action.ViewActions.typeText
23 | import androidx.test.espresso.assertion.ViewAssertions.matches
24 | import androidx.test.espresso.matcher.ViewMatchers.withId
25 | import androidx.test.espresso.matcher.ViewMatchers.withText
26 | import androidx.test.ext.junit.runners.AndroidJUnit4
27 | import androidx.test.rule.ActivityTestRule
28 | import com.example.android.databinding.twowaysample.ui.MainActivity
29 | import org.junit.Rule
30 | import org.junit.Test
31 | import org.junit.runner.RunWith
32 |
33 |
34 | /**
35 | * UI tests that check that the two-way data binding inputs are working.
36 | *
37 | * For simplicity, only this feature is tested. A real app would inject a test timer and cover
38 | * more cases.
39 | */
40 | @RunWith(AndroidJUnit4::class)
41 | class BasicUsageTest {
42 |
43 | @get:Rule
44 | var activityRule = ActivityTestRule(MainActivity::class.java)
45 |
46 | @Test
47 | fun work_increments() {
48 | // Set an initial work time
49 | onView(withId(R.id.setWorkTime)).perform(typeText("5"), pressImeActionButton())
50 |
51 | // Increment it
52 | onView(withId(R.id.workplus)).perform(click())
53 |
54 | // Check that it was incremented and formatted correctly
55 | onView(withId(R.id.setWorkTime)).check(matches(withText("6.0")))
56 | }
57 |
58 | @Test
59 | fun work_decrements() {
60 | // Set an initial work time
61 | onView(withId(R.id.setWorkTime)).perform(typeText("5"), pressImeActionButton())
62 |
63 | // Decrement it
64 | onView(withId(R.id.workminus)).perform(click())
65 |
66 | // Check that it was incremented and formatted correctly
67 | onView(withId(R.id.setWorkTime)).check(matches(withText("4.0")))
68 | }
69 |
70 | @Test
71 | fun set_increments() {
72 | // Set an initial number of sets
73 | val initialSets = 5
74 | onView(withId(R.id.numberOfSets))
75 | .perform(typeText(initialSets.toString()), pressImeActionButton())
76 |
77 | // Increment it
78 | onView(withId(R.id.setsIncrease)).perform(click())
79 |
80 | // Check that it was incremented and formatted correctly
81 | val setsFormat = activityRule.activity.resources.getString(
82 | R.string.sets_format)
83 | onView(withId(R.id.numberOfSets))
84 | .check(matches(withText(String.format(setsFormat, 1, initialSets + 1))))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
20 |
21 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/data/IntervalTimerViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.data
18 |
19 | import androidx.lifecycle.ViewModel
20 | import androidx.lifecycle.ViewModelProvider
21 | import com.example.android.databinding.twowaysample.util.DefaultTimer
22 |
23 |
24 | /**
25 | * Factory for ViewModels
26 | */
27 | object IntervalTimerViewModelFactory : ViewModelProvider.Factory {
28 |
29 | override fun create(modelClass: Class): T {
30 | if (modelClass.isAssignableFrom(IntervalTimerViewModel::class.java)) {
31 | return IntervalTimerViewModel(DefaultTimer) as T
32 | }
33 | throw IllegalArgumentException("Unknown ViewModel class")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/ui/AnimationBindingAdapters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.ui
18 |
19 | import android.animation.ArgbEvaluator
20 | import android.animation.ObjectAnimator
21 | import android.animation.ValueAnimator
22 | import androidx.databinding.BindingAdapter
23 | import androidx.constraintlayout.widget.ConstraintLayout
24 | import androidx.core.content.ContextCompat
25 | import android.view.View
26 | import android.view.animation.DecelerateInterpolator
27 | import com.example.android.databinding.twowaysample.R
28 |
29 | /**
30 | * A collection of [BindingAdapter]s used to create animations in the app
31 | */
32 | object AnimationBindingAdapters {
33 |
34 | private const val VERTICAL_BIAS_ANIMATION_DURATION = 900L
35 |
36 | private const val BG_COLOR_ANIMATION_DURATION = 500L
37 |
38 | /**
39 | * Controls a background color animation.
40 | *
41 | * @param view one of the timers (work/rest)
42 | * @param timerRunning whether the app timer is running
43 | * @param activeStage whether this particular timer (work/rest) is active
44 | */
45 | @BindingAdapter(value=["animateBackground", "animateBackgroundStage"], requireAll = true)
46 | @JvmStatic fun animateBackground(view: View, timerRunning: Boolean, activeStage: Boolean) {
47 | // If the timer is not running, don't animate and set the default color.
48 | if (!timerRunning) {
49 | view.setBackgroundColor(
50 | ContextCompat.getColor(view.context, R.color.disabledInputColor))
51 | // This tag prevents a glitch going from reset to started.
52 | view.setTag(R.id.hasBeenAnimated, false)
53 | return
54 | }
55 |
56 | // activeStage controls whether this particular timer (work or rest) is active.
57 | if (activeStage) {
58 | // Start animation
59 | animateBgColor(view, true)
60 | // This tag prevents a glitch going from paused to started.
61 | view.setTag(R.id.hasBeenAnimated, true)
62 | } else {
63 | // Prevent "end" animation if animation never started
64 | val hasItBeenAnimated = view.getTag(R.id.hasBeenAnimated) as Boolean?
65 | if (hasItBeenAnimated == true) { // this means false if null
66 | // End animation
67 | animateBgColor(view, false)
68 | view.setTag(R.id.hasBeenAnimated, false)
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * Controls an animation that moves a view up and down.
75 | *
76 | * @param view one of the timers (work/rest)
77 | * @param timerRunning whether the app timer is running
78 | * @param activeStage whether this particular timer (work/rest) is active
79 | */
80 | @BindingAdapter(value=["animateVerticalBias", "animateVerticalBiasStage"],
81 | requireAll = true)
82 | @JvmStatic fun animateVerticalBias(view: View, timerRunning: Boolean, activeStage: Boolean) {
83 | // Change the vertical bias of the View depending on the current state
84 | when {
85 | timerRunning && activeStage -> animateVerticalBias(view, 0.6f) // Workout
86 | timerRunning && !activeStage -> animateVerticalBias(view, 0.4f) // Rest
87 | else -> animateVerticalBias(view, 0.5f) // Idle
88 | }
89 | }
90 |
91 | private fun animateBgColor(view: View, tint: Boolean) {
92 | val colorRes = ContextCompat.getColor(view.context, R.color.colorPrimaryLight)
93 | val color2Res = ContextCompat.getColor(view.context, R.color.disabledInputColor)
94 | val animator = if (tint)
95 | ObjectAnimator.ofObject(view,
96 | "backgroundColor",
97 | ArgbEvaluator(),
98 | color2Res,
99 | colorRes)
100 | else
101 | ObjectAnimator.ofObject(view,
102 | "backgroundColor",
103 | ArgbEvaluator(),
104 | colorRes,
105 | color2Res)
106 | animator.duration = BG_COLOR_ANIMATION_DURATION
107 | animator.start()
108 | }
109 |
110 | private fun animateVerticalBias(view: View, position: Float) {
111 | val layoutParams: ConstraintLayout.LayoutParams =
112 | view.layoutParams as ConstraintLayout.LayoutParams
113 | val animator = ValueAnimator.ofFloat(layoutParams.verticalBias, position)
114 | animator.addUpdateListener { animation ->
115 | val newParams: ConstraintLayout.LayoutParams =
116 | view.layoutParams as ConstraintLayout.LayoutParams
117 | val animatedValue = animation.animatedValue as Float
118 | newParams.verticalBias = animatedValue
119 | view.requestLayout()
120 | }
121 | animator.interpolator = DecelerateInterpolator()
122 | animator.duration = VERTICAL_BIAS_ANIMATION_DURATION
123 | animator.start()
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/ui/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.ui
18 |
19 | import android.content.Context
20 | import androidx.databinding.BindingAdapter
21 | import android.os.Build
22 | import android.view.View
23 | import android.view.inputmethod.EditorInfo
24 | import android.view.inputmethod.InputMethodManager
25 | import android.widget.EditText
26 | import android.widget.ProgressBar
27 | import android.widget.TextView
28 | import com.example.android.databinding.twowaysample.R
29 |
30 |
31 | /**
32 | * A collection of [BindingAdapter]s for different UI-related tasks.
33 | *
34 | * In Kotlin you can write the Binding Adapters in the traditional way:
35 | *
36 | * ```
37 | * @BindingAdapter("property")
38 | * @JvmStatic fun propertyMethod(view: ViewClass, parameter1: Param1, parameter2: Param2...)
39 | * ```
40 | *
41 | * Or using extension functions:
42 | *
43 | * ```
44 | * @BindingAdapter("property")
45 | * @JvmStatic fun ViewClass.propertyMethod(parameter1: Param1, parameter2: Param2...)
46 | * ```
47 | *
48 | * See [EditText.clearTextOnFocus].
49 | *
50 | * Also, keep in mind that @JvmStatic is only necessary if you define the methods inside a class or
51 | * object. Consider moving the Binding Adapters to the top level of the file.
52 | */
53 | object BindingAdapters {
54 |
55 | /**
56 | * When defined in an [EditText], this [BindingAdapter] will clear the text on focus and
57 | * set the previous value if the user doesn't enter one. When the focus leaves, it calls
58 | * the listener that was passed as an argument.
59 | *
60 | * Note that `android:selectAllOnFocus="true"` does something similar but not exactly the same.
61 | *
62 | * @see [clearTextOnFocus] for a version without a listener.
63 | */
64 | @BindingAdapter("clearOnFocusAndDispatch")
65 | @JvmStatic fun clearOnFocusAndDispatch(view: EditText, listener: View.OnFocusChangeListener?) {
66 | view.onFocusChangeListener = View.OnFocusChangeListener { focusedView, hasFocus ->
67 | val textView = focusedView as TextView
68 | if (hasFocus) {
69 | // Delete contents of the EditText if the focus entered.
70 | view.setTag(R.id.previous_value, textView.text)
71 | textView.text = ""
72 | } else {
73 | if (textView.text.isEmpty()) {
74 | val tag: CharSequence? = textView.getTag(R.id.previous_value) as CharSequence
75 | textView.text = tag ?: ""
76 | }
77 | // If the focus left, update the listener
78 | listener?.onFocusChange(focusedView, hasFocus)
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * Clears the text on focus.
85 | *
86 | * This method is using extension functions. It's equivalent to:
87 | * ```
88 | * @JvmStatic fun clearTextOnFocus(view: EditText, enabled: Boolean)...
89 | * ```
90 | */
91 | @BindingAdapter("clearTextOnFocus")
92 | @JvmStatic fun EditText.clearTextOnFocus(enabled: Boolean) {
93 | if (enabled) {
94 | clearOnFocusAndDispatch(this, null)
95 | } else {
96 | this.onFocusChangeListener = null
97 | }
98 | }
99 |
100 | /**
101 | * Hides keyboard when the [EditText] is focused.
102 | *
103 | * Note that there can only be one [TextView.OnEditorActionListener] on each [EditText] and
104 | * this [BindingAdapter] sets it.
105 | */
106 | @BindingAdapter("hideKeyboardOnInputDone")
107 | @JvmStatic fun hideKeyboardOnInputDone(view: EditText, enabled: Boolean) {
108 | if (!enabled) return
109 | val listener = TextView.OnEditorActionListener { _, actionId, _ ->
110 | if (actionId == EditorInfo.IME_ACTION_DONE) {
111 | view.clearFocus()
112 | val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE)
113 | as InputMethodManager
114 | imm.hideSoftInputFromWindow(view.windowToken, 0)
115 | }
116 | false
117 | }
118 | view.setOnEditorActionListener(listener)
119 | }
120 |
121 | /*
122 | * Instead of having if-else statements in the XML layout, you can create your own binding
123 | * adapters, making the layout easier to read.
124 | *
125 | * Instead of
126 | *
127 | * `android:visibility="@{viewmodel.isStopped ? View.INVISIBLE : View.VISIBLE}"`
128 | *
129 | * you use:
130 | *
131 | * `android:invisibleUnless="@{viewmodel.isStopped}"`
132 | *
133 | */
134 |
135 | /**
136 | * Makes the View [View.INVISIBLE] unless the condition is met.
137 | */
138 | @Suppress("unused")
139 | @BindingAdapter("invisibleUnless")
140 | @JvmStatic fun invisibleUnless(view: View, visible: Boolean) {
141 | view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
142 | }
143 |
144 | /**
145 | * Makes the View [View.GONE] unless the condition is met.
146 | */
147 | @Suppress("unused")
148 | @BindingAdapter("goneUnless")
149 | @JvmStatic fun goneUnless(view: View, visible: Boolean) {
150 | view.visibility = if (visible) View.VISIBLE else View.GONE
151 | }
152 |
153 | /**
154 | * In [ProgressBar], [ProgressBar.setMax] must be called before [ProgressBar.setProgress].
155 | * By grouping both attributes in a BindingAdapter we can make sure the order is met.
156 | *
157 | * Also, this showcases how to deal with multiple API levels.
158 | */
159 | @BindingAdapter(value=["android:max", "android:progress"], requireAll = true)
160 | @JvmStatic fun updateProgress(progressBar: ProgressBar, max: Int, progress: Int) {
161 | progressBar.max = max
162 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
163 | progressBar.setProgress(progress, false)
164 | } else {
165 | progressBar.progress = progress
166 | }
167 | }
168 |
169 | @BindingAdapter("loseFocusWhen")
170 | @JvmStatic fun loseFocusWhen(view: EditText, condition: Boolean) {
171 | if (condition) view.clearFocus()
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.ui
18 |
19 | import android.annotation.SuppressLint
20 | import androidx.activity.viewModels
21 | import android.content.Context
22 | import androidx.databinding.DataBindingUtil
23 | import androidx.databinding.Observable
24 | import androidx.databinding.ObservableInt
25 | import android.os.Bundle
26 | import androidx.appcompat.app.AppCompatActivity
27 | import android.util.Log
28 | import com.example.android.databinding.twowaysample.BR
29 | import com.example.android.databinding.twowaysample.R
30 | import com.example.android.databinding.twowaysample.data.IntervalTimerViewModel
31 | import com.example.android.databinding.twowaysample.data.IntervalTimerViewModelFactory
32 | import com.example.android.databinding.twowaysample.databinding.IntervalTimerBinding
33 |
34 | const val SHARED_PREFS_KEY = "timer"
35 |
36 | /**
37 | * This activity only takes care of binding a ViewModel to the layout. All UI calls are delegated
38 | * to the Data Binding library or Binding Adapters ([BindingAdapters]).
39 | *
40 | * Note that not all calls to the framework are removed, activities are still responsible for non-UI
41 | * interactions with the framework, like Shared Preferences or Navigation.
42 | */
43 | class MainActivity : AppCompatActivity() {
44 |
45 | private val intervalTimerViewModel: IntervalTimerViewModel
46 | by viewModels(
47 | factoryProducer = { IntervalTimerViewModelFactory }
48 | )
49 |
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 |
54 | val binding: IntervalTimerBinding = DataBindingUtil.setContentView(
55 | this, R.layout.interval_timer)
56 | val viewmodel = intervalTimerViewModel
57 | binding.viewmodel = viewmodel
58 |
59 | /* Save the user settings whenever they change */
60 | observeAndSaveTimePerSet(
61 | viewmodel.timePerWorkSet, R.string.prefs_timePerWorkSet)
62 | observeAndSaveTimePerSet(
63 | viewmodel.timePerRestSet, R.string.prefs_timePerRestSet)
64 |
65 | /* Number of sets needs a different */
66 | observeAndSaveNumberOfSets(viewmodel)
67 |
68 | if (savedInstanceState == null) {
69 | /* If this is the first run, restore shared settings */
70 | restorePreferences(viewmodel)
71 | observeAndSaveNumberOfSets(viewmodel)
72 | }
73 | }
74 |
75 | private fun observeAndSaveTimePerSet(timePerWorkSet: ObservableInt, prefsKey: Int) {
76 | timePerWorkSet.addOnPropertyChangedCallback(
77 | object : Observable.OnPropertyChangedCallback() {
78 | @SuppressLint("CommitPrefEdits")
79 | override fun onPropertyChanged(observable: Observable?, p1: Int) {
80 | Log.d("saveTimePerWorkSet", "Saving time-per-set preference")
81 | val sharedPref =
82 | getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE) ?: return
83 | sharedPref.edit().apply {
84 | putInt(getString(prefsKey), (observable as ObservableInt).get())
85 | commit()
86 | }
87 | }
88 | })
89 | }
90 |
91 | private fun restorePreferences(viewModel: IntervalTimerViewModel) {
92 | val sharedPref =
93 | getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE) ?: return
94 | val timePerWorkSetKey = getString(R.string.prefs_timePerWorkSet)
95 | var wasAnythingRestored = false
96 | if (sharedPref.contains(timePerWorkSetKey)) {
97 | viewModel.timePerWorkSet.set(sharedPref.getInt(timePerWorkSetKey, 100))
98 | wasAnythingRestored = true
99 | }
100 | val timePerRestSetKey = getString(R.string.prefs_timePerRestSet)
101 | if (sharedPref.contains(timePerRestSetKey)) {
102 | viewModel.timePerRestSet.set(sharedPref.getInt(timePerRestSetKey, 50))
103 | wasAnythingRestored = true
104 | }
105 | val numberOfSetsKey = getString(R.string.prefs_numberOfSets)
106 | if (sharedPref.contains(numberOfSetsKey)) {
107 | viewModel.numberOfSets = arrayOf(0, sharedPref.getInt(numberOfSetsKey, 5))
108 | wasAnythingRestored = true
109 | }
110 | if (wasAnythingRestored) Log.d("saveTimePerWorkSet", "Preferences restored")
111 | viewModel.stopButtonClicked()
112 | }
113 |
114 | private fun observeAndSaveNumberOfSets(viewModel: IntervalTimerViewModel) {
115 | viewModel.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
116 | @SuppressLint("CommitPrefEdits")
117 | override fun onPropertyChanged(observable: Observable?, p1: Int) {
118 | if (p1 == BR.numberOfSets) {
119 | Log.d("saveTimePerWorkSet", "Saving number of sets preference")
120 | val sharedPref =
121 | getSharedPreferences(SHARED_PREFS_KEY, Context.MODE_PRIVATE) ?: return
122 | sharedPref.edit().apply {
123 | putInt(getString(R.string.prefs_numberOfSets), viewModel.numberOfSets[1])
124 | commit()
125 | }
126 | }
127 | }
128 | })
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/ui/NumberOfSetsBindingAdapters.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.ui
18 |
19 | import android.content.Context
20 | import androidx.databinding.BindingAdapter
21 | import androidx.databinding.InverseBindingAdapter
22 | import androidx.databinding.InverseBindingListener
23 | import androidx.databinding.InverseMethod
24 | import android.view.View
25 | import android.widget.EditText
26 | import android.widget.TextView
27 | import com.example.android.databinding.twowaysample.R
28 |
29 |
30 | /**
31 | * The [EditText] that controls the number of sets is using two-way Data Binding. Applying a
32 | * 2-way expression to the `android:text` attribute of the EditText triggers an update on every
33 | * keystroke. This is an alternative implementation that uses a [View.OnFocusChangeListener]
34 | * instead.
35 | *
36 | * `numberOfSetsAttrChanged` creates a listener that triggers when the focus is lost
37 | *
38 | * `hideKeyboardOnInputDone` (in a different file) will clear focus when the `Done` action on
39 | * the keyboard is dispatched, triggering `numberOfSetsAttrChanged`.
40 | */
41 | object NumberOfSetsBindingAdapters {
42 |
43 | /**
44 | * Needs to be used with [NumberOfSetsConverters.setArrayToString].
45 | */
46 | @BindingAdapter("numberOfSets")
47 | @JvmStatic fun setNumberOfSets(view: EditText, value: String) {
48 | view.setText(value)
49 | }
50 |
51 | /**
52 | * Called when the [InverseBindingListener] of the `numberOfSetsAttrChanged` binding adapter
53 | * is notified of a change.
54 | *
55 | * Used with the inverse method of [NumberOfSetsConverters.setArrayToString], which is
56 | * [NumberOfSetsConverters.stringToSetArray].
57 | */
58 | @InverseBindingAdapter(attribute = "numberOfSets")
59 | @JvmStatic fun getNumberOfSets(editText: EditText): String {
60 | return editText.text.toString()
61 | }
62 |
63 | /**
64 | * That this Binding Adapter is not defined in the XML. "AttrChanged" is a special
65 | * suffix that lets you manage changes in the value, using two-way Data Binding.
66 | *
67 | * Note that setting a [View.OnFocusChangeListener] overrides other listeners that might be set
68 | * with `android:onFocusChangeListener`. Consider supporting both in the same binding adapter
69 | * with `requireAll = false`. See [android.databinding.adapters.CompoundButtonBindingAdapter]
70 | * for an example.
71 | */
72 | @BindingAdapter("numberOfSetsAttrChanged")
73 | @JvmStatic fun setListener(view: EditText, listener: InverseBindingListener?) {
74 | view.onFocusChangeListener = View.OnFocusChangeListener { focusedView, hasFocus ->
75 | val textView = focusedView as TextView
76 | if (hasFocus) {
77 | // Delete contents of the EditText if the focus entered.
78 | textView.text = ""
79 | } else {
80 | // If the focus left, update the listener
81 | listener?.onChange()
82 | }
83 | }
84 | }
85 |
86 | /* This sample showcases the NumberOfSetsConverters below, but note that they could be used
87 | also like: */
88 |
89 | @BindingAdapter("numberOfSets_alternative")
90 | @JvmStatic fun setNumberOfSets_alternative(view: EditText, value: Array) {
91 | view.setText(String.format(
92 | view.resources.getString(R.string.sets_format,
93 | value[0] + 1,
94 | value[1])))
95 | }
96 |
97 | @InverseBindingAdapter(attribute = "numberOfSets_alternative")
98 | @JvmStatic fun getNumberOfSets_alternative(editText: EditText): Array {
99 | if (editText.text.isEmpty()) {
100 | return arrayOf(0, 0)
101 | }
102 |
103 | return try {
104 | arrayOf(0, editText.text.toString().toInt()) // First item is not passed
105 | } catch (e: NumberFormatException) {
106 | arrayOf(0, 0)
107 | }
108 | }
109 | }
110 |
111 | /**
112 | * Converters for the number of sets attribute.
113 | */
114 | object NumberOfSetsConverters {
115 |
116 | /**
117 | * Used with `numberOfSets` to convert from array to String.
118 | */
119 | @InverseMethod("stringToSetArray")
120 | @JvmStatic fun setArrayToString(context: Context, value: Array): String {
121 | return context.getString(R.string.sets_format, value[0] + 1, value[1])
122 | }
123 |
124 | /**
125 | * This is the Inverse Method used in `numberOfSets`, to convert from String to array.
126 | *
127 | * Note that Context is passed
128 | */
129 | @JvmStatic fun stringToSetArray(unused: Context, value: String): Array {
130 | // Converts String to long
131 | if (value.isEmpty()) {
132 | return arrayOf(0, 0)
133 | }
134 |
135 | return try {
136 | arrayOf(0, value.toInt()) // First item is not passed
137 | } catch (e: NumberFormatException) {
138 | arrayOf(0, 0)
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/util/Converter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | @file:JvmName("Converter")
17 | package com.example.android.databinding.twowaysample.util
18 |
19 | import kotlin.math.round
20 |
21 | fun fromTenthsToSeconds(tenths: Int) : String {
22 | return if (tenths < 600) {
23 | String.format("%.1f", tenths / 10.0)
24 | } else {
25 | val minutes = (tenths / 10) / 60
26 | val seconds = (tenths / 10) % 60
27 | String.format("%d:%02d", minutes, seconds)
28 | }
29 | }
30 |
31 | fun cleanSecondsString(seconds: String): Int {
32 | // Remove letters and other characters
33 | val filteredValue = seconds.replace(Regex("""[^\d:.]"""), "")
34 | if (filteredValue.isEmpty()) return 0
35 | val elements: List = filteredValue.split(":").map { it -> round(it.toDouble()).toInt() }
36 |
37 | var result: Int
38 | return when {
39 | elements.size > 2 -> 0
40 | elements.size > 1 -> {
41 | result = elements[0] * 60
42 | result += elements[1]
43 | result * 10
44 | }
45 | else -> elements[0] * 10
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/util/ObservableViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.util
18 |
19 | import androidx.lifecycle.ViewModel
20 | import androidx.databinding.Bindable
21 | import androidx.databinding.Observable
22 | import androidx.databinding.PropertyChangeRegistry
23 |
24 | /**
25 | * An [Observable] [ViewModel] for Data Binding.
26 | */
27 | open class ObservableViewModel : ViewModel(), Observable {
28 |
29 | private val callbacks: PropertyChangeRegistry by lazy { PropertyChangeRegistry() }
30 |
31 | override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
32 | callbacks.add(callback)
33 | }
34 |
35 | override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
36 | callbacks.remove(callback)
37 | }
38 |
39 | /**
40 | * Notifies listeners that all properties of this instance have changed.
41 | */
42 | @Suppress("unused")
43 | fun notifyChange() {
44 | callbacks.notifyCallbacks(this, 0, null)
45 | }
46 |
47 | /**
48 | * Notifies listeners that a specific property has changed. The getter for the property
49 | * that changes should be marked with [Bindable] to generate a field in
50 | * `BR` to be used as `fieldId`.
51 | *
52 | * @param fieldId The generated BR id for the Bindable field.
53 | */
54 | fun notifyPropertyChanged(fieldId: Int) {
55 | callbacks.notifyCallbacks(this, fieldId, null)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/java/com/example/android/databinding/twowaysample/util/TimerWrapper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.util
18 |
19 | import java.util.*
20 |
21 | /**
22 | * The timer used by the app is customizable. This way tests can run synchronously and very fast.
23 | *
24 | * See [DefaultTimer] for the default implementation and IntervalTimerViewModelTest.kt for a
25 | * test implementation.
26 | */
27 | interface Timer {
28 |
29 | fun reset()
30 | fun start(task: TimerTask)
31 | fun getElapsedTime(): Long
32 | fun updatePausedTime()
33 | fun getPausedTime() : Long
34 | fun resetStartTime()
35 | fun resetPauseTime()
36 | }
37 |
38 | /**
39 | * The default timer is used in the normal execution of the app.
40 | */
41 | object DefaultTimer : Timer {
42 |
43 | private const val TIMER_PERIOD_MS = 100L
44 |
45 | private var startTime = System.currentTimeMillis()
46 | private var pauseTime = 0L
47 |
48 | override fun getPausedTime() : Long = pauseTime - startTime
49 |
50 | override fun getElapsedTime() = System.currentTimeMillis() - startTime
51 |
52 | override fun resetPauseTime() {
53 | pauseTime = System.currentTimeMillis()
54 | }
55 |
56 | override fun resetStartTime() {
57 | startTime = System.currentTimeMillis()
58 | }
59 |
60 | override fun updatePausedTime() {
61 | startTime += System.currentTimeMillis() - pauseTime
62 | }
63 |
64 | private var timer = java.util.Timer()
65 |
66 | override fun reset() {
67 | timer.cancel()
68 | }
69 |
70 | override fun start(task: TimerTask) {
71 | timer = java.util.Timer()
72 | timer.scheduleAtFixedRate(task, 0, TIMER_PERIOD_MS)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
23 |
28 |
29 |
35 |
38 |
41 |
42 |
43 |
44 |
50 |
51 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
27 |
32 |
37 |
42 |
47 |
52 |
57 |
62 |
67 |
72 |
77 |
82 |
87 |
92 |
97 |
102 |
107 |
112 |
117 |
122 |
127 |
132 |
137 |
142 |
147 |
152 |
157 |
162 |
167 |
172 |
177 |
182 |
187 |
188 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 | #aed581
20 | #E1E2E1
21 | #7da453
22 | #add681
23 | #819ca9
24 | #29434e
25 | #000000
26 | #ffffff
27 | #fffafafa
28 |
29 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | TwoWaySample
19 | Start
20 | Reset
21 | Pause
22 | -
23 | +
24 | Sets: %d/%d
25 | SHARED_PREFS_TIMEPERWORKSET
26 | SHARED_PREFS_TIMEPERRESTSET
27 | SHARED_PREFS_NUMBEROFSETS
28 |
29 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/main/res/values/tags.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
22 |
25 |
28 |
--------------------------------------------------------------------------------
/TwoWaySample/app/src/test/java/com/example/android/databinding/twowaysample/data/IntervalTimerViewModelTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.example.android.databinding.twowaysample.data
18 |
19 | import com.example.android.databinding.twowaysample.util.Timer
20 | import org.hamcrest.CoreMatchers.`is`
21 | import org.junit.Assert.assertThat
22 | import org.junit.Test
23 | import java.util.*
24 |
25 | /**
26 | * Unit tests for [IntervalTimerViewModel].
27 | *
28 | * For simplicity, only a feature is tested: The timer is started and paused at a specific time.
29 | * Then, the tests check that the exposed times are correct.
30 | */
31 | class IntervalTimerViewModelTest {
32 |
33 | @Test
34 | fun timePerWork_after100ms() {
35 | testWorkTimeLeftAfterTicks(10)
36 | }
37 |
38 | @Test
39 | fun timePerWork_after200ms() {
40 | testWorkTimeLeftAfterTicks(20)
41 | }
42 |
43 | @Test
44 | fun timePerWork_afterOneWorkCycle() {
45 | testWorkTimeLeftAfterTicks(INITIAL_SECONDS_PER_WORK_SET * 10)
46 | }
47 |
48 | @Test
49 | fun timePerWork_afterMoreThanOneWorkCycle() {
50 | // Create a ViewModel with a timer that stops after a a complete work set and a bit more.
51 | val viewModel = IntervalTimerViewModel(TestTimer(INITIAL_SECONDS_PER_WORK_SET * 10 + 10))
52 |
53 | // Start timer
54 | viewModel.timerRunning = true
55 |
56 | // Check that the timer exposes the correct data
57 | assertThat(viewModel.workTimeLeft.get(), `is`(0))
58 | assertThat(viewModel.restTimeLeft.get(), `is`(INITIAL_SECONDS_PER_REST_SET * 10 - 10 + 1))
59 | }
60 |
61 | @Test
62 | fun timePerWorkAndRest_afterWorkSet() {
63 | // Create a ViewModel with a timer that stops after a a complete work set and a bit more.
64 | val viewModel = IntervalTimerViewModel(TestTimer(INITIAL_SECONDS_PER_WORK_SET * 10))
65 |
66 | // Start timer
67 | viewModel.timerRunning = true
68 |
69 | // Check that the timer exposes the correct data
70 | assertThat(viewModel.workTimeLeft.get(), `is`(1))
71 | assertThat(viewModel.restTimeLeft.get(), `is`(INITIAL_SECONDS_PER_REST_SET * 10))
72 | }
73 |
74 | private fun testWorkTimeLeftAfterTicks(tenths: Int) {
75 | // Create a ViewModel with a timer that stops after a time.
76 | val viewModel = IntervalTimerViewModel(TestTimer(tenths))
77 |
78 | // Start timer
79 | viewModel.timerRunning = true
80 |
81 | // Check that the timer exposes the correct data
82 | assertThat(viewModel.workTimeLeft.get(), `is`(INITIAL_SECONDS_PER_WORK_SET * 10 - tenths + 1))
83 | }
84 | }
85 |
86 | /**
87 | * A timer used for tests that executes a task a number of times. It uses ticks instead of
88 | * a real clock.
89 | */
90 | class TestTimer(private val ticks: Int = 5) : Timer {
91 | private var running = false
92 | private var elapsedTicks = 0
93 | private var startTime = 0L
94 | private var pauseTime = 0L
95 |
96 | override fun start(task: TimerTask) {
97 | running = true
98 | while (running && elapsedTicks < ticks) {
99 | task.run()
100 | elapsedTicks++
101 | }
102 | }
103 |
104 | override fun reset() {
105 | running = false
106 | }
107 |
108 | override fun getPausedTime() : Long = pauseTime - startTime
109 |
110 | override fun getElapsedTime() = (elapsedTicks * 100) - startTime
111 |
112 | override fun resetPauseTime() {
113 | pauseTime - (elapsedTicks * 100)
114 | }
115 |
116 | override fun resetStartTime() {
117 | startTime = (elapsedTicks * 100).toLong()
118 | }
119 |
120 | override fun updatePausedTime() {
121 | startTime += (elapsedTicks * 100) - pauseTime
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/TwoWaySample/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
18 |
19 | buildscript {
20 | ext {
21 | // SDK and tools
22 | compileSdkVersion = 33
23 | minSdkVersion = 21
24 | targetSdkVersion = 33
25 |
26 | // App dependencies
27 | activityVersion = '1.6.1'
28 | constraintLayoutVersion = '2.1.4'
29 | espressoVersion = '3.5.1'
30 | gradleVersion = '7.4.0'
31 | junitVersion = '4.13.2'
32 | hamcrestVersion = '1.3'
33 | kotlinVersion = '1.8.0'
34 | androidXTestRunnerVersion = '1.5.2'
35 | androidXTestExtVersion = '1.1.5'
36 | androidXTestRulesVersion = '1.5.0'
37 | appCompatVersion = '1.6.0'
38 | archLifecycleVersion = '2.5.1'
39 | }
40 |
41 | repositories {
42 | google()
43 | mavenCentral()
44 | }
45 | dependencies {
46 | classpath "com.android.tools.build:gradle:$gradleVersion"
47 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
48 |
49 | // NOTE: Do not place your application dependencies here; they belong
50 | // in the individual module build.gradle files
51 | }
52 | }
53 |
54 | allprojects {
55 | repositories {
56 | google()
57 | mavenCentral()
58 | }
59 | }
60 |
61 | task clean(type: Delete) {
62 | delete rootProject.buildDir
63 | }
64 |
--------------------------------------------------------------------------------
/TwoWaySample/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2018 The Android Open Source Project
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | # Project-wide Gradle settings.
18 |
19 | # IDE (e.g. Android Studio) users:
20 | # Gradle settings configured through the IDE *will override*
21 | # any settings specified in this file.
22 |
23 | # For more details on how to configure your build environment visit
24 | # http://www.gradle.org/docs/current/userguide/build_environment.html
25 |
26 | # Specifies the JVM arguments used for the daemon process.
27 | # The setting is particularly useful for tweaking memory settings.
28 | android.enableJetifier=true
29 | android.useAndroidX=true
30 | org.gradle.jvmargs=-Xmx1536m
31 |
32 | # When configured, Gradle will run in incubating parallel mode.
33 | # This option should only be used with decoupled projects. More details, visit
34 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
35 | # org.gradle.parallel=true
36 |
--------------------------------------------------------------------------------
/TwoWaySample/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/TwoWaySample/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/TwoWaySample/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/TwoWaySample/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/TwoWaySample/screenshots/screenshot2way.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/android/databinding-samples/b6ef92fb6099b6320a8be7b6047000f53610c3a1/TwoWaySample/screenshots/screenshot2way.png
--------------------------------------------------------------------------------
/TwoWaySample/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2018 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | include ':app'
18 |
--------------------------------------------------------------------------------
/scripts/checksum.sh:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2022 Google, Inc.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | #!/bin/bash
18 | SAMPLE=$1
19 | RESULT_FILE=$2
20 |
21 | if [ -f $RESULT_FILE ]; then
22 | rm $RESULT_FILE
23 | fi
24 | touch $RESULT_FILE
25 |
26 | checksum_file() {
27 | echo $(openssl md5 $1 | awk '{print $2}')
28 | }
29 |
30 | FILES=()
31 | while read -r -d ''; do
32 | FILES+=("$REPLY")
33 | done < <(find $SAMPLE -type f \( -name "build.gradle*" -o -name "gradle-wrapper.properties" -o -name "robolectric.properties" \) -print0)
34 |
35 | # Loop through files and append MD5 to result file
36 | for FILE in ${FILES[@]}; do
37 | echo $(checksum_file $FILE) >> $RESULT_FILE
38 | done
39 | # Now sort the file so that it is idempotent
40 | sort $RESULT_FILE -o $RESULT_FILE
--------------------------------------------------------------------------------