但是,这种封装方式还是存在耦合:页面与它所使用的LoadingView
仍然存在绑定关系。如果需要复用到其它App中,因为每个App的UI风格可能不同,对应的LoadingView
布局也可能会不一样,要想复用必须先将页面与LoadingView
解耦。
如何解耦?
1. 梳理一下我们需要实现的效果
- 页面的
LoadingView
可切换,且不需要改动页面代码 - 页面中可指定
LoadingView
的显示区域(例如导航栏Title不希望被LoadingView
覆盖) - 支持在Fragment中使用
- 支持加载失败页面中点击重试
- 兼容不同页面显示的UI有细微差别(例如提示文字可能不同)
2. 确定思路
说到View的解耦,很容易联想到Android系统中的AdapterView(我们常用的GridView和ListView都是它的子类)及support包里提供的ViewPager、RecyclerView等,它们都是通过Adapter来解耦的,将自身的逻辑与需要动态变化的子View进行分离。我们也可以按照这个思路来解耦LoadingView
:
- 创建一个工具类,用于管理LoadingView各个状态的UI展示
- 创建一个Adapter接口,外部提供实现类,通过getView方法创建具体的LoadingView
- 每个App提供一个Adapter的实现,并注册到工具类中
- 工具类从Adapter.getView获取具体的LoadingView,所以页面中使用的代码无需改动
(已实现)页面的LoadingView可切换,且不需要改动页面代码
- 由于每个页面或View的加载状态互相之间无关联关系,需要创建一个用于管理具体某个LoadingView的状态持有类:Holder
- 指定LoadingView所需覆盖的View时,动态新建一个FrameLayout布局
- 将原View从ParentView中移除,并用它的LayoutParams将FrameLayout添加到ParentView中替代原View在ParentView中的位置
- 再将原View添加到FrameLayout中
- 在Fragment.onCreateView/RecyclerView.Adapter.onCreateViewHolder等方法中创建的View时,由于View尚未添加到任何容器中,并无getParent()返回null,此时需要用动态生成的FrameLayout代替原View作为方法的返回值返回
上代码更容易理解:
public Holder wrap(View view) {
FrameLayout wrapper = new FrameLayout(view.getContext());
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp != null) {
wrapper.setLayoutParams(lp);
}
if (view.getParent() != null) {
ViewGroup parent = (ViewGroup) view.getParent();
int index = parent.indexOfChild(view);
parent.removeView(view);
parent.addView(wrapper, index);
}
LayoutParams newLp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
wrapper.addView(view, newLp);
return new Holder(mAdapter, view.getContext(), wrapper);
}
(已实现)页面中可指定LoadingView的显示区域
(已实现)支持在Fragment中使用
另外,还顺带支持在RecyclerView、ListView、GridView、ViewPager等情况下的使用
- 为了不侵入UI,将加载失败点击重试的点击功能放在
Adapter.getView
中实现 - 与Android系统中的Adapter不同的是,我们的
Adapter
是全局使用的,而失败重试所需执行逻辑每个页面都不一样 - 因为
Holder
可以持有每个具体的LoadingView
,可以将retryTask
通过Holder
传递给Adapter
- 只需要在
Adapter.getView
时将Holder
作为参数传入,即可在创建LoadingView
时获取该retryTask
对象,并在点击重试按钮时执行retryTask
- 同理,可以通过
Holder
传递一些附加参数给Adapter
,以兼容在不同页面上布局的细微差异
(已实现)支持加载失败页面中点击重试
(已实现)兼容不同页面显示的UI有细微差别(例如提示文字可能不同)
使用Gloading来轻松实现低耦合的全局LoadingView
Gloading是一个基于Adapter思路实现的深度解耦App中全局LoadingView的轻量级工具(只有一个java文件,不到300行,其中注释占100+行,aar仅6K)
1、 依赖Gloading
compile ‘com.billy.android:gloading:1.0.0’
2、 创建Adapter
,在getView
方法中实现创建各种状态视图(加载中、加载失败、空数据等)的逻辑
Gloading不侵入UI布局,完全由用户自定义。示例如下:
public class GlobalAdapter implements Gloading.Adapter {
@Override
public View getView(Gloading.Holder holder, View convertView, int status) {
GlobalLoadingStatusView loadingStatusView = null;
//convertView为可重用的布局
//Holder中缓存了各状态下对应的View
// 如果status对应的View为null,则convertView为上一个状态的View
// 如果上一个状态的View也为null,则convertView为null
if (convertView != null && convertView instanceof GlobalLoadingStatusView) {
loadingStatusView = (GlobalLoadingStatusView) convertView;
}
if (loadingStatusView == null) {
loadingStatusView = new GlobalLoadingStatusView(holder.getContext(), holder.getRetryTask());
}
loadingStatusView.setStatus(status);
return loadingStatusView;
}
class GlobalLoadingStatusView extends RelativeLayout {
public GlobalLoadingStatusView(Context context, Runnable retryTask) {
super(context);
//初始化LoadingView
//如果需要支持点击重试,在适当的时机给对应的控件添加点击事件
}
public void setStatus(int status) {
//设置当前的加载状态:加载中、加载失败、空数据等
//其中,加载失败可判断当前是否联网,可现实无网络的状态
// 属于加载失败状态下的一个分支,可自行决定是否实现
}
}
}
3、 初始化Gloading
的默认Adapter
Gloading.initDefault(new GlobalAdapter());
复制代码
注:可以用AutoRegister在Gloading类装载进虚拟机时自动完成初始化注册,无需在app层执行注册,耦合度更低
4、在需要使用LoadingView
的地方获取Holder
//在Activity中显示, 父容器为: android.R.id.content
Gloading.Holder holder = Gloading.getDefault().wrap(activity);
//传递点击重试需要执行的task,该task在Adapter中用holder.getRetryTask()获取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask);
//传递点击重试需要执行的task和一个任意类型的扩展参数,该参数在Adapter中用holder.getData()获取
Gloading.Holder holder = Gloading.getDefault().wrap(activity).withRetry(retryTask).withData(obj);
or
//为某个View显示加载状态
//Gloading会自动创建一个FrameLayout,将view包裹起来,LoadingView也显示在其中
Gloading.Holder holder = Gloading.getDefault().wrap(view);
//传递点击重试需要执行的task,该task在Adapter中用holder.getRetryTask()获取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask);
//传递点击重试需要执行的task和一个任意类型的扩展参数,该参数在Adapter中用holder.getData()获取
Gloading.Holder holder = Gloading.getDefault().wrap(view).withRetry(retryTask).withData(obj);
5、 使用Holder
来显示各种加载状态
//显示加载中的状态,通常是显示一个加载动画
holder.showLoading()
//显示加载成功状态(一般是隐藏LoadingView)
holder.showLoadSuccess()
//显示加载失败状态
holder.showFailed()
//数据加载完成,但数据为空
holder.showEmpty()
//如果以上默认提供的状态不能满足使用,可使用此方法调用其它状态
holder.showLoadingStatus(status)
更多API详情请查看 Gloading JavaDocs
更多Demo示例代码请查看 Gloading Demo, 也可下载Demo apk体验
6、封装到BaseActivity/BaseFragment中
- 让BaseActivity和BaseFragment的子类中使用LoadingView更方便
- 子类中使用LoadingView的业务逻辑与实现分离
- 如果原来就是封装到BaseActivity/BaseFragment中的,那么可以无缝切换到Gloading
- 如果以后需要将Gloading移除替换成其它实现,也无需修改业务代码
示例代码如下:
public abstract class BaseActivity extends Activity {
protected Gloading.Holder mHolder;
/**
- make a Gloading.Holder wrap with current activity by default
- override this method in subclass to do special initialization
- @see SpecialActivity
*/
protected void initLoadingStatusViewIfNeed() {
if (mHolder == null) {
//bind status view to activity root view by default
mHolder = Gloading.getDefault().wrap(this).withRetry(new Runnable() {
@Override
public void run() {
onLoadRetry();
}
});
}
}
protected void onLoadRetry() {
nd status view to activity root view by default
mHolder = Gloading.getDefault().wrap(this).withRetry(new Runnable() {
@Override
public void run() {
onLoadRetry();
}
});
}
}
protected void onLoadRetry() {