Using AlertDialog and DialogFragment

The NukeFromOrbit sample module in the Sampler and SamplerJ projects has a UI that consists of a really big button:

NukeFromOrbit, As Initially Launched
NukeFromOrbit, As Initially Launched

Clicking the button brings up the dialog shown earlier in the chapter:

NukeFromOrbit Sample, Showing Dialog
NukeFromOrbit Sample, Showing Dialog

Clicking either button dismisses the dialog, but if you click the “Nuke It” button, we also display a Toast.

Defining the Dialog

This particular sample uses DialogFragment and AlertDialog to display the fragment. The apps have a ConfirmationDialogFragment that overrides onCreateDialog() and creates an AlertDialog to display:

package com.commonsware.jetpack.samplerj.dialog;

import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;

public class ConfirmationDialogFragment extends DialogFragment {
  @NonNull
  @Override
  public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
    final NavController navController = NavHostFragment.findNavController(this);
    final ViewModelProvider viewModelProvider = new ViewModelProvider(navController.getViewModelStoreOwner(R.id.nav_graph));
    final GraphViewModel vm = viewModelProvider.get(GraphViewModel.class);

    return new AlertDialog.Builder(requireActivity())
      .setTitle(R.string.dialog_title)
      .setMessage(R.string.dialog_message)
      .setPositiveButton(R.string.dialog_positive, (dialog, which) -> vm.onAccept())
      .setNegativeButton(R.string.dialog_negative, (dialog, which) -> vm.onDecline())
      .setOnCancelListener(dialog -> vm.onDecline())
      .create();
  }
}
package com.commonsware.jetpack.sampler.dialog

import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment

class ConfirmationDialogFragment : DialogFragment() {
  override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val navController = NavHostFragment.findNavController(this)
    val viewModelProvider =
      ViewModelProvider(navController.getViewModelStoreOwner(R.id.nav_graph))
    val vm = viewModelProvider.get(GraphViewModel::class.java)

    return AlertDialog.Builder(requireActivity())
      .setTitle(R.string.dialog_title)
      .setMessage(R.string.dialog_message)
      .setPositiveButton(R.string.dialog_positive) { _, _ -> vm.onAccept() }
      .setNegativeButton(R.string.dialog_negative) { _, _ -> vm.onDecline() }
      .setOnCancelListener { vm.onDecline() }
      .create()
  }
}

To create an AlertDialog, we use an AlertDialog.Builder. This takes our activity as a constructor parameter, so we use requireActivity() to get the activity that is hosting this fragment and use it. As the name suggests, AlertDialog.Builder offers a builder-style API to create the dialog, where we can call a series of functions one after the next. Specifically we configure:

For the buttons, in addition to the caption, we also provide a lambda expression that will be invoked if the button gets clicked. What that lambda expression does, and the rest of the stuff in onCreateDialog(), we will explore more later on.

Displaying the Dialog

The classic way of displaying a DialogFragment is to create an instance and call show() on it. This certainly works and you are welcome to use it.

However, if you are using the Navigation component, you can set up a DialogFragment as a destination in a navigation graph, simply by using a <dialog> element instead of a <fragment> element:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:id="@+id/nav_graph"
  app:startDestination="@id/mainFragment">

  <fragment
    android:id="@+id/mainFragment"
    android:name="com.commonsware.jetpack.sampler.dialog.MainFragment"
    android:label="MainFragment" >
    <action
      android:id="@+id/confirmNuke"
      app:destination="@id/confirmationDialogFragment" />
  </fragment>
  <dialog
    android:id="@+id/confirmationDialogFragment"
    android:name="com.commonsware.jetpack.sampler.dialog.ConfirmationDialogFragment"
    android:label="ConfirmationDialogFragment" />
</navigation>

Here, we have ConfirmationDialogFragment set up in a graph, reachable by a confirmNuke action from MainFragment.

MainFragment — which happens to be displaying that big button — can then just navigate to ConfirmationDialogFragment without knowing or caring that this is a dialog versus some other type of destination:

    binding.nuke.setOnClickListener(v ->
      NavHostFragment.findNavController(MainFragment.this)
        .navigate(R.id.confirmNuke));
    requireBinding().nuke.setOnClickListener {
      NavHostFragment.findNavController(this@MainFragment)
        .navigate(R.id.confirmNuke)
    }

Here, we are using view binding to bind a click listener to the nuke button, so when the button is clicked, we show the dialog.

Sharing a ViewModel

Often, an activity or fragment needs a private ViewModel for managing its own data. However, sometimes, it would be useful to have shared ViewModel objects. For example, in this sample app, MainFragment needs to know whether the user clicked the positive or the negative button. This implies some amount of data-sharing between MainFragment and ConfirmationDialogFragment, and sharing a ViewModel would be one approach for that sort of sharing.

That is handled by having the right ViewModelProvider:

In Kotlin, instead of using the viewModels() property delegate, you can use activityViewModels() to get a ViewModel from the activity’s scope, sharing that ViewModel with the activity.

It is also possible to get a ViewModelProvider tied not to an activity or a fragment, but to a navigation graph. All of the destinations in that navigation graph will share ViewModel objects obtained from the graph-specific ViewModelProvider.

To do that, we:

Both MainFragment and ConfirmationDialogFragment do this via similar blocks of code:

    final NavController navController = NavHostFragment.findNavController(this);
    final ViewModelProvider viewModelProvider = new ViewModelProvider(navController.getViewModelStoreOwner(R.id.nav_graph));
    final GraphViewModel vm = viewModelProvider.get(GraphViewModel.class);
    val navController = NavHostFragment.findNavController(this)
    val viewModelProvider =
      ViewModelProvider(navController.getViewModelStoreOwner(R.id.nav_graph))
    val vm = viewModelProvider.get(GraphViewModel::class.java)

Both our MainFragment instance and our ConfirmationDialogFragment instance now share a GraphViewModel instance.

Reacting to the Dialog

But what we really want is to get the button-click events from ConfirmationDialogFragment to MainFragment. The GraphViewModel is a key piece of that, but we need some other elements as well… and we will take a look at those in an upcoming chapter.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.