前言
最近比較空閒,並且想自己構建一個完整項目,來了解自己的不足,剛好Gank提供了API,於是決定一試。
效果展示
頁面 | 效果 |
---|---|
首頁效果 | |
網絡狀態 | |
每日詳情 |
第三方庫
控件綁定:ButterKnife
網絡請求:okhttp
事件分發:EventBus
方法數量突破:multidex
Json解析:fastjson
圖片加載:Glide
圖片瀏覽控件:PinchImageView
項目框架
首先,整體的項目框架,採用MVP框架。
關於Android框架,還是比較自由的。根據需求去選擇自己喜歡的框架就可以。
這裏我想加深一下自己對MVP框架的理解,於是選擇採用MVP框架。
關於MVC、MVP、MVVM的框架含義,可以參考此篇短文,言簡意賅。
該項目的框架基本仿照Google開源的Android框架模板。
雖然存在一些差異,不過大同小異。
BaseActivity
一個好的基類,可以大大減少後續的工作量。
首先,我們來稍微屢一下全局的要求:
標題欄:基本所有頁面都會有,但是有些頁面可能需要定製化,甚至可能沒有。
頁面控制:此處是一大頭,包括Loading、無網、無數據、數據展示等多種頁面樣式以及展示邏輯。
綜上,我認爲我們需要爲BaseActivity設置一個layout文件,它持有ToolBar和各個狀態的頁面UI,並且可以控制頁面UI的展示時機。
在子Activity中,繼承BaseActivity後只需設置數據UI的樣式即可。
接下來我們就開始構建BaseActivity,首先來構建BaseActivity的layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/c_ffffff"
android:orientation="vertical">
<!--ToolBar-->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/c_3f51b5"
android:minHeight="?attr/actionBarSize" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<!--Loading Page-->
<include
android:id="@+id/view_loading"
layout="@layout/view_loading"
android:visibility="gone" />
<!--無數據 Page-->
<include
android:id="@+id/view_no_data"
layout="@layout/view_no_data"
android:visibility="gone" />
<!--無網 Page-->
<include
android:id="@+id/view_no_net"
layout="@layout/view_no_net"
android:visibility="gone" />
<!--數據 Page-->
<FrameLayout
android:id="@+id/view_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
可以看到,它包括了ToolBar、LoadingPage、無數據Page、無網Page以及數據Page。
接下來我們在BaseActivity中對控件進行初始化,開始書寫頁面狀態的控制代碼。
此處各位看官要有意識,這些控制代碼,不應存在於BaseActivit中。
原因有很多,首先因爲Activity是我們MVP的View層,它僅僅掌控UI變化的結果,而不應該持有UI變化的邏輯判斷。其次,如果我們將來封裝BaseFragment,難道需要將這些邏輯代碼再寫一遍應用至BaseFragment中嗎?
所以這裏我決定在BaseActivity中創建成員變量PageController。每個Activity都會有一個獨立的PageController,它持有Activity,來通知Activity的UI變化,並且和Activity擁有相同的生命週期:
//頁面狀態控制器,每個頁面持有一個,使用弱引用來持有Activity
//存在於BaseActivity中,無需手動控制,在頁面銷燬時自動釋放
public class PageController {
//Activity對象
private WeakReference<BaseActivity> weakActivity;
//當前頁面狀態
private int currentState;
public PageController(BaseActivity activity) {
weakActivity = new WeakReference<>(activity);
}
//銷燬方法
public void onDestory() {
if (weakActivity == null) {
return;
}
weakActivity.clear();
weakActivity = null;
}
}
接下來就開始構建具體的頁面控制邏輯。
這個邏輯因人而異,我這裏就不介紹自己構建時的思路了,源碼裏的註釋都有體現。
在完成PageController後,當你需要構建BaseFragment時,僅僅需要將fragment.getAcitivty()傳遞至PageController中,就可以使用PageController了。
之後在實際使用過程中,我也發現了一個問題:
由於我們的頁面是根據頁面狀態來顯示的,也就是說隨着狀態變化,頁面中有且只有一種類型的頁面處於顯示狀態。
這就產生了一個巨大的問題:當數據View有下拉刷新或者抽屜效果時,如果網絡超時或者無數據,數據View就會隱藏顯示對應的狀態View,數據View的隱藏代表着下拉刷新或抽屜效果的View被隱藏了,也就代表着用戶無法使用了。
這明顯是不符合用戶操作習慣的。
這個問題待解決。
網絡請求
使用okhttp來作爲本次開發的網絡請求底層。
再好的框架,想要適應一個項目,必定需要進行定製化的封裝。
在封裝之前,先回想一下一次完整的網絡請求,注意我們項目是MVP框架:
用戶在Activity中發出請求—>調用Presenter層的網絡請求方法->調用Model層的網絡請求方法->傳遞給Presenter請求結果以及數據->傳遞給Activity結果以及數據
在MVP框架下,一次網絡請求基本就是這樣的了。
不過這就牽扯到框架問題了,我們先回到網絡請求中,不去考慮框架。
如何最大限度的抽象,是我們現在要着重考慮的。
並且還要考慮併發、緩存、異步同步等一系列事情。
該項目的網絡請求封裝簡直不忍直視,基本是本着能用就行的思想來進行封裝的。。。
我發現此處是我的薄弱項,在將來有空時我會好好學習的。。。。
這裏就不做過多介紹了,其實網上有很多基於okhttp封裝相當好的網絡請求框架,想使用的話也可以使用。
Activity切換動畫
Activity的切換動畫是可以定製的,大家都知道。這裏我定義了多套Activity切換動畫,並封裝爲Jump,和PageController一樣,每個Acitivty持有一個。
爲什麼要這樣做?因爲我認爲Activity的切換動畫體驗其實很重要。
有些時候,從一個Activity打開一個Activity時,是一種流程,類似的有修改密碼功能:
輸入完成舊密碼之後點擊下一步,打開輸入新密碼頁面,就屬於一種流程頁面。那麼它的動畫應該就和上個頁面很契合,給人一種頁面連貫感。
有些時候,即將打開的Activity與上個Activity毫無關聯,用戶將在這個頁面領略到風格完全不同的數據,此時Activity的切換動畫就需要給人一種打開新世界的感覺。
比如一些詳情頁、或者一些輪播圖跳轉等。
在封裝Jump的過程中,我想到了兩種方案:
將Jump封裝爲單例靜態的,並在Jump中封裝一個Map集合來記錄每個Activity的打開動畫的類型,當這個Activity要關閉時,去查詢匹配對應的關閉動畫。
每個Activity持有一個Jump,該Jump記錄了該Activity打開時的動畫類型,在關閉時直接去查詢匹配即可。
兩種方案理解之後,可以很明顯地發現,第2種方案更簡單一些,並且第2種方案的性能會更好一些?
所以我是選擇了第2種方案。
接着在開發過程中,遇到了一個難點,和大家分享一下:
在使用封裝的Jump跳轉時,該Jump是上個Activity持有的Jump,而不是即將打開的Activity持有的Jump。
這就導致了使用Jump去獲取動畫類型時,獲取的是上個Activity的動畫類型,而不是即將打開的動畫類型。
爲什麼不使用即將打開的Activity的Jump呢?因爲在跳轉過去的時候,也就是startActivity()時,我們是沒有即將打開的Activity的實例的。
我寫了很多代碼來進行測試,發現大部分解決方案都是需要在兩處指定動畫類型。
我認爲這是不符合編程思想的。將來如果有人接手這些代碼,那麼必定不知道要指定Activity的動畫類型,需要修改兩處代碼。
目前我是通過添加Jump中封裝的跳轉方法的參數,用來指定動畫類型,就像這樣子:
public void to(Intent intent) {}
public void to(Intent intent, Jump.JumpType type) {}
public void to(Intent intent, int requestCode) {}
public void to(Intent intent, int requestCode,Jump.JumpType type) {}
可以發現除了傳遞Intent之外,我又添加了指定動畫類型JumpType的跳轉方法。
在指定JumpType的跳轉方法中,將JumpType同時塞入到Intent當中,然後在BaseActivity中去解析JumpType並賦值給Jump,就是整體的流程。
這裏一定要注意,當指定JumpType時,我們不能將JumpType複製給當前Jump,因爲這個JumpType是下個Activity所做的動畫,而不是當前Activity所做的動畫。
具體代碼可參考:Jump
TODO
到目前爲止,還有一些功能沒有開發:
搜索功能
Gank的API中擁有搜索的API,該功能在後續會去實現。
網絡請求優化
從該項目當中,我發現自己對網絡請求的理解和Android文件存儲的方式存在很多問題,等到有空,我會將網絡請求這邊重構,包括添加File緩存、異步同步請求、文件上傳接收請求等一系列基本功能。
RxJava+Retrofit
本項目當中沒有使用RxJava。在後期有空我會重新再寫一遍該項目,會使用RxJava和其他框架來實現,這算是立一個flag吧。
結語
這一週的精力除了完成本職工作外,基本上都放到了TigerGank中。
我會慢慢抽空繼續完善該項目,這周就先這樣了。
開發過程中遇到很多瓶頸,解決瓶頸的代碼肯定是不優雅的。主要還是因爲自己的水平太差。
一起加油,感興趣的朋友也可以自己開發Gank。
感謝
上述所有第三方庫