Giao tiếp bằng các phân mảnh

Để tái sử dụng các phân mảnh, hãy tạo từng phân mảnh dưới dạng một thành phần hoàn toàn độc lập xác định bố cục và hành vi của chính cấu hình đó. Sau khi đã xác định được các phân mảnh tái sử dụng này, có thể liên kết các phân mảnh đó với 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 tổng hợp.

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

Thư viện Fragment cung cấp hai tùy chọn giao tiếp: ViewModel dùng chung và API Kết quả phân mảnh. Tùy chọn được đề xuất phụ thuộc vào trường hợp sử dụng. Để chia sẻ dữ liệu ổn định với bất kỳ API tùy chỉnh nào, bạn nên sử dụng ViewModel. Để có kết quả một lần có dữ liệu có thể được đặt trong Bundle, bạn nên sử dụng API kết quả phân mảnh.

Các phần sau cho biết cách sử dụng ViewModel và API kết quả phân mảnh để liên lạc giữa các phân 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 phân mảnh hoặc giữa các phân mảnh và hoạt động của máy chủ. 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, vui lòng xem nội dungtổ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 phân mảnh và hoạt động của máy chủ lưu trữ. Ví dụ như khi bạn muốn chuyển đổi một thành phần giao diện người dùng toàn cầu dựa trên tương tác trong một phân mảnh.

Hãy cân nhắc những tính năng sau 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;
    }
}

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

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

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 phân mảnh

Hai hoặc nhiều phân 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 phân mảnh hiển thị một danh sách và một phân mảnhkhác cho phép người dùng áp dụng nhiều bộ lọc khác nhau cho danh sách. Trường hợp này có thể không hề đơn giản để triển khai nếu không có phân mảnh ghép giao tiếp trực tiếp, nghĩa là các trường hợp đó không còn độc lập nữa. Ngoài ra, cả hai phân mảnh đều phải xử lý trường hợp phân mảnh khác chưa được tạo hoặc hiển thị.

Các phân mảnh này có thể chia sẻ ViewModel bằng cách sử dụng phạm vi hoạt động để xử lý hoạt động giao tiếp. Bằng cách chia sẻ ViewModel theo cách đó, các phân mảnh không cần phải nhận biết về nhau cũng như hoạt động không cần làm gì để hỗ trợ cho việc giao tiếp.

Ví dụ sau đây cho thấy cách hai phân mảnh có thể sử dụng ViewModel dùng chung để 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);
    }
}

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

Chia sẻ dữ liệu giữa phân mảnh gốc và phân mảnh con

Khi làm việc với các phân mảnh con, phân mảnh gốc và phân mảnh con có thể cần chia sẻ dữ liệu với nhau. Để chia sẻ dữ liệu giữa các phân mảnh, hãy lấy phân mảnh gốc làm phạm vi 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);
        ...
    }
}

Đưa ViewModel vào Biểu đồ điều hướng

Nếu đang sử dụng Thư viện điều hướng, bạn cũng có thể phân ViewModel cho vòng đời của đích đến NavBackStackEntry. 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 API kết quả phân mảnh

Trong một số trường hợp, bạn có thể muốn chuyển giá trị một lần giữa hai phân mảnh hoặc giữa một phân mảnh với hoạt động của máy chủ. Ví dụ: bạn có một phân mảnh với mã đọc QR, chuyển dữ liệu trở lại phân mảnh trước. Bắt đầu bằng phân mảnh 1.3.0-alpha04, mỗi FragmentManager triển khai FragmentResultOwner. Điều này có nghĩa là FragmentManager hoạt động như một kho lưu trữ trung tâm cho các kết quả phân 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ả phân mảnh và lắng nghe 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 phân 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. phân mảnh B gửi dữ liệu đến phân mảnh A bằng FragmentManager.

Trong phân mảnh B (là phân mảnh tạo ra kết quả), bạn phải đặt kết quả trên cùng FragmentManager bằng cách sử dụng cùng một requestKey. Có thể thực hiện việc này bằng cách sử dụng APIsetFragmentResult():

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 khóa nhất định. Nếu gọi setFragmentResult() nhiều lần cho cùng một khóa 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ả nhưng không có trình nghe tương ứng để nhận kết quả, kết quả sẽ được lưu trữ trong FragmentManager cho đến khi bạn đặt một trình nghe bằng khóa tương tự. Khi người nghe nhận được kết quả và kích hoạt lệnh gọi lại onFragmentResult(), kết quả sẽ bị xóa. Hành vi này có hai ảnh hưởng quan trọng:

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

Kiểm tra kết quả phân mảnh

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

Để kiểm tra setFragmentResultListener(), hãy tạo một tình huống với phân 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 phân mảnh gốc và phân mảnh con

Để chuyển kết quả từ một phân mảnh con sang phân mảnh gốc, phân mảnh gốc phải sử dụng getChildFragmentManager() thay vì getParentFragmentManager() khi gọi setFragmentResultListener().

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