1. 问题场景
日常应用开发中,使用RecyclerView 来展示列表页是非常常见的,而这些列表页经常会需要展示网络错误页、数据空白页还有加载页等其他状态的页面。如果应用全局使用同一套页面,那就是一种比较轻松的情况了。实际工作中,应用通常会包含多种错误页面,有些仅仅是图标、文案不同,有些可能还包含其他额外操作(例如,网络错误时可以重新加载,数据空白时可以跳转至推荐页)。这样的情况下,就需要对各个页面的特殊状态定制界面。常见方案有:
- 在各个页面的XML布局文件中直接编写对应的界面或者
include 对应界面,然后在代码中通过setVisibility 方法来控制各个状态下页面的显示。(这种方法不会导致隐式的耦合,各个页面相对独立,但是代码量比较多,而且多次对控件进行setVisibility 的时候可能出错) - 自定义
RecyclerView 的Adapter 和ViewHolder 来管理多个状态,通过调用Adapter 提供的接口方法来改变页面状态。(这种方法可以简化页面调用的代码,并提高复用性,但是会产生很多public 的Adapter 类,如果多个页面错误地共用一个Adapter 类,那么就会产生隐式的耦合,修改的时候就得检查这个类有哪些页面再使用,而且这些类中还会有重复的状态切换的代码)
本文就分享一种改良方法来处理这类场景,既不会导致代码数量增多、复用性降低,也不会导致过多的共用。 (StateDecoratorAdapter完整实现见本文最下方,完整的示例代码见:StateAdapterDemo示例项目)
2. StateDecoratorAdapter介绍
最初编写这个工具的时候考虑到尽量兼容已有代码、非侵入式的特点,决定参考装饰器模式的思想来为RecyclerView.Adapter 创建一个装饰器,并给这个新类添加状态的管理和切换功能。因此StateDecoratorAdapter 继承自RecyclerView.Adapter ,并可以管理多个Adapter ,在各个状态下采用对应的Adapter 来完成实际功能。
这个工具涉及三个类型:完成主要功能的State 和StateDecoratorAdapter ,以及一个辅助用的、用于减少使用代码的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方法
public StateDecoratorAdapter registerState(State state, RecyclerView.Adapter adapter) public StateDecoratorAdapter registerState(State state, ViewHolderAdapter adapter)
- 方法说明:这个方法用于注册其他状态的适配器。这里的适配器可以是直接继承自
RecyclerView.Adapter 的类,也可以是实现ViewHolderAdapter的类或lambda表达式。返回的对象就是当前StateDecoratorAdapter 对象,用于方法链式调用。
2.2.3 setState和getCurrentState方法
public synchronized void setState(State state) public State getCurrentState()
- 方法说明:这两个方法分别用于设置和获取当前状态,如果设置的状态不存在对应的适配器,则发出一个异常。注意,
setState 需要在主线程调用。
2.3 ViewHolder辅助适配器:ViewHolderAdapter
这个类型并不是关键类型,是为了减少使用代码、使用更方便而添加的辅助接口。这个接口包含四个方法,分别对应于RecyclerView.Adapter 的四个常用方法:onCreateViewHolder 、onBindViewHolder 、getItemCount 以及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 。如果有些页面有特殊操作,例如错误页的重试功能,我们只需要在对应的ViewHolder 或registerState 的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;
}
}
}
|