目的:学习 LceeWidget 的使用。
创建一个 widget 继承 LceeWidget,分别渲染 4中状态的页面,点击按钮重新加载数据。
Lcee 分别代表 Loading、Content、Empty、Error 四种状态。 一般应用为了更好的用户体验,都会有这四种状态。 但要实现这四种状态,一般都不太优雅,尤其是使用 xml 来绘制 UI 的时候,需要在一个 xml 中放入 4种 状态的 UI,这样会导致 xml 内容过多,不便于阅读和维护。另外令人头疼的是,在 Activity 或 Fragment 中,即便是使用 MVP,要维护4中状态的切换,也是一件麻烦的事。
LceeWidget 就是为了解决繁杂的状态管理问题。
第一次进入,显示 loading。 如果数据加载完成后,显示 content,里面由1 个 list 和 圆形按钮 组成。 如果数据为空,显示 empty。 如果请求失败,显示 error。 点击 empty、error 或 content 中的按钮 重新请求数据。
为了模拟3种请求后的状态,这里用伪随机数,来返回 空 或 error 或 内容。
public List<UserBean> getUsersFromRemote() throws NetworkErrorException{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// mock empty and error
switch (random.nextInt(3)) {
case 0: {// empty
return Collections.emptyList();
}
case 1: {// error
throw new NetworkErrorException("network error");
}
// other: content
}
int count = randomCount();
List<UserBean> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
int id = randomId();
String name = randomName();
UserBean bean = new UserBean(id, name);
list.add(bean);
}
return list;
}
private int randomCount() {
return random.nextInt(30) + 5;
}
因为用到了 RecyclerView,所以每一行需要一个 Widget
来显示。
实际上就是把前面一节的 UserWidget
稍微修改了一下。
public class UserItemWidget extends StatelessWidget<LinearLayout, LinearWidget> {
private TextWidget twId;
private TextWidget twName;
private UserBean user;
public UserItemWidget(Context context, Lifecycle lifecycle) {
super(context, lifecycle);
}
@Override
protected LinearWidget build(Context context, Lifecycle lifecycle) {
twId = new TextWidget(context, lifecycle);
twName = new TextWidget(context, lifecycle);
return new LinearWidget(context, lifecycle, twId, twName);
}
@Override
public void initWidget(LinearWidget widget) {
widget.orientation(LinearWidget.horizontal)
.padding(16.0f);
twId.marginEnd(16.0f);
}
@Override
public void update() {
super.update();
if (user == null)
return;
twId.text(user.getId() + "");
twName.text(user.getName());
}
public void setData(UserBean user) {
this.user = user;
update();
}
}
除了 item UI 之外,还需要一个 Adapter。
作为有经验的开发者,adapter 你应该写过无数遍了,这里简单带过,唯一有点不同的是,之前都是 inflate
出 view,现在改成 Widget.render()
。
因为比较简单,这里只讲几点不同的地方。
onCreateViewHolder
里面,返回的 ViewHolder 里面保存的UI由 View
变成 了 Widget
,但由于创建 Widget
需要 lifecycle,所以在构造方法中传进来。
onBindViewHolder
中,通过 holder 拿到 widget,设置数据。
public class UserItemAdapter extends RecyclerView.Adapter<UserItemAdapter.ViewHolder> {
private List<UserBean> data = new ArrayList<>();
private Lifecycle lifecycle;
public UserItemAdapter(Lifecycle lifecycle) {
this.lifecycle = lifecycle;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
UserItemWidget widget = new UserItemWidget(parent.getContext(), lifecycle);
return new ViewHolder(widget);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
UserBean item = getData().get(position);
holder.widget.setData(item);
if (itemClickListener != null) {
holder.itemView.setOnClickListener(v -> {
itemClickListener.onClick(v, position);
});
}
}
@Override
public int getItemCount() {
return data.size();
}
public void setData(List<UserBean> data) {
if (null != data)
this.data = data;
else
this.data.clear();
notifyDataSetChanged();
}
public void addData(List<UserBean> data) {
if (null == data || data.size() == 0)
return;
int index = getItemCount();
this.data.addAll(data);
notifyItemRangeInserted(index, data.size());
}
public static class ViewHolder extends RecyclerView.ViewHolder {
UserItemWidget widget;
public ViewHolder(UserItemWidget widget) {
super(widget.render());
this.widget = widget;
}
}
public List<UserBean> getData() {
return data;
}
private ItemClickListener itemClickListener;
public void setOnItemClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
public interface ItemClickListener {
void onClick(View view, int position);
}
}
新建 UserLceeWidget
继承 LceeWidget
。
需要实现 renderLoading
、renderContent
、renderEmpty
、renderError
、onLoadData
等方法。
大致意思从方法名就可以知道。
先实现 renderLoading
、renderEmpty
、renderError
这 3 个简单的。
基本上一行代码搞定,如果你需要自定义样式,可以参考 Common 系列来自己写一个 Widget。
这里empty 和 error 都传入了一个 reload 变量,用于点击后出发重新加载,如果你不想有任何动作,传入 null 就可以。
private View.OnClickListener reload = v -> reload();
@Override
protected Widget renderLoading() {
return new CommonLoadingWidget(context, lifecycle);
}
@Override
protected Widget renderEmpty() {
return new CommonEmptyWidget(context, lifecycle, "No data. Click to reload", reload);
}
@Override
protected Widget renderError() {
return new CommonEmptyWidget(context, lifecycle, "Network error. Click to reload", reload);
}
一般来说,content 才是主要的 UI,简单起见,构建 UI 的时候,先不考虑数据怎么获取。 这也符合 MVVM 的设计思想。
这里外面用一个 FrameWidget
,里面是 RecyclerWidget
和 BaseAndroidWidget<FloatingActionButton>
组成。
这里,为了让 renderContent
清晰简洁,能一眼看得到层次结构,就把内部的 widget 构建抽成一个方法(这是我在做 react 开发的时候,总结的一种提高可读性的代码风格,读过 《clean code》 的人应该也会这么做吧)
这里有一点需要提一下,这里 Recycler
和 Fab
都采用了继承的方法是初始化属性,是因为这里不得不用继承来实现。 如果其他情况,我推荐调用方法来初始化,这样可以减少匿名类的数量(不过对性能影响不大,会让方法数减少一些)。
private List<UserBean> data = Collections.emptyList();
@Override
protected Widget renderContent() {
return new FrameWidget(context, lifecycle,
renderRecycler(),
renderFab()
).matchParent();
}
private RecyclerWidget renderRecycler() {
return new RecyclerWidget<UserItemAdapter>(context, lifecycle) {
@Override
protected void initProps() {
width = matchParent;
height = matchParent;
layoutManager = new LinearLayoutManager(context);
adapter = new UserItemAdapter(lifecycle);
}
@Override
public void updateView(RecyclerView view) {
adapter.setData(data);
}
};
}
private BaseAndroidWidget<FloatingActionButton> renderFab() {
return new FloatingActionButtonWidget(context, lifecycle)
.wrapContent()
.layoutGravity(Gravity.END | Gravity.BOTTOM)
.margin(16.f)
.onClick(reload);
}
UI都构建好了,下面是获取数据,获取数据这一步也变得很单纯,不需要关注UI怎么渲染,也不需要用异步(各种回调烦死了),只要关注数据和对应的状态即可。
这里实现相当简洁,一行代码获取数据,如果你确信数据不可能是空 或 不需要 empty 的状态,那么直接2行代码就可以搞定。
这里返回值是表示获取数据后的下一个状态。也就是你返回什么状态,那么,UI就会变成相应的状态。
@Override
protected LceeStatus onLoadData() throws NetworkErrorException {
data = UserDataSource.getInstance().getUsersFromRemote();
if (data.isEmpty())
return LceeStatus.Empty;
return LceeStatus.Content;
}
需要注意的是, onLoadData
总是在非主线程中运行,如果发生错误,直接抛出相应的 Exception 即可,无需主动返回 LceeStatus.Error
,本框架将自动触发 showError
状态,可在 renderError
中通过成员变量 lastError
得到错误进行处理
上面基本满足了正常的需求,但考虑难免有其他需求,比如:如何主动触发状态变更?
重新加载数据你应该知道了,调用 reload
方法即可,那其他状态呢?
不妨点进 reload
方法看看如何实现的,就是调用了 showLoading
。
所以如果你想换其他状态,可以调用 showXXX
方法。
或者你还可以直接调用 updateStatus
方法。
那么如何感知状态改变?
重写 onStatusChanged
方法,注意保留 super.onStatusChanged
。
这里简单介绍了 Lcee 的用法。 通过这一篇,你应该可以理解 MVVM 的强大之处。 让UI逻辑变得如此清晰。
这个例子,并没有解决常见的下拉刷新,上滑加载更多问题。 但不要慌,好歹我们已经迈出了一步。
在解决这个问题之前,先把上一节留下的第二个问题解决了。