42

ViewBinding 实战,递进优雅的写波代码

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ%3D%3D&%3Bmid=2650831327&%3Bidx=1&%3Bsn=389305d89d013639e1e529c49c71ae3d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

稳住,今天是周六。

上周看到这篇文章,其实我还是蛮喜欢类似的文章的,喜欢这种循序渐进去重构代码的感觉。

另外一些新技术都缺少一些实战的文章,最近如果发现有不错的文章,也会上门求授权,争取推送。

不过看完后,总觉得 ViewBinding 和 DataBinding 能力非常类似,于是又找了一些和 DataBinding 的区别文章来看,恩...还是没有看到太本质的区别,后面可能会详细的对比一波,有比较了解的也欢迎留言,先看看这篇 ViewBinding 实战吧。

完全没了解过 View Binding,可以先看下面这篇介绍:

AS 3.6 Canary 中推出新技术 视图绑定 View Binding

另外 GSY 的新书出版了,明天会写篇文章感谢一下 GSY 一直以来的帮助与支持,顺便送 15 本新书给大家。

下面开始正文。

1

小试牛刀

公司刚来了一个小伙伴,名叫 小白 ,刚毕业的小伙子,这天茶余饭后,聊天聊起了代码复用的问题。 确实,代码复用,可以说是我们每一个有理想的程序员的追求。 于是想借机考考他。

我:说到代码复用,那!Android开发中,布局该如何复用呢?

比如,像下面所示的这样一个卡片设计,很多页面都有用到,不可能每个页面都去写一遍吧?如何能很好的实现复用呢?

BFFFfi6.png!web

小白: 西哥,你这个问题也太简单了,虽然我才学Android不久,但是这个我还是知道的,我们都知道,Android 布局中,有个一个<include /> 标签,可以引用一个布局。

我们可以把这个复用的卡片写成一个单独的布局,然后在每个页面使用<include />包含进来就好了呀!

于是二话没说,就是干,马上就开始写起了代码!

首先,抽出一个公共的布局叫 card_item.xml 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="5dp"
    android:layout_margin="10dp"
    app:cardElevation="2dp">
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <ImageView
        android:id="@+id/avatar"
        android:layout_width="80dp"
        android:layout_height="90dp"
        android:src="@mipmap/logo"
        android:scaleType="centerCrop"
        android:layout_centerVertical="true"
        android:layout_marginLeft="15dp"
        />
    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#333"
        android:textSize="18sp"
        android:layout_toRightOf="@+id/avatar"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        />
    <TextView
        android:id="@+id/des"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#999"
        android:textSize="12sp"
        android:layout_below="@+id/name"
        android:layout_toRightOf="@+id/avatar"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        />
</RelativeLayout>
</androidx.cardview.widget.CardView>

接着,在每一个使用该卡片设计的地方,使用<include /> 标签将card_item.xml布局引入进来。

新建布局文件fragment.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include layout="@layout/card_item" />

</LinearLayout>

然后新建一个Fragment,名叫MyFragment,代码如下:

class MyFragment: Fragment() {
    private lateinit var avatar: ImageView
    private lateinit var name: TextView
    private lateinit var desc: TextView

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.my_fragment,container,false)
        avatar = view.findViewById(R.id.avatar)
        name = view.findViewById(R.id.name)
        desc = view.findViewById(R.id.des)
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        avatar.setImageResource(R.mipmap.logo)
        name.text = "技术最TOP"
        desc.text = "扒最前沿科技动态,聊最TOP编程技术~"
    }
}

然后运行一下,效果如下:

rEv6ziQ.png!web

然后,在其他需要的页面,如MyFragment2、MyFragment3,按照前面的步骤,引入布局,绑定数据,就好了。

非常简单,5分钟就写好了。小白略带微笑的说到。

我: 嗯,小伙子不错不错,这样确实可以,布局文件确实复用了,但是你看看你的Fragment啊,比如我有4个Fragment,MyFragment1、MyFragment2,MyFragment3、MyFragment4,那其实我每个Fragment中的大部分代码都是相同的。

如下:

    // 声明View
    private lateinit var avatar: ImageView
    private lateinit var name: TextView
    private lateinit var desc: TextView

    // 绑定View
    val view = inflater.inflate(R.layout.my_fragment1,container,false)
    avatar = view.findViewById(R.id.avatar)
    name = view.findViewById(R.id.name)
    desc = view.findViewById(R.id.des)

    // 绑定数据
    avatar.setImageResource(R.mipmap.logo)
    name.text = "技术最TOP"
    desc.text = "扒最前沿科技动态,聊最TOP编程技术~"

上面这些样板代码看起来很难受啊,每个页面都要这样写,并且后期不好维护,比如,我CardView 里面新增加一个View,那么这些用到的页面都得改。

有没有办法能把这些样板代码也一起复用呢?

小白有点迷惑,用手挠挠头,若有所思。

2

自定义View包装

不一会儿,小白大叫一声,我有办法了!

小白: 我们可以借助自定义View来封装一下,我们把Fragment中的样板代码,抽到一个View 中去,然后提供一个API方法给外部来设置数据,每个使用的地方,将<include /> 引入的布局换成自定义的View, 然后在Fragment中调用API设置数据就可以了。

小白一脸自豪,说干就干,又开始重构前面的代码。

首先,将样板代码抽取一个View名叫CardItem,将声明View、绑定View、绑定数据的逻辑都放在这里,代码如下:

class CardItem @JvmOverloads constructor(
    context: Context, attributes: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attributes, defStyleAttr) {

    private  var ivAvatar: ImageView
    private  var tvName: TextView
    private  var tvDesc: TextView

    init {
        val view = LayoutInflater.from(context).inflate(R.layout.card_item,null,false)
        ivAvatar = view.findViewById(R.id.avatar)
        tvName = view.findViewById(R.id.name)
        tvDesc = view.findViewById(R.id.des)

        addView(view)
    }

    fun setData(imageAvatarRes: Int, name: String, desc: String) {
        ivAvatar.setImageResource(imageAvatarRes)
        tvName.text = name
        tvDesc.text = desc
    }
}

如上面代码所示,我们提供了一个方法setData来绑定数据。

然后使用的地方,先替换布局文件的<include /> ,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

<!--    <include layout="@layout/card_item" />-->

    <com.jay.jetpack.viewbinding.CardItem
        android:id="@+id/card_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

然后在Fragment中,调用setData绑定数据:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    cardItem.setData(imageAvatarRes = R.mipmap.logo,name="技术最TOP",desc = "扒最前沿科技动态,聊最TOP编程技术~")
}

运行一下代码,效果如下图所示:

f2QJ7vJ.png!web

才过10分钟,小白就把代码重构好了。

我: 不错不错,小伙子,这种方案很好,几乎大部分代码都公用了。

但是不够完美, 有一个小问题,你看这个自定义View类,里面同样是很多样板代码,如果我们又有另一个布局需要公用,那么我可能就需要再添加一个自定义View,把CardItem里面的代码拷贝过去,然后改吧改吧,改成对应的布局和View,当项目越来越大的时候,这种自定义View可能就越多。

但是他们的大部分代码其实是相同的。

有没有办法能够解决这个问题,把这里面的样板代码也消除呢?

小白又陷入了沉思!

小白: 这我真不知道了,还有什么办法?西哥给我讲讲呗。

我: 你有听说过ViewBinding吗?

小白: 听过听过!就是Google 最新出的Jetpack组件嘛,江湖上声称干掉findViewById,取代黄油刀ButterKnife的大杀器。

我: 对,就是这个,我们可以用这个,加上Kotlin 的特性来做更完美的优化。

3

ViewBinding的救赎

ViewBinding是Jetpack中新添加的组件,首先,在build.gradle中开启:

viewBinding {
        enabled = true
 }

开启ViewBinding后,他会自动帮我的布局生成对应的类,比如我们上面的card_item.xml,会给我生成一个CardItemBinding.java类,my_fragment2.xml会生成

MyFragment2Binding.java,生成规则为:布局文件的名字去掉下划线 + Binding后缀,以驼峰的形式。

如下:

bmmaaiA.png!web

首先,把布局中的<CardItem /> 换成 <include /> 标签。代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include android:id="@+id/topCard" layout="@layout/card_item"/>

</LinearLayout>

然后,我们就可以不用findViewById()来绑定View了,可以直接使用xxBinding类访问View,Fragment代码如下:

class MyFragment2: Fragment(R.layout.my_fragment2) {
    private lateinit var binding: MyFragment2Binding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = MyFragment2Binding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.topCard.apply {
            avatar.setImageResource(R.mipmap.logo)
            name.text="技术最TOP"
            des.text = "扒最前沿科技动态,聊最TOP编程技术。"
        }
    }
}

这样,我们就把一个几十行代码的自定义View类,变成了4代码,是不是就非常爽了。

先别高兴,还有点问题,虽然我们去掉了样板代码,但是还是存在我们最初的那个问题,那就是,如果复用的布局增加或者减少View的话,那么在每个调用的地方都要更改。 

这可不是我们想要的,怎么解决这个问题呢?

还好有Kotlin,我们可以用Kotlin的扩展函数来优化!

4

Kotlin扩展函数 + ViewBinding

我们把绑定数据的那一段代码,抽一个扩展函数:

fun CardItemBinding.bind(imageResId: Int,nameStr: String, descStr: String){
        avatar.setImageResource(imageResId)
        name.text = nameStr
        des.text = descStr
}

我们在CardItemBinding上扩展了一个bind方法。

现在我们如何调用了?下面这样:

binding.topCard.bind(imageResId = R.mipmap.logo,
            nameStr = "技术最TOP Super",
            descStr = "扒最前沿科技动态,聊最TOP编程技术。Super")

运行一下,效果如下:

mMryy2F.png!web

完美实现,我们把自定义View,替换成了一个ViewBinding的扩展函数,代码从原来的33行,减少到了现在的4行。

后期维护也很方便,增加减少View,直接在扩展方法里面更改就好。

并且,如果还有其他的复用布局,我们再添加一个扩展方法就好了,这就非常爽了!

小白: 啥?等于说,利用Kotin + ViewBinding 可以替换自定义View了?妙啊! 我也去写一个来试试!

推荐阅读

“新技术” 又又又又来了?

“手把手”的性能优化文章来了!

面试官: 说一下你做过哪些性能优化?

vEVjqa7.jpg!web

扫一扫  关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

┏(^0^)┛明天见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK