1. 程式人生 > >從零開始的Android新專案5

從零開始的Android新專案5

如期而至的Repository篇,內部實現則由Realm、Retrofit,以及記憶體級LruCache組成。
Repository,顧名思義,即倉庫,向上層遮蔽了資料來源和內部實現細節,不需要了解貨物來源,只需要拿走就行了。

由於篇幅問題,將分為上下兩篇,本篇主要介紹Retrofit的應用和Repository層組裝,下篇會講解本地快取(包括Realm和記憶體快取)以及基於異常的設計。

Why Repository

首先,為什麼我們需要Repository層呢?一言以蔽之,遮蔽細節。

上層(activity/fragment/presenter)不需要知道資料的細節(或者說 - 資料來源),來自於網路、資料庫,亦或是記憶體等等。如此,一來上層可以不用關心細節,二來底層可以根據需求修改,不會影響上層,兩者的分離用可以幫助協同開發。

舉些例子:
- 當現在是無網狀態,我希望列表能直接顯示上一次的資料,而不會是空頁面。
- 除非好友的使用者資料過期(比如超過一天),否則希望直接使用本地快取中的,但如果快取沒有,或者過期,則需要拉取並更新。
- 點贊後,即便請求還沒傳送或者沒有收到response,仍然希望顯示點贊後的狀態。
等等。

如果這些需求,我們都要實現在View或者Presenter中,就會導致充斥大量資料邏輯,目的不單一,難以維護。而Repository層就是來封裝這些邏輯的。

Overview

如圖,業務層只能看到repository介面。

Repository Overview

Retrofit

Retrofit是Android界網紅公司

Square所開發維護的一個HTTP網路庫,目前最新版本是2.0.2(截止2016年4月30日)。其內部使用了自家的OkHttp

關於Retrofit的實現機制啊簡介的,網上已經很多了,這裡我就不囉嗦了,官方文件見專案主頁。這裡主要講講實際專案中的應用實踐。

import

root build.gradle:

def retrofitVersion = "2.0.2"
def okHttpVersion = '3.2.0'

project.ext {
    libRetrofit = "com.squareup.retrofit2:retrofit:${retrofitVersion}
"
libRetrofitConverterGson = "com.squareup.retrofit2:converter-gson:${retrofitVersion}" libRetrofitAdapterRxJava = "com.squareup.retrofit2:adapter-rxjava:${retrofitVersion}" libOkHttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}" }

repository module的build.gradle:

dependencies {
    compile rootProject.ext.libRetrofit
    compile rootProject.ext.libRetrofitConverterGson
    compile rootProject.ext.libRetrofitAdapterRxJava
    compile rootProject.ext.libOkHttpLoggingInterceptor
}

OkHttpClient

自底向上地,我們需要一個OkHttpClient來設定給Retrofit,這裡作為例項,放出一段包含大部分你可能會用到的功能的Client建立程式碼,可以根據需要進行調整。

private OkHttpClient getClient() {
    // log用攔截器
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();

    // 開發模式記錄整個body,否則只記錄基本資訊如返回200,http協議版本等
    if (IS_DEV) {
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
    } else {
        logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
    }

    // 如果使用到HTTPS,我們需要建立SSLSocketFactory,並設定到client
    SSLSocketFactory sslSocketFactory = null;

    try {
        // 這裡直接建立一個不做證書串驗證的TrustManager
        final TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType)
                            throws CertificateException {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType)
                            throws CertificateException {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };

        // Install the all-trusting trust manager
        final SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        // Create an ssl socket factory with our all-trusting manager
        sslSocketFactory = sslContext.getSocketFactory();
    } catch (Exception e) {
        Logger.e(TAG, e.getMessage());
    }

    return new OkHttpClient.Builder()
            // HeadInterceptor實現了Interceptor,用來往Request Header新增一些業務相關資料,如APP版本,token資訊
            .addInterceptor(new HeadInterceptor())
            .addInterceptor(logging)
            // 連線超時時間設定
            .connectTimeout(10, TimeUnit.SECONDS)
            // 讀取超時時間設定
            .readTimeout(10, TimeUnit.SECONDS)
            .sslSocketFactory(sslSocketFactory)
            // 信任所有主機名
            .hostnameVerifier((hostname, session) -> true)
            // 這裡我們使用host name作為cookie儲存的key
            .cookieJar(new CookieJar() {
                private final HashMap<HttpUrl, List<Cookie>> cookieStore = new HashMap<>();

                @Override
                public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                    cookieStore.put(HttpUrl.parse(url.host()), cookies);
                }

                @Override
                public List<Cookie> loadForRequest(HttpUrl url) {
                    List<Cookie> cookies = cookieStore.get(HttpUrl.parse(url.host()));
                    return cookies != null ? cookies : new ArrayList<>();
                }
            })
            .build();
}

如上包含了大部分你可能需要的特性,可以自由進行組合。

RxJava非同步請求

public static MrService getInstance() {
    if (mInstance == null) {
        synchronized (MrService.class) {
            if (mInstance == null) {
                mInstance = new MrService();
            }
        }
    }
    return mInstance;
}

private MrService() {
    this(true);
}

private MrService(boolean useRxJava) {
    Retrofit.Builder builder = new Retrofit.Builder()
            .baseUrl(IS_DEV ? API_DEV_URL : API_PRODUCT_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(getClient());
    if (useRxJava) {
        builder.addCallAdapterFactory(RxJavaCallAdapterFactory.create());
    }
    mRetrofit = builder.build();
}

對應API請求類如

public interface SystemApi {
    ...
    @FormUrlEncoded
    @POST("user/feedback")
    Observable<MrResponse> feedback(@Field("content") String content,
                                    @Field("model_name") String modelName,
                                    @Field("system_version") String systemVersion,
                                    @Field("img_keys") List<String> imageKeyList);
}

同步請求

有時候我們需要做同步請求,比如提供結果給一些第三方庫,它們可能需要直接返回對應資料(像我最近碰到的融雲….),而我們只需要拉資料同步返回,對其所線上程和呼叫事件均一臉懵逼。

這時候就需要建立一個同步的retrofit客戶端,其實就是不要去使用RxJava的adapter啦。

public static MrService getSynchronousInstance() {
    if (mSyncInstance == null) {
        synchronized (MrService.class) {
            if (mSyncInstance == null) {
                mSyncInstance = new MrService(false);
            }
        }
    }
    return mSyncInstance;
}

對應地,我們需要定義請求類,這裡我們需要使用Call<>去包一下最終解析物件的類。

public interface RongCloudApi {
    @FormUrlEncoded
    @POST("im/getGroupInfo")
    Call<MrResponse> getGroupInfoSynchronous(@Field("group_id") String groupId);

    @FormUrlEncoded
    @POST("user/nameCardLite")
    Call<MrResponse> getNameCardLiteSynchronous(@Field("uid") String userId);
}

資料格式解析

資料的解析當然是必不可少的一環了,常用格式對應的序列化庫以retrofit官網為例:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

部分高大上公司可能自己使用內部的二進位制格式,自己實現ConverterFactory去解析就行了。

這裡以最常用的json為例,使用GsonConverterFactory,良好的資料結構通常都會帶有狀態碼和對應資訊:

@SerializedName("status_no")
private int statusCode;

@SerializedName("status_msg")
private String statusMessage;

根據statusCode可以快速判斷是否出現錯誤,通常0或者某個正數為正確,負數則根據和伺服器的協定做不同處理。
這裡對Gson的bean,推薦使用外掛GsonFormat,生成起來很方便。

至於具體的資料,則有兩種方案,一是使用data作為key把具體資料套起來,內部則使用K/V進行儲存,保證不存在不規範的直接丟一個array在data裡面的情形。

二次的組合解析

public class CommonResponse {

    @SerializedName("status_no")
    private int statusCode;

    @SerializedName("status_msg")
    private String statusMessage;

    @SerializedName("time")
    private long time;

    @SerializedName("data")
    public Object data;

    // setter and getter
}

二次組合的解析通過將建立一個通用的Response Bean來做泛解析,如果statusCode表明介面請求成功,則繼續解析data:

public static <T> Observable<T> extractData(Observable<MrResponse> observable, Class<T> clazz) {
    return observable.flatMap(response -> {
        if (response == null) {
            return Observable.error(new NetworkConnectionException());
        } else if (response.getStatusCode() == ResponseException.STATUS_CODE_SUCCESS) {
            return Observable.just(mGson.fromJson(mGson.toJson(response.data), clazz));
        } else {
            Logger.e(TAG, response.data);
            return Observable.error(new ResponseException(response));
        }
    });
}

呼叫則如:

@Override
public Observable<AlbumApiResult> listPhoto(String uid) {
    return RepositoryUtils.extractData(mAlbumApi.listPhoto(uid), AlbumApiResult.class);
}

所有介面都可以通過RepositoryUtils.extractData()進行泛型呼叫。

如此一來,如果response為空,我們僅在statusCode正確時才會去解析具體的資料,否則丟擲對應的異常(基於異常的資料層設計在下面會具體講)。

單次的繼承處理

上一種處理方式儘管看起來很優雅,但是存在一個問題,就是會重複解析,當statusCode正確時,會對data的object再次進行json處理。如果確實是error,比如statusCode為-1、-2這種,確實節省了開銷,因為gson會去反射構造對應類的adapter,解析所有欄位,建立對應的BoundField。

但考慮到大部分情況下還是正確的response居多,所以也可以使用繼承的結構,我們建立BaseResponse存放通用欄位,其他所有Gson Bean則繼承該BaseResponse

public class BaseResponse {

    @SerializedName("status_no")
    private int statusCode;

    @SerializedName("status_msg")
    private String statusMessage;

    @SerializedName("time")
    private long time;

    // setter and getter
}

public class ConcreteResponse extends BaseResponse {

    @SerializedName("other_fields")
    private String otherFields;

    // ...
}

對應的判斷和error丟擲可以參照上小節的,這裡就不贅述了。

Repository層組裝實現

組裝即根據組合各個資料來源,如此又分為直接在實現方法中組合結果,亦或是通過DataStoreFactory進行封裝。根據複雜度和個人喜好而定,畢竟使用後者需要新增好多類,相對來說有一點重。

基於介面的設計實現

拿一個最簡單的repository,七牛Repository來作例子:

public interface QiniuRepository {
    Observable<QiniuToken> getQiniuUploadToken();
}
public class QiniuDataRepository implements QiniuRepository {

    @Inject
    protected QiniuApi mQiniuApi;

    @Inject
    public QiniuDataRepository() {
    }

    @Override
    public Observable<QiniuToken> getQiniuUploadToken() {
        return RepositoryUtils.extractData(mQiniuApi.getQiniuUploadToken(), QiniuToken.class);
    }
}

DataStoreFactory

使用DataStoreFactory封裝資料來源:

@Singleton
public class UserDataStoreFactory {

    private final Context mContext;
    private final UserCache mUserCache;

    @Inject
    protected UserApi mUserApi;

    @Inject
    public UserDataStoreFactory(Context context, UserCache userCache) {
        if (context == null || userCache == null) {
            throw new IllegalArgumentException("Constructor parameters cannot be null!!!");
        }
        mContext = context.getApplicationContext();
        mUserCache = userCache;
    }

    /**
     * Create {@link UserDataStore} from a user id.
     */
    public UserDataStore create(String userId) {
        UserDataStore userDataStore;

        if (!mUserCache.isExpired() && mUserCache.isCached(userId)) {
            userDataStore = new DiskUserDataStore(mUserCache);
        } else {
            userDataStore = createCloudDataStore();
        }

        return userDataStore;
    }

    /**
     * Create {@link UserDataStore} to retrieve data from the Cloud.
     */
    public UserDataStore createCloudDataStore() {
        return new CloudUserDataStore(mUserApi, mUserCache);
    }
}

老實說這樣的話,一來要寫很多方法和介面,二來通過Factory判斷建立哪種DataStore還是挺麻煩的,比如使用者主頁資料我們可以判斷,但登陸登出這些,就需要直接指定createCloudDataStore()了,所以個人認為意義不大。

在實現方法中組合

如下是使用DBFlow和網路Api進行組合的一個list獲取介面。

我們使用RxJava的concat組合2個Observable,前者從cache(資料庫)獲取資料,後者從網路Api獲取資料,通常資料庫當然會更快。我們還保留了一個引數isForceRefresh來保證在某些情況下可以強制從網路獲取資料。

    @Override
    public Observable<List<OperationPositionWrapper>> getHome(final boolean isForceRefresh) {
        final Observable<List<OperationPositionWrapper>> fromCache = Observable.create(
                new Observable.OnSubscribe<List<OperationPosition>>() {
                    @Override
                    public void call(Subscriber<? super List<OperationPosition>> subscriber) {
                        List<OperationPosition> dbCache = new Select().from(OperationPosition.class).queryList();
                        if (dbCache != null) {
                            subscriber.onNext(dbCache);
                        }
                        subscriber.onCompleted();
                    }
                })
                .map(new Func1<List<OperationPosition>, List<OperationPositionWrapper>>() {
                    @Override
                    public List<OperationPositionWrapper> call(List<OperationPosition> operationPositions) {
                        return OperationPositionMapper.wrap(operationPositions);
                    }
                })
                .filter(new Func1<List<OperationPositionWrapper>, Boolean>() {
                    @Override
                    public Boolean call(List<OperationPositionWrapper> operationPositionWrappers) {
                        return ListUtils.isNotEmpty(operationPositionWrappers);
                    }
                });

        final Observable<List<OperationPositionWrapper>> fromNetwork = RepositoryUtils.observableWithApi(new GetOperationPositionsForYouleHomeApi())
                .map(new Func1<List<OperationPositionPO>, List<OperationPositionWrapper>>() {
                    @Override
                    public List<OperationPositionWrapper> call(List<OperationPositionPO> operationPositionList) {
                        return OperationPositionMapper.transform(operationPositionList);
                    }
                })
                .doOnNext(new Action1<List<OperationPositionWrapper>>() {
                    @Override
                    public void call(List<OperationPositionWrapper> operationPositionWrappers) {
                        if (ListUtils.isNotEmpty(operationPositionWrappers)) {
                            new Delete().from(OperationPosition.class).queryClose();
                        }
                        for (OperationPositionWrapper wrapper : operationPositionWrappers) {
                            wrapper.getOperationPosition().save();
                        }
                    }
                });

        if (isForceRefresh) {
            return fromNetwork;
        } else {
            return Observable.concat(fromCache, fromNetwork);
        }
    }

總結

本篇為Repository層的上篇,主要介紹了組合及Retrofit的應用。下篇將會講述資料庫,記憶體Cache,以及統一的異常處理設計。

另外,打個小廣告,本司的新產品Crew已經在各大Android應用市場上線,專注於職場垂直社交。一搜和興趣相投的人聊天。iOS版本正在稽核中。

2個字找到志趣相投的職場夥伴,秒搜陌生人同類,智慧自動破冰。多關鍵字疊加,高效率鎖定職場同僚。精準匹配興趣物件,超輕聊天,更能一鍵組建群聊,加入一群人的狂歡。

demo沒空寫了,反正我也沒混淆,直接反編譯來黑我吧。哈哈。有bug或者功能上的意見建議歡迎直接反饋給我。

歡迎關注我們的公眾號:魔都三帥,歡迎大家來投稿~

公眾號