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。

此时,得通过一个 ActivityResultLauncherlaunch(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。