以片段通訊

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

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

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 的範圍可以為 ListFragment 限定在 NavBackStackEntry 內:

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)
    // We 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);
    // We 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
            }
        });
    }
}