Jetpck 才是真的豪華全家桶
引言
- 通過(guò)視圖綁定功能专普,可以更輕松地編寫可與視圖交互的代碼。
- 在模塊中啟用視圖綁定之后码耐,系統(tǒng)會(huì)為該模塊中的每個(gè) XML 布局文件生成一個(gè)綁定類追迟。
- 綁定類的實(shí)例包含對(duì)在相應(yīng)布局中具有 ID 的所有視圖的直接引用。
- 在大多數(shù)情況下骚腥,視圖綁定會(huì)替代 findViewById敦间。
整體預(yù)覽
1. 使用說(shuō)明
1.1 環(huán)境配置
1.1.1 版本要求
?ViewBinding 在 Android Studio 3.6 Canary 11 及更高版本中可用。
1.1.2 模塊啟用
//build.gradle
android {
buildFeatures {
viewBinding true
}
}
1.2 語(yǔ)法說(shuō)明
1.2.1 layout布局文件
<?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">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
?生成規(guī)則:將 XML 文件的名稱轉(zhuǎn)換為駝峰式大小寫束铭,并在末尾添加“Binding”一詞廓块。比如上面的文件是 item_rv.xml,那么會(huì)生成ItemRvBindiing的綁定類契沫。
?綁定內(nèi)容:生成對(duì)應(yīng)的綁定類均包含對(duì)根視圖以及具有 ID 的所有視圖的引用带猴。如果視圖沒(méi)有添加 ID,則不會(huì)生成對(duì)應(yīng)的綁定類引用懈万。
?根視圖:每個(gè)綁定類還包含一個(gè) getRoot()
方法浓利,用于為相應(yīng)布局文件的根視圖提供直接引用。比如上面的就是LinearLayout根視圖钞速。
1.2.2 layout文件忽略
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
?構(gòu)建優(yōu)化 & 內(nèi)存優(yōu)化:ViewBinding的開(kāi)啟會(huì)對(duì)所有的布局文件進(jìn)行綁定類生成贷掖,如果有些布局文件不需要綁定類生成, 則可以在根布局添加設(shè)置進(jìn)行關(guān)閉渴语。
?Apk瘦身優(yōu)化:在release版本苹威,可以添加 shrinkResources 將沒(méi)有用到的綁定類不打包在apk中。
1.3 場(chǎng)景舉例
1.3.1 Activity
class ViewBindingActivity : AppCompatActivity() {
private lateinit var binding: ActivityViewBindingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//傳統(tǒng)方式
// setContentView(R.layout.activity_view_binding)
//ViewBinding方式
binding = ActivityViewBindingBinding.inflate(layoutInflater);
val view = binding.root
setContentView(view)
//ViewBinding操作演示
binding.rename.setOnClickListener {
binding.name.text = binding.name.text.toString() + "-rename"
}
}
}
三步走
- 調(diào)用生成的綁定類中包含的靜態(tài)
inflate()
方法驾凶。此操作會(huì)創(chuàng)建該綁定類的實(shí)例以供 Activity 使用牙甫。 - 通過(guò)調(diào)用
getRoot()
方法或使用 Kotlin 屬性語(yǔ)法獲取對(duì)根視圖的引用。 - 將根視圖傳遞到
setContentView()
调违,使其成為屏幕上的活動(dòng)視圖窟哺。
1.3.2 Fragment
class ViewBindingFragmentSample : Fragment() {
private var _binding: FragmentViewBindingBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//傳統(tǒng)方式
// return inflater.inflate(R.layout.fragment_view_binding, container, false)
//style 1
_binding = FragmentViewBindingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//style 2(需要調(diào)用onCreateView中的傳統(tǒng)方式)
// _binding = FragmentViewBindingBinding.bind(view)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
三步走
- 調(diào)用生成的綁定類中包含的靜態(tài)
inflate()
方法。此操作會(huì)創(chuàng)建該綁定類的實(shí)例以供 Fragment 使用技肩。 - 通過(guò)調(diào)用
getRoot()
方法或使用 Kotlin 屬性語(yǔ)法獲取對(duì)根視圖的引用且轨。 - 從
onCreateView()
方法返回根視圖(也可以在onViewCreated()
進(jìn)行bind()
),使其成為屏幕上的活動(dòng)視圖虚婿。
1.3.3 ViewHolder
class RvAdapter(private val mData : List<String>) : RecyclerView.Adapter<RvAdapter.RvViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RvViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemRvTextBinding.inflate(inflater)
return RvViewHolder(binding)
}
override fun getItemCount(): Int {
return mData.size
}
override fun onBindViewHolder(holder: RvViewHolder, position: Int) {
holder.getBinding().title.text = mData[position]
}
class RvViewHolder(private val binding: ItemRvTextBinding) : RecyclerView.ViewHolder(binding.root) {
fun getBinding() : ItemRvTextBinding {
return binding
}
}
}
三步走
- 調(diào)用生成的綁定類中包含的靜態(tài)
inflate()
方法旋奢。此操作會(huì)創(chuàng)建該綁定類的實(shí)例以供 ViewHolder 使用。 - 在
onCreateViewHolder
創(chuàng)建 ViewHolder然痊,使其成為屏幕上的活動(dòng)視圖至朗。 - 從
onBindViewHolder()
進(jìn)行綁定類的屬性獲取更新。
1.4 ViewBinding類
1.4.1 文件說(shuō)明
1.4.1.1 綁定類路徑
JavaModel:app/build/generated/data_binding_base_class_source_out/buildTypes
/out/packageName
/databinding
Layout文件:app/build/intermediates/data_binding_layout_info_type_merge/buildTypes
/out
1.4.1.2 綁定類內(nèi)容
JavaModel
- 作用1:根據(jù)layout文件生成視圖剧浸,并與父布局關(guān)聯(lián)(可能)锹引。
- 作用2:根據(jù)生成的視圖矗钟,綁定子視圖組件的引用。
public final class ActivityViewBindingBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView; //根對(duì)象
@NonNull
public final LinearLayout detail; //只要xml寫了ID嫌变,那么就會(huì)生成對(duì)應(yīng)的引用
@NonNull
public final TextView name;
@NonNull
public final Button rename;
@NonNull
public final RecyclerView rv;
//私有構(gòu)造函數(shù)真仲,只能用在下面的 bind()方法中,符合最少知道原則
private ActivityViewBindingBinding(@NonNull LinearLayout rootView, @NonNull LinearLayout detail,
@NonNull TextView name, @NonNull Button rename, @NonNull RecyclerView rv) {
this.rootView = rootView;
this.detail = detail;
this.name = name;
this.rename = rename;
this.rv = rv;
}
@Override
@NonNull
//根布局可獲取
public LinearLayout getRoot() {
return rootView;
}
@NonNull
//inflate 重載方法
public static ActivityViewBindingBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
//inflate 重載方法:生成視圖并綁定子視圖組件的引用
public static ActivityViewBindingBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
//生成視圖
View root = inflater.inflate(R.layout.activity_view_binding, parent, false);
if (attachToParent) { //是否添加父布局初澎。tip:這個(gè)會(huì)影響這個(gè)layout文件的根視圖參數(shù)是否生效
parent.addView(root);
}
return bind(root); //綁定子視圖組件的引用
}
@NonNull
//綁定子視圖組件的引用
public static ActivityViewBindingBinding 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.detail;
//回歸原始秸应,依然通過(guò) findViewById 獲取視圖,并保存保存引用
LinearLayout detail = rootView.findViewById(id);
if (detail == null) {
break missingId;
}
id = R.id.name;
TextView name = rootView.findViewById(id);
if (name == null) {
break missingId;
}
id = R.id.rename;
Button rename = rootView.findViewById(id);
if (rename == null) {
break missingId;
}
id = R.id.rv;
RecyclerView rv = rootView.findViewById(id);
if (rv == null) {
break missingId;
}
//返回最終的綁定類
return new ActivityViewBindingBinding((LinearLayout) rootView, detail, name, rename, rv);
}
//視圖解析錯(cuò)誤異常拋出
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
Layout文件
- 作用:為了生成JavaModel對(duì)象
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app/src/main/res/layout/activity_view_binding.xml"
//因?yàn)橹粏⒂昧薞iewBinding碑宴,所以isBindingData是false
isBindingData="false" isMerge="false" layout="activity_view_binding"
modulePackage="com.kejiyuanren.jetpack" rootNodeType="android.widget.LinearLayout">
//標(biāo)記了Targets信息软啼,為了生成對(duì)應(yīng)的JavaModel
<Targets>
<Target tag="layout/activity_view_binding_0" view="LinearLayout">
<Expressions />
<location endLine="57" endOffset="14" startLine="1" startOffset="0" />
</Target>
<Target id="@+id/name" view="TextView">
<Expressions />
<location endLine="14" endOffset="36" startLine="9" startOffset="4" />
</Target>
<Target id="@+id/detail" view="LinearLayout">
<Expressions />
<location endLine="43" endOffset="18" startLine="22" startOffset="4" />
</Target>
<Target id="@+id/rename" view="Button">
<Expressions />
<location endLine="41" endOffset="35" startLine="36" startOffset="8" />
</Target>
<Target id="@+id/rv" view="androidx.recyclerview.widget.RecyclerView">
<Expressions />
<location endLine="55" endOffset="41" startLine="51" startOffset="4" />
</Target>
</Targets>
</Layout>
1.4.2 ViewBinding類 原理分析
?分析入口:從databinding-compiler
進(jìn)行分析,因?yàn)樵诰幾g環(huán)節(jié)會(huì)被調(diào)用延柠。
1.4.2.1 從何而來(lái)祸挪?
結(jié)論:利用res/layout中的xml文件,解析生成元素對(duì)象贞间,進(jìn)行緩存贿条。
(1)LayoutXmlProcessor -> processResources()
public boolean processResources(final ResourceInput input)
throws ParserConfigurationException, SAXException, XPathExpressionException,
IOException {
……
//核心處理
ProcessFileCallback callback = new ProcessFileCallback() {
……
if (input.isIncremental()) {
//增量編譯
processIncrementalInputFiles(input, callback);
} else {
//全量編譯
processAllInputFiles(input, callback);
}
……
}
(2)LayoutXmlProcessor -> processAllInputFiles()
private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback)
throws IOException, XPathExpressionException, SAXException,
ParserConfigurationException {
……
for (File firstLevel : input.getRootInputFolder().listFiles()) {
if (firstLevel.isDirectory()) { //是否是路徑
//文件夾是否為layout開(kāi)頭
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
//創(chuàng)建對(duì)應(yīng)文件夾
callback.processLayoutFolder(firstLevel);
//noinspection ConstantConditions
//遍歷文件夾中的xml文件
for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
//處理布局文件
callback.processLayoutFile(xmlFile);
}
} else {
……
}
(3)LayoutXmlProcessor -> processSingleFile()
public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output)
throws ParserConfigurationException, SAXException, XPathExpressionException,
IOException {
//解析xml文件
final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
.parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup);
if (bindingLayout != null && !bindingLayout.isEmpty()) {
//解析出來(lái)的元素對(duì)象進(jìn)行緩存
mResourceBundle.addLayoutBundle(bindingLayout, true);
return true;
}
return false;
}
(4)LayoutFileParser -> parseXml()
public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
@NonNull final File outputFile, @NonNull final String pkg,
@NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup)
throws ParserConfigurationException, IOException, SAXException,
XPathExpressionException {
……
//解析繼續(xù)
return parseOriginalXml(
RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),
pkg, encoding);
……
}
(5)LayoutFileParser -> parseOriginalXml()
private static ResourceBundle.LayoutFileBundle parseOriginalXml(
@NonNull final RelativizableFile originalFile, @NonNull final String pkg,
@NonNull final String encoding)
throws IOException {
……
//文件解析(這個(gè)是databing用的,viewbinding的話在實(shí)現(xiàn)中直接return null)
XMLParser.ElementContext data = getDataNode(root);
XMLParser.ElementContext rootView = getViewNode(original, root);
……
//生成元素對(duì)象
ResourceBundle.LayoutFileBundle bundle =
new ResourceBundle.LayoutFileBundle(
originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
isMerge);
final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
//數(shù)據(jù)解析(這個(gè)是databing用的增热,viewbinding的話在實(shí)現(xiàn)中直接return null)
parseData(original, data, bundle);
parseExpressions(newTag, rootView, isMerge, bundle);
return bundle;
……
}
?
1.4.2.2 途徑哪里整以?
結(jié)論:利用上一節(jié)生成的元素對(duì)象緩存,解析生成中間件layout文件(build目錄下的xml文件)峻仇。
(1)LayoutXmlProcessor -> writeLayoutInfoFiles()
//
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
//元素對(duì)象集合遍歷
for (List<ResourceBundle.LayoutFileBundle> layouts : mResourceBundle.getLayoutBundles()
.values()) {
for (ResourceBundle.LayoutFileBundle layout : layouts) {
//利用元素對(duì)象緩存生成layout文件
writeXmlFile(writer, xmlOutDir, layout);
}
}
……
}
(2)LayoutXmlProcessor -> writeXmlFile()
private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
ResourceBundle.LayoutFileBundle layout)
throws JAXBException {
//生成文件名
String filename = generateExportFileName(layout);
//寫文件
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
?
1.4.2.3 去往何處公黑?
結(jié)論:利用上一節(jié)生成的中間件layout文件,解析生成ViewBinding類摄咆。
(1)BaseDataBinder -> init()
init {
input.filesToConsider
.forEach {
it.inputStream().use {
// 將中間件layout中的xml文件 轉(zhuǎn)成 LayoutFileBundle
val bundle = ResourceBundle.LayoutFileBundle.fromXML(it)
// 緩存進(jìn) ResourceBundle
resourceBundle.addLayoutBundle(bundle, true)
}
……
}
(2)BaseDataBinder -> generateAll()
fun generateAll(writer : JavaFileWriter) {
……
//根據(jù)文件名進(jìn)行分組排序凡蚜,并進(jìn)行遍歷所有ResourceBundle
resourceBundle.layoutFileBundlesInSource.groupBy { it.mFileName }.forEach {
val layoutName = it.key
val layoutModel = BaseLayoutModel(it.value)
//BaseLayoutBinderWriter生成
val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
//BaseLayoutBinderWriter的解析處理:binderWriter.write(),并寫文件:writer.writeToFile()
writer.writeToFile(binderWriter.write())
……
}
(3)BaseLayoutBinderWriter -> write()
//解析數(shù)據(jù): createType()吭从,并生成JavaFile:javaFile()
fun write() = javaFile(binderTypeName.packageName(), createType()) {
addFileComment("Generated by data binding compiler. Do not edit!")
}
(4)BaseLayoutBinderWriter -> createType()
//解析所有朝蜘,細(xì)節(jié)就不跟了
private fun createType() = classSpec(binderTypeName) {
superclass(viewDataBinding)
addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
addFields(createBindingTargetFields())
addFields(createVariableFields())
addMethod(createConstructor())
addMethods(createGettersAndSetters())
addMethods(createStaticInflaters())
}
?
原理分析總結(jié):
- Step1:layout文件(xml資源文件) -> 解析 -> 元素緩存 。
- Step2:元素緩存 -> 解析 -> layout文件(中間件) 涩金。
- Step3:layout文件(中間件) -> 解析 -> ViewBinding類 谱醇。
2. 橫向?qū)Ρ?/h1>
2.1 findViewById
與使用 findViewById 相比,視圖綁定具有一些很顯著的優(yōu)點(diǎn):
- Null 安全:由于視圖綁定會(huì)創(chuàng)建對(duì)視圖的直接引用鸭廷,因此不存在因視圖 ID 無(wú)效而引發(fā) Null 指針異常的風(fēng)險(xiǎn)枣抱。
- 類型安全:每個(gè)綁定類中的字段均具有與它們?cè)?XML 文件中引用的視圖相匹配的類型熔吗。這意味著不存在發(fā)生類轉(zhuǎn)換異常的風(fēng)險(xiǎn)辆床。
這些差異意味著布局和代碼之間的不兼容將會(huì)導(dǎo)致構(gòu)建在編譯時(shí)(而非運(yùn)行時(shí))失敗。
2.2 DataBinding
視圖綁定和數(shù)據(jù)綁定均會(huì)生成可用于直接引用視圖的綁定類桅狠。但是讼载,視圖綁定旨在處理更簡(jiǎn)單的用例轿秧,與數(shù)據(jù)綁定相比,具有以下優(yōu)勢(shì)
:
- 更快的編譯速度:視圖綁定不需要處理注釋咨堤,因此編譯時(shí)間更短菇篡。
- 易于使用:視圖綁定不需要特別標(biāo)記的 XML 布局文件,因此在應(yīng)用中采用速度更快一喘。在模塊中啟用視圖綁定后驱还,它會(huì)自動(dòng)應(yīng)用于該模塊的所有布局。
反過(guò)來(lái)凸克,與數(shù)據(jù)綁定相比议蟆,視圖綁定也具有以下限制
:
- 視圖綁定不支持布局變量或布局表達(dá)式,因此不能用于直接在 XML 布局文件中聲明動(dòng)態(tài)界面內(nèi)容萎战。
- 視圖綁定不支持雙向數(shù)據(jù)綁定咐容。
推薦
:在某些情況下,最好在項(xiàng)目中同時(shí)使用視圖綁定和數(shù)據(jù)綁定蚂维〈亮#可以在需要高級(jí)功能的布局中使用數(shù)據(jù)綁定,而在不需要高級(jí)功能的布局中使用視圖綁定虫啥。
3. 小結(jié)
?ViewBinding相比DataBinding更輕量級(jí)蔚约,不是每次殺雞都需要用牛刀。盡快把 findViewById 忘了吧涂籽!
?