與片段通訊

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

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

Fragment 程式庫提供兩種通訊選項:共用 ViewModel 和 Fragment Result API。建議做法視其用途而異。若要與自訂 API 共用永久性資料,請使用 ViewModel。針對包含的資料可放置在 Bundle 中的一次性結果,請使用 Fragment Result API。

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

使用 ViewModel 共用資料

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

與代管活動共用資料

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

請考慮以下 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 的範圍限定為導覽圖

如果使用的是導覽程式庫,也可以將 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,請參閱「在程式輔助下透過導覽元件進行互動」。

使用 Fragment Result API 取得結果

在某些情況下,您可能想在兩個片段之間或是在片段與代管活動之間傳送一個單次性的值。舉例來說,您可能有一個片段,用來讀取 QR 代碼並將資料傳回至前一個片段。

若是 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.
            }
        });
    }
}