StateDecoratorAdapter:RecyclerView多状态适配器

1. 问题场景

日常应用开发中,使用RecyclerView来展示列表页是非常常见的,而这些列表页经常会需要展示网络错误页、数据空白页还有加载页等其他状态的页面。如果应用全局使用同一套页面,那就是一种比较轻松的情况了。实际工作中,应用通常会包含多种错误页面,有些仅仅是图标、文案不同,有些可能还包含其他额外操作(例如,网络错误时可以重新加载,数据空白时可以跳转至推荐页)。这样的情况下,就需要对各个页面的特殊状态定制界面。常见方案有:

  1. 在各个页面的XML布局文件中直接编写对应的界面或者include对应界面,然后在代码中通过setVisibility方法来控制各个状态下页面的显示。(这种方法不会导致隐式的耦合,各个页面相对独立,但是代码量比较多,而且多次对控件进行setVisibility的时候可能出错)
  2. 自定义RecyclerViewAdapterViewHolder来管理多个状态,通过调用Adapter提供的接口方法来改变页面状态。(这种方法可以简化页面调用的代码,并提高复用性,但是会产生很多publicAdapter类,如果多个页面错误地共用一个Adapter类,那么就会产生隐式的耦合,修改的时候就得检查这个类有哪些页面再使用,而且这些类中还会有重复的状态切换的代码)

本文就分享一种改良方法来处理这类场景,既不会导致代码数量增多、复用性降低,也不会导致过多的共用。
(StateDecoratorAdapter完整实现见本文最下方,完整的示例代码见:StateAdapterDemo示例项目

2. StateDecoratorAdapter介绍

最初编写这个工具的时候考虑到尽量兼容已有代码、非侵入式的特点,决定参考装饰器模式的思想来为RecyclerView.Adapter创建一个装饰器,并给这个新类添加状态的管理和切换功能。因此StateDecoratorAdapter继承自RecyclerView.Adapter,并可以管理多个Adapter,在各个状态下采用对应的Adapter来完成实际功能。

这个工具涉及三个类型:完成主要功能的StateStateDecoratorAdapter,以及一个辅助用的、用于减少使用代码的ViewHolderAdapter类型。

2.1 状态(State)

既然涉及多个状态,那么必然会有一个数据结构用来表示状态。有些人习惯使用字符串常量,有些人喜欢整数常量,还有人偏向枚举类或自定义类。考虑再三,我还是选择将状态表示为一个示意性接口(没有任何方法的接口):

public interface State {

}

这样已有的枚举或类只要实现这个接口就可用被用作状态,例如,后面示例中的State枚举类就实现为:

public enum State implements StateDecoratorAdapter.State {
    ERROR,  //错误状态
    NORMAL, //正常状态
    EMPTY,  //空白状态
    LOADING //加载状态
}

2.2 多状态适配器StateDecoratorAdapter

这个类完成多状态管理和切换的主要功能,各个接口定义如下:

2.2.1 decorate静态方法
  • 方法签名:public static StateDecoratorAdapter decorate(State state, RecyclerView.Adapter<?> adapter)
  • 方法说明:这个方法用于创建StateDecoratorAdapter类的实例,参数中的state通常为正常状态的对象,而adapter则是该状态对应的适配器。注意,这里的state默认被设置为当前状态。
2.2.2 registerState方法
  • 方法签名:
  1. public StateDecoratorAdapter registerState(State state, RecyclerView.Adapter adapter)
  2. public StateDecoratorAdapter registerState(State state, ViewHolderAdapter adapter)
  • 方法说明:这个方法用于注册其他状态的适配器。这里的适配器可以是直接继承自RecyclerView.Adapter的类,也可以是实现ViewHolderAdapter的类或lambda表达式。返回的对象就是当前StateDecoratorAdapter对象,用于方法链式调用。
2.2.3 setState和getCurrentState方法
  • 方法签名:
  1. public synchronized void setState(State state)
  2. public State getCurrentState()
  • 方法说明:这两个方法分别用于设置和获取当前状态,如果设置的状态不存在对应的适配器,则发出一个异常。注意,setState需要在主线程调用。

2.3 ViewHolder辅助适配器:ViewHolderAdapter

这个类型并不是关键类型,是为了减少使用代码、使用更方便而添加的辅助接口。这个接口包含四个方法,分别对应于RecyclerView.Adapter的四个常用方法:onCreateViewHolderonBindViewHoldergetItemCount以及getItemViewType。为了可以使用函数式接口的特性,给除了onCreateViewHolder以外的三个方法添加默认实现:

@FunctionalInterface
public interface ViewHolderAdapter {
    @NonNull
    RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType);

    default void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {

    }

    default int getItemCount() {
        return 1;
    }

    default int getItemViewType(int position) {
        return 0;
    }
}

这样依赖,对于没有交互的空白页、错误页,只要使用一两行代码就可以将新的状态界面添加到StateDecoratorAdapter中:

adapter.registerState(State.EMPTY, (parent, viewType) -> new EmptyViewHolder(parent));

3. 使用示例

这里假设我们要显示的是一个数字列表,那么我们的主要ViewHolder可以实现为:

public class ItemHolder extends RecyclerView.ViewHolder {
    private final HolderItemBinding binding;

    public static ItemHolder create(ViewGroup viewGroup) {
        HolderItemBinding binding = HolderItemBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
        return new ItemHolder(binding);
    }

    private ItemHolder(@NonNull HolderItemBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
    }

    public void setData(int data) {
        binding.tvText.setText("No." + data);
    }
}

假定我们的空白页、错误页、加载页都没有特殊交互,我们再添加一个显示简单页面的ViewHolder

public class SimpleViewHolder extends RecyclerView.ViewHolder {
    public SimpleViewHolder(@NonNull ViewBinding binding) {
        super(binding.getRoot());
    }
}

而我们的Activity中对应功能可以这样编写:

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private StateDecoratorAdapter adapter;

    private final List<Integer> dataList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        adapter = StateDecoratorAdapter.decorate(State.NORMAL, new RecyclerView.Adapter<ItemHolder>() {//这个Adapter用自己的实际Adapter替代
            @NonNull
            @Override
            public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                return ItemHolder.create(parent);
            }

            @Override
            public void onBindViewHolder(@NonNull ItemHolder holder, int position) {
                holder.setData(dataList.get(position));
            }

            @Override
            public int getItemCount() {
                return dataList.size();
            }
        });
        adapter.registerState(State.EMPTY, (parent, viewType) -> //注册空页面的Adapter
                new SimpleViewHolder(HolderEmptyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)));
        adapter.registerState(State.ERROR, (parent, viewType) -> //注册错误页面的Adapter
                new SimpleViewHolder(HolderErrorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)));
        adapter.registerState(State.LOADING, (parent, viewType) -> //注册加载页面的Adapter
                new SimpleViewHolder(HolderLoadingBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)));

        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
        binding.recyclerView.setAdapter(adapter);

        binding.btnRetry.setOnClickListener(view -> requestData());
    }

    @Override
    protected void onResume() {
        super.onResume();
        binding.recyclerView.post(this::requestData);
    }

    /* 这个方法用于模拟接口请求 */
    private void requestData() {
        dataList.clear();
        adapter.setState(State.LOADING);
        Single
                .fromCallable(() -> {
                    Thread.sleep(2000);
                    Random random = new Random();
                    if (random.nextBoolean()) {
                        throw new IllegalStateException("Something error");  //模拟出错的情况
                    } else if (random.nextBoolean()) {
                        return Collections.<Integer>emptyList();             //模拟空的情况
                    } else {
                        return Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); //模拟正常情况
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(list -> {
                    if (Objects.isNull(list) || list.isEmpty()) {
                        adapter.setState(State.EMPTY); //空的时候只需要设置空状态即可
                    } else {
                        dataList.addAll(list);
                        adapter.setState(State.NORMAL);//正常情况下,将数据加入集合中,再设置
                    }
                }, throwable -> adapter.setState(State.ERROR)); //出错时候只需要设置错误状态即可
    }
}

这样我们就完成正常列表数据页、错误页、空白页、加载页状态切换和数据展示功能。可以看到上面requestData方法中状态切换的代码非常简洁明了,不容易出错;给列表添加其他状态的页面时,也只需要少量代码,而且还可以直接使用现有的Adapter。如果有些页面有特殊操作,例如错误页的重试功能,我们只需要在对应的ViewHolderregisterState的lambda表达式中实现就可以了,不影响其他状态界面和功能。

4. 实现代码

import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * RecyclerView 的 adapter 装饰器类,支持根据状态来切换不同的 adapter
 *
 * @author skyline1225
 */
public class StateDecoratorAdapter extends RecyclerView.Adapter {
    /* 状态与适配器的映射Map */
    private final Map<State, RecyclerView.Adapter> stateAdapterMap = new ConcurrentHashMap<>();
    /* 当前状态 */
    @NonNull
    private State currentState;
    /* 当前状态对应的适配器 */
    @NonNull
    private RecyclerView.Adapter currentAdapter;
    @Nullable
    private RecyclerView attachedRecyclerView;

    public static StateDecoratorAdapter decorate(@NonNull State state, @NonNull RecyclerView.Adapter<?> adapter) {
        return new StateDecoratorAdapter(state, adapter);
    }

    /**
     * @param state   初始状态
     * @param adapter 初始 Adapter
     */
    private StateDecoratorAdapter(@NonNull State state, @NonNull RecyclerView.Adapter<?> adapter) {
        currentState = state;
        currentAdapter = adapter;
        registerState(state, adapter);
    }

    /**
     * 注册对应状态的适配器
     *
     * @param state   注册的状态
     * @param adapter 状态对应的适配器
     * @return 装饰对象自身
     */
    public StateDecoratorAdapter registerState(@NonNull State state, @NonNull RecyclerView.Adapter adapter) {
        stateAdapterMap.put(state, adapter);
        return this;
    }

    /**
     * 注册对应状态的辅助适配器
     *
     * @param state   注册的状态
     * @param adapter 状态对应的辅助适配器
     * @return 装饰对象自身
     */
    public StateDecoratorAdapter registerState(State state, ViewHolderAdapter adapter) {
        return registerState(state, new RecyclerView.Adapter() {
            @NonNull
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                return adapter.onCreateViewHolder(parent, viewType);
            }

            @Override
            public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
                adapter.onBindViewHolder(holder, position);
            }

            @Override
            public int getItemCount() {
                return adapter.getItemCount();
            }

            @Override
            public int getItemViewType(int position) {
                return adapter.getItemViewType(position);
            }
        });
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return currentAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        currentAdapter.onBindViewHolder(holder, position);
    }

    @Override
    public int getItemCount() {
        return currentAdapter.getItemCount();
    }

    @Override
    public int getItemViewType(int position) {
        return currentAdapter.getItemViewType(position);
    }

    /**
     * 新的状态和当前状态不想等时,设置当前状态为新的状态,并更新当前适配器; 否则,跳过
     *
     * @param state 新的状态
     */
    public synchronized void setState(@NonNull State state) {
        if (!Objects.equals(state, currentState)) {
            RecyclerView.Adapter adapter = stateAdapterMap.get(state);
            if (adapter != null) {
                currentAdapter = adapter;
                currentState = state;

                if (attachedRecyclerView != null) {
                    attachedRecyclerView.setAdapter(this);
                }
            } else {
                throw new IllegalStateException(String.format("Can not find a adapter corresponding to state[%s]", state));
            }
        }
    }

    @NonNull
    public State getCurrentState() {
        return currentState;
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        attachedRecyclerView = recyclerView;
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onDetachedFromRecyclerView(recyclerView);
        attachedRecyclerView = null;
    }

    /**
     * 状态的示意性接口
     */
    public interface State {

    }

    /**
     * 辅助适配器,简化创建适配器的代码
     */
    @FunctionalInterface
    public interface ViewHolderAdapter {
        @NonNull
        RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType);

        default void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {

        }

        default int getItemCount() {
            return 1;
        }

        default int getItemViewType(int position) {
            return 0;
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值