46
2017年當個 潮潮derAndroid工程師 必須要知道的事 Johnny Lee 2017/1/25

Android architecture blueprints overview

Embed Size (px)

Citation preview

在2017年當個潮潮der~ Android工程師必須要知道的事

Johnny Lee 2017/1/25

2016 Android Top 10 Libraries

Ref: http://chuansong.me/n/1493021151427

2016 Android Top 10 Libraries

Ref: http://chuansong.me/n/1493021151427

2016 Android Top 10 Libraries

Ref: http://chuansong.me/n/1493021151427

為什麼需要這些東西?

“The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult.”

Ref: https://github.com/googlesamples/android-architecture

Android Architecture Blueprints“A collection of samples to discuss and showcase different architectural tools and patterns for Android app”

Ref: https://github.com/googlesamples/android-architecture

Outline

● Architectural patterns○ MVP○ MVP-Clean

● Architectural tools○ MVP-Dagger○ MVP-RxJava

To-do App

● Task list activity● Task detail activity● Add task activity● Statistics activity

To-do App

● Task list activity● Task detail activity● Add task activity● Statistics activity

To-do App

● Task list activity● Task detail activity● Add task activity● Statistics activity

To-do App

● Task list activity● Task detail activity● Add task activity● Statistics activity

MVPModel-View-Presenter

What is MVP

● View is a layer that displays data and reacts to user actions○ Activity○ Fragment○ View

● Model is a data access layer○ Database API○ Remote server API

● Presenter provides View with data from Model○ Presenter also handles background tasks

Ref: http://konmik.com/post/introduction_to_model_view_presenter_on_android/

Activity

Without MVP - Activity is a god object

With MVP

Presenter

Model

Activity

ViewConstract

View and Presenter’s creation & binding

public class TaskDetailActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager()

.findFragmentById(R.id.contentFrame);

if (taskDetailFragment == null) {

taskDetailFragment = TaskDetailFragment.newInstance(taskId);

ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),

taskDetailFragment, R.id.contentFrame);

}

new TaskDetailPresenter(

taskId,

Injection.provideTasksRepository(getApplicationContext()),

taskDetailFragment);

}

}

View

Presenter

View and Presenter’s creation & binding

public class TaskDetailActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

TaskDetailFragment taskDetailFragment = (TaskDetailFragment) getSupportFragmentManager()

.findFragmentById(R.id.contentFrame);

if (taskDetailFragment == null) {

taskDetailFragment = TaskDetailFragment.newInstance(taskId);

ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),

taskDetailFragment, R.id.contentFrame);

}

new TaskDetailPresenter(

taskId,

Injection.provideTasksRepository(getApplicationContext()),

taskDetailFragment);

}

}

public class TaskDetailFragment extends Fragment

implements TaskDetailContract.View {

@Override

public void setPresenter(@NonNull

TaskDetailContract.Presenter presenter)

{

mPresenter = checkNotNull(presenter);

}

}

public class TaskDetailPresenter implements TaskDetailContract.Presenter {

private final TasksRepository mTasksRepository;

private final TaskDetailContract.View mTaskDetailView;

private String mTaskId;

public TaskDetailPresenter(String taskId,

TasksRepository tasksRepository,

TaskDetailContract.View taskDetailView) {

mTaskDetailView.setPresenter(this);

}

}

Contract

public interface TaskDetailContract {

interface View extends BaseView<Presenter> {

void setLoadingIndicator(boolean active);

void showMissingTask();

void hideTitle();

void showTitle(String title);

// ...

}

interface Presenter extends BasePresenter {

void editTask();

void deleteTask();

void completeTask();

void activateTask();

}

}

public interface BaseView<T> {

void setPresenter(T

presenter);

}

View implementation

public class TaskDetailFragment extends Fragment implements TaskDetailContract.View {

@Override

public void onResume() {

mPresenter.start();

}

@Override

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

fab.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

mPresenter.editTask();

}

});

return root;

}

@Override

public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) {

case R.id.menu_delete:

mPresenter.deleteTask();

return true;

}

return false;

}

// ...

}

Presenter operation

Presenter operation

Presenter operation

Presenter implementationpublic class TaskDetailPresenter implements TaskDetailContract.Presenter {

private final TasksRepository mTasksRepository;

private final TaskDetailContract.View mTaskDetailView;

public TaskDetailPresenter(@Nullable String taskId,

@NonNull TasksRepository tasksRepository,

@NonNull TaskDetailContract.View taskDetailView) {

mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");

mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!");

mTaskDetailView.setPresenter(this);

}

@Override

public void start() { openTask(); }

private void openTask() {

mTaskDetailView.setLoadingIndicator(true);

mTasksRepository.getTask(mTaskId, new TasksDataSource.GetTaskCallback() {

@Override

public void onTaskLoaded(Task task) {

mTaskDetailView.setLoadingIndicator(false);

showTask(task);

}

@Override

public void onDataNotAvailable() {

mTaskDetailView.showMissingTask();

}

});

}

// ...

}

View operation

View operationView operation

View operation

Model operation

public class TasksRepository implements TasksDataSource {

private static TasksRepository INSTANCE = null;

private final TasksDataSource mTasksRemoteDataSource;

private final TasksDataSource mTasksLocalDataSource;

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,

@NonNull TasksDataSource tasksLocalDataSource) {

mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);

mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);

}

public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,

TasksDataSource tasksLocalDataSource) {

if (INSTANCE == null) {

INSTANCE = new TasksRepository(tasksRemoteDataSource,

tasksLocalDataSource);

}

return INSTANCE;

}

// ...

}

Model implementation

public class TasksRepository implements TasksDataSource {

private static TasksRepository INSTANCE = null;

private final TasksDataSource mTasksRemoteDataSource;

private final TasksDataSource mTasksLocalDataSource;

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,

@NonNull TasksDataSource tasksLocalDataSource) {

mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);

mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);

}

public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,

TasksDataSource tasksLocalDataSource) {

if (INSTANCE == null) {

INSTANCE = new TasksRepository(tasksRemoteDataSource,

tasksLocalDataSource);

}

return INSTANCE;

}

// ...

}

Model implementation

public interface TasksDataSource {

interface LoadTasksCallback {

void onTasksLoaded(List<Task> tasks);

void onDataNotAvailable();

}

interface GetTaskCallback {

void onTaskLoaded(Task task);

void onDataNotAvailable();

}

void getTasks(@NonNull LoadTasksCallback callback);

void getTask(@NonNull String taskId,

@NonNull GetTaskCallback callback);

void saveTask(@NonNull Task task);

void completeTask(@NonNull Task task);

void completeTask(@NonNull String taskId);

void activateTask(@NonNull Task task);

void activateTask(@NonNull String taskId);

void clearCompletedTasks();

void refreshTasks();

void deleteAllTasks();

void deleteTask(@NonNull String taskId);

}

Model implementation public class TasksRepository implements TasksDataSource {

public void getTasks(@NonNull final LoadTasksCallback callback) {

if (mCachedTasks != null && !mCacheIsDirty) {

callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));

return;

}

if (mCacheIsDirty) {

getTasksFromRemoteDataSource(callback);

} else {

mTasksLocalDataSource.getTasks(new LoadTasksCallback() {

@Override

public void onTasksLoaded(List<Task> tasks) {

refreshCache(tasks);

callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));

}

@Override

public void onDataNotAvailable() {

getTasksFromRemoteDataSource(callback);

}

});

}

}

}

Respond immediately with cache if available and not dirty

If the cache is dirty we need to fetch new data from the network.

Query the local storage if available. If not, query the network.

Clean

What is Clean Architecture

Ref: https://en.wikipedia.org/wiki/Robert_Cecil_Martin

What is Clean Architecture

Dependency Rule: source code dependencies can only point inwards and nothing in an inner circle can know anything at all about something in an outer circle.

Ref: http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/

What is Clean Architecture

● Entities○ These are the business objects of the application.

● Use Cases○ These use cases orchestrate the flow of data to and from the

entities. Are also called Interactors.

● Interface Adapters (Presenter)○ This set of adapters convert data from the format most

convenient for the use cases and entities. Presenters and Controllers belong here.

● Frameworks and Drivers (UI)○ This is where all the details go: UI, tools, frameworks, etc.

Ref: http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/

What is Clean Architecture

● Independent of Frameworks● Testable

○ Business rules (Use cases) can be tested without external element

● Independent of UI○ The UI can change easily, without changing the rest of the

system● Independent of Database

○ Business rules (Use cases) are not bound to the database● Independent of any external agency

○ Business rules (Use cases) don’t know anything at all about the outside world

Ref: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

Presenter implementationpublic class TaskDetailPresenter implements TaskDetailContract.Presenter {

private final TaskDetailContract.View mTaskDetailView;

private final UseCaseHandler mUseCaseHandler;

private final GetTask mGetTask;

private final CompleteTask mCompleteTask;

private final ActivateTask mActivateTask;

private final DeleteTask mDeleteTask;

public TaskDetailPresenter(@NonNull UseCaseHandler useCaseHandler,

@Nullable String taskId,

@NonNull TaskDetailContract.View taskDetailView,

@NonNull GetTask getTask,

@NonNull CompleteTask completeTask,

@NonNull ActivateTask activateTask,

@NonNull DeleteTask deleteTask) {

mUseCaseHandler = checkNotNull(useCaseHandler, "useCaseHandler cannot be null!");

mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!");

mGetTask = checkNotNull(getTask, "getTask cannot be null!");

mCompleteTask = checkNotNull(completeTask, "completeTask cannot be null!");

mActivateTask = checkNotNull(activateTask, "activateTask cannot be null!");

mDeleteTask = checkNotNull(deleteTask, "deleteTask cannot be null!");

mTaskDetailView.setPresenter(this);

}

}

Use case

Background executor

Presenter implementation

public class TaskDetailPresenter implements TaskDetailContract.Presenter {

private void openTask() {

mTaskDetailView.setLoadingIndicator(true);

mUseCaseHandler.execute(mGetTask, new GetTask.RequestValues(mTaskId),

new UseCase.UseCaseCallback<GetTask.ResponseValue>() {

@Override

public void onSuccess(GetTask.ResponseValue response) {

Task task = response.getTask();

mTaskDetailView.setLoadingIndicator(false);

showTask(task);

}

@Override

public void onError() {

mTaskDetailView.showMissingTask();

}

});

}

}

View operation

View operationView operation

View operation

Model operation

Use case implementationpublic class GetTask extends UseCase<GetTask.RequestValues, GetTask.ResponseValue> {

private final TasksRepository mTasksRepository;

public GetTask(@NonNull TasksRepository tasksRepository) {

mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");

}

@Override

protected void executeUseCase(final RequestValues values) {

mTasksRepository.getTask(values.getTaskId(), new TasksDataSource.GetTaskCallback() {

@Override

public void onTaskLoaded(Task task) {

if (task != null) {

ResponseValue responseValue = new ResponseValue(task);

getUseCaseCallback().onSuccess(responseValue);

} else {

getUseCaseCallback().onError();

}

}

@Override

public void onDataNotAvailable() {

getUseCaseCallback().onError();

}

});

}

}

RxJava

What is RxJava

“RxJava is a Java VM implementation of Reactive Extensions: a library for composing asynchronous and event-based programs by using observable sequences.”

“It extends the observer pattern to support sequences of data/events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety and concurrent data structures.”

Ref: https://github.com/ReactiveX/RxJava

View implementation

public class TaskDetailFragment extends Fragment implements TaskDetailContract.View {

@Override

public void onResume() {

super.onResume();

mPresenter.subscribe();

}

@Override

public void onPause() {

super.onPause();

mPresenter.unsubscribe();

}

@Override

public void setPresenter(@NonNull TaskDetailContract.Presenter presenter) {

mPresenter = checkNotNull(presenter);

}

}

Presenter implementationpublic class TaskDetailPresenter implements TaskDetailContract.Presenter {

@NonNull private final TasksRepository mTasksRepository;

@NonNull private final TaskDetailContract.View mTaskDetailView;

@NonNull private final BaseSchedulerProvider mSchedulerProvider;

@NonNull private CompositeSubscription mSubscriptions;

public TaskDetailPresenter(@Nullable String taskId,

@NonNull TasksRepository tasksRepository,

@NonNull TaskDetailContract.View taskDetailView,

@NonNull BaseSchedulerProvider schedulerProvider) {

mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");

mTaskDetailView = checkNotNull(taskDetailView, "taskDetailView cannot be null!");

mSchedulerProvider = checkNotNull(schedulerProvider, "schedulerProvider cannot be null");

mSubscriptions = new CompositeSubscription();

mTaskDetailView.setPresenter(this);

}

@Override

public void subscribe() {

openTask();

}

@Override

public void unsubscribe() {

mSubscriptions.clear();

}

}

Presenter implementationpublic class TaskDetailPresenter implements TaskDetailContract.Presenter {

private final TasksRepository mTasksRepository;

private final TaskDetailContract.View mTaskDetailView;

private final BaseSchedulerProvider mSchedulerProvider;

private CompositeSubscription mSubscriptions;

private void openTask() {

if (Strings.isNullOrEmpty(mTaskId)) {

mTaskDetailView.showMissingTask();

return;

}

mTaskDetailView.setLoadingIndicator(true);

mSubscriptions.add(mTasksRepository

.getTask(mTaskId)

.subscribeOn(mSchedulerProvider.computation())

.observeOn(mSchedulerProvider.ui())

.subscribe(

// onNext

this::showTask,

// onError

throwable -> {

},

// onCompleted

() -> mTaskDetailView.setLoadingIndicator(false)));

}

}

View operation

View operation

View operation

View operation

Model operation

Model implementationpublic class TasksRepository implements TasksDataSource {

private final TasksDataSource mTasksRemoteDataSource;

private final TasksDataSource mTasksLocalDataSource;

@VisibleForTesting Map<String, Task> mCachedTasks;

@VisibleForTesting boolean mCacheIsDirty = false;

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,

@NonNull TasksDataSource tasksLocalDataSource) {

mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);

mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);

}

@Override

public Observable<List<Task>> getTasks() {

if (mCachedTasks != null && !mCacheIsDirty) {

return Observable.from(mCachedTasks.values()).toList();

} else if (mCachedTasks == null) {

mCachedTasks = new LinkedHashMap<>();

}

Observable<List<Task>> remoteTasks = getAndSaveRemoteTasks();

if (mCacheIsDirty) {

return remoteTasks;

} else {

Observable<List<Task>> localTasks = getAndCacheLocalTasks();

return Observable.concat(localTasks, remoteTasks)

.filter(tasks -> !tasks.isEmpty())

.first();

}

}

}

Observer pattern

Compose sequences

Model implementationpublic class TasksRepository implements TasksDataSource {

private final TasksDataSource mTasksRemoteDataSource;

private final TasksDataSource mTasksLocalDataSource;

@VisibleForTesting Map<String, Task> mCachedTasks;

@VisibleForTesting boolean mCacheIsDirty = false;

private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,

@NonNull TasksDataSource tasksLocalDataSource) {

mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);

mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);

}

@Override

public Observable<List<Task>> getTasks() {

if (mCachedTasks != null && !mCacheIsDirty) {

return Observable.from(mCachedTasks.values()).toList();

} else if (mCachedTasks == null) {

mCachedTasks = new LinkedHashMap<>();

}

Observable<List<Task>> remoteTasks = getAndSaveRemoteTasks();

if (mCacheIsDirty) {

return remoteTasks;

} else {

Observable<List<Task>> localTasks = getAndCacheLocalTasks();

return Observable.concat(localTasks, remoteTasks)

.filter(tasks -> !tasks.isEmpty())

.first();

}

}

}

private Observable<List<Task>> getAndCacheLocalTasks() {

return mTasksLocalDataSource.getTasks()

.flatMap(new Func1<List<Task>, Observable<List<Task>>>() {

@Override

public Observable<List<Task>> call(List<Task> tasks) {

return Observable.from(tasks)

.doOnNext(task ->

mCachedTasks.put(task.getId(), task))

.toList();

}

});

}

private Observable<List<Task>> getAndSaveRemoteTasks() {

return mTasksRemoteDataSource

.getTasks()

.flatMap(new Func1<List<Task>, Observable<List<Task>>>() {

@Override

public Observable<List<Task>> call(List<Task> tasks) {

return Observable.from(tasks).doOnNext(task -> {

mTasksLocalDataSource.saveTask(task);

mCachedTasks.put(task.getId(), task);

}).toList();

}

})

.doOnCompleted(() -> mCacheIsDirty = false);

}

Dagger

What is Dagger

“Dagger is a fully static, compile-time dependency injection framework for both Java and Android. It is an adaptation of an earlier version created by Square and now maintained by Google.”

“Dagger is a replacement for FactoryFactory classes that implements the dependency injection design pattern without the burden of writing the boilerplate. It allows you to focus on the interesting classes. Declare dependencies, specify how to satisfy them, and ship your app.”

Ref: https://google.github.io/dagger/

View and Presenter’s creation & bindingpublic class TaskDetailActivity extends AppCompatActivity {

@Inject TaskDetailPresenter mTaskDetailPresenter;

@Override

protected void onCreate(Bundle savedInstanceState) {

TaskDetailFragment taskDetailFragment =

(TaskDetailFragment) getSupportFragmentManager()

.findFragmentById(R.id.contentFrame);

if (taskDetailFragment == null) {

taskDetailFragment = TaskDetailFragment.newInstance(taskId);

ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),

taskDetailFragment, R.id.contentFrame);

}

DaggerTaskDetailComponent.builder()

.taskDetailPresenterModule(

new TaskDetailPresenterModule(taskDetailFragment,

taskId))

.tasksRepositoryComponent(((ToDoApplication) getApplication())

.getTasksRepositoryComponent()).build()

.inject(this);

}

}

Field injection

Inject presenter Provide view

Dagger configurationpublic class TaskDetailActivity extends AppCompatActivity {

@Inject TaskDetailPresenter mTaskDetailPresenter;

@Override

protected void onCreate(Bundle savedInstanceState) {

// ...

DaggerTaskDetailComponent.builder()

.taskDetailPresenterModule(

new TaskDetailPresenterModule(taskDetailFragment, taskId))

.tasksRepositoryComponent(((ToDoApplication) getApplication())

.getTasksRepositoryComponent()).build()

.inject(this);

}

}

@FragmentScoped

@Component(dependencies = TasksRepositoryComponent.class,

modules = TaskDetailPresenterModule.class)

public interface TaskDetailComponent {

void inject(TaskDetailActivity taskDetailActivity);

}

Dagger configurationpublic class TaskDetailActivity extends AppCompatActivity {

@Inject TaskDetailPresenter mTaskDetailPresenter;

@Override

protected void onCreate(Bundle savedInstanceState) {

// ...

DaggerTaskDetailComponent.builder()

.taskDetailPresenterModule(

new TaskDetailPresenterModule(taskDetailFragment, taskId))

.tasksRepositoryComponent(((ToDoApplication) getApplication())

.getTasksRepositoryComponent()).build()

.inject(this);

}

}

@Module

public class TaskDetailPresenterModule {

private final TaskDetailContract.View mView;

private final String mTaskId;

public TaskDetailPresenterModule(

TaskDetailContract.View view, String taskId) {

mView = view;

mTaskId = taskId;

}

@Provides

TaskDetailContract.View provideTaskDetailContractView() {

return mView;

}

@Provides

String provideTaskId() {

return mTaskId;

}

}

Dagger configurationpublic class TaskDetailActivity extends AppCompatActivity {

@Inject TaskDetailPresenter mTaskDetailPresenter;

@Override

protected void onCreate(Bundle savedInstanceState) {

// ...

DaggerTaskDetailComponent.builder()

.taskDetailPresenterModule(

new TaskDetailPresenterModule(taskDetailFragment, taskId))

.tasksRepositoryComponent(((ToDoApplication) getApplication())

.getTasksRepositoryComponent()).build()

.inject(this);

}

} @Singleton

@Component(modules = {TasksRepositoryModule.class,

ApplicationModule.class})

public interface TasksRepositoryComponent {

TasksRepository getTasksRepository();

}

@Module

public class TasksRepositoryModule {

@Singleton @Provides @Local

TasksDataSource provideTasksLocalDataSource(Context context) {

return new TasksLocalDataSource(context);

}

@Singleton @Provides @Remote

TasksDataSource provideTasksRemoteDataSource() {

return new FakeTasksRemoteDataSource();

}

}

@Module

public final class ApplicationModule {

private final Context mContext;

ApplicationModule(Context context) {

mContext = context;

}

@Provides

Context provideContext() {

return mContext;

}

}

Presenter implementation

final class TaskDetailPresenter implements TaskDetailContract.Presenter

{

private TasksRepository mTasksRepository;

private TaskDetailContract.View mTaskDetailView;

@Nullable String mTaskId;

@Inject

TaskDetailPresenter(@Nullable String taskId,

TasksRepository tasksRepository,

TaskDetailContract.View taskDetailView) {

mTasksRepository = tasksRepository;

mTaskDetailView = taskDetailView;

mTaskId = taskId;

}

@Inject

void setupListeners() {

mTaskDetailView.setPresenter(this);

}

}

Constructor injection

Method injection