前言
寫這個項目的初衷主要是爲了熟悉 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