帶你看 Google RxJava+MVP Sample

為什么要看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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,909評(píng)論 25 709
  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,857評(píng)論 2 45
  • 完成圖 是用今天的sand 作的暗線,中間的部分不知道怎么用陰影,就沒(méi)畫(huà)了。 day 1--day7的暗線,我感覺(jué)...
    M有如果閱讀 286評(píng)論 3 8
  • 一 “每到深夜時(shí),我身上的每一個(gè)細(xì)胞都爭(zhēng)相和我訴說(shuō)它們的心事,可我只能安慰它們:再等等,不要急。你們要跟我一起變成...
    廉子閱讀 954評(píng)論 7 17
  • 我們家院子?xùn)|北角的幾棵樹(shù)組成了一道籬笆墻,把我家和鄰居家分開(kāi)??爝^(guò)年了,我和老爸要修剪樹(shù)枝。這幾棵樹(shù)已經(jīng)跟隨我們多...
    Ironlad閱讀 1,147評(píng)論 3 3

友情鏈接更多精彩內(nèi)容