IllegalArgumentException when using Otto with a retained Fragment - android

I am using Otto 1.3.3 and when I resume my application sometimes I get an IllegalArgumentException with the following stacktrace:
Caused by: java.lang.IllegalArgumentException: Producer method for type class
com.couchsurfing.mobile.ui.setup
.SessionProviderFragment$SessionConnectionStateChangeEvent found on
type class com.couchsurfing.mobile.ui.setup.SessionProviderFragment,
but already registered by type class
com.couchsurfing.mobile.ui.setup.SessionProviderFragment.
at com.squareup.otto.Bus.register(Bus.java:194)
at com.couchsurfing.mobile.ui.BaseRetainedFragment
.onCreate(BaseRetainedFragment.java:20)
The SessionProviderFragment has its instance retained, please find below the extended class:
public abstract class BaseRetainedFragment extends SherlockFragment {
#Inject
Bus bus;
#Override
public void onCreate(final Bundle state) {
super.onCreate(state);
((CouchsurfingApplication) getActivity().getApplication()).inject(this);
setRetainInstance(true);
bus.register(this);
}
#Override
public void onDestroy() {
super.onDestroy();
bus.unregister(this);
bus = null;
}
}
I tried both using bus.register(this) in onAttach() or onCreate(), that didn't change the issue.

The proper place to register on the bus is in onResume() and the proper place to unregister is in onPause() like so:
public abstract class BaseRetainedFragment extends RoboSherlockFragment {
#Inject private Bus bus;
#Override
public void onCreate(final Bundle state) {
super.onCreate(state);
setRetainInstance(true);
}
#Override
public void onResume() {
super.onResume();
bus.register(this);
}
#Override
public void onPause() {
super.onDestroy();
bus.unregister(this);
}
}
Note that onDestroy() is not guaranteed to be called.
You might be about to comment on this and say, hey Chris, if I register in onResume() and and events are fired before I hit this method I won't receive the events! You would be right, but this means you aren't using Producers like you should be.
Also note, if you use roboguice-sherlock you don't have to inject yourself. You also don't need to null the Bus when the Fragment goes out of scope the garbage collector will clean it up for you.

I've used Otto and EventBus mostly to pass updates from background services to Activities and Fragments. I don't know your exact use case, but the most common use for me was to update the UI (e.g. ProgressBar, status message, etc).
Having said that, what I've found as most efficient, is to register the bus in the onViewCreated() method of the fragment and unregister it in the onDestroyView() method. Provided that the bus messages are persistent (via a provider for Otto or sticky events for EventBus), you will not lose any messages this way.

I am using one "Retained Fragment" per activity to save the state of an HTTP session request. My issue was that I didn't instantiate my "Retained Fragment" the proper way.
Before I had in onCreate():
if (savedInstanceState == null) {
sessionProviderFragment = new SessionProviderFragment();
getSupportFragmentManager().beginTransaction().add(sessionProviderFragment,
SessionProviderFragment.TAG).commit();
}
Apparently the code above could create several SessionProviderFragment when quitting the activity is reopening it later.
It seams that the correct way is :
sessionProviderFragment = (SessionProviderFragment) getSupportFragmentManager()
.findFragmentByTag(SessionProviderFragment.TAG);
// If not retained (or first time running), we need to create it.
if (sessionProviderFragment == null) {
sessionProviderFragment = new SessionProviderFragment();
getSupportFragmentManager().beginTransaction().add(sessionProviderFragment,
SessionProviderFragment.TAG).commit();
}
if (savedInstanceState == null) {
initUiFragment();
}
I also moved the bus register/unregister in onResume/onPause in my BaseFragment to be sure that I will always have one SessionProviderFragment registered on the bus at a time.

It's not really safe to have an #Produce on a Fragment, because more than one instance of the fragment can exist (and be registered on the bus) at the same time.
In my opinion #Produce really only makes sense on a singleton.

Related

Otto unregistering the same instance

I have a BaseFragment which within it's onCreateView method, creates a MyObject class. Both of these are inside a ViewPager.
Two different fragments extends from the BaseFragment - FragmentA, FragmentB.
This means FragmentA and FragmentB both have their own instances of the MyObject object.
Within the BaseFragment, I call myObject.initialise(); on the MyObject object from the onStart(); method and cleanUp(); from the onStop();
#Override
public void onStart()
{
super.onStart();
myObject.initialise();
}
#Override
public void onStop()
{
myObject.cleanUp();
super.onStop();
}
Again - this lives inside the BaseFragment so both FragmentA and FragmentB have this in their lifecycle.
The initialise(); function and cleanUp(); functions look like this:
#Override
public void initialise()
{
BusManager.register(this);
}
#Override
public void cleanUp()
{
BusManager.unregister(this);
}
FragmentA will generally close first and it successfully unregisters. When FragmentB closes however, it crashes because it think this was not registered.
I checked the memory address of this and it appears that it tries to unregister the same thing twice.
Missing event handler for an annotated method. Is class com.example.app.MyObject registered?
Why is it doing this? I have made sure that MyObject is a new instance.
For the comment above, note that onDestroy() is not necessary called:
https://developer.android.com/reference/android/app/Activity.html#onDestroy()
You should not count on that for Otto's register / unregister call.
In regarding to Subby's question: I've had scenarios where onStart() / onStop() being called twice. What I ended up is placing a try-catch block. Definitely not a clean solution, but that's how I do before finding out why the lifecycle is messed up.

What is the best approach to call register/unregister eventbus on fragments?

I'm brand new using Event Bus from otto lib, So far I created a Event Bus Singleton class, which I'm using in several parts of my code. Now I'm working on a fragment view, But I still have a question, regarding:
When is the best time to register/unregister my event bus?
In a couple of posts I read that onStart() and onStop(), but without any specific reason why.
public class SomeFragment extends Fragment {
#Override
public void onStart() {
super.onStart();
EventBusSingleton.register(this);
}
#Override
public void onStop() {
super.onStop();
EventBusSingleton.unregister(this);
}
}
If I follow the same approach as in the activities doing the call onResume() and onPause() works fine as well.
public class SomeFragment extends Fragment {
#Override
public void onResume() {
super.onResume();
EventBusSingleton.register(this);
}
#Override
public void onPause() {
super.onPause();
EventBusSingleton.unregister(this);
}
}
What could be the potential risk(if exist) from each call way?
onPause()/onResume() is called when your activity does not have the focus anymore but could still be visible (think a dialog or alert on top of your activity).
onStop()/onStart() is called when your activity is not visible anymore.
Which one to use depends your use case. I believe it's not really a problem to have callbacks executed while in the paused state so I would just put the register/unregister in onStop()/onStart() but if you really want to make sure, you can put them in onPause()/onResume().
My problem was what my fragmets had two instances for bad coding, y delete de uneccesary instance and it solve the problem

Where in a Fragment should I unregister a BroadcastReceiver?

I have a Fragment with a ListView and a BroadcastReceiver that updates the ListView when new data arrives. The Fragments life is controlled by a ViewPager.
In regard to where to (un)register the BroadcastReceiver, I found several places suggesting to do it in
onResume():
refreshReceiver = new RefreshReceiver();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
refreshReceiver,
refreshIntentFilter);
onPause():
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(refreshReceiver);
However, this does not work properly. When I long-press the home button to get to the Recent apps screen, onPause() is called. When new information comes in while I'm on Recent apps, the ListView misses the update and shows old information after I go back there.
Now I was thinking about moving the unregistering to the onStop() method (or even in onDestroy()), but is that guaranteed to be called when the fragment is destroyed? I was worried because if the BroadcastManager holds a reference to the BroadcastReceiver and that in turn holds a reference to the Fragment, it would be quite a serious memory leak.
if you don't need receiver, then unregister it. if you use it only in that fragment, you may consider unregistering it onViewDestroyed().
According this answer , if your fragment is single and not in pager you can un/register LBM in :
#Override
public void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
broadcast_manager, new IntentFilter("filter"));
}
#Override
public void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(broadcast_manager);
}
Else you need to find out when fragment is visible then register LBM.
through this:
public class MyFragment extends Fragment {
#Override
public void setMenuVisibility(final boolean visible) {
super.setMenuVisibility(visible);
if (visible) {
// Register LBM
}
}
// ...
}
you can register LBM in fragment

Fragment Life cycle issue

I have implemented a Fragment and override its lifecycle callbackas as following:
#Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dactivity = getActivity();
dactivity.bindService(new Intent(dactivity,EventosDatabaseService.class), this, 0);
}
#Override
public void onDestroy() {
super.onDestroy();
dactivity.unbindService(this);
}
It is documented that onActivityCreated call must heppen prior to onDestroy, then how come i get NullPointerException thrown because dactivity is null on onDestroy? what should i do to avoid it while making sure the binding wont produce a leak?
BTW, the RetainInstance of this fragment is false if that metters
Try bind your service in Fragment.onResume with registering broadcast listeners if available, and use Fragment.onPause to unbind service and unregister listeners
I wish the image i posted could offer better understanding to figure out how fragments work.

Handling onNewIntent in Fragment

I am writing an application that uses NFC to read some data stored on it. My application uses Fragments and Fragment don't come with onNewIntent() method. Since, the data I am reading is done with my separate class which handles NFC related operation, the only thing I need to do is update the TextView inside the Fragment. However this implementation can also be used to pass new Intent to the Fragment.
Here is my current implementation which makes use of an interface. I am calling the listener after new Intent is received and NFC related checks succeeds. This is the FragmentActivity which hosts Fragment.
public class Main extends FragmentActivity implements
ActionBar.OnNavigationListener {
private Bundle myBalanceBundle;
private NFC nfcObj;
private NewBalanceListener newBlanceListener;
#Override
public void onNewIntent(Intent intent) {
setIntent(intent);
}
#Override
protected void onResume() {
getNFCState();
super.onResume();
}
private void getNFCState() {
//Other NFC related codes
else if (nfc_state == NFC.NFC_STATE_ENABLED){
readNFCTag();
}
}
private void readNFCTag() {
//Other NFC related codes
if (getIntent().getAction().equals(NfcAdapter.ACTION_TECH_DISCOVERED)) {
nfcObj.setTag((Tag) getIntent().getParcelableExtra(
NfcAdapter.EXTRA_TAG));
nfcObj.readQuickBalance();
transitQuickReadFragment(nfcObj.getCurrentBalance());
}
}
private void transitQuickReadFragment(String balance) {
// Creates a balance bundle and calls to select MyBalance Fragment if it
// is not visible. Calls listener is it is already visible.
if (actionBar.getSelectedNavigationIndex() != 1) {
if (myBalanceBundle == null)
myBalanceBundle = new Bundle();
myBalanceBundle.putString(Keys.BALANCE.toString(), balance);
actionBar.setSelectedNavigationItem(1);
} else {
newBlanceListener.onNewBalanceRead(balance);
}
}
#Override
public boolean onNavigationItemSelected(int position, long id) {
// Other fragment related codes
fragment = new MyBalance();
fragment.setArguments(myBalanceBundle);
newBlanceListener = (NewBalanceListener) fragment;
// Other fragment related codes
}
// Interface callbacks. You can pass new Intent here if your application
// requires it.
public interface NewBalanceListener {
public void onNewBalanceRead(String newBalance);
}
}
This is MyBalance Fragment which has TextView that needs to be updated whenever NFC is read:
public class MyBalance extends Fragment implements NewBalanceListener {
private TextView mybalance_value;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//Other onCreateView related code
Bundle bundle = this.getArguments();
if (bundle != null)
mybalance_value.setText(bundle.getString(Keys.BALANCE.toString(),
"0.00"));
else
mybalance_value.setText("0.00");
//Other onCreateView related code
}
#Override
public void onNewBalanceRead(String newBalance) {
mybalance_value.setText(newBalance);
}
}
This code works perfectly like expected for my application but, I want to know if there is better way to handle new Intent from Fragments?
This is an old question, but let me answer it in case anybody bumps into it.
First of all you have a bug in your code:
You can't register Fragments as listeners inside Activity the way you do it. The reason is that Activity and Fragments can be destroyed by the system and re-created later from saved state (see documentation on Recreating an Activity). When this happens, new instances of both the Activity and the Fragment will be created, but the code that sets the Fragment as a listener will not run, therefore onNewBalanceRead() will never be called. This is very common bug in Android applications.
In order to communicate events from Activity to Fragment I see at least two possible approaches:
Interface based:
There is an officially recommended approach for communication between Fragments. This approach is similar to what you do now in that it uses callback interfaces implemented by either Fragment or Activity, but its drawback is a tight coupling and lots of ugly code.
Event bus based:
The better approach (IMHO) is to make use of event bus - "master component" (Activity in your case) posts "update" events to event bus, whereas "slave component" (Fragment in your case) registers itself to event bus in onStart() (unregisters in onStop()) in order to receive these events. This is a cleaner approach which doesn't add any coupling between communicating components.
All my projects use Green Robot's EventBus, and I can't recommend it highly enough.
There is at least one alternative: From Activity.onNewIntent documentation:
An activity will always be paused before receiving a new intent, so you can count on onResume() being called after this method.
Note that getIntent() still returns the original Intent. You can use setIntent(Intent) to update it to this new Intent.
FragmentActivity.onNewIntent documentation is different but I don't think it contradicts the above statements. I also make the assumption that Fragment.onResume will be called after FragmentActivity.onResume, even though the documentation seems a little fussy to me, though my tests confirm this assumption. Based on this I updated the Intent in the activity like so (examples in Kotlin)
override fun onNewIntent(intent: Intent?) {
setIntent(intent)
super.onNewIntent(intent)
}
And in Fragment.onResume I could handle the new intent like so
override fun onResume() {
super.onResume()
doStuff(activity.intent)
}
This way the activity don't need to know about what fragments it holds.
No, there is no better way. Fragments can live longer than Activities and are not necessarily tied to them at all so providing new intents would not make sense.
Btw, you have a few bugs in your code :)
if (actionBar.getSelectedNavigationIndex() != 1) {
Magic numbers are bad! use a constant.
if (myBalanceBundle == null)
myBalanceBundle = new Bundle();
myBalanceBundle.putString(Keys.BALANCE.toString(), balance);
actionBar.setSelectedNavigationItem(1);
we already know that the navigationitem is set to 1
} else {
newBlanceListener.onNewBalanceRead(balance);
Add a null check. The user might have never selected a navigation item.

Categories

Resources