Search code examples
androidkotlinandroid-edittextandroid-alertdialogandroid-dialogfragment

Custom DialogFragment with AlertDialog returns EditText as ""


I have a custom DialogFragment that I'm using to capture user input that I will create a database entry with. I'm using EditText in an AlertDialog. I am trying to use a single activity for my application and the original tutorial I was studying was using multiple activities and intents but that seems outdated for most cases.

When I debug I find that the EditText is returning "" and is showing up as empty when I call TextUtils.isEmpty() in the MainActivity onDialogPositiveClick.

I've done a lot of combing through the forms here and I'm confused by:

1)many of the answers I find are in Java and not Kotlin

2)many mention onCreate but do not specify onCreateView vs. onCreateDialog or if there's just an onCreate that I need to override.

I have researched this and found answers that confuse me a bit about when and if I need to inflate the layout. This current itteration I didn't inflate it at all. I just set it in the AlertDialog builder.

Maybe it's the interface I'm not understanding. How am I supposed to pass information between the dialog and MainActivity? The interface seems to pass the dialog itself but I seem to be missing something when it comes to getting the EditText from the dialog.

My custom DialogFragment

class NewSongFragment : DialogFragment() {
    lateinit var listener: NewSongListener

    lateinit var editNewSong: EditText
    lateinit var editBPM: EditText

    interface NewSongListener {
        fun onDialogPositiveClick(dialog: DialogFragment)
        fun onDialogNegativeClick(dialog: DialogFragment)
    }

    /** The system calls this to get the DialogFragment's layout, regardless
    of whether it's being displayed as a dialog or an embedded fragment. */
   /*
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout to use as dialog or embedded fragment
        return inflater.inflate(R.layout.fragment_new_song, container, false)

    }
*/
    // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
    override fun onAttach(context: Context) {
        super.onAttach(context)
        // Verify that the host activity implements the callback interface
        try {
            // Instantiate the NoticeDialogListener so we can send events to the host
            listener = context as NewSongListener
        } catch (e: ClassCastException) {
            // The activity doesn't implement the interface, throw exception
            throw ClassCastException((context.toString() +
                    " must implement NewSongListener"))
        }
    }



    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        return activity?.let {
            // Use the Builder class for convenient dialog construction
            val builder = AlertDialog.Builder(it)

            //add inflater
            //val inflater = requireActivity().layoutInflater;
            //val view = inflater.inflate(R.layout.fragment_new_song, null)
            builder
                .setView(R.layout.fragment_new_song)
                .setCancelable(true)
                .setNegativeButton(R.string.cancel,DialogInterface.OnClickListener { dialog, id ->
                    dialog?.cancel()
                })
                .setPositiveButton(R.string.button_save,
                    DialogInterface.OnClickListener {dialog, _ ->
                        listener.onDialogPositiveClick(this)
                    })

            // Create the AlertDialog object and return it
            builder.create()
        } ?: throw IllegalStateException("Activity cannot be null")

    }

}

My MainActivity

class MainActivity : AppCompatActivity(),NewSongFragment.NewSongListener {
    private val songViewModel: SongViewModel by viewModels {
        SongViewModelFactory((application as SongApplication).repository)
    }

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

        //create view
        val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
        val adapter = ItemAdapter(this,
            ItemAdapter.OnClickListener { rating -> songViewModel.insertRating(rating) }
        )
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        //initialize data
        songViewModel.allSongs.observe(this) { song ->
            // Update the cached copy of the songs in the adapter.
            song.let { adapter.submitList(it) }
        }


        // Use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView

        recyclerView.setHasFixedSize(true)

        //add song button
        val fab = findViewById<FloatingActionButton>(R.id.fab)
        fab.setOnClickListener {
            showNewSongDialog()
            }
        }

    private fun showNewSongDialog() {
        // Create an instance of the dialog fragment and show it
        val dialog = NewSongFragment()
        dialog.show(supportFragmentManager, "NewSongFragment")
    }
    override fun onDialogPositiveClick(dialog: DialogFragment) {
        // User touched the dialog's positive button
        val editNewSong = dialog.view?.findViewById<EditText>(R.id.newSongTitle)
        val editBPM = dialog.view?.findViewById<EditText>(R.id.newSongBpm)
        if(TextUtils.isEmpty(editNewSong?.text)){

        }else{
            val newSong = Song(editNewSong?.text.toString(),100)
            songViewModel.insertSong(newSong)
            val rating = Rating(System.currentTimeMillis(),newSong.songTitle, 50)
            songViewModel.insertRating(rating)
        }

    }

    override fun onDialogNegativeClick(dialog: DialogFragment) {
        // User touched the dialog's negative button
    }


}


Solution

  • You are adding the layout with a resource identifier, so your call to get the view is returning null. (Why? The view is inflated internally and just handled differently.) Since you are using the AlertDialog to collect data, you will have to add an inflated view.

    I am also going to suggest that you change the interface to hide the details of the dialog; There is no reason for the main activity to know the internal structure of the dialog. It just needs the song title and BPM and maybe some other stuff. You will find the code a little easier to understand and maintain.

    Here is a slight rework. This code just captures the song title, but it can easily be extended to include other data as well.

    In NewSongFragment:

    interface NewSongListener {
        fun onDialogPositiveClick(songTitle: String)
        fun onDialogNegativeClick(dialog: DialogFragment)
    }
    
    val inflater = requireActivity().layoutInflater;
    val view = inflater.inflate(R.layout.fragment_new_song, null)
    builder
        .setView(view)
        .setCancelable(true)
        .setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { dialog, id ->
            dialog?.cancel()
        })
        .setPositiveButton(R.string.button_save)
        { dialog, _ ->
            Log.d("Applog", view.toString())
            val songTitle = view?.findViewById<EditText>(R.id.newSongTitle)?.text
            listener.onDialogPositiveClick(songTitle.toString())
        }
    

    In MainActivity.kt

    override fun onDialogPositiveClick(songTitle: String) {
        // songTitle has the song title string
    }
    

    Android dialogs have some quirks. Here are a number of ways to do fragment/activity communication.