If you make your foundations strong, then you can rise high and touch the sky
Android framework does not advocate any specific way to design your application. That in a way, make us more powerful and vulnerable at the same time.
Over the years writing code for Android, I realized software design is very expensive but not costly in implementation. It’s because incorporating a lot of change cycles and features addition/removal over a period of time wreaks havoc in your software if not designed properly with separation of concern.
After applying MVP, MVVM for the project, I’d like to bring up my thoughts on them
- MVP : it’s fit for a small project. For large ones, MVP gets verbose, much boilerplate code for
Contractor
,View
interface, bindView
. - MVVM: it becomes powerful when
data binding
is enabled. I’m still confusing the role ofViewModel
. Does it handle actions? How to do that?Actually, I’ve never seen a good example of MVVM on Android.
Since React Native has taken the world by storm, I started to integrate it in the native app. Redux is de facto way to build React apps. In some cases, RN is not a best fit. so I wanted to combine best of both worlds. Presented below is a opinionated architecture for implementing production apps with Redux. It is shown by example with a step by step implementation walkthrough of an actual app. It attempts to apply the core principles behind Redux and a variant of it called Redux-Observable in a practical way.
What is Redux?
Redux is a set of guidelines that decouples the code for reusability and testability. It divides the application components based on its role, called separation of concern.
There are a couple important goals for Redux that you need to keep in mind:
- Deterministic View Renders
- Deterministic State Reproduction to mobile.
Determinism is important for application testability and diagnosing and fixing bugs. If your application views and state are nondeterministic, it’s impossible to know whether or not the views and state will always be valid. You might even say that nondeterminism is a bug in itself.
The main purpose of Redux is to isolate state management from I/O side effects such as rendering the view or working with the network. When side-effects are isolated, code becomes much more simple. It’s a lot easier to understand and test your business logic when it’s not all tangled up with network requests and View
updates.
When your view render is isolated from network, I/O and state updates, you can achieve a deterministic view render, meaning: given the same state, the view will always render the same output. It eliminates the possibility of problems such as race conditions from asynchronous stuff randomly wiping out bits of your view, or mutilating bits of your state as your view is in the process of rendering.
Redux-Observable divides the application into three basic components:
- Action is just event name
- Epic is a
Transformer
which takes a stream of actions and returns a stream of results. Actions in, results out. - Reducer work like
MapReduce
. It maps aResult
then reduce it to a new state. Reducers must be pure functions. No exceptions. A pure function must be given the same input, always returns the same output and have no side-effects.
4. ViewModel: It is a bridge that connects a Store and a View. It also acts as an instructor to the View.
5. Store: holds application, activity state, modify state via dispatch(action)
6. State is UIModel
. It determines what will show up on a View
If you want to dive deep into a world of beautiful coding and be mesmerized then follow along with this article. I will focus on a common sample app pattern; load a list, pull to refresh, load more.
Firstly, we create a HomeAction
interface containing all actions in the HomeScreen
. Those are Load
, LoadMore
, Refresh
public interface HomeAction {
Action LOAD = new Action();
Action LOAD_MORE = new Action();
Action REFRESH = new Action();
}
Secondly, we make a Result
. The output of the action above is just loading, content or error.
public abstract class LoadResult implements Result {...
public static LoadResult inProgress() {
return builder()
.loading(true)
.make();
} public static LoadResult success(List<HomeSection> content) {
return builder()
.content(content)
.make();
} public static LoadResult failure(Throwable throwable) {
return builder()
.error(throwable)
.make();
} public abstract boolean loading(); @Nullable
public abstract Throwable error(); public abstract List<HomeSection> content();
Next up, we define HomeState
. To make great use of State
, I’ll add some complex cases eg: error when refresh failed, when load failed, when load more failed.
public abstract class HomeState {
public abstract boolean loading();
public abstract boolean loadingMore();
public abstract boolean refreshing();
@Nullable
public abstract Throwable loadError();
@Nullable
public abstract Throwable loadMoreError();
@Nullable
public abstract Throwable refreshError();
public abstract List<HomeSection> content();
public abstract int page();
}
Then, Epic
is where your business logic resides. Every time Store
dispatches an Action
, every Epic
will focus on their own responsibility. In this case, LoadEpic
only accepts action type LOAD
, then get HomeFeed
from the Model
or Repository
. When Action
is called, we start the Action
with loading state. Is it just easy?
public class LoadEpic implements Effect<HomeListState> {
private final HomeModel homeModel;
public LoadEffect(HomeModel homeModel) {
this.homeModel = homeModel;
}
@Override
public Observable<Result> apply(Observable<Action> action$,
BehaviorSubject<HomeState> state) {
return action$
.filter(action -> action == LoadAction.LOAD)
.filter(ignored -> state.getValue().content().isEmpty())
.flatMap(ignored -> homeModel.getHome(1)
.map(LoadResult::success)
.onErrorReturn(LoadResult::failure)
.subscribeOn(Schedulers.io())
.startWith(LoadResult.inProgress()));
}
}
After that, the Reducer
. I think this class is very explicit.
public class HomeReducer implements Reducer<HomeListState> {
@Override
public HomeListState apply(HomeListState state, Result result)
throws Exception {
if (result instanceof LoadResult) {
final LoadResult loadResult = (LoadResult) result;
if (loadResult.loading()) {
return HomeState.builder(state)
.loading(true)
.make();
} else if (loadResult.error() != null) {
return HomeState.builder(state)
.loading(false)
.loadError(loadResult.error())
.make();
} else {
return HomeState.builder(state)
.content(loadResult.content())
.loading(false)
.make();
}
} //... throw new IllegalArgumentException("Unknown result");
}
}
The next step is, let’s see what’s inside the Store
. Store
is bound to Application
, Activity
scope. When store is initialized, it takes a specific Reducer
, Epics
and initial State
as its argument for the constructor. To keep the previous state and transform it when Action
is dispatched, we take advantage of scan
operator.
public class Store<State> {
private final Reducer<State> reducer;
private final PublishSubject<Action> action$;
private final BehaviorSubject<State> state$;
private final Observable<Result> result$;
Store(@NonNull State initialState,
@NonNull Reducer<State> reducer,
@NonNull Effect<State>[] effects) {
this.reducer = reducer;
this.action$ = PublishSubject.create();
this.state$ = BehaviorSubject.createDefault(initialState);
this.result$ = Observable.fromArray(effects)
.flatMap(transformer -> transformer.apply(action$, state$));
}
public Observable<State> state$() {
return state$;
}
public void dispatch(Action action) {
action$.onNext(action);
}
void startBinding() {
disposable = result$
.scan(state.getValue(), reducer)
.subscribe(state$::onNext);
}
void stopBinding() {
disposable.dispose();
}
}
Next up, ViewModel
observes what changes in the Store
then tells View
what to do.
public class HomeViewModel {
private final Observable<HomeState> state$;
public HomeViewModel(Observable<HomeState> state$) {
this.state$ = state$;
}
public Observable<HomeState> loading$() {
return state$
.filter(HomeState::loading);
}
public Observable<HomeState> refreshing$() {
return state$
.filter(HomeState::refreshing);
}
public Observable<HomeState> loadingMore$() {
return state$
.filter(HomeState::loadingMore);
}
public Observable<Throwable> loadError$() {
return state$
.filter(state -> state.loadError() != null)
.map(HomeState::loadError);
}
public Observable<Throwable> refreshError$() {
return state$
.filter(state -> state.refreshError() != null)
.map(HomeState::refreshError);
}
public Observable<Throwable> loadMoreError$() {
return state$
.filter(state -> state.loadMoreError() != null)
.map(HomeState::loadMoreError);
}
public Observable<List<HomeSection>> content$() {
return state$
.filter(state -> !state.loading()
&& !state.refreshing()
&& state.loadError() == null
&& state.refreshError() == null)
.map(HomeState::content);
}
}
Lastly, we just do a bit setup in View
such as: Injection
, connect ViewModel
and View
, what to render. As you can see, View
is pretty dump. That’s it.
class HomeActivity extends BaseActivity {
...@Override
protected void onStart() {
super.onStart();
store.dispatch(LoadAction.LOAD);
disposeOnStop(viewModel.loading$().subscribe(ignored -> renderLoading()));
disposeOnStop(viewModel.refreshing$().subscribe(ignored -> renderRefreshing()));
disposeOnStop(viewModel.loadingMore$().subscribe(ignored -> renderLoadingMore()));
disposeOnStop(viewModel.loadError$().subscribe(this::renderLoadError));
disposeOnStop(viewModel.loadMoreError$().subscribe(this::renderLoadMoreError));
disposeOnStop(viewModel.refreshError$().subscribe(this::renderRefreshError));
disposeOnStop(viewModel.content$().subscribe(this::renderContent));
}...
}
Conclusion
Pretty much of the code above can be unit test. Certainly creating a modern app architecture need more time than creating an app the classic way but at the end we are sure that we have a strong, scalable code base.
If you want to share a different methodology that works for you, feel free to PR your own implementation for the above project and we’ll provide it as a branch for comparison.
Thanks for reading this article. Be sure to click ❤ below to recommend this article if you found it helpful. It would let others get this article in feed and spread the knowledge.
Source code is available at https://github.com/dbof10/redux-observable
Thanks anh @Giang for the init implementation.