Gloading效果演示
为Activity添加加载状态
加载成功 | 加载失败 点击重试 |
加载成功 无数据 |
个别页面使用特殊的Loading视图 |
---|---|---|---|
为View添加加载状态
单个View | 多个View | 用于GridView | 用于RecyclerView 并且不显示文字 |
---|---|---|---|
背景
Loading动画几乎每个Android App中都有。
一般在需要用户等待的场景,显示一个Loading动画可以让用户知道App正在加载数据,而不是程序卡死,从而给用户较好的使用体验。
同样的道理,当加载的数据为空时显示一个数据为空的视图、在数据加载失败时显示加载失败对应的UI并支持点击重试会比白屏的用户体验更好一些。
加载中、加载失败、空数据的UI风格,一般来说在App内的所有页面中需要保持一致,也就是需要做到全局统一。
1. 传统的做法
- 定义一个(或多个)显示不同加载状态的控件或者xml布局文件(例如:
LoadingView
) - 每个页面的布局中都写上这个view
- 在
BaseActivity/BaseFragment
中封装LoadingView
的初始化逻辑,并封装加载状态切换时的UI显示逻辑,暴露给子类以下方法:void showLoading();
//调用此方法显示加载中的动画void showLoadFailed();
//调用此方法显示加载失败界面void showEmpty();
//调用此方法显示空页面void onClickRetry();
//子类中实现,点击重试的回调方法
- 在
BaseActivity/BaseFragment
的子类中可通过上一步的封装比较方便地使用加载状态显示功能
这种使用方式耦合度太高,每个页面的布局文件中都需要添加LoadingView
,使用起来不方便而且维护成本较高,一旦UI设计师需要更改布局,修改起来成本较高。
2. 好一点的封装方法
- 定义一个(或多个)显示不同加载状态的控件或者xml布局文件(例如:
LoadingView
) - 定义一个工具类(
LoadingUtil
)来管理LoadingView
,不同状态显示不同的UI(或者在多个View之间切换显示) - 在
BaseActivity/BaseFragment
中对LoadingUtil
的使用进行封装,暴露给子类以下方法:void showLoading();
//调用此方法显示加载中的动画void showLoadFailed();
//调用此方法显示加载失败界面void showEmpty();
//调用此方法显示空页面void onClickRetry();
//子类中实现,点击重试的回调方法- abstract int getContainerId(); //子类中实现,LoadingUtil动态创建LoadingView并添加到该方法返回id对应的控件中
- 在
BaseActivity/BaseFragment
的子类中可通过上一步的封装比较方便地使用加载状态显示功能
这种封装的好处是通过封装动态地创建LoadingView
并添加到指定的父容器中,让具体页面无需关注LoadingView
的实现,只需要指定在哪个容器中显示即可,很大程度地进行了解耦。如果公司只在一个App中使用,这基本上就够了。
但是,这种封装方式还是存在耦合:页面与它所使用的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,所以页面中使用的代码无需改动
1 | (已实现)页面的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作为方法的返回值返回
上代码更容易理解:
1 | public Holder wrap(View view) { |
1 | (已实现)页面中可指定LoadingView的显示区域 |
- 为了不侵入UI,将加载失败点击重试的点击功能放在
Adapter.getView
中实现 - 与Android系统中的Adapter不同的是,我们的
Adapter
是全局使用的,而失败重试所需执行逻辑每个页面都不一样 - 因为
Holder
可以持有每个具体的LoadingView
,可以将retryTask
通过Holder
传递给Adapter
- 只需要在
Adapter.getView
时将Holder
作为参数传入,即可在创建LoadingView
时获取该retryTask
对象,并在点击重试按钮时执行retryTask
- 同理,可以通过
Holder
传递一些附加参数给Adapter
,以兼容在不同页面上布局的细微差异
1 | (已实现)支持加载失败页面中点击重试 |
使用Gloading来轻松实现低耦合的全局LoadingView
Gloading是一个基于Adapter思路实现的深度解耦App中全局LoadingView的轻量级工具(只有一个java文件,不到300行,其中注释占100+行,aar仅6K)
1、 依赖Gloading
1 | compile 'com.billy.android:gloading:1.0.0' |
2、 创建Adapter
,在getView
方法中实现创建各种状态视图(加载中、加载失败、空数据等)的逻辑
Gloading不侵入UI布局,完全由用户自定义。示例如下:
1 | public class GlobalAdapter implements Gloading.Adapter { |
3、 初始化Gloading
的默认Adapter
1 | Gloading.initDefault(new GlobalAdapter()); |
注:可以用AutoRegister在Gloading类装载进虚拟机时自动完成初始化注册,无需在app层执行注册,耦合度更低
4、在需要使用LoadingView
的地方获取Holder
1 | //在Activity中显示, 父容器为: android.R.id.content |
or
1 | //为某个View显示加载状态 |
5、 使用Holder
来显示各种加载状态
1 | //显示加载中的状态,通常是显示一个加载动画 |
更多API详情请查看 Gloading JavaDocs
更多Demo示例代码请查看 Gloading Demo, 也可下载Demo apk体验
6、封装到BaseActivity/BaseFragment中
- 让BaseActivity和BaseFragment的子类中使用LoadingView更方便
- 子类中使用LoadingView的业务逻辑与实现分离
- 如果原来就是封装到BaseActivity/BaseFragment中的,那么可以无缝切换到Gloading
- 如果以后需要将Gloading移除替换成其它实现,也无需修改业务代码
示例代码如下:
1 | public abstract class BaseActivity extends Activity { |
7、 兼容多App场景下的页面、View的复用
每个App的LoadingView
可能会不同,只需为每个App提供不同的Adapter
,不同App调用不同的Gloading.initDefault(new GlobalAdapter());
,具体页面中的使用代码无需改动。
注:如果使用AutoRegister,则只需在不同App中创建各自的 Adapter
实现类即可,无需手动注册。只需改动2处gradle文件即可:
- 修改根目录build.gradle,添加对AutoRegister的依赖
1 | buildscript { |
- 修改主application module下的build.gradle,添加如下代码即可实现Adapter的自动注册
1 | apply plugin: 'auto-register' |
总结
本文介绍了全局LoadingView在实际使用过程中可能存在的一些耦合情况,并指出了由此会影响多个App的LoadingView的UI风格不一致导致页面难以复用的问题,同时给出了解决思路。
另外,本文着重介绍了如何使用Gloading来轻松实现低耦合的全局LoadingView,喜欢的同学请顺手甩个star支持一下 :)