Google Kotlin基础训练营Cupcake应用RadioButton绑定错误求助
参与Google Kotlin基础训练营,使用Shared ViewModel构建Cupcake应用,代码与官方仓库完全一致,但所有RadioButton的android:checked属性绑定出现错误,提示表达式与checked的boolean属性不兼容。
错误信息
@{viewModel.date.equals(viewModel.dateOptions[2])' is incompatible with attribute checked (attr) boolean.
相关代码
FlavorFragment布局代码
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2020 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- Layout for selecting a flavor --> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".FlavorFragment"> <data> <variable name="viewModel" type="com.example.cupcake.model.OrderViewModel" /> <variable name="flavorFragment" type="com.example.cupcake.FlavorFragment" /> </data> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/side_margin"> <RadioGroup android:id="@+id/flavor_options" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <RadioButton android:id="@+id/vanilla" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.flavor.equals(@string/vanilla)}" android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}" android:text="@string/vanilla" /> <RadioButton android:id="@+id/chocolate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.flavor.equals(@string/chocolate)}" android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}" android:text="@string/chocolate" /> <RadioButton android:id="@+id/red_velvet" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.flavor.equals(@string/red_velvet)}" android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}" android:text="@string/red_velvet" /> <RadioButton android:id="@+id/salted_caramel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}" android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}" android:text="@string/salted_caramel" /> <RadioButton android:id="@+id/coffee" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.flavor.equals(@string/coffee)}" android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}" android:text="@string/coffee" /> </RadioGroup> <View android:id="@+id/divider" style="@style/Widget.Cupcake.Divider" android:layout_width="0dp" android:layout_height="1dp" android:layout_marginTop="@dimen/side_margin" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/flavor_options" /> <TextView android:id="@+id/subtotal" style="@style/Widget.Cupcake.TextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/side_margin" android:text="@{@string/subtotal_price(viewModel.price)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/divider" tools:text="Subtotal $5.00" /> <Button android:id="@+id/cancel_button" style="?attr/materialButtonOutlinedStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/side_margin" android:onClick="@{() -> flavorFragment.cancelOrder()}" android:text="@string/cancel" app:layout_constraintEnd_toStartOf="@id/next_button" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/next_button" /> <Button android:id="@+id/next_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/side_margin" android:onClick="@{() -> flavorFragment.goToNextScreen()}" android:text="@string/next" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/cancel_button" app:layout_constraintTop_toBottomOf="@id/subtotal" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> </layout>
OrderViewModel代码
package com.example.cupcake.model import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.* private const val PRICE_PER_CUPCAKE = 2.00 private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00 class OrderViewModel : ViewModel() { init { resetOrder() } private val _quantity = MutableLiveData<Int>() val quantity: LiveData<Int> = _quantity private val _flavor = MutableLiveData<String>() val flavor: LiveData<String> = _flavor private val _date = MutableLiveData<String>() val date: LiveData<String> = _date private val _price = MutableLiveData<Double>() val price: LiveData<String> = Transformations.map(_price) { NumberFormat.getCurrencyInstance().format(it) } val dateOptions = getPickupOptions() fun setQuantity(numberCupcakes: Int) { _quantity.value = numberCupcakes updatePrice() } fun setDate(pickupDate: String) { _date.value = pickupDate updatePrice() } fun setFlavor(desiredFlavor: String) { _flavor.value = desiredFlavor } fun hasNoFlavorSet(): Boolean { return _flavor.value.isNullOrEmpty() } private fun getPickupOptions(): List<String> { val options = mutableListOf<String>() val formatter = SimpleDateFormat("EE MMM d", Locale.getDefault()) val calendar = Calendar.getInstance() // Create a list of dates starting with the current date and the following 3 dates repeat(4) { options.add(formatter.format(calendar.time)) calendar.add(Calendar.DATE, 1) } return options } private fun updatePrice() { var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE // If the user selected the first option (today) for pickup, add the surcharge if (dateOptions[0] == _date.value) { calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP } _price.value = calculatedPrice } fun resetOrder() { _quantity.value = 0 _flavor.value = "" _date.value = dateOptions[0] _price.value = 0.0 } }
PickupFragment布局代码
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2020 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- Layout for selecting a pickup date --> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context=".PickupFragment"> <data> <variable name="viewModel" type="com.example.cupcake.model.OrderViewModel" /> <variable name="pickupFragment" type="com.example.cupcake.PickupFragment" /> </data> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/side_margin"> <RadioGroup android:id="@+id/date_options" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <RadioButton android:id="@+id/option0" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])" android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}" android:text="@{viewModel.dateOptions[0]}" tools:text="Thursday" /> <RadioButton android:id="@+id/option1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked= "@{viewModel.date.equals(viewModel.dateOptions[1])}" android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}" android:text="@{viewModel.dateOptions[1]}" tools:text="Friday" /> <RadioButton android:id="@+id/option2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])" android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}" android:text="@{viewModel.dateOptions[2]}" tools:text="Saturday" /> <RadioButton android:id="@+id/option3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])" android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}" android:text="@{viewModel.dateOptions[3]}" tools:text="Sunday" /> </RadioGroup> <View android:id="@+id/divider" style="@style/Widget.Cupcake.Divider" android:layout_width="0dp" android:layout_height="1dp" android:layout_marginTop="@dimen/side_margin" android:layout_marginBottom="@dimen/side_margin" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/date_options" /> <TextView android:id="@+id/subtotal" style="@style/Widget.Cupcake.TextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/side_margin" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/divider" android:text="@{@string/subtotal_price(viewModel.price)" tools:text="Subtotal $5.00" /> <Button android:id="@+id/next_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/side_margin" android:text="@string/next" android:onClick="@{() -> pickupFragment.goToNextScreen()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/subtotal" /> </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> </layout>
解决方案
1. 修复XML语法错误
PickupFragment布局中多个表达式存在语法缺失,导致编译器无法正确解析类型:
- 补全option0、option2、option3的
android:checked属性表达式末尾的},例如将android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])"改为android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}" - 补全subtotal的
android:text属性表达式末尾的},改为android:text="@{@string/subtotal_price(viewModel.price)}"
2. 优化Data Binding表达式(可选)
由于viewModel.date和viewModel.flavor都是LiveData类型,Data Binding会自动解包获取当前值,可使用Kotlin的==运算符替代equals()方法,表达式更简洁且语义一致:
- 将
@{viewModel.date.equals(viewModel.dateOptions[0])}修改为@{viewModel.date == viewModel.dateOptions[0]} - 将
@{viewModel.flavor.equals(@string/vanilla)}修改为@{viewModel.flavor == @string/vanilla}
完成上述修改后,编译器即可正确识别表达式的boolean返回值,与checked属性的类型匹配,错误即可解决。
内容的提问来源于stack exchange,提问作者Meet Soni
相关产品推荐
相关产品推荐

