lzyprime 博客 (github)
創(chuàng)建時(shí)間:2021.04.23
qq及郵箱:2383518170
kotlin & android 筆記
λ:
# ViewBinding DataBinding
# 倉(cāng)庫(kù)地址: https://github.com/lzyprime/android_demos
# branch: viewBinding
git clone -b viewBinding https://github.com/lzyprime/android_demos
最近幾個(gè)月忙于寫(xiě)需求,積累了太多要總結(jié)的東西祥楣。當(dāng)然也正是這幾個(gè)月的大量實(shí)踐,對(duì)一些知識(shí)有了新的認(rèn)識(shí)和發(fā)現(xiàn)兽间。
ViewBinding
DataBinding
通過(guò) xml
聲明嘀略,生成對(duì)應(yīng)代碼,刨開(kāi)生成的源碼看一下讼育,大概就能明白原理稠集。
有用的可能就是 val binding by viewBinding<T>()
的兩個(gè)拓展函數(shù)實(shí)現(xiàn)。其余就是如官網(wǎng)文檔一樣的備忘錄內(nèi)容忧饭,方便知識(shí)點(diǎn)查找词裤。
ViewBinding
生成的源碼
ViewBinding 庫(kù)代替之前的kotlin-android-extensions
, 根據(jù)布局文件 layout/example.xml
生成對(duì)應(yīng)的[ExampleBinding]
.
以[FragmentDetailBinding]
為例, 看一下生成的源碼逆航。
public final class FragmentDetailBinding implements ViewBinding {
@NonNull
private final FrameLayout rootView;
@NonNull
public final ImageView imageView;
private FragmentDetailBinding(@NonNull FrameLayout rootView, @NonNull ImageView imageView) {
this.rootView = rootView;
this.imageView = imageView;
}
@Override
@NonNull
public FrameLayout getRoot() {
return rootView;
}
@NonNull
public static FragmentDetailBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static FragmentDetailBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.fragment_detail, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static FragmentDetailBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.imageView;
ImageView imageView = rootView.findViewById(id);
if (imageView == null) {
break missingId;
}
return new FragmentDetailBinding((FrameLayout) rootView, imageView);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
基類[ViewBinding]
是interface
, 只有一個(gè)getRoot
方法,返回顯示的View
/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
/**
* Returns the outermost {@link View} in the associated layout file. If this binding is for a
* {@code <merge>} layout, this will return the first view inside of the merge tag.
*/
@NonNull
View getRoot();
}
每份生成的代碼:
- 根據(jù)
layout/fragment_detail.xml
下劃線名稱生成對(duì)應(yīng)駝峰類名FragmentDetailBinding
- 根據(jù)布局文件中組件
id
, 生成對(duì)應(yīng)駝峰式成員名渔肩,類型為組件類型. 如imageView: ImageView
- 根部局生成為
rootView
構(gòu)造函數(shù)私有因俐,需要的參數(shù)為上述根據(jù)id
生成的成員.
private FragmentDetailBinding(@NonNull FrameLayout rootView, @NonNull ImageView imageView)
同時(shí)生成3個(gè)靜態(tài)函數(shù)作為工廠構(gòu)造
- 兩個(gè)
inflate
用傳入的[inflater: LayoutInflater]
獲得對(duì)應(yīng)的View
. - 調(diào)用
bind
,通過(guò)findViewById
獲得各個(gè)組件, 然后通過(guò)私有構(gòu)造得到[FragmentDetailBinding]
也就是說(shuō), findViewById
的過(guò)程靠生成代碼解決周偎,所以在拿到一個(gè)ViewBinding
實(shí)例時(shí), 可以通過(guò)成員直接訪問(wèn)抹剩。
kotlin 偽代碼大概寫(xiě)一下工廠構(gòu)造的調(diào)用關(guān)系
fun inflate(inflater: LayoutInflater): FragmentDetailBinding = inflate(inflater, null, false)
fun inflate(inflater: LayoutInflater,
parent: ViewGroup,
attachToParent: Boolean,
): FragmentDetailBinding {
...
val root: View = inflater.inflate(...)
...
return bind(root)
}
fun bind(rootView: View): FragmentDetailBinding {
// findViewById
val imageView = rootView.findViewById(R.id.imageView)
return FragmentDetailBinding(rootView, imageView)
}
使用
- 當(dāng)前沒(méi)有View, 需要新建
// 官網(wǎng)例子:
// Activity
class ResultProfileActivity : AppCompatActivity(){
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
// 通過(guò) inflate 新建
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
}
// Fragment
class ResultProfileFragment : Fragment() {
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
- 已有視圖,直接通過(guò)
bind
獲得
// Fragment 構(gòu)造直接傳 R.layout.fragment_detail
class DetailFragment : Fragment(R.layout.fragment_detail) {
private var _binding: FragmentDetailBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 此時(shí)R.layout.fragment_detail對(duì)應(yīng)View已存在澳眷,直接 bind
_binding = FragmentDetailBinding.bind(view)
...
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
同理其他地方,沒(méi)有視圖調(diào)用inflate
構(gòu)造,有視圖調(diào)用bind
直接獲得.
Activity, Fragment 使用優(yōu)化
存在的問(wèn)題:
- 過(guò)程重復(fù)矢空。 每個(gè)
Activity
和Fragment
中复亏,流程相同耕突,僅僅是具體[ViewBinding]
的區(qū)別培遵。 -
Fragment
中,onDestroyView
時(shí)要將_binding
置空,對(duì)于binding
的操作時(shí)機(jī)靠自己保證女轿,時(shí)序自己保證北救。 -
lateinit var
在代碼掃描中視為風(fēng)險(xiǎn)行為攘宙,不建議使用(個(gè)人項(xiàng)目隨意)缓淹。
仿照
val model: VM by viewModels<VM>()
通過(guò)拓展函數(shù), 委托, 反射
, 實(shí)現(xiàn)類似
val binding: FragmentDetailBinding by viewBinding<FragmentDetailBinding>()
/**
* 用于[Activity]生成對(duì)應(yīng)[ViewBinding].
*
* @exception ClassCastException 當(dāng) [VB] 無(wú)法通過(guò)
* `VB.inflate(LayoutInflater.from(this#Activity))` 構(gòu)造成功時(shí)拋出
* */
@MainThread
inline fun <reified VB : ViewBinding> Activity.viewBinding() = object : Lazy<VB> {
private var cached: VB? = null
override val value: VB
get() =
cached ?: VB::class.java.getMethod(
"inflate",
LayoutInflater::class.java,
).invoke(null, layoutInflater).let {
if (it is VB) {
cached = it
it
} else {
throw ClassCastException()
}
}
override fun isInitialized(): Boolean = cached != null
}
// example
class MainActivity : AppCompatActivity() {
private val binding by viewBinding<ActivityMainBinding>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 確保調(diào)用該函數(shù)設(shè)置binding.root
setContentView(binding.root)
}
}
Activity
內(nèi)聯(lián)拓展函數(shù)匀借,通過(guò)調(diào)用inflate(inflater: LayoutInflater)
版本生成binding
。需要自己確保在onCreate
之后使用芒率,否則拿不到Activity.layoutInflater
, 構(gòu)造失敗
/**
* 用于 [Fragment] 內(nèi)構(gòu)造對(duì)應(yīng) [ViewBinding].
*
* @exception ClassCastException 當(dāng) [VB] 無(wú)法通過(guò) `VB.bind(view)` 構(gòu)造成功時(shí)拋出
*
* 函數(shù)會(huì)自動(dòng)注冊(cè)[Fragment.onDestroyView]時(shí)的注銷操作.
* */
@MainThread
inline fun <reified VB : ViewBinding> Fragment.viewBinding() = object : Lazy<VB> {
private var cached: VB? = null
override val value: VB
get() = cached ?: VB::class.java.getMethod(
"bind",
View::class.java,
).invoke(VB::class.java, this@viewBinding.requireView()).let {
if (it is VB) {
// 監(jiān)聽(tīng)Destroy事件
viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyView() {
cached = null
}
})
cached = it
it
} else {
throw ClassCastException()
}
}
override fun isInitialized(): Boolean = cached != null
}
// example
class ExampleFragment:Fragment(R.layout.example_fragment) {
private val binding by viewBinding<ExampleFragmentBinding>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 確保在此之后使用binding
binding.xxxTextView.text = "sssss"
}
}
Fragment
內(nèi)聯(lián)拓展函數(shù)匪蟀,通過(guò)調(diào)用bind(rootView: View)
版本生成binding
查刻。
前提是調(diào)用Fragment(@LayoutRes)
版本構(gòu)造, 利用Fragment
默認(rèn)的onCreateView
行為得到View
仔沿。因此要在onViewCreated
后使用binding
净当。否則Fragment.requireView()
拿不到view, bind
失敗像啼。
通過(guò)viewLifecycleOwner.lifecycle
監(jiān)聽(tīng)Destroy
行為萄传,將cached
賦為null
, 當(dāng)重新構(gòu)建View
時(shí)脊串,binding
的isInitialized() == false
, 認(rèn)為沒(méi)有初始化,重新走value get()
中的邏輯,達(dá)到重新綁定的效果锭碳。
總結(jié):原有問(wèn)題仍有一部分未解決(如: 自己保證執(zhí)行時(shí)序), 但一定程度上減少了重復(fù)代碼袁稽,尤其是Fragment
中。
DataBinding
DataBinding
相當(dāng)于ViewBinding++
在xml
中傳遞和使用數(shù)據(jù)
<?xml version="1.0" encoding="utf-8"?>
<!-- layout作為根 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 數(shù)據(jù) -->
<data>
<variable name="user" type="com.example.User"/>
</data>
<!-- 布局 -->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/> <!-- 使用數(shù)據(jù) -->
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/> <!-- 使用數(shù)據(jù) -->
</LinearLayout>
</layout>
// data class User(val firstName: String, val lastName: String)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")
}
基類[ViewDataBinding]
public abstract class ViewDataBinding extends BaseObservable implements ViewBinding
實(shí)現(xiàn)了
[ViewBinding]
, 生成的代碼中inflate, bind
函數(shù)簽名相同擒抛,內(nèi)部實(shí)現(xiàn)略有不同推汽,所以上邊by viewBinding<T>()
仍然適用。同時(shí)繼承
[BaseObservable]
, 使得本身成為[Observable]
, 可觀察者
除了像ViewBinding
中構(gòu)造方式, 還可以使用DataBindingUtil
:
// Activity, 等價(jià)于 inflate + setContentView
val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// or
val binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
綁定表達(dá)式
<data>
中
<data>
<!-- 聲明 -->
<variable name="user" type="com.example.User"/>
<!-- 導(dǎo)入 -->
<import type="android.view.View"/>
<!-- 類型別名 -->
<import type="com.example.real.estate.View" alias="Vista"/>
<!-- 集合 -->
<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"/>
<!-- 在布局中使用
android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"
-->
</data>
布局中歧沪,表達(dá)式
- 算術(shù)運(yùn)算符
+ - / * %
- 字符串連接運(yùn)算符
+
- 邏輯運(yùn)算符
&& ||
- 二元運(yùn)算符
& | ^
- 一元運(yùn)算符
+ - ! ~
- 移位運(yùn)算符
>> >>> <<
- 比較運(yùn)算符
== > < >= <=
instanceof
- 分組運(yùn)算符
()
- 字面量運(yùn)算符 - 字符歹撒、字符串、數(shù)字诊胞、null
- 類型轉(zhuǎn)換
- 方法調(diào)用
- 字段訪問(wèn)
- 數(shù)組訪問(wèn)
[]
- 三元運(yùn)算符
?:
<!-- 當(dāng)鏈?zhǔn)秸{(diào)用中存在可空類型時(shí), 如: -->
<TextView android:text="@{a.b.c.d.e}"/>
<!-- 相當(dāng)于 -->
<TextView android:text="@{a?.b?.c?.d?.e}"/>
<!-- 其中有一環(huán)為空, 則表達(dá)式值為null -->
<TextView android:text="@{expr ?? defautValue}"/>
<!-- 相當(dāng)于 -->
<TextView android:text="@{expr != null ? expr : defautValue}"/>
<!-- 資源引用 -->
android:padding="@{large ? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{@string/nameFormat(firstName, lastName)}"
...
<!-- function -->
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:onClick="@{() -> presenter.onSaveClick(task)}" />
...
</LinearLayout>
<!--
class Presenter {
fun onSaveClick(view: View, task: Task){}
}
-->
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
<!--
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
-->
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}"
<!-- ?: -->
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
適配器
現(xiàn)有的 資源引用表達(dá)式
滿足大多數(shù)情況暖夭,但也有例外,常見(jiàn)為ImageView
中撵孤。所以用適配器指定處理方法
@BindingMethods
// 將 android:tint 交由 setImageTintList(ColorStateList) 處理, 而非原有 setTint()
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
@BindingAdapter
@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);
}
}
//xml
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
-
@BindingConversion
, 自定義轉(zhuǎn)換
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
//xml
<View android:background="@{isError ? @drawable/error : @color/white}" .../>
-
@TargetApi
, 監(jiān)聽(tīng)器有多個(gè)方法時(shí)迈着,需要拆分處理
// View.OnAttachStateChangeListener 為例
// 有兩個(gè)方法:onViewAttachedToWindow(View) 和 onViewDetachedFromWindow(View)
// 1. 拆分
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
fun onViewDetachedFromWindow(v: View)
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
fun onViewAttachedToWindow(v: View)
}
// 2. BindAdapter
@BindingAdapter(
"android:onViewDetachedFromWindow",
"android:onViewAttachedToWindow",
requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach:OnViewAttachedToWindow?) {
...
}
// 3. xml中使用
Observable
, LiveData
作為數(shù)據(jù)
數(shù)據(jù)更新時(shí),UI自動(dòng)刷新
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
ObservableArrayList
ObservableArrayMap
// 自定義
class User : BaseObservable() {
@get:Bindable // 給getter方法打標(biāo)簽, BR中會(huì)生成對(duì)應(yīng)條目
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName) // 刷新UI
}
@get:Bindable
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName) // 刷新UI
}
}
或者用 LiveData
, 在代碼中需要調(diào)用setLifecycleOwner()
<!-- data class User(val firstName: LiveData<String>, val lastName: LiveData<String>) -->
<!-- xml中 -->
<data>
<variable name="duration" type="LiveData<String>"/>
<variable name="user" type="com.example.User"/>
</data>
<TextView android:text="@{user.firstName}"/>
<TextView android:text="@{duration}"/>
// kotlin
class ExampleFragment : Fragment(R.layout.example_fragment) {
...
binding.duration = liveData<String> { emitSource(...) }
binding.user = model.user
binding.setLifecycleOwner(viewLifecycleOwner)
...
}
結(jié)合兩者使用:
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
// 添加訂閱
override fun addOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
// 取消訂閱
override fun removeOnPropertyChangedCallback(
callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
// 全量刷新
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
// 精確刷新
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
數(shù)據(jù)雙向綁定 @={}
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean = data.rememberMe
fun setRememberMe(value: Boolean) {
if (data.rememberMe != value) {
data.rememberMe = value
// React to the change.
saveData()
notifyPropertyChanged(BR.remember_me)
}
}
}
使用@InverseBindingAdapter
和@InverseBindingMethod
, 自定義雙向綁定
// 1. 數(shù)據(jù)變動(dòng)時(shí)調(diào)用的方法
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue
}
}
// 2. view變動(dòng)時(shí)調(diào)用的方法
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
// 3. 變動(dòng)時(shí)機(jī)和方式, 后綴`AttrChanged`
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
view: MyView,
attrChange: InverseBindingListener
) {
// 使用 InverseBindingListener 告知數(shù)據(jù)綁定系統(tǒng)邪码,特性已更改
// 數(shù)據(jù)綁定系統(tǒng)調(diào)用@InverseBindingAdapter綁定的方法
// warning: 避免陷入循環(huán)刷新.
}