Search code examples
androidkotlinandroid-alertdialogdagger-2

Dagger 2 Inject DialogUtils class in activity


I'm trying to inject my DialogUtils class that requires an activity context in its constructor into my LoginActivity. Below code is working but I'm just initializing DialogUtils and not injecting it.

DialogUtils.kt

class DialogUtils constructor(context: Context) {

    private val dialog: AlertDialog

    init {
        dialog = AlertDialog.Builder(context)
            .setPositiveButton(R.string.ok) { dialog, _ ->
                dialog.dismiss()
            }
            .create()
    }

    fun showDialog(title: String, msg: String) {
        dialog.setTitle(title)
        dialog.setMessage(msg)
        dialog.show()
    }

    fun clear() {
        if (dialog.isShowing) {
            dialog.dismiss()
        }
    }

}

LoginActivity.kt

class LoginActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory
    private val loginViewModel by lazy {
        ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel::class.java)
    }
    private lateinit var dialogUtils: DialogUtils

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        dialogUtils = DialogUtils(this)
    }

    override fun onPause() {
        super.onPause()
        dialogUtils.clear()
    }

}

What I want to do is to @Inject dialogUtils: DialogUtils in my LoginActivity but for this I'll need activity context. How can I achieve this keeping in mind that DialogUtils is used in many activities.


Solution

  • There is a flaw in your design here, that has nothing to do with DI or Dagger even: It's that your DialogUtils class will use plain AlertDialogs instead of DialogFragments. This means that dialogs generated by your class are not persistent on the screen (e.g. during activity restarts like when the device rotates) which gives a bad user experience.

    So what I'd do instead is to write a custom AlertDialogFragment implementation that communicates via callbacks with it's host (which might be an Activity or a Fragment), meaning that your dialog fragment implementation looks in certain places if it finds a host that implements its callback and communicates back user actions (such as button clicks, etc.). This is some utility code I usually use in my projects for this purpose:

    
    inline fun <reified T> Fragment.requireCallback(): T =
        findCallback() ?: error("No parent / target found or parent / target does not implement " + T::class.java)
    
    inline fun <reified T> Fragment.findCallback(): T? {
        val callback: Any? = parentFragment ?: targetFragment ?: context
        return callback as? T
    }
    

    This could go into your AlertDialogFragment implementation like this:

    class AlertDialogFragment : DialogFragment() {
    
        private lateinit var listener: Listener
    
        interface Listener {
            fun onDialogResult(requestCode: Int, result: AlertDialogResult)
        }
    
        override fun onAttach(context: Context) {
            super.onAttach(context)
            listener = requireCallback()
        }
        ...
    }
    

    Now when you instantiate your dialog in your parent Activity or Fragment, you just need to let this parent implement the AlertDialogFragment.Listener interface and you'd forward it's results to your ViewModel - and the nice thing is: this would work across activity restarts!

    You might wonder whether you want to require the callback to be implemented on the host for each dialog; my personal rule is: for one-button dialogs that mainly confirm actions (like error dialogs) I don't require the callback to be present (i.e. use the plain findCallback()), while for special DecisionDialogFragment implementations that have more than one button to click I make the callback usually required, so my app actually crashes if the callback is not implemented on the host as this usually means a missing piece of code on my side.