Skip to content

Latest commit

 

History

History
321 lines (239 loc) · 10.8 KB

1.LceeWidget.md

File metadata and controls

321 lines (239 loc) · 10.8 KB

目的

目的:学习 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 中的按钮 重新请求数据。

准备

UserDataSource

为了模拟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;
    }

UserItemWidget

因为用到了 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();
    }

}

UserItemAdapter

除了 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

新建 UserLceeWidget 继承 LceeWidget。 需要实现 renderLoadingrenderContentrenderEmptyrenderErroronLoadData 等方法。

大致意思从方法名就可以知道。

lee

先实现 renderLoadingrenderEmptyrenderError 这 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

一般来说,content 才是主要的 UI,简单起见,构建 UI 的时候,先不考虑数据怎么获取。 这也符合 MVVM 的设计思想。

这里外面用一个 FrameWidget,里面是 RecyclerWidgetBaseAndroidWidget<FloatingActionButton> 组成。

这里,为了让 renderContent 清晰简洁,能一眼看得到层次结构,就把内部的 widget 构建抽成一个方法(这是我在做 react 开发的时候,总结的一种提高可读性的代码风格,读过 《clean code》 的人应该也会这么做吧)

这里有一点需要提一下,这里 RecyclerFab 都采用了继承的方法是初始化属性,是因为这里不得不用继承来实现。 如果其他情况,我推荐调用方法来初始化,这样可以减少匿名类的数量(不过对性能影响不大,会让方法数减少一些)。

    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);
    }

onLoadData

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 得到错误进行处理

change status

上面基本满足了正常的需求,但考虑难免有其他需求,比如:如何主动触发状态变更?

重新加载数据你应该知道了,调用 reload 方法即可,那其他状态呢?

不妨点进 reload 方法看看如何实现的,就是调用了 showLoading

所以如果你想换其他状态,可以调用 showXXX 方法。

或者你还可以直接调用 updateStatus 方法。

notify status change

那么如何感知状态改变?

重写 onStatusChanged 方法,注意保留 super.onStatusChanged

总结

这里简单介绍了 Lcee 的用法。 通过这一篇,你应该可以理解 MVVM 的强大之处。 让UI逻辑变得如此清晰。

这个例子,并没有解决常见的下拉刷新,上滑加载更多问题。 但不要慌,好歹我们已经迈出了一步。

在解决这个问题之前,先把上一节留下的第二个问题解决了。