Giao tiếp với các phân đoạn

Để sử dụng lại các mảnh, hãy tạo mảnh ở dạng thành phần hoàn toàn độc lập, tự xác định bố cục và hành vi. Sau khi xác định các mảnh có thể sử dụng lại này, bạn có thể liên kết các mảnh đó với một hoạt động và kết nối chúng với logic ứng dụng để nhận dạng giao diện người dùng kết hợp chung.

Để phản ứng đúng cách với các sự kiện của người dùng hoặc chia sẻ thông tin trạng thái, thường thì bạn cần có kênh liên lạc giữa một hoạt động và các mảnh hoạt động đó, hoặc giữa 2 hoặc nhiều mảnh. Để giữ các mảnh độc lập, không nên để các mảnh giao tiếp trực tiếp với mảnh khác hoặc với hoạt động của máy chủ lưu trữ.

Thư viện Fragment cung cấp 2 lựa chọn giao tiếp: ViewModel dùng chung và Fragment Result API (API Kết quả mảnh). Lựa chọn khuyên dùng phụ thuộc vào trường hợp sử dụng. Để chia sẻ dữ liệu ổn định với các API tuỳ chỉnh, hãy dùng ViewModel. Nếu bạn muốn nhận được kết quả dùng một lần với dữ liệu có thể đặt trong Bundle, hãy dùng Fragment Result API (API Kết quả mảnh).

Các phần sau cho biết cách dùng ViewModel và Fragment Result API (API Kết quả mảnh) để giao tiếp giữa các mảnh và hoạt động.

Chia sẻ dữ liệu bằng ViewModel

ViewModel là lựa chọn lý tưởng khi cần chia sẻ dữ liệu giữa nhiều mảnh hoặc giữa các mảnh và hoạt động của máy chủ lưu trữ. Các đối tượng ViewModel lưu trữ và quản lý dữ liệu giao diện người dùng. Để biết thêm thông tin về ViewModel, hãy xem bài viết Tổng quan về ViewModel.

Chia sẻ dữ liệu với hoạt động của máy chủ lưu trữ

Trong một số trường hợp, bạn cần chia sẻ dữ liệu giữa các mảnh và hoạt động của máy chủ lưu trữ. Ví dụ: bạn có thể cần bật/tắt một thành phần giao diện người dùng chung dựa trên hoạt động tương tác trong một mảnh.

Hãy cân nhắc ItemViewModel sau:

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

Trong ví dụ này, dữ liệu đã lưu trữ được gói trong một lớp MutableLiveData. LiveData là một lớp phần tử giữ dữ liệu có thể ghi nhận được và nhận biết vòng đời. MutableLiveData cho phép thay đổi giá trị của lớp đó. Để biết thêm thông tin về LiveData, hãy xem bài viết Tổng quan về LiveData.

Cả mảnh và hoạt động của máy chủ lưu trữ đều có thể truy xuất một thực thể dùng chung của ViewModel có phạm vi hoạt động bằng cách chuyển hoạt động vào hàm khởi tạo ViewModelProvider. ViewModelProvider xử lý việc tạo thực thể cho ViewModel hoặc truy xuất thành phần đó nếu đã tồn tại. Cả hai thành phần đều có thể ghi nhận và sửa đổi dữ liệu này.

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

Chia sẻ dữ liệu giữa các mảnh

2 hoặc nhiều mảnh trong cùng một hoạt động thường cần giao tiếp với nhau. Ví dụ: hãy tưởng tượng một mảnh hiển thị một danh sách, còn một mảnh khác cho phép người dùng áp dụng nhiều bộ lọc khác nhau cho danh sách đó. Việc triển khai trường hợp này không hề đơn giản nếu các mảnh không giao tiếp trực tiếp với nhau, nhưng sau đó các mảnh này không còn độc lập nữa. Ngoài ra, cả hai mảnh đều phải xử lý trường hợp mảnh khác chưa được tạo hoặc hiển thị.

Những mảnh này có thể chia sẻ một ViewModel bằng cách sử dụng phạm vi hoạt động của chúng để xử lý quá trình giao tiếp. Khi chia sẻ ViewModel theo cách này, các mảnh không nhất thiết phải biết về nhau và hoạt động không cần phải làm gì để hỗ trợ quá trình giao tiếp.

Ví dụ sau đây cho thấy cách 2 mảnh có thể dùng chung một ViewModel để giao tiếp:

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

Cả hai mảnh đều dùng hoạt động của máy chủ lưu trữ làm phạm vi cho ViewModelProvider. Vì các mảnh sử dụng cùng một phạm vi nên chúng nhận cùng một thực thể của ViewModel, cho phép giao tiếp qua lại.

Chia sẻ dữ liệu giữa mảnh mẹ và mảnh con

Khi làm việc với các mảnh con, mảnh mẹ và mảnh con có thể cần chia sẻ dữ liệu với nhau. Để chia sẻ dữ liệu giữa các mảnh này, hãy dùng mảnh mẹ làm phạm vi ViewModel, như minh hoạ trong ví dụ sau:

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

Xác định phạm vi của ViewModel trong Biểu đồ điều hướng

Nếu đang dùng Thư viện điều hướng, bạn cũng có thể xác định phạm vi ViewModel trong vòng đời của NavBackStackEntry đích đến. Ví dụ: ViewModel có thể nằm trong phạm vi NavBackStackEntry của ListFragment:

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

Để biết thêm thông tin về việc phạm vi ViewModel với một NavBackStackEntry, hãy xem nội dung Tương tác có lập trình với thành phần Điều hướng.

Nhận kết quả bằng cách sử dụng Fragment Result API (API Kết quả mảnh)

Trong một số trường hợp, bạn có thể cần chuyển giá trị dùng một lần giữa 2 mảnh hoặc giữa một mảnh và hoạt động của máy chủ lưu trữ. Ví dụ: bạn có thể dùng một mảnh để đọc mã QR, giúp chuyển dữ liệu trở lại mảnh trước.

Trong Fragment phiên bản 1.3.0 trở lên, mỗi FragmentManager sẽ triển khai FragmentResultOwner. Việc này đồng nghĩa rằng FragmentManager có thể đóng vai trò là kho lưu trữ trung tâm cho các kết quả mảnh. Thay đổi này cho phép các thành phần giao tiếp với nhau bằng cách đặt kết quả mảnh và theo dõi kết quả đó mà không yêu cầu các thành phần tham chiếu trực tiếp với nhau.

Chuyển kết quả giữa các mảnh

Để chuyển dữ liệu từ phân mảnh B trở về phân mảnh A, trước tiên hãy đặt trình nghe xử lý kết quả trên phân mảnh A là phân mảnh nhận kết quả. Gọi hàm setFragmentResultListener() trên FragmentManager của phân mảnh A như trong ví dụ sau:

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.
        }
    });
}
phân mảnh B gửi dữ liệu tới phân mảnh A bằng FragmentManager
Hình 1. Mảnh B gửi dữ liệu đến mảnh A bằng FragmentManager.

Trong mảnh B (mảnh tạo ra kết quả), hãy đặt kết quả trên cùng FragmentManager bằng cách sử dụng cùng một requestKey. Bạn có thể thực hiện việc này thông qua API setFragmentResult():

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

Sau đó, phân mảnh A sẽ nhận được kết quả và thực thi lệnh gọi lại trình xử lý nếu phân mảnh là STARTED.

Bạn chỉ có thể sở hữu một trình nghe và kết quả cho một khoá nhất định. Nếu gọi setFragmentResult() nhiều lần cho cùng một khoá và nếu trình xử lý không phải là STARTED, hệ thống sẽ thay thế mọi kết quả đang chờ xử lý bằng kết quả đã cập nhật.

Nếu đặt một kết quả mà không có trình nghe tương ứng để nhận kết quả, thì kết quả sẽ được lưu trữ trong FragmentManager cho đến khi bạn đặt một trình nghe bằng cùng một khoá. Sau khi trình nghe nhận được một kết quả và kích hoạt lệnh gọi lại onFragmentResult(), kết quả đó sẽ bị xoá. Hành vi này có hai ảnh hưởng quan trọng:

  • Các mảnh trong ngăn xếp lui không nhận được kết quả cho đến khi được đẩy ra và STARTED.
  • Nếu một mảnh đang theo dõi kết quả STARTED khi bạn đặt kết quả, lệnh gọi lại của trình nghe sẽ kích hoạt ngay lập tức.

Kiểm thử kết quả mảnh

Dùng FragmentScenario để kiểm thử các lệnh gọi đến setFragmentResult()setFragmentResultListener(). Tạo một tình huống cho mảnh đang được kiểm thử bằng cách sử dụng launchFragmentInContainer hoặc launchFragment, sau đó gọi thủ công phương thức chưa được kiểm thử.

Để kiểm thử setFragmentResultListener(), hãy tạo một tình huống bằng mảnh thực hiện lệnh gọi đến setFragmentResultListener(). Tiếp theo, hãy gọi trực tiếp cho setFragmentResult() và xác minh kết quả:

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

Để kiểm tra setFragmentResult(), hãy tạo một tình huống có đoạn thực hiện lệnh gọi đến setFragmentResult(). Tiếp theo, hãy gọi trực tiếp setFragmentResultListener() và xác minh kết quả:

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

Chuyển kết quả giữa mảnh mẹ và mảnh con

Để chuyển kết quả từ một mảnh con sang mảnh mẹ, hãy dùng getChildFragmentManager() từ mảnh mẹ thay vì getParentFragmentManager() khi gọi setFragmentResultListener().

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.
            }
        });
}
Một phân mảnh con có thể sử dụng FragmentManager để gửi kết quả đến phân mảnh gốc
Hình 2 Một phân mảnh con có thể sử dụng FragmentManager để gửi kết quả đến phân mảnh gốc.

Phân mảnh con đặt kết quả trên FragmentManager. Sau đó, phân mảnh gốc sẽ nhận được kết quả khi nó là phân mảnh 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);
    }
});

Nhận kết quả trong hoạt động trên máy chủ lưu trữ

Để nhận được kết quả phân mảnh trong hoạt động lưu trữ, hãy đặt trình xử lý kết quả trên trình quản lý phân mảnh bằng cách sử dụng 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.
            }
        });
    }
}