公司刚来了一个小伙伴,名叫小白
,刚毕业的小伙子,这天茶余饭后,聊天聊起了代码复用的问题。确实,代码复用,可以说是我们每一个有理想的程序员的追求。于是想借机考考他。
我
:说到代码复用,那!Android开发中,布局该如何复用呢?
比如,像下面所示的这样一个卡片设计,很多页面都有用到,不可能每个页面都去写一遍吧?如何能很好的实现复用呢?
小白
:西哥,你这个问题也太简单了,虽然我才学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编程技术~" } } 复制代码
然后运行一下,效果如下:
然后,在其他需要的页面,如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,那么这些用到的页面都得改。有没有办法能把这些样板代码也一起复用呢?
小白有点迷惑,用手挠挠头,若有所思。
不一会儿,小白大叫一声,我有办法了!
小白
:我们可以借助自定义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" /> </Line 复制代码
然后在Fragment中,调用setData绑定数据
:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) cardItem.setData(imageAvatarRes = R.mipmap.logo,name="技术最TOP",desc = "扒最前沿科技动态,聊最TOP编程技术~") } 复制代码
运行一下代码,效果如下图所示:
才过10分钟,小白就把代码重构好了。
我:
不错不错,小伙子,这种方案很好,几乎大部分代码都公用了。
但是不够完美,有一个小问题,你看这个自定义View类,里面同样是很多样板代码,如果我们又有另一个布局需要公用,那么我可能就需要再添加一个自定义View,把CardItem里面的代码拷贝过去,然后改吧改吧,改成对应的布局和View,当项目越来越大的时候,这种自定义View可能就越多。但是他们的大部分代码其实是相同的。
有没有办法能够解决这个问题,把这里面的样板代码也消除呢?
小白又陷入了沉思!
小白
:这我真不知道了,还有什么办法?西哥给我讲讲呗。
我
:你有听说过ViewBinding
吗?
小白
:听过听过!就是Google 最新出的Jetpack组件嘛,江湖上声称干掉findViewById
,取代黄油刀ButterKnife的大杀器。
我
:对,就是这个,我们可以用这个,加上Kotlin 的特性来做更完美的优化。
ViewBinding是Jetpack中新添加的组件,首先,在build.gradle中开启:
viewBinding { enabled = true } 复制代码
开启ViewBinding后,他会自动帮我的布局生成对应的类,比如我们上面的card_item.xml
,会给我生成一个CardItemBinding.java
类,my_fragment2.xml
会生成MyFragment2Binding.java
,生成规则为:布局文件的名字去掉下划线 + Binding后缀,以驼峰的形式。如下:
首先,把布局中的<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的扩展函数来优化!
我们把绑定数据的那一段代码,抽一个扩展函数:
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") 复制代码
运行一下,效果如下:
完美实现,我们把自定义View,替换成了一个ViewBinding的扩展函数,代码从原来的33行,减少到了现在的4行。
后期维护也很方便,增加减少View,直接在扩展方法里面更改就好。
并且,如果还有其他的复用布局,我们再添加一个扩展方法就好了,这就非常爽了!
小白
:啥?等于说,利用Kotin + ViewBinding 可以替换自定义View了?妙啊!
我也去写一个来试试!
文章首发于公众号:
「 技术最TOP 」
,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」
第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你