1. 程式人生 > >優雅地處理載入中(loading),重試(retry)和無資料(empty)等

優雅地處理載入中(loading),重試(retry)和無資料(empty)等

LoadSir是一個高效易用,低碳環保,擴充套件性良好的載入反饋頁管理框架,在載入網路或其他資料時候,根據需求切換狀態頁面,可新增自定義狀態頁面,如載入中,載入失敗,無資料,網路超時,佔位圖,登入失效等常用頁面。可配合網路載入框架,結合返回狀態碼,錯誤碼,資料進行狀態頁自動切換,封裝使用效果更佳。 LoadSir現在版本已經升級至1.3.6,相關內容請參考Github最新說明

本文前面是使用流程,基於1.2.2完成,後面是原理解析,如果大家有興趣,可耐心看完。

效果預覽

in Activity in View in Fragment

Placeholder Muitl-Fragment ViewPage+Fragment

使用場景

下面為大家常見的載入反饋頁面:

loading error timeout

empty custom placeholder

面對這麼多狀態頁面,你是不是還在用include的方式,setVisibility(View.VISIBLE/GONE),這種方式即不方便控制,也造成了檢視層級冗餘(你要把所有狀態佈局include進一個檢視)。如果有一種工具,能把這些事都做了就好了。恰好, LoadSir

把這些事做了,接下來我們就來了解一下它。

LoadSir的功能及特點

  • 支援Activity,Fragment,Fragment(v4),View狀態回撥
  • 適配多個Fragment切換,及Fragment+ViewPager切換,不會狀態疊加或者狀態錯亂
  • 利用泛型轉換輸入訊號和輸出狀態,可根據網路返回體的狀態碼或者資料返回自動適配狀態頁,實現全域性自動狀態切換
  • 只加載唯一一個狀態檢視,不會預載入全部檢視
  • 可保留標題欄(Toolbar,titile view等)
  • 可設定重新載入點選事件(OnReloadListener)
  • 可自定義狀態頁(繼承Callback類)
  • 可在子執行緒直接切換狀態
  • 可設定初始狀態頁(常用進度頁作為初始狀態)
  • 不需要設定列舉或者常量狀態值,直接用狀態頁類型別(xxx.class)作為狀態碼
  • 可擴充套件狀態頁面,在配置中新增自定義狀態頁
  • 可對單個狀態頁單獨設定點選事件,根據返回boolean值覆蓋或者結合OnReloadListener使用,如網路錯誤可跳轉設定頁
  • 可全域性單例配置,也可以單獨配置
  • 無預設頁面,低耦合,開發者隨心配置

開始使用LoadSir

LoadSir的使用只需要簡單的三步,三步上籃的三步。

新增依賴

compile 'com.kingja.loadsir:loadsir:1.3.6'

第一步: 配置

全域性配置方式

全域性配置方式,使用的是單例模式,即獲取的配置都是一樣的。可在Application中配置,新增狀態頁,設定初始化狀態頁,建議使用這種配置方式。

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LoadSir.beginBuilder()
                .addCallback(new ErrorCallback())//'新增各種狀態頁
                .addCallback(new EmptyCallback())
                .addCallback(new LoadingCallback())
                .addCallback(new TimeoutCallback())
                .addCallback(new CustomCallback())
                .setDefaultCallback(LoadingCallback.class)//設定預設狀態頁
                .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();
}

第三步: 回撥

直接回調

protected void loadNet() {
        // 進行網路訪問...
        // 進行回撥
        loadService.showSuccess();//成功回撥
        loadService.showCallback(EmptyCallback.class);//其他回撥
    }

轉換器回撥 (推薦使用)

如果你不想再每次回撥都要手動進行的話,可以選擇註冊的時候加入轉換器,可根據返回的資料,適配對應的回撥。

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://成功回撥
                if (httpResult.getData().size() == 0) {
                    resultCode = EmptyCallback.class;
                }else{
                    resultCode = SuccessCallback.class;
                }
                break;
            case ERROR_CODE:
                resultCode = ErrorCallback.class;
                break;
        }
        return resultCode;
    }
});

回撥的時候直接傳入轉換器指定的資料型別。

loadService.showWithConvertor(httpResult);

自定義回撥頁

LoadSir為了完全解耦,沒有預設任何狀態頁,開發者根據需求自定義自己的回撥頁面,比如載入中,沒資料,錯誤,超時等常用頁面, 設定佈局及自定義點選邏輯

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則兩個都執行
    }

    //是否在顯示Callback檢視的時候顯示原始圖(SuccessView),返回true顯示,false隱藏
    @Override
    public boolean getSuccessVisible() {
        return super.getSuccessVisible();
    }

    //將Callback新增到當前檢視時的回撥,View為當前Callback的佈局View
    @Override
    public void onAttach(Context context, View view) {
        super.onAttach(context, view);
    }

    //將Callback從當前檢視刪除時的回撥,View為當前Callback的佈局View
    @Override
    public void onDetach() {
        super.onDetach(context, view);
    }
}

動態修改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效果狀態頁類似ShimmerRecyclerView的效果. LoadSir只用了一個自定義狀態頁PlaceHolderCallback就完成類似的效果,是不是很棒 :p

看到這,想必各位使用LoadSir應該沒問題了,如果還想再進一步瞭解它的內部結構,可以繼續往下看。

原理解析

流程圖

關鍵類

  • LoadSir:提供單例模式獲取全域性唯一例項,內部儲存配置資訊,根據配置建立LoadService。
  • LoadService:具體操作服務類,提供showSuccess,showCallback,showWithCoverator等方法來進行狀態頁回撥。
  • LoadLayout:最終顯示在使用者面前的檢視View,替換了原佈局,是LoadService直接操作物件,要顯示的狀態頁的檢視會被新增到LoadLayout上。
  • Callback:狀態頁抽象類,抽象自定義佈局和自定義點選事件兩個方法留給子類實現。
  • Coverator:轉換介面,可將網路返回實體轉換成對應的狀態頁,達到自動適配狀態頁的目的。

我們直接觀察在Activity中普通載入和使用LoadSir載入檢視的區別

>>>沒使用LoadSir

>>>使用LoadSir

大家可以看到,LoadSir用LoadLayout把原來的佈局給替代掉了,原來的佈局加在了LoadLayout上,其它自定義的狀態頁也同樣會被加到這個LoadLayout上(顯示的時候),而且LoadLayout的子View只有一個,就是當前要顯示的狀態頁佈局,並沒有把當前不顯示的比如載入中佈局,錯誤佈局,無資料佈局載入進來,這也是LoadSir的優點之一,按需載入,並且只加載一個狀態佈局。

>>>替換邏輯

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進行檢視分離,避免了狀態頁疊加或錯位。

看到這的童鞋應該也大概知道LoadSir是怎麼回事了,如果想明白LoadSir的程式碼實現,請繼續往下看。

原始碼解析

我們按上面三步上籃的步驟來稍微分析下原始碼

>>>第一步:配置

單例模式獲取LoadSir,在LoadSir構造的時候建立預設配置

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主要提供新增狀態頁,和設定預設狀態頁的方法

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()來設定全域性配置。

public class LoadSir  {
   ...
    public static Builder beginBuilder() {
        return new Builder();
    }

    public static class Builder {
      
        public void commit() {
            getDefault().setBuilder(this);
        }
      ...
    }
}

>>>第二步:註冊

LoadSir註冊後返回的是LoadService,一看名字大家就明白這是服務類,就是我們所說的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的構造方法中根據target等資訊建立Success檢視,並且生成LoadLayout,相當於LoadSir每次註冊都會建立一個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);
    }

>>>第三步:回撥

LoadService的三個回撥方法最終呼叫的都是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);
        }
    }

這個方法可以說是最後的執行者,就做兩件事,刪除LoadLayout所有子View(重置),新增指定的佈局頁View(回撥)。

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一個完整的配置,註冊,回撥的過程完成了。不知道你們明白了沒,反正我是有點口渴了。

總結

建議在Application中全域性配置,在BaseActivity,BaseFragment或者MVP中封裝使用,能極大的減少程式碼量,讓你的程式碼更加優雅,生活更加愉快。時間和個人能力有限,如果大家發現需要改進的地方,歡迎提交issue。 如果這個庫對你有用的話,也請點個star:p