Android Mosby MVI bind to service in presenter

2019-06-03 04:02发布

问题:

I am creating a small app using Mosby.

The app has a service which I want to bind to. I guess the correct place to do this is in the presenter. But I can't really figure out how to do it.

What I want to archive is when the service is bound I want to call a method on it and push that value to the view, so that the state right now is correct.

When the service sends updates on the event bus I want to push that to the view as well.

I have found some example on the later part, but nothing about how to bind/unbind the service in the presenter.

My stab on it was to create something like this in the activity:

@NonNull
@Override
public MyPresenter createPresenter() {
    return new MyPresenter(new MyService.ServiceHandler() {
            @Override
            public void start(ServiceConnection connection) {
                Intent intent = new Intent(MyActivity.this, MyService.class);
                startService(intent);
                bindService(intent, connection, Context.BIND_AUTO_CREATE);
            }

            @Override
            public void stop(ServiceConnection connection) {
                unbindService(connection);
            }
        });

And then in the presenter do something like this:

private ServiceConnection connection;
private boolean bound;
private MyService service;

public MyPresenter(MyService.ServiceHandler serviceHandler) {
    super(new MyViewState.NotInitialiezedYet());

    this.serviceHandler = serviceHandler;

    connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
           MyService.LocalBinder binder = (MyService.LocalBinder) service;
            service = binder.getService();
            bool isInitialized = service.isInitialized();
            // how do i push isInitialized to view? 

        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
}

@Override
public void attachView(@NonNull SplashView view) {
    super.attachView(view);
    serviceHandler.start(connection);
    bound = true;
}

@Override
public void detachView(boolean retainInstance) {
    super.detachView(retainInstance);
    if(bound) {
        serviceHandler.stop(connection);
        bound = false;
    }
}

@Override
protected void bindIntents() {
    //Not sure what this would look like?
}

public void onEventInitialized(InitializedEvent event) {
    //how do I push this to the view?
 }   

Am I on the correct path? What would be the correct way of doing this? How would I send the value from the service to the view in onServiceConnected and when I get events on the event bus in onEventInitialized?

回答1:

A few things to note before we dive into a possible implementation:

  1. in Mosby, Presenters survive screen orientation per default and just the View is attached / detached. If you create a ServiceHandler in your Activity you have a memory leak because ServiceHandler is an annonaymous class instantiated in your Activity and therefore has a reference to the outer Activity instance. To avoid that you can use your Application class as context to call bindService() and unbindService().
  2. Services are business logic, so you better put that logic of binding service not into the View layer (Activity), but rather in it's own "business logic" component i.e. let's call this component MyServiceInteractor.
  3. When you move that part in a business logic, you may are wondering when to unbind / stop the service. In your code, you have done it in Presenter detachView(). While that works, Presenter now has some explicit knowledge of business logic internals and how they work. A more Rx alike solution for that would be to tie the lifecycle of service connection to the "lifecycle" of an Rx Observable. That means, service connection should be closed, once the observable is unsubscribed / disposed. This also matches perfectly with 1. "Presenter survive screen orientation changes" (and keep observable subscriptions alive during screen orientation changes).
  4. Any callback / listener can easily be "wrapped" into a Rx Observable by using Observable.create().
  5. I, personally, find Services (especially bounded services) cumbersome to work with and introduce a much higher complexity in your code. You might (or might be not) be able to achive the same without services. But it really depends on your concrete app / use case.

With that said, let's see how a possible solution could look like (pseudo alike code, may not compile):

public class MyServiceInteractor {

  private Context context;

  public MyServiceInteractor(Context context) {
    this.context = context.getApplicationContext();
  }

  public Observable<InitializedEvent> getObservable() {
    return Observable.create(emitter -> {
      if (!emitter.isDisposed()) {

        MyService.ServiceHandler handler = new MyService.ServiceHandler() {

          @Override public void start(ServiceConnection connection) {
            Intent intent = new Intent(context, MyService.class);
            context.startService(intent);
            context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
          }

          @Override public void stop(ServiceConnection connection) {
            context.unbindService(connection);
          }
        };

        emitter.onNext(handler);
        emitter.onComplete();
      }
    }).flatMap(handler ->
        Observable.create( emitter -> {
          ServiceConnection connection = new ServiceConnection() {
            @Override public void onServiceConnected(ComponentName name, IBinder service) {
              MyService.LocalBinder binder = (MyService.LocalBinder) service;
              MyService service = binder.getService();
              boolean isInitialized = service.isInitialized();
              if (!emitter.isDisposed())
                 emitter.onNext(new InitializedEvent(isInitialized));
            }

            @Override public void onServiceDisconnected(ComponentName name) {
              // you may want to emit an event too
            }
          };

        })
        .doOnDispose({handler.stop()})
    );
  }
}

So basically MyServiceInteractor.getObservable() creates a bridge to the Rx Observable world and stops the service connection when the observable get's unsubsribed. Please note that this code snippet may not compile. It's just to illustrate how a possible solution / workflow could look like.

Then your Presenter could look like this:

public class MyPresenter extends MviBasePresenter<MyView, InitializedEvent> {
  private MyServiceInteractor interactor;

  public MyPresenter(MyServiceInteractor interactor){
     this.interactor = interactor;
  }

  @Override
  void bindIntents(){
    Observable<InitializedEvent> o = intent(MyView::startLoadingIntent) // i.e triggered once in Activity.onStart()
        .flatMap( ignored -> interactor.getObservable() );

    subscribeViewState(o, MyView::render);
  }
}

So the main question / issue here is not very MVI or MVP or MVVM specific, it's mostly how do we "wrap" the android Service callbacks into a RxJava observable. Once we have this, the rest should be easy.

The only MVI related thing is to connect the dots: The view actually has to trigger an intent to start the service connection. This is done in bindIntents() via myView.startLoadingIntent()

I hope that helps.