How to use Material dialogs with DialogFragment
Tags: android, dialog, materialdesign, kotlin
Published: 2020-05-23 08:59:16

In this article, I'll tell you how to use the new Material dialogs with [`DialogFragment`](https://developer.android.com/reference/androidx/fragment/app/DialogFragment).

Material dialogs are a re-styling of commonly-used [`Dialog`](https://developer.android.com/reference/android/app/Dialog.html). I won't go into too many details on what they do or how to customize them here, you can read this great article from Nick Rout: [Hands-on with Material Components for Android: Dialogs](https://medium.com/over-engineering/hands-on-with-material-components-for-android-dialogs-75c6d726f83a).

Here is, for comparison, an AppCompat `AlertDialog` and a Material `AlertDialog`:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/1ci1lbgjjdvim992k39z.png" alt="AppCompat Alert Dialog vs Material Dialog" style="zoom:25%;" />
You can see the differences in default shadow, size, typography, etc... But the real advantage is to be able to support Material typography and shape out of the box with theme configuration.

For example, here is the result with bold title and custom shape (You can look into [styles.xml](https://github.com/Bhullnatik/MaterialDialogsFragmentExample/blob/master/app/src/main/res/values/styles.xml) for how to do this):

<img src="https://dev-to-uploads.s3.amazonaws.com/i/mjmammu0dl4szm8lj6k3.png" alt="Customized Material AlertDialog" style="zoom:25%;" />

Nice isn't it? Except it is quite limited to use only `Dialog`. You can't use complex layouts other than the 4 types mentioned in Nick's article, or use them with more complex view hierarchy, or from [Navigation Architecture Component](https://developer.android.com/guide/navigation/navigation-getting-started#create-dialog). For all that, you will have to use a [`DialogFragment`](https://developer.android.com/reference/androidx/fragment/app/DialogFragment).

If you try to display a `DialogFragment` now, we will see it still looks like a regular `AlertDialog`:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/7lgrlw14l8eeg10xlsr7.png" alt="AppCompat DialogFragment" style="zoom:25%;" />

So let's move onto the solution on how to use Material dialog with `DialogFragment`!

**The solution:**

`DialogFragment`s are useful to show more complex layouts in a dialog way, allowing you to control and manipulate your custom layout. They act like regular [`Fragment`](https://developer.android.com/guide/components/fragments), meaning you have to override [`onCreateView()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#onCreateView(android.view.LayoutInflater,%20android.view.ViewGroup,%20android.os.Bundle)) & [`onViewCreated()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#onViewCreated(android.view.View, android.os.Bundle)) to display your custom view.

To use Material dialogs with `DialogFragment`, you can override [`onCreateDialog()`](https://developer.android.com/reference/kotlin/androidx/fragment/app/DialogFragment?hl=en#oncreatedialog) in your `DialogFragment`, to return a `MaterialDialog`. It is mentionned briefly by Nick in their article, you can see the example on [this commit](https://github.com/Bhullnatik/MaterialDialogsFragmentExample/commit/addb9c2db8b9b57fe04748b0010165a427583204#diff-3de1fb467da1da746330c0ad22bf49b9R23). So implement it, show it and you get:

<img src="https://dev-to-uploads.s3.amazonaws.com/i/t14ybfl3xkjuqjpu9atb.png" alt="MaterialDialogBuilder with DialogFragment" style="zoom:25%;" />

...Nothing. How is this possible? 

It's because `DialogFragment` ignores `onCreateView()` if you override `onCreateDialog()`, as it assumes the dialog will take care of its own view. As the docs say:

> when doing so, `onCreateView(LayoutInflater, ViewGroup, Bundle)` does not need to be implemented since the AlertDialog takes care of its own content.

The solution would be to use [`setView()`](https://developer.android.com/reference/android/app/Dialog#setContentView(android.view.View)) on our `MaterialDialogBuilder` inside `onCreateDialog`. You can check the example at [this commit](https://github.com/Bhullnatik/MaterialDialogsFragmentExample/blob/5b9890ebe8c96ac8738cba223269b7cee5050b03/app/src/main/java/io/bhullnatik/materialdialogsfragmentexample/ExampleDialogFragment.kt#L13) and it works! 🎉

<img src="https://dev-to-uploads.s3.amazonaws.com/i/unw5gdiruzsiir1boini.png" alt="Customized DialogFragment" style="zoom:25%;" />

The problem with this approach, is that for complex layouts it is difficult to manage as we would have to handle all communications with our views when creating the `Dialog`, which is not very clean. 

It can also cause sneaky crashes, as [`getView()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getView()) on our `DialogFragment` will return `null`, since the view wasn't properly initialized this way. It also breaks [`ViewBinding`](https://developer.android.com/topic/libraries/view-binding) or [Kotlin synthetics](https://antonioleiva.com/kotlin-android-extensions/), which may require painful migrations on larger codebases. 

So is there a better way?

**The hack:**

It turns out we have pretty much everything we need at hand to make it work without needing to migrate existing code.

In a new class inheriting `DialogFragment`, we can override `onCreateDialog()` to make it use `onCreateView()` & `onViewCreated()` to display and  manage the `Fragment`'s layout, like so:

```kotlin
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return MaterialAlertDialogBuilder(requireContext(), theme).apply {
            dialogView = onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState)

            setView(dialogView)
        }.create()
    }
```

Note that we don't need to pass a proper `container` here to `onCreateView` since this is used when the fragment will be added to a view hierarchy, which is rarely the case for a `DialogFragment`. We also properly restoring the state with `savedInstanceState`.

We can then override the `getView()`  for our `Fragment` as to fix accessing our views:

```kotlin
    override fun getView(): View? {
        return dialogView
    }
```

This basically redirects view accessors to refer to our dialog's custom view, fixing in the process tools making it easier to access views inside `Fragment`s.

We are guaranteed that it will be valid for the lifecycle of the `Dialog`, and therefore of the `DialogFragment`. 

And that's it! You can put that into a new `MaterialDialogFragment` class, change your `DialogFragment` inheritance to `MaterialDialogFragment`, and enjoy your hassle-free Material dialogs. You can find the complete [complete example here](https://github.com/Bhullnatik/MaterialDialogsFragmentExample) and the [`MaterialDialogFragment` class here](https://github.com/Bhullnatik/MaterialDialogsFragmentExample/blob/master/app/src/main/java/io/bhullnatik/materialdialogsfragmentexample/MaterialDialogFragment.kt).

Hope it could help you in some way, don't hesitate to comment if you have a question or suggestion. 👋

**⚠️ Disclaimer:** This was not battle-tested and could break/not handle everything a `DialogFragment` does in some situations. Use at your own risk. There is an issue open on this subject, that you can follow here: [MaterialComponents Android: Use MaterialDialog with Navigation Component - Issue #540](https://github.com/material-components/material-components-android/issues/540)