Android日积月累系列之六-Activity与Fragment和Fragment与Fragment通信
为了重复使用 Fragment,请将每个 Fragment 构建为一个完全独立的组件,使其定义自己的布局和行为。定义这些可重复使用的 Fragment 后,您可以将其与 Activity 关联,并将其与应用逻辑联系起来,以实现整体复合界面。
为了正确响应用户事件,或为了共享状态信息,通常需要在 activity 与其 fragment 之间或者两个或更多 fragment 之间具有通信渠道。为使 Fragment 保持独立,您不应让 Fragment 直接与其他 Fragment 或与其宿主 Activity 进行通信。
Fragment
库提供了两个通信选项:共享的 ViewModel
和 Fragment Result API。建议的选项取决于用例。如需与任何自定义 API 共享持久性数据,您应使用 ViewModel
。对于包含的数据可放置在 Bundle
中的一次性结果,您应使用 Fragment Result API。
1.ViewModel作用范围
ViewModel可能在一个Activity或Fragment内使用,也可以指定其作用范围,以在多个Activity/Fragment里共享同一对象,实现Activity和Fragment间通信。
以kotlin为例
1.1 Activity和Fragment共享同一个ViewModel实例
关键点
private val viewModel: ItemViewModel by activityViewModels()
class MainActivity : AppCompatActivity() {
// Using the viewModels() Kotlin property delegate from the activity-ktx
// artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this, Observer { item ->
// Perform an action with the latest item data
})
}
}
class ListFragment : Fragment() {
// Using the activityViewModels() Kotlin property delegate from the
// fragment-ktx artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by activityViewModels()
// Called when the item is clicked
fun onItemClicked(item: Item) {
// Set a new item
viewModel.selectItem(item)
}
}
1.2 多个Fragment之间共享同一个ViewModel实现通讯
同一 activity 中的两个或更多 fragment 通常需要相互通信。例如,假设有一个 Fragment 显示一个列表,另一个 Fragment 允许用户对该列表应用各种过滤器。如果 Fragment 不直接通信(这意味着,它们不再独立),这种情况可能不容易实现。此外,这两个 fragment 还必须处理另一个 fragment 尚未创建或不可见的情况。
这两个 Fragment 可以使用其 Activity 范围共享 ViewModel
来处理这种通信。通过以这种方式共享 ViewModel
,Fragment 不需要相互了解,Activity 也不需要执行任何操作来促进通信。
以下示例展示了两个 Fragment 如何使用共享的 ViewModel
进行通信:
请注意,这两个 Fragment 都将其宿主 Activity 用作 ViewModelProvider
的范围。由于这两个 Fragment 使用同一范围,因此它们会收到 ViewModel
的同一实例,这使它们能够来回通信。
注意:ViewModel
会一直在内存中,直到其范围限定到的 ViewModelStoreOwner
永久消失。在一个 Activity 架构中,如果 ViewModel
的范围限定为 Activity,那么它本质上是单例。首次实例化 ViewModel
之后,使用 activity 范围检索 ViewModel
的后续调用始终返回相同的现有 ViewModel
以及现有数据,直到 activity 的生命周期永久结束。
class ListViewModel : ViewModel() {
val filters = MutableLiveData<Set<Filter>>()
private val originalList: LiveData<List<Item>>() = ...
val filteredList: LiveData<List<Item>> = ...
fun addFilter(filter: Filter) { ... }
fun removeFilter(filter: Filter) { ... }
}
class ListFragment : Fragment() {
// Using the activityViewModels() Kotlin property delegate from the
// fragment-ktx artifact to retrieve the ViewModel in the activity scope
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
// Update the list UI
}
}
}
class FilterFragment : Fragment() {
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filters.observe(viewLifecycleOwner, Observer { set ->
// Update the selected filters UI
}
}
fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter)
fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter)
}
1.3 在父 Fragment 与子 Fragment 之间共享数据
使用子 Fragment 时,父 Fragment 及其子 Fragment 可能需要相互共享数据。如需在这些 Fragment 之间共享数据,请将父 Fragment 用作 ViewModel
范围。
class ListFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
// Update the list UI
}
}
}
class ChildFragment: Fragment() {
// Using the viewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel using the parent fragment's scope
private val viewModel: ListViewModel by viewModels({requireParentFragment()})
...
}
1.4 将 ViewModel
的范围限定为导航图 实现共享通信
如果您使用的是 Navigation 库,还可以将 ViewModel
的范围限定为目的地的 NavBackStackEntry
的生命周期。例如,可以将 ViewModel
的范围限定为 ListFragment
的 NavBackStackEntry
:
class ListFragment: Fragment() {
// Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx
// artifact to retrieve the ViewModel using the NavBackStackEntry scope
// R.id.list_fragment == the destination id of the ListFragment destination
private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner, Observer { item ->
// Update the list UI
}
}
}
2 使用 Fragment Result API 获取结果
在某些情况下,您可能要在 Fragment 之间或 Fragment 与其宿主 Activity 之间传递一次性值。例如,您可能有一个 fragment,它读取二维码,并将数据传回之前的 fragment。在 fragment 版本 1.3.0 及更高版本中,每个 FragmentManager
都实现了 FragmentResultOwner
。这意味着,FragmentManager
可以充当 fragment 结果的集中存储区。此更改通过设置 Fragment 结果并监听这些结果而不要求组件直接相互引用,让这些组件能够相互通信。
2.1使用 Fragment Result API 在 Fragment 之间传递结果
如需将数据从 Fragment B 传回 Fragment A,请先在 Fragment A(即接收结果的 Fragment)上设置结果监听器。对 Fragment A 的 FragmentManager
调用 setFragmentResultListener()
,如以下示例所示:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
在 Fragment B(即生成结果的 Fragment)中,您必须使用相同的 requestKey
在同一 FragmentManager
上设置结果。您可以使用 setFragmentResult()
API 来完成此操作:
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
然后,一旦 Fragment A 处于 STARTED
状态,它就会收到结果并执行监听器回调。
对于给定的键,只能有一个监听器和结果。如果您对同一个键多次调用 setFragmentResult()
,并且监听器未处于 STARTED
状态,则系统会将所有待处理的结果替换为更新后的结果。如果您设置的结果没有相应的监听器来接收,则结果会存储在 FragmentManager
中,直到您设置一个具有相同键的监听器。监听器收到结果并触发 onFragmentResult()
回调后,结果会被清除。这种行为有两个主要影响:
- 返回堆栈上的 Fragment 只有在被弹出且处于
STARTED
状态之后才会收到结果。 - 如果在设置结果时监听结果的 Fragment 处于
STARTED
状态,则会立即触发监听器的回调。
fragment-ktx封装的FragmentResult的几个方法,都是用到了parentFragmentManager
public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) {
parentFragmentManager.setFragmentResult(requestKey, result)
}
public fun Fragment.clearFragmentResult(requestKey: String) {
parentFragmentManager.clearFragmentResult(requestKey)
}
public fun Fragment.setFragmentResultListener(requestKey: String,listener: ((requestKey: String, bundle: Bundle) -> Unit)) {
parentFragmentManager.setFragmentResultListener(requestKey, this, listener)
}
public fun Fragment.clearFragmentResultListener(requestKey: String) {
parentFragmentManager.clearFragmentResultListener(requestKey)
}
2.2 在父 Fragment 与子 Fragment 之间传递结果
如需将结果从子 Fragment 传递到父 Fragment,父 Fragment 在调用 setFragmentResultListener()
时应使用 getChildFragmentManager()
而不是 getParentFragmentManager()
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// We set the listener on the child fragmentManager
childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
子 Fragment 在其 FragmentManager
上设置结果。然后,一旦父 Fragment 处于 STARTED
状态,它就会收到结果:
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
2.3 在宿主 Activity 中接收结果Fragment里的结果
如需在宿主 activity 中接收 fragment 结果,请使用 getSupportFragmentManager()
在 fragment 管理器上设置结果监听器。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
}