优雅地封装和使用 ViewBinding
/? ?今日科技快訊? ?/
近日,有網友在社交平臺展示使用筋膜槍搶茅臺的操作。對此天貓超市官方作出回應,表示此方法不可靠,并存在身體受傷的可能,希望廣大網友理性購物。
/? ?作者簡介? ?/
明天就是周六啦,祝大家都能有一個愉快的周末!
本篇文章來自DylanCai的投稿,分享了他對ViewBinding的封裝,相信會對大家有所幫助!同時也感謝作者貢獻的精彩文章!
DylanCai的博客地址:
https://juejin.cn/user/4195392100243000/posts
/? ?前言? ?/
之前看到官方公眾號發文章說準備棄用 Kotlin Extensions Gradle 插件了。可能有些人不知道 Kotlin Extensions 插件是什么,就是用 Kotlin 寫 Android 有個很爽的功能是,可以直接用布局里的 id 拿到控件對象。或許一些人經常這么寫,但不知道是用一個插件實現的。要在 build.gradle 里配置了下面的代碼才會生效,之前創建項目時會自動帶上,現在最新版的模板已移除。
apply?plugin:?'kotlin-android-extensions'用 id 獲取布局里的控件對象該插件的一個叫 Kotlin synthetic 的功能。貌似挺好的呀,不然寫一個控件就要聲明成 laterinit var 再調用 findViewById 才能拿到控件對象,寫起來很繁瑣。
這么方便的功能官方為什么要棄用呢?詳細內容的可以看這篇文章《Kotlin Android Extensions 的未來計劃》(https://mp.weixin.qq.com/s/pa1YOFA1snTMYhrjnWqIgg),官方提到了以下幾點:
污染全局命名空間。
不能暴露可空性信息。
僅支持 Kotlin 代碼。
官方的建議是用 ViewBinding 來代替 Kotlin synthetic 。那么相對的,ViewBinding 會有以下優勢:
不污染命名空間。這個在我放棄 Kotlin synthetics 用 ViewBinding 時很有感觸,終于不用在類文件里看到小寫下劃線命名的對象了,終于都統一成駝峰命名,強迫癥患者表示這波很舒服。
可以減少獲取控件的空指針異常。這是 Kotlin synthetics、ButterKnife、findViewById 都存在的問題,大家應該多多少少都有遇到過。而用 ViewBinding 的話,在布局上有什么控件才能獲取什么控件,這就不會出錯。
支持 Java 代碼。還在用 Java 的朋友可以考慮放棄 ButterKnife 了。
還有一點官方沒有提到,就是用了 ViewBinding 能夠很方便地使用 DataBinding。假如現在還在用 MVP,在未來想用 Jetpack MVVM 時就很容易了。
不過我之前了解過 ViewBinding,使用起來還是有點繁瑣,所以那時沒有改用 ViewBinding。現在官方表示一年后要棄用 Kotlin synthetic,不能用 id 獲取控件了,所以現在還是慢慢用起來吧。我花了些時間對 ViewBinding 進行封裝后,覺得可以用來代替 Kotlin synthetic 或者 ButterKnife,用 Kotlin 或者 Java 的朋友趕緊來試試吧。
下面來講一下 ViewBinding 怎么使用和個人的封裝建議。
/? ?基礎用法? ?/
首先要在 module 的 build.gradle 文件配置開啟 ViewBinding:
android?{...viewBinding?{enabled?=?true} }這樣該模塊下每個 XML 文件都生成一個對應的綁定類,每個綁定類會包含根視圖以及具有 ID 的所有視圖的引用。綁定類的命名是:將 XML 文件的名稱轉換為駝峰命名,并在末尾添加 “Binding” 。
比如現在有 activity_main.xml 文件:
<?xml?version="1.0"?encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout?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"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/tv_hello_world"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello?World!"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"?/></androidx.constraintlayout.widget.ConstraintLayout>這會生成一個叫 ActivityMainBinding 的綁定類。該類的對象可以通過 getRoot() 方法獲得根布局,并且可以獲得一個叫 tvHelloWorld 的 TextView 對象。
如果不想生成某個布局的綁定類,可以在根視圖添加 tools:viewBindingIgnore="true" 屬性。
那這個綁定類的對象怎么實例化呢?該類會生成相關的 inflate 靜態方法,調用該方法即可獲得綁定對象。
class?MainActivity?:?AppCompatActivity()?{private?lateinit?var?binding:?ActivityMainBindingoverride?fun?onCreate(savedInstanceState:?Bundle?)?{super.onCreate(savedInstanceState)binding?=?ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)binding.tvHelloWorld.text?=?"Hello?Android!"} }在 Fragment 使用有點不同,由于 Fragment 的存在時間比其視圖長,需要在 onDestroyView() 方法中清除對綁定類實例的所有引用,所以寫起來會有點麻煩。
class?HomeFragment?:?Fragment()?{private?var?_binding:?HomeFragmentBinding??=?nullprivate?val?binding?get()?=?_binding!!override?fun?onCreateView(inflater:?LayoutInflater,?container:?ViewGroup?,?savedInstanceState:?Bundle?):?View?{_binding?=?ResultProfileBinding.inflate(inflater,?container,?false)return?binding.root}override?fun?onViewCreated(view:?View,?savedInstanceState:?Bundle?)?{super.onViewCreated(view,?savedInstanceState)binding.tvHelloWorld.text?=?"Hello?Android!"}override?fun?onDestroyView()?{super.onDestroyView()_binding?=?null} }還有在 Adapter 的使用,因為布局不是只創建一次,而是每有一項數據就會創建,不能像上面那樣在 Adapter 里寫一個 binding 全局變量,這樣 binding 只會得到最后一次創建的視圖。所以 binding 對象應該是給 ViewHolder 持有。
class?TextAdapter(private?val?list:?List<String> )?:?RecyclerView.Adapter<TextAdapter.TextViewHolder>()?{override?fun?onCreateViewHolder(parent:?ViewGroup,?viewType:?Int):?TextViewHolder?{val?binding?=?ItemTextBinding.inflate(LayoutInflater.from(parent.context),?parent,?false)return?TextViewHolder(binding)}override?fun?onBindViewHolder(holder:?TextViewHolder,?position:?Int)?{val?content?=?list[position]holder.binding.tvContent.text?=?content}override?fun?getItemCount()?=?list.sizeclass?TextViewHolder(val?binding?:?ItemTextBinding)?:?RecyclerView.ViewHolder(binding.root) }常見的情況就講完了,總結一下 ViewBinding 的用法是,獲取綁定對象,然后用 getRoot() 方法拿到根視圖來替代使用到布局的地方。后面就可以通過綁定對象獲取布局上的控件對象。
一些使用 Java 的朋友可能會看不太懂上面的代碼。這木有關系,因為不推薦直接用,模板代碼用 Java 寫起來更長。把上面文字看了,代碼理解個大概,能比較清楚 ViewBinding 的用法就行了,接下來就是講怎么封裝來使用比較好。
/? ?封裝建議? ?/
用慣了 Kotlin synthetic 用 id 獲取控件,再看 ViewBinding 的用法多少會覺得有點繁瑣,所以需要封裝一下了,畢竟 ViewBinding 能減少 id 寫錯或類型寫錯導致的異常,而且前者快棄用了。個人想到了兩種封裝思路。
不依托于基類
類似在 Kotlin 使用 ViewModel 的用法,做到聲明了對象即可使用,不用管是怎么創建的,不用考慮什么時候要清除實例,不用每次去寫 inflate 的模板代碼。這種用法的好處是想用就用,無需繼承什么基類,泛用性更強,移植代碼更加容易。會用到一些 Kotlin 的特性,不適用于 Java。Java 的推薦用法還在后面。
先來分析一下,首先肯定要調用 inflate() 方法,不然怎么實例化 binding 對象。但是我們可以做到使用前自動 inflate(),無需手動調用。這就用到延時委托來實現,在 Fragment 因為要清除實例后面另說。然后就是 inflate() 方法需要傳 layoutInflater,而 Activity 、Dialog 都有提供對應 get 方法,所以就變成獲取 Activity 、Dialog 對象,可以傳參,但是更推薦寫成拓展函數傳進來。剩下一個問題,怎么調用 inflate() 方法,方法名和參數固定,可以用反射。但我們仍要一個 Class 對象,這可以通過內斂方法來獲取泛型的 Class 對象。
上述的是封裝思路,需要了解一些 Kotlin 的用法,有興趣的自己去研究一下,涉及的知識點較多就不過多展開了。以下是封裝好的代碼:
inline?fun?<reified?VB?:?ViewBinding>?Activity.inflate()?=?lazy?{inflateBinding<VB>(layoutInflater).apply?{?setContentView(root)?} }inline?fun?<reified?VB?:?ViewBinding>?Dialog.inflate()?=?lazy?{inflateBinding<VB>(layoutInflater).apply?{?setContentView(root)?} }@Suppress("UNCHECKED_CAST") inline?fun?<reified?VB?:?ViewBinding>?inflateBinding(layoutInflater:?LayoutInflater)?=VB::class.java.getMethod("inflate",?LayoutInflater::class.java).invoke(null,?layoutInflater)?as?VB看不懂的沒關系,知道怎么用就行。下面是 Activity 的使用示例,省去了 inflate() 和 setContentView() 的代碼,在 Dialog 使用是類似的。
class?MainActivity?:?AppCompatActivity()?{private?val?binding:?ActivityMainBinding?by?inflate()override?fun?onCreate(savedInstanceState:?Bundle?)?{super.onCreate(savedInstanceState)binding.tvHelloWorld.text?=?"Hello?Android!"} }而 Fragment 的封裝就不一樣了,首先 inflate() 方法還要傳 parent 對象就不好處理,可以換個思路,我們用另一個生成的方法 bind(),只需傳個 View,在 Fragment 很好拿。另外還需要釋放 binding 對象,不能用延時委托改用屬性委托。下面是封裝的代碼:
inline?fun?<reified?VB?:?ViewBinding>?Fragment.bindView()?=FragmentBindingDelegate(VB::class.java)class?FragmentBindingDelegate<VB?:?ViewBinding>(private?val?clazz:?Class<VB> )?:?ReadOnlyProperty<Fragment,?VB>?{private?var?isInitialized?=?falseprivate?var?_binding:?VB??=?nullprivate?val?binding:?VB?get()?=?_binding!!override?fun?getValue(thisRef:?Fragment,?property:?KProperty<*>):?VB?{if?(!isInitialized)?{thisRef.viewLifecycleOwner.lifecycle.addObserver(object?:?LifecycleObserver?{@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)fun?onDestroyView()?{_binding?=?null}})_binding?=?clazz.getMethod("bind",?View::class.java).invoke(null,?thisRef.requireView())?as?VBisInitialized?=?true}return?binding} }使用起來就體現出封裝的優勢了,不用特地寫個 _binding 來清除實例對象,不用重寫 onDestoryView() 方法。
class?HomeFragment?:?Fragment(R.layout.fragment_home)?{private?val?binding:?FragmentHomeBinding?by?bindView()override?fun?onViewCreated(view:?View,?savedInstanceState:?Bundle?)?{super.onViewCreated(view,?savedInstanceState)binding.tvHelloWorld.text?=?"Hello?Android!"} }構造函數里的布局記得別漏了,因為需要用布局創建出 View ,我們才能調用 bind() 方法。
還有列表的封裝,前面說了 binding 對象是給 ViewHolder 持有,所以我們寫一個 BindingViewHolder 來接收 binding。
class?BindingViewHolder<VB?:?ViewBinding>(val?binding:?VB)?:?RecyclerView.ViewHolder(binding.root)當然這還不夠,因為需要個 binding 對象,同樣要用到反射進行實例化。我們得到 binding 對象后可以順便把 BindingViewHolder 對象創建了,所以直接封裝一個創建的方法。
inline?fun?<reified?T?:?ViewBinding>?newBindingViewHolder(parent:?ViewGroup):?BindingViewHolder<T>?{val?method?=?T::class.java.getMethod("inflate",?LayoutInflater::class.java,?ViewGroup::class.java,?Boolean::class.java)val?binding?=?method.invoke(null,?LayoutInflater.from(parent.context),?parent,?false)?as?Treturn?BindingViewHolder(binding) }怎么用呢?在 onCreateViewHolder 調用封裝的方法就創建了 BindingViewHolder 對象,然后在 onBindViewHolder 方法通過 holder 持有的 binding 就能拿到得到布局里控件了。
class?TextAdapter(private?val?list:?List<String> )?:?RecyclerView.Adapter<BindingViewHolder<ItemTextBinding>>()?{override?fun?onCreateViewHolder(parent:?ViewGroup,?viewType:?Int)?=newBindingViewHolder<ItemTextBinding>(parent)override?fun?onBindViewHolder(holder:?BindingViewHolder<ItemTextBinding>,?position:?Int)?{val?content?=?list[position]holder.binding.tvContent.text?=?content}override?fun?getItemCount()?=?list.size }以上的封裝簡化了綁定類固定的 inflate 模板代碼和 Fragment 清除實例對象的代碼,在普通的 Activity、Fragment、Dialog、Adapter 都能使用,非常靈活。接下來講另外一種封裝思路。
依托于基類
主要是把 binding 對象封裝在基類里替換掉布局,這樣可以進一步減少聲明 binding 對象的代碼。還有前面的用法在某些基類使用時可能會存在 setContentView() 的調用時機問題,因為用到 binding 才會實例化和設置根布局。也許還沒設置根視圖,基類就去找控件,遇到的話可以改用下面的方式封裝。
因為這里想教大家怎么去改造自己的基類,會涉及到 Kotlin 和 Java 兩種寫法,還有幾種類型的基類,講完的話篇幅很長。所以寫了一個庫 ViewBindingKtx ,讓大家用最少的代碼使用上 ViewBinding,同時也方便自己平時在項目中使用。
下面只是介紹部分用法,完整的用法和例子請到 Github(https://github.com/DylanCaiCoding/ViewBindingKtx) 中查看。如果覺得對你有幫助,希望能點個 star 支持一下。
在 build.gradle 里配置 viewBinding 和添加依賴。包含了前面封裝的拓展函數,不想把代碼拷來拷去的話也可以添加依賴來使用。
dependencies?{implementation?'com.dylanc:viewbinding-ktx:1.0.0' }介紹一下如何改造 Java 寫的 Activity 基類。首先要給基類增加一個繼承 ViewBinding 的泛型,然后類里增加一個 binding 全局變量。用工具類初始化 binding,刪掉原來設置布局的代碼,改為設置 binding.getRoot()。以下是核心的代碼。
public?abstract?class?BaseBindingActivity<VB?extends?ViewBinding>?extends?AppCompatActivity?{private?VB?binding;@Overrideprotected?void?onCreate(@Nullable?Bundle?savedInstanceState)?{super.onCreate(savedInstanceState);binding?=?ViewBindingUtil.inflateWithGeneric(this,?getLayoutInflater());setContentView(binding.getRoot());}public?VB?getBinding()?{return?binding;} }下面是基類改造后的使用示例。
class?MainActivity?extends?BaseBindingActivity<ActivityMainBinding>?{@Overrideprotected?void?onCreate(@Nullable?Bundle?savedInstanceState)?{super.onCreate(savedInstanceState);getBinding().tvHelloWorld.setText("Hello?Android!");} }無需聲明控件變量,代碼簡潔很多,而且不會有 id 寫錯或者類型轉換的問題,所以趕緊把 ButterKnife 換了吧。
另外再講一下列表的基類封裝,這里以個人一直在使用的列表庫 Drakeet/MultiType 為例子。先看下原本的用法,ViewDelegate 可以當成 Adapter 來看。
class?FooViewDelegate?:?ItemViewDelegate<Foo,?FooViewDelegate.ViewHolder>()?{override?fun?onCreateViewHolder(context:?Context,?parent:?ViewGroup):?ViewHolder?{return?ViewHolder(LayoutInflater.from(context).inflate(R.layout.item_foo,?parent,?false))}override?fun?onBindViewHolder(holder:?ViewHolder,?item:?Foo)?{holder.binding.tvFoo.text?=?item.value}class?ViewHolder(itemView:?View)?:?RecyclerView.ViewHolder(itemView)?{val?fooView:?TextView?=?itemView.findViewById(R.id.foo)} }來封裝基類,同樣要在基類增加一個繼承 ViewBinding 的泛型,然后將原來的 ViewHolder 換成 BindingViewHolder,最后在 onCreateViewHolder 方法里調用一個用泛型創建 BindingViewHolder 的方法。下面是封裝好的代碼。
abstract?class?BindingViewDelegate<T,?VB?:?ViewBinding>?:?ItemViewDelegate<T,?BindingViewHolder<VB>>()?{override?fun?onCreateViewHolder(context:?Context,?parent:?ViewGroup)?=newBindingViewHolderWithGeneric<VB>(parent) }使用起來就簡單很多,可以對比一下前面的基礎用法。
class?FooViewDelegate?:?BindingViewDelegate<Foo,?ItemFooBinding>()?{override?fun?onBindViewHolder(holder:?BindingViewHolder<ItemFooBinding>,?item:?Foo)?{holder.binding.tvFoo.text?=?item.value} }本文所封裝的代碼用到了反射,開啟混淆時要增加以下配置:
-keepclassmembers?class?*?implements?androidx.viewbinding.ViewBinding?{public?static?**?inflate(...);public?static?**?bind(***); }更多基類改造封裝 ViewBinding 的 Java 、Kotlin 示例請到?GitHub?查看。
關于用反射進行封裝
可能有些人比較介意用反射,其實我也不太想用,能有其它更好的方式實現誰會特意用反射呢。如果反射的使用帶來了足夠的便利性,個人覺得還是可以接受的。比如 ViewModel 的源碼也用了反射進行實例化,相較于自己手動創建 ViewModel 對象,使用官方的 ViewModelProviders 獲取 ViewModel 對象能在 Activity 和 Fragment 銷毀重建時恢復數據。
其實本文的封裝從本質上來說是和 ButterKnife 一樣的。同樣生成了綁定控件的類,ButterKnife 用注解生成,ViewBinding 解析 XML 生成。都用到了反射,調用 ButterKnife.bind(this) 時反射了一次,我們調用工具類方法時反射了一次。最終的目的都是減少模板代碼的編寫,讓代碼更簡潔。所以用反射來封裝 ViewBinding 個人覺得是合適的。
/? ?總結? ?/
本文講了官方棄用 Kotlin Extensions 插件的原因和使用 ViewBinding 的好處,可以避免 id 寫錯或類型寫錯導致的異常。然后講述了 ViewBinding 的基礎用法,并給出了兩種 ViewBinding 的封裝建議。
后面介紹了個人封裝的庫 ViewBindingKtx,讓大家用最少的代碼使用上 ViewBinding,所以該棄用 Kotlin synthetic 和 ButterKnife 了。
項目地址:
https://github.com/DylanCaiCoding/ViewBindingKtx
推薦閱讀:
Android 卡頓調研
我又開發了一個非常好用的開源庫
用爛的LruCache,你真的完全懂了么?
歡迎關注我的公眾號
學習技術或投稿
長按上圖,識別圖中二維碼即可關注
總結
以上是生活随笔為你收集整理的优雅地封装和使用 ViewBinding的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【JavaScript】DOM和事件简介
- 下一篇: mysql set password_M