I have a custom widget saved as an android library project.
The custom widget uses Fragments and so needs to access the Application's FragmentManager.
I would like my custom widget to be compatible with Applications that extend Activity (Honeycomb or higher) and also Applications that extend FragmentActivity.
To achieve this I need my custom widget to decide whether to use getFragmentManager() or getSupportFragmentManager() based on whether the parent extends Activity or FragmentActivity as shown below.
switch (getApplicationType()) {
case ACTIVITY__HONEYCOMB_ONWARD
FragmentManager fm = ((Activity)getContext()).getFragmentManager();
//...
break;
case FRAGMENT_ACTIVITY
FragmentManager fm = ((FragmentActivity)getContext()).getSupportFragmentManager();
//...
break;
//...
The bit that has me stumped is how to write the tests in my getApplicationType() method.
private int getApplicationType() {
if (??? How do I write this test ???) {
return ACTIVITY__HONEYCOMB_ONWARD;
} else if (??? How do I write this test ???) {
return FRAGMENT_ACTIVITY;
} else {
//...
}
}
Use instanceof.
For example:
if ( getParent() instanceof Activity ) {
return ACTIVITY;
} else if ( getParent() instanceof Fragment ) {
return FRAGMENT;
} etc…
Related
I have an activity defined with a fragment described in XML. In the activity, I retrieve a reference to the fragment:
#Override
public void onStart() {
super.onStart();
FragmentManager manager = getSupportFragmentManager();
mFragment = (DetailActivityFragment) manager.findFragmentById(R.id.fragment);
...
}
I call the fragment's (custom) updateUI() method, and it has a different ID than that in the activity:
DetailActivity: tbm 440; got fragment: (0x038c6009) (invisible)
DetailActivityFragment: tbm 162; fragment: (0x05f54af9) updating UI
Then, when the fragment is destroyed:
DetailActivityFragment: tbm 131; destroying fragment (0x038c6009)
i.e. the same fragment that was created inside the activity, and different from the actual fragment.
In case it matters, here's how I log the fragment IDs:
Log.d(TAG, String.format("tbm 162; (0x%08x) updating UI", System.identityHashCode(this));
Also, the ID shown in the 'tbm 162' log statement is always the same, and always matches the ID of the first time the fragment was instantiated.
Is there any particular reason for this perversity? How can a fragment's im-memory ID change between the time it is instantiated and the time the fragment is referenced inside itself? The issue of course is that inside the updateUI method, UI elements are referenced that are contained within the (now hidden and detached) original fragment, so the UI never actually changes visibly.
TIA!
Edit:
The updateUI() method does nothing but display some fields, so I don't think it is relevant, but as requested in case I'm missing something:
public void updateUi(final InformativeBean bean) {
Log.d(TAG, String.format("tbm 162; (0x%08x) (%s) updating UI",
System.identityHashCode(this),
this));
if (mView == null || bean == null) {
return;
}
if (bean.getBattery() != null) {
TextView battery_info = (TextView) mView.findViewById(R.id.battery_field);
battery_info.setText(String.format("%s%%", bean.getBattery().getPercentage()));
}
ImageView burnin_view = (ImageView) mView.findViewById(R.id.burnin_flag_image);
if (bean.isBurninComplete()) {
burnin_view.setImageResource(R.drawable.ic_flag_burnin_complete);
} else {
burnin_view.setImageResource(R.drawable.ic_flag_burnin_incomplete);
}
Program program = bean.getProgram();
Program new_program = bean.getPendingProgram();
TextView current_program_view = (TextView) mView.findViewById(R.id.current_program_field);
current_program_view.setBackgroundColor(mTransparentColor);
if (new_program == null) {
if (program == null) {
return;
}
current_program_view.setText(program.toString());
} else if (program == null || program.equals(new_program)) {
current_program_view.setText(new_program.toString());
} else {
current_program_view.setText(String.format("%s -> %s", program, new_program));
current_program_view.setBackgroundColor(mProgramBackgroundColor);
}
}
Edit:
Even weirder, the fragment changes ID even within itself:
DetailActivityFragment: tbm 065; (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) in attach
DetailActivityFragment: tbm 104; (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) in onCreateView
DetailActivity: tbm 463; got fragment: (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) (invisible)
DetailActivityFragment: tbm 162; (0x068d3e1a) (DetailActivityFragment{68d3e1a}) updating UI
so it's 0x06a9e492 in onAttach and onCreateView (and that's the reference the activity receives), but stubbornly 0x068d3e1a inside updateUI.
After much investigation it turns out that a field inside the activity containing the errant fragment was holding a reference to that activity. Changing the field so it is passed the current activity instance (vs holding an implicit reference to the field's outer activity) fixed the issue. I'm still somewhat confused as to how a fragment's ID can change even while it is 'active', but at least I can make progress.
This helped a lot: http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html. I didn't spot the problem at first as being a kind of leak.
Effectively, this was the (original, errant) code:
DetailActivity
...
some_field = new Listener() {
// The outer activity is retained, causing its fragment to be (partially?) re-used.
onSomeEvent() {
Toast.makeText(DetailActivity.this, "Got Some Event", ...).show();
}
};
...
And the new, working code is more like:
DetailActivity
...
some_field = ListenerWithActivity.getInstance(this);
...
ListenerWithActivity:
public static ListenerWithActivity getInstance(Activity activity) {
ListenerWithActivity existing = getExisting();
if (existing == null) {
existing = new ListenerWithActivity();
}
existing.setActivity(activity);
return existing;
}
public void onSomeEvent() {
Toast.makeText(getActivity(), "Got Some Event", ...).show();
}
I've a activity which basically is :
public class FragmentContainer extends FragmentActivityBase implements IRefreshListener {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getIntent().getExtras() == null
|| getIntent().getExtras().get("type") == null) {
showProductList();
}
else
{
if (getIntent().getExtras().get("type").equals("customer"))
showCustomerList();
}
#Override
public void showProductList() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
// load the product list
ProductList fragment = new ProductList();
fragmentTransaction.replace(R.id.fragment_container, fragment)
.addToBackStack(null);
fragmentTransaction.commit();
}
.....
}
in the fragment, I use onCreateView to get intent and then I create my view.
If I need to change the fragment, I get the reference to the parent Activity (taken from onAttach) and I call method referenced by the IRefreshListener.
like :
IRefreshListener mCallback;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception.
try {
mCallback = (IRefreshListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IRefreshListener");
}
}
public void callCustomer() {
mCallback.showCustomerList();
}
It works but whne I change the orientation, even I use setRetainInstance(true) it will be reseted.
I have 2 questions :
Do I use the good pattern to manage my application. The big activity which contains one fragment become bigger with the time
How should I handle orientation change ?
Regards
I do not find this pattern is more perfect or best one, although it is or was a suggestion from Google. Because it could be a worse coding style if fragment knows particular activity or listeners, you might write more and more code, when you wanna to let your fragment know more its "container" or "parents". Will the fragment later be used for other activity which has not been implemented with IRefreshListener etc, you will code much more.
My introduce is using Otto-Bus or Event-Bus. You can just send message from one to one. Every one doesn't have to know each other.
It looks like it's possible to get all fragments of an Activity pretty easily. But how can I get all subfragments for a given fragment ?
This question is also related to getParentFragment API 16
You can do it in the same way -- just use the FragmentManager obtained using the Fragment instance's getChildFragmentManager() instead of the Activity FragmentManager. Of course, this assumes you're using a recompiled version of the support library with getFragments() not hidden, or are using reflection to get invoke that method.
The following solution is not perfect but it works in some extent :
If Android SDK is 17+, then it works fine
below SDK 17 it works for fragments at the root level (added directly to activity), and also works fine for fragments of level 1 (added to a fragment at root level).
for fragments of level >= 2, then it will always return a fragment of root level. It means that it is not possible to return the real parent of a fragment whose level is >=2, it will always return its ancestor at the root level.
And, unfortunately, it means you must have access to the activity class, so this solution is not really generic.
Here is the solution. The MyActivity class is given below.
public static Fragment getParentFragment(Fragment fragment) {
if( Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN)
return fragment.getParentFragment();
MyActivity activity = (MyActivity)fragment.getActivity();
List<Fragment> fragmentList = activity.getActiveFragments();
if( fragmentList.contains( fragment) ) {
return null;
}
for( Fragment fragmentLevel1 : fragmentList ) {
if( fragmentLevel1.getFragmentManager() == fragment.getFragmentManager() ) {
return fragmentLevel1;
}
}
//this is not supposed to happen, it might be better to throw an exception
return null;
}
Where MyActivity is based on : Is there a way to get references for all currently active fragments in an Activity?
public class MyActivity {
List<WeakReference<Fragment>> fragList = new ArrayList<WeakReference<Fragment>>();
#Override
public void onAttachFragment (Fragment fragment) {
fragList.add(new WeakReference(fragment));
}
public List<Fragment> getActiveFragments() {
ArrayList<Fragment> ret = new ArrayList<Fragment>();
for(WeakReference<Fragment> ref : fragList) {
Fragment f = ref.get();
if(f != null) {
if(f.isVisible()) {
ret.add(f);
}
}
}
return ret;
}
}
My app works with a long form which I decided to divide in multiple Fragments in a ViewPager. When you press the "save" option, the validation process starts.
Basically the validation is that some EditTexts are not empty. I'm looping through all Fragments in the ViewPager check if all fields has valid values.
// Inside Fragment
public boolean areFieldsValid() {
return !mEditText.getText().toString().isEmpty()
}
public void showErrors() {
mEditText.setError("cannot be blank");
}
If a field inside a Fragment is not valid, then viewPager.setCurrentItem(position, true); and fragment.showErrors() are called to go to that Fragment and show the user the error.
The problem comes when onCreateView() hasn't been called on the Fragment that has the error.
This happens either because you haven't navigated to that Fragment yet (supposing the user's on fragment1, error is on fragment7 and the user pressed "save" while on fragment1) or because the user rotated the device and all views are destroyed on every Fragment.
This problem/issue is not only that mEditText would be null, but also that the Fragment saved its state, so it might not even been blank. In other words, the following code is not an option, because even if the pointer is null, it might not be empty.
// Inside Fragment
public boolean areFieldsValid() {
return mEditText != null && !mEditText.getText().toString()isEmpty();
}
At this point I'm wondering if my architecture is wrong. I decided to go with ViewPager cause the form is really long, and I've been passing data from Fragment to Activity through callbacks.
Given the above settings, how can I validate fields and show the user which field is the one with the error?
You can't just assume that UI components will be there anytime you want. That fragment might be gone, killed or worse, destroyed without saving it's instance state.
What I offer is to save data on database and check if everything is set on save button click event. This can be done using ContentProviders and SQLiteDatabase. As Virgil Said in here "Persist more, persist often."
I have implemented a similar thing, but my approach is to go fragment by fragment. Hope this helps.
I add an interface,
public interface AddActionInterface {
public void onAddButtonClicked();
}
I created a base fragment which implements this interface as,
public abstract class BaseFragment extends Fragment implements AddActionInterface {
#Override
public void onAddButtonClicked() {
if (isAdded() && isVisible()) {
executeAction();
}
}
protected abstract void executeAction();
}
Then we will call our Interface object like this in the activity. Create a List like below,
List<AddActionInterface> listeners = new ArrayList<AddActionInterface>();
and add your fragment to the list inside the view pager as,
listeners.add(fragment);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.content_frame, fragment, tag).commit();
Simply call the below in the onOptionsItemSelected method.
if (item.getItemId() == R.id.action_add) {
for (AddActionInterface listener : listeners) {
listener.onAddButtonClicked();
}
}
What the above method does is calls the onAddButtonClicked() method which is implemented in the BaseFragment.
Trick here is that every time the button in the action bar is clicked it will pass the control to the BaseFragment which checks if the current fragment is still attached then will call the executeAction() method of the respective fragment which being abstract every fragment can have their own implementation.
So say for FragmentA you will simply have to extend it from BaseFragment and override executeAction() method. You can write fragment specific implementations.
This process is called dependency inversion principle. See if you can put all these pieces in right place else let me know. :) Wow this is huge. :)
On the viewpager class:
public void validate() {
for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {
Fragment fragment = mSectionsPagerAdapter.getItem(i);
if(!(fragment instanceof Validetable)) {
return;
}
Validetable validetable = (Validetable) mSectionsPagerAdapter.getItem(i);
Fragment invalidFragment = validetable.validate();
if (invalidFragment == null) {
Toast.makeText(getActivity(), "valido", Toast.LENGTH_LONG).show();
}
else {
mViewPager.setCurrentItem(i);
break;
}
}
On each fragment you do:
public static boolean isValid = true;
#Override
public Fragment validate() {
if ( StringUtils.isBlank(ColetaLocal.getInstance().getNivel())) {
isValid = false;
return this;
}
isValid = true;
return null;
}
#Override
public void onResume () {
super.onResume();
treatErrorsShowing();
}
private void treatErrorsShowing() {
if (!isValid) {
showErrors();
}
else {
clearErrors();
}
}
I ended up validating each Fragment before moving to the next one.
Reason:
The initial idea was to validate on save, and if there was an Fragment with invalid data, move to that fragment and show the errors. But since there is no way to determine the state of Views inside a Fragment if it is not visible, you cannot validate input.
My application consists of several fragments. Up until now I've had references to them stored in a custom Application object, but I am beginning to think that I'm doing something wrong.
My problems started when I realized that all my fragment's references to mActivity becomes null after an orientation change. So when I call getActivity() after an orientation change, a NullPointerException is thrown.
I have checked that my fragment's onAttach() is called before I make the call to getActivity(), but it still returns null.
The following is a stripped version of my MainActivity, which is the only activity in my application.
public class MainActivity extends BaseActivity implements OnItemClickListener,
OnBackStackChangedListener, OnSlidingMenuActionListener {
private ListView mSlidingMenuListView;
private SlidingMenu mSlidingMenu;
private boolean mMenuFragmentVisible;
private boolean mContentFragmentVisible;
private boolean mQuickAccessFragmentVisible;
private FragmentManager mManager;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*
* Boolean variables indicating which of the 3 fragment slots are visible at a given time
*/
mMenuFragmentVisible = findViewById(R.id.menuFragment) != null;
mContentFragmentVisible = findViewById(R.id.contentFragment) != null;
mQuickAccessFragmentVisible = findViewById(R.id.quickAccessFragment) != null;
if(!savedInstanceState != null) {
if(!mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(true);
} else if(mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(false);
}
return;
}
mManager = getSupportFragmentManager();
mManager.addOnBackStackChangedListener(this);
final FragmentTransaction ft = mManager.beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
if (!mMenuFragmentVisible && mContentFragmentVisible) {
/*
* Only the content fragment is visible, will enable sliding menu
*/
setupSlidingMenu(true);
onToggle();
ft.replace(R.id.contentFragment, getCustomApplication().getSportsFragment(), SportsFragment.TAG);
} else if (mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(false);
/*
* Both menu and content fragments are visible
*/
ft.replace(R.id.menuFragment, getCustomApplication().getMenuFragment(), MenuFragment.TAG);
ft.replace(R.id.contentFragment, getCustomApplication().getSportsFragment(), SportsFragment.TAG);
}
if (mQuickAccessFragmentVisible) {
/*
* The quick access fragment is visible
*/
ft.replace(R.id.quickAccessFragment, getCustomApplication().getQuickAccessFragment());
}
ft.commit();
}
private void setupSlidingMenu(boolean enable) {
/*
* if enable is true, enable sliding menu, if false
* disable it
*/
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// launch the fragment that was clicked from the menu
}
#Override
public void onBackPressed() {
// Will let the user press the back button when
// the sliding menu is open to display the content.
if (mSlidingMenu != null && mSlidingMenu.isMenuShowing()) {
onShowContent();
} else {
super.onBackPressed();
}
}
#Override
public void onBackStackChanged() {
/*
* Change selected position when the back stack changes
*/
if(mSlidingMenuListView != null) {
mSlidingMenuListView.setItemChecked(getCustomApplication().getSelectedPosition(), true);
}
}
#Override
public void onToggle() {
if (mSlidingMenu != null) {
mSlidingMenu.toggle();
}
}
#Override
public void onShowContent() {
if (mSlidingMenu != null) {
mSlidingMenu.showContent();
}
}
}
The following is a stripped version of the CustomApplication. My thoughts behind this implementation was to guarantee only one instance of each fragment throughout my application's life cycle.
public class CustomApplication extends Application {
private Fragment mSsportsFragment;
private Fragment mCarsFragment;
private Fragment mMusicFragment;
private Fragment mMoviesFragment;
public Fragment getSportsFragment() {
if(mSsportsFragment == null) {
mSsportsFragment = new SportsFragment();
}
return mSsportsFragment;
}
public Fragment getCarsFragment() {
if(mCarsFragment == null) {
mCarsFragment = new CarsFragment();
}
return mCarsFragment;
}
public Fragment getMusicFragment() {
if(mMusicFragment == null) {
mMusicFragment = new MusicFragment();
}
return mMusicFragment;
}
public Fragment getMoviesFragment() {
if(mMoviesFragment == null) {
mMoviesFragment = new MoviesFragment();
}
return mMoviesFragment;
}
}
I am very interested in tips on how to best implement multiple fragments and how to maintain their states. For your information, my applicaion consists of 15+ fragments so far.
I have done some research and it seems that FragmentManager.findFragmentByTag() is a good bet, but I haven't been able to successfully implement it.
My implementation seems to work good except for the fact that mActivity references become null after orientation changes, which lets me to believe that I may have some memory leak issues as well.
If you need to see more code, please let me know. I purposely avoided including fragment code as I strongly believe issues are related to my Activity and Application implementations, but I may be wrong.
Thanks for your time.
My thoughts behind this implementation was to guarantee only one instance of each fragment throughout my application's life cycle
This is probably part, if not all, of the source of your difficulty.
On a configuration change, Android will re-create your fragments by using the public zero-argument constructor to create a new instance. Hence, your global-scope fragments will not "guarantee only one instance of each fragment".
Please delete this custom Application class. Please allow the fragments to be re-created naturally, or if they need to live for the life of a single activity, use setRetainInstance(true). Do not attempt to reuse fragments across activities.
I don't see where are you using the reference to mActivity. But don't hold a reference to it. Always use getActivity since the Activity can be recreated after orientation change. Also, don't ever set the fragment's fields by setters or by assigning always use a Bundle and Arguments
Best practice for instantiating a new Android Fragment
Also you can use setRetainInstance(true) to keep all the fragment's members during orientation change.
Understanding Fragment's setRetainInstance(boolean)
To resolve this problem you have to use the activity object provided by onAttach method of fragment so when you change the orientation fragment is recreated so onAttach give you the current reference
you can use onAttach(Context context) to create a private context variable in fragment like this
#Override
public void onAttach(Context context) {
this.context = context;
super.onAttach(context);
}
on changing orientation, onAttach gives you new reference to the context, if you want reference to activity, you can typecast context to activity.
Context can also be reassigned inside onCreate in fragments as OnCreate is called when device is rotated
private Context mContext;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//get new activity reference here
mContext = getActivity();
}
pass this mContext throughout the fragment
If you don't setRetainInstance(true) in onCreate ... the collection e.g List<Object>, Vector<Object> in Application class will get null. Make sure you setRetainInstance(true) to make them alive.