How to get the current fragment when using FragmentStatePagerAdapter - java

It is surprisingly difficult to get the current fragment when using either of the pager adapters. With the FragmentPagerAdapter, however, you can look for a fragment with the tag "android:switcher:" + viewId + ":" + id.
Unfortunately, there does not seem to be a standard tag for the FragmentStatePagerAdapter. A related question provided a couple answers which suggested manually keeping a cache of the fragments, which were noted as being inadequate when doing a rotation: the underlying adapter stores state in a bundle and restores it when it is created, causing any simple caching solution to fail.

I found a better solution. getCurrentFragment() cannot be implemented correctly from what I can tell.
My code was previously launching a dialog and then calling back to the Activity which was stored by the dialog at onAttach. The Activity then needed to find the correct Fragment, which was problematic.
The correct solution is to first call setTargetFragment() on the new dialog fragment:
SelectProblemDialogFragment f = SelectProblemDialogFragment.newInstance(args);
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), "select_problem_dialog_fragment");
and then in onAttach(), simply use that as the listener.
#Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
try
{
mListener = (SelectProblemDialogListener) getTargetFragment();
if (mListener == null)
{
mListener = (SelectProblemDialogListener) activity;
}
} catch (ClassCastException e)
{
throw new ClassCastException("Must implement SelectProblemDialogListener");
}
}

Related

How can I turn off localbroadcast listener in recyclerview adapter? Of is there a better way to communicate with an activity called from it?

I'm new to SO and fairly new to coding, so please accept my apologies in advance if I break rules or expectations here.
I have an unusual setup involving two recyclerViews, which I'll explain here and also paste a simplified version of the code below (as there is so much not relevant to this question).
In what I'll call verticalRecyclerViewActivity, a verticalRecyclerViewAdapter is called, with data it fetches from Firebase and loads into arrayLists.
If the user clicks on an item in the vertical recyclerview, a new dialog fragment which I'll call horizontalRecyclerViewDialogFragment is inflated, and that loads what I'll call horizontalRecyclerView (which has similar items to the vertical one, in more detail, with options to click on them to review them).
If the user clicks on an item in the horizontalRecyclerView, a new activity which I'll call reviewItem is started (through an Intent). When the user submits their review, it finishes and returns (through the backstack) to the horizontal RecyclerView. That can also happen if they press the back button without actually submitting a review. That all works fine, but I need the horizontalRecyclerView to show that they have (or haven't) reviewed the item and state the score they gave it in a review.
Calling notifyDataSetChanged won't work for this because of how information comes through two recyclerViews and Firebase calls (or, at least, it would be very inefficient).
I've tried using startActivityForResult (I know it's deprecated, but if I could get that to work I could try using the newer equivalent which I don't yet understand) but the problem is that the result is returned to the original (VerticalRecylcerView) activity, which is two recyclerView adapters and one fragment beneath what needs to be updated, and I don't know how to pass that data to the horizontal Recyclerview.
I've also tried using interfaces but was unable to pass it through the Intent (tried using Parcelable and Serializable, but it seems neither can work in this situation?).
Since the review is updated on Firebase, I could have the horizontal Recyclerview listen for a change, but that seems very inefficient?
So I've found a solution using localBroadcast (which I know is also deprecated). The Intent (with the review score) is transmitted when it is reviewed and received in the horizontal recyclerView adapter. But when and how should I unregister the adapter? Ideally the receiver would be turned on when the user goes to the Review activity and turned off once the user returns from that activity and the (horizontal) recyclerView holder is updated, whether the review is successfully submitted or whether the user just presses the back button and never submits a review.
My question is similar to this one: How to unregister and register BroadcastReceiver from another class?
That is noted as a duplicate of this one: How to unregister and register BroadcastReceiver from another class?
There's a lot in those questions I don't understand, but the important difference I think between their and my cases is that I would just like the receiver to know when a review is submitted, and ideally be unregistered then, or possibly when the viewHolder is recycled, which I tried but also didn't work since it's not connected to the viewHolder (should it be?).
Any help would be much appreciated, thank you!
public class verticalRecyclerViewActivity extends AppCompatActivity {
// Loads an XML file and assembles an array from Firebase.
mVerticalRecyclerView = findViewById(R.id.verticalRecyclerView);
verticalRecyclerViewAdaptor mVerticalRecyclerViewAdaptor = new verticalRecyclerViewAdaptor (this); // also pass other information it needs
mVerticalRecyclerView .setAdapter(mVerticalRecyclerViewAdaptor);
}
public class verticalRecyclerViewAdaptor extends RecyclerView.Adapter<verticalRecyclerViewAdaptor.singleHolder> {
// Usual recyclerView content
holder.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
horizontalRecyclerViewFragment mHorizontalRecyclerViewFragment = new horizontalRecyclerViewFragment();
// lots of arguments passed it needs.
FragmentManager fragmentManager = ((FragmentActivity) view.getContext()).getSupportFragmentManager();
mHorizontalRecyclerViewFragment.show(fragmentManager, null);
}
public class mHorizontalRecyclerViewFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity().getApplicationContext(); // Not sure why I need this, but it works.
View horizontalRecyclerViewView = getActivity().getLayoutInflater().inflate(R.layout.horizontal_recyclerview_holder, new CardView(getActivity()), false);
Dialog horizontalRecyclerViewDialog = new Dialog(getActivity());
horizontalRecyclerViewDialog.setContentView(horizontalRecyclerViewView);
mHorizontalRecyclerView = horizontalRecyclerViewView.getRootView().findViewById(R.id.horizontalRecyclerView);
mHorizontalRecyclerViewAdapter = new horizontalRecyclerViewAdapter (mContext)
// Other arguments passed
mHorizontalRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity(),
LinearLayoutManager.HORIZONTAL, false));
mHorizontalRecyclerView.setAdapter(mHorizontalRecyclerViewAdapter);
}
public class horizontalRecyclerViewAdapter extends RecyclerView.Adapter<horizontalRecyclerViewAdapter.horizontalRecyclerViewHolder> {
public horizontalRecyclerViewAdapter(){}
// Blank constructor and also one with lots of arguments for it to work.
public horizontalRecyclerViewAdapter.horizontalRecyclerViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.horizontal_recyclerview_adaptor_holder, parent, false);
return new horizontalRecyclerViewAdapter.horizontalRecyclerViewHolder(view);
}
public void onBindViewHolder(#NonNull horizontalRecyclerViewHolder holder, int position) {
// Connect up various views.
holder.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
LocalBroadcastManager.getInstance(mContext).registerReceiver(reviewSubmittedListener, new IntentFilter("reviewSubmitted"));
Intent reviewNow = new Intent(view.getContext(), ReviewActivity.class);
// Put extra details with the intent
view.getContext().startActivity(reviewNow);
}
BroadcastReceiver reviewSubmittedListener = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent reviewFinishedIntent) {
int reviewScore = reviewFinishedIntent.getExtras().getInt("reviewScore");
// Update the horizontal RecyclerView with the information received from the review Activity.
}
};
}
public class ReviewActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_review_item);
// Set up the review, using Firebase and data passed through the intent.
}
public void submitReview() {
// Check that the review is complete/valid and submit it through Firebase
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(ReviewItemActivity.this);
Intent reviewFinishedIntent = new Intent("reviewSubmitted");
reviewFinishedIntent.putExtra("reviewScore", overallScore);
LocalBroadcastManager.getInstance(this).sendBroadcast(reviewFinishedIntent);
finish();
}
If you are using RxJava you can use the RxBus else you can use one of many EventBus implementation for this.
If that is not the path you want to take then you can have a shared view model object that can be used only for communication between fragments see this article.

Keeping objects onPause()

I'm having a lot of trouble with Android Fragments, and I don't know why.
I want to keep an object in memory whenever I pause the app (onPause()).
I have a MapView with some layers. And I want to keep those layers, so the map doesn't have to reload from scratch and recreate them whenever I pause and resume the app.
The thing is, I can't save them with onSaveInstanceState(). They are objects, not primitives. They are also from a library, so I can't make them implement Serializable or Parcelable.
I want to find a solution without resorting to:
android:configChanges="orientation|screenSize">
to my Activity, since it is considered bad practice. Also, it doesn't work for when you pause and resume the app.
I also tried
this.setRetainInstance(true); // retain this fragment
on my fragment's onCreate(), but I would also like to avoid it. It doesn't work anyway.
After much trial, error and testing, I found out that my fragment's onSaveInstanceState() doesn't even run. I can't even keep a simple boolean whenever I pause and resume the app.
I read:
Saving Android Activity state using Save Instance State
https://developer.android.com/training/basics/activity-lifecycle/recreating
savedInstanceState is always null
Amog many other posts and, of course, the Android documentation.
Some of my code:
Creating the fragment:
navFragment = FragNavigation.newInstance(); // keeping the reference to the navFragment
// setting up the viewpager
ViewPager viewPager = findViewById(R.id.vpMainViewPager);
MainPagerAdapter mainAdapter = new MainPagerAdapter(getSupportFragmentManager(), this);
mainAdapter.addFragment(navFragment);
viewPager.setAdapter(mainAdapter);
Trying to save a simple boolean in the instance state (remember: this doesn't even run):
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
Log.e(TAG, "onSaveInstanceState: is map loaded? " + mapIsLoaded );
outState.putBoolean("mapIsLoaded", mapIsLoaded);
}
And trying to load it:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRetainInstance(true); // retain this fragment
Log.e(TAG, "onCreate: null? " + (savedInstanceState == null) );
if (savedInstanceState != null)
mapIsLoaded = savedInstanceState.getBoolean("mapIsLoaded", false);
}
What am I missing? I'm clearly not understanding something. Why is it so hard to keep values when you pause the app?
I would suggest storing the MapView layers using a ViewModel as per Architecture Components. If that is not an option, you can manually store and retrieve objects using the Activities getLastNonConfigurationInstance.
#Override
public void onPause() {
/**
* store your data
*/
super.onPause();
}
#Override
public void onResume() {
/**
* retrieve your data
*/
super.onResume();
}

DialogFragment (support v4) crashes the app (NPE)

I already checked a lot of same questions on StackOverflow, but I didn't find any solution to my issue.
In a DialogFragment, I call an AsyncTask method and when the result has been received from the server, I launched another DialogFragment.
Here is the code I use to launch the DialogFragment :
public class RequesterConfirmRent extends DialogFragment {
// Called from onPostExecute() in AsyncTask class.
public void onPostComputeAmountToPay(JSONArray array){
double amountToPay = 0.0;
String ownerName = "";
try{
if(!array.getJSONObject(0).getBoolean("success"))
Log.e("Error", "Error with JSON received");
else {
amountToPay = array.getJSONObject(1).getDouble("amountToPay");
ownerName = array.getJSONObject(2).getString("ownerName");
}
}catch(JSONException e){
Log.e(e.getClass().getName(), "JSONException", e);
}
// Create and show the DialogFragment
Paiement p = new Paiement();
Bundle bdl = new Bundle();
bdl.putString("ownerName", ownerName);
bdl.putDouble("amountToPay", amountToPay);
p.setArguments(bdl);
// Buggy line (NPE)
p.show(getFragmentManager(), "4554");
}
}
And here is the code of the DialogFragment I try to display:
public class Paiement extends DialogFragment {
#NonNull
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if(getDialog() == null)
super.setShowsDialog(false);
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setTitle("Synthesis of your rent");
return dialog;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_paiement, container, false);
init(rootView);
return rootView;
}
private void init(View v){// Bla bla ...}
}
And I alway got a NullPointerException when I call the .show() method?
What did I do wrong?
Many thanks for your help!
EDIT 1 : As requested, here is the LogCat
05-11 09:58:34.470 31384-31384/com.example.celien.drivemycar
E/AndroidRuntime﹕ FATAL EXCEPTION: main
java.lang.NullPointerException
at android.support.v4.app.DialogFragment.show(DialogFragment.java:136)
at com.example.celien.drivemycar.fragment.RequesterConfirmRent.onPostComputeAmountToPay(RequesterConfirmRent.java:148)
EDIT 2 I modified the code like this, and it appears that getFragmentManager() is null. Why?
Paiement p = new Paiement();
Bundle bdl = new Bundle();
bdl.putString("ownerName", ownerName);
bdl.putDouble("amountToPay", amountToPay);
p.setArguments(bdl);
// BUGGY LINE
android.support.v4.app.FragmentManager f = getFragmentManager();
if(p == null)
Log.d("Exception ", "p is null");
if(f == null)
Log.d("Exception ", "f is null");
try {
p.show(f, "4554");
}catch(NullPointerException e){
Log.d("Exception ", e.toString());
}
EDIT 3:
Got some fresh infos! To avoid the creation of this Dialog, I display data in a Toast: Toast.makeText(this.getActivity(), "You have to pay "+amountToPay+"e to " +ownerName, Toast.LENGTH_LONG).show(); and also get a NPE!
BUT if I use the Log system, everything's fine :
Log.d("Rcvd ", String.valueOf(amountToPay));
Log.d("Rcvd ", ownerName);
So, why is my activity null?
If you get a NPE when calling p.show() but not p.setArguments(), it could be that p is ok but something inside the show call isn't?
On possible point to solve is that you're using a support version of FragmentManager, with the getFragmentManager() call. Try the getSupportFragmentManager() instead. It will fall back to the proper one when needed.
On the other hand, you're calling android.support.v4.app.FragmentManager. If you manually added the package, it's weird, so chances are your IDE did it for you. You could try to remove the package behind FragmentManager, and hope for the code be compliant to the non-support standard framework. Could be that only this reference to the support library is done, so removing the package part would solve the issue.
My advice: In an app, always stick to either the standard framework or the support library when defining activities and fragments. Because of that, make sure that every Activity and Fragment you create extends a proper support (or standard framework) version. Mixing them will end up with unexpected crashes.
Also, as mentioned in one of my comments, AsyncTask runs freely even after your fragment was detached, so no activity is properly referenced by this fragment anymore. This answer tells you to check if Fragment was detached by looking at isDetached(). Check for his answer. He's talking about using Loaders instead of AsyncTasks or move the AsyncTask up to the activity, so the activity is always available. Looking at the future, Loader is the best option (since it's the natural evolution of AsyncTask), looking at the present, try to move the AsyncTask up to the common Activity.

Refreshing a view inside a fragment

I have searched the numerous questions that look like this one, but haven't found my answer in any of them.
I have an activity that has 3 tabs accessible through the action bar. I achieved this by adding 3 fragments that inflate a custom view I made extending the view class.
At the moment the database changes, I try to refresh the view in my tab by calling invalidate()/postinvalidate(), but this does not work. The same is true for calling onCreateView of the fragment just as many other options I considered.
When I go to another tab and go back, however, the change has been made and my view is updated as it should be.
How can I simulate the same thing that happens when changing to another tab? What does happen. I tried to look at the Fragment lifecycle (tried to call onCreateView()) to figure it out but it just doesn't want to refresh/redraw as it should.
The data is loaded properly, as the data is changed when I change to another tab.
I deleted some of the code as it is no longer relevant. I implemented Cursorloaders instead of my own Observer pattern to notify a change. This is my main activity right now.
The question is what should I do now if I want to redraw the view inside these fragments. If I apply fragmentObject.getView().invalidate() it does not work. I'm having the same problem as before, but now my Observer to notify a change in the database is properly implemented with loaders.
public class ArchitectureActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ActionBar actionbar = getActionBar();
actionbar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
ActionBar.Tab EditTab = actionbar.newTab().setText("Edit");
ActionBar.Tab VisualizeTab = actionbar.newTab().setText("Visualize");
ActionBar.Tab AnalyseTab = actionbar.newTab().setText("Analyse");
Fragment editFragment = new EditFragment();
Fragment visualizeFragment = new VisualizeFragment();
Fragment analyseFragment = new AnalyseFragment();
EditTab.setTabListener(new MyTabsListener(editFragment));
VisualizeTab.setTabListener(new MyTabsListener(visualizeFragment));
AnalyseTab.setTabListener(new MyTabsListener(analyseFragment));
actionbar.addTab(EditTab);
actionbar.addTab(VisualizeTab);
actionbar.addTab(AnalyseTab);
ArchitectureApplication architectureApplication = (ArchitectureApplication)getApplicationContext();
architectureApplication.initialize();
getLoaderManager().initLoader(0, null, this);
getLoaderManager().initLoader(1, null, this);
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id == 0){
return new CursorLoader(this, GraphProvider.NODE_URI , null, null, null, null);
} else if (id == 1){
return new CursorLoader(this, GraphProvider.ARC_URI , null, null, null, null);
}
return null;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// Reloading of data, actually happens because when switching to another tab the new data shows up fine
Log.e("Data", "loaded");
}
public void onLoaderReset(Loader<Cursor> loader) {
}
}
Don't try to call onCreateView() yourself... it's a lifecycle method and should be called only by the framework.
Fragments are re-usable UI components. They have their own lifecycle, display their own view, and define their own behavior. You usually don't need to have your Activity mess around with the internal workings of a Fragment, as the Fragment's behavior should be self-contained and independent of any particular Activity.
That said, I think the best solution is to have each of your Fragments implement the LoaderManager.LoaderCallbacks<D> interface. Each Fragment will initialize a Loader (i.e. a CursorLoader if you are using a ContentProvider backed by an SQLite database), and that Loader will be in charge of (1) loading the data on a background thread, and (2) listening for content changes that are made to the data source, and delivering new data to onLoadFinished() whenever a content change occurs.
This solution is better than your current solution because it is entirely event-driven. You only need to refresh the view when data is delivered to onLoadFinished() (as opposed to having to manually check to see if the data source has been changed each time you click on a new tab).
If you are lazy and just want a quick solution, you might be able to get away with refreshing the view in your Fragment's onResume() method too.
I had a similar (although not identical) problem that I could solve in the following way:
In the fragment I added
public void invalidate() {
myView.post(new Runnable() {
#Override
public void run() {
myView.invalidate();
}
});
}
and I called this method from the activity when I wanted to refresh the view myView of the fragment. The use of post() ensures that the view is only invalidated when it is ready to be drawn.
I've found a workaround to refresh the view inside my fragment. I recreated (new CustomView) the view every time the database has been updated. After that I call setContentView(CustomView view). It looks more like a hack, but it works and nothing else that I tried does.
Although my problem was not actually solved, I gave the reward to Alex Lockwood. His advice on Loaders made my application better and it caused me to keep looking for a solution that I eventually found.
I had the same issue.
My solution was detach fragment and attach it again.
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
Fragment f = getFragment(action);
if(forceUpdate)
{
fragmentTransaction.detach(f);
fragmentTransaction.attach(f);
}
fragmentTransaction.replace(R.id.mainFragment, f);
fragmentTransaction.commit();
currentAction = action;
The fastest solution working for me:
#Override
public void onPause() {
super.onPause();
if (isRemoving() && fragmentView != null) {
((ViewGroup) fragmentView).removeAllViews();
}
}

Fragment onResume doesn't get called after Fragment is being detached and then re-attached

I'm trying to get the handle on all the new ActionBar and Fragments API.
I have an main activity, and I want it to manage two different tabs.
I'm using the ActionBarSherlock in order to support older version than ICS.
Each tab contains its own Fragment (each one is a subclass of SherlockListFragment)
I got it to work basically nice, but I have a problem that I'm sure that is stupid, but I can't figure it out yet:
On the first time each Fragment is shown, everything is OK, the list is populated and so the MenuItems in the ActionBar.
But the second time you see a tab (After swicth and switch-back), Neither the list get populated, nor the ActionBar MenuItems.
This is how I'm switching the tabs:
#Override
public void onTabSelected(Tab tab, FragmentTransaction transaction) {
SherlockListFragment toAttach = // Find the right fragment here...
if (toAttach != null) {
if (toAttach.isAdded() == false) {
transaction.add(R.id.tab_placeholder, toAttach,
REMINDER_FRAGMENT_TAG);
} else {
transaction.attach(toAttach);
}
}
}
And onTabUneselect I'm detaching the Fragment:
#Override
public void onTabUnselected(Tab tab, FragmentTransaction transaction) {
SherlockListFragment toDetach = // Find the right fragment
if (toDetach != null) {
transaction.detach(toDetach);
}
}
I'm populating the lists and the ActionBar menu in onResume:
#Override
public void onResume() {
super.onResume();
setHasOptionsMenu(true);
fillRemindersList();
}
I also tried it in onStart and onCreateView but it didn't help...
So what am I missing here?
And if there are others issues in my code that I'm unaware of, please do tell.
Thanks!
EDIT:
I just confirmed that onResume dosen't get called after I switch tabs, which is definetly wrong since I'm detaching and re-attaching them...
Am I switching tabs the wrong way?
Try using transaction.remove(fragment) in onTabUnselected and transaction.replace in onTabSelected.
Doing the beginTransaction() and commit() outside of this code I assume or did you forget?
You can see a trick used here from the samples as well:
https://github.com/JakeWharton/ActionBarSherlock/blob/master/samples/fragments/src/com/actionbarsherlock/sample/fragments/FragmentTabs.java

Categories

Resources