I followed the official documentation to create a Dialog prompt on my app. It was working, but when I added the listener to process the data from the calling Fragment, I got a crash. It cannot instantiate the context as a listener. Here is some of my code :
The calling fragment :
class ConfigWifiFragment : Fragment(), BleManagerListener, WifiPasswordDialogFragment.NoticeDialogListener {
companion object {
fun newInstance() = ConfigWifiFragment()
}
private var _binding: FragmentConfigWifiBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private var dialogPassword = WifiPasswordDialogFragment()
//... some code
private fun connectToWifiNetwork(ssid: String)
{
Log.i("Connection", ssid)
Log.i("Context", context.toString())
getFragmentManager()?.let { dialogPassword.show(it, "passwordWifi") }
}
override fun onDialogPositiveClick(dialog: DialogFragment) {
TODO("Not yet implemented")
Log.i("ConfigWifi", dialog.config_wifi_password.text.toString())
}
override fun onDialogNegativeClick(dialog: DialogFragment) {
TODO("Not yet implemented")
Log.i("ConfigWifi", "not ok")
}
}
The dialog:
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment
class WifiPasswordDialogFragment : DialogFragment() {
internal lateinit var listener: NoticeDialogListener
interface NoticeDialogListener {
fun onDialogPositiveClick(dialog: DialogFragment)
fun onDialogNegativeClick(dialog: DialogFragment)
}
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 NoticeDialogListener // CRASH
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() +
" must implement NoticeDialogListener"))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
// Get the layout inflater
val inflater = requireActivity().layoutInflater;
// Inflate and set the layout for the dialog
// Pass null as the parent view because its going in the dialog layout
builder.setView(inflater.inflate(R.layout.dialog_wifi_password, null))
// Add action buttons
.setPositiveButton(R.string.validation,
DialogInterface.OnClickListener { dialog, id ->
// sign in the user ...
Log.i("dialog", "ok")
listener.onDialogPositiveClick(this)
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener { dialog, id ->
//getDialog().cancel()
Log.i("dialog", "Cancel")
listener.onDialogNegativeClick(this)
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
The code crash in that line: listener = context as NoticeDialogListener, it cannot get the context as a NoticeDialogListener even if it's implemented in the calling fragment (and then the exception is thrown). I checked the ID of the context before showing the dialogue prompt, and the one inside; it's the same.
The only difference between my code and the example is I'm using a Fragment and not a FragmentActivity. How could I make works my code in my case?
I found a solution, I don't know if it's the more elegant way, but here is it:
1st: I removed the onAttach function in my DialogFragment
2nd: I created a method to set the listener :
fun setListener(_listener : WifiPasswordDialogListener)
{
listener = _listener
}
3rd: I called that method just before the show method:
private fun connectToWifiNetwork(ssid: String)
{
Log.i("Connection", ssid)
Log.i("Context", context.toString())
dialogPassword.setListener(this)
getFragmentManager()?.let { dialogPassword.show(it, "passwordWifi") }
}
I can now access to the data of my DialogBox from the calling fragment like this :
override fun onDialogPositiveClick(dialog: DialogFragment) {
Log.i("ConfigWifi", dialog.dialog?.config_wifi_password?.text.toString())
}
Related
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
}
}
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.
Because you are adding the dialog as a Fragment, you should use onCreateView to inflate the view, rather than trying to add a view in onCreateDialog.
I have a RecyclerView in a fragment. I need to initialize a fragment of adding a new item through a custom dialog. In the dialog, I need to get the text and uri of the image (in this case, for a start, only the text to make it easier to understand). I have problems passing data from Edit Text in Dialog Fragment.
My Activity with recycler view:
class AddExerciseActivity : AppCompatActivity(),NoticeDialogFragment.NoticeDialogListener {
private lateinit var recyclerAdapter: RecyclerViewAdapter
private lateinit var exercisesList: ArrayList<ExercisesModel>
private lateinit var recyclerview: RecyclerView
private lateinit var addNewExerciseButton : ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_new_exercise)
init()
addNewExerciseButton = findViewById(R.id.ib_addNewExercise)
addNewExerciseButton.setOnClickListener {
addNewExercise()
}
}
private fun init(){
exercisesList = ArrayList(ExercisesModel.createBaseList(20))
setupRecyclerView()
}
private fun setupRecyclerView() {
recyclerAdapter = RecyclerViewAdapter(this, exercisesList)
recyclerview = findViewById(R.id.recycler_view)
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerview.adapter = recyclerAdapter
recyclerview.setHasFixedSize(true)
}
private fun addNewExercise(){
val dialog = NoticeDialogFragment()
dialog.show(supportFragmentManager, "NoticeDialogFragment")
//Todo how to add new items from dialog
recyclerAdapter.notifyDataSetChanged()
}
override fun onDialogPositiveClick(dialog: DialogFragment) {
TODO("Not yet implemented")
}
override fun onDialogNegativeClick(dialog: DialogFragment) {
TODO("Not yet implemented")
}
}
My Dialog Fragment:
class NoticeDialogFragment : DialogFragment() {
internal lateinit var listener: NoticeDialogListener
interface NoticeDialogListener {
fun onDialogPositiveClick(dialog: DialogFragment)
fun onDialogNegativeClick(dialog: DialogFragment)
}
// 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 NoticeDialogListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() +
" must implement NoticeDialogListener"))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val inflater = requireActivity().layoutInflater;
builder.setView(inflater.inflate(R.layout.dialog_signin, null))
// Add action buttons
.setPositiveButton(R.string.addExercise,
DialogInterface.OnClickListener { dialog, id ->
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener { dialog, id ->
getDialog()?.cancel()
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
You need to add a FragmentResultListener to the FragmentManager. This will allow the DialogFragment to communicate with Fragment/Activity that created it.
Here is a great article on the break down https://proandroiddev.com/android-fragments-fragment-result-805a6b2522ea
If you do this you would be able to get rid of the NoticeDialogListener.
Once you have the FragmentResultListener setup then you need to make ExercisesModel Parcelable if not already. In the dialog the user gives all the necessary info to build a ExercisesModel. When they click confirm/ok you then call
val bundle = bundleOf(
"RESPONSE" to RESPONSE_OK, //Let the listener know ok/confirm was clicked.
"EXERCISE_MODEL" to createdExerciseModel
)
parentFragmentManager.setFragmentResult("CREATE_EXERCISE_MODEL", bundle)
Inside the original Fragment/Activity.
FragmentResultListener { requestKey: String, result: Bundle -> {
if ("CREATE_EXERCISE_MODEL".equals(requestKey)) {
var response = result.getCharSequence("RESPONSE")
if (response.equals(RESPONSE_OK)) {
var exerciseModel = result.getParcelable("EXERCISE_MODEL")
// You will need to add an add method to the
// adapter to allow adding to it internal list.
// Notify the changes to the adapter.
recyclerAdapter.add(exerciseModel)
}
}
}
}
How does one handle button clicks in a Dialog fragment. I would like to run method in fragment once a dialog button is clicked, the example given in dev docs works for activities but not fragments.
MyAlertDialog
class MyAlertDialog : DialogFragment() {
var listener: MyListener
interface MyListener {
onPositiveClick(dialog: DialogFragment)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as MyListener
} catch (e: ClassCastException) {
throw ClassCastException((context.toString() + " must implement MyDailogListener")
}
}
override fun onCreateDialog(savedInstanceState:Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage("Do you want to reset the score.")
.setPositiveButton("Confirm",
DialogInterface.OnClickListener { dialog, id ->
...
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
MyFragment
class MyFragment : Fragment(), MyAlertDialog.MyListener {
...
fun launchAlertDialog() {
val dailog = MyAlertDialog().also {
it.show(requireActivity().supportFragmentManager, "DialogInfoFragment")
}
}
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
}
The DialogFragment is launched from the activity not from the fragment despite being called in the fragment. Linking the interface this way will only work if you are connecting to an activity.
If you are only updating data, you could pass the ViewModel into the DialogFragment through the constructor.
Fragment
fun launchAlertDialog() {
val fragment = MyFragment(viewModel)
fragment.show(requireActivity().supportFragmentManager, "MyAlertDialog")
}
DialogFragment
class MyAlertDialog(val viewModel: MainViewModel) : DialogFragment() {
overrideOnCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(...)
.setPositiveButton("My Text", DialogInterface.OnClickListener { dialog, id ->
viewModel.updateMyData()
})
builder.create()
}
} ?: throw IllegalStateException("Activity cannot be null")
}
You could also use this approach here to run a function in the Fragment.
https://stackoverflow.com/a/65116644/3943340
Keep in mind that setTargetFragment is now deprecated, as of API 28(could be wrong on the API).
I have a DialogFragment with a listener for when a button gets clicked to call a function in my fragment.
I am getting lateinit property listener has not been initialized when I click the positive button.
DialogFragment
class CreateCollectionDialog: DialogFragment() {
lateinit var listener: CreateCollectionDialogListener
interface CreateCollectionDialogListener {
fun onDialogPositiveClick(dialog: DialogFragment, collectionName: String)
// fun onDialogNegativeClick(dialog: DialogFragment)
}
override fun onAttachFragment(childFragment: Fragment) {
println("onAttachFragment")
super.onAttachFragment(childFragment)
listener = context as CreateCollectionDialogListener
println(listener)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val inflater = requireActivity().layoutInflater
builder.setView(inflater.inflate(R.layout.dialog_collection, null))
.setPositiveButton("Create", DialogInterface.OnClickListener { dialog, id ->
// Create new collection
var newCollectionName = view?.findViewById<EditText>(R.id.newCollectionName)?.text.toString()
if (!newCollectionName.equals("") && newCollectionName != null) {
listener.onDialogPositiveClick(this, newCollectionName)
}
})
.setNegativeButton("Cancel", DialogInterface.OnClickListener { dialog, id ->
// User canceled dialog
// listener.onDialogNegativeClick(this)
})
builder.create()
}?: throw IllegalStateException("Activity cannot be null")
}
override fun onStart() {
super.onStart()
val positive: Button = (dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_POSITIVE)
positive.setTextColor(resources.getColor(R.color.topColor))
val negative: Button = (dialog as AlertDialog?)!!.getButton(AlertDialog.BUTTON_NEGATIVE)
negative.setTextColor(Color.RED)
}
}
Fragment
class CollectionsFragment: Fragment(), CreateCollectionDialog.CreateCollectionDialogListener {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add -> {
val createDialog = CreateCollectionDialog()
createDialog.show(fragmentManager!!, "")
return true
}
}
return false
}
override fun onDialogPositiveClick(dialog: DialogFragment, collectionName: String) {
addNewCollection(collectionName)
}
}
onAttachFragment is called when a fragment is attached as a child of this fragment, which in this case, never and not required.
Use onAttach(Context context) for current scenario. Dialog fragment has no child so onAttachFragment will never be called.
To initialize the listener from the parent fragment, use:
// inside fragment lifecycle methods like onviewcreated etc
listener = getParentFragment() as CreateCollectionDialogListener
The simplest way to solve this problem would be to assign the listener at the time you create the dialog:
when (item.itemId) {
R.id.add -> {
val createDialog = CreateCollectionDialog()
createDialog.listener = this
createDialog.show(fragmentManager!!, "")
return true
}
}
However, note that this will have problems if the activity is destroyed and recreated due to a configuration change.
To solve that, I would leverage the concept of "target fragments":
when (item.itemId) {
R.id.add -> {
val createDialog = CreateCollectionDialog()
createDialog.setTargetFragment(this, 0)
createDialog.show(fragmentManager!!, "")
return true
}
}
And now, in your other fragment, instead of having a listener field, you can just cast the targetFragment property:
if (!newCollectionName.equals("") && newCollectionName != null) {
val listener = targetFragment as CreateCollectionDialogListener
listener.onDialogPositiveClick(this, newCollectionName)
}
Problem seems to be with your fragmentManager!!
Try using childFragmentManager to open the DialogFragment.
Also, check if lateinit listener is actually initialized or not.
Using Anko library is pretty easy, but when i rotate screen, my dialog dismisses. The only way how to avoid this is to use child of DialogFragment() with method show(fm, TAG).
So we need to override method onCreateDialog(savedInstanceState: Bundle?): Dialog which returns Dialog instance. But Anko's alert{ }.build() returns DialogInterface instance
So, is there any way to use anko in this situation?
alert {
message = "Message"
positiveButton("OK") {
//stuff
}
negativeButton("NOT OK") {
//stuff
}
}.show()
EDIT
So, that what I did.
I've created abstract BaseDialogFragment:
abstract class BaseDialogFragment : DialogFragment() {
abstract val ankoAlert: AlertBuilder<DialogInterface>
protected var dialogListener: DialogListener? = null
protected val vm by lazy {
act.getViewModel(DialogViewModel::class)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dialogListener = parentFragment as? DialogListener
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
(ankoAlert.build() as? Dialog)
?: error("Anko DialogInterface is no longer backed by Android Dialog")
}
Then I've created some dialogs, like that:
class MyDialogFragment : BaseDialogFragment() {
companion object {
fun create() = MyDialogFragment ()
}
override val ankoAlert: AlertBuilder<DialogInterface>
get() = alert {
negativeButton(R.string.app_canceled) {
dialogListener?.onDismiss?.invoke()
}
customView = createCustomView(vm.data)
}
fun createCustomView(data: Data): View {
//returning view
}
}
Also my DialogListener is sort of that:
interface DialogListener {
var onDismiss: () -> Unit
val onClick: (Data) -> Unit
var onPostClick: (Data) -> Unit
}
And finally, in parent fragment we can use:
MyDialogFragment.create().show(childFragmentManager, MyDialogFragment::class.java.simpleName)
Hope it will help somebody.
From the Android Documentation, Dialog implements DialogInterface. So all known subclasses of Dialog including AlertDialog implement that interface.
You can cast and return the result from the build as follows:
return alert {
message = "Message"
positiveButton("OK") {
//stuff
}
negativeButton("NOT OK") {
//stuff
}
}.build() as Dialog
This will work but if Anko ever changes its implementation you will get a ClassCastException. To get a cleaner error you can use the following.
val dialogInterface = alert {
message = "Message"
positiveButton("OK") {
//stuff
}
negativeButton("NOT OK") {
//stuff
}
}.build()
return (dialogInterface as? Dialog) ?: error("Anko DialogInterface is no longer backed by Android Dialog")
This gives you a more explicit error, but most likely won't be needed.
If you don't have dynamic UI then you can use android:configChanges="orientation" in your activity in manifest, look likes:
<activity android:name=".MainActivity"
android:configChanges="orientation">
...
</activity>