I read the many posts that have already dealt with similar issues but haven't been able to find something that answers my question completely.
I have an Android app that uses nested fragments (from the v4 support library). I have a main FragmentActivity that contains a fragment, and that fragment contains a ViewPager which enables swiping between 3 internal fragments.
I'd like to be able to save the state of each of the 3 internal nested fragments, and for that I overrode the onSaveInstanceState() method for each of the 3 internal fragments and attempted to restore the state in onActivityCreated(), like so:
InternalFragment1.java:
public class InternalFragment1 extends Fragment {
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
// Commands to attach to main UI components...
if(savedInstanceState != null) {
// Commands to restore the saved state...
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
// Commands to save the state into outState...
super.onSaveInstanceState(outState);
}
}
However when onActivityCreated() is called, savedInstanceState is always null, regardless of whether a saved state exists or not.
I should also point out that calling this.setRetainInstance() throws an exception stating: "Can't retain fragments that are nested in other fragments".
How can I properly save and restore the nested fragments' state?
I had a similar issue and was looking for hints on solving it. Eventually, I realized that my parent fragment's onCreateView included:
mChildFragment = ChildFragment.newInstance(mId);
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, mChildFragment).commit();
This, of course, creates a new instance of the child fragment, which has a null bundle for the savedInstanceState. Surrounding the above block with a conditional:
if(savedInstanceState == null) {
mChildFragment = ChildFragment.newInstance(mId);
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, mChildFragment).commit();
}
seems to make it work, in that now the onCreate in the child fragment sees the non-null savedInstanceState I created for it in onSaveInstanceState, and is restored to how I want it.
if you use setRetainInstance(true) then of course the bundle is null.
The fragment is not destroyed but only detached from the current activity and attached to the new activity. Only when the fragment is destroyed, you get a bundle with the values you saved in onSaveInstanceState.
Just try to remove setRetainInstance(true).
That's a problem you may encounter when the parent fragment is retaining.
You can try this:
http://ideaventure.blogspot.lu/2014/10/nested-retained-fragment-lost-state.html
But I would better recommend removing the setRetaining() on the parentFragment.
There doesn't seem to be a simple way for a nested fragment to retain information. My solution was to have the parent fragment hold onto a map of Bundles and the nested fragments get their own during onCreate. The biggest issue with this is you can't have more than one instance of each nested fragment.
Ex (sorry this is in Kotlin, but it's the same thing in Java)
class ParentFragment : Fragment(), ParentFragmentListener {
val bundles = SparseArray<Bundle>()
fun getChildBundle(fragmentId : Int) : Bundle {
if (bundles.get(fragmentId) == null) {
val bundle = Bundle()
bundles.put(fragmentId,bundle)
return bundle
}
return bundles.get(fragmentId)
}
}
interface ParentFragmentListener {
fun getChildBundle(fragmentId : Int) : Bundle
}
class ChildFragment : Fragment() {
lateinit var childBundle : Bundle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val listener = parentFragment as? ParentFragmentListener
val childBundle = listener?.getFragmentsSavedBundle(UNIQUE_FRAGMENT_ID)
if (childBundle != null) this.childBundle = childBundle else childBundle = Bundle()
}
}
Related
I'm trying to understand the behavior of fragments when a configuration change occurs.
I have created a project where an activity will host a fragment in onCreate():
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(savedInstanceState==null) {
mFragment1 = new Fragment1();
mManager = getFragmentManager();
mManager.beginTransaction().add(R.id.container_1, mFragment1, null)
.add(R.id.container_1, mFragment1, null)
.commit();
}
}
If the transaction happens only if savedInstanceState is null, and activity and fragments are destroyed and recreated,why does my activity host the fragment again when the device is rotated?
My point is: if the savedinstancestate is not null after rotation , why does the fragment manager add the fragments again?
Thank you.
I belive what you are looking for is here
https://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
and
if you want to know the shorcut just put
setretaininstance(true);
inside of Fragments.
Fragments has it’s own life cycle but it always be embedded with an activity so that the fragments life cycle is directly affected by the host activity’s life cycle.
I think this will help: When you rotate your device and the screen changes orientation, Android usually destroys your application's existing Activities and Fragments and recreates them. Android does this so that your application can reload resources based on the new configuration.
if you use SaveInstanceState like below, then its hold the value, and will not be null and you can handle.
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putString(savedName, name);
}
So my issue is basically my MainActivity is initially loaded with a Fragment, which we will call MyFragment.
I am loading JSON, from online and wanting to pass into my MyFragment.
The problem is arising when setContentView is called in the MainActivity, it is calling onCreateView in MyFragment, which contains getArguments.getSerializable("myTag"). The key isn't passed because I haven't loaded the JSON yet.
Can you help me resolve this issue?
Here is my code:
In my MyFragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if(getArguments() != null) {
coll = (HashSet<String>) getArguments().getSerializable("myTag");
}
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_my, container, false);
}
MainActivity (assume I loaded my JSON already):
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadJSON();
passTagsToFragment(); //passes to the fragment
}
public void passTagsToFragment(){
Bundle bundle = new Bundle();
bundle.putSerializable("myTags", tagsSet);
TagsFragment frag = new MyFragment();
frag.setArguments(bundle);
}
EDIT:
Basically, my issue is that I want to load the MainActivity fully, before even starting to load the Fragment. Not sure how to do that.
EDIT 2:
I fixed the problem here is my code: (Changed the variable names)
MainActivity.java
public TagsFragment passInfoToTagsFramgent(){
Bundle bundle = new Bundle();
bundle.putSerializable("tags", tagsList);
TagsFragment frag = new TagsFragment();
frag.setArguments(bundle);
return frag;
}
in OnPostExecute of MainActvity.java:
Fragment tagFragment = passInfoToTagsFramgent();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, tagFragment);
transaction.commit();
You should call your passTagsToFragment() method in post execute method of your asynctask when all json data gets loaded.
protected void onPostExecute(Long result) {
passTagsToFragment();
}
loadJSON is from online source, so I assume it is an AsyncTask.
I usually do this as a lamda expression (kotlin):
loadJSON({passTagsToFragment()})
loadJSON should take a lamda expression as parameter:
loadJSON(private val callback:()->Unit )
and in the AsyncTask::onPostExecute, you should call the callback:
callback.invoke()
or just
callback()
Then you made sure the fragment is opened when the JSON is loaded and passed to fragment.
OK let me try to make it in Java.
In your AsyncTask which loads JSON, you will need an interface e.g.,
public interface JSONLoadCallback {
void loaded();
}
And the its constructor takes the interface as parameter:
class JSONLoader : AsyncTask<....> {
JSONLoader(JSONLoadCallback callback) {
_callback = callback;
}
#Override
public void onPostExecute() {
_callback.loaded();
}
}
And your Activity implements JSONLoadCallback:
#Override
public void loaded() {
passTagsToFragment();
}
And should pass itself to the AsyncTask:
JSONLoader(this).executeOnExecutor();
This way, the loaded() function is fired when JSON load is finished.
You see, Java codes are very verbal, Kotlin almost removed the necessity of Java interface.
As per my understanding, you can call Loadjson() method on fragment as well and use data accordingly but if you have some specific logic you can use asynctask and on json retrieval with progress bar you can set any MyFragment callback and update your fragment accordingly.
So, what my problem is that in one fragment(w/i a viewpager, I'll call this Fragment A) I click on this dynamically created button that adds a new fragment(I'll call this Fragment B) in a framelayout which allows me to use PayPal service. On PayPal Activity result, Fragment B communicates with the main Activity via a communicator(an interface class) to call Fragment A to change that text. But I'm getting a null pointer exeception crash.
To be specific:
what I did was that I made a global TextView variable that is initialized on click. I did this b/c I have a list of other things that are dynamically inflated and to avoid the TextView from being initialized with wrong layout I initialized it on click.
bidChange.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
eventListChangeKey = keyVal;
eventListChangeIdx = eventListIdx;
eventBiddingChangeIdx = finalI;
priceToChage = (TextView) biddersLayout.findViewById(R.id.single_list_bidder_bid_price);
Bundle bundle = new Bundle();
bundle.putInt("auctionID", auctionId);
bundle.putInt("dateID", dateId);
bundle.putInt("FromWhere", 2);
Fragment fragment = new Fragment_Home_ItemInfo_Bid();
fragment.setArguments(bundle);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
fragmentManager.beginTransaction()
.add(R.id.container_mainScreen, fragment, "itemInfo_bid")
.addToBackStack(null)
.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
});
In the main activity
public void changeBidderPrice(String s) {
Fragment fragment = viewPagerAdapter.getItem(1);
((Fragment_List) fragment).changePrice(s);
}
is what I do
back in Fragment A
public void changePrice(String val) {
priceToChage.setText(val);
dataMap.get(eventListChangeKey).get(eventListChangeIdx).getBidList().get(eventBiddingChangeIdx).setPrice(val);
}
I've thought this over an over but I just can't figure this out. I've searched for similar cases in StackOverflow but I wasn't able to get a help.
Would the problem be the way I initialize that TextView? or is it the way I'm calling Fragment A from the main activity?
for fragments onViewCreated() is called after onCreateView() and ensures that the fragment's root view is non-null. Any view setup should happen here. E.g., view lookups, attaching listeners.
source : codepath
for activities onCreate()
In my custom view I have a Java class running a simple game. When the game is finished I'd like to display my DialogFragment, but the getFragmentManager() method seems to be undefined.
FragmentManager manager = getFragmentManager();
Finish finish = new Finish();
finish.show(manager, "done");
I've tried getting the manager through a Fragment obj as:
Fragment fragment = new Fragment();
FragmentManager manager = fragment.getFragmentManager();
But that returns as null. I know it's a new Fragment instance, but I'm not sure what value to give it. Any help would be much appreciated.
if your view is attached to an Activity then simply you can do
((Activity)getContext()).getFragmentManager();
or
((ActivityCompat)getContext()).getSupportFragmentManager();
and to be more safe please make sure you check against of the View Context is instance of an Activity by doing such:
if(getContext() instanceof Activity)// do something
and a better solution is, i had rely on a callback between the View and the Activity.
I use that simple helper method:
fun getFragmentManager(context: Context?): FragmentManager? {
return when (context) {
is AppCompatActivity -> context.supportFragmentManager
is ContextThemeWrapper -> getFragmentManager(context.baseContext)
else -> null
}
}
For AndroidX, you can try:
ContextThemeWrapper themeWrapper = (ContextThemeWrapper) getContext();
FragmentManager fm = ((AppCompatActivity) themeWrapper.getBaseContext()).getSupportFragmentManager();
As recommoned by #k0sh, instaceof safety check is recommonded.
You can use this (in Kotlin):
if ((context as ContextThemeWrapper).baseContext is AppCompatActivity) {
//View is attached to an AppCompatActivity
} else {
//View is not attached to an AppCompatActivity
}
}
I'm having trouble with restoring state of a View inside a ViewPager. The content of the ViewPager is a view extending FrameLayout.
The problem is the FrameLayout.onRestoreInstanceState() is not being called if added programmatically into the ViewPager
Here's the code of my Activity.java
private ViewPager vPager;
private MainPagerAdapter mAdapter;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
setContentView(R.layout.activity_layout);
// all the findViewById stuff
CustomView cv1 = new CustomView(this);
CustomView cv2 = new CustomView(this);
cv1.setId(R.id.custom_view_id_1);
cv2.setId(R.id.custom_view_id_2);
mAdapter = MainPagerAdapter();
mAdapter.addView(cv1);
mAdapter.addView(cv2);
vPager.setAdapter(mAdapter);
}
MainPagerAdapter is a class from the accepted answer of this question
Source code for CustomView.java
#Override
protected Parcelable onSaveInstanceState() {
Log.d(TAG, "onSaveInstanceState() called");
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
Log.d(TAG, "onRestoreInstanceState() called");
}
Here's my findings so far:
onSaveInstanceState() will be called but onRestoreInstanceState() is not
When I tried to add the View directly to the root of the Activity, it calls both functions.
I found out that Activity's onRestoreState function will be called before the ViewPager calls the adapter's instantiateItem() function. So when the activity restore its state, the ViewPager doesn't have any children yet, thus the savedState doesn't belong to anyone
So I figure out that I need to make one of two things to work:
Make sure the ViewPager instantiate the item before trying to restore the state, or
Calls the CustomView's onRestoreInstanceState() manually.
I somehow managed to make option number 2, but is there any way to do option number 1?
If I understood your question, you can save your ViewPager items state using mPage.setOffscreenPageLimit(4); 4 is the number of my Fragments inside ViewPager.