You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter自定义过滤实现及相关开发问题咨询

Flutter - 自定义过滤的最佳实现方式

当前实现

站点列表页面

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../data/metro_repository.dart';
import '../data/http_metro_datasource.dart';
import '../data/sqflite_metro_datasource.dart';
import '../models/station.dart';
import 'station_detail_page.dart';

class ListaPage extends StatefulWidget {
  const ListaPage({super.key});

  @override
  State<ListaPage> createState() => _ListaPageState();
}

class _ListaPageState extends State<ListaPage> {
  String _pesquisaNome = "";

  @override
  Widget build(BuildContext context) {
    final repositorio = MetroRepository(
      local: context.read<SqfliteMetroDataSource>(),
      remote: context.read<HttpMetroDataSource>(),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text("Stations"),
      ),
      body: FutureBuilder<List<Station>>(
        future: repositorio.getAllStations(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return const Center(
              child: Text("Error loading stations"),
            );
          }

          final stations = snapshot.data ?? [];

          final filteredStations = stations.where((station) {
            final name = station.name.toLowerCase().trim();

            final matchesSearch =
                name.contains(_pesquisaNome.toLowerCase().trim());

            final hasLetterL = name.contains("l");
            final isSingleWord =
                name.split(RegExp(r'\s+')).length == 1;

            final isForbidden =
                hasLetterL && isSingleWord;

            return matchesSearch && !isForbidden;
          }).toList();

          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(12),
                child: TextField(
                  decoration: InputDecoration(
                    hintText: "Search station...",
                    prefixIcon: const Icon(Icons.search),
                    border: OutlineInputBorder(
                      borderRadius:
                          BorderRadius.circular(12),
                    ),
                  ),
                  onChanged: (value) {
                    setState(() {
                      _pesquisaNome = value;
                    });
                  },
                ),
              ),
              Expanded(
                child: ListView.builder(
                  itemCount: filteredStations.length,
                  itemBuilder: (context, index) {
                    final station =
                        filteredStations[index];

                    return Card(
                      child: ListTile(
                        title: Text(station.name),
                        subtitle: Text(
                          station.lineName.toString(),
                        ),
                        onTap: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (_) =>
                                  StationDetailPage(
                                station: station,
                              ),
                            ),
                          );
                        },
                      ),
                    );
                  },
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

仪表盘页面

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../data/metro_repository.dart';
import '../data/http_metro_datasource.dart';
import '../data/sqflite_metro_datasource.dart';
import '../models/station.dart';

class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    final repositorio = MetroRepository(
      local: context.read<SqfliteMetroDataSource>(),
      remote: context.read<HttpMetroDataSource>(),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text("Dashboard"),
      ),
      body: FutureBuilder<List<Station>>(
        future: repositorio.getAllStations(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          final stations = snapshot.data!;

          final stationsWithA =
              stations.where((station) {
            return station.name
                .toLowerCase()
                .startsWith("a");
          }).length;

          final filteredStations =
              stations.where((station) {
            final name =
                station.name.toLowerCase().trim();

            final hasLetterL =
                name.contains("l");

            final isSingleWord =
                name.split(RegExp(r'\s+')).length == 1;

            return hasLetterL &&
                isSingleWord;
          }).length;

          return ListView(
            padding: const EdgeInsets.all(16),
            children: [
              Card(
                child: ListTile(
                  title:
                      const Text("Stations with A"),
                  trailing: Text(
                    stationsWithA.toString(),
                  ),
                ),
              ),
              Card(
                child: ListTile(
                  title: const Text(
                    "Filtered Stations",
                  ),
                  trailing: Text(
                    filteredStations.toString(),
                  ),
                ),
              ),
              Card(
                child: ListTile(
                  title:
                      const Text("Total Stations"),
                  trailing: Text(
                    stations.length.toString(),
                  ),
                ),
              ),
              Card(
                child: ListTile(
                  title:
                      const Text("Incidents"),
                  trailing: const Text("0"),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

事件校验

String? validateIncident({
  required int rating,
  required String note,
}) {
  if (rating == 5 &&
      note.trim().length < 20) {
    return "A note with at least 20 characters is required.";
  }

  return null;
}

技术问询

  • 在FutureBuilder中直接执行此类过滤与统计计算是否合理,还是应将该逻辑迁移至repository/service层?
  • 在大型Flutter项目中,应如何组织这类业务规则?
  • 有没有更简洁的方案来计算仪表盘统计数据,避免每次重建时重复计算?

问题解答

1. FutureBuilder内执行过滤统计的合理性判断

页面专属的临时简单过滤(比如列表的搜索框匹配)可以留在页面,但要封装成单独方法,避免代码堆在builder里。但像你代码中跨页面复用的规则(比如两次用到的「含字母L且为单单词」的过滤逻辑),必须迁移到repository或专门的业务逻辑层:

  • 避免重复代码,后续修改只需维护一处;
  • 业务逻辑集中管理,UI层仅负责展示,职责边界更清晰;
  • 数据量大时,在repo层提前缓存过滤/统计结果,减少UI重建时的重复计算损耗。

2. 大型项目中业务规则的组织方式

  • 分层拆分:把业务逻辑抽离到domain层,比如创建StationFilter、StationStatsCalculator、IncidentValidator这类单一职责的类,repository层负责数据获取并调用domain层的规则处理,UI层只做触发操作和展示;
  • 复用优先:所有跨页面/组件的规则必须封装成公共类,放在业务相关的公共目录下,禁止在多个页面重复编写相同逻辑;
  • 配合状态管理:如果是需要实时更新的过滤/统计,结合Provider、Riverpod等状态管理工具,把计算好的数据存在状态中,UI直接监听状态变化,无需每次重建都重新计算。

3. 避免仪表盘重复计算的方案

  • 缓存计算结果:将Dashboard改为StatefulWidget,在首次获取到站点数据时计算统计值并存在state变量中,后续UI重建直接使用缓存值;
  • 状态管理托管:创建StationStatsNotifier这类状态类,在获取到所有站点后一次性计算好stationsWithA、filteredStationsCount等统计值,UI直接消费这些预计算的结果,除非数据源更新,否则不会重新计算;
  • 缓存数据源:如果站点数据不会频繁更新,把获取到的站点列表存在全局缓存或本地存储中,下次进入页面直接使用缓存数据,跳过重复请求和计算。

内容的提问来源于stack exchange,提问作者Gjk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 03:36:58