View Binding in DialogFragment with custom layout in Kotlin

Viewed 2559

I loved using Kotlin synthetic for its simplicity and code elegance but now they made it depricated and push you to use those ugly view bindings.

There are plenty of answers on how to use it in activites and Fragments, but could not find any examples for custom layout alert dialogs.

Here is the code which worked perfectly with Kontlin synthetic.

import kotlinx.android.synthetic.main.dialog_reward.*

class RewardDialog: DialogFragment() {
    private var mView: View? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return mView
    }
    
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            mView = it.layoutInflater.inflate(R.layout.dialog_reward, null)
            AlertDialog.Builder(it).apply {
                setView(mView)
            }.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        //reference layout elements by name freely
        
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        mView = null
    }
}

How do I migrate this to view bindings?

3 Answers

You can simply use generated ViewBinding views here and not use onCreateDialog

@AndroidEntryPoint
class RewardDialog : DialogFragment() {
private var binding: DialogRewardBinding by autoCleared()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setStyle(STYLE_NORMAL, R.style.Theme_MaterialComponents_Light_Dialog_MinWidth)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    binding = DialogRewardBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    //reference layout elements by name freely
    binding.tvReward.setOnClickListener { }
   }

}

autoCleared() is an extension function which will null out all the view in onDestroy() taken from google's architecture component sample here

You can set R.style.Theme_MaterialComponents_Light_Dialog_MinWidth theme in onCreate() so that DialogFragment follows the minWidth defined in Material Compnent theme on deferent screen sizes just like AlertDialog

Edit:

If you are not using the material component library then you can set the width in onViewCreated() using the Kotlin extension.

 setWidthPercent(ResourcesCompat.getFloat(resources, R.dimen.dialogWidthPercent).toInt())

Kotlin extenstion function

fun DialogFragment.setWidthPercent(percentage: Int) {
val percent = percentage.toFloat() / 100
val displayMetrics = Resources.getSystem().displayMetrics
val rect = displayMetrics.run { Rect(0, 0, widthPixels, heightPixels) }
val percentWidth = rect.width() * percent
dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)

} 

I have ended up with the following solution. Thanks to @Kishan Maurya for the hint about binding.root.

private var _binding: DialogRewardBinding? = null
private val binding get() = _binding!!

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    return activity?.run {
        //initiate the binding here and pass the root to the dialog view
        _binding = DialogRewardBinding.inflate(layoutInflater).apply {
            //reference layout elements by name freely here
        }
        AlertDialog.Builder(this).apply {
            setView(binding.root)
        }.create()
    } ?: throw IllegalStateException("Activity cannot be null")
}


override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}
class RewardDialog : DialogFragment() {

    private var mView: View? = null
    private lateinit var dialogBinding: DialogRewardBinding

    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // either this way we can init dialogBinding
        dialogBinding = DialogRewardBinding.inflate(inflater, container, false)
        return dialogBinding.root
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return activity?.let {
            // either this way we can init dialogBinding
            dialogBinding = DataBindingUtil.setContentView(it, R.layout.dialog_reward)
            AlertDialog.Builder(it).apply { setView(mView) }.create()
        } ?: throw IllegalStateException("Activity cannot be null")
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        with(view) {
            myText.text = "Demo"
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        mView = null
    }
}

instead of mView, you can use dialogBinding.root

layout

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/myText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="demo"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Related