為什么要看Google的sample?
Google 自己寫(xiě)的不一定就是最好的,但是它代表了一種標(biāo)準(zhǔn)。養(yǎng)成一種良好的編碼習(xí)慣是十分重要的。而讓彼此都能看得懂對(duì)方的代碼,最重要的代碼簡(jiǎn)潔。而 Google 一直是簡(jiǎn)潔的代表,不是嗎?
Github 地址
看這個(gè) sample 項(xiàng)目還有許多分支呢! 大家各取所需吧。
完成的samples
Stable samples
todo-mvp/ - Basic Model-View-Presenter architecture.
todo-mvp-loaders/ - Based on todo-mvp, fetches data using Loaders.
todo-databinding/ - Based on todo-mvp, uses the Data Binding Library.
todo-mvp-clean/ - Based on todo-mvp, uses concepts from Clean Architecture.
todo-mvp-dagger/ - Based on todo-mvp, uses Dagger2 for Dependency Injection
todo-mvp-contentproviders/ - Based on todo-mvp-loaders, fetches data using Loaders and uses Content Providers
todo-mvp-rxjava/ - Based on todo-mvp, uses RxJava for concurrency and data layer abstraction.
進(jìn)展中的samples
Samples in progress
dev-todo-mvp-tablet/ - Based on todo-mvp, adds a master/detail view for tablets.
Also, see "New sample" issues for planned samples.
其余的分支
External samples
External samples are variants that may not be in sync with the rest of the branches.
todo-mvp-fragmentless/ - Based on todo-mvp, uses Android views instead of Fragments.
todo-mvp-conductor/ - Based on todo-mvp, uses the Conductor framework to refactor to a single Activity architecture.
是不是覺(jué)得想要學(xué)的話時(shí)間不夠呢?所以放棄生活中一些不重要的瑣事吧。
大多數(shù)情況下選擇不做什么,比選擇要做什么顯得更重要呢。畢竟時(shí)間有限,要做的事太多。
有時(shí)候想想,寫(xiě)了博客自己真的不一定會(huì)再去看一遍。但是寫(xiě)了博客自己真的記得更清楚了,在我看來(lái)寫(xiě)博客就像是對(duì)技術(shù)的告白,告訴它,你有多喜歡用它,或者多么幸運(yùn)遇見(jiàn)她。所以寫(xiě)博客的時(shí)候是開(kāi)心的話,為什么不寫(xiě)呢?哈哈。
有關(guān)函數(shù)式編程,我之前覺(jué)得很 cool??赐赀@個(gè) sample,確實(shí)很 cool。
下面就記錄一下我在看 mvp rxjava 項(xiàng)目中學(xué)到的一些東西。
Gradle項(xiàng)目配置
將依賴庫(kù)版本號(hào)定義在根 build.gradle 中,因?yàn)橐粋€(gè)復(fù)雜的項(xiàng)目可能存在多個(gè) module。如果分別定義在不同的 module 中,到時(shí)候版本統(tǒng)一更新一定是一個(gè)頭疼的問(wèn)題。
所以你需要這樣做:
在跟 build.gradle 中定義所需的版本號(hào):
// Define versions in a single place
ext {
// Sdk and tools
minSdkVersion = 10
targetSdkVersion = 22
compileSdkVersion = 23
buildToolsVersion = '23.0.2'
// App dependencies
supportLibraryVersion = '24.1.1'
guavaVersion = '18.0'
junitVersion = '4.12'
mockitoVersion = '1.10.19'
powerMockito = '1.6.2'
hamcrestVersion = '1.3'
runnerVersion = '0.4.1'
rulesVersion = '0.4.1'
espressoVersion = '2.2.1'
rxjavaVersion = '1.1.8'
rxandroidVersion = '1.2.1'
sqlbriteVersion = '0.7.0'
}
在 module 中引用版本號(hào):
/*
Dependency versions are defined in the top level build.gradle file. This helps keeping track of
all versions in a single place. This improves readability and helps managing project complexity.
*/
dependencies {
// App's dependencies, including test
compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:cardview-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:design:$rootProject.supportLibraryVersion"
......
// Resolve conflicts between main and test APK:
androidTestCompile "com.android.support:support-annotations:$rootProject.supportLibraryVersion"
androidTestCompile "com.android.support:support-v4:$rootProject.supportLibraryVersion"
androidTestCompile "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion"
}
關(guān)于RxJava
沒(méi)有用過(guò) Rxjava 的可以看看這篇文章
BasePresenter
包含了訂閱,和取消訂閱
public interface BasePresenter {
void subscribe();
void unsubscribe();
}
BaseView
這是必須的,每個(gè)fragment或activity必須setPresenter吧。
public interface BaseView<T> {
void setPresenter(T presenter);
}
util
工具類,耦合性比較低,先看看。
ActivityUtils
幫助 Activity 加載 UI,定義了一個(gè)添加 Fragment 的方法。
/**
* This provides methods to help Activities load their UI.
*/
public class ActivityUtils {
/**
* The {@code fragment} is added to the container view with id {@code frameId}. The operation is
* performed by the {@code fragmentManager}.
*
*/
public static void addFragmentToActivity (@NonNull FragmentManager fragmentManager,
@NonNull Fragment fragment, int frameId) {
checkNotNull(fragmentManager);
checkNotNull(fragment);
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(frameId, fragment);
transaction.commit();
}
}
EspressoIdlingResource
閑置資源的靜態(tài)對(duì)象,僅僅在 build type 為 mock 時(shí)有效。這個(gè)還不是很懂,有待了解。
/**
* Contains a static reference to {@link IdlingResource}, only available in the 'mock' build type.
*/
public class EspressoIdlingResource {
private static final String RESOURCE = "GLOBAL";
private static sampleCountingIdlingResource mCountingIdlingResource =
new sampleCountingIdlingResource(RESOURCE);
public static void increment() {
mCountingIdlingResource.increment();
}
public static void decrement() {
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource() {
return mCountingIdlingResource;
}
}
sampleCountingIdlingResource
EspressoIdlingResource,的具體實(shí)現(xiàn)。在訪問(wèn)UI的塊測(cè)試中,這個(gè)類可以用來(lái)包裝操作。有待了解。
public final class sampleCountingIdlingResource implements IdlingResource {
private final String mResourceName;
private final AtomicInteger counter = new AtomicInteger(0);
// written from main thread, read from any thread.
private volatile ResourceCallback resourceCallback;
/**
* Creates a sampleCountingIdlingResource
*
* @param resourceName the resource name this resource should report to Espresso.
*/
public sampleCountingIdlingResource(String resourceName) {
mResourceName = checkNotNull(resourceName);
}
@Override
public String getName() {
return mResourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
/**
* Increments the count of in-flight transactions to the resource being monitored.
*/
public void increment() {
counter.getAndIncrement();
}
/**
* Decrements the count of in-flight transactions to the resource being monitored.
*
* If this operation results in the counter falling below 0 - an exception is raised.
*
* @throws IllegalStateException if the counter is below 0.
*/
public void decrement() {
int counterVal = counter.decrementAndGet();
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle now! Tell espresso.
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
}
if (counterVal < 0) {
throw new IllegalArgumentException("Counter has been corrupted!");
}
}
}
schedulers 包
共有6種調(diào)度器,可以使用它靈活的切換線程。
| 調(diào)度器類型 | 效果 |
|---|---|
| Schedulers.computation() | 用于計(jì)算任務(wù), 如事件循環(huán)或和回調(diào)處理,不要用于IO操作(IO操作請(qǐng)使用Schedulers.io());默認(rèn)線程數(shù)等于處理器的數(shù)量 |
| Schedulers.from(executor) | 使用指定的Executor作為調(diào)度器 |
| Schedulers.immediate() | 在當(dāng)前線程立即開(kāi)始執(zhí)行任務(wù) |
| Schedulers.io() | 用于IO密集型任務(wù),如異步阻塞IO操作,這個(gè)調(diào)度器的線程池會(huì)根據(jù)需要增長(zhǎng);對(duì)于普通的計(jì)算任務(wù),請(qǐng)使用Schedulers.computation();Schedulers.io()默認(rèn)是一個(gè)CachedThreadScheduler,很像一個(gè)有線程緩存的新線程調(diào)度器 |
| Schedulers.newThread() | 為每個(gè)任務(wù)創(chuàng)建一個(gè)新線程 |
| Schedulers.trampoline() | 當(dāng)其它排隊(duì)的任務(wù)完成后,在當(dāng)前線程排隊(duì)開(kāi)始執(zhí)行 |
BaseSchedulerProvider
用來(lái)提供不同的調(diào)度器。
/**
* Allow providing different types of {@link Scheduler}s.
*/
public interface BaseSchedulerProvider {
@NonNull
Scheduler computation();
@NonNull
Scheduler io();
@NonNull
Scheduler ui();
}
ImmediateSchedulerProvider
提供當(dāng)前線程的調(diào)度器,在當(dāng)前線程立即執(zhí)行這些操作。
/**
* Implementation of the {@link BaseSchedulerProvider} making all {@link Scheduler}s immediate.
*/
public class ImmediateSchedulerProvider implements BaseSchedulerProvider {
@NonNull
@Override
public Scheduler computation() {
return Schedulers.immediate();
}
@NonNull
@Override
public Scheduler io() {
return Schedulers.immediate();
}
@NonNull
@Override
public Scheduler ui() {
return Schedulers.immediate();
}
}
SchedulerProvider
提供不同線程的調(diào)度器用于計(jì)算,IO和UI的操作,并單例。
/**
* Provides different types of schedulers.
*/
public class SchedulerProvider implements BaseSchedulerProvider {
@Nullable
private static SchedulerProvider INSTANCE;
// Prevent direct instantiation.
private SchedulerProvider() {
}
public static synchronized SchedulerProvider getInstance() {
if (INSTANCE == null) {
INSTANCE = new SchedulerProvider();
}
return INSTANCE;
}
@Override
@NonNull
public Scheduler computation() {
return Schedulers.computation();
}
@Override
@NonNull
public Scheduler io() {
return Schedulers.io();
}
@Override
@NonNull
public Scheduler ui() {
return AndroidSchedulers.mainThread();
}
}
數(shù)據(jù)操作
那么這個(gè)項(xiàng)目從哪里看比較好呢? 我喜歡從數(shù)據(jù)源開(kāi)始看。
這個(gè)項(xiàng)目有關(guān)數(shù)據(jù)所有的操作都是通過(guò)Sqlite完成的,但是demo里用數(shù)據(jù)庫(kù)模擬了一個(gè)遠(yuǎn)程的連接。
我們找到data目錄。這個(gè)目錄下有一個(gè)包source,和一個(gè)普通的model類Task。
source下分為local,remote,TasksDataSource接口和TasksDataSource類(TasksDataSource接口的具體實(shí)現(xiàn))。
TasksDataSource
支個(gè)接口定義了了數(shù)據(jù)有關(guān)的所有操作。
public interface TasksDataSource {
Observable<List<Task>> getTasks();
Observable<Task> getTask(@NonNull String taskId);
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);
}
local包
local代表本地?cái)?shù)據(jù)。在這個(gè)包下,存放和本地?cái)?shù)據(jù)相關(guān)的類。
首先對(duì)數(shù)據(jù)庫(kù)進(jìn)行基本定義。
TasksPersistenceContract對(duì)象用于存儲(chǔ)本地?cái)?shù)據(jù)庫(kù)數(shù)據(jù)。
不需要被實(shí)例化的類,需私有化構(gòu)造函數(shù)。
/**
* The contract used for the db to save the tasks locally.
*/
public final class TasksPersistenceContract {
// To prevent someone from accidentally instantiating the contract class,
// give it an empty constructor.
private TasksPersistenceContract() {}
/* Inner class that defines the table contents */
/*內(nèi)部類用來(lái)聲明表的內(nèi)容*/
public static abstract class TaskEntry implements BaseColumns {
public static final String TABLE_NAME = "task";
public static final String COLUMN_NAME_ENTRY_ID = "entryid";
public static final String COLUMN_NAME_TITLE = "title";
public static final String COLUMN_NAME_DESCRIPTION = "description";
public static final String COLUMN_NAME_COMPLETED = "completed";
}
}
TasksDbHelper類用于數(shù)據(jù)庫(kù)的創(chuàng)建,升級(jí),降級(jí)等操作。
public class TasksDbHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "Tasks.db";
private static final String TEXT_TYPE = " TEXT";
private static final String BOOLEAN_TYPE = " INTEGER";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + TasksPersistenceContract.TaskEntry.TABLE_NAME + " (" +
TasksPersistenceContract.TaskEntry._ID + TEXT_TYPE + " PRIMARY KEY," +
TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION + TEXT_TYPE + COMMA_SEP +
TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED + BOOLEAN_TYPE +
" )";
public TasksDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Not required as at version 1
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Not required as at version 1
}
}
數(shù)據(jù)庫(kù)相關(guān)操作
TasksLocalDataSource是TasksDataSource接口在本地?cái)?shù)據(jù)庫(kù)操作的具體實(shí)現(xiàn)。
這里使用到了SQLBrite,可以看看這篇:戳這。
SqlBrite是對(duì) Android 系統(tǒng)的 SQLiteOpenHelper 的封裝,對(duì)SQL操作引入了響應(yīng)式語(yǔ)義 (Rx)(用來(lái)在 RxJava 中使用)
/**
* Created by heinika on 16-11-27.
* 有關(guān)數(shù)據(jù)庫(kù)操作的統(tǒng)一管理
*/
public class TasksLocalDataSource implements TasksDataSource {
private static TasksDataSource INSTANCE;
@NonNull
private final BriteDatabase mDatabaseHelper;
private Func1<Cursor,Task> mTaskMapperFunction;
private TasksLocalDataSource(Context context, BaseSchedulerProvider schedulerProvider){
TasksDbHelper dbHelper = new TasksDbHelper(context);
SqlBrite sqlBrite = SqlBrite.create();
mDatabaseHelper = sqlBrite.wrapDatabaseHelper(dbHelper,schedulerProvider.io());
mTaskMapperFunction = new Func1<Cursor,Task>(){
@Override
public Task call(Cursor c) {
//從數(shù)據(jù)庫(kù)中取到task的數(shù)據(jù)并返回
String itemId = c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_ENTRY_ID));
String title = c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_TITLE));
String description =
c.getString(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_DESCRIPTION));
boolean completed =
c.getInt(c.getColumnIndexOrThrow(TaskEntry.COLUMN_NAME_COMPLETED)) == 1;
return new Task(title, description, itemId, completed);
}
};
}
@Override
public Observable<List<Task>> getTasks() {
String[] projection = {
TaskEntry.COLUMN_NAME_ENTRY_ID,
TaskEntry.COLUMN_NAME_TITLE,
TaskEntry.COLUMN_NAME_DESCRIPTION,
TaskEntry.COLUMN_NAME_COMPLETED
};
String sql = String.format("SELECT %s FROM %s", TextUtils.join(",", projection), TaskEntry.TABLE_NAME);
return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)
.mapToList(mTaskMapperFunction);
}
@Override
public Observable<Task> getTask(@NonNull String taskId) {
String[] projection = {
TaskEntry.COLUMN_NAME_ENTRY_ID,
TaskEntry.COLUMN_NAME_TITLE,
TaskEntry.COLUMN_NAME_DESCRIPTION,
TaskEntry.COLUMN_NAME_COMPLETED
};
//TextUtils是Android提供的有關(guān)text的工具類
String sql = String.format("SELECT %s FROM %s WHERE %s LIKE ?",
TextUtils.join(",", projection), TaskEntry.TABLE_NAME, TaskEntry.COLUMN_NAME_ENTRY_ID);
return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql, taskId)
.mapToOneOrDefault(mTaskMapperFunction, null);
}
@Override
public void saveTask(@NonNull Task task) {
checkNotNull(task);
ContentValues values = new ContentValues();
values.put(TaskEntry.COLUMN_NAME_ENTRY_ID, task.getId());
values.put(TaskEntry.COLUMN_NAME_TITLE, task.getTitle());
values.put(TaskEntry.COLUMN_NAME_DESCRIPTION, task.getDescription());
values.put(TaskEntry.COLUMN_NAME_COMPLETED, task.isCompleted());
mDatabaseHelper.insert(TaskEntry.TABLE_NAME, values, SQLiteDatabase.CONFLICT_REPLACE);
}
@Override
public void completeTask(@NonNull Task task) {
completeTask(task.getId());
}
/**
* 完成任務(wù)
* @param taskId 任務(wù)id
*/
@Override
public void completeTask(@NonNull String taskId) {
ContentValues values = new ContentValues();
values.put(TaskEntry.COLUMN_NAME_COMPLETED, true);
String selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = {taskId};
mDatabaseHelper.update(TaskEntry.TABLE_NAME, values, selection, selectionArgs);
}
@Override
public void activateTask(@NonNull Task task) {
activateTask(task.getId());
}
/**
* 激活的任務(wù)
* @param taskId
*/
@Override
public void activateTask(@NonNull String taskId) {
ContentValues values = new ContentValues();
values.put(TaskEntry.COLUMN_NAME_COMPLETED, false);
String selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = {taskId};
mDatabaseHelper.update(TaskEntry.TABLE_NAME, values, selection, selectionArgs);
}
@Override
public void clearCompletedTasks() {
String selection = TaskEntry.COLUMN_NAME_COMPLETED + " LIKE ?";
String[] selectionArgs = {"1"};
mDatabaseHelper.delete(TaskEntry.TABLE_NAME, selection, selectionArgs);
}
@Override
public void refreshTasks() {
// Not required because the {@link TasksRepository} handles the logic of refreshing the
// tasks from all the available data sources.
// 不需要,因?yàn)門(mén)asksRepository控制著所有數(shù)據(jù)刷新的邏輯
}
@Override
public void deleteAllTasks() {
mDatabaseHelper.delete(TaskEntry.TABLE_NAME, null);
}
@Override
public void deleteTask(@NonNull String taskId) {
String selection = TaskEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = {taskId};
mDatabaseHelper.delete(TaskEntry.TABLE_NAME, selection, selectionArgs);
}
}
remote包
這個(gè)包下只有一個(gè)類TasksRemoteDataSource.java用來(lái)模擬遠(yuǎn)程地?cái)?shù)據(jù)連接。
因?yàn)槭悄M的,所以和TasksLocalDataSource.java幾乎一樣,只是多加了一個(gè)延時(shí)。
在現(xiàn)實(shí)情況下,這里用來(lái)處理各種網(wǎng)絡(luò)連接的api。
/**
* Implementation of the data source that adds a latency simulating network.
* 繼承datasourse并添加一個(gè)模擬的有延遲的網(wǎng)絡(luò)
*/
public class TasksRemoteDataSource implements TasksDataSource {
private static TasksRemoteDataSource INSTANCE;
private static final int SERVICE_LATENCY_IN_MILLIS = 5000;
private final static Map<String, Task> TASKS_SERVICE_DATA;
static {
TASKS_SERVICE_DATA = new LinkedHashMap<>(2);
addTask("Build tower in Pisa", "Ground looks good, no foundation work required.");
addTask("Finish bridge in Tacoma", "Found awesome girders at half the cost!");
}
public static TasksRemoteDataSource getInstance() {
if (INSTANCE == null) {
INSTANCE = new TasksRemoteDataSource();
}
return INSTANCE;
}
......
@Override
public void refreshTasks() {
// Not required because the {@link TasksRepository} handles the logic of refreshing the
// tasks from all the available data sources.
}
@Override
public void deleteAllTasks() {
TASKS_SERVICE_DATA.clear();
}
@Override
public void deleteTask(@NonNull String taskId) {
TASKS_SERVICE_DATA.remove(taskId);
}
}
TasksRepository
加載數(shù)據(jù)源到緩存的具體實(shí)現(xiàn)。
/**
* Concrete implementation to load tasks from the data sources into a cache.
* 加載數(shù)據(jù)源到緩存的具體實(shí)現(xiàn)
* <p/>
* For simplicity, this implements a dumb synchronisation between locally persisted data and data
* obtained from the server, by using the remote data source only if the local database doesn't
* exist or is empty.
* 舉一個(gè)簡(jiǎn)單的例子,啞同步:在選擇本地?cái)?shù)據(jù)和遠(yuǎn)程服務(wù)器的數(shù)據(jù)時(shí),只有當(dāng)本地不存在或者為空時(shí),才會(huì)從網(wǎng)上獲取數(shù)據(jù)。
*/
public class TasksRepository implements TasksDataSource {
@Nullable
private static TasksRepository INSTANCE = null;
@NonNull
private final TasksDataSource mTasksRemoteDataSource;
@NonNull
private final TasksDataSource mTasksLocalDataSource;
/**
* This variable has package local visibility so it can be accessed from tests.
*/
@VisibleForTesting
@Nullable
Map<String, Task> mCachedTasks;
/**
* Marks the cache as invalid, to force an update the next time data is requested. This variable
* has package local visibility so it can be accessed from tests.
*/
@VisibleForTesting
boolean mCacheIsDirty = false;
// Prevent direct instantiation.
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
@NonNull TasksDataSource tasksLocalDataSource) {
mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
}
/**
* Returns the single instance of this class, creating it if necessary.
*
* @param tasksRemoteDataSource the backend data source
* @param tasksLocalDataSource the device storage data source
* @return the {@link TasksRepository} instance
*/
public static TasksRepository getInstance(@NonNull TasksDataSource tasksRemoteDataSource,
@NonNull TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
/**
* Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
* next time it's called.
*/
public static void destroyInstance() {
INSTANCE = null;
}
/**
* Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
* available first.
*/
@Override
public Observable<List<Task>> getTasks() {
// Respond immediately with cache if available and not dirty
// 當(dāng)cache存在立即響應(yīng)
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 {
// Query the local storage if available. If not, query the network.
Observable<List<Task>> localTasks = getAndCacheLocalTasks();
return Observable.concat(localTasks, remoteTasks)
.filter(new Func1<List<Task>, Boolean>() {
@Override
public Boolean call(List<Task> tasks) {
return !tasks.isEmpty();
}
}).first();
}
}
statistics包
用來(lái)處理和展示任務(wù)的統(tǒng)計(jì)信息。
StatisticsContract
這個(gè)契約類用來(lái)規(guī)定View和Presenter的接口。將這兩個(gè)接口寫(xiě)在一起,一目了然。
/**
* This specifies the contract between the view and the presenter.
*/
public interface StatisticsContract {
interface View extends BaseView<Presenter> {
void setProgressIndicator(boolean active);
void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks);
void showLoadingStatisticsError();
boolean isActive();
}
interface Presenter extends BasePresenter {
}
}
StatisticsPresenter
StatisticsPresenter操作都在訂閱和取消訂閱中進(jìn)行。通過(guò)StatisticsPresenter你可以清晰的理解整個(gè)頁(yè)面在什么情況下發(fā)生了什么。
而關(guān)于UI的具體改變,并不關(guān)心。所以當(dāng)UI改變時(shí),你并不需要修改這個(gè)類,只需要修改fragment或activity就行了。
/**
* Listens to user actions from the UI ({@link StatisticsFragment}), retrieves the data and updates
* the UI as required.
*/
public class StatisticsPresenter implements StatisticsContract.Presenter {
@NonNull
private final TasksRepository mTasksRepository;
@NonNull
private final StatisticsContract.View mStatisticsView;
@NonNull
private final BaseSchedulerProvider mSchedulerProvider;
@NonNull
private CompositeSubscription mSubscriptions;
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView,
@NonNull BaseSchedulerProvider schedulerProvider) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "statisticsView cannot be null!");
mSchedulerProvider = checkNotNull(schedulerProvider, "schedulerProvider cannot be null");
mSubscriptions = new CompositeSubscription();
mStatisticsView.setPresenter(this);
}
@Override
public void subscribe() {
loadStatistics();
}
@Override
public void unsubscribe() {
mSubscriptions.clear();
}
private void loadStatistics() {
mStatisticsView.setProgressIndicator(true);
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
Observable<Task> tasks = mTasksRepository
.getTasks()
.flatMap(new Func1<List<Task>, Observable<Task>>() {
@Override
public Observable<Task> call(List<Task> tasks) {
return Observable.from(tasks);
}
});
Observable<Integer> completedTasks = tasks.filter(new Func1<Task, Boolean>() {
@Override
public Boolean call(Task task) {
return task.isCompleted();
}
}).count();
Observable<Integer> activeTasks = tasks.filter(new Func1<Task, Boolean>() {
@Override
public Boolean call(Task task) {
return task.isActive();
}
}).count();
Subscription subscription = Observable
.zip(completedTasks, activeTasks, new Func2<Integer, Integer, Pair<Integer, Integer>>() {
@Override
public Pair<Integer, Integer> call(Integer completed, Integer active) {
return Pair.create(active, completed);
}
})
.subscribeOn(mSchedulerProvider.computation())
.observeOn(mSchedulerProvider.ui())
.doOnTerminate(new Action0() {
@Override
public void call() {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
EspressoIdlingResource.decrement(); // Set app as idle.
}
}
})
.subscribe(new Action1<Pair<Integer, Integer>>() {
@Override
public void call(Pair<Integer, Integer> stats) {
mStatisticsView.showStatistics(stats.first, stats.second);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
mStatisticsView.showLoadingStatisticsError();
}
}, new Action0() {
@Override
public void call() {
mStatisticsView.setProgressIndicator(false);
}
});
mSubscriptions.add(subscription);
}
}
StatisticsActivity
展示統(tǒng)計(jì)信息的activity。在這個(gè)activity中設(shè)置了toolbar和導(dǎo)航欄,添加了statisticsFragment。
并創(chuàng)建了StatisticsPresenter,初始化了StatisticsPresenter中的TasksRepository,StatisticsView,SchedulerProvider。
/**
* Show statistics for tasks.
*/
public class StatisticsActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.statistics_act);
// Set up the toolbar.
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
ab.setTitle(R.string.statistics_title);
ab.setHomeAsUpIndicator(R.drawable.ic_menu);
ab.setDisplayHomeAsUpEnabled(true);
// Set up the navigation drawer.
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
if (navigationView != null) {
setupDrawerContent(navigationView);
}
StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager()
.findFragmentById(R.id.contentFrame);
if (statisticsFragment == null) {
statisticsFragment = StatisticsFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
statisticsFragment, R.id.contentFrame);
}
new StatisticsPresenter(
Injection.provideTasksRepository(getApplicationContext()), statisticsFragment,
Injection.provideSchedulerProvider());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Open the navigation drawer when the home icon is selected from the toolbar.
mDrawerLayout.openDrawer(GravityCompat.START);
return true;
}
return super.onOptionsItemSelected(item);
}
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.list_navigation_menu_item:
Intent intent =
new Intent(StatisticsActivity.this, TasksActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
break;
case R.id.statistics_navigation_menu_item:
// Do nothing, we're already on that screen
break;
default:
break;
}
// Close the navigation drawer when an item is selected.
menuItem.setChecked(true);
mDrawerLayout.closeDrawers();
return true;
}
});
}
}
StatisticsFragment
StatisticsFragment 實(shí)現(xiàn)了 StatisticsContract.View 接口,在 setPresenter 方法中傳入 Presenter,并在其他方法中實(shí)現(xiàn) UI 布局變化。
傳入的 presenter 中的 subscribe() 方法和 unsubscribe() 需要分別在 onResume() 和 onPause() 調(diào)用,用來(lái)獲取或釋放資源。
/**
* Main UI for the statistics screen.
*/
public class StatisticsFragment extends Fragment implements StatisticsContract.View {
private TextView mStatisticsTV;
private StatisticsContract.Presenter mPresenter;
public static StatisticsFragment newInstance() {
return new StatisticsFragment();
}
@Override
public void setPresenter(@NonNull StatisticsContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.statistics_frag, container, false);
mStatisticsTV = (TextView) root.findViewById(R.id.statistics);
return root;
}
@Override
public void onResume() {
super.onResume();
mPresenter.subscribe();
}
@Override
public void onPause() {
super.onPause();
mPresenter.unsubscribe();
}
@Override
public void setProgressIndicator(boolean active) {
if (active) {
mStatisticsTV.setText(getString(R.string.loading));
}
}
@Override
public void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks) {
if (numberOfCompletedTasks == 0 && numberOfIncompleteTasks == 0) {
mStatisticsTV.setText(getResources().getString(R.string.statistics_no_tasks));
} else {
String displayString = getResources().getString(R.string.statistics_active_tasks) + " "
+ numberOfIncompleteTasks + "\n" + getResources().getString(
R.string.statistics_completed_tasks) + " " + numberOfCompletedTasks;
mStatisticsTV.setText(displayString);
}
}
@Override
public void showLoadingStatisticsError() {
mStatisticsTV.setText(getResources().getString(R.string.statistics_error));
}
@Override
public boolean isActive() {
return isAdded();
}
}
剩下的包
還剩下 addedittask,taskdetail,tasks 這幾個(gè)包。他們的結(jié)構(gòu)和 statistics 相同,就不在描述了。
相關(guān)的測(cè)試
這個(gè)sample對(duì)測(cè)試還是相當(dāng)重視的,有需要的可以去學(xué)習(xí)一下。
個(gè)人博客地址:http://heinika.coding.me/2016/12/06/MyBlog/LearnFromMvpRxjavaSimple/