與片段通訊

如要重複使用片段,請將片段建構為完全獨立的元件,讓片段自行定義版面配置和行為。定義可重複使用的片段後,您可以將這些片段與活動建立關聯,並與應用程式邏輯建立連結,形成複合的整體 UI。

如要正確回應使用者事件或分享狀態資訊,您通常需要在活動和活動片段之間,或是兩個以上的片段之間具備通訊管道。為了讓片段保持獨立,「請勿」讓片段直接與其他片段或其主機活動通訊。

Fragment 程式庫提供兩種通訊選項:共用的 ViewModel,以及 Fragment Result API。建議做法視用途而異。若要與自訂 API 共用持續性資料,請使用 ViewModel。若是一次性結果,且其中的資料可放置在 Bundle,請使用 Fragment Result API。

以下章節介紹如何使用 ViewModel 和 Fragment Result API,在片段及活動之間通訊。

使用 ViewModel 共用資料

如果需要在多個片段之間,或是片段和其主機活動之間共用資料,ViewModel 是最佳選擇。ViewModel 物件會儲存及管理 UI 資料。如要進一步瞭解 ViewModel,請參閱「ViewModel 總覽」。

與主機活動共用資料

在某些情況下,您或許需要在片段和其主機活動之間共用資料。舉例來說,您可能想根據片段中的互動方式,切換某個全域 UI 元件。

請考慮以下 ItemViewModel

Kotlin

class ItemViewModel : ViewModel() {
    private val mutableSelectedItem = MutableLiveData<Item>()
    val selectedItem: LiveData<Item> get() = mutableSelectedItem

    fun selectItem(item: Item) {
        mutableSelectedItem.value = item
    }
}

Java

public class ItemViewModel extends ViewModel {
    private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>();
    public void selectItem(Item item) {
        selectedItem.setValue(item);
    }
    public LiveData<Item> getSelectedItem() {
        return selectedItem;
    }
}

在此範例中,儲存的資料會包裝在 MutableLiveData 類別中。LiveData 這種類別是會留意生命週期且可觀測的資料容器,MutableLiveData 允許變更其值。如要進一步瞭解 LiveData,請參閱「LiveData 總覽」。

透過將活動傳遞至 ViewModelProvider 建構函式,片段及其主機活動可在活動範圍中擷取 ViewModel 的共用例項。ViewModelProvider 會處理 ViewModel 的例項化,如果已有例項則會直接擷取。兩個元件皆可觀察及修改這項資料。

Kotlin

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)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    private ItemViewModel viewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
        viewModel.getSelectedItem().observe(this, item -> {
            // Perform an action with the latest item data.
        });
    }
}

public class ListFragment extends Fragment {
    private ItemViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);
        ...
        items.setOnClickListener(item -> {
            // Set a new item.
            viewModel.select(item);
        });
    }
}

在片段間共用資料

同一活動中兩個以上的片段通常需要彼此通訊。舉例來說,假設某個片段顯示清單,另一個片段則允許使用者為該清單套用各種篩選器。如果片段間並未直接通訊,要實作這種情況並不容易,但屆時片段已不再是獨立狀態。此外,這兩個片段都必須處理另一個片段尚未建立或顯示的情況。

這些片段可以利用其活動範圍共用 ViewModel 來處理這項通訊。透過這種方式共用 ViewModel,兩個片段不需要彼此瞭解,活動也不需要採取任何措施來促進通訊。

以下範例說明兩個片段如何使用共用的 ViewModel 通訊:

Kotlin

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)
}

Java

public class ListViewModel extends ViewModel {
    private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>();

    private final LiveData<List<Item>> originalList = ...;
    private final LiveData<List<Item>> filteredList = ...;

    public LiveData<List<Item>> getFilteredList() {
        return filteredList;
    }

    public LiveData<Set<Filter>> getFilters() {
        return filters;
    }

    public void addFilter(Filter filter) { ... }

    public void removeFilter(Filter filter) { ... }
}

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        });
    }
}

public class FilterFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
            // Update the selected filters UI.
        });
    }

    public void onFilterSelected(Filter filter) {
        viewModel.addFilter(filter);
    }

    public void onFilterDeselected(Filter filter) {
        viewModel.removeFilter(filter);
    }
}

這兩個片段都使用自己的主機活動做為 ViewModelProvider 的範圍。由於兩個片段的範圍相同,就會接收相同的 ViewModel 例項,因此能相互通訊。

在父項和子項片段之間共用資料

使用子項片段時,父項片段及子項片段可能需要相互共用資料。如要在這些片段之間共用資料,請使用父項片段做為 ViewModel 範圍,如以下範例所示:

Kotlin

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()})
    ...
}

Java

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(this).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        }
    }
}

public class ChildFragment extends Fragment {
    private ListViewModel viewModel;
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
        ...
    }
}

將 ViewModel 的範圍限定為導覽圖

如果使用 Navigation 程式庫,也可以將 ViewModel 的範圍限定為目的地 NavBackStackEntry 的生命週期。舉例來說,ViewModel 的範圍可以限定為 ListFragmentNavBackStackEntry

Kotlin

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.
        }
    }
}

Java

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    NavController navController = NavHostFragment.findNavController(this);
        NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment)

        viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI.
        }
    }
}

若要進一步瞭解如何將 ViewModel 範圍限定為 NavBackStackEntry,請參閱「透過程式輔助方法與 Navigation 元件互動」。

使用 Fragment Result API 取得結果

在某些情況下,您可能會想在兩個片段之間,或是在片段和其主機活動之間傳遞一次性的值。舉例來說,您可能有一個片段可讀取 QR code,並將資料傳回至前一個片段。

若是 Fragment 1.3.0 以上版本,則每個 FragmentManager 都會實作 FragmentResultOwner。這表示 FragmentManager 可做為片段結果的中央存放區。這項變更可設定片段結果並監聽這些結果,讓元件不必直接彼此參照,就能相互通訊。

在片段之間傳遞結果

若要將資料從片段 B 傳回片段 A,請先在片段 A(也就是收到結果的片段)設定一個結果監聽器。呼叫片段 A FragmentManager 上的 setFragmentResultListener(),如以下範例所示:

Kotlin

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.
    }
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
            // We use a String here, but any type that can be put in a Bundle is supported.
            String result = bundle.getString("bundleKey");
            // Do something with the result.
        }
    });
}
片段 b 使用 FragmentManager 將資料傳送至片段 a
圖 1。 片段 B 使用 FragmentManager 將資料傳送至片段 A。

在產生結果的片段 B 中,必須使用同一個 requestKey 在相同的 FragmentManager 上設定結果,方法是使用 setFragmentResult() API:

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Java

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

片段 A 會取得結果,並在片段為 STARTED 時執行事件監聽器的回呼。

對於特定鍵,只能有一個事件監聽器和結果。如果對同一個鍵多次呼叫 setFragmentResult(),而且事件監聽器並非處於 STARTED 狀態,系統會將所有待處理結果取代為更新後的結果。

如果您設定的結果沒有相對應的事件監聽器可接收,它會儲存在 FragmentManager 中,直到您設定具有相同鍵的事件監聽器。一旦監聽器收到結果並啟動 onFragmentResult() 回呼,系統會清除結果。這個行為會產生兩個主要的影響:

  • 返回堆疊上的片段在由系統彈出且處於 STARTED 狀態時,才會收到結果。
  • 如果設定結果時,監聽結果的片段處於 STARTED 狀態,會立即觸發事件監聽器的回呼。

測試片段結果

請使用 FragmentScenario 測試對 setFragmentResult()setFragmentResultListener() 的呼叫。請使用 launchFragmentInContainerlaunchFragment 為待測試片段建立情境,接著手動呼叫未測試的方法。

若要測試 setFragmentResultListener(),請使用呼叫 setFragmentResultListener() 的片段來建立情境。接下來,請直接呼叫 setFragmentResult() 並確認結果:

@Test
fun testFragmentResultListener() {
    val scenario = launchFragmentInContainer<ResultListenerFragment>()
    scenario.onFragment { fragment ->
        val expectedResult = "result"
        fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
        assertThat(fragment.result).isEqualTo(expectedResult)
    }
}

class ResultListenerFragment : Fragment() {
    var result : String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the Kotlin extension in the fragment-ktx artifact.
        setFragmentResultListener("requestKey") { requestKey, bundle ->
            result = bundle.getString("bundleKey")
        }
    }
}

若要測試 setFragmentResult(),請使用呼叫 setFragmentResult() 的片段來建立情境。接下來,請直接呼叫 setFragmentResultListener() 並確認結果:

@Test
fun testFragmentResult() {
    val scenario = launchFragmentInContainer<ResultFragment>()
    lateinit var actualResult: String?
    scenario.onFragment { fragment ->
        fragment.parentFragmentManager
                .setFragmentResultListener("requestKey") { requestKey, bundle ->
            actualResult = bundle.getString("bundleKey")
        }
    }
    onView(withId(R.id.result_button)).perform(click())
    assertThat(actualResult).isEqualTo("result")
}

class ResultFragment : Fragment(R.layout.fragment_result) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById(R.id.result_button).setOnClickListener {
            val result = "result"
            // Use the Kotlin extension in the fragment-ktx artifact.
            setFragmentResult("requestKey", bundleOf("bundleKey" to result))
        }
    }
}

在父項和子項片段之間傳遞結果

如要將結果從子項片段傳遞至父項片段,請在呼叫 setFragmentResultListener() 時,使用父項片段中的 getChildFragmentManager(),而非 getParentFragmentManager()

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Set the listener on the child fragmentManager.
    childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("bundleKey")
        // Do something with the result.
    }
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Set the listener on the child fragmentManager.
    getChildFragmentManager()
        .setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
}
子項片段可以使用 FragmentManager 將結果傳送至父項
圖 2 子項片段可以使用 FragmentManager 將結果傳送至其父項。

子項片段在其 FragmentManager 上設定結果。接著當片段為 STARTED 時,父項就會收到結果:

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact.
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

Java

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        // The child fragment needs to still set the result on its parent fragment manager.
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

接收主機活動的結果

如要在主機活動中接收片段結果,請使用 getSupportFragmentManager() 在片段管理員中設定結果監聽器。

Kotlin

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.
        }
    }
}

Java

class MainActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                // We use a String here, but any type that can be put in a Bundle is supported.
                String result = bundle.getString("bundleKey");
                // Do something with the result.
            }
        });
    }
}