MVP架構(gòu)實(shí)現(xiàn)的Github客戶端(3-功能實(shí)現(xiàn))

系列文章:
1-準(zhǔn)備工作
2-搭建項(xiàng)目框架
3-功能實(shí)現(xiàn)
4-加入網(wǎng)絡(luò)緩存

前文搭建了項(xiàng)目的大體架構(gòu), 本文以實(shí)現(xiàn)github中最常用的搜索repository功能為例, 講述下功能開發(fā)的整個(gè)過程以及關(guān)于MVP, Dagger2注入, Rx以及Retrofit, OkHttp等在準(zhǔn)備工作中提到的一系列開源庫的使用.

1, 功能梳理

基本功能類似于Github中的Search功能一致, 包含Repository/User/Issue/Code的搜索, 并可選排序方式(Best match, Most stars等等).

本文以搜索Repository為例, 默認(rèn)按照Most Stars來排序列舉搜索結(jié)果. 監(jiān)聽用戶輸入關(guān)鍵字, 當(dāng)超過600ms沒有輸入時(shí)自動(dòng)開始搜索, 按照stars數(shù)顯示搜索結(jié)果.

目前只是以此功能來演示前文的MVP框架, 并不完善~
源碼將持續(xù)更新, 完善功能. 歡迎star.

效果如下:


功能演示

2, 從數(shù)據(jù)開始(M)

Github相關(guān)API

https://api.github.com/search/repositories?q=tetris+language:assembly&sort=stars&order=desc

根據(jù)API, 我們可以寫Retrofit的接口以及相關(guān)Model類:

public interface RepoService {

    @GET("search/repositories")
    Observable<SearchResultResp> searchRepo(@Query("q") String key, @Query("sort") String sort,
                                            @Query("order") String order, @Query("page") int page,
                                            @Query("per_page") int pageSize);
}

其中Repo這個(gè)Model類就不貼了, 太長.
建議大家可以使用Android Studio的插件GsonFormat來生成:


GsonFormat

Retrofit/OkHttpClient設(shè)計(jì)

大家都知道Retrofit一般會(huì)是單例, 會(huì)設(shè)置一個(gè)baseUrl.
考慮到Github客戶端這個(gè)應(yīng)用, 我會(huì)加入Trending這個(gè)功能, 但是官方并沒有提供相關(guān)接口. 這個(gè)baseUrl就可以不同接口不一樣.
另外使用的OkHttpClient也可能針對不同接口使用不同的interceptor.
在此做了一個(gè)小框架, 以便擴(kuò)展(時(shí)刻牢記開閉原則).

data_architecture
  • ApiEndpoint 接口提供baseUrl.
  • BaseOkHttpClient 提供OkHttpClient實(shí)例.
  • BaseRetrofit 依賴ApiEndpoint和BaseOkHttpClient, 提供Retrofit實(shí)例.
  • GithubRepoRetrofit 是基于github的url的Retrofit實(shí)現(xiàn).

數(shù)據(jù)層整體結(jié)構(gòu)

綜上, 數(shù)據(jù)層的整體結(jié)構(gòu)如下:

M_architecture
  • RepoApi 定義業(yè)務(wù)接口.
  • RepoDataSource 實(shí)現(xiàn)RepoApi接口, 通過RepoService這個(gè)Retrofit格式的Restful接口取數(shù)據(jù)并可以在此做相應(yīng)處理.

3, 繪制界面(V)

這部分就不細(xì)說了, 參看代碼. 抽象了幾個(gè)Base類:

V_architecture

4, 數(shù)據(jù)關(guān)聯(lián)顯示(P)

Presenter作為M和V的橋梁, 如2-搭建項(xiàng)目框架所說, 其擁有V和M的實(shí)例:

P_architecture

通過BaseMvpPresenter中的attachView來獲取View實(shí)例, 在其構(gòu)造函數(shù)中關(guān)聯(lián)RepoApi這個(gè)M層接口.

在此抽象了一個(gè)RxMvpPresenter的基類, 主要用來解決Observable的unsubsribe問題.

5, 將MVP注入起來(Dagger2)

上面介紹完M, V, P這三個(gè)角色, 但是他們是怎么關(guān)聯(lián)起來的呢? 或者說怎么實(shí)例化的呢? 有請Dagger2出場.

具體的Dagger2的使用請自行Google~, 基本作用如下幾點(diǎn). 我后續(xù)也會(huì)抽時(shí)間整理Dagger2的使用經(jīng)驗(yàn), 敬請期待.

下面簡單說下我們這個(gè)功能中的注入情況:s

使用@Module和@Providers來對外提供依賴

搭建項(xiàng)目框架我們提到di的基本框架中, 就寫了一個(gè)ApplicationModule用來對外提供Application級別的依賴, 在此我們可以將Retrofit Service放在這里:

@Module
public class ApplicationModule {
    protected final Application mApplication;

    public ApplicationModule(Application application) {
        mApplication = application;
    }

    @Provides
    Application provideApplication() {
        return mApplication;
    }

    @Provides
    @ApplicationContext
    Context provideContext() {
        return mApplication;
    }

    @Provides
    @Singleton
    RepoService provideRepoService(GithubRepoRetrofit retrofit) {
        return retrofit.get().create(RepoService.class);
    }
}

可以看到我們使用的是GithubRepoRetrofit這個(gè)Retrofit示例來創(chuàng)建Service的.
那么這個(gè)GithubRepoRetrofit是怎么實(shí)例化的呢? 往下看...

構(gòu)造函數(shù)上的@Inject

當(dāng)用@Inject來注解一個(gè)構(gòu)造函數(shù)時(shí), Dagger2會(huì)根據(jù)其構(gòu)造參數(shù)來生成一個(gè)該實(shí)例. 這樣做是為了避免定義太多的@Provides.
例如:

@Inject
public GithubRepoRetrofit(NormalHttpClient mHttpClient) {
    this.mHttpClient = mHttpClient;
}

@Inject
public NormalHttpClient() {
}

@Inject
public SearchPresenter(RepoApi api) {
    this.mRepoApi = api;
}

變量上的@Inject

上面兩個(gè)原則用來提供實(shí)例, 這個(gè)就是用來請求實(shí)例的.
例如在SearchFragment里面我們需要一個(gè)SearchPresenter的實(shí)例:

@Inject
SearchPresenter mPresenter;

如上即可, 我們無需關(guān)注這個(gè)Presenter怎么實(shí)例化的. 這個(gè)給了我們很多想象空間, 我們可以隨意替換這個(gè)Presenter的實(shí)現(xiàn), 都不需要改動(dòng)View的代碼.

@Component的作用

以上講了實(shí)例的產(chǎn)生和消費(fèi), 那么他們是怎么關(guān)聯(lián)的呢? 這就需要@Component了, Component用來做實(shí)例關(guān)聯(lián)和inject.
所有的@Component類都會(huì)在編譯時(shí)生成一個(gè)Dagger開頭的類, 里面根據(jù)Component定義來提供實(shí)例, 和注入關(guān)系.

例如我們的Component:

@PerActivity
@Component(
        dependencies = ApplicationComponent.class,
        modules = {ActivityModule.class, RepoModule.class, TrendingRepoModule.class})
public interface MainComponent extends ActivityComponent {

    void inject(MostStarFragment mostStarFragment);

    void inject(TrendingFragment trendingFragment);

    void inject(SearchFragment searchFragment);
}

可以看到MainComponent依賴ApplicationComponent, 并從ActivityModule, RepoModule, TrendingRepoModule這三個(gè)Module中獲取依賴實(shí)例.

另外提供inject關(guān)系到MostStarFragment, TrendingFragment, SearchFragment這三個(gè)使用者身上, 要使用這個(gè)inject關(guān)系, 還需在onCreate中inject一下:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getComponent(MainComponent.class).inject(this);

    mPresenter.attachView(this);
}

具體大家可以查看源碼以及編譯后生成的DaggerMainComponent.

6, RxBinding/RxJava的使用

在這個(gè)功能中, 使用RxBinding對EditText的Text Change事件做了事件流處理, 以便可以很方便的做延時(shí), 過濾, 與查詢接口合并等. 具體RxBinding的功能不在此細(xì)說了.

功能使用如下:

public void bindSearchView(EditText searchTextView) {
    mCompositeSubscription.add(RxTextView.textChanges(searchTextView)
            .subscribeOn(AndroidSchedulers.mainThread())
            .filter(new Func1<CharSequence, Boolean>() {
                @Override
                public Boolean call(CharSequence charSequence) {
                    return charSequence.length() > 0;
                }
            })
            .debounce(600, TimeUnit.MILLISECONDS)
            .subscribeOn(Schedulers.io())
            .switchMap(new Func1<CharSequence, Observable<ArrayList<Repo>>>() {
                @Override
                public Observable<ArrayList<Repo>> call(CharSequence charSequence) {
                    String key = charSequence.toString();
                    return mRepoApi.searchMostStarredRepo(key, mLanguage)
                                .onErrorResumeNext(new Func1<Throwable, Observable<? extends ArrayList<Repo>>>() {
                                    @Override
                                    public Observable<? extends ArrayList<Repo>> call(Throwable throwable) {
                                        getMvpView().showError(throwable);
                                        AppLog.d("searchMostStarredRepo onErrorResumeNext:" + throwable);
                                        return Observable.empty();
                                    }
                                })
                                .doOnSubscribe(new Action0() {
                                    @Override
                                    public void call() {
                                        mActivity.runOnUiThread(new Runnable() {
                                            @Override
                                            public void run() {
                                                getMvpView().showLoading();
                                            }
                                        });
                                    }
                                })
                                .doOnTerminate(new Action0() {
                                    @Override
                                    public void call() {
                                        mActivity.runOnUiThread(new Runnable() {
                                            @Override
                                            public void run() {
                                                getMvpView().dismissLoading();
                                            }
                                        });
                                    }
                                });
                }
            })
            .observeOn(AndroidSchedulers.mainThread())
            .onErrorResumeNext(new Func1<Throwable, Observable<? extends ArrayList<Repo>>>() {
                @Override
                public Observable<? extends ArrayList<Repo>> call(Throwable throwable) {
                    AppLog.d("onErrorResumeNext:" + throwable);
                    getMvpView().showError(throwable);
                    return Observable.empty();
                }
            })
            .subscribe(new ResponseObserver<ArrayList<Repo>>() {
                @Override
                public void onSuccess(ArrayList<Repo> repos) {
                    getMvpView().showContent(repos);
                }

                @Override
                public void onError(Throwable e) {
                    AppLog.d("onError:" + e);
                    getMvpView().showError(e);
                }
            }));

}

看起來好長, 實(shí)際就幾步:
1, 字符串長度過濾.
2, debounce操作, 即僅在過了一段指定的時(shí)間還沒發(fā)射數(shù)據(jù)時(shí)才發(fā)射一個(gè)數(shù)據(jù)
3, 與查詢接口做switchMap操作.

7, 結(jié)語

至此, 一個(gè)基于Dagger2, RxJava, Retrofit, OkHttp等library的MVP架構(gòu)的實(shí)現(xiàn)已經(jīng)完成. 這幾篇文章只是提供給大家一個(gè)架構(gòu)的方向, 歡迎大家fork.

另外GithubApp這個(gè)工程還會(huì)持續(xù)更新, 包括功能上和架構(gòu), 以及一些新的主流庫的使用也會(huì)在這個(gè)工程上體現(xiàn), 歡迎關(guān)注star.

GithubApp源碼

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

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

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