Clean architecture on Android Notes

(This notes is from watching Clean architecture on Android by Marko Miloš)

Clean architecture on Android

Why… ?

Architecture matters

Clean architecture

  • Independent of frameworks
  • Testable
  • Independent of UI
  • Independent of database / storage
  • Independent of any external agency

The Dependency Rule

Dependency Rule

  • Inner layers should not know anything (or use any code) from outer layers
  • Change of outer layers should not affect inner layers

Layers

Layers

Presentation

Model View Presenter (MVP)

View

  • bind data to view
  • scheduling animations / transistions
  • propagate user input to presenter
  • Activity, Fragment, View

Presenter

  • orchestrate and execute use cases
  • prepare / format data for the view

Flow

Present Flow

Example

Timeline

Frameworks involved:

  • Dagger 2
  • RxJava
  • Retrolambda

Abstraction

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Timeline {
List<Tweet> tweets;
}
public interface TimelineView extends View {
void displayError(String message);
void displayTimeline(List<Tweet> tweets);
}
public interface TimelinePresenter extends Presenter<TimelineView> {
void onRefresh();
void onTweetSelected(Tweet tweet);
}

View implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TimelineActivity extends AppCompatActivity implements TimelineView {
@Inject TimelinePresenter timelinePresenter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
injectDependencies();
timelinePresenter.bind(this);
// ...
}
@Override
public void displayError(String message) {
// ...
}
@Override
public void displayTimeline(List<Tweet> tweets) {
// ...
}
}

Presenter implmentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TimelinePresenterImpl extends AbsPresenter<TimelineView> implements TimelinePresenter {
private final Navigator navigator;
private final GetTimelineUseCase useCase;
@Inject
public TimelinePresenterImpl(Navigator navigator, GetTimelineUseCase useCase) {
this.navigator = navigator;
this.useCase = useCase;
}
@Override
protected void onBind() {
useCase.execute(new TimelineSubscriber());
}
@Override
protected void onRefresh() {
useCase.execute(new TimelineSubscriber());
}
@Override
protected onTweetSelected(Tweet tweet) {
navigator.openTweetDetails(tweet);
}
}

Callback / Subscriber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private final class TimelineSubscriber extends Subscriber<Timeline> {
@Override
public void onCompleted() {
//...
}
@Override
public void onError(Throwable e) {
view().displayError(ErrorMessageFactory.create(e));
}
@Override
public void onNext(Timeline timeline) {
view().displayTimeline(timeline.tweets);
}
}

Domain

Domain Layer

Model

Just some POJOs (Can be applied using AutoValue)

Interactor

1
2
3
4
5
public interface Interactor<T> {
void execute(Subscriber<T> subscriber);
Observable<T> execute();
}

Contract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
abstract class UseCase<T> implements Interactor<T> {
private final ThreadExecutor threadExecutor;
private final PostExecutionThread postExecutionThread;
private Subscription subscription = Subscriptions.empty();
public UseCase(ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread) {
this.threadExecutor = threadExecutor;
this.postExecutionThread = postExecutionThread;
}
abstract Observable<T> createObservable();
@Override
public void execute(Subscriber<T> subscriber){
this.subscription = createObservable()
.subscriptOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler())
.subscribe(subscriber);
}
@Override
public Observable<T> execute() {
return createObservable();
}
public void unsubscribe() {
if (!subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
}

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GetTimelineUseCase extends UseCase<Timeline> {
private final TimelineRepository timelineRepository;
@Inject
public GetTimelineUseCase(ThreadExecutor threadExecutor,
PostExecutionThread postExecutionThread,
TimelineRepository timelineRepository) {
super(threadExecutor, postExecutionThread);
this.timelineRepository = timelineRepository;
}
@Override
Observable<Timeline> createObservable() {
return timelineRepository.getTimeline();
}
}
1
2
3
4
5
public interface TimelineRepository {
Observable<Timeline> getTimeline();
Observable<Timeline> getTimeline(String hashTag);
Observable<Timeline> getTimeline(Region region);
}

Business logic

1
2
3
4
5
public final class LocationOperations {
public Region toRegion(Location location) {
// Business logic implementation
}
}

Adding a feature

Regional timeline feature

Data

Data Layer

Data source

1
2
3
4
5
public interface TimelineDataSource {
Observable<Timeline> getTimeline();
Observable<Timeline> getTimeline(String hashTag);
Observable<Timeline> getTimeline(Region region);
}

Network implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TimelineDataSourceNetwork implements TimelineDataSource {
private final TwitterApi twitterApi;
private final TimelineReponseMapper mapper;
public TimelineDataSourceNetwork(TwitterApi twitterApi, TimelineReponseMapper mapper) {
this.twitterApi = twitterApi;
this.mapper = mapper;
}
@Override
public Observable<Timeline> getTimeline() {
return twitterApi
.getTimeline()
.retryWhen(new RetryWithDelay(3, 500))
.map(result -> mapper.toTimeline(result.reponse()));
// Other methods implementations
}
}
1
2
3
4
5
6
public interface TwitterApi {
@GET("statuses/home_timeline")
Observable<Response<TimelineResponse>> getTimeline();
// Other APIs
}

Repository implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class TimelineRepositoryImpl implements TimelineRepository {
private final TimelineDataSourceNetwork networkDataSource;
private final TimelineDataSourceDisk diskDataSource;
private final TimelineDataSourceMemory memoryDataSource;
@Inject
public TimelineRepositoryImpl(TimelineDataSourceNetwork networkDataSource,
TimelineDataSourceDisk diskDataSource,
TimelineDataSourceMemory memoryDataSource) {
this.networkDataSource = networkDataSource;
this.diskDataSource = diskDataSource;
this.memoryDataSource = memoryDataSource;
}
@Override
public Observable<Timeline> getTimeline() {
return Observable
.concat(memory(), diskWithSave(), networkWithSave())
.first(timeline -> timeline != null);
}
private Observable<Timeline> memory() {
return memoryDataSource.timeline();
}
private Observable<Timeline> diskWithSave() {
return diskDataSource
.timeline()
.doOnNext(memoryDataSource::persist);
}
private Observable<Timeline> networkWithSave() {
return networkDataSource
.timeline()
.doOnNext(timeline -> {
diskDataSource.persist(timeline);
memoryDataSource.persist(timeline);
});
}
// Other methods / implementations
}

Data mapping

1
2
3
4
5
6
7
8
9
10
public class Tweet {
private long id;
private String author;
private String content;
private String createdAt;
private boolean favourited;
private int retweetCount;
// Constructors, getters, setters, methods...
}

Use one entity would cause a super complex entity since it need to talk with database, network, and memory.

Data mapping

Complete flow

Complete flow

Swappable implementations

Swappable implementations

Packaging

Packaging

Package by layer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// apply plugin: 'com.android.library'
data (module)
|
|----repository
|----cache
|----database
|----api
|----exception
|----shared
// apply plugin: 'java'
domain (module)
|
|----model
|----interactor
|----executor
|----api
|----abstraction
|----shared
// apply plugin: ''
presentation (module)
|
|----presenter
|----view
|----navigation
|----shared

Would cause some trouble versioning each module, but putting them in one module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app(module)
|
|----data
| |
| |----repository
| |----cache
| |----database
| |----api
| |----exception
| |----shared
|
|----domain
| |
| |----model
| |----interactor
| |----executor
| |----api
| |----abstraction
| |----shared
|
|----presentation
| |
| |----presenter
| |----view
| |----navigation
| |----shared

would cause accidental reference between layers.

Package by feature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app
|
|----core
| |
| |----data
| |----utils
|----timeline
|----details
|----gallery
|----post
|----profile
|----notifications
|----messaging
|----metrics
|----search
|----settings

Feature changes only happen under package. Class visibility might need to be public sometimes.