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.
Related
I am making a list app using rooms for database. I created a dialog box to add things to the list. How do I return the data from a dialog to add items to the main activity.
AddListDialog.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_add_url)
tvAddToLib.setOnClickListener{
val content = etContent.text.toString()
if (content.isEmpty()){
Toast.makeText(context, "Enter Text", Toast.LENGTH_SHORT).show()
return#setOnClickListener
}
val item = UrlList(addTime, content, readTime = null)
addDialogListener.onAddButtonClicked(item)
dismiss()
}
tvCancel.setOnClickListener {
cancel()
}
}
Main Activity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dao = ListDao
val repository = UrlRepository(dao)
val factory = ListViewModelFactory(repository)
val viewModel = ViewModelProviders.of(this, factory).get(ListViewModel::class.java)
val adapter = ListAdapter(listOf(), viewModel)
rvListenItems.layoutManager = LinearLayoutManager(this)
rvListenItems.adapter = adapter
viewModel.getList().observe(this, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
fabAdd.setOnClickListener {
AddUrlDialog(this, object : AddDialogListener{
override suspend fun onAddButtonClicked(item: List) {
viewModel.upsert(item)
}
}).show()
}
Adddialoglistener.kt
import com.example.listen.data.db.entities.List
interface AddDialogListener {
suspend fun onAddButtonClicked(item : List)
}
have you tried to use a new .xml layout and ContentView(layoutid) it on your main activity as a dialog ? lets make one dialog with list now.
first make your .xml layout that will contain a list, we will use as a dialog to return any actions from it in the same kotlain main class.
see pictures ^_^ i hope it will work, good luck.
step1
step2
step3
step4
step5
step6
step7
step8
i have made a horizontal list of buttons instead, i thought it would be cool !, but you can put anything in your .xml layout then as i illustrated .show()
it as dialog setting it in the contentView(layoutID) of the dialog you have initiated in anywhere ^_^, good luck.
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)
}
}
}
}
I'm building an Android app that has different pages that mainly have some EditText. My goal is to handle the click on the EditText and shows a DialogAlert with an EditText, then the user can put the text, click "save" and the related field in the database (I'm using Room and I've tested the queries and everything works) will be updated. Now I was able to handle the text from the DialogFragment using interface but I don't know how to say that the text retrieved is related to the EditText that I've clicked. What is the best approach to do this?
Thanks in advance for your help.
Let's take this fragment as example:
class StaticInfoResumeFragment : Fragment(), EditNameDialogFragment.OnClickCallback {
private val wordViewModel: ResumeStaticInfoViewModel by viewModels {
WordViewModelFactory((requireActivity().application as ManagementCinemaApplication).resumeStaticInfoRepo)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val root = inflater.inflate(R.layout.fragment_static_info_resume, container, false)
wordViewModel.resumeStaticInfo.observe(viewLifecycleOwner) { words ->
println("test words: $words")
}
val testView = root.findViewById<TextInputEditText>(R.id.textInputEditText800)
testView.setOnClickListener{
val fm: FragmentManager = childFragmentManager
val editNameDialogFragment = EditNameDialogFragment.newInstance("Some Title")
editNameDialogFragment.show(fm, "fragment_edit_name")
}
resumeStaticInfoViewModel.firstName.observe(viewLifecycleOwner, Observer {
testView.setText(it)
})
return root
}
override fun onClick(test: String) {
println("ciao test: $test")
wordViewModel.updateFirstName(testa)
}}
Then I've the ViewModel:
class ResumeStaticInfoViewModel(private val resumeStaticInfoRepo: ResumeStaticInfoRepo): ViewModel() {
val resumeStaticInfo: LiveData<ResumeStaticInfo> = resumeStaticInfoRepo.resumeStaticInfo.asLiveData()
fun updateFirstName(resumeStaticInfoFirstName: String) = viewModelScope.launch {
resumeStaticInfoRepo.updateFirstName(resumeStaticInfoFirstName)
}
....
And the DialogFragment:
class EditNameDialogFragment : DialogFragment() {
private lateinit var callback: OnClickCallback
interface OnClickCallback {
fun onClick(test: String)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
callback = parentFragment as OnClickCallback
} catch (e: ClassCastException) {
throw ClassCastException("$context must implement UpdateNameListener")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().getString("title")
val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
alertDialogBuilder.setTitle(title)
val layoutInflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val alertCustomView = layoutInflater.inflate(R.layout.alert_dialog_edit_item, null)
val editText = alertCustomView.findViewById<EditText>(R.id.alert_edit)
alertDialogBuilder.setView(alertCustomView)
alertDialogBuilder.setPositiveButton(
"Save",
DialogInterface.OnClickListener { dialog, which ->
callback.onClick(editText.text.toString())
})
alertDialogBuilder.setNegativeButton("No") { _: DialogInterface, _: Int -> }
return alertDialogBuilder.create()
}
companion object {
fun newInstance(title: String?): EditNameDialogFragment {
val frag = EditNameDialogFragment()
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
Do you mean you just want to show a basic dialog for entering some text, and you want to be able to reuse that for multiple EditTexts? And you want a way for the dialog to pass the result back, but also have some way of identifying which EditText it was created for in the first place?
The thing about dialogs is they can end up being recreated (like if the app is destroyed in the background, and then restored when the user switches back to it) so the only real configuration you can do on it (without getting into some complexity anyway) is through its arguments, like you're doing with the title text.
So one approach you could use is send some identifier parameter to newInstance, store that in the arguments, and then pass it back in the click listener. So you're giving the callback two pieces of data in onClick - the text entered and the reference ID originally passed in. That way, the activity can handle the ID and decide what to do with it.
An easy value you could use is the resource ID of the EditText itself, the one you pass into findViewById - it's unique, and you can easily use it to set the text on the view itself. You're using a ViewModel here, so it should be updating automatically when you set a value in that, but in general it's a thing you could do.
The difficulty is that you need to store some mapping of IDs to functions in the view model, so you can handle each case. That's just the nature of making the dialog non-specific, but it's easier than making a dialog for each property you want to update! You could make it a when block, something like:
// you don't need the #ResId annotation but it can help you avoid mistakes!
override fun onClick(text: String, #ResId id: Int) {
when(id) {
R.id.coolEditText -> viewModel.setCoolText(text)
...
}
}
where you list all your cases and what to call for each of them. You could also make a map like
val updateFunctions = mapOf<Int, (String) -> Unit>(
R.id.coolEditText to viewModel::setCoolText
)
and then in your onClick you could call updateFunctions[id]?.invoke(text) to grab the relevant function for that EditText and call it with the data. (Or use get which throws an exception if the EditText isn't added to the map, which is a design error you want to get warned about, instead of silently ignoring it which is what the null check does)
I'm following the MVP pattern for my app and I am having confusion about adding the event listeners to the alert dialog and also passing the text view value from alert dialog to activity back. Which is the best approach?.
Use of Listener is the best approach. How? Let see-
I am assuming you are using custom dialog, for this apply these steps to get proper callbacks-
Make an Interface inside your CustomDialog class which will return you the callback event.
Implement that interface in your Presenter/ViewModel class.
Now your Presenter/ViewModel Override that callback method and from here you can use that for your next task.
Pass Presenter/ViewModel reference to your Custom Dialog as the instance of Interface.
Now call the interface method where you want inside your Dialog.
A Quick Code Example(in kotlin)
CustomDialog
class CustomDialog : DialogFragment() {
var listener: Listener? = null
var messageText = ""
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(this.activity)
val rootView = activity?.layoutInflater?.inflate(R.layout.dialog_custom, null)
isCancelable = false
val messageTextView: TextView? = rootView?.findViewById(R.id.messageTextView)
val okButton: Button? = rootView?.findViewById(R.id.okButton)
if (messageText.isNotBlank()) {
messageTextView?.text = messageText
}
okButton?.setOnClickListener {
listener?.customOkClicked()
dismiss()
}
builder.setView(rootView)
return builder.create()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
return super.onCreateView(inflater, container, savedInstanceState)
}
interface Listener {
fun customOkClicked()
}
}
YouPresenter
class MyPresenter: CustomDialog.Listener {
.
.
// other code
override fun customOkClicked() {
// your next step
}
}
YourActivity
class YourActivity: Activity() {
.
.
fun showCustomDialog() {
CustomDialog().apply {
this.listener = presenter // reference of your presenter class.
this.messageText = msg
}.show(supportFragmentManager, "custom_dialog_tag")
}
}
Note: You can apply same approach with the MVVM, do the same thing with your ViewModel in case.
Hope this is the answer you are looking for.
You should LayoutInflater to inflate your dialog into a view. Then you can easily retrive the value as follows :
LayoutInflater layoutInflater = LayoutInflater.from(context);
View view = layoutInflater.inflate(R.layout.dialog, null, false);
EditText textView = view.findViewById(R.id.editText);
String text = textView.getText();
I have an Activity that calls a DialogFragment like this:
private fun showDeleteDetailDialog(itemView: View, categoryId: String, detailId: String) {
val dialog = DeleteDetailDialogFragment.newInstance(categoryId, detailId)
dialog.show(this#DetailsActivity.fragmentManager, "DeleteDetailDialog")
}
And this is the code for my DialogFragment (a click on the PositiveButton deletes an item in Firebase database):
class DeleteDetailDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Use the Builder class for convenient dialog construction
val categoryId = arguments.getString(ARG_CATEGORY_ID)
val detailId = arguments.getString(ARG_DETAIL_ID)
val builder = AlertDialog.Builder(activity)
builder.setMessage(R.string.delete_detail)
.setPositiveButton(R.string.delete, { dialog, id ->
deleteDetail(categoryId, detailId)
})
.setNegativeButton(R.string.cancel, { dialog, id ->
// User cancelled the dialog
})
// Create the AlertDialog object and return it
return builder.create()
}
private fun deleteDetail(categoryId: String, detailId: String) {
// get the detail reference for the specified category
val deleteRef = FirebaseDatabase.getInstance().getReference("details").child(categoryId).child(detailId)
// remove detail
deleteRef.removeValue()
// get the reference for the specified favorite, identified by detailId
val deleteFaveRef = FirebaseDatabase.getInstance().getReference("favorites").child(detailId)
// remove favorite
deleteFaveRef.removeValue()
}
companion object {
private val ARG_CATEGORY_ID = "category_id"
private val ARG_DETAIL_ID = "detail_id"
fun newInstance(categoryId: String, detailId: String): DeleteDetailDialogFragment {
val fragment = DeleteDetailDialogFragment()
val args = Bundle()
args.putString(ARG_CATEGORY_ID, categoryId)
args.putString(ARG_DETAIL_ID, detailId)
fragment.arguments = args
return fragment
}
}
}
When I call the Dialog, the Dialog window pops up. When I then click Cancel (the NegativeButton) the Dialog disappears as expected. When I click Delete (the PositiveButton), the Dialog disappears, again as expected.
BUT, after a successful Delete, when I call the Dialog again, a click on Cancel does not immediately dismiss the dialog; instead, the Dialog box pops up again and only disappears after a second click on Delete. There seems to be an issue with the FragmentManager. What am I missing here?
You should call
getDialog().dismiss()
NOTE
You should create a Custom Dialog within DeleteDetailDialogFragment .
class DeleteDetailDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View?
{
val rootView = inflater.inflate(R.layout.your_layout, container,false)
return rootView
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
return dialog
}
There's nothing wrong with the code above! Removing an item in Firebase database triggered an onDataChange event in my ValueEventListener which then called an updateUI function. Unfortunately, I had my onItemTouchListener handling onItemClick events (like my remove() instruction via DialogFragment) placed there, which worked fine as long as I didn't change any data in my Firebase database. But removing an item there triggered a loop that caused the "erratic" behavior of my code. The solution was to move the call to my onItemTouchListener (that handles onItemClicks) from the updateUI function to the onCreate section of my code. Huh, I learnt something!