Developer Aspirations

YAPB - Yet Another Programming Blog

Wednesday

13

January 2016

Don't Use an EventBus on Mobile

by Colin Miller, on Android, development, EventBus

A practice that I've noticed infecting some mobile developers is attempting to use an EventBus in replacement of method calls. The idea being to introduce high levels of decoupling where publishers of events need to never know who will be consuming them, and for consumers to not require knowledge of the producers. While this sounds great in theory, in practice it can suffer from overly engineered and complicated code that is difficult to follow, hard to debug, slower, and less stable. In a recent project, my team has been actively working to remove usages of EventBus from our code.

A Bus

The Problem

The main problem with an EventBus turns out to be one of the most touted advantages: loose coupling of the publisher and the subscriber. Tight coupling in code can cause a lot of issues, making classes less reusable and more likely to be difficult to test. Using an EventBus to pass messages would seem to solve these problems, but actually can make testing more difficult and errors more likely to occur without notice. Code is also more difficult to read when we're using an EventBus. Take the following fictional example (shortened just to give an idea):

public class MovieFragment extends Fragment implements View.OnClickListener
{
  private MovieController movieController;
  private Button playButton;

  public MovieFragment(MovieController controller)
  {
    movieController = controller;
  }

  public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle bundle)
  {
    View root = inflater.inflate(R.layout.movie_layout, null);
    playButton = root.findViewById(R.id.play_button);
    return root;
  }

  public void onClick(View view)
  {
    if (view == playButton)
    {
      movieController.play();
    }
  }
}

Pretty basic right? seems like we just say when a user clicks on the playButton, we call the MovieController's play() method.

public class MovieController {
  private MediaPlayer moviePlayer;

  public MovieController(MediaPlayer player) {
    moviePlayer = player;
  }

  public void play() {
    moviePlayer.seekTo(0);
    moviePlayer.play();
  }
}

Most likely our controller would do some more, but this is just for illustrative purposes. If you saw this code, you'd be pretty sure you knew what it did. What happens when you click on the play button is that the MovieController's play() method is called. Looking at that method, you can see that it seeks to the beginning of the video file, and calls play. You can test this code pretty easily by just mocking the MovieController when you test the MovieFragment, or mocking the MediaPlayer when you test MovieController. Here's this same example using a (fictional) EventBus:

public class MovieFragment extends Fragment implements View.OnClickListener
{
  private EventBus eventBus;
  private Button playButton;

  public MovieFragment(EventBus bus)
  {
    eventBus = bus;
  }

  public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle bundle)
  {
    View root = inflater.inflate(R.layout.movie_layout, null);
    playButton = root.findViewById(R.id.play_button);
    return root;
  }

  public void onClick(View view)
  {
    eventBus.publish(new PlayEvent());
  }
}

Of course we'll need a PlayEvent:

public class PlayEvent {} // doesn't do anything other than being a marker for an event

And adjustments to the MovieController:

public class MovieController implements EventBus.Subscriber {
  private MediaPlayer moviePlayer;
  private EventBus eventBus;

  public MovieController(MediaPlayer player, EventBus bus) {
    moviePlayer = player;
    eventBus = bus;
    eventBus.subscribe(this);
  }

  public void onEvent(Object event) {
    if (event instanceof PlayEvent) {
      play();
    } else {
      // we don't care about this event, but we might have had additional if/else for stop, seek.. etc
    }
  }

  private void play() {
    moviePlayer.seekTo(0);
    moviePlayer.play();
  }
}

Now instead of having an instance of MovieController, the MovieFragment doesn't need to know who will control the video, all it has to do is publish a PlayEvent. Meanwhile the MovieController doesn't need to know who's pressing play, only that someone is based on the event it receives. So if we needed a different controller later, we could replace it without changing a single line of MovieFragment. Sounds good right?

Unfortunately, this almost never actually useful. When you build MovieFragment, you've got MovieController already in mind. Rarely will you ever make a different one or have multiple options to choose from. They are logically coupled, even though an EventBus can make them decoupled in code. If you're a new developer looking at the MovieFragment class, how do you know what happens when play gets pressed? Normally it's obvious that you would look into the MovieController class for the play method, but there is no play method being called from MovieFragment. Instead it's just a call to EventBus which has no business logic in it and can't be easily traced. Instead you need to search for all instances of the PlayEvent class and see who's using it to match on events, then trace from there. Unfortunately, if the incorrect EventBus was passed into MovieController, the code may still compile and run, but the play method would never get called.

Not shown in the above example would have to be code to hook the same EventBus up into the MovieFragment as is used the MovieController. If a different one is used, the message would never be received. We could guarantee it with a singleton EventBus for the entire app (yay, global variables...), but then all events for the entire system would pass to these controllers. Also, when the MovieFragment leaves scope you probably don't need the MovieController any more and it should deregister itself. The EventBus would have a reference to all subscribers so not deregistering can lead to a memory leak. You either have to push a 'deregisterMovieController' event through the EventBus, or keep an instance of MovieController in MovieFragment so it can be deregistered upon the destruction of MovieFragment. Since it's possible that other classes may also be wanting to send messages to MovieController, you won't really know when to deregister unless you use some sort of reference counting mechanism. These are problems you're encountering just for using an EventBus.

You could feed the eventBus into the MovieController yourself as part of the MovieFragment controller, but then you've just coupled MovieFragment to MovieController and the entire point of an EventBus is wasted in any event. You may as well just call the play method directly and skip the EventBus; which is ultimately the solution. Testing is also more difficult because now you're having to test events being published, consumed, and subscribed to. In addition, the wiring of these components up would need to be tested as well or you could easily make a mistake where the program compiles, but no data is being passed.

Ultimately in most situations, an EventBus isn't at all necessary and actually makes the code larger, more difficult to read and follow, leads to a greater number of bugs, and doesn't supply the benefits of code decoupling that the design pattern promotes. So basically, don't use an EventBus on mobile, it's not worth it.

comments powered by Disqus