emmaguy / rxjava-mvp-giphy
- вторник, 7 июня 2016 г. в 03:12:29
Java
Showcase of RxJava used with MVP and some other popular android libraries
A showcase of RxJava and Model View Presenter, plus a number of other popular libraries for android development, including AutoValue, Retrofit, Moshi, and ButterKnife. Unit tests covering any business logic and Robolectric tests verifying the ui.
The app is a simple master/detail implementation: we retrieve a list of gifs from the Giphy api and present them on the TrendingActivity
in a RecyclerView
. When a gif is clicked on, we load it in by itself in the GifDetailActivity
.
This setup has a number of advantages over a non-MVP app architecture
Presenter
is view agnostic and does not care how an action was triggered, making a clear division which is easy to changeView
interface is very simple - the methods are usually one liners, doing something on the android Activity
e.g. just setting a view's state to View.GONE
- which also makes them easy to testPresenter
object and abstracts the View
for easy mocking, so we can unit test all the things, e.g:
Observable
s exposing future actions via the View
interface, allowing our Presenter
s to be entirely statelessTestScheduler
The app is packaged by component/feature, under the com.emmaguy.giphymvp.feature
package, to keep everything as private as possible. This means that any classes which contain logic for the trending
feature is contained within feature.trending
package and cannot be mistakenly used or extended elsewhere.
Each component consists of a Presenter
class, a View
interface which the corresponding Activity
implements and a Module
/Component
for dependencies. The components currently map 1-1 to Activities, but could easily use custom views instead.
The View
interface enables the Presenter
to be pure Java and not have to know about anything android:
interface View extends PresenterView {
@NonNull Observable<Void> onRefreshAction();
void setTrendingGifs(@NonNull final List<Gif> gifs);
void showLoading();
void hideLoading();
void goToGif(@NonNull final Gif gif);
...
}
The interface exposes:
Observable<Object>
)
Presenter
's one lifecycle method, onViewAttached
CompositeSubscription
via the method unsubscribeOnViewDetach
, which will unsubscribe from all subscriptions when the view is detachedPresenter
is exposed to by using a return type of Observable<Void>
, often it's enough just to know the action has happenedshow
/hide
), or methods which set
data/state goTo
e.g. goToGif
)When we retrieve data from the network, we have to manage a number of states - loading, idle, error cases. Coupled to this, we also have a number of combinations of data - no data, initial data and later, incremental data.
The TrendingNetworkManager
separately exposes to the presenter Observable
s for both the loading state and the data itself. We can thus distinguish between the following states:
Activity
Toast
or Snackbar
detailing what went wrong with the incremental loadWhilst this could all be managed by the TrendingPresenter
, it is much more easily modeled separately. Using one rx chain we could perform both the showing of progress and the data at the same time, but updating the view then becomes a side effect and it very easily gets messy, particularly when edge cases are discovered.
With them decoupled, the view expresses an action to refresh data, and the manager of that is responsible for emitting the progress state change. When the network request is complete, it can emit that it succeeded or failed alongside whatever data it managed to retrieve.
Note: the network manager could easily be extended to decide whether or not to even make the network request and retrieve from a cache instead.
This project does not use Dagger, instead it does the simple DI required manually.
We instead create simple classes suffixed with Module
that contain static factory methods that construct the required dependencies, and create interfaces suffixed Component
which list the injectable items for each feature
.
Example Module:
class TrendingModule {
private static TrendingPresenter presenter;
static TrendingPresenter trendingGifsPresenter() {
if (presenter == null) {
presenter = new TrendingPresenter(trendingGifManager(), AndroidSchedulers.mainThread());
}
return presenter;
}
private static TrendingNetworkManager trendingGifManager() {
return new TrendingNetworkManager(giphyApi(), Schedulers.io());
}
private static GiphyApi giphyApi() {
final Moshi moshi = new Moshi.Builder().add(new AutoValueMoshiAdapterFactory()).build();
final Retrofit retrofit = new Retrofit.Builder().baseUrl("http://api.giphy.com/")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build();
return retrofit.create(GiphyApi.class);
}
}
Example Component:
interface TrendingComponent extends BaseComponent {
@NonNull TrendingPresenter getPresenter();
}
Then, when the dependencies are needed, we can create the required components using the factory methods. We abstract this into the BaseActivity
, which also performs the ButterKnife binding/unbinding and Presenter
lifecycle methods attaching/detaching the view.
@CallSuper @Override protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
inject(createComponent());
setContentView(getLayoutId());
unbinder = ButterKnife.bind(this);
onViewCreated(savedInstanceState);
getPresenter().onViewAttached(getPresenterView());
}
We separate the createComponent
and inject
steps as an easy way to support orientation change - the first time we call both create
and inject
, any subsequent times we need to inject we can just call inject
and we can reuse the classes - which means we have an in memory cache of data from our network requests (held in the Presenter
) for free.
Copyright 2016 Emma Guy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.