Jetpack:你还在findViewById么?

栏目: IOS · Android · 发布时间: 5年前

内容简介:又到周末好时光,开始嗨之前再抽点时间看看本文,阅读本文入门需要5分钟,进阶需要20分钟,掌握需要30分钟,运用需要时间就有点长了......能看到最后的都是大佬,收下我的膜拜。本文技术内容讲的是关于Data Binding Library的那点事,有的同学可能解过了,有的娃可能都不知道是什么东东...。为了不落伍,和大家一样优秀,决定写Jetpack方面的文章。与别人不一样的是,会加入自己的理解和栗子,而不是简单的翻译(我英文水平也不行)。如果大家发现有误的地方,希望多加指点,在此谢过。一年前有缘看了一下J

又到周末好时光,开始嗨之前再抽点时间看看本文,阅读本文入门需要5分钟,进阶需要20分钟,掌握需要30分钟,运用需要时间就有点长了......能看到最后的都是大佬,收下我的膜拜。本文技术内容讲的是关于Data Binding Library的那点事,有的同学可能解过了,有的娃可能都不知道是什么东东...。为了不落伍,和大家一样优秀,决定写Jetpack方面的文章。与别人不一样的是,会加入自己的理解和栗子,而不是简单的翻译(我英文水平也不行)。如果大家发现有误的地方,希望多加指点,在此谢过。

About Jetpack

一年前有缘看了一下Jetpack,但并没有过多的去关注,最近在看Google IO 2019相关资料,看到了Jetpack的身影,不得不陷入深思,无法自拔。

JetPack的官方说法:

Jetpack 是 Android 软件组件的集合,使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

总结性

  • 加速开发:以组件的形式供我们依赖使用。
  • 消除样板代码:还记得在 Activity 中一大堆 findViewById 么?能做的不止这么多。
  • 构建高质量应用:现在化设计、避开bug、向后兼容。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助提高工作效率。可全部使用,也可混合搭配!

以上是对官网的摘录。作为开山之篇,先从架构方向的 数据绑定库 入门开始,让同学感受它的魅力。

Data Binding Library(数据绑定库)

借助数据绑定库(Data Binding Library),可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。数据绑定库要求在Android 4.0以上,Gradle 1.5.0以上。实践证明Android SDK和Gradle版本越高,对Data Binding的支持越好,越简单,速度越快。

举个栗子,这个栗子不重,两只手指可以举起来:

findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}
复制代码

栗子中通过findViewById找到TextView组件,并将其绑定到 viewModel 变量的 userName 属性。而下面在布局文件中使用数据绑定库将文本直接分配到TextView组件上,这样就无需调用上述任何 Java 代码。

<TextView  android:text="@{viewmodel.userName}" />
复制代码

竟然这么好用,为啥不了解看看呢?

配置

在我们的项目 build.gradle 文件下配置如下代码。

android {
    ...
    dataBinding {
        enabled = true
    }
}
复制代码

如果Gradle插件版本在 3.1.0-alpha06 以上,可以使用新的Data Binding编译器,有利于加速绑定数据文件的生成。在项目的 gradle.properties 文件添加如下配置。

android.databinding.enableV2=true
复制代码

同步一下,没什么问题的话,配置已经成功了~

入门

  • 定义一个数据对象
data class User(var name: String, var age: Int)
复制代码
  • 布局绑定

我们创建名为activity_main.xml的布局文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.gitcode.jetpack.User"/>
   </data>

   <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">
       //在TextView中使用
       <TextView android:layout_width="match_parent"
                 android:gravity="center"
                 android:text="@{user.name}"
                 android:layout_height="match_parent"/>
   </LinearLayout>
</layout>
复制代码

布局文件的根元素不再是以往的LinearLayout、RelativeLayout等等,而是layout。在 data 元素内添加 variable ,其属性表示声明一个 com.gitcode.jetpack.User 类型的变量 user 。如果多个变量的话,可在 data 元素下添加多个 varialbe 元素,格式是一致的。

<data>
   <variable name="user" type="com.gitcode.jetpack.User"/>
   <variable name="time" type="com.gitcode.jetpack.Time"/>
</data>
复制代码

@{} 语法中使用表达式将变量赋值给view的属性。例如:这里将user变量的firstName属性赋值给TextView的text属性。

android:text="@{user.firstName}"
复制代码
  • 绑定数据

此时布局声明的user变量值还是初始值,我们需要为其绑定数据。

默认情况下,会根据目前布局文件名称来生成一个绑定类(binding class),例如当前布局文件名是activity_main,那么生成的类名就是ActivityMainBinding。

绑定类会拥有当前布局声明变量,并声明getter或者setter方法,也就是说ActivityMainBinding类会带有user属性和getUser、setUser方法,变量的默认初始化与Java一致:引用类型为null,int为0,bool为false。

在MainActivity的onCreate()方法中添加如下代码,将数据绑定到布局上。

val binding: ActivityMainBinding 
        = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.user = User("GitCode", 3)
复制代码

经典代码是这样的:

setContentView(R.layout.activity_main)
val user=User("GitCode",3)
val tvName=findViewById<TextView>(R.id.tvName)
tvName.text = user.name
复制代码

可有看出,使用数据绑定库会使代码简洁很多,可读性也很高。 运行一下项目,既可以考到效果了~

Jetpack:你还在findViewById么?

如果是在Fragment、Adapter中使用,那就要换个姿势了。

val listItemBinding = ListItemBinding
            .inflate(layoutInflater, viewGroup, false)
//或者
val listItemBinding = DataBindingUtil
            .inflate(layoutInflater, R.layout.list_item, viewGroup, false)

复制代码

恭喜,你已经入门了

可以选择继续学习,

看下文

也可以当做了解

点个赞

看看其他文章了~

布局与绑定表达式

在一开始介绍Data Binding Libaray时,就使用了 @{} 语法,花括号里面的内容称为绑定表达式,绑定表达式其实并不复杂,跟我们正常使用Java和Kotlin语言的表达式没多大区别。那我们可以在表达式中使用什么类型的运算符或者关键字呢?

常用运算符

运算符 符号
算术 加、减、乘、除、求余(+ 、 - 、* 、/、 %)
逻辑 与、或(&&、||)
一元 + 、-、 !、 ~
移位 >>、 >>>、 <<
关系 == 、> 、<、 >= 、<=(使用符号<时,要换成&lt;)

其他常用的

同时也支持字符拼接 + , instanceof ,分组、属性访问、数组访问、 ?: 、转型、访问调用,基本类型等等等。 也就是说,绑定表达式语言大多数跟宿主代码(Java or Kotlin)的表达式差不多。为什么说是大多数,因为不能使用 thissupernewExplicit generic invocation (明确的泛型调用)等。

丢个栗子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
复制代码

再举丢个栗子:

android:text="@{user.displayName ?? user.lastName}"
复制代码

如果 user.displayName 不为null则使用,否则使用 user.lastName .在这里也看得出,可以通过表达式访问类的属性。绑定类会自动检查当前变量是否为null,以避免发生空指针异常。栗子:如果 user 变量为null,那么 user.lastName 也会是null。

集合

像数组,链表,Maps等常见的集合,都可以采用下标 [] 访问它们的元素。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
//或者
android:text="@{map.key}"
复制代码

注意在 data 元素内添加了 import 元素,表示导入该类型的定义,这样表达式中引用属性可读性高点,使用也方便。

来个容易掰的栗子:

<data>
    <import type="android.view.View"/>
</data>

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
复制代码

通过导入View类型,就可以使用相关属性,例如这里的 View.VISIBLE

有时导入的类全名太长了或者存在相同类型的类名,我们就可以给它取个别名,然后就可用别名进行coding~

<import type="android.view.View"/>

<import type="com.gitcode.jetpack.View"
        alias="JView"/>
复制代码

使用资源

使用下面语法:

android:padding="@{@dimen/largePadding}"
复制代码

相关资源的的表达式引用,贴张官网截图:

Jetpack:你还在findViewById么?

事件处理

数据绑定库允许我们在事件到View时候通过表达式去处理它。 在数据绑定库中支持两种机制:方法调用和监听器绑定。

好想一笔带过,因为原文看不明白~~~~(>_<)~~~~
复制代码

方法调用

点击事件会直接绑定到处理方法上,当一个事件发生,会直接传给绑定的方法。类似我们在布局上使用 android:onclick 与Activity 的方法绑定。在编译的时候已经绑定,在 @{} 表达式中的方法如果在Activity找不到或者方法名错误,就会在编译时期报错,方法签名(返回类型和参数相同)一致。

丢个栗子:

定义一个接口,用于处理事件。

//定义一个处理点击事件的类
interface  MethodHandler {
    fun onClick(view: View)
}
复制代码

在布局声明了methodHandler变量,并在Button的 onClick 方法使用表达式 @{methodHandler::onClick} , onClick 方法需要与上面接口一致,不然编译器期报错。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        ...
        <variable name="methodHandler"
            type="com.gitcode.jetpack.MethodHandler"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal"
            android:layout_height="match_parent">
         ...
        <Button android:layout_width="wrap_content"
                android:text="Method references"
                android:layout_marginTop="10dp"
                android:onClick="@{methodHandler::onClick}"
                android:layout_height="wrap_content"/>
    </LinearLayout>
</layout>
复制代码

然后在Activity中实现MethodHandler,并赋值给绑定类的变量。

class MainActivity : AppCompatActivity(), MethodHandler{
    lateinit var binding: ActivityMainBinding

      override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.methodHandler = this
    }
    
    override fun onClick(view: View) {
        Log.i(TAG, "Method handling")
    }
}
复制代码

因此,当我们点击Button的时候,Activity的onClick方法就会被回调。

监听器绑定

监听器绑定与方法调用不同的是,监听器不再编译器与处理方法绑定,而是在点击事件传递到当前view时,才与处理方法绑定,而且监听器并不要表达式方法名与处理方法同名,只要返回类型一致即可,如果有返回值得话。

来个栗子:

  • 定义接口用于处理事件
interface  ListenerHandler {
    fun onClickListener(view: View)
}
复制代码
  • 在布局中定义变量和表达式
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="listener" type="com.gitcode.jetpack.ListenerHandler"/>
    </data>

    <Button android:layout_width="wrap_content"
            android:text="Listener"
            android:layout_marginTop="10dp"
            android:onClick="@{(view)->listener.onClickListener(view)}"
            android:layout_height="wrap_content"/>
    </LinearLayout>
<layout>
复制代码

注意到使用lambda表达式,因此可以在 @{} 内做更多操作,如预处理数据等。

  • 处理方法 同样在Activity实现ListenerHandler方法,并赋值给绑定类的变量。
class MainActivity : AppCompatActivity(), ListenerHandler {
    lateinit var binding: ActivityMainBinding
    
    override fun onClickListener(view: View) {
        Log.i(TAG, "Listener handling")
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.listener=this
    }
}
复制代码

点击Button,就能看到onClickListener回调了~

不过瘾的,看官网吧

好了,讲到这里,大家喝杯奶茶续命,休息会吧~

Jetpack:你还在findViewById么?

吃完瓜了没?吃完了就该继续撸文了,毕竟革命尚未成功~

绑定类

前面讲的大多数是在布局中去使用表达式,从这开始,讲点代码中的操作。在一开始入门时候,讲到会根据当前布局生成绑定类,绑定类类名由布局名称根据Pascal规则和添加Binding后缀生成。举个栗子就明白了,当前布局名称:activity_shared.xml。生成绑定类名称:ActivitySharedBinding。

那么绑定类的作用是什么?

绑定类是数据绑定库为让我们可以 访问布局中的变量和视图 而生成的类。

如何创建或者定制绑定类呢?

创建绑定类

  • 使用静态inflate()方法
ActivityMainBinding.inflate(LayoutInflater.from(this))
复制代码

重载版本

ActivityMainBinding.inflate(getLayoutInflater(), viewGroup, false)
复制代码
  • 使用静态bind()方法
//一般这种情况是布局有作其他用途
ActivityMainBinding.bind(viewRoot)
复制代码
  • 在Fragment,ListView,或RecyclerView的adapter使用
val listItemBinding = ListItemBinding.inflate(layoutInflater,
                        viewGroup, false)
// 或者
val listItemBinding = DataBindingUtil
                 .inflate(layoutInflater, R.layout.list_item,
                 viewGroup, false)
复制代码

定制绑定类

通过修改 data 元素的class属于达到定制不同名称的绑定类,和其所存储位置。

//生成绑定类名为:ContactItem,存放在当前组件的绑定类包中
<data class="ContactItem">
    …
</data>

//生成绑定类名为:ContactItem,存放在当前组件包中
<data class=".ContactItem">
    …
</data>
//生成绑定类名为:ContactItem,存放在com.gitcode包中
<data class="com.gitcode.ContactItem">
    …
</data>
复制代码

访问Views

如果需要访问布局中Views,需要给Views添加id,数据绑定库会尽快通过findViewById去绑定。并在Activity中通过绑定类使用。例如:

binding.tvName.text="GitCode"
复制代码

访问变量

数据绑定库会为在布局中声明的变量在绑定类中生成setter和getter。例如:

binding.user=User("GitCode",3)
复制代码

绑定类官网

绑定适配器

每个布局表达式都对应着一个绑定适配器,用于进行设置相应属性或监听器所需的框架调用.通俗点说,我们通过调用什么方法去给属性赋值?我们在代码通过setText()方法给view的text属性赋值。讲的就是下面的代码:

binding.tvAge.text="20" //通过tvAge的setText()给TextView的android:text属性赋值
复制代码

好像跟我们平常调用的没什么区别:

tvAge.text="20"
复制代码

这里讲的就是这个,当数据变化时,我们调用合适的方法(例如setText方法),去给view的属性赋值(例如android:text的text属性)。还不懂的话,继续看~

给View的属性赋值

数据绑定库提供三种方式让我们去给View的属性赋值:库自己决定选择调用方法;明确指定调用方法;自定义调用逻辑方法。

库自动选择

假如View有个属性color,库会尝试去查找setColor(args)方法,参数args的类型需要和表达式的返回类型一致。例如 android:color=@{"black"} ,因为 "black" 是字符串类型,所以args的参数类型就是String。命名空间 android 并没有作强制要求,也可以是 gitcode:color=@{"black"} 。库查找方法的标准是 setXXX() 方法名和参数类型,这里的 XXX 是指属性名。

明确指定

虽然库自动选择已经很智能了,但有时view的属性和方法名并不一致,这是就需要我们明确指定,避免库自动选择找不到。例如ImageView的 android:tint 属性是关联到 setImageTintList(ColorStateList) 方法,而不是 setTint() ,这时,就需要明确指定了。

@BindingMethods(value = [
BindingMethod(
    type = android.widget.ImageView::class,
    attribute = "android:tint",
    method = "setImageTintList")])
复制代码

BindingMethods是注解在类上的,例如Activity。可以包含一个到多个BindingMethod注解。BindingMethod中type表示当前方法(method)匹配到到哪个View的属性(attribute)上。

定制逻辑方法

虽然上面两者已经满足了大多数情况,但一些特殊情况还是需要自己处理逻辑的。例如,view的 android:paddingLeft 属性,没有 setPaddingLeft(int) 方法,但提供了 setPadding(left, top, right, bottom) 方法。这时候就需要我们自定义逻辑了。

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}
复制代码

BindingAdapter注解允许定制属性的setter逻辑。setPaddingLeft方法的第一个参数必须是我们要处理属性的逻辑的View,后面的参数是根据BindingAdapter注解的属性来定位的。例如这里BindingAdapter注解只声明了 android:paddingLeft 属性,那么参数padding就是paddigLeft对应的值。设置多个属性是这样子的:

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

<ImageView app:imageUrl="@{venue.imageUrl}"
        app:error="@{@drawable/venueError}" />
复制代码

从这里可以看出,库对命名空间并没有作要求。注解的值imageUrl和error类型必须对应方法参数url和error的类型String和Drawable,只有ImageView同时匹配到两个属性,上述方法才会生效。为此,可以通过设置 requireAll = false ,匹配一个值也会生效。

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}
复制代码

类型转换

在绑定表达式返回一个对象时,库会选择一个方法来设置属性的值,而该对象会转型为方法参数的类型。这种机制可以方便使用ObservableMap来存储数据。

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />
复制代码

绑定表达式的 userMap["lastName"] 会返回值,该值会查找setText(CharSequence) 方法中自动转型为字符串并设置给TextView的text属性。但参数类型不确定的时候,就需要进行强制类型转换了,以表明类型。

有时候,绑定表达式返回的类型与设置属性方法的参数类型并不一致。例如: android:background 属性期待的是Drawable(setBackground(drawable),但设置color值时确实一个Int。

<View
   android:background="@{@color/red}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
复制代码

这时候我们需要使用BindingConversion注解将返回值类型Int转换成期待的类型Drawable。

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
复制代码

总结

写本文的时候,参考官网,看英文文档,对一个英语刚过四级的人...词我都认识,但组成句子,我就一脸懵逼了...

写到一半的时候,想放弃,或者想一笔带过...但,说过,要打造高质量文章,和对读者负责,所以熬了几个夜...夜太黑,没人担心明天会不会后悔~

看了一下别人的文章,基本都是支持参考官网翻译的,并没有加入个人理解和筛选。而本文是在多次参考阅读官网文章之下加入个人理解,让本文更加通俗易懂,更清晰表达官网的意图。

能看到结尾的同学也是很牛逼,需要很大的耐心,给你点个:+1:。那能不能举个爪,让我看看你们的:open_hands:。

Data Binding还有其他知识点,我发现的英语水平已经不够用,大家可以看看原汁原味的官网,或者等到后面我再把它写完...

Data Binding Library可能需要翻墙,不会翻墙找我...

要不要点个赞支持一下呢? 必须点,支持老铁写更多优质文章


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Letting Go of the Words

Letting Go of the Words

Janice (Ginny) Redish / Morgan Kaufmann / 2007-06-11 / USD 49.95

"Redish has done her homework and created a thorough overview of the issues in writing for the Web. Ironically, I must recommend that you read her every word so that you can find out why your customer......一起来看看 《Letting Go of the Words》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具