Hot questions for Using EventBus in fragment

Question:

I'm start learning RxJava and I like it so far. I have a fragment that communicate with an activity on button click (to replace the current fragment with a new fragment). Google recommends interface for fragments to communicate up to the activity but it's too verbose, I tried to use broadcast receiver which works generally but it had drawbacks.

Since I'm learning RxJava I wonder if it's a good option to communicate from fragments to activities (or fragment to fragment)?. If so, whats the best way to use RxJava for this type of communication?. Do I need to make event bus like this one and if that's the case should I make a single instance of the bus and use it globally (with subjects)?


Answer:

Yes and it's pretty amazing after you learn how to do it. Consider the following singleton class:

public class UsernameModel {

    private static UsernameModel instance;

    private PublishSubject<String> subject = PublishSubject.create();

    public static UsernameModel instanceOf() {
        if (instance == null) {
            instance = new UsernameModel();
        }
        return instance;
    }

    /**
     * Pass a String down to event listeners.
     */
    public void setString(String string) {
        subject.onNext(string);
    }

    /**
     * Subscribe to this Observable. On event, do something e.g. replace a fragment
     */
    public Observable<String> getStringObservable() {
        return subject;
    }

}

In your Activity be ready to receive events (e.g. have it in the onCreate):

UsernameModel usernameModel = UsernameModel.instanceOf();

//be sure to unsubscribe somewhere when activity is "dying" e.g. onDestroy
subscription = usernameModel.getStringObservable()
        .subscribe(s -> {
            // Do on new string event e.g. replace fragment here
        }, throwable -> {
            // Normally no error will happen here based on this example.
        });

In you Fragment pass down the event when it occurs:

UsernameModel.instanceOf().setString("Nick");

Your activity then will do something.

Tip 1: Change the String with any object type you like.

Tip 2: It works also great if you have Dependency injection.


Update: I wrote a more lengthy article

Question:

I am using EventBus to notify Activity/Fragment when I get response from the server. Everything works good so far, but problem arises when I consume two network calls in same Fragment or Activity. The problem is the same method onEvent(String response) get calls for both responses from server. The response of call 1 is different from call 2.

I came up with a solution - I added CallType in NetworkReqest but I can't notify the activity/fragment about the network call since post() takes only one parameter.

Here is the relevant code -

public class NetworkRequest {
    EventBus eventBus = EventBus.getDefault();

    public void stringParamRequest(String url, final Map<String, String> params,String callType) {
        StringRequest jsonObjRequest = new StringRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        eventBus.post(response);
                    }
                }, new Response.ErrorListener() {

            @Override
            public void onErrorResponse(VolleyError error) {
                VolleyLog.d("volley", "Error: " + error.getMessage());
                eventBus.post(error);
            }
        }) {

            @Override
            public String getBodyContentType() {
                return "application/x-www-form-urlencoded; charset=UTF-8";
            }

            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                Map<String, String> param = params;
                return param;
            }

        };
        SkillSchoolApplication.get().addToRequestQueue(jsonObjRequest);
    }

    public void stringRequest(String url, String callType) {
        StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                eventBus.post(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {

            }
        });
        SkillSchoolApplication.get().addToRequestQueue(stringRequest);
    }


}

Method Inside the fragment/activity Here arise the problem when after getting the response from one request i fire another request which is dependent of the respose of the first request

@Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(String response) {
        Log.d(TAG, response);
        boolean isCourseAvaible = false;
        if (!isCourseAvaible) {
            isCourseAvaible = true;
            List<CoursesDTO> coursesDTOs = AppMgr.coursesMgr(response);
            String[] ids = new String[0];
            String id;
            if (coursesDTOs != null) {
                ids = new String[coursesDTOs.size()];
                for (int i = 0; i < coursesDTOs.size(); i++) {
                    ids[i] = coursesDTOs.get(i).getListId();

                }
            }
            id = TextUtils.join(",", ids);
            Map<String, String> map = new HashMap<>();
            map.put("part", "snippet,contentDetails");
            map.put("playlistId", id);
            map.put("key", AppConstants.YOUTUBE_KEY);
            NetworkRequest networkRequest = new NetworkRequest();
            networkRequest.stringParamRequest("some url", map);
        } else {
            Log.d(TAG, response);
        }

    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEvent(VolleyError error) {
        Log.d(TAG, error.toString());
        Toast.makeText(getActivity(), "Something went wrong " + error.toString(), Toast.LENGTH_SHORT).show();
    }

How can I differentiate the callType within onEvent(). Some guidance is required. Thanks much.


Answer:

One option is to wrap the two pieces of data you need into a class and pass that to event bus. Leaving off private members, getters/setters and constructors for brevity.

class NetworkResponse() {
   public String callType;
   public String response;
}

When you get a response, allocate a NetworkResponse and populate it with the response and the call type and post that to the event bus.

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(NetworkResponse networkResponse) {
  if(networkResponse.callType.equals(CALL_1)) {
     // ...
  } else if (networkResponse.callType.equals(CALL_2)) {
     // ...
  }

}

Question:

I've a splashscreen Fragment that is registered to event bus:

@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onStop() {
    EventBus.getDefault().unregister(this);
    super.onStop();
}

If the screen goes autolock (or any other event that could call onStop), the container activity goes onStop and the fragment is no more capable to receive the (network) event. I'm thinking about moving "unregister" logic to onDestroy method. Is it a good idea?


Answer:

You can move the event bus events to onStart() and onStop(), BUT you should know that certain methods cannot be called after onSaveInstanceState() (for example, dismissing a DialogFragment crashes you out).

So while this example was for Otto, I switched it to EventBus, and I personally have a wrapper that stores events while the application is paused.

public enum SingletonBus {
    INSTANCE;

    private static String TAG = SingletonBus.class.getSimpleName();

    private final Vector<Object> eventQueueBuffer = new Vector<>();

    private boolean paused;

    public <T> void post(final T event) {
            if (paused) {
                eventQueueBuffer.add(event);
            } else {
                EventBus.getDefault().post(event);
            }
    }

    public <T> void register(T subscriber) {
        EventBus.getDefault().register(subscriber);
    }

    public <T> void unregister(T subscriber) {
        EventBus.getDefault().unregister(subscriber);
    }

    public boolean isPaused() {
        return paused;
    }

    public void setPaused(boolean paused) {
        this.paused = paused;
        if (!paused) {
            Iterator<Object> eventIterator = eventQueueBuffer.iterator();
            while (eventIterator.hasNext()) {
                Object event = eventIterator.next();
                post(event);
                eventIterator.remove();
            }
        }
    }
}

Then in a BaseActivity of sorts,

public class BaseActivity extends AppCompatActivity {
     @Override
     public void onPostResume() {
          super.onPostResume();
          SingletonBus.INSTANCE.setPaused(false);
     }

     @Override
     public void onPause() {
          SingletonBus.INSTANCE.setPaused(true);
          super.onPause();
     }
}

Question:

I have an issue with EventBus from Greenrobot.

I was trying to post an event from a background service form my sync adapter and catch it in a fragment to update the UI.

The problem is that when I try to post the event from sync adapter I have get the following in the debug log:

No subscribers registered for event class olexiimuraviov.ua.simplerssreader.event.UpdateUIEvent
No subscribers registered for event class org.greenrobot.eventbus.NoSubscriberEvent

I register fragment in onResume and unregister it in onPause

@Override
public void onResume() {
    super.onResume();
    EventBus.getDefault().register(this);
    if (mDebug) Log.d(LOG_TAG, EventBus.getDefault().isRegistered(this) + "");
}

@Override
public void onPause() {
    super.onPause();
    EventBus.getDefault().unregister(this);
}

Log statement in onResume shows that fragment is successfully registered.

Here is onEvent method:

@Subscribe
public void onEvent(UpdateUIEvent event) {
    if (mSwipeRefreshLayout.isRefreshing())
        mSwipeRefreshLayout.setRefreshing(false);
}

I was trying to call onEvent method with Background threadmode but it didn't help.

After this I was trying to use handler to post event but eventbus still can't find any registered subscribers for event.

 new Handler(Looper.getMainLooper()).post(new Runnable() {
     @Override
     public void run() {
         EventBus.getDefault().post(new UpdateUIEvent());
     }
 });

Here is my onPerform method of sync adapter:

    @Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
    // Fetch data from server
    } finally {
       // close everything
       new Handler(Looper.getMainLooper()).post(new Runnable() {
           @Override
           public void run() {
               Log.d(LOG_TAG, "Update event");
               EventBus.getDefault().post(new UpdateUIEvent());
           }
       });
    }
}

How can I send event from a sync adapter to a fragment using Greenrobot's EventBus?


Answer:

tl;dr

You never have to worry about which thread you post an event from, but you do have to worry about which process you post it from. Your sync adapter is running in a different process to your fragment - the EventBus only works within the process in which it was created.

Long Version

You never need to spin up different threads or even worry about which thread you are on anywhere except where you subscribe for the event.

check http://greenrobot.org/eventbus/documentation/delivery-threads-threadmode/ for more details but the syntax is very self explanatory:

@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void thisMethodWillBeRunInABackgroundThread(DoLoginEvent event)
{
    //Run some code that makes an HTTP request
}

And to run something on the UI:

@Subscribe(threadMode = ThreadMode.MAIN)
public void thisMethodWillBeRunOnTheUIThread(LoginSuccessfulEvent event)
{
    //Run some code that touches the UI
}

When you call post() to fire off both the DoLoginEvent and the LoginSuccessfulEvent it absolutely doesn't matter which thread you are in.

Now to the real problem that you have: events don't seem to be captured on the subscribing objects.

I suspect that this is because your sync adapter runs in a different Process. The EventBus only operates within the Process it is created.

A sync adapter, like a BroadcastReceiver or a Service runs in a separate process to allow you to sync data with your server without any interaction with your App itself. The idea is that you sync with your server even while the app is closed, save whatever to a database, then when the app starts up, it checks that database (fast) instead of having to sync with the server (slow). But if your app is alive when the sync adapter runs, then you want to save to the database but also tell the app that there is some new information right?

So in order to allow your sync adapter to communicate with your application I would advise you to setup a BroadcastReceiver in your fragment/activity and then broadcast an intent from your sync adapter.

This mechanism is a very similar concept to the EventBus, you can even send sticky intents! Your fragment listens for these intents, but only while it's alive. If your sync adapter sends a broadcast while your application is closed, then nothing happens. But when the fragment starts up and registers for those intents, it will get the last sticky one.

public class LogFragment extends Fragment {

    private LogReceiver mReceiver;

    public class LogReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //you can call the EventBus from in here
        }
    }


    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mReceiver = new LogReceiver();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        getActivity().registerReceiver(mReceiver, new IntentFilter("com.whatever.whatever"));
        return view;
    }

    @Override
    public void onDestroyView() {
        getActivity().unregisterReceiver(mReceiver);
        super.onDestroy();
    }
}

And then from your Sync Adapter

Intent sendIntent = new Intent("com.whatever.whatever");
sendIntent.putExtra("SYNC_ADAPTER_EXTRA", "Your sync adapter says hi");
context.sendBroadcast(sendIntent);

Question:

I am not new to using event bus but I got strange behavior in the following scenario:

I have MainActivity which have two fragments that need to update their contents using Otto event bus from SecondActivity which launched from MainActivity. The problem is, when SecondActivity launched, MainActivity paused and thus, the bus will be unregistered inside its onPause method and when EventBus fired its event from SecondActivity, it won't be received by the two fragments under MainActivity.

Btw, MainActivity has a subscriber method to received an update from an AsyncTask thread and as a result it should register and unregister event bus as well.

Here is my BusProvider class code:

public class BusProvider {

   private static final Bus BUS = new Bus(ThreadEnforcer.ANY);

   public static Bus getInstance() {
       return BUS;
   }

   private BusProvider() {
   }
}

Here is my event bus code for MainActivity:

@Override
protected void onResume() {
    super.onResume();
    bus.register(this);
}

@Override
protected void onPause() {
    super.onPause();
    bus.unregister(this);
}

@Subscribe
public void onAsyncResult(SomeEvent event) {
    // do something
}

Here is my FragmentA and FragmentB event bus code:

@Override
protected void onResume() {
    super.onResume();
    bus.register(this);
}

@Override
protected void onPause() {
    super.onPause();
    bus.unregister(this);
}

@Subscribe
public void onValueUpdate(UpdateEvent event) {
    mTvValue.setText(event.value);
}

Now this code inside SecondActivity which I need to update both fragments from it:

BusProvider.getInstance().post(new UpdateEvent("Value Updated!"));

Whats happening is that the UpdateEvent fires from SecondActivity but not received by the two fragments A and B.

Any helpful answer is appreciated.


Answer:

As you correctly pointed out:

The problem is, when SecondActivity launched, MainActivity paused and thus, the bus will be unregistered inside its onPause method

Your fragments' onPause methods are being executed once SecondActivity is launched, causing them to unregister from event bus. So, you need to move register and unregister to onCreate and onDestroy respectively

FragmentA and FragmentB event bus code should be:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    bus.register(this);
}

@Override
public void onDestroy() {
    super.onDestroy();
    bus.unregister(this);
}

Question:

Problem: I am using EventBus by greenrobot to pass some events. It is working for me unfortunately for the scenario of passing data between two fragments it does not. So the event does not get fired.

Question: Do I misunderstand the concept? Or is there a mistake in my code?

Note: Both Fragments exist at the time of sending the event. One fragment is the parent and the other one the child to display details.

detail fragment:

public class DetailFragment extends Fragment {
(...)

refreshButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
EventBus.getDefault().post(new IntentRefresh());
            }
        });
(...)

}

EventBus class:

public class IntentRefresh {

    public IntentRefresh (){}

    public void refreshParent() {

    }

}

parent fragment:

public class ParentFragment extends Fragment {

    (...)

    @Override
    public void onPause() {
        super.onPause();
        EventBus.getDefault().unregister(this);
    }

    @Override
    public void onResume() {
        super.onResume();
        EventBus.getDefault().register(this);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void updateList(IntentRefresh intentRefresh) {
        Toast.makeText(getActivity(), "LUEEEEPT", Toast.LENGTH_SHORT).show();
    }

    (...)
}

Answer:

The Fragment lifecycle is quite a bit more complicated than the Activity lifecycle. I would guess that your onResume() isn't being called how you think it is. I would recommend moving your registering and un registering to the onAttach() and onDetach() methods.

Question:

I used EventBus for communication between Activity and Fragment without problems, however when I tired to do the same with two Fragments, EventBus notifies me that there has been no subscriber for given event. Here is a sample example of sending event from FragmentB to subscribed FragmentA:

Sample FragmentA(Receiver, which doesn't receive a message):

public class FragmentA extends Fragment {

private View mView;

@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(int msg) {/* Do something */};

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mView = inflater.inflate(R.layout.a_fragment_layout, container, false);
    EventBus.getDefault().register(this);
    return mView;
}

@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}
}

Sample FragmentB(Sender):

public class FragmentB extends Fragment {

private View mView;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mView = inflater.inflate(R.layout.b_fragment_layout, container, false);
    sendMessage(1);
    return mView;
}

private void sendMessage(int msg){
    EventBus.getDefault().post(msg);
}
}

Error which I get:

No subscribers registered for event class org.greenrobot.eventbus.NoSubscriberEvent

The problem seems to be quite simple, however I can't figure it out.


Answer:

Well, create a simple model class, let's call it NotifyEvent

public class NotifyEvent {
    public int mValue;

    public NotifyEvent(int value){
        this.mValue = value;
    }
}

and now sending and receiving should be like

in FragmentB:

private void sendMessage(int msg){
    EventBus.getDefault().post(new NotifyEvent(msg));
}

in FragmentA:

@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(NotifyEvent event){
        int msg = event.mValue;
        // do something with msg.
}

for details please go here

Question:

Question, How can an AppCompatActivity communicate with FragmentActivity using the EventBus?

Findings, FragmentActivity can communicate with AppCompatActivity and the onEvent method is called, but if we switch the communication path to AppCompatActivity communicates with FragmentActivity, the onEvent method is never called.

public class MainActivity extends AppCompatActivity{
 private String data;
 @Override protected void onCreate{
  data = "private String data from MainActivity";
  EventBus.getDefault().postSticky(data);
 }
}

public class AccountFade extends FragmentActivity{
 private String mAccountFadeData;
 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
 public void onEvent(String s){
  Toast.makeText(this, "private String mAccountFadeData from MainActivity", Toast.LENGTH_LONG).show();
  this.mAccountFadeData = s;
 }

 @Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  EventBus.getDefault().register(this);
 }

 @Override public void onDestroy(){
  EventBus.getDefault().unregister(this);
  super.onDestroy();
 }
}

Answer:

You must create class that contain one String variable and fire that class by EventBus.

So first create class like this

public class DataChangeEvent {
    private String data;

    public DataChangeEvent(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

Now use this for your event

Post in Activtiy :

 EventBus.getDefault().postSticky(new DataChangeEvent(data));

Get in FragmentActivity :

 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
 public void onEvent(DataChangeEvent event){
      Toast.makeText(this, "private String mAccountFadeData from MainActivity", Toast.LENGTH_LONG).show();
      this.mAccountFadeData = event.getData();
 }

Question:

In my application i want use EventBus for call some methods in another activities. I want when click on backButton in activity (DetailActivity), call one method in fragment (MainFragment). in MainFragment i have recyclerView and open this activity (DetailActivity) with recyclerView adapter.

I write below codes in DetailActivity and MainFragment but when click on backButton, not call method in MainFragment.

MainFragment codes :

public void onStart() {
    EventBus.getDefault().register(this);
    super.onStart();
}

@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onRefreshAuctions(EventUpdateAuctionsState event){
    Toast.makeText(context, "OK", Toast.LENGTH_SHORT).show();
}

DetailActivity codes :

@Override
public void onBackPressed() {
    finishWithAnimate();
    EventBus.getDefault().post(new EventUpdateAuctionsState());
}

EventUpdateAuctionsState codes :

public class EventUpdateAuctionsState {
    public EventUpdateAuctionsState() {
    }
}

Why not call method in MainFragment? How can i fix it?


Answer:

Perhaps your Fragment is in stopped state when event is fired. Try to register, unregister in create/destroy lifecycle.

public void onCreate() {
    super.onCreate();
    EventBus.getDefault().register(this);
}

@Override
public void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(this);
}

Suggestion

By the way why are you using EventBus for just backpress implementation. You can simply do this. If you fragment is child of DetailActivity.

@Override
public void onBackPressed() {
    finishWithAnimate();
    // get your fragment
    if(fragment!=null) fragment.onRefreshAuctions();
}

You can use getFragmentManager().findFragmentByTag("tag") if you don't have fragment instance.

Question:

Problem: For some communication scenarios I am using EventBus. I have got an event that is already successfully fired and subscribed by different components in my app. Now I need an activity to subscribe that event. Unfortunately it is not reached.

Question: How can I achieve that the activity does subscribe the event correctly, either? Is the problem with registering the activity?

Note: I have found that post that suggests to use onStart() and onStop() events.

my activity class:

public class MachineActivity extends AppCompatActivity{

  (...)

  @Override
  protected void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
  }

  @Override
  protected void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
  }

  @Subscribe(threadMode = ThreadMode.MAIN)
  public void characteristicsChangeByUser(IntentChangeByUser intentChangeByUser) {
    // Do something here.
  }

  (...)
}

EventBus class:

public class IntentChangeByUser {

  int position;
  int value;

  public IntentChangeByUser(int position, int value){
    this.position = position;
    this.value = value;
  }

  public int getPosition() {
    return position;
  }

  public int getValue() {
    return value;
  }
}

Answer:

Could be because you have the wrong way to unregister then EventBus in onStop(). Change to this:

@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onStop() {
   EventBus.getDefault().unregister(this);
    super.onStop();
}

Then make sure in your Subscriber Activity, there is received Event:

@Subscribe(threadMode = ThreadMode.MAIN)
public void characteristicsChangeByUser(IntentChangeByUser intentChangeByUser) {
  // Do something here.
  Log.d("Activity", "IntentChangeByUser Event received");
}