【Flutter】一种实现可选择列表的思路

4,711 阅读3分钟

一般来说,在 Flutter 中写可选择列表的思路就是将列表中的选择状态用一个数组保存起来,每当用户点击列表中的任意一项的时候,这个数组便会更新。

class SomeListViewState extend State<SomeListView> {
	final selections = <bool>[];
	
	@override
	void initState() {
    super.initState();
		selections.addAll(List.filled(widget.data.length, false));
	}
	
	@override
	Widget build() {
		return ListView(
			...
			children: widget.data.mapWithIndex((detail, index) {
				return SomeWidget(
					...
          selected: selection[index],
					onTap:() {
						_tapItem(detail, index);
					},
				);
			},
    );
	}
	
	_tapItem(SomeData detail, int index) {
		setState(() {
      selections[index] = !selections[index];
    });
	}
}

这样写看起来没什么问题,即便是列表中的数据多起来也不会有太大问题,因为 ListView 提供了虚拟列表的特性,使得 setState 只会去刷新当前已展示的组件。

下面将以实现一个单选列表来讲解我的思路。

单选组件

即便如此,仍然有部分刷新是不必要的。上面的代码中的 setState 会将可见区域内的所有 SomeWidget 进行刷新,然而,当用户单选的时候,只要记住上一个用户所选的状态,那么理论上,只有两个 SomeWidget 需要为此进行变更。这种思路下,每个 SomeWidget 必然都是一个 StatefulWidget 。

在这里,我使用了 RxDart 作为响应式工具,,当监听到外部输入的时候,会立刻更新内部状态。

// 单选列表中单个元素组件的状态
class _SomeWidgetState extends State<SomeWidget> {
  ...
  bool value = false;
  StreamSubscription subscription;
  BehaviorSubject<bool> controller;
  
  @override
  void initState() {
    super.initState();
    controller = widget.controller;
    // 监听输入
    subscription = controller.listen((nextValue) {
      setState(() {
        // 在这里进行变更
        value = nextValue;
      });
    });
  }
  
  // 注意:组件销毁的时候,需要取消订阅。
  @override
  void dispose() {
    super.dispose();
    subscription?.cancel();
  }
  ...
}

组件需要监听变化外部输入。

@override
Widget build(BuildContext context) {
  return ListTile(
    dense: true,
    title: widget.title,
    selected: value,
    onTap: () {
      // 将当前状态取反,就是下一个状态
      final theValue = !value;

      // 更新组件
      controller.add(theValue);
      
      // 传出给外部
      widget.onChanged(theValue);
    },
  );
}

这样,列表中的所有组件都将带有状态。下一步,我们需要把它们的状态集合起来,让列表来进行控制。

单选列表控制器

为了控制这些状态,我们得先构造一个列表控制器,它保存了当前列表中,所有组件的状态。

class SingleCheckListController {
  // 为了方便,使用了一个 map 来对这些组件状态进行收集。
  final Map<int, BehaviorSubject<bool>> _controllers = {};

  // 记录上一个为 true 的组件状态
  int _prevTrue = -1;

  SingleCheckController();

  BehaviorSubject<bool> get(int index) {
    final controller = _controllers[index];
    assert(controller != null);
    return controller;
  }
  ...
}

单选功能是一个简单的状态切换,需要对传入的数据, _prevTrue 等状态进行判断。

void select(int index, {bool value}) {
  final selectedController = _controllers[index];
  selectedController.add(value);

  // 如果未 true 进行下一步
  if (value) {
    // 剔除不符合要求的情况
    if (_prevTrue == index) {
      return;
    }
    // 如果不是初始状态,那么得将之前的组件的状态更新。
    if (_prevTrue != -1) {
      _controllers[_prevTrue].add(false);
    }
    // 更新 _prevTrue
    _prevTrue = index;
  }
}

每当整个列表更新的时候,都需要根据输入列表的长度进行更新。整体的设计思路还是为了避免过多的创建状态,使得之前已经创建的 BehaviorSubject 能够继续使用。

void reset(int length) {
  _prevTrue = -1;
  for (var index = 0; index < length; index++) {
    BehaviorSubject<bool> controller = _controllers[index];
    if (controller == null) {
      // 如果为空,则新增
      controller = BehaviorSubject.seeded(false);
      _controllers[index] = controller;
    } else {
      controller.add(false);
    }
  }
}

单选列表

到了单选列表这一步,就变得非常简单了。我们的单选列表一定是 StatefulWdiget ,因为需要保存上述控制器的状态。

class _SingleCheckListViewState<T> extends State<SingleCheckListView<T>> {
  ...

  final SingleCheckListController controller = SingleCheckListController();

  @override
  void initState() {
    super.initState();
    controller.reset(widget.items.length);
  }

  @override
  void dispose() {
    super.dispose();
    // 注意:需要销毁所有的状态,避免内存泄露
    controller.dispose();
  }

  ...
}

至此,我们已经完成了基本工作,一般是这样调用的。

SingleCheckListView<int>(
  items: items,
  builder: (context, item, index) {
    return Text('this is $item');
  },
  onItemTap: (item, index, selected) {
    print('select $item, at $index, state is $selected');
  },
),

小结

本文介绍了一种实现可选列表的思路,一般来说,直接使用 setState 更新不会有太大的问题,因为有 ListView 的虚拟列表存在。而这我所提供的设计思路,主要解决的是多余的组件刷新问题,让组件刷新控制在更细的粒度上。