本节摘要:Android 用声明式 XML 描述界面结构、用命令式 Kotlin 驱动行为,二者靠
android:id与引用查找连接。本节讲清 setContentView 如何加载布局、findViewById 与 View Binding 两条接线路线的差别,并把猜数字 App 的点击逻辑真正接通。
为什么界面要写在 XML 里而不是纯代码里?拆开看两者的擅长面就明白了:XML 的标签嵌套天然表达"结构",改个属性立刻在预览里看到效果,设计师和开发可以围着同一份文件讨论;而 Kotlin 擅长"行为"——响应点击、改状态、跳页面。Android 把这两件事分给两种语言,各干各的强项。
分工的边界在哪?我的经验法则:界面长什么样(结构、尺寸、初始文字)归 XML;界面在运行中怎么变(用户点了一下之后发生什么)归 Kotlin。 有些灰色地带,比如按状态显示不同文字,两边都能做,此时优先 XML 里写初始值、代码里负责切换,逻辑最清晰。
写布局时最外层标签上那一长串 xmlns:android 不是仪式,它是命名空间声明。没有它,android: 前缀的属性无法解析。还有一个常见的 tools: 命名空间,专供预览使用——比如给 EditText 填一段示例文字,只在布局设计器里显示,运行时不出现:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" tools:text="太大了,往小猜" android:text="" /> </LinearLayout>
tools:text 让你在设计器里就能看到"有提示文字时的样子",又不影响线上初始为空。这种小技巧在调布局时非常省事。
setContentView(R.layout.activity_main) 执行后,XML 被 inflate( inflation,膨胀成真实的 View 对象树)挂到 Activity 上。此后代码想操作某个控件,就按 id 找回它:
class MainActivity : AppCompatActivity() { private lateinit var tvResult: TextView private lateinit var etGuess: EditText private lateinit var btnGuess: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvResult = findViewById(R.id.tv_result) etGuess = findViewById(R.id.et_guess) btnGuess = findViewById(R.id.btn_guess) btnGuess.setOnClickListener { val input = etGuess.text.toString().toIntOrNull() tvResult.text = when { input == null -> "请输入有效数字" input > answer -> "太大了" input < answer -> "太小了" else -> "猜对了!共 ${++count} 次" } } } // answer 与 count 的初始化见 1.1 节 }
R.id.tv_result 是编译期生成的整数常量,与 XML 里 @+id/tv_result 一一对应。findViewById 返回的类型是泛型推断的,Kotlin 里通常不用手写强转。
这条路线的问题:id 写错或类型写错要到运行时才炸;控件一多,满屏 findViewById 样板代码。
View Binding 为每份布局自动生成一个绑定类,把"找控件"变成"直接访问属性",编译期就检查类型与存在性。先在模块的构建配置里打开开关(约三行),然后用法变成:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnGuess.setOnClickListener { val input = binding.etGuess.text.toString().toIntOrNull() binding.tvResult.text = when { input == null -> "请输入有效数字" input > answer -> "太大了" input < answer -> "太小了" else -> "猜对了!共 ${++count} 次" } } } }
绑定类的名字由布局文件名转来:布局文件编译后生成对应的绑定类,binding.root 就是布局的根视图。与 findViewById 相比:null 安全、类型安全、改 id 后编译器立刻报错点。新项目建议默认用 View Binding,findViewById 仍要会——老代码、面试、看第三方示例都绕不开它。
