So I've learned that I need an empty constructor in order for my fragments not to crash on reinitialization. My problem is that I use lists of data for my fragments when they are initialized (at least some of them). So what would be a good way to start new fragments with a list of data. Should I in the OnCreate() make a getData method which gets the data from some other source or what would be a proper approach?
Feeding the bundle with data really wouldn't be a very good approach as I have a lot of data.
So let's take a case (I understand it tons better that way).
When a user clicks on a button the fragment is started. What I used to do was creating a new fragment this way:
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.center_container, new DetailFragment(item));
fragmentTransaction.addToBackStack(DETAIL_TAG);
fragmentTransaction.commit();
Then in my fragment:
public DetailFragment(EventItem item) {
mItem = item;
mPlaces = Database.getContainerPlaces(getActivity()).getValidItems();
}
I can't give all the data to a bundle, so that wouldn't work. So what should I do?
A: Should I initialize the fragment with the empty constructor and then from my activity use setters to set the data directly in the fragment? However, won't I be missing data if the user presses home, Android closes the fragment and the user later returns?
B: Should I initialize the fragment with factory pattern and call setRetainInstance(true), give the fragment a key for identifying the data, and then letting the fragment fetch the data needed in onCreateView from some third source?
C: Should I just make an empty constructor and then in onCreate() fetch the data needed for the fragment?
It should be noted that the app is locked in portrait so the issue is primarily with maintaining the objects when Android closes and the user restarts.
So what would be a good way to start new fragments with a list of data.
Use the factory pattern and the "arguments" Bundle, such as:
package com.commonsware.empublite;
import android.os.Bundle;
public class SimpleContentFragment extends AbstractContentFragment {
private static final String KEY_FILE="file";
protected static SimpleContentFragment newInstance(String file) {
SimpleContentFragment f=new SimpleContentFragment();
Bundle args=new Bundle();
args.putString(KEY_FILE, file);
f.setArguments(args);
return(f);
}
#Override
String getPage() {
return(getArguments().getString(KEY_FILE));
}
}
If you are retaining your fragment instance, you should be able to get away with just using ordinary setters to put stuff in data members. The "arguments" Bundle is retained as part of configuration changes, so for non-retained instances, this is the way to ensure your setup data is retained if the user rotates the screen, etc.
Related
I have a Fragment that is used in a ViewPager. Fragment instances are constructed via factory method like this:
public static MyFragment newInstance(int sectionNumber, List<String> aList) {
MyFragment fragment = new MyFragment();
fragment.list = aList;
return fragment;
}
The list is passed from the activity's onCreate() method to a SectionsPagerAdapter instance where I call newInstance() for my fragment. The list can never be null (there are checks in the code).
Problem: The application fails with NPE in onStart() method when I try to enter
Multi-Window view. The list is null.
What I noticed is that the fields that I set in newInstance() call are now nulls. Only those that are initialized in onCreateView() are assigned with objects. I guess the system initializes my Fragment in a different way bypassing my newInstance() method.
Question: Why fields that are set in my factory method are set to null? Why it happens only in Multi-Window mode? How to prevent this?
Probably I misuse something, thought creating a fragment via factory method is what Astroid Studio offers by default.
You should never be setting data in your fragment like that because of this exact issue.
When your app goes into multi window mode your fragment/activity get recreated so any data you passed in like that is lost.
What you should be doing that does handles configuration changes is pass your data in a bundle to your fragment via setArguments. When your fragment loads you then get the bundle via getArguments then grab the information from the bundle and proceede as normal.
get/setArguments holds the bundle information through configuration changes
it would look something like this
myFragment = new MyFragment();
Bundle b = new Bundle();
b.putLong("list",aList);
myFragment.setArguments(b);
Multi-window is a form of Configuration change. I bet if you rotate the device the same issue will occur.
In your Activity's AndroidManifest block, try adding the following (as a property of <activity>:
android:configChanges="keyboard|screenLayout|screenSize|orientation"
This will tell Android that you want to handle Configuration changes yourself, and will call your Activity's (and Fragment's) onConfigurationChanged() method instead of handling itself. Leave that method alone and you should be good.
I Faced some problem with the relevance of the data in application.
In our app we have 1 activity and many fragments.
For example, we have 1 Fragment with list of User and there button for like(with 3 states: none/like/favorite).
In next page we have full description of User and like button too.
If we press like button in userList, there will be no problem and in details screen we see correct like state. But if we click like button in User details and go back to list, there still past data here.
We can't use activityForResult because fragment.
We using rx, maybe there some way to easy resolve this problem?
In order to reuse the Fragment UI components, you should build each as a completely self-contained, modular component that defines its own layout and behavior. Each Fragment then can communicate data to the Activity, and the Activity can also send data to Fragments.
In your case, the Activity can hold the actual data. When user modifies something in the details Fragment, the Fragment should intimate that to the Activity. The Activity, then in turn should inform the list Fragment.
Actually, you can use the onActivityResult() approach with fragments. I've seen this pattern a couple of times in different projects, up to you to decide whether it suits you.
Assuming your left fragment is AFragment and your right fragment is BFragment, here's the idea.
(1) In AFragment, set a target fragment prior to commiting a transaction from A to B:
Fragment bFragment = new BFragment();
bFragment.setTargetFragment(this, 42); // `this` is your AFragment instance
// transact here
(2) Provide onActivityResult():
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// your logic here
}
(3) In BFragment, prepare your data in onDetach() and call onActivityResult() on your target.
#Override
public void onDetach() {
super.onDetach();
if (getTargetFragment() != null) {
Intent data = new Intent();
// pass your data here
getTargetFragment().onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, data);
}
}
This way, whenever BFragment pops back, your AFragment has a chance to handle its data.
The way I deal with this situation in my app is to use a central "repository" of SharedPreferences to store serialized data across multiple screens. Then every time a new Activity (or in your case, Fragment) gets created or resumed, it reaches into the serialized data repo to populate its shared data.
This is how I would imagine this would work in your case. To use your "user" example, I would create a SharedPreferences file to store my list of user data objects. So I would have a class that stores everything I need to know about a user -- their info, their "like" status, etc.
Assuming I have the users in a List somewhere, I would just serialize that with Gson, and save the serialized data into the SharedPreferences I created. Then, each Fragment can just poll the serialized data to get the current state of the user. If some changes are made, each Fragment would save the data back to the same SharedPreferences file so that other Fragments could then use it to instantiate their views.
Here's some pseudo code...
//Some kind of data class for User
class MyUser {
String name;
String location;
String likeStatus;
//... whatever else
}
Then, assuming you have a List of users somewhere, I would serialize that List and store it in a global SharedPreferences file like this...
//ArrayList<MyUser> users <- defined somewhere
Gson gson = new Gson();
String usersJson = gson.toJson(users);
SharedPreferences userPrefs = getSharedPreferences("users_storage", MODE_PRIVATE);
SharedPreferences.Editor editor = userPrefs.edit();
editor.putString("users_data_key", usersJson);
editor.apply();
Then when a new Fragment is loaded and it needs to know the current state of the users, it simply opens the SharedPreferences file and gets the current state of everything like this...
SharedPreferences userPrefs = getSharedPreferences("users_storage", MODE_PRIVATE);
String usersJson = userPrefs.getString("users_data_key", null);
Type collectionType = new TypeToken<ArrayList<MyUser>>(){}.getType();
ArrayList<MyUser> users = gson.fromJson(usersJson, collectionType);
Just to be clear, this approach would be all done in your Activity, and when you were starting a new Fragment, you would simply pass in the data it needs to instantiate correctly. Then, if a Fragment needs to save the data, it would simply call a method of your Activity and pass it the data that needs to be saved, and your Activity would execute the code above to save it.
I have a headless fragment to retain my data during config changes.
rFrag = new RetainedFragment();
getSupportFragmentManager()
.beginTransaction()
.add(rFrag, MODEL)
.commit();
What is the best way to access this fragment while inside of another activity or fragment besides the original activity that the headless fragment is attached to?
Using this doesn't work:
RetainedFragment rFrag = (RetainedFragment)getSupportFragmentManager
.findFragmentByTag(model);
I did a search and I believe this is because I don't have the retained fragment added onto the backstack, but adding a headless fragment onto the backstack isn't what I want to do.
Right now I just set the retained fragment to be public and static like this:
public static RetainedFragment rFrag;
But I feel like this isn't good practice to use static variables like that.
First of all, I don't know what you mean by "headless fragment".
Secondly, I don't know what RetainedFragment() class is but I will assume that it is just a class you created that extends Fragment.
Thirdly, you can't access a Fragment through other Activity. Every fragment is attached to one Activity and when that Activity is not visible its Fragments are not accessible.
Lastly, even if you force to access by using static methods and fields and such, you are right, it is not a good practice. You can, and should, use Intent extras and Fragment arguments to pass data from one to another. You mentioned you need to retain some data, so you actually don't really need the whole Fragment, right? You can just save, load and pass around the data you need.
I have a fragment that loads json data from server
and display it in a list.
my problem is that when I move to a different fragment on the same activity, that fragments data is being deleted.
when I go back to that fragment all the data is empty, and it's loading again from the server.
is there a way to keep the fragment alive in the background?
that's the code I use to switch fragments :
private void fragmentSwitch() {
this.getFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
.replace(R.id.main_fragment_container, fragment)
.commit();
}
Thanks a lot in advance.
I see 3 different options:
Your activity keeps the data, so you can switch between fragments without loosing data.
After your fragment is created you can call one method of the activity to get the data.
For example you can create a method like getJsonData() in your activity and call it from your fragment (YourActivity)getActivity().getJsonData()
You save data in SharedPreferences and access it from you fragment
You save data in Database and access it from your fragment
So I am trying to get some experience with Fragments, but I'm finding some roadblocks.
My current situation is as follows.
I have an activity that displays a List whose content is determined by Extra Intent parameters sent from the 'calling' activity.
This List activity uses ListFragment declared in the XML like so:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="#color/black">
<fragment class="com.pixlworks.NLC.DirectoryBrowse$ListingFragment"
android:id="#+id/listing"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Currently I get the parameter that indicates the type of content directly in the Fragment by accessing the Extra data of the Activity Intent (or saved Bundle if available):
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
mListingType = savedInstanceState.getString(Utils.DIRECTORY_TYPE_STORE_KEY);
else
mListingType = getActivity().getIntent().getStringExtra(Utils.DIRECTORY_TYPE_STORE_KEY);
// get content by type, create and set the adapter
}
Now part of my problem is that I am not sure this is the right way to 'pass' that parameter from the Activity to the Fragment.
On top of that, I am getting issues with this setup when using the Action Bar's UP Navigation. When I click on an item in this List Activity it goes to another activity showing the details of the selected item. From this detail activity:
If I use the back button, the List Activity is brought back from the stack as usual and everything works fine.
If I use the ActionBar's UP (despite following steps here), it would seem that a new instance is created instead of using the one in the stack and this new instance obviously is not getting the Extra parameter in the Intent. Since I am expecting the value to exist in the saved Bundle or in the Intent, my app crashes in this situation.
So to boil things down, I am not sure which of these to follow and how to make them work properly with 'UP' navigation:
A) Hold the 'type' parameter in a field in the Activity and save it in the Activity's Bundle onSaveInstanceState. In which case I am not sure how to then pass the value to the Fragment. In this case I would just need to make sure that UP calls the existing instance of the Activity List
B) Continue with my current setup of saving the value in the Fragment instead of the Activity, but again, how to handle the UP navigation correctly?
I know it is kind of multiple things I am asking here at the same time, but they are all connected, so I hope that I can get some help on this.
Thanks for any help in advance!
The UP navigation makes more sense to be used within the same activity level. That is the intention of the codes that you followed in the developers page. Because you started a new activity, if you want to return to previous activity like the back button you will need to call finish() to destroy the details activity first.
As for passing data from activity to fragment, when you create a new instance of fragment, you can pass the data to it as bundle, for example:
// in fragment class
public static MyFragment newInstance(Bundle arg) {
MyFragment f = new MyFragment();
f.setArguments(arg);
return f;
}
When you create a new fragment, you can call:
// in activity
Bundle arg = new Bundle();
int info = ...;
arg.putInt("INFO",info);
...
MyFragment mFragment = MyFragment.newInstance(arg);
Finally, to get the data in fragment:
int info = getArguments().getInt("INFO");
...
Instead of directly calling MyFragment mFragment = new MyFragment() to instantiate the fragment, you should use a static method to instantiate it. This is to prevent some crashes which might happen if you rotate the screen and the framework complains that it couldn't find a public empty constructor.
UPDATE
To answer your questions:
1) Say you start from activity A -> activity B. Then in activity B you press the up button. By logic of use, the up button will not bring you back to activity A, because its intention is to navigate one level up,but still inside, activity B. To return to activity A, you need to call finish() to destroy activity B first.
2) If your fragment is created in xml, you still can set arguments. In your xml, you set an id for the fragment android:id="#+id/fragment_id", then
// in activity
FragmentManager fm = getSupportFragmentManager(); // or getFragmentManager() if you don't have backward compatibility
MyFragment mFragment = fm.findFragmentById(R.id.fragment_id);
Bundle arg = new Bundle();
// put data blah blah
mFragment.setArguments(arg);
Just make sure you set the arguments before you use the fragment.
Simply said, intent is used when you pass data between calling activities; bundle is used when you want to pass data from activity to fragment.