LoadSir是一個高效易用,低碳環(huán)保,擴展性良好的加載反饋頁管理框架,在加載網(wǎng)絡(luò)或其他數(shù)據(jù)時候,根據(jù)需求切換狀態(tài)頁面,可添加自定義狀態(tài)頁面,如加載中,加載失敗,無數(shù)據(jù),網(wǎng)絡(luò)超時,占位圖,登錄失效等常用頁面。可配合網(wǎng)絡(luò)加載框架,結(jié)合返回狀態(tài)碼,錯誤碼,數(shù)據(jù)進行狀態(tài)頁自動切換,封裝使用效果更佳。
LoadSir現(xiàn)在版本已經(jīng)升級至1.3.6,相關(guān)內(nèi)容請參考Github最新說明Github傳送門
本文前面是使用流程,基于1.2.2完成,后面是原理解析,如果大家有興趣,可耐心看完。
效果預(yù)覽
| in Activity | in View | in Fragment |
|---|---|---|
![]() |
![]() |
![]() |
| Placeholder | Muitl-Fragment | ViewPage+Fragment |
|---|---|---|
![]() |
![]() |
![]() |
使用場景
下面為大家常見的加載反饋頁面:
| loading | error | timeout |
|---|---|---|
![]() |
![]() |
![]() |
| empty | custom | placeholder |
|---|---|---|
![]() |
![]() |
![]() |
面對這么多狀態(tài)頁面,你是不是還在用include的方式,setVisibility(View.VISIBLE/GONE),這種方式即不方便控制,也造成了視圖層級冗余(你要把所有狀態(tài)布局include進一個視圖)。如果有一種工具,能把這些事都做了就好了。恰好, LoadSir 把這些事做了,接下來我們就來了解一下它。
LoadSir的功能及特點
- 支持Activity,F(xiàn)ragment,F(xiàn)ragment(v4),View狀態(tài)回調(diào)
- 適配多個Fragment切換,及Fragment+ViewPager切換,不會狀態(tài)疊加或者狀態(tài)錯亂
- 利用泛型轉(zhuǎn)換輸入信號和輸出狀態(tài),可根據(jù)網(wǎng)絡(luò)返回體的狀態(tài)碼或者數(shù)據(jù)返回自動適配狀態(tài)頁,實現(xiàn)全局自動狀態(tài)切換
- 只加載唯一一個狀態(tài)視圖,不會預(yù)加載全部視圖
- 可保留標題欄(Toolbar,titile view等)
- 可設(shè)置重新加載點擊事件(OnReloadListener)
- 可自定義狀態(tài)頁(繼承Callback類)
- 可在子線程直接切換狀態(tài)
- 可設(shè)置初始狀態(tài)頁(常用進度頁作為初始狀態(tài))
- 不需要設(shè)置枚舉或者常量狀態(tài)值,直接用狀態(tài)頁類類型(xxx.class)作為狀態(tài)碼
- 可擴展狀態(tài)頁面,在配置中添加自定義狀態(tài)頁
- 可對單個狀態(tài)頁單獨設(shè)置點擊事件,根據(jù)返回boolean值覆蓋或者結(jié)合OnReloadListener使用,如網(wǎng)絡(luò)錯誤可跳轉(zhuǎn)設(shè)置頁
- 可全局單例配置,也可以單獨配置
- 無預(yù)設(shè)頁面,低耦合,開發(fā)者隨心配置
開始使用LoadSir
LoadSir的使用只需要簡單的三步,三步上籃的三步。
添加依賴
compile 'com.kingja.loadsir:loadsir:1.3.6'
第一步: 配置
全局配置方式
全局配置方式,使用的是單例模式,即獲取的配置都是一樣的。可在Application中配置,添加狀態(tài)頁,設(shè)置初始化狀態(tài)頁,建議使用這種配置方式。
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
LoadSir.beginBuilder()
.addCallback(new ErrorCallback())//'添加各種狀態(tài)頁
.addCallback(new EmptyCallback())
.addCallback(new LoadingCallback())
.addCallback(new TimeoutCallback())
.addCallback(new CustomCallback())
.setDefaultCallback(LoadingCallback.class)//設(shè)置默認狀態(tài)頁
.commit();
}
}
單獨配置方式
如果你即想保留全局配置,又想在某個特殊頁面加點不同的配置,可采用該方式。
LoadSir loadSir = new LoadSir.Builder()
.addCallback(new LoadingCallback())
.addCallback(new EmptyCallback())
.addCallback(new ErrorCallback())
.build();
loadService = loadSir.register(this, new Callback.OnReloadListener() {
@Override
public void onReload(View v) {
// 重新加載邏輯
}
});
第二步: 注冊
在Activity中使用
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content);
// Your can change the callback on sub thread directly.
LoadService loadService = LoadSir.getDefault().register(this, new Callback.OnReloadListener() {
@Override
public void onReload(View v) {
// 重新加載邏輯
}
});
}}
在View 中使用
ImageView imageView = (ImageView) findViewById(R.id.iv_img);
LoadSir loadSir = new LoadSir.Builder()
.addCallback(new TimeoutCallback())
.setDefaultCallback(LoadingCallback.class)
.build();
loadService = loadSir.register(imageView, new Callback.OnReloadListener() {
@Override
public void onReload(View v) {
loadService.showCallback(LoadingCallback.class);
// 重新加載邏輯
}
});
在Fragment 中使用
由于Fragment添加到Activitiy方式多樣,比較特別,所以在Fragment中注冊方式不同于上面兩種,大家先看模板代碼:
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle
savedInstanceState) {
//第一步:獲取布局View
rootView = View.inflate(getActivity(), R.layout.fragment_a_content, null);
//第二步:注冊布局View
LoadService loadService = LoadSir.getDefault().register(rootView, new Callback.OnReloadListener() {
@Override
public void onReload(View v) {
// 重新加載邏輯
}
});
//第三步:返回LoadSir生成的LoadLayout
return loadService.getLoadLayout();
}
第三步: 回調(diào)
直接回調(diào)
protected void loadNet() {
// 進行網(wǎng)絡(luò)訪問...
// 進行回調(diào)
loadService.showSuccess();//成功回調(diào)
loadService.showCallback(EmptyCallback.class);//其他回調(diào)
}
轉(zhuǎn)換器回調(diào) (推薦使用)
如果你不想再每次回調(diào)都要手動進行的話,可以選擇注冊的時候加入轉(zhuǎn)換器,可根據(jù)返回的數(shù)據(jù),適配對應(yīng)的回調(diào)。
LoadService loadService = LoadSir.getDefault().register(this, new Callback.OnReloadListener() {
@Override
public void onReload(View v) {
// 重新加載邏輯
}}, new Convertor<HttpResult>() {
@Override
public Class<? extends Callback> map(HttpResult httpResult) {
Class<? extends Callback> resultCode = SuccessCallback.class;
switch (httpResult.getResultCode()) {
case SUCCESS_CODE://成功回調(diào)
if (httpResult.getData().size() == 0) {
resultCode = EmptyCallback.class;
}else{
resultCode = SuccessCallback.class;
}
break;
case ERROR_CODE:
resultCode = ErrorCallback.class;
break;
}
return resultCode;
}
});
回調(diào)的時候直接傳入轉(zhuǎn)換器指定的數(shù)據(jù)類型。
loadService.showWithConvertor(httpResult);
自定義回調(diào)頁
LoadSir為了完全解耦,沒有預(yù)設(shè)任何狀態(tài)頁,開發(fā)者根據(jù)需求自定義自己的回調(diào)頁面,比如加載中,沒數(shù)據(jù),錯誤,超時等常用頁面,
設(shè)置布局及自定義點擊邏輯
public class CustomCallback extends Callback {
@Override
protected int onCreateView() {
return R.layout.layout_custom;
}
@Override
protected boolean onRetry(final Context context, View view) {
//布局點擊事件
Toast.makeText(context.getApplicationContext(), "Hello mother fuck! :p", Toast.LENGTH_SHORT).show();
//子控件事件
(view.findViewById(R.id.iv_gift)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context.getApplicationContext(), "It's your gift! :p", Toast.LENGTH_SHORT).show();
}
});
return true;//返回true則覆蓋了register時傳入的重試點擊事件,返回false則兩個都執(zhí)行
}
//是否在顯示Callback視圖的時候顯示原始圖(SuccessView),返回true顯示,false隱藏
@Override
public boolean getSuccessVisible() {
return super.getSuccessVisible();
}
//將Callback添加到當前視圖時的回調(diào),View為當前Callback的布局View
@Override
public void onAttach(Context context, View view) {
super.onAttach(context, view);
}
//將Callback從當前視圖刪除時的回調(diào),View為當前Callback的布局View
@Override
public void onDetach() {
super.onDetach(context, view);
}
}
動態(tài)修改Callback
loadService = LoadSir.getDefault().register(...);
loadService.setCallBack(EmptyCallback.class, new Transport() {
@Override
public void order(Context context, View view) {
TextView mTvEmpty = (TextView) view.findViewById(R.id.tv_empty);
mTvEmpty.setText("fine, no data. You must fill it!");
}
});
代碼混淆
-dontwarn com.kingja.loadsir.**
-keep class com.kingja.loadsir.** {*;}
占位圖布局效果
placeholder效果狀態(tài)頁類似ShimmerRecyclerView的效果. LoadSir只用了一個自定義狀態(tài)頁PlaceHolderCallback就完成類似的效果,是不是很棒 :p
看到這,想必各位使用LoadSir應(yīng)該沒問題了,如果還想再進一步了解它的內(nèi)部結(jié)構(gòu),可以繼續(xù)往下看。
原理解析
流程圖

關(guān)鍵類
- LoadSir:提供單例模式獲取全局唯一實例,內(nèi)部保存配置信息,根據(jù)配置創(chuàng)建LoadService。
- LoadService:具體操作服務(wù)類,提供showSuccess,showCallback,showWithCoverator等方法來進行狀態(tài)頁回調(diào)。
- LoadLayout:最終顯示在用戶面前的視圖View,替換了原布局,是LoadService直接操作對象,要顯示的狀態(tài)頁的視圖會被添加到LoadLayout上。
- Callback:狀態(tài)頁抽象類,抽象自定義布局和自定義點擊事件兩個方法留給子類實現(xiàn)。
- Coverator:轉(zhuǎn)換接口,可將網(wǎng)絡(luò)返回實體轉(zhuǎn)換成對應(yīng)的狀態(tài)頁,達到自動適配狀態(tài)頁的目的。
我們直接觀察在Activity中普通加載和使用LoadSir加載視圖的區(qū)別
>>>沒使用LoadSir

>>>使用LoadSir

大家可以看到,LoadSir用LoadLayout把原來的布局給替代掉了,原來的布局加在了LoadLayout上,其它自定義的狀態(tài)頁也同樣會被加到這個LoadLayout上(顯示的時候),而且LoadLayout的子View只有一個,就是當前要顯示的狀態(tài)頁布局,并沒有把當前不顯示的比如加載中布局,錯誤布局,無數(shù)據(jù)布局加載進來,這也是LoadSir的優(yōu)點之一,按需加載,并且只加載一個狀態(tài)布局。
>>>替換邏輯
public static TargetContext getTargetContext(Object target) {
ViewGroup contentParent;
Context context;
if (target instanceof Activity) {
Activity activity = (Activity) target;
context = activity;
contentParent = (ViewGroup) activity.findViewById(android.R.id.content);
} else if (target instanceof View) {
View view = (View) target;
contentParent = (ViewGroup) (view.getParent());
context = view.getContext();
} else {
throw new IllegalArgumentException("The target must be within Activity, Fragment, View.");
}
...
if (contentParent != null) {
contentParent.removeView(oldContent);
}
return new TargetContext(context, contentParent, oldContent, childIndex);
}
大家可以看到,在Activity和View中的情況都比較簡單,直接獲取target的父控件,然后在父控件中替換掉該布局即可。在Fragment中,由于可能多個Fragment的布局View并存在一個父控件里,所以不能簡單地使用父控件刪除子View方式替換,也有可能父控件是ViewPager,不能通過addView()的方式添加LoadLayout。因此Fragment的注冊方式是直接返回了LoadLayout到Activity上。這樣也達到了一樣的目的。
下面是ViewPager+Fragment場景中使用LoadSir的視圖,兩個Fragment用各自的LoadLayout進行視圖分離,避免了狀態(tài)頁疊加或錯位。

看到這的童鞋應(yīng)該也大概知道LoadSir是怎么回事了,如果想明白LoadSir的代碼實現(xiàn),請繼續(xù)往下看。
源碼解析
我們按上面三步上籃的步驟來稍微分析下源碼
>>>第一步:配置
單例模式獲取LoadSir,在LoadSir構(gòu)造的時候創(chuàng)建默認配置
public static LoadSir getDefault() {
if (loadSir == null) {
synchronized (LoadSir.class) {
if (loadSir == null) {
loadSir = new LoadSir();
}
}
}
return loadSir;
}
private LoadSir() {
this.builder = new Builder();
}
Builder主要提供添加狀態(tài)頁,和設(shè)置默認狀態(tài)頁的方法
public static class Builder {
private List<Callback> callbacks = new ArrayList<>();
private Class<? extends Callback> defaultCallback;
public Builder addCallback(Callback callback) {
callbacks.add(callback);
return this;
}
public Builder setDefaultCallback(Class<? extends Callback> defaultCallback) {
this.defaultCallback = defaultCallback;
return this;
}
...
public LoadSir build() {
return new LoadSir(this);
}
}
LoadSir提供beginBuilder()...commit()來設(shè)置全局配置。
public class LoadSir {
...
public static Builder beginBuilder() {
return new Builder();
}
public static class Builder {
public void commit() {
getDefault().setBuilder(this);
}
...
}
}
>>>第二步:注冊
LoadSir注冊后返回的是LoadService,一看名字大家就明白這是服務(wù)類,就是我們所說的Service層。
public LoadService register(Object target, Callback.OnReloadListener onReloadListener) {
return register(target, onReloadListener, null);
}
public <T> LoadService register(Object target, Callback.OnReloadListener onReloadListener, Convertor<T>
convertor) {
TargetContext targetContext = LoadSirUtil.getTargetContext(target);
return new LoadService<>(convertor, targetContext, onReloadListener, builder);
}
在LoadService的構(gòu)造方法中根據(jù)target等信息創(chuàng)建Success視圖,并且生成LoadLayout,相當于LoadSir每次注冊都會創(chuàng)建一個LoadLayout。
LoadService(Convertor<T> convertor, TargetContext targetContext, Callback
.OnReloadListener onReloadListener, LoadSir.Builder builder) {
this.convertor = convertor;
Context context = targetContext.getContext();
View oldContent = targetContext.getOldContent();
loadLayout = new LoadLayout(context, onReloadListener);
loadLayout.addCallback(new SuccessCallback(oldContent, context,
onReloadListener));
if (targetContext.getParentView() != null) {
targetContext.getParentView().addView(loadLayout, targetContext.getChildIndex(), oldContent
.getLayoutParams());
}
initCallback(builder);
}
>>>第三步:回調(diào)
LoadService的三個回調(diào)方法最終調(diào)用的都是loadLayout.showCallback(callback);
public void showSuccess() {
loadLayout.showCallback(SuccessCallback.class);
}
public void showCallback(Class<? extends Callback> callback) {
loadLayout.showCallback(callback);
}
public void showWithConvertor(T t) {
if (convertor == null) {
throw new IllegalArgumentException("You haven't set the Convertor.");
}
loadLayout.showCallback(convertor.map(t));
}
我們直接看LoadLayout的showCallback方法,先做Callback是否配置判斷,然后進行線程安全操作。重點還是showCallbackView(callback);
public void showCallback(final Class<? extends Callback> callback) {
if (!callbacks.containsKey(callback)) {
throw new IllegalArgumentException(String.format("The Callback (%s) is nonexistent.", callback
.getSimpleName()));
}
if (LoadSirUtil.isMainThread()) {
showCallbackView(callback);
} else {
postToMainThread(callback);
}
}
這個方法可以說是最后的執(zhí)行者,就做兩件事,刪除LoadLayout所有子View(重置),添加指定的布局頁View(回調(diào))。
private void showCallbackView(Class<? extends Callback> status) {
if (getChildCount() > 0) {
removeAllViews();
}
for (Class key : callbacks.keySet()) {
if (key == status) {
addView(callbacks.get(key).getRootView());
}
}
}
自此,LoadSir一個完整的配置,注冊,回調(diào)的過程完成了。不知道你們明白了沒,反正我是有點口渴了。
總結(jié)
建議在Application中全局配置,在BaseActivity,BaseFragment或者MVP中封裝使用,能極大的減少代碼量,讓你的代碼更加優(yōu)雅,生活更加愉快。時間和個人能力有限,如果大家發(fā)現(xiàn)需要改進的地方,歡迎提交issue。
如果這個庫對你有用的話,也請點個star:p Github傳送門











