The document discusses the architecture of a mobile application called KOR. It describes several key components:
1) Interactors handle retrieving and processing data from various sources like the network or disk and notifying the presentation layer of results or errors.
2) Repositories provide a common interface to retrieve and cache data from different sources like memory, disk or a backend server using various implementations.
3) The presentation layer separates user interface concerns from business logic using patterns like MVC and facilitates supporting different mobile platforms and frameworks.
5. /** Created by sefford on 9/06/13. */
public abstract class MovieitBaseRequest<T> extends Request<T> {
public MovieitBaseRequest(int method, String url, HashMap<String, String> parameters, BusManager bus, boolean hasToCache) { /* … */ }
public MovieitBaseRequest(int method, String url, BusManager bus, boolean hasToCache){ /* … */ }
private static String generateParametersString(HashMap<String, String> parameters) { /* … */ }
private static Map<String, String> initializeHeaders() { /* … */ }
@Override public Map<String, String> getHeaders() throws AuthFailureError { /* … */ }
@Override protected Response<T> parseNetworkResponse(NetworkResponse response) { return processResponse(response.getBody()); }
@Override protected void deliverResponse(T response) { bus.post(response); }
@Override public void deliverError(VolleyError error) { bus.post(error); }
public T processResponse(String json) {
T content = processJSon(json);
content = postProcess(content);
if (hasToCache) {
saveToCache(content);
}
return content;
}
protected abstract T processJSon(String json);
protected T postProcess(T content) { /* … */ }
protected abstract void saveToCache(T object);
}
6. Retrieval
Postprocess
Save to cache
Notify
Fetch the data (network, disk, system...)
Chance to operate through the data (sort, filter, sync…)
Persist the data on cache (d’oh)
Notify sucessful or failure completion
10. public abstract class Interactor<R extends ResponseInterface, E extends ErrorInterface>
implements Runnable, InteractorNotification<R, E> {
// Postable interface allows us to hide callbacks, but we use a bus
protected Interactor(Postable postable, Loggable log, InteractorIdentification delegate) {
this.postable = postable;
this.log = log;
this.delegate = delegate;
}
@Override
public void notifySuccess(R content) {
postable.post(content);
}
}
11. public class StandardNetworkInteractor<R extends ResponseInterface, E extends ErrorInterface>
extends NetworkInteractor<R, E> {
// NetworkInteractor defines the way to notify an error
@Override
public void run() {
try {
final R content = ((NetworkDelegate<R, E>) delegate).retrieveNetworkResponse();
final R processedContent = ((NetworkDelegate<R, E>) delegate).postProcess(content);
((NetworkDelegate<R, E>) delegate).saveToCache(processedContent);
notifySuccess(processedContent);
} catch (Exception x) {
// Pokemon exception, but the delegate handles the correct response
notifyError(((NetworkDelegate<R, E>) delegate).composeErrorResponse(x));
}
}
}
12. public interface NetworkDelegate<R extends ResponseInterface, E extends ErrorInterface>
extends InteractorIdentification {
R retrieveNetworkResponse() throws Exception;
R postProcess(R content);
void saveToCache(R object);
E composeErrorResponse(Exception error);
}
Kor originally was relying on Volley for modelling the “Request” objects. Part of this inheritance is maintained throught “Success/Failure” Notifications.
Note the ProcessResponse element
Kor divides the request in four basic steps: Retrieval, Postprocess,Save and Notify.
The problem with Volley back then was the coupling to the framework, and specifically the boilerplate Volley Request needed to handle.
One of the problems we found with the original Volley Request was that sometimes, persisting everything on disk took longer than anticipated, and the UX was performing issues.
So what we did was to extract the steps into a Strategy-like class that handled the general flow and leave the Request itself to fill the gaps through delegation.
Eventually after reading a little more of Clean Architecture we used a little more correct naming.
We can interchange Delegates with different Strategies, to micromanage better output responses with minimal boilerplate and adapt to requirements change. We have Interactors that priorize fast notification over saving to cache, or cyclically request stuff.
The Executor (aka Provider) class is the one who executes Interactors. The implementation of this class is not part of Kor, as it is intended to be managed per app.
What we do to abstract on the presentation layer is to hide the creation of the interactors with a Factory class. This factory produces composite interactors (Network/Cache or both) and the Executor has the information of the status (network speed, status of the composite) and execute or hold the necessary ones until it is available. So we just ask for instance, “Execute the Get User Interactor”
The Repository we use is very powerful, as we rely continously on the Repository interface.
We provide a BaseRepository implementation which is basically a two level cache. By using this interface we cover 90% of our needs. The different parts of the Repos are intended to work together or independently and we can switch fast one for other.
During our experimentation with several ways to improve cacheing we have implemented several flavors of Repository, which seamlessly integrates with our current setup.
There was a point where we found a limitation on our current architecture, in particular with the Presentation layer. We had tested most of our logic classes. We had a lot of presentation logic scattered on our Fragments, and was too complex to test.
Jorge saw Banes’ Philm app which used MVP and we had the idea to modularize even further the UI components.
What we came with was to add an intermediate layer to get rid of the Android framework. This class is the only dependency of each of the Fragments, and adapts its interface to the necessities of the Fragment itself. It usually goes on a Bind - Release lifecycle.
Using Scoped Injection and Composition we reduce boilerplate code and dynamically load the necessary components. We target API Levels, Screen configuration or even behavior states.
We have detected several configurations we run from time to time
The same Facade can configure different Presenter sets [Example: Same Screen for Holo & Lollipop]
The Facade needs to be composed to extend its behavior [PlanDetail + PlanDetailInvitation]
Composition of Facades by a Bigger Facade [Moving Purchase Flow to a single screen]
For handling the Application Architecture we use a single activity. This allows us to let this activity provide several “App-wide” services which are not particular of any screen and live through the full existence of the UI.
Each of these aspects of the application is centralized to its own controller and requests can be sent to the particular element that handles it [Example: Clicking on a button can request the ActionBar to enter “Search” mode]
As every of these controllers, again use interfaces atop them, we can configure them as we wish [Examples: StatusBar for api 15, 19, 20 or NFC Controller]