SmileWeather之DataBinding篇
前言
本系列文章将以SmileWeather为实例来记录一下如何开发一款简单的APP,麻雀虽小五脏俱全。项目地址
基本介绍
DataBinding也是属于Jetpack中的一部分,Google开发文档中是这样介绍的:
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化的)将布局中的界面组件绑定到应用中的数据源。
简单来讲就是使用一些表达式来将数据与控件进行绑定,当然这就分了单向绑定和双向绑定。顾名思义单向绑定是由数据去驱动UI,双向绑定则是数据和UI可以互相驱动映射。接下来就简单的记录一下两者在项目中如何使用,和日常开发过程中会碰到哪些bug。
集成到项目
在应用模块的 build.gradle 文件中添加 dataBinding 元素
android {
...
dataBinding {
enabled = true
}
}
然后在布局文件中添加一些标签,代表这个XML是需要拿来绑定的,这些标签是最外层的。data里面我们可以引入一些包(import)和添加我们需要的一些变量(variable)。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto"
>
<data>
<import type="com.smile.weather.entity.NowEntity"/>
<import type="android.view.View"/>
<variable
name="nowData"
type="NowEntity" />
<variable
name="loadByCode"
type="java.lang.Integer" />
<variable
name="cityName"
type="java.lang.String" />
<variable
name="isLocate"
type="java.lang.Boolean" />
</data>
....
</layout>
单向绑定
接下来我们要一个TextView显示城市名,关键地方就是 android:text="@{cityName}"
这样我们的数据就与TextView绑定了。
<TextView
android:id="@+id/detail_head_address_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/detail_head_temp_content_tv"
android:textColor="@color/white"
android:textSize="30sp"
android:text="@{cityName}"
android:layout_marginLeft="10dp"
app:layout_constraintLeft_toRightOf="@+id/detail_head_locate_img"
/>
这就完了?
不存在的,我们还可以通过数据控制View的显示和隐藏以及响应点击事件,ImageView还可以加载网络图片…
DataBinding还支持很多表达式和运算符,下面列举一下:
- 算术运算符 + - / * %
- 字符串连接运算符 +
- 逻辑运算符 && ||
- 二元运算符 & | ^
- 一元运算符 + - ! ~
- 移位运算符 >> >>> <<
- 比较运算符 == > < >= <=(请注意,< 需要转义为 <)
- instanceof
- 分组运算符 ()
- 字面量运算符 - 字符、字符串、数字、null
- 类型转换
- 方法调用
- 字段访问
- 数组访问 []
- 三元运算符 ?:
接下来我们通过三元运算符来实现一个控制View的显示和隐藏
<ImageView
android:id="@+id/detail_head_locate_img"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/icon_local"
android:layout_marginLeft="30dp"
android:visibility="@{isLocate ? View.VISIBLE : View.INVISIBLE }"
app:layout_constraintTop_toTopOf="@id/detail_head_address_tv"
app:layout_constraintBottom_toBottomOf="@id/detail_head_address_tv"
app:layout_constraintLeft_toLeftOf="parent"/>
这些还远远不够,我们还可以自定义一些方法来实现我们的需要,比如说,我想要一个ImageView根据天气code来加载不同的icon,这就需要我们来进行自定义方法了。
关键字 @BindingAdapter("name")
代码如下:
@BindingAdapter("loadByCode")
@JvmStatic
fun loadImageByCode(imageView: ImageView, code: String?) {
if (code != null) {
imageView.setImageResource(IconUtils.getSmallIcon(code.toInt()!!))
}
}
<ImageView
android:id="@+id/item_f_icon_day_img"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/s_unknown"
bind:loadByCode="@{dailyForecast.iconDay}"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginLeft="10dp"
app:layout_constraintTop_toBottomOf="@id/item_f_time_tv" />
这里有个踩坑记录一下,由于Kotlin没有static的特性和null-safe检查所以需要添加@JvmStatic注解并且在参数后面加?规避空指针异常,记得引入命名空间。
xmlns:bind="http://schemas.android.com/apk/res-auto"
双向绑定
顾名思义就是View层对数据进行了操作会改变数据源里面的内容,数据源改变了会影响到View层,单向绑定和双向绑定表达方式很像,就是"@{}“改成了”@={}"。项目中的一个地方就使用到了,就是搜索城市的时候,我们输入了城市名后不需要我们再去手动获取EditText里面的内容,双向绑定就是数据驱动视图,视图也能驱动数据。
<EditText
android:id="@+id/search_input_et"
android:layout_width="0dp"
android:layout_height="40dp"
app:layout_constraintLeft_toRightOf="@id/search_return_img"
app:layout_constraintTop_toTopOf="@id/search_return_img"
app:layout_constraintBottom_toBottomOf="@id/search_return_img"
app:layout_constraintRight_toLeftOf="@id/search_img"
android:background="@drawable/shape_search_input"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:text="@={ viewModel.mInputCity }"
android:maxLines="1"
/>
好了,开始进阶双向绑定,就是SwipeRefreshLayout的使用,在项目中有使用到下拉刷新更新数据,我们知道可以通过setRefreshing(boolean b)
来控制SwipeRefreshLayout的状态,关闭和开启,所以可以使用一个LiveData来驱动视图(SwipeRefreshLayout),重点是如何通过View来驱动数据。
其实只要我们获取到View的状态就可以来驱动LiveData数据。
代码先列出来,慢慢讲解
object SwipeRefreshLayoutHandler {
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
@JvmStatic
fun setSwipeRefreshLayoutRefreshing(
swipeRefreshLayout: SwipeRefreshLayout,
newValue: Boolean
) {
if (swipeRefreshLayout.isRefreshing != newValue)
swipeRefreshLayout.isRefreshing = newValue!!
}
@JvmStatic
@InverseBindingAdapter(
attribute = "app:bind_swipeRefreshLayout_refreshing",
event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"
)
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean? =
swipeRefreshLayout.isRefreshing
@BindingAdapter(
"app:bind_swipeRefreshLayout_refreshingAttrChanged",
requireAll = false
)
@JvmStatic
fun setOnRefreshListener(
swipeRefreshLayout: SwipeRefreshLayout,
bindingListener: InverseBindingListener?
) {
if (bindingListener != null)
swipeRefreshLayout.setOnRefreshListener {
bindingListener.onChange()
}
}
}
- 第一个方法很容易理解就是给view使用的,将view与数据进行绑定。
- 第二个方法应该后面再讲,他主要是用来桥接第一个和第三个方法,所以我就放在中间了。
- 第三个方法是拿来观察view的状态变化,SwipeRefreshLayout每次的刷新状态都是通过这个来进行监听,然后告诉InverseBindingListener去通知DataBinding。
- 开始就说了第二个方法是拿来桥接的有没有看到
event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"
与第三个方法的注解内容一样。
整个流程就是用户下拉刷新的时候 InverseBindingListener通知DataBinding,LiveData就会从SwipeRefreshLayout.isRefreshing中获取状态,并且对数据进行同步。
对了,这个还有一个关键点,就是view驱动数据,数据又驱动view就成了死循环了,所以我们需要对状态进行一个判断,状态改变了才会去驱动,第一个方法就有做了处理。
if (swipeRefreshLayout.isRefreshing != newValue)
swipeRefreshLayout.isRefreshing = newValue!!
结尾
差不多记录了一些DataBinding的基本用法,当然这都是一些比较简单的操作,日常开发过程中还会有更加复杂的业务逻辑,所以需要后面还得继续探索。