避免 ListView 不必要的刷新

Avoid ListView's unwanted refresh

如以下动画所示,当我点击 StreamBuilder() 正在查询的列表项之一时,它会在右侧较暗的容器中显示项目数据(它始终是 “_JsonQueryDocumentSnapshot”的实例)。但是在每次点击的同时,整个列表都在刷新自己,我认为这不是很划算。

如何避免这种不需​​要的刷新? 也欢迎有 GetX 状态管理依赖的回答。

class Schedule extends StatefulWidget {
  @override
  _ScheduleState createState() => _ScheduleState();
}

class _ScheduleState extends State<Schedule> {

  final FirebaseFirestore _db = FirebaseFirestore.instance;
  final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));

  var _chosenData;

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: StreamBuilder<QuerySnapshot>(
            stream: _db.collection('Schedule').where('date', isGreaterThan: _yesterday).limit(10).orderBy('date').snapshots(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.active) {
                return ListView.builder(
                  itemCount: snapshot.data!.docs.length,
                  itemBuilder: (context, index) {
                    var data = snapshot.data!.docs[index];
                    return ListTile(
                      leading: Icon(Icons.person),
                      title: Text(data['project'], style: TextStyle(fontWeight: FontWeight.bold)),
                      subtitle: Text(data['parkour']),
                      onTap: () {
                        setState(() {_chosenData = data;});
                      },
                    );
                  },
                );
              } else {
                return Center(child: CupertinoActivityIndicator());
              }
            },
          ),
        ),
        VerticalDivider(),
        Expanded(
          child: Container(
            alignment: Alignment.center,
            color: Colors.black26,
            child: Text('$_chosenData'),
          ),
        ),
      ],
    );
  }
}

调用 setState() 会通知框架 Schedule 的状态已更改,这会导致重新构建小部件,因此您的 StreamBuilder.

您可以将流逻辑移动到小部件树的上层。因此,setState() 不会触发 StreamBuilder.

的重建
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: FirebaseFirestore.instance
          .collection('Schedule')
          .where(
            'date',
            isGreaterThan: DateTime.now().subtract(Duration(days: 1)),
          )
          .limit(10)
          .orderBy('date')
          .snapshots(),
      builder: (context, snapshot) {
        return Schedule(snapshot: snapshot); // Pass snapshot to Schedule
      },
    );
  }
}

另一种方法是使用调用一次的 Stream.listen in initState()。这样您的流就不会在每次调用 setState() 时都被订阅。

...

late StreamSubscription<QuerySnapshot> _subscription;

@override
void initState() {
  _subscription = _db
    .collection('Schedule')
    .where('date', isGreaterThan: _yesterday)
    .limit(10)
    .orderBy('date')
    .snapshots()
    .listen((QuerySnapshot querySnapshot) {
      setState(() {
        _querySnapshot = querySnapshot;
      });
    });

  super.didChangeDependencies();
}

@override
void dispose() {
  _subscription.cancel(); // Cancel the subscription
  super.dispose();
}

...

对我来说最简单的解决方案就是让它无状态并使用 Getx class.

class ScheduleController extends GetxController {
  var chosenData;

  void updateChosenData(var data) {
    chosenData = data;
    update();
  }
}

你的 Schedule.dart 看起来像这样:

class Schedule extends StatelessWidget {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  final DateTime _yesterday = DateTime.now().subtract(Duration(days: 1));

  @override
  Widget build(BuildContext context) {
    final controller = Get.put(ScheduleController());
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: StreamBuilder<QuerySnapshot>(
            stream: _db
                .collection('Schedule')
                .where('date', isGreaterThan: _yesterday)
                .limit(10)
                .orderBy('date')
                .snapshots(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.active) {
                return ListView.builder(
                  itemCount: snapshot.data!.docs.length,
                  itemBuilder: (context, index) {
                    var data = snapshot.data!.docs[index];
                    return ListTile(
                      leading: Icon(Icons.person),
                      title: Text(data['project'],
                          style: TextStyle(fontWeight: FontWeight.bold)),
                      subtitle: Text(data['parkour']),
                      onTap: () => controller.updateChosenData(data), // calls method from GetX class
                    );
                  },
                );
              } else {
                return Center(child: CupertinoActivityIndicator());
              }
            },
          ),
        ),
        VerticalDivider(),
        Expanded(
          child: Container(
            alignment: Alignment.center,
            color: Colors.black26,
            child: GetBuilder<ScheduleController>(
              builder: (controller) => Text('${controller.chosenData}'), // only this rebuilds
            ),
          ),
        ),
      ],
    );
  }
}

这样 listview.builder 永远不会重建,当您选择不同的 ListTile 时,只会重建 GetBuilder 内的文本小部件。