sockeqwe / mosby

A Model-View-Presenter / Model-View-Intent library for modern Android apps
http://hannesdorfmann.com/mosby/
Apache License 2.0
5.49k stars 841 forks source link

Can you explain more about how internals of MviBasePresenter works? #200

Closed jungornti closed 7 years ago

jungornti commented 7 years ago

Hello, can you explain more about how internals of MviBasePresenter works?

It would be nice if you explain how internals of MviBasePresenter works, specifically how do presenter re-attach view's intent after screen orientation change? It looks like presenter keeps some registry of intents that were observed by presenter, and on view reattach it connects it back?

sockeqwe commented 7 years ago

it looks like presenter keeps some registry of intents that were observed by presenter, and on view reattach it connects it back?

Yep, that's basically it.

mvibasepresenter

intent() function creates internally a PublishSubject and subscribeViewState() internally creates a BehaviorSubject which are stored in his internal "registry". The "registry" is basically just a List.

If you take a look at the source code of intent() you will see that actually this function takes a parameter intent(ViewIntentBinder) https://github.com/sockeqwe/mosby/blob/master/mvi-common/src/main/java/com/hannesdorfmann/mosby3/mvi/MviBasePresenter.java#L364

ViewIntentBinder is actually just a interface with a bind(View) method.

basically writing something like

protected void bindIntents(){
    intent(FooView::someIntent());
}

is actually

protected void bindIntents(){
    intent(new ViewIntentBinder() {
        Observable<Something> bind(FooView view){
            return view.someIntent();
       }
    });
}

intent(ViewIntentBinder) adds this ViewBinder to the internal "Registry" and subscribes it with a PublishSubject. Whenever a View gets attached, we basically iterate over this "Registry" (List) and call viewIntentBinder.bind(newAttachedView)

Does that answer your question?

jungornti commented 7 years ago

Thank you very much for such a high detail explanation, now i'm closer to undestanding. Am i right, that calling this:

protected void bindIntents(){
    intent(new ViewIntentBinder() {
        Observable<Something> bind(FooView view){
            return view.someIntent();
       }
    });
}

actually creates a closure to the view? And then .bind is called later, the correct intent is returned, because reference to view is stored in closure? And if i'm right, calling .bind, when view is detached may result in NPE?

Sorry for silly questions, but i'm really new to java and want to really understand what is going on :)

jungornti commented 7 years ago

Oh, i think, i got it, when it is called:

protected void bindIntents(){
    intent(new ViewIntentBinder() {
        Observable<Something> bind(FooView view){
            return view.someIntent();
       }
    });
}

then the instance of ViewIntentBinder is created, and that concrete instance knows what concrete intent to return from the given view, so there are no closures. And because of that, we can't talk about NPE, because non null V view is passed to that concrete ViewIntentBinder instance in bindIntentActually.

sockeqwe commented 7 years ago

Exactly! It is guaranteed that view which is passed to bind(View) is never null.

And yes, bindIntents() just create the ViewIntentBinder objects. The ViewIntentBinder.bind(view) method is called later to actually bind the views intent. Also these ViewIntentBinders will be called again when the view gets reattached to the presenter (but bindIntents () isn't called again while reattaching the view, because we can reuse the ViewIntentBinder that we have created previously)

jungornti notifications@github.com schrieb am Do., 2. Feb. 2017, 07:10:

Oh, i think, i got it, when it is called:

protected void bindIntents(){ intent(new ViewIntentBinder() { Observable bind(FooView view){ return view.someIntent(); } }); }

then the instance of ViewIntentBinder is created, and that concrete instance knows what concrete intent to return from the given view, so there are no closures. And because of that, we can't talk about NPE, because non null V view is passed to that concrete ViewIntentBinder instance in bindIntentActually.

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/sockeqwe/mosby/issues/200#issuecomment-276876760, or mute the thread https://github.com/notifications/unsubscribe-auth/AAjnrrysAzpdKBObFDZosEA3U8caTxSyks5rYXNFgaJpZM4Lz5jw .

jungornti commented 7 years ago

Hannes, thank you very much for your time on explanation, you are a great man! Now i understood.

sockeqwe commented 7 years ago

You are welcome

jungornti notifications@github.com schrieb am Do., 2. Feb. 2017, 09:54:

Hannes, thank you very much for your time on explanation, you are a great man! Now i understood.

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/sockeqwe/mosby/issues/200#issuecomment-276900987, or mute the thread https://github.com/notifications/unsubscribe-auth/AAjnrjVEFe5fXUlY2_joVPASyaM3CNi-ks5rYZnPgaJpZM4Lz5jw .

hussam789 commented 7 years ago

I'm looking at ProductDetailsPresenter and there are 3 intents: addToShoppingCartIntent, removeFromShoppingCartIntent and loadDetailsIntent. but only loadDetailsIntent is passed to the subscribeViewState method.

intent(ProductDetailsView::addToShoppingCartIntent)
        .doOnNext(product -> Timber.d("intent: add to shopping cart %s", product))
        .flatMap(product -> interactor.addToShoppingCart(product).toObservable())
        .subscribe();

    intent(ProductDetailsView::removeFromShoppingCartIntent)
        .doOnNext(product -> Timber.d("intent: remove from shopping cart %s", product))
        .flatMap(product -> interactor.removeFromShoppingCart(product).toObservable())
        .subscribe();

    Observable<ProductDetailsViewState> loadDetails =
        intent(ProductDetailsView::loadDetailsIntent)
            .doOnNext(productId -> Timber.d("intent: load details for product id = %s", productId))
            .flatMap(interactor::getDetails)
            .observeOn(AndroidSchedulers.mainThread());

    subscribeViewState(loadDetails, ProductDetailsView::render);

is it because the other 2 intents don't cause the business logic to change the viewState (and eventually calling render())?

sockeqwe commented 7 years ago

the other two intents cause the business logic to change, but the result (the changed model) is pushed back by the loadDetails observable.

Take a look at

public class DetailsInteractor {
  private final ProductBackendApiDecorator backendApi;
  private final ShoppingCart shoppingCart;

  private Observable<ProductDetail> getProductWithShoppingCartInfo(int productId) {
    List<Observable<?>> observables =
        Arrays.asList(backendApi.getProduct(productId), shoppingCart.itemsInShoppingCart());

    return Observable.combineLatest(observables, objects -> {
      //
      // This will invoked again whenever a item 
      // has been added or removed from shopping cart
      //
      Product product = (Product) objects[0];
      List<Product> productsInShoppingCart = (List<Product>) objects[1];
      boolean inShoppingCart = false;
      for (Product p : productsInShoppingCart) {
        if (p.getId() == productId) {
          inShoppingCart = true;
          break;
        }
      }

      return new ProductDetail(product, inShoppingCart);
    });
  }

  /**
   * Get the details of a given product
   */
  public Observable<ProductDetailsViewState> getDetails(int productId) {
    return getProductWithShoppingCartInfo(productId)
        .subscribeOn(Schedulers.io())
        .map(ProductDetailsViewState.DataState::new)
        .cast(ProductDetailsViewState.class)
        .startWith(new ProductDetailsViewState.LoadingState())
        .onErrorReturn(ProductDetailsViewState.ErrorState::new);
  }

  public Completable addToShoppingCart(Product product) {
    return shoppingCart.addProduct(product); // This will trigger again combineLatest from above
  }

  public Completable removeFromShoppingCart(Product product) {
    return shoppingCart.removeProduct(product); // This will trigger again combineLatest from above
  }
}