I need some help with a problem I'm having with DialogFragments in an Android app. I've been stuck for too long. I'm sure I'm doing something incorrectly, but I don't know what. The best way for me to explain the problem is with a minimal example:
MainActivity.java
Our main activity has a button that shows a dialog.
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void showDialogFragment(DialogFragment fragment) {
// DialogFragment.show() will take care of adding the fragment
// in a transaction. We also want to remove any currently showing
// dialog, so make our own transaction and take care of that here.
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);
// Show the dialog.
fragment.show(ft, "dialog");
}
public void showDialogA(View v) {
showDialogFragment(new DialogFragmentA());
}
}
DialogFragmentA.java
DialogFragmentA is a simple dialog with two buttons. One button cancels, the other shows DialogFragmentB.
public class DialogFragmentA extends AppCompatDialogFragment implements DialogInterface.OnClickListener {
#Override
#NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setMessage("Dialog A")
.setTitle("Dialog A")
.setNegativeButton("Cancel", this)
.setPositiveButton("Go to B", this)
.create();
}
#Override
public void onClick(DialogInterface dialogInterface, int i) {
if (i == DialogInterface.BUTTON_NEGATIVE) {
dismiss();
} else if (i == DialogInterface.BUTTON_POSITIVE) {
dismiss();
((MainActivity) getActivity()).showDialogFragment(new DialogFragmentB());
}
}
}
DialogFragmentB.java
DialogFragmentB is similar. One button takes you back to A.
public class DialogFragmentB extends AppCompatDialogFragment implements DialogInterface.OnClickListener {
#Override
#NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle("Dialog B")
.setMessage("Dialog B")
.setPositiveButton("Do Something", this)
.setNegativeButton("Go to A", this)
.create();
}
#Override
public void onClick(DialogInterface dialogInterface, int i) {
if (i == DialogInterface.BUTTON_NEGATIVE) {
dismiss();
((MainActivity) getActivity()).showDialogFragment(new DialogFragmentA());
} else if (i == DialogInterface.BUTTON_POSITIVE) {
dismiss();
}
}
}
The problem I'm having is that if I go from one dialog to the next, the original dialog is not properly dismissed. For example, from MainActivity:
Click "Show Dialog A".
Click "Go to B".
Click "Do something". This should close all dialogs. But in fact takes you back to dialog A.
There are many other ways to produce similar behavior. I'm sure I've implemented something incorrectly or I'm doing something that's not allowed, but I don't know what or how to fix it. If you want to experiment, I can provide a Github repository with the code from above.
As DigitalNinja pointed out, there was a problem with the way I was creating new dialog fragments. Specifically, most of what I was doing in MainActivity.showDialogFragment() (above) was either unnecessary or wrong.
This revised version of MainActivity.showDialogFragment() fixes all the problems I was having:
public void showDialogFragment(DialogFragment fragment) {
fragment.show(getSupportFragmentManager(), "dialog");
}
Related
At first my main problem was at how to call a method from the same class, even tough I think I found a way to do this, it's not working as I expected, and I would like to know what would be the best approach to my case.
This is the code I'm working on:
public class EscolhaAtendimento extends AppCompatActivity {
private ViewPager mSlideViewPager;
private LinearLayout mDotLayout;
String TAG = "TasksSample";
private TextView[] mDots;
private SliderAdapter sliderAdapter;
Dialog myDialog;
#Override
public void onCreate (Bundle SavedInstanceState){
super.onCreate(SavedInstanceState);
setContentView(R.layout.escolha_atendimento);
mSlideViewPager = findViewById(R.id.slideViewPager);
mDotLayout = findViewById(R.id.dotsLayout);
sliderAdapter = new SliderAdapter(this);
mSlideViewPager.setAdapter(sliderAdapter);
addDotsIndicator(0);
mSlideViewPager.addOnPageChangeListener(viewListener);
myDialog = new Dialog(this);
}
public void addDotsIndicator(int position){
mDots = new TextView[8];
mDotLayout.removeAllViews();
for (int i= 0; i < mDots.length; i++){
mDots[i] = new TextView(this);
mDots[i].setText(Html.fromHtml("•"));
mDots[i].setTextSize(35);
mDots[i].setTextColor(getResources().getColor(R.color.colorTransparentWhite));
mDotLayout.addView(mDots[i]);
}
if (mDots.length > 0){
mDots[position].setTextColor(getResources().getColor(R.color.colorWhite));
}
}
ViewPager.OnPageChangeListener viewListener = new ViewPager.OnPageChangeListener(){
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected (int i) {
switch (i) {
case 0: {
myDialog.show();
}
addDotsIndicator(i);
}
}
#Override
public void onPageScrollStateChanged(int state) {
}
};
public void ShowPopup(View v) {
TextView txtclose;
//Button btnFollow;
myDialog.setContentView(R.layout.pop_upfinal);
txtclose = myDialog.findViewById(R.id.txtclose);
txtclose.setText("X");
//btnFollow = (Button) myDialog.findViewById(R.id.btnfollow);
txtclose.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myDialog.dismiss();
}
});
myDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
myDialog.show();
}
}
This class is an activity that on user swipe, the text and image from the buttons will change, even though their IDs will stay the same. (That's controlled by another class, it's working well).
Now, I wanted the image button on the activity do something different depending on which page is selected, and that's why there is a initial switch on the onPageSelected method, inside the Page change listener. The image button in the layout has the android:onClick="ShowPopup" tag, and I guess that also complicates things for me, if I wanted it to do something different in that same activity? Also, calling it that way on the switch, every time I change pages, and go back to the first one the popup window will open, since my call is explicit there. (As I said, even tough I found a way to somehow call my method, or at least it's result, it's not working as I expected).
Edit
I tried then changing it like this, so that the button wouldn't rely on the android:onClick="ShowPopup" Tag, and also wouldn't need to call a void method directly on the switch:
Added
public ImageButton popupchoice;
And also this to onCreate method:
popupchoice = this.findViewById(R.id.imgslide1);
Inside the switch I called it like this to get the button ID:
popupchoice.setOnClickListener(image1);
And set the View.OnClickListener like this:
View.OnClickListener image1 = new View.OnClickListener() {
public void onClick(View v) {
TextView txtclose;
//Button btnFollow;
myDialog.setContentView(R.layout.pop_upfinal);
txtclose = myDialog.findViewById(R.id.txtclose);
txtclose.setText("X");
//btnFollow = (Button) myDialog.findViewById(R.id.btnfollow);
txtclose.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myDialog.dismiss();
}
});
myDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
myDialog.show();
}
};
But that returns me:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setOnClickListener(android.view.View$OnClickListener)' on a null object reference at .EscolhaAtendimento$1.onPageSelected(EscolhaAtendimento.java:81) Line 81 is the one inside the switch with the popupchoice.setOnClickListener(image1).
This error happens on page change, when coming back to the first Page, and also the button click won't work anymore.
I think you can use
EscolhaAtendimento.this.ShowPopup from inside your switch.
Hey fellow stackoverflowers!!!
I'm wondering what the best way to pass a string taken from a Dialog Fragment based on user input on the Dialog into the main activity which called the string?
Here's my specific example but it's really long so if you don't feel like going through it don't worry about everything below.
Here's my source code, I've ommitted the imports n stuff
public class GroupNameFragment extends AppCompatDialogFragment {
private EditText edittGroupName;
public static String GROUP_NAME = "com.example.mashu.walkinggroup.controller - groupName";
// When the views are inflated, get access to them
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
edittGroupName = Objects.requireNonNull(getView()).findViewById(R.id.edittGroupName);
}
#NonNull
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Get reference to fragment's layout
View view = LayoutInflater.from(getActivity())
.inflate(R.layout.group_name_layout, null);
// OK button listener
DialogInterface.OnClickListener listener = (dialog, which) -> {
if (which == DialogInterface.BUTTON_POSITIVE) {
// If OK pressed, create bundle to be accessed in OnDismissListener in MapActivity,
// which contains the groupName user inputted
String groupName = edittGroupName.getText().toString();
Bundle bundle = new Bundle();
bundle.putString(GROUP_NAME, groupName);
setArguments(bundle);
}
};
// Build alert dialog
return new AlertDialog.Builder(getActivity())
.setTitle("Choose your Group Name!")
.setView(view)
.setPositiveButton(android.R.string.ok, listener)
.create();
}
// Extracts groupName from the bundle set up in the onClickListener above
public static String getGroupName(GroupNameFragment dialog) {
Bundle bundle = getArguments();
return bundle.getString(GROUP_NAME);
}
}
What I attempted to do was to this: First, I get access to the EditText that the user will type in their response. Then I set the Dialog Listener for the OK button which creates a bundle using the setArguments function which contains the groupName when the user is done, which will be accessed in the other activity later on by using the static getGroupName function. Here's the function in the main activity which creates the Dialog and sets the onDismissListener
private void createGroupNameDialog() {
// Instantiate Dialog
// Support Fragment Manager for backwards compatibility
FragmentManager manager = getSupportFragmentManager();
GroupNameFragment dialog = new GroupNameFragment();
dialog.show(manager, "GroupNameDialog");
// OnDismissListener callback function to be run whenever dialog dismissed.
dialog.getDialog().setOnDismissListener(new DialogInterface.OnDismissListener() {
#Override
public void onDismiss(DialogInterface dialogInterface) {
// Update groupName based on what user inputted and update marker name at origin
groupName = GroupNameFragment.getGroupName(dialog);
originMarker.setTitle(groupName);
}
});
}
I think the problem is in groupName = GroupNameFragment.getGroupName(dialog). I feel like theres a better way to get the bundle here, and it seems weird to use the function as static and then pass in specific instance of GroupNameFragment in order to get the bundle (wouldn't that instance be gone by then since it's being used in the "OnDismiss"?). Also, the app crashes the second createGroupNameDialog is called, but it doesn't crash and actually opens the dialog window if I comment out the OnDismissListener, so I'm sure the problems in there somewhere but I don't know why it crashes before the dialog box even opens since OnDismiss happens AFTER the user dismisses the Dialog Box.
Thanks!!!
I accomplished passing variables back using an interface and listeners. I'll show you how I handled it (although I used a DialogFragment, this should still work for AlertDialogs, and in this example I passed an integer, not a string, but it would work for any data type).
public class DialogFragmentOtherMedia extends DialogFragment {
int dialogResult;
//The interface is important!
public interface YesNoListener {
void onYesOtherMedia(int output);
void onNoOtherMedia(int output);
}
//Checking for ClassCastException is nice here.
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (!(activity instanceof YesNoListener)) {
throw new ClassCastException(activity.toString() + " must implement YesNoListener");
}
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
dialogResult = 0;
final String mediaType[] = {getString(R.string.Ringtones),getString(R.string.Music),getString(R.string.Alarms)};
return new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.Select_Other_Media_Type))
.setSingleChoiceItems(mediaType, dialogResult, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
//Log.d("DialogFragmentOtherMedia.onCreateDialog","Item clicked: " + mediaType[which]);
dialogResult = which;
}
})
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
//Casting the activity to YesNoListener is very important here!
//You'll register the listener in the activity later, by implementing the interface.
((YesNoListener) getActivity()).onYesOtherMedia(dialogResult);
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
//Same thing for your other callbacks.
((YesNoListener) getActivity()).onNoOtherMedia(dialogResult);
}
})
.create();
}
}
Then you just need to implement it in your activity where you called the dialog from:
public class AlarmDetailsActivity extends Activity
DialogFragmentOtherMedia.YesNoListener {
//All of your activity stuff here...
#Override
public void onYesOtherMedia(int result) {
Log.i("Tag", "onYes Result: " + result);
}
#Override
public void onNoOtherMedia(int result) {
Log.i("Tag", "onNo Result: " + result);
}
}
Sorry about all of the random strings and extra alert dialog. I just wanted to show some actual working code from my app. I tried to add comments next to the important stuff. Hope this helps!
I have an activity that has 3 fragments on it with Tabs, one of them is called "TaskFragment".
In my main Activity i only load the fragments.
In TaskFragment i have a RecyclerView that is working fine and is showing the items as intended.
The problem comes, when i insert data using a DialogFragment, because it does insert data (i am using DbFlow ORM), but it does not (of course) refresh the adapter since it is in the TaskFragment fragment inside the DetailMainActivity activity as i said.
I have tried to use onResume() and onPause() in order to refresh the adapter, but they are never called since the activity does not get paused or in onresume for a DialogFragment.
I have tried aswell to use an interface, but it does not work and i have searched all over stackoverflow and google with no luck.
I leave here some of my code for you to understand better:
DetailMainActivity.java
Here in the onClick interface i show the DialogFragment to the user to input the information.
FragmentManager fm = getSupportFragmentManager();
AddSimpleTask sptask = new AddSimpleTask();
sptask.show(fm, "tag");
TaskFragment.java
In this fragment i have my RecyclerView
private void setupRecyclerView() {
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
mRecyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (DetailMainActivity.FAB_Status) {
DetailMainActivity.hideFAB();
DetailMainActivity.FAB_Status = false;
}
return false;
}
});
}
private void setupAdapter() {
adapter = new DetailMainTaskAdapter(simpleTaskList, this);
}
AddSimpleTask
And this is my DialogFragment. I have set a setOnShowListener() in order to avoid the DialogFragment to get dismiss early.
#Override
public void onShow(DialogInterface dialogInterface) {
final AlertDialog dialog =(AlertDialog) getDialog();
if (dialog != null){
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
positiveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mEditTextName.getText().toString().trim().isEmpty() ||
mEditTextContent.getText().toString().trim().isEmpty() ) {
if (mEditTextName.getText().toString().trim().isEmpty()) {
mEditTextName.setError("Can not be empty");
}
if (mEditTextContent.getText().toString().trim().isEmpty()) {
mEditTextContent.setError("Can not be empty");
}
}else {
presenter.beingInsertion(mEditTextName.getText().toString().trim(), mEditTextContent.getText().toString().trim()
, foreignId);
}
}
});
negativeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dismiss();
}
});
}
}
If the insert is successfully achieved the onInsertSuccess method is called (i am using MVP)
#Override
public void onInsertSuccess() {
Snackbar.make(getActivity().findViewById(R.id.containerMainDetail), "Actividad agregada", Snackbar.LENGTH_SHORT).show();
dismiss();
}
I have called adapter.notifyDataSetChanged() in many places, and i also tried with a custom interface, but i can not make this work.
Sorry for the long post, but thanks in advance for your help.
There are some errors in your statement but I'll get to that later. notifyDataSetChanged() only notifies the adapter that the underlying list (or array) has changed. The implication is that you first need to requery your database and obtain the new list before calling notifyDataSetChanged() on the adapter else there is no point as the underlying list will still be the same and it will not update the adapter.
The correct way of calling this will be through your custom listener interface and not in the onPause()/onResume() callbacks as there is the possibility that the user does not enter a value and hence you will unnecessarily be querying the database. In your custom listener interface implementation, first update the list with the new data from the DB and then notify the adapter.
Which leads to the error in assumption that onPause()/onResume() callbacks do not happen when your Activity is covered by a DialogFragment - this is incorrect. The moment the activity view is even partially covered, the onPause() callback is triggered.
I used the documentation here to create a dialogfragment. The code is :
public static MyAlertDialogFragment newInstance(int title) {
MyAlertDialogFragment frag = new MyAlertDialogFragment();
Bundle args = new Bundle();
args.putInt("title", title);
frag.setArguments(args);
return frag;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
int title = getArguments().getInt("title");
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.alert_dialog_icon)
.setTitle(title)
.setPositiveButton(R.string.alert_dialog_ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
((FragmentAlertDialog)getActivity()).doPositiveClick();
}
}
)
.setNegativeButton(R.string.alert_dialog_cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
((FragmentAlertDialog)getActivity()).doNegativeClick();
}
}
)
.create();
}}
the dialogfragment here is associated with only the activity FragmentAlertDialog. Is there any way to associate it with multiple activities? I have my calling activity name in onCreateDialog by passing it through setArguements(). Any way to use it ? I checked this question and was hoping for a confirmation/better way.
Instead of having a FragmentAlerDialog activity, you could define somewhere an interface (by somewhere I mean either a public static interface in DialogFragment class, or a separate public interface Java file), and any Activity that wishes to display the dialog could implement this interface.
One common practice that I use is to have a root Activity for all my project activities. Make that root activity implement that interface and then you can display that DialogFragment from anywhere.
I'll just post what edits I made in my code, all credits to #gunar,
create new DialogImplement.java as:
package com.example.test;
public interface DialogImplement
{
public void doPositiveClick();
}
Add #Override in activity code before the implementation of doPositiveClick(), for eg:
#Override
public void doPositiveClick()
{
//do what you want to do
}
make sure your activity implements DialogImplement, and modify code in the question as:
((DialogImplement)getActivity()).doPositiveClick(); //Or negative click code
Hope this helps..Cheers :]
I have a simple AlertDialog that displays a list of some items and upon clicking one of them, the clicked item is passed back to the enclosing Activity. I also want to perform some default handling when the user cancels the dialog (using the back button) - more specifically, I want to pass an empty string to the activity in such case.
However, if I put the dialog in a DialogFragment (from the compatibility package), the OnCancelListener is not called when I close the dialog with the back button. What am I doing wrong?
public class SelectItemDialog extends DialogFragment {
public interface Callback {
void onItemSelected(String string);
}
private static final String ARG_ITEMS = "items";
private Callback callback;
public static SelectItemDialog fromItems(Collection<String> items) {
SelectItemDialog fragment = new SelectItemDialog();
fragment.setArguments(newArguments(items));
return fragment;
}
private static Bundle newArguments(Collection<String> items) {
Bundle arguments = new Bundle();
arguments.putStringArray(ARG_ITEMS, items.toArray(new String[items.size()]));
return arguments;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (Callback) activity;
}
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final String[] items = getArguments().getStringArray(ARG_ITEMS);
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.dialog_select_email_title)
.setItems(items, new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
callback.onItemSelected(items[which]);
}
})
.setOnCancelListener(new OnCancelListener() {
#Override
public void onCancel(DialogInterface dialog) {
// this code is not executed
callback.onItemSelected("");
throw new RuntimeException("dialog cancelled");
}
})
.create();
}
}
It might have to do with the fact that there is no explicit call to cancel() from your code.
The OnCancelListener documentation says:
This will only be called when the dialog is canceled
Which probably needs an explicit cancel() call.
Either make a positive/negative button with a OnClickListener that calls DialogInterface#cancel() or use a OnDismissListener() with an extra check to see if a list item was clicked.
Also, to listen for a back keypress and cancel the dialog, you can set up an OnKeyListener, like outlined in this SO answer
Also, once you have the Dialog set up, it would also be a good idea to use Dialog#setCanceledOnTouchOutside() in case the the user taps outside the Dialog.
Edit: The below part is the easy way to handle cancel events in a DialogFragment.
Since you are using a DialogFragment, this class has a very handy method, DialogFragment#onCancel() which gets called when the DialogFragment is cancelled. Do your logic in there.
DialogFragments are more complex, with a slightly different lifecycle than normal dialogs. Therefore, first check the documentation if you have a certain Dialog-based approach that you are trying to port to a DialogFragment, some methods may exist that allow your new implementation to function properly!
If you are using DialogFragment and want to listen back button then use this -
this.getDialog().setOnKeyListener(new Dialog.OnKeyListener() {
#Override
public boolean onKey(DialogInterface dialog, int keyCode,
KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (****) {
your logic
}
return true;
}
return false;
}
});
Note: DialogFragment own the Dialog.setOnCancelListener and Dialog.setOnDismissListener callbacks. You must not set them yourself.
To find out about these events, override onCancel(DialogInterface) and onDismiss(DialogInterface).
public class SelectItemDialog extends DialogFragment {
#Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
//your code hear
dialog.cancel();
}
}
And you should remove .setOnCancelListener()
Actually if you want to use DialogFragment, you can never add OnCancelListener or OnDismissListener to it, since the Dialog Fragment owns callbacks to these methods!
You have 3 options here:
1- go with regular dialogs.2- set your dialog fragment to cancellable(false) and add a cancel button to the dialog.3- check #Nikhil Pingle answer.
this is from the documentation of the Dialog Fragment
* <p><em>Note: DialogFragment own the {#link Dialog#setOnCancelListener
* Dialog.setOnCancelListener} and {#link Dialog#setOnDismissListener
* Dialog.setOnDismissListener} callbacks. You must not set them yourself.</em>
* To find out about these events, override {#link #onCancel(DialogInterface)}
* and {#link #onDismiss(DialogInterface)}.</p>
Cancel Listener or Dismiss listener in DialogFragment can achieve by onDismiss
DialogFragment newFragment = new DatePickerFragment();
newFragment.show(getFragmentManager(), "datePicker");
newFragment.onDismiss(new DialogInterface(){
#Override
public void cancel() {
// TODO Auto-generated method stub
}
#Override
public void dismiss() {
// TODO Auto-generated method stub
}
});