Comunicare con i frammenti

Per riutilizzare i frammenti, creali come componenti completamente autonomi che definiscono il proprio layout e comportamento. Una volta definiti questi frammenti riutilizzabili, puoi associarli a un'attività e connetterli alla logica dell'applicazione per realizzare l'interfaccia utente composita complessiva.

Per reagire in modo adeguato agli eventi utente e condividere informazioni sullo stato, spesso devi disporre di canali di comunicazione tra un'attività e i suoi frammenti o tra due o più frammenti. Per mantenere i frammenti autonomi, non fare in modo che comunicano direttamente con altri frammenti o con la relativa attività host.

La libreria Fragment offre due opzioni per la comunicazione: un elemento ViewModel condiviso e l'API Fragment Result. L'opzione consigliata dipende dal caso d'uso. Per condividere i dati permanenti con le API personalizzate, utilizza un ViewModel. Per un risultato una tantum con dati che possono essere inseriti in una Bundle, utilizza l'API Fragment Result.

Le seguenti sezioni mostrano come utilizzare ViewModel e l'API Fragment Result per comunicare tra frammenti e attività.

Condividere dati utilizzando un ViewModel

ViewModel è la scelta ideale quando devi condividere dati tra più frammenti o tra frammenti e la relativa attività host. ViewModel oggetti archiviano e gestiscono i dati dell'interfaccia utente. Per maggiori informazioni su ViewModel, consulta la panoramica di ViewModel.

Condividi dati con l'attività dell'organizzatore

In alcuni casi, potrebbe essere necessario condividere i dati tra i frammenti e la relativa attività host. Ad esempio, potresti voler attivare/disattivare un componente dell'interfaccia utente globale in base a un'interazione all'interno di un frammento.

Considera quanto segue 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;
    }
}

In questo esempio, i dati archiviati sono aggregati in una classe MutableLiveData. LiveData è una classe di titolari di dati osservabile con consapevolezza del ciclo di vita. MutableLiveData consente di modificare il suo valore. Per maggiori informazioni su LiveData, consulta la panoramica di LiveData.

Sia il frammento sia l'attività host possono recuperare un'istanza condivisa di ViewModel con ambito attività passando l'attività al costruttore ViewModelProvider. L'elemento ViewModelProvider gestisce la creazione di un'istanza di ViewModel o il recupero, se esiste già. Entrambi i componenti possono osservare e modificare questi dati.

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

Condividi dati tra frammenti

Due o più frammenti nella stessa attività spesso devono comunicare tra loro. Ad esempio, immagina un frammento che mostra un elenco e un altro che consente all'utente di applicare vari filtri all'elenco. L'implementazione di questo caso non è banale senza i frammenti che comunicano direttamente, ma non sono più autonomi. Inoltre, entrambi i frammenti devono gestire lo scenario in cui l'altro frammento non è ancora stato creato o non è visibile.

Questi frammenti possono condividere un ViewModel utilizzando l'ambito dell'attività per gestire questa comunicazione. Se condividi il ViewModel in questo modo, i frammenti non devono essere a conoscenza l'uno dell'altro e l'attività non deve fare nulla per facilitare la comunicazione.

L'esempio seguente mostra come due frammenti possono utilizzare un elemento ViewModel condiviso per comunicare:

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

Entrambi i frammenti utilizzano la propria attività host come ambito per ViewModelProvider. Poiché i frammenti utilizzano lo stesso ambito, ricevono la stessa istanza di ViewModel, che consente loro di comunicare tra loro.

Condividere dati tra un frammento principale e uno secondario

Quando lavori con i frammenti figlio, il frammento padre e i relativi frammenti figlio potrebbero dover condividere dati tra loro. Per condividere dati tra questi frammenti, utilizza il frammento padre come ambito ViewModel, come mostrato nell'esempio seguente:

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

Imposta l'ambito di un ViewModel sul grafico di navigazione

Se utilizzi la libreria di navigazione, puoi anche limitare un ViewModel al ciclo di vita di NavBackStackEntry di una destinazione. Ad esempio, ViewModel può essere limitato all'ambito NavBackStackEntry per 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.
        }
    }
}

Per ulteriori informazioni su come definire l'ambito di un elemento ViewModel in un NavBackStackEntry, consulta Interagire in modo programmatico con il componente Navigazione.

Ottenere risultati utilizzando l'API Fragment Result

In alcuni casi, potresti voler passare un valore una tantum tra due frammenti o tra un frammento e la sua attività host. Ad esempio, potresti avere un frammento che legge i codici QR, in modo da ritrasmettere i dati a un frammento precedente.

Nella versione Fragment 1.3.0 e successive, ogni elemento FragmentManager implementa FragmentResultOwner. Ciò significa che un FragmentManager può fungere da archivio centrale per i risultati relativi ai frammenti. Questa modifica consente ai componenti di comunicare tra loro impostando i risultati dei frammenti e monitorandoli, senza richiedere che i componenti abbiano riferimenti diretti l'uno all'altro.

Trasmetti risultati tra frammenti

Per ritrasmettere i dati al frammento A dal frammento B, imposta prima un listener di risultati sul frammento A, il frammento che riceve il risultato. Chiama setFragmentResultListener() sul frammento A FragmentManager, come mostrato nell'esempio seguente:

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.
        }
    });
}
il frammento b invia dati al frammento a utilizzando un FragmentManager
Figura 1. Il frammento B invia i dati al frammento A utilizzando un FragmentManager.

Nel frammento B, il frammento che produce il risultato, imposta il risultato sullo stesso FragmentManager utilizzando lo stesso requestKey. A tale scopo, utilizza l'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);
    }
});

Il frammento A riceve il risultato ed esegue il callback del listener una volta che il frammento è STARTED.

Per una determinata chiave puoi avere un solo listener e un solo risultato. Se chiami setFragmentResult() più di una volta per la stessa chiave e se il listener non è STARTED, il sistema sostituisce eventuali risultati in attesa con il risultato aggiornato.

Se imposti un risultato senza un listener corrispondente per riceverlo, il risultato viene archiviato in FragmentManager finché non imposti un listener con la stessa chiave. Quando un listener riceve un risultato e attiva il callback onFragmentResult(), il risultato viene cancellato. Questo comportamento ha due implicazioni principali:

  • I frammenti nello stack posteriore non ricevono risultati finché non vengono stati scaricati e sono STARTED.
  • Se un frammento che ascolta un risultato è STARTED quando il risultato è impostato, il callback del listener si attiva immediatamente.

Risultati del frammento di test

Utilizza FragmentScenario per testare le chiamate a setFragmentResult() e setFragmentResultListener(). Crea uno scenario per il frammento sottoposto a test utilizzando launchFragmentInContainer o launchFragment, quindi chiama manualmente il metodo che non viene testato.

Per testare setFragmentResultListener(), crea uno scenario con il frammento che effettua la chiamata a setFragmentResultListener(). Dopodiché chiama direttamente setFragmentResult() e verifica il risultato:

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

Per testare setFragmentResult(), crea uno scenario con il frammento che effettua la chiamata a setFragmentResult(). Dopodiché chiama direttamente setFragmentResultListener() e verifica il risultato:

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

Passa i risultati tra frammenti padre e figlio

Per passare un risultato da un frammento figlio a un elemento padre, utilizza getChildFragmentManager() dal frammento principale anziché getParentFragmentManager() quando chiami 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.
            }
        });
}
un frammento figlio può utilizzare FragmentManager per inviare un risultato al frammento padre
Figura 2 Un frammento figlio può utilizzare FragmentManager per inviare un risultato al relativo elemento padre.

Il frammento figlio imposta il risultato sul relativo FragmentManager. Il padre riceve quindi il risultato una volta che il frammento è 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);
    }
});

Ricevi i risultati nell'attività dell'host

Per ricevere un risultato relativo ai frammenti nell'attività host, imposta un listener di risultati nel gestore di frammenti utilizzando 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.
            }
        });
    }
}