Android开发-布局绑定

在使用 Android Studio 2022.2.1 完成一个点击按钮弹出 Toasr 提醒的实践记录,本文中的路径均为在Project项目结构下的路径

项目配置

.\app\build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.test'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.test"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

.\app\src\main\AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

使用findViewById()

首先是一个含有一个 button 的 activity ,路径为 .\app\src\main\java\com\example\test\MainActivity.kt,先使用最基础的findViewById()方式获取 button 对象,并添加点击时间监听方法:

package com.example.test

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.Toast

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 根据layout布局文件设置当前视图
        setContentView(R.layout.main_layout)
        // 通过在layout文件中给button设置的id获取button对象
        val button1: Button = findViewById(R.id.button1)
        // 添加点击事件监听器
        button1.setOnClickListener {
            Toast.makeText(this, "Button 1 have been clicked", Toast.LENGTH_SHORT).show()
        }
    }
}

以及其对应的 layout 文件,路径为.\app\src\main\res\layout\main_layout.xml

<?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">
    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1"
    />

</LinearLayout>

此时运行效果正常:

findViewById

使用kotlin-android-extensions插件

尝试配置kotlin-android-extensions,修改\app\build.gradle文件的plugin部分,引入kotlin-android-extensions插件,具体修改如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-android-extensions'
}
...

此时点击 Sync now,产生如下报错:

The 'kotlin-android-extensions' Gradle plugin is no longer supported. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.

发现在新版本的 AndroidStudio 中kotlin-android-extensions已不再支持,推荐使用View Binding进行绑定。放弃使用该方法,在plugins部分中删除对 kotlin-android-extensions的引用。

使用 View Binding

配置View Binding,修改\app\build.gradle文件,在android{}中新增一个buildFeatures{},将viewBinding设为ture。具体修改如下:

...
android {
    namespace 'com.example.test'
    compileSdk 33

    buildFeatures {
        viewBinding true
    }

    defaultConfig {
    ...
}
...

修改完build.gradle不要忘了点一下Sync now。之后修改 activity 文件MainActivity.kt,将viewBinding类引入,并通过类获得按钮的实例。在开启 View Binding 之后系统会为每个 XML 布局文件生成一个绑定类,类名是将 XML 文件的名称转换为驼峰式命名后在末尾添加“Binding”得到的,类中包含根页面与各种控件的引用。这里的main_layout.xml布局会生成对应的MainLayoutBinding类。具体修改如下:

package com.example.test

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
// 需要导入对应的绑定类,一般IDE自己会生成
import com.example.test.databinding.MainLayoutBinding

class MainActivity : AppCompatActivity() {
    // 将绑定类实例作为Activity类的成员变量,方便后续调用
    private lateinit var binding : MainLayoutBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 获取绑定类的实体类进行实例化
        binding = MainLayoutBinding.inflate(layoutInflater)
        // 获取绑定类中的根视图 (Root View)
        setContentView(binding.root)
        // 对绑定类中的button添加点击事件监听
        binding.button1.setOnClickListener{
            Toast.makeText(this, "button 1 have been clicked - ViewBinding", Toast.LENGTH_SHORT).show()
        }
    }
}

此时运行正常:

ViewBinding

遇到的问题

问题描述

在使用ViewBinding的方式进行改变,如果不修改 setContentView(R.layout.main_layout)setContentView(binding.root) 虽然编译正常且按钮可以点击,但不能正常弹出 Toast 提示:

package com.example.test

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import com.example.test.databinding.MainLayoutBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding : MainLayoutBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = MainLayoutBinding.inflate(layoutInflater)

        // 获取绑定类中的根视图 (Root View),功能正常
        // setContentView(binding.root)
        // 根据layout文件创建视图,功能失效
        setContentView(R.layout.main_layout)

        // 使用findViewById方法获取控件并设置
        // findViewById<Button>(R.id.button1).setOnClickListener{
        //     Toast.makeText(this, "button 1 have been clicked - findViewById", Toast.LENGTH_SHORT).show()
        // }
        // 使用绑定类实例 binding.获取控件并设置
        binding.button1.setOnClickListener{
           Toast.makeText(this, "button 1 have been clicked - ViewBinding", Toast.LENGTH_SHORT).show()
        }
    }
}

演示如下:

UnEditSetView

与 使用了setContentView(R.layout.main_layout)后不能使用binding获取控件 不同的是:使用了setContentView(binding.root)后 仍可使用findViewById(R.id.button1) 获取控件,功能正常显示(需要复现的话调整一下上方代码的注释)。总结一下功能关系:

setContentView() 参数 binding.button1 获取控件 findViewById(R.id.button1) 获取控件
R.layout.main_layout 正常 正常
binding.root 失效 正常

原因解析

我们可以在由ViewBinding自动生成的MainLayoutBinding.java中找到具体的实现,该文件的路径为.\app\build\generated\data_binding_base_class_source_out\debug\out\com\example\test\databinding\MainLayoutBinding.java

// Generated by view binder compiler. Do not edit!
package com.example.test.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import androidx.viewbinding.ViewBindings;
import com.example.test.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class MainLayoutBinding implements ViewBinding {
  @NonNull
  private final LinearLayout rootView;

  @NonNull
  public final Button button1;

  private MainLayoutBinding(@NonNull LinearLayout rootView, @NonNull Button button1) {
    this.rootView = rootView;
    this.button1 = button1;
  }

  @Override
  @NonNull
  public LinearLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static MainLayoutBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static MainLayoutBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.main_layout, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static MainLayoutBinding 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.button1;
      Button button1 = ViewBindings.findChildViewById(rootView, id);
      if (button1 == null) {
        break missingId;
      }

      return new MainLayoutBinding((LinearLayout) rootView, button1);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

我们根据这个代码来分析一下binding的实例化过程:

  • MainActivity.kt中实例化MainLayoutBinding使用的是MainLayoutBinding类中的静态方法inflate(layoutInflater)
  • 这个方法会接着调用inflater.inflate(R.layout.main_layout, null, false)inflate()返回的是根据UML描述创建的新视图(View) 。该语句在这里的意义是:根据main_layout.xml生成一个根视图root,也就是说在这个root中有一个button1的子视图(或者说控件),并且root视图没有父视图。
  • 接下来调用MainLayoutBinding类中的静态方法bind(root)。在 bind函数中同样会使用R.id.button1 获取button1的内部编号值。
  • 再调用ViewBindings.findChildViewById()并获取在root中的button1的视图实例的引用,
  • 调用构造函数生成一个MainLayoutBinding类的对象并返回,返回的实例中成员变量button1是成员变量root的一个子视图的引用。

之后是通过setContentView()来将一个View设置为当前显示的视图。之后的findViewById会在当前显示的视图中进行查找。问题就在这里:

  • 当使用setContentView(binding.root)时,会将binding.root作为当前视图,此时binding.button1也是在当前视图里的,所以通过binding.button1或者findViewById<Button>(R.id.button1)找到的都是作为binding.root的子视图的那个button1,所以设置后都可以正常显示 Toast 消息。
  • 当使用setContentView(R.layout.main_layout)时,会根据main_layout.xml创建一个新的视图,并把这个新视图作为当前视图。需要注意的是这个新创建的视图与刚才创建的bing.root完全不相关,他们分别是两个完全独立的对象。所以在之后设置findViewById<Button>(R.id.button1)仍能正常显示消息。但通过binding.button1设置时,由于binding.button1所属的父页面binding.root并不是当前视图(换句话说binding.button1findViewById<Button>(R.id.button1)指向的是不同的对象),所以虽然进行了设置,但是仍然在当前视图并不显示。

搞清楚原因之后可以验证一下,如果把代码修改成这个样子,连续点击3次 Button 1,运行的结果会是什么呢:

package com.example.test

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import com.example.test.databinding.MainLayoutBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding : MainLayoutBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = MainLayoutBinding.inflate(layoutInflater)
        // 根据layout文件创建视图
        setContentView(R.layout.main_layout)

        // 使用findViewById方法获取控件并设置
        findViewById<Button>(R.id.button1).setOnClickListener{
            Toast.makeText(this, "button 1 have been clicked - findViewById", Toast.LENGTH_SHORT).show()
            // 切换当前视图到 binding.root对应的视图
            setContentView(binding.root)
        }
        // 使用绑定类实例 binding.获取控件并设置
        binding.button1.setOnClickListener{
            Toast.makeText(this, "button 1 have been clicked - ViewBinding", Toast.LENGTH_SHORT).show()
            // 再创建一个新的视图并切换
            setContentView(R.layout.main_layout)
        }
    }
}

是第一次点击显示button 1 have been clicked - findViewById,第二次点击显示button 1 have been clicked - ViewBinding,第三次点击不显示。

FinalTest

原因很简单,先看代码:创建binding时产生了一个button1先叫它A,在第一次使用setContentView(R.layout.main_layout)后又创建了一个button1先叫它B,此时B是在当前视图里的,也就是说findViewById<Button>(R.id.button1)获得到的是B。然后给B设置弹窗,同时设置点击B之后将binding.root作为当前视图。之后设置点击A的消息提示,并设置点击A后使用setContentView(R.layout.main_layout)再创建一个全新的视图,这个视图又有一个新的Button1先叫它C

在运行的时候,第一次点击点击的是B,第二次点击的是A,第三次点击的是C,由于没有给C设置点击后的动作,所以在之后一直点击的都是C了,而且没有弹窗。问题彻底解决。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇