Android: Prevent fragment from becoming "active" while replacing another - android

I have the following invocations:
context.getSupportFragmentManager().popBackStack(tag.name(), 1);
context.getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, tag.name()).addToBackStack(tag.name()).commit();
...while I have at least 2 other fragments on the backstack that were opened before. When these two commands will be executed and the latest fragment has been popped off the backstack, for a very short period of time, the fragment before this fragment is going to be active before the popped fragment has been replace by the given one. And that's the problem, because this fragment fetches data from server and is displaying a progress dialog. Sometimes this leads to race conditions and strange behaviour.
How can I prevent the "non-active" fragments from becoming active while replacing another fragment?
Here is a short explanation of the situation:
MainActivity -> opens Fragment1 -> opens Fragment2a -> opens EditActivity -> after "save action", Fragment2a will be popped and a new Fragment2b will be added into the fragment_container of the MainActivity. While this happens, Fragment1 is doing things, but it must not do this. I want to prevent Fragment1 to do any tasks. It should somehow just stay in background and "sleep".

What about using observer pattern? You create an interface with a method to set fragments to busy starte or do some logic.
All fragments implement this interfac and you create a list that contains these interface inside Activity. If a fragment is registered add this fragments to list or remove them with unregister methods for example.
(MyActivity)getActivity.register(this); can be called to register a fragment. Or you can call a method like in active fragment if you wish to set other fragments except this one as busy (MyActivity)getActivity.setActive(this) and inside MyActivity you can declare this method as
public void setActive(IStateController fragment) {
for(IStateController f: listOfSubscribers) {
if(f == fragment) {
// This fragment called the method and should active
// other fragments can be set to busy or waiting state
}
}
}
I really can't say if it works for you but interacting with fragments without being aware of each other can be done this way, or you can check EventBus library.

Just check
if(getActivity() == null || !isAdded){
return; //don't do your code that touches UI if it is not active.
}
but make sure you have your onPause remove your busy indicator then if you don't plan to wait for completion or you will create a memory leak of window leaking.

I'm not sure if I misuse the fragment manager concept so that this situation can occur or if the fragment manager concept is a total crap (like a lot in Android), but I solved it by using some workaround. When I start or replace a new fragment, I store immediately an ID in the application context. When a Fragment is getting started, it checks if is has the correct id. If not, I return a null view and the fragment won't be created. The check looks like this:
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState)
{
return FragmentCreationHelper.createFragmentView(inflater, container, TAG, getFragmentId(), R.layout.fragment_mycoolview);
}
The helper class...
public class FragmentCreationHelper
{
public static View createFragmentView(LayoutInflater inflater, ViewGroup container, String loggingTag, FragmentTag fragmentId, int template)
{
Log.d(loggingTag, "onCreateView()");
if (MyContext.getNextVisibleFragment() == null || fragmentId.equals(MyContext.getNextVisibleFragment()))
{
Log.d(loggingTag, "inflating view...");
return inflater.inflate(template, container, false);
}
else
{
Log.d(loggingTag, "Skipping view inflation. Fragment should not be displayed.");
return null;
}
}
}

Related

Why is fragment calling OnCreate / OnCreateView when using fragment transactions with FrameLayout?

The main page of my application has a FrameLayout.
I'm instantiating two fragments when the activity starts, and I'm trying to use a menu button to swap between the fragment.
scanHistoryFrag = new HistoryFragment();
scanFrag = new ScanFragment();
I never replace these objects - I use the same ones throughout the lifecycle of the application. However, when I swap them in my FrameLayout...
private void ChangeFragment(Android.Support.V4.App.Fragment fragment)
{
Android.Support.V4.App.FragmentTransaction ft = SupportFragmentManager.BeginTransaction();
ft.Replace(Resource.Id.fragmentContainer, fragment);
ft.Commit();
}
OnCreate and OnCreateView are called on the Fragment again... which means any adjustments I made post creation on that fragment are overwritten with initial values again. I can't seem to find any explanation for why this is happening or how I might avoid it.
The ChangeFragment method is being called by OnOptionsItemSelected, as I'm using a menu button to toggle them.
I never replace these objects - I use the same ones throughout the lifecycle of the application.
Initialization of a subclass of Fragment just create a instance of this class object, the constructor of this class will be called, but it will not go through the lifecycle of Fragment unless this Fragment is added, for more information, you can refer to Fragments. To understand it easier, I personal think the instance saves the data state of this Fragment class, but the events of lifecycle handle the view state of this Fragment.
which means any adjustments I made post creation on that fragment are overwritten with initial values again.
Yes, you're right. To avoid overwritting with initial values again, we can cache the fragment's view in OnCreateView for example like this:
private View rootView;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Use this to return your custom view for this Fragment
// return inflater.Inflate(Resource.Layout.YourFragment, container, false);
if (rootView == null)
{
//first time creating this fragment view
rootView = inflater.Inflate(Resource.Layout.fragmentlayout1, container, false);
//Initialization
//TODO:
}
else
{
//not first time creating this fragment view
ViewGroup parent = (ViewGroup)rootView.Parent;
if (parent != null)
{
parent.RemoveView(rootView);
}
}
return rootView;
}

How can I know that onCreateView has been called from an outer class?

I'm really curious about how to determine (from an outer class) if a fragment's onCreateView() has already been called. I have searched for similar questions but found none.
For instance, is fragment.isAdded() a good indicator?
My first thought was simply fragment.getView() != null, but I'm not 100% sure it would be reliable as it seems, and I'm also slightly reluctant to use it (for no particular reason, I just tend to avoid nullity checks). I would be happy to find a workaround. Suggestions I had:
isAdded()
Return true if the fragment is currently added to its activity.
This line is quite ambiguous in my opinion; added is not attached, but neither created. It might refer to FragmentTransaction.add() (which is semantically wrong because you can have <fragment>s stuck in your layout without having to call add or replace).
Still, FragmentTransaction.add() documentation gives no info nor makes you think added -> created. I'd say no.
isVisible()
Return true if the fragment is currently visible to the user. This means it: (1) has been added, (2) has its view attached to the window, and (3) is not hidden.
Looks good, in the sense that isVisible() -> isCreated, but the third option makes it isCreated != isVisible. I just think of fragments inside a view pager: not all are visible, but the fragments near the currently visible fragment are added, created and alive, you can call methods on them. But for them, isVisible() == false. This is kind of too strict.
isInLayout()
Return true if the layout is included as part of an activity view
hierarchy via the < fragment> tag. This will always be true when
fragments are created through the < fragment> tag, except in the case
where an old fragment is restored from a previous state and it does
not appear in the layout of the current state.
I don't think this applies here.
getView() != null
Returns
The fragment's root view, or null if it has no layout.
This still looks the one and only solution. I'd just like a confirmation about that.
Implement a callback
..to be called onCreateView() or, better, onViewCreated(). But:
I don't need to call something as soon as the fragment is created (why would you need that?), I need something to check at a given time;
One should define the opposite, say, onViewNotAvailableAnymore(), to make the check meaningful at all times;
I don't see how this would be different, or better, than getView != null.
Does Fragment.isAdded() imply that onCreateView has been called?
NO!! NO!! pause NOOOOOOO00000000000!!!!!
SIr
Fragment.isAdded() is a notification that your Fragment has been added to your Activity, end of story.
The add() method in FragmentTransaction has 3 different methods, all adds Fragment to an Activity ,and, two goes further to create your Fragments View and attach it to a Parent ViewGroup by the aid of LayoutInflater provided your first parameter is not 0 (id != 0)
To check if onCreateView() has been called you need to override onViewCreated().
getView() will always return null unless onCreateView() is done
your solution is check Fragment.isVisible()
FYI: There is nothing wrong that i see with the documentation. Its pretty clear sir.
Hope i am lucid Sir
Assuming
you're not interested in whether the Fragment is visible
you want to know only if the onCreateView(...) method has been called by the Android framework
you need to use existing Fragment API methods to find out
then use
getView() != null
provided that you inflate the layout and return the View inside onCreateView(...) .
A non-Fragment-API approach is to add a callback in onCreate(...), which you then call in onCreateView() or later (in lifecycle) method.
It can be done using interface. make an interface OnCreateViewListerner
public interface OnViewCreatedListener
{
void onCreateCalled();
}
create a static object of OnViewCreatedListener in your Fragment and initialize it within in your Outer class and Outer class implement this interface like
public class CustomClass implements OnViewCreatedListener{
#Override
public void onCreateCalled() {
}
public void initializeInterface()
{
FragmentTest.listerner = this;
}
.....
}
then override your onCreateView() method and write this
public class FragmentTest extends Fragment{
public static OnViewCreatedListener listerner;
View mView;
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// if (container == null)
if(listerner!= null)
{
listerner.onCreateCalled();
}
mView = inflater.inflate(R.layout.fragment_product, container, false);
return mView;
}
}
Hope this will help you.
Please consider this approach as I did same like this:
Define an Interface with in your Fragment say:
public Interface IDoSomething{
void intimateAfterOnCreateView();
}
Now, call this method with in onStart() of a fragment as according to life cycle this method will be called after the onCreateView().
Outside from this fragment just implement IDoSomething and you will get an overrided method(intimateAfterOnCreateView()) automatically.
Now this method's execution will show that onCreateView() has been called.
I just want to share my knowledge, may be it helps.
If isAdded() on a Fragment returns true, it doesn't mean that the onCreateView() has been called. In fact, isAdded returns true even during the onAttach callback, that is called before the onCreate().
I would go with extending the Fragment class and adding a public method that you can use to reach from outer of your custom Fragment Class.
When the onCreateView() is called, you can set a boolean value to true and according to your architecture, you can set it to false back again when it's in onPause() or onStop() or onDestroyView() or onDestroy() or onDetach(), up to you.
I don't think the methods you mentioned in your question will provide you exactly what you need.
How can I know that onCreateView has been called from an outer class?
You need to create interface inside your fragment and implement it in the container activity (let's say MainActivity).
1. First create an interface inside your fragment:
// Container activity must implement this interface
public interface OnCreateViewCalledListener {
void OnCreateViewCalled(String someData);
}
2. Next implement the interface inside your container activity (lets say it is MainActivity) and call it's method:
public class MainActivity extends AppCompatActivity implements
YourFragment.OnCreateViewCalledListener {
...
#Override
public void OnCreateViewCalled(String someData) {
Toast.makeText(MainActivity.this, "OnCreateView was called and passed " + someData)
}
3. Then you need to check if MainActivity implements interface callbacks (this step is crucial to make it work properly):
//Using onAttach method to check that activity has implemented callbacks
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Make sure that container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnCreateViewCalledListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnCreateViewCalledListener");
}
}
4. And finally you need to trigger the callback inside onCreateView:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mCallback.OnCreateViewCalled("Some useful data");
...
}
That's it!
EDIT:
To let other class know that onCreateView was called, please use the onCreateViewCalled() callback inside the MainActivity (e.g. use another interface to trigger callback in other class).
Also it is not mandatory to pass data into the OnCreateViewCalled()
Does Fragment.isAdded() imply that onCreateView has been called?
YES
isAdded() Return true if the fragment is currently added to its activity. (Implicitly onCreateView() has been called).

How to reuse fragment's view in Android?

I am creating a Fragment Activity with tabs. Now whenever i select a tab, corresponding fragment gets recreated and its
onCreateView()
method is called.
But what i want is to reuse the view of the fragment so that whenever a tab is selected system does not call
onCreateView()
of fragment instead it shows the previous view of the fragment if it exists.
Please reply soon.
You may want to show and hide the fragments instead of adding and removing them when selecting a tab.
For example:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (frag.isAdded()) {
transaction.show(R.id.layout, frag);
} else {
transaction.add(R.id.layout, frag);
}
transaction.commit();
setOffScreenPageLimit() method can be used to set how many fragments you want to keep alive even if they are not visible to the user.
Try this, it works for screen rotation and should work for your situation as well:
1) When you add fragment for the first time, create it with name parameter:
getSupportFragmentManager()
.beginTransaction()
.replace(<containerID>, <fragment>, YOUR_FRAGMENT_NAME_CONST)
.commit();
2) When you need to find it:
YourFragmentType fragment;
fragment = (YourFragmentType) getSupportFragmentManager()
.findFragmentByTag(YOUR_FRAGMENT_NAME_CONST);
if (fragment != null){
//TODO set data to the existing fragment
}
else{
//TODO create and initialize your fragment
}
Edit:
You should differentiate Fragment object creation and onCreateView() being called.
It's right that you should avoid unnecessary object creation and so reuse fragments.On the other hand (as for my experience) it's better to adhere Android's onCreateView() politics to guarantee consistent user experience.
If you really want to save CPU time and avoid re-inflating complicated view (and settle all issues yourselves) - you may just check it is null like this:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// View
if (mView == null) {
mView = inflater.inflate(R.layout.your_fragment, container, false);
...
}
...
}

Unable to access layout inflater as child view has not initialised

I have an Android activity that searches a web service for new content to display then either displays a NoResultFragment or a ResultFragment which represents a swipe stack for the user to swipe through the items returned. Because I need to manage the stack, retrieving more data in the background as the stack gets low etc from the Activity, all of the stack details are held at the activity level and the yes/no actions trigger methods on the activity. All good so far.
The problem is I am using the layout inflater in the ResultFragment class to generate dynamic child Views, each one of which represents an item on the stack. These then get returned to the Activity controller which manages them, sends them to the fragment to display, hides them, moves them around etc, so I need access to the child item Views from the activity to do all this. I need to generate the actual child views from within the ResultFragment though, as that is where they will be visually displayed.
I create the ResultFragment, set it to the content area and then try and generate the child views by calling into the fragment created. The error is that the onViewCreate() method has not yet been called on the ResultFragment class as it has only just been added to the content frame, so there is no layoutinflater and my method to return the child View fails. I get the feeling there is something off with my design here, can someone shed some light on how to do this? Is it as simple as just passing through the parent layoutinflater from the Activity class?
Child view creation method
public View getChildView(StorySeed seed, int seedIndex)
{
final View m_view = inflater.inflate(R.layout.fragment_item, null); // Code to populate the view
return m_view;
}
activity method
private void initialiseResults(ArrayList<StorySeed> storySeeds) {
resultsFragment = new ResultsFragment(storySeeds, getApplicationContext());
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, resultsFragment)
.commit();
// load the first results to screen
seedIndex = 1;
for (int i = 0; i < seedsToDisplay; i++) {
getNextToStack();
}
}
It is the call to getNextToStack() that is going into the Fragment class and calling the getChildView() method
I would suggest that you create the views in the activity (the controller) and pass them to the fragment as needed. The fragment is your MVC "view" and it should only tell the controller what happened. The controller decides what to do after that.
The way you can have one fragment replace itself by another is to call a method on the activity. Here's a quick example:
interface IAppController {
void onResultsNotFound();
}
class MyActivity extends Activity implements IAppController{
....
public void onResultNotFound(){
//switch fragments
}
}
class MyFragment {
....
void myMethod(){
IAppController controller = (IAppController) getActivity();
controller.onResultsNotFound();
}
}
Hope this helps

Detaching a Fragment not triggering onSaveInstanceState()

My Android application has an ActionBar that changes which Fragment occupies a certain FrameLayout. I am trying to use onSaveInstanceState to save the state of a Fragment when the tab is changed, so that it can be recovered in onCreateView.
The problem is, onSaveInstanceState is never called. The Fragment's onDestroyView and onCreateView methods are called, but the Bundle supplied to onCreateView remains null.
Can someone please explain to me when onSaveInstanceState is actually called, how I can make sure it gets called when switching tabs, or the best practice for saving and restoring the state of a Fragment when it is detached and re-attached?
Fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.event_log, container, false);
// Retrieve saved state
if (savedInstanceState != null){
System.out.println("log retrieved");
} else {
System.out.println("log null");
}
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
System.out.println("log saved");
super.onSaveInstanceState(outState);
// more code
}
Activity:
/**
* Detach the current Fragment, because another one is being attached.
*/
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (tab.getText().equals(getString(R.string.tab_events))){
if (frEventLog != null) {
ft.detach(frEventLog);
}
}
Fragment#onSaveInstanceState is only called when the Activity hosting the Fragment is destroyed AND there is a chance that you can come back to the same activity AND the fragment is still added to the FragmentManager. The most common case would be screen rotation.
I think your Fragment will also need to do setRetainInstance(true) in onCreate for example. Not exactly sure about that point though.
You should also see this method being called when you press the home button for example. That will destroy the activity but you can go back to it by using the task list for example.
If you just detach() the fragment all you need to do to get it back is to ask the FragmentManager for it.
There are two examples you should have a look at:
ActionBar FragmentTabs and TabHost FragmentTabs
The TabHost example uses
ft.add(containerId, fragment, tag);
// later
fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
to find the instances of previously added Fragments, works until you remove() a Fragment
Regarding onCreateView / onDestroyView: That is called once a fragment gets detached because the next time you attach it needs to create a new View. Note that Fragment#onDetached() is not called when you detach() the fragment because it is still attached to the Activity. It is only detached from the view-hierarchy.
There is another nice example on how to retain fragment state / how to use fragments to retain state in Android Training - Caching Bitmaps.
That example is missing a critical line though:
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit(); // << add this
}
return fragment;
}

Categories

Resources