HappyFresh Android Architecture V3 Known as Catapult

Ade Fruandta
HappyTech
Published in
4 min readApr 9, 2020

Almost 2 years we implemented VIPER as our architecture in android app. The idea are:

  1. We want equate architecture between iOS and Android. So we have same ui logic and business logic for our app.
  2. More easier to unit test.

The fact is we had struggle to doing that. Why? The biggest reason is our module become big, complex, and many dependencies. And we feel the presenter only our bridge from view to interactor or otherwise interactor to view .

After many research, we decide to go back the origin android design pattern from google, using MVVM. In the mean time, my colleague post interesting article. The article tells about architecture in Netflix. One of quote in that article:

Activity or fragment is not our view

That’s very controversial quote. We all know in Activity or Fragment we can set layout, and get each view in that layout, and put our ui logic in there. At the first time, my brain reject that quote (lol). But, after we see the sample code, and trying to implementing in our app, we get a conclusion. This architecture we can combine with MVVM.

How we create new architecture?

In this article, I assume you already read Netflix Architecture.

In this new architecture, we have 3 pillar: Component, Module, Use case. And each pillar will be connected with Event.

Component

Component will be our presenter for our UiView. So component will be have relations one on one with UiView.

Component vs UiView

Basically, it similar with Netflix Architecture. Component will receive an event Ui State Event from module to control how UiView will be shown. And UiView can send an event User Interaction Event. The User Interaction Event can be subscribe by component it self or module. In UiView, you do Butterknife things, set text, set adapter, and so on.

Module

Module will be our command control for each component.

Module

Module can send Ui State Event or subscribe User Interaction Event.

Use Case

Use case is our business logic. Each use case only contain one case for our business, ie: LoginUseCase, SignUpUseCase, etc. Use case only initiate by module

Enough for the theory, let’s jump into code.

So, let create an app with name HappyPromo. In HappyPromo app, have 2 activity, first for showing promo list, second for showing promo detail.

First, we create an event. In this project we need a coupled event.

UiStateEvent

  • Error
public class Error implements UiStateEvent {

private Throwable throwable;

public Error(Throwable throwable) {
this.throwable = throwable;
}

public Throwable getThrowable() {
return throwable;
}
}
  • Loaded
public class Loaded<T> implements UiStateEvent {

private T data;

public Loaded(T data) {
this.data = data;
}

public T getData() {
return data;
}
}
  • Loading
public class Loading implements UiStateEvent {

}

UserInteractionEvent

  • IntentOpenPromoDetail
public class IntentOpenPromoDetail implements UserInteractionEvent {

private Promo promo;

public IntentOpenPromoDetail(Promo) {
this.promo = promo;
}

public Promo getPromo() {
return promo;
}
}

After we create an event, we create a component.

In HappyPromo app, we need 2 component:

  • PromoListComponent
public class PromoListComponent extends Component<PromoListUiView> {   @Subscribe(PromoListComponent.class)
public void onLoading(Loading loading) {
getUiView().showProgress();
getUiView().show();
}
@Subscribe(PromoListComponent.class)
public void onError(Error error) {
getUiView().hide();
}
@Subscribe(PromoListComponent.class)
public void onLoading(Loaded<List<Promo>> loaded) {
List<Promo> promoList = loaded.getData();
if (promoList == null || promoList.isEmpty()) {
getUiView().showEmptyState();
getUiView().show();
}
else {
getUiView().showPromoList(promoList);
getUiView().show();
}
}
}

As you can see, our component will be more simple and readable.

  • PromoDetailComponent
public class PromoDetailComponent extends Component<PromoDetailUiView> {   @Subscribe(PromoDetailComponent.class)
public void onLoading(Loading loading) {
getUiView().showProgress();
getUiView().show();
}
@Subscribe(PromoDetailComponent.class)
public void onError(Error error) {
getUiView().hide();
}
@Subscribe(PromoDetailComponent.class)
public void onLoading(Loaded<Promo> loaded) {
getUiView().showPromo(loaded.getData());
getUiView().show();
}
}

After we create a component, we create module.

In HappyPromo app, we need create 2 module:

  • PromoList
public class PromoListActivity extends AppCompatActivity {@Override
public void onCreate(Bundle saveInstanceState) {
setContentView(R.layout.activity_promo_list);
EventObservable.bindSubscriber(this, EventObservable.get(this));
// Initiate component
ComponentProviders.of(this).add(PromoListComponent.class, findViewById(R.id.component_promo_list));
// Send Loading event into PromoListComponent
EventObservable.get(this).emit(PromoListComponent.class, new Loading());
// Get list of promo from API
new GetListPromoUseCase().subscribe(promoList -> {
// Send promoList with Loaded event into PromoListComponent
EventObservable.get(this).emit(PromoListComponent.class, new Loaded<>(promoList));
}, error -> { // Send error with Error event into PromoListComponent
EventObservable.get(this).emit(PromoListComponent.class, new Error(error));
});
}
// Handle if user intent open promo detail from PromoListComponent
@Subscribe(PromoListComponent.class)
public void intentOpenPromoListDetail(IntentOpenPromoDetail intentOpenPromoDetail) {
Intent intent = new Intent(this, PromoDetailActivity.class);
intent.putExtra("promo_id", intentOpenPromoDetail.getPromo().getId());
startActivity(intent);}
  • PromoDetail
public class PromoDetailActivity extends AppCompatActivity {  @Override
public void onCreate(Bundle saveInstanceState) {
setContentView(R.layout.activity_promo_list);

// Get promoId from extras
Long promoId = getIntent().getLongExtra("promo_id", -1);
// Initiate component
ComponentProviders.of(this).add(PromoDetailComponent.class, findViewById(R.id.component_promo_detail));
// Send Loading event into PromoDetailComponent
EventObservable.get(this).emit(PromoDetailComponent.class, new Loading());
// Get promo from API
new GetPromoUseCase(promoId).subscribe(promo -> {
// Send promo with Loaded event into PromoDetailComponent
EventObservable.get(this).emit(PromoDetailComponent.class, new Loaded<>(promo));
}, error -> {
// Send error with Error event into PromoDetailComponent
EventObservable.get(this).emit(PromoDetailComponent.class, new Error(error));
});
}
}

In this app we can see each part have their responsibility. Component only know how to show the data. Module only know when get the data, and send event. And use case only know how to get the data.

In our case, UseCase will be handle from ViewModel, so our Activity or Fragment more clean from business logic.

That’s all for now. Next time, I will create a simple app for sample.

If you interested or want contribute, visit our github.

--

--