Berkomunikasi dengan fragmen

Untuk menggunakan ulang fragmen, build setiap komponen sebagai komponen mandiri yang menentukan tata letak dan perilakunya sendiri. Setelah menentukan fragmen yang dapat digunakan kembali ini, Anda dapat mengaitkannya dengan aktivitas dan menghubungkannya dengan logika aplikasi untuk mewujudkan keseluruhan UI gabungan.

Untuk bereaksi secara tepat terhadap peristiwa pengguna, atau berbagi informasi status, Anda sering kali perlu memiliki saluran komunikasi antara aktivitas dan fragmennya, atau antara dua fragmen atau lebih. Agar fragmen tetap mandiri, Anda tidak boleh membiarkan fragmen berkomunikasi langsung dengan fragmen lainnya atau dengan aktivitas host-nya.

Library Fragment menyediakan dua opsi komunikasi: ViewModel bersama dan Fragment Result API. Opsi yang direkomendasikan bergantung pada kasus penggunaan. Untuk membagikan data tetap dengan API kustom apa pun, Anda harus menggunakan ViewModel. Untuk hasil sekali pakai dengan data yang dapat ditempatkan di Bundle, Anda harus menggunakan Fragment Result API.

Bagian berikut menunjukkan cara menggunakan ViewModel dan Fragment Result API untuk berkomunikasi antara fragmen dan aktivitas Anda.

Membagikan data menggunakan ViewModel

ViewModel adalah pilihan ideal saat Anda perlu berbagi data antara beberapa fragmen atau antara fragmen dan aktivitas host-nya. Objek ViewModel menyimpan dan mengelola data UI. Untuk informasi selengkapnya tentang ViewModel, baca Ringkasan ViewModel.

Membagikan data dengan aktivitas host

Pada kasus tertentu, Anda mungkin perlu membagikan data antara fragmen dan aktivitas host-nya. Misalnya, Anda mungkin ingin mengalihkan komponen UI global berdasarkan interaksi dalam fragmen.

Pertimbangkan ItemViewModel berikut:

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

Dalam contoh ini, data yang disimpan akan digabungkan ke dalam class MutableLiveData. LiveData adalah class penyimpanan data observable berbasis siklus proses. MutableLiveData memungkinkan nilainya diubah. Untuk informasi selengkapnya tentang LiveData, baca Ringkasan LiveData.

Fragmen dan aktivitas host-nya dapat mengambil instance bersama ViewModel dari cakupan aktivitas dengan meneruskan aktivitas ke konstruktor ViewModelProvider. ViewModelProvider menangani pembuatan instance ViewModel atau mengambilnya jika sudah ada. Kedua komponen dapat mengamati dan memodifikasi data ini:

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

Membagikan data antar-fragmen

Dua fragmen atau lebih dalam aktivitas yang sama sering kali perlu saling berkomunikasi. Misalnya, bayangkan satu fragmen yang menampilkan daftar dan lainnya yang memungkinkan pengguna menerapkan berbagai filter ke daftar. Kasus ini mungkin tidak mudah diterapkan tanpa kedua fragmen berkomunikasi secara langsung, yang berarti fragmen tersebut tidak lagi berfungsi mandiri. Selain itu, kedua fragmen harus menangani skenario saat fragmen lain belum dibuat atau terlihat.

Fragmen ini dapat membagikan ViewModel menggunakan cakupan aktivitasnya untuk menangani komunikasi ini. Dengan membagikan ViewModel menggunakan cara ini, fragmen tidak perlu saling mengetahui, dan aktivitasnya tidak perlu melakukan apa pun untuk memfasilitasi komunikasi.

Contoh berikut menunjukkan cara dua fragmen dapat menggunakan ViewModel bersama untuk berkomunikasi:

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

Perhatikan bahwa kedua fragmen menggunakan aktivitas host-nya sebagai cakupan untuk ViewModelProvider. Karena fragmen menggunakan cakupan yang sama, fragmen tersebut menerima instance ViewModel yang sama, yang memungkinkannya berkomunikasi dua arah.

Membagikan data antara fragmen induk dan turunan

Saat menangani fragmen turunan, fragmen induk dan fragmen turunannya mungkin perlu membagikan data kepada satu sama lain. Untuk membagikan data antara fragmen ini, gunakan fragmen induk sebagai cakupan 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);
        ...
    }
}

Memberi cakupan ViewModel ke Grafik Navigasi

Jika menggunakan library Navigasi, Anda juga dapat memberi cakupan ViewModel ke siklus proses NavBackStackEntry tujuan. Misalnya, ViewModel dapat dicakup ke NavBackStackEntry untuk 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
        }
    }
}

Untuk informasi selengkapnya tentang pencakupan ViewModel ke NavBackStackEntry, baca Berinteraksi secara terprogram dengan komponen Navigasi.

Mendapatkan hasil menggunakan Fragment Result API

Pada kasus tertentu, sebaiknya Anda meneruskan nilai sekali pakai antara dua fragmen atau antara sebuah fragmen dan aktivitas host-nya. Misalnya, Anda mungkin memiliki fragmen yang membaca kode QR, sehingga meneruskan data kembali ke fragmen sebelumnya. Mulai dari Fragment 1.3.0-alpha04, setiap FragmentManager menerapkan FragmentResultOwner. Ini berarti bahwa FragmentManager dapat bertindak sebagai penyimpanan pusat hasil fragmen. Perubahan ini memungkinkan komponen untuk berkomunikasi satu sama lain dengan menetapkan hasil fragmen dan memproses hasil tersebut tanpa mengharuskan komponen tersebut mereferensikan satu sama lain secara langsung.

Meneruskan hasil antar-fragmen

Untuk meneruskan data kembali dari fragmen B ke fragmen A, tetapkan pemroses hasil pada fragmen A, yakni fragmen yang menerima hasil. Panggil setFragmentResultListener() di FragmentManager fragmen A, seperti yang ditunjukkan dalam contoh berikut:

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
        }
    });
}
fragmen b mengirim data ke fragmen a menggunakan FragmentManager
Gambar 1. Fragmen B mengirim data ke fragmen A menggunakan FragmentManager.

Dalam fragmen B, fragmen yang memberikan hasil, Anda harus menetapkan hasilnya pada FragmentManager yang sama menggunakan requestKey yang sama. Anda dapat melakukannya menggunakan 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);
    }
});

Fragmen A kemudian menerima hasilnya dan mengeksekusi callback pemroses setelah fragmen STARTED.

Anda hanya dapat memiliki satu pemroses dan satu hasil untuk setiap kunci. Jika Anda memanggil setFragmentResult() lebih dari sekali untuk kunci yang sama, dan jika pemroses bukan STARTED, sistem akan mengganti hasil yang tertunda dengan hasil yang diperbarui. Jika Anda menetapkan hasil tanpa pemroses yang sesuai yang akan menerimanya, hasilnya akan disimpan dalam FragmentManager hingga pemroses dengan kunci yang sama ditetapkan. Setelah pemroses menerima hasil dan mengaktifkan callback onFragmentResult(), hasilnya akan dihapus. Perilaku ini memiliki dua implikasi utama:

  • Fragmen pada data sebelumnya tidak menerima hasil hingga fragmen tersebut muncul dan STARTED.
  • Jika fragmen yang memproses hasil adalah STARTED saat hasilnya ditetapkan, callback pemroses akan langsung diaktifkan.

Menguji hasil fragmen

Gunakan FragmentScenario untuk menguji panggilan ke setFragmentResult() dan setFragmentResultListener(). Buat skenario untuk fragmen yang sedang diuji menggunakan launchFragmentInContainer atau launchFragment, lalu panggil metode yang tidak sedang diuji secara manual.

Untuk menguji setFragmentResultListener(), buat skenario dengan fragmen yang membuat panggilan ke setFragmentResultListener(). Selanjutnya, panggil setFragmentResult() secara langsung, dan verifikasi hasilnya:

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

Untuk menguji setFragmentResult(), buat skenario dengan fragmen yang melakukan panggilan ke setFragmentResult(). Selanjutnya, panggil setFragmentResultListener() secara langsung, dan verifikasi hasilnya:

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

Meneruskan hasil antara fragmen induk dan turunan

Untuk meneruskan hasil dari fragmen turunan ke induk, fragmen induk harus menggunakan getChildFragmentManager(), dan bukan getParentFragmentManager() saat memanggil 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
            }
        });
}
fragmen turunan dapat menggunakan FragmentManager untuk mengirimkan hasil
            ke induknya
Gambar 2 Fragmen turunan dapat menggunakan FragmentManager untuk mengirimkan hasil ke induknya.

Fragmen turunan menetapkan hasil pada FragmentManager-nya. Fragmen induk kemudian menerima hasilnya setelah fragmen 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);
    }
});

Menerima hasil dalam aktivitas host

Untuk menerima hasil fragmen dalam aktivitas host, tetapkan pemroses hasil pada pengelola fragmen menggunakan 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
            }
        });
    }
}