寫一個 WanAndroid 客戶端吧!

前言

寫這個項目的初衷主要是爲了熟悉 mvp 這種架構設計模式以及一些主流的第三方框架在項目中的使用方法。主要使用到的技術有:MVP + RxJava2 + Retrofit + ButterKnife + Glide + EventBus + Androidx + Room 等等。主要的功能我就不多介紹了,大家登錄 WanAndroid 網站 或者下載本項目跑一下就可以看到了。我會盡量從零開始介紹這樣一個項目是怎麼一步一步搭建起來的,最好下載項目源碼再結合本文的介紹能比較快速的理解,源碼的 github 地址我貼在文章底部。好了,廢話不多說,我們開始吧!

框架搭建以及依賴添加

WanAndroid 客戶端使用 mvp 架構搭建,那麼首先我們來看一下怎麼搭建 mvp 的基礎架構。
先創建一個空項目,名字就叫 WanAndroid, 然後創建一個 basemvp 的包,這個包裏面的文件如圖所示:
在這裏插入圖片描述
我來解釋一下各個文件的作用吧!
BaseActivity : 這是所有 Activity 的父類,我們創建的 Activity 都需要繼承自這個抽象父類。之所以抽象出一個公共的父類,也是爲了減少我們編寫一下重複的代碼。來,看一下 BaseActivity 的 onCreate() 方法。

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getXMLId());
        ButterKnife.bind(this);
        if (useEventBus()) {
            EventBus.getDefault().register(this);
        }

        mInjectPresenters = new ArrayList<>();
        Field[] fields = this.getClass().getDeclaredFields();   // 解釋1。
        for (Field field : fields) {
            //獲取變量上面的註解類型
            InjectPresenter injectPresenter = field.getAnnotation(InjectPresenter.class);
            if (injectPresenter != null) {
                try {
                    Class<? extends BasePresenter> type = (Class<? extends BasePresenter>) field.getType();
                    BasePresenter mInjectPresenter = type.newInstance();
                    mInjectPresenter.attach(this);                  // P 綁定 V 層。
                    field.setAccessible(true);
                    field.set(this, mInjectPresenter);
                    mInjectPresenters.add(mInjectPresenter);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }catch (ClassCastException e){
                    e.printStackTrace();
                    throw new RuntimeException("SubClass must extends Class:BasePresenter");
                }
            }
        }

        init(savedInstanceState);
    }

這裏先綁定 ButterKnife, 使用 ButterKnife 主要是減少我們編寫很多的 findViewById() 等代碼。然後註冊 EventBus, 從解釋1開始就比較關鍵了,先獲取到實現類的所有成員變量,然後判斷哪個成員變量被 InjectPresenter 註解修飾,之後利用反射實例化該成員變量,然後調用 mInjectPresenter.attach(this) 綁定 view。這裏就將 View 層和對應的 Presenter 層綁定起來了。注意除了綁定,我們還需要在 onDestroy() 中進行解綁操作。

BaseFragment :這是所有 Fragment 的父類,作用和 BaseActivity 類似,BaseFragment 的 onCreateView() 方法和 BaseActivity 的 onCreate() 方法所做的操作時一樣的,代碼我就不貼了。

BaseModel : 這是所有 model 的父類。是一個空的抽象類。

BasePresenter : 這是所有 Presenter 的父類,它是一個泛型類,我們看一下它的 attach 方法吧。

@Override
    public void attach(IBaseView view) {
        mReferenceView = new SoftReference<>(view);

        //使用動態代理做統一的邏輯判斷 aop 思想     解釋1.
        mProxyView = (V) Proxy.newProxyInstance(view.getClass().getClassLoader(), view.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                if (mReferenceView == null || mReferenceView.get() == null) {
                    return null;
                }
                return method.invoke(mReferenceView.get(), objects);
            }
        });

        //通過獲得泛型類的父類,拿到泛型的接口類實例,通過反射來實例化 model    解釋2.
        ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
        if (type != null) {
            Type[] types = type.getActualTypeArguments();
            try {
                mModel = (M) ((Class<?>) types[1]).newInstance();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

還記得我們在 onCreate() 方法中調了 mInjectPresenter.attach(this) 嗎?這裏傳進來的就是具體的 view (就是我們的 Activity 或者 Fragment),然後看解釋1,我們通過動態代理的方法做了統一的判空處理,這樣就不需要在每一個具體使用的地方做判空處理了。
然後看解釋2, 我們獲取到具體實現類的父類,然後獲取父類的第二個泛型的接口類實例,然後實例化該對象,也就是我們的 具體 model 的實現類了。

其他的幾個類文件我就不解釋了,看看代碼就很好理解,可能我解釋的不是很清楚,看源碼應該能理解的更清楚一些吧!

接下來講講依賴的添加,以往我們都是在 module 中的 build.gradle 中添加依賴,但是這樣不太好管理各種依賴的版本,於是我們可以創建一個單獨文件來統一管理依賴和依賴的版本號。
首先在 根目錄(也就是project層)創建一個 dependencies.gradle 文件,然後在 project 的 build.gradle 的最上面添加一行:

apply from: "dependencies.gradle"

之後我們就可以在 module 中的 build.gradle 中像這樣添加依賴了。

implementation rootProject.androidSupportLibs
implementation rootProject.extraSupportLibs
implementation rootProject.ext.networkLibs
implementation rootProject.ext.rxJavaLibs
annotationProcessor rootProject.ext.annotationsProcessorLibs

詳細的大家可以參考源碼。
講完了框架的搭建和添加依賴,接下來講講主體功能的實現。
主要的功能包括:引導頁,首頁,項目,體系,導航,登錄,註冊,收藏,黑夜模式(暫未實現),頭像上傳(暫未實現)。
那接下來就從功能角度講講具體實現以及用到的一些框架和 mvp 是怎麼通過 Presenter 將 view 層和 model 層隔離開的。

引導頁

首先 app 儘量就是一個引導頁:LoadingActivity, 在這個 activity 裏面我們需要去動態申請一些權限,如拍照,讀寫文件權限。在權限申請完之後我們 sleep 1500ms 再進入到主界面中,這樣有過渡的效果,不至於一下子就跳到主界面。

首頁

進入 MainActivity, 我們使用 BottomNavigationView 來實現底部導航欄的效果,再使用 DrawerLayout 來實現滑動菜單的效果,這些都比較簡單,代碼看源碼就好,我都做了相應的註釋,我們看看效果吧!
在這裏插入圖片描述在這裏插入圖片描述
這裏需要說明一下,BottomNavigationView 底部的四個導航欄分別對應的是四個 Fragment,默認顯示的是首頁的這個 Fragment, 也就是 HomeFragment。可以看到 HomeFragment 分爲了兩個部分,上面的輪播圖和下面的列表,輪播圖主要使用的是一個第三方框架:com.youth.banner:banner ,列表的實現使用的就是 RecyclerView 了。
我們都知道 RecyclerView 的 Adapter 寫起來會有點麻煩,特別是要實現一些特殊效果,這裏使用了一個特別好用的 Adapter 的第三方框架:BaseRecyclerViewAdapterHelper
另外,我們的數據都是通過網絡獲取的,那有可能出現數據獲取失敗,正在獲取中等不同的狀態,我們需要顯示不同的 ui 界面,針對不同的狀態顯示不同的 ui, 這裏也是使用了一個第三方的框架:MultipleStatusView:com.classic.common:multiple-status-view
如果代碼有哪裏看不到的可以先學習一下我上面說到的框架,另外源碼中有必要的我都加了一些註釋。
介紹完了 ui 層,下面我們講講數據是怎麼獲取並顯示在界面上的。

在 HomeFragment 中定義了一個成員變量:mHomePresenter, 該成員變量使用了 @InjectPresenter 註解進行修飾,我們在上面講了,使用這個註解修飾之後,在 BaseFragment 中就能夠實例化該對象並將 mHomePresenter 和 HomeFragment 的對象綁定起來,那我們看看 HomePresenter 的代碼:

public class HomePresenter extends BasePresenter<HomeContract.View, HomeModel> implements HomeContract.Presenter {

    private static final String TAG = "HomePresenter";

    @Override
    public void initData(Context context) {
        /***
         * 初始化banner數據
         */
        getModel().getBannerData(context).subscribe(listBaseBean ->
                getView().showBanner(listBaseBean.getData()), throwable ->
                AppLog.debug(TAG, "get banner data error: " + throwable.getMessage()));

        getModel().getArticlesData(context, 0).subscribe(articleListBaseBean ->
                getView().showArticleList(articleListBaseBean.getData().getDatas()), throwable ->
                AppLog.debug(TAG, "get articles data error: " + throwable.getMessage()));
    }
}

它繼承自BasePresenter,第二個泛型傳的是 HomeModel,我們不妨也一起看看 HomeModel 的代碼,這樣結合起來就能夠很容易理解 RxJava2 和 Retrofit 是怎麼結合使用的。HomeModel 的代碼如下:

public class HomeModel extends BaseModel implements HomeContract.Model {


    @Override
    public Observable<BaseBean<List<BannerBean>>> getBannerData(Context context) {
        return HttpHelper.getInstance(context)
                .getRetrofitClient()
                .builder(HomeApi.class)
                .getHomeBannerData()
                .subscribeOn(Schedulers.io())       // 操作在子線程中進行。
                .observeOn(AndroidSchedulers.mainThread());    // 返回的是在主線程中。
    }

    @Override
    public Observable<BaseBean<ArticleList>> getArticlesData(Context context, int pageNo) {
        return HttpHelper
                .getInstance(context)
                .getRetrofitClient()
                .builder(HomeApi.class)
                .getArticlesData(pageNo)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

我們通過一個 HttpHelper 的工具類獲取到 RetrofitClient 對象,然後通過建造者模式傳入一個 HomeApi.class
接口類,然後調用該接口類的 getHomeBannerData() 方法獲取到首頁 Banner 的數據,返回的是一個 Observable 對象。接着我們在 HomePresenter 的 initData() 方法中調用 HomeModel 的 getBannerData() 方法,在調用 subscribe() 方法進行註冊,這樣請求完成之後就會回調到 subscribe() 方法的匿名內部類中,這裏是使用了 lambda 表達式簡化了匿名內部類的寫法,之後調用 getView().showBanner() 就可以將數據返回到 HomeFragment 中了。
這樣我們就完成了一個數據請求-回調的過程,在 HomeModel 中獲取數據,通過 HomePresenter 進行中轉,最終調到 HomeFragment 中,通過 Retrofit 和 RxJava2 的很好結合,我們的代碼邏輯結構非常的清晰。
其他模塊的數據請求-回調也是和這類似的,我就不重複解釋其他模塊的數據請過程了,這裏如果有不瞭解 RxJava2 的使用的話,可以參考這個系列文章:
給初學者的RxJava2.0教程(一) : https://www.jianshu.com/p/464fa025229e

項目

講完了首頁之後,我們接着看項目這個模塊的效果和實現,首先來看看效果:
在這裏插入圖片描述
我們可以通過左右滑動查看不同分類下面的詳細文章,其實類似的效果我們在很多 app 中都可以看到,像微博,蜻蜓fm等。所以這是一個很經典的實現效果。那究竟是怎麼實現的呢?
答案其實就是 TabLayout + ViewPager 的組合。關於這兩者結合使用網上已經有很多很好的文章了,我就不再贅述了。也可以參考源碼的實現,我都加了註釋。

體系

先看看效果吧!
在這裏插入圖片描述
在這裏插入圖片描述
體系這個模塊的實現其實比較簡單,首先是一個 RecyclerView 顯示各個知識體系的數據,然後 RecyclerView 子項的點擊事件又是一個 TabLayout + ViewPager 的組合。

導航

我們再來看看導航頁面的效果。
在這裏插入圖片描述
怎麼樣?效果還是挺不錯的吧!其實使用到的是 google 爸爸提供的的一個流式佈局容器:FlexboxLayout。
我們在 RecyclerView 的子項中添加一個 TextView 和 FlexboxLayout,然後在 Adapter 中獲取到 FlexboxLayout,然後給該容器添加多個 TextView, 就可以實現這種效果了。可以看看 Adapter 的 convert() 代碼:

@Override
    protected void convert(@NonNull BaseViewHolder helper, NavigationBean item) {
        helper.setText(R.id.item_navigation_title, item.getName());
        FlexboxLayout flexboxLayout = helper.getView(R.id.item_navigation_fbl);
        for (int i = 0; i < item.getArticles().size(); i++) {
            final NavigationBean.NavigationItem navigationItem = item.getArticles().get(i);
            TextView textView = createFlexItemTextView(flexboxLayout);
            textView.setText(navigationItem.getTitle());
            textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mNavigationFragment != null) {
                        mNavigationFragment.showNavigationDetail(navigationItem);
                    }
                }
            });
            flexboxLayout.addView(textView);
        }
    }

    private TextView createFlexItemTextView(FlexboxLayout flexboxLayout) {
        return (TextView) LayoutInflater.from(flexboxLayout.getContext()).inflate(R.layout.navigation_recyclerview_child_item, flexboxLayout, false);
    }

哦,忘了說了,xml 的佈局大部分使用的都是 ConstraintLayout 約束佈局,這也是谷歌推薦的佈局容器,減少多層佈局的嵌套,提升響應性能。
好了,講完了這幾大模塊之後,我們還有登錄註冊和收藏沒講。

登錄和註冊

這裏我講講思路就好,首先做了一些簡單的邏輯判斷用於驗證輸入的賬號和密碼是否爲空,兩次輸入的密碼是否一致,然後獲取到用戶密碼之後一路往下調,登錄成功或者失敗都會給我們一個回調反饋,我們再根據反饋做頁面的處理就好。在登錄成功之後,我們將用戶的姓名保存在數據庫中,這裏我使用的是 Room,這也是 google 推出的一個 ORM(對象關係映射)框架,是 google 架構組件中的一員。這裏多說一句,google 推出的架構組件真的非常好用,像 lifecycles, livedata, viewmodel 等,使用 mvvm + google 架構組件開發 app 也是很棒的一種體驗。不妨嘗試一下。
在本地保存了用戶姓名之後,我們重複進入退出app,仍然能夠保存當前已經登錄的用戶,在用戶退出登錄後清除 User 表的數據。

關於收藏我就不多說了,收藏功能需要在用戶已經登錄的前提下才能使用,但是這裏有個bug,我已經登錄成功之後,但是調收藏的接口還是返回給我沒有登錄,呃…還沒解決這個問題。

結束

好了,這個項目主要是我拿來練手,學習一下一些框架的使用,它還是有很多很多不完善的地方。這裏特別感謝一下鴻洋大神提供的 api,也感謝開源,感謝這麼多好用的第三方框架和其他優秀作者開源的 WanAndroid 客戶端源碼。非常感謝。
源碼 github 地址:https://github.com/AlongLing/WanAndroid

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章