ViewHolder 模式
Pan原生实现了ViewHolder模式,开发者无需自行绑定ViewHolder。
ListView,Adapter和ViewModel的嵌套
在ListView或GridView等可重复布局中,都需要使用到Adapter,一般来说,通过继承BaseAdapter并实现相应方法来完成。
使用ListView的界面中,界面实际上被分为了两层:
- 列表层,即ListView,以及与ListView同级的其他View,例如list.xml
- Item层,即ListView中的每一个元素,同时,可能还有headerView和footerView以及其他自定义的View,同处这一层,例如item.xml
这两层基本没有关系,只通过Adapter建立了微弱的联系,Item其实并不需要知道自己身处在哪个ListView中,而ListView也不太关心底层的Item的具体实现。因此,ViewModel也可以同样使用两层,以明确这种弱关系,同时解耦两层的渲染逻辑。
首先是ListViewModel,主要负责ListView层面的渲染,同时在Adapter中使用ItemViewModel把Item的渲染代理出去。
@Xml(R.layout.list) public class ListViewModel extends GeneralViewModel{ @BindView(R.id.list) public ListView vListView; private List<Data> mDatas; private BaseAdapter mAdapter; @Override protected void onInit(){ vListView.setAdapter(mAdapter = new ListViewAdapter()); } @Override protected void render(){ mAdapter.notifyDatasetChanged(); } public ListViewModel setData(List<Data> datas){ mDatas = datas; return this; } class ListViewAdapter extends BaseAdapter{ //... @Override public View getView(int position, View convertView, ViewGroup parent){ //两层ViewModel只有非常弱的联系 return Pan.with(getActivity(), ItemViewModel.class) .getViewModel(parent, convertView, false). .setData(datas.get(position)) .getRootView(); } } }
其次是ItemViewModel,ItemViewModel并不需要知道自己被用在了何处,可以是Activity中某个部分使用了,也可能是Adapter中使用了,但ItemViewModel并不关心。
@Xml(R.layout.item) public class ItemViewModel extends GeneralViewModel{ @BindView(R.id.text) public TextView vTextView; private Data mData; public ItemViewModel setData(Data data){ mData = data; return this; } @Override protected void render(){ vTextView.setText(mData.toString()); } }
最后,在Activity中的调用,也很简洁,只需要绑定ListViewModel。
//在Activity中 protected void onCreate(Bundle savedInstance){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Pan.with(this, ListViewModel.class) .controlledBy(ListViewController.class) .getViewModel(); }
ViewHolder重用中的异步Controller
Controller不会被重用
ViewModel通过Tag识别,进行了重用,但getViewModel会重新实例化一个Controller并执行新的初始化周期。
在大多数情况下,这是符合直觉的情况,因为当用户下滑到下一个卡片时,需要根据数据重新绑定事件。这里的主要问题在于,如果Controller包含了异步操作,例如进行了网络请求,当请求返回时,用户已经下滑了,可能的状态如下:
- ViewModel:随着下滑,ViewModel被重用,绑定的数据被更新
- Old Controller:发起请求的Controller,含有ViewModel引用,且在请求返回时,尝试更新数据
- New Controller:下滑后,新的Item绑定的Controller
因此,在这种情况,应注意异步更新数据不应该直接通过ViewModel的引用更新数据,而是更新final化的方法内局域变量,例如:
//in controller protected void updateInfo(){ final Data bindedData = $vm.getData(); requestData((data) -> { bindedData.setText(data.getText()); // wrong way: // $vm.getData().setText(data.getText()); }); }
这个坑是ViewHolder模式与生俱来的,而不是Pan引起的。在不使用Pan的情况下,同样的更新错误也可能会在Adapter中发生。Pan并不解决这一问题,因为大多数情况下Adapter中的数据是伴随列表改变的,而不是自行异步更新。当遇到异步更新的情况,开发者也应该清楚在ViewHolder模式中这会带来的问题,小心即可。
RecyclerViewModel
RecyclerViewModel用于RecyclerView中,由于Java的单继承,RecyclerView.Adapter需要返回一个ViewHolder的子类对象,因此,这里使用RecyclerViewModel作为基类以替换GeneralViewModel。其他用法和ListView完全一致。
@Xml(R.layout.item) public class RecyclerItemViewModel extends RecyclerViewModel{ //... } //Adapter public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerItemViewModel>{ @Override RecyclerItemViewModel onCreateViewHoler(ViewGroup parent, int viewType){ return Pan.with(getLifecycleObserved(), RecyclerItemViewModel.class) .getViewModel(parent, null, false); } @Override void onBindViewHolder(RecyclerItemViewModel holder, int position){ //holder.setData(...) } }