Android 笔记:Intent
Intent 用于组件与系统间传递消息。Android 组件除了 Activity 之外,还有 Service,Broadcast Receiver 和 Content Provider。
启动应用内的 Activity
在 Android 中,启动一个 Activity 并非像其它常见的前端开发中所做的那样,直接调用另一个 Activity 的构造方法来创建新的 Activity。而是调用当前 Activity 的 startActivity(Intent)
方法,通过系统的 ActivityManager 来创建 Activity 实例,并调用该实例的 onCreate(Bundle?)
方法。
class MainActivity : AppCompatActivity {
override fun onCreate(savedInstanceState: Bundle?) {
binding.cheatButton.setOnClickListener {
val intent = Intent(this, CheatActivity::class.java)
startActivity(intent)
}
}
}
这里 Intent 的构造函数有两个参数,前者是个 Context
对象,在这里是当前 Activity 实例自身。用于传递给 ActivityManager,告知其在哪里可以找到目标 Activity。要理解它,需要了解在 Android 中,各组件的启动是靠各种系统服务(例如 ActivityManager, ServiceManager)负责的,不同的 Activity 与 Activity 是被设计成相互独立的——即使是在同一个应用当中。但在一个 Activity 中启动同一应用中的另一个 Activity 这种场景中,context 只会是当前 Activity,也就是 this
。如果要启动其它应用中的 Activity,则需要用到 URI。这么看来,其实这个 this
完全可以省掉,设计成默认值。这个 API 让我设计的话,我会设计成:startActivity(CheatActivity::class.java)
。
第二个参数是要启动的目标 Activity 的 Class。ActivityManager 会在应用的 manifest 文件中查找该 Class 对应的声明。如果找不到对应的声明,会抛出一个 ActivityNotFoundException
。
用 Intent 携带数据
Intent 内有个叫 extra 的键值对容器,可以用来携带数据,在不同的 Android 组件间传递。
首先,在接收方声明它所需要的 key,这个 key 通常以包名为前缀,用以防止不同应用间消息的命名冲突。然后在接收方写好获取和使用数据的代码:
const val EXTRA_ANSWER_IS_TRUE = "me.duron600.myapplication.answer_is_true"
class CheatActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheatBinding
private var answerIsTrue: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCheatBinding.inflate(layoutInflater)
setContentView(binding.root)
answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false)
binding.showAnswerButton.setOnClickListener {
val answerText = if (answerIsTrue) R.string.true_button else R.string.false_button
binding.answerTextView.setText(answerText)
}
}
}
发送方调用 startActivity(Intent)
传入 Intent 来启动接收方的 Activity。接收方会使用这个 Intent 构造 Activity,在其内部,通过 intent
的 getter 可以直接访问到这个传入的 Intent。
在发送方,只需要调用 Intent 的 putExtra(String, T)
就可以传入数据:
binding.cheatButton.setOnClickListener {
val intent = Intent(this, CheatActivity::class.java)
intent.putExtra(EXTRA_ANSWER_IS_TRUE, getCurrentQuestion().answer)
startActivity(intent)
}
接收、处理 Activity 返回的结果
有些场景需要用到 Activity 返回的结果,比如文章编辑器 Activity 调用负责浏览文件的 Activity 浏览并打开某个图片,把图片载入编辑器 Activity。
此时,得通过一个 ActivityResultLauncher
的 launch(Intent)
方法来加载 Activity,而非直接调用 startActivity(Intent)
。构造这个 ActivityResultLauncher,要用到 registerForActivityResult()
方法。该方法接受两个参数,第 1 个是个 ActivityResultContract
,Android 内置了许多 Contract,包括但不限于:
-
StartActivityForResult
:通用的结果返回契约 -
RequestPermission
:请求单个权限 -
RequestMultiplePermissions
:请求多个权限 -
TakePicture
:拍照 -
PickContact
:选择联系人 -
GetContent
:选择内容(如图片、文件) -
CreateDocument
:创建文档
如果有必要,可以继承 ActivityResultContract
来编写自己的 Contract。
registerForActivityResult()
的第 2 个参数是个回调 lambda,会在子 Activity 返回时执行,并将结果通过 lambda 的参数 result
返回。
val cheatLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
quizViewModel.isCheater = result.data?.getBooleanExtra(EXTRA_ANSWER_SHOWN, false) ?: false
}
}
val intent = Intent(this, CheatActivity::class.java)
cheatLauncher.launch(intent)
而在子 Activity 那一端,则需要往另一个 Intent 里 putExtra()
,并使用 setResult()
方法将数据塞入 Acitvity 的结果中,最后在 Activity 退出时返回给启动它的父 Activity。
binding.showAnswerButton.setOnClickListener {
// ...
val data = Intent()
data.putExtra(EXTRA_ANSWER_SHOWN,true)
setResult(Activity.RESULT_OK, data)
}
这样子 Activity 返回的数据就传到了上面 registerForActivityResult()
的 lambda 参数中。
需要注意的是,setResult()
的结果,在屏幕旋转后会失效,如果 setResult()
之后不是立即返回之前的 Activity 的话,需要考虑用 ViewModel 来防止这种问题。
startActivityForResult()
和 onActivityResult()
在一些旧的代码中,可能存在着 startActivityForResult()
和 onActivityResult()
这些回调方法。实际上,上面的代码用到的 registerForActivityResult()
等一系列 API(Activity Result APIs),是构建于这两个方法之上的。
同 onSaveInstanceState()
一样,如果看见这些过时的 API 调用,应该尽量尝试修改为使用新的 API。
隐式 Intent
用具体的 Context 和 Class 构造的 Intent 叫显式 Intent。如果需要在一个应用中的 Activity 中启动另一个应用中的 Activity,则需要用到隐式 Intent。