当我只是想要图标时,Flutter的 FutureBuilder会重新构建整个页面

移动开发 2026-07-08

我正在开发一个Flutter应用,用来展示地铁站列表,使用 FutureBuilder

每个站点都有一个收藏按钮(Icons.favorite / Icons.favorite_border),收藏状态本地存储在SQLite中。

当用户点击收藏按钮时,我会更新站点状态并将改动保存到数据库中。然而,在调用 setState() 之后,FutureBuilder 会再次执行该Future,从而导致整个列表重新加载,数据也会再次被获取。

我的目标是在不重新加载整个列表的前提下,仅更新所选站点的收藏图标。

import 'package:cmproject/connectivity_module.dart';
import 'package:cmproject/data/http_metro_datasource.dart';
import 'package:cmproject/data/sqflite_metro_datasource.dart';
import 'package:cmproject/models/incident_report.dart';
import 'package:cmproject/models/station.dart';
import '../models/line_state.dart';
import '../models/station_cais_info.dart';
import 'metro_datasource.dart';

class MetroRepository extends MetroDataSource {
  SqfliteMetroDataSource local;
  HttpMetroDataSource remote;
  ConnectivityModule connectivityService;

  MetroRepository({required this.local, required this.remote, required this.connectivityService});





  // Get
  @override
  Future<List<Station>> getAllStations() async {
    if (await connectivityService.checkConnectivity()) {
      print("Com internet, irei buscar á API");

      var stations = await remote.getAllStations();

      for (var station in stations) {
        await local.insertStation(station);
      }

      return await local.getAllStations();
    } else {
      print("Sem internet, irei buscar á base de dados");
      return await local.getAllStations();
    }
  }

  @override
  Future<Station> getStationDetail(String id) async {
    if (await connectivityService.checkConnectivity()){
      print("Com internet, irei buscar á API");
      Station station = await remote.getStationDetail(id);

      Station stationLocal = await local.getStationDetail(id);

      station.reports = stationLocal.reports;
      return station;
    }else{
      print("Sem internet, irei buscar á base de dados");
      return local.getStationDetail(id);
    }
  }

  @override
  Future<List<Station>> getStationsByName(String name) async {
    if (await connectivityService.checkConnectivity()){
      print("Com internet, irei buscar á API");
      return remote.getStationsByName(name);
    }else{
      print("Sem internet, irei buscar á base de dados");
      return local.getStationsByName(name);
    }
  }




  // Insert
  @override
  Future<void> insertStation(Station station) async{
    return local.insertStation(station);
  }

  @override
  Future<void> attachIncident(String id, IncidentReport report) async{
    return local.attachIncident(id, report);
  }








  // Funçoes adicionais
  // Tempo de espera
  Future<List<StationCaisInfo>> getStationTime(String id) async{
    return remote.getStationTime(id);
  }


  // Todas os reports com as suas devidas estacoes
  Future<List<Map<String, dynamic>>> getAllReportsWithStation() async {
    return local.getAllReportsWithStation();
  }



  // Status das linhas
  Future<LineStatus> getLineStatus() async {
    return remote.getLineStatus();
  }

  Future<void> updateFavorite(String stationId, bool isFavorite) async {
    return local.updateFavorite(stationId, isFavorite);
  }

  Future<List<Station>> getFavoriteStations() async {
    return local.getFavoriteStations();
  }
}

Sqlite数据

import 'package:cmproject/data/metro_datasource.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import '../models/incident_report.dart';
import '../models/station.dart';


class SqfliteMetroDataSource extends MetroDataSource {
  // Base de dados
  Database? _database;

  SqfliteMetroDataSource (){
    init();
  }

  Future<void> init() async {
    _database = await openDatabase(join(await getDatabasesPath(), "ml.db"),
        onCreate: (db, version) async {
      // estaçoes
      await db.execute("CREATE TABLE station("
          "id TEXT PRIMARY KEY, "
          "name TEXT NOT NULL, "
          "latitude REAL NULL, "
          "longitude REAL NULL, "
          "lineName TEXT NULL, "
          "favorite INTEGER NOT NULL DEFAULT 0"
          ")");

      // Reports
      await db.execute("CREATE TABLE incident_report("
          "id INTEGER PRIMARY KEY AUTOINCREMENT, "
          "stationId TEXT NOT NULL, "
          "timestamp TEXT NOT NULL, "
          "rate INTEGER NOT NULL, "
          "notes TEXT NULL, "
          "type TEXT NOT NULL, "
          "FOREIGN KEY (stationId) REFERENCES station(id)"
          ")");
    }, version: 1);
  }




  // Get
  @override
  Future<List<Station>> getAllStations() async{
    if (_database == null){
      throw Exception("Nao inicializou a base de dados");
    }

    List result = await _database!.rawQuery("SELECT * FROM station");
    return result.map((entry) => Station.fromDB(entry)).toList();
  }

  @override
  Future<List<Station>> getStationsByName(String name) async {
    if (_database == null){
      throw Exception("Nao inicializou a base de dados");
    }

    List result = await _database!.rawQuery(
        "SELECT * FROM station WHERE name LIKE ?",
        ['%$name%']
    );
    return result.map((entry) => Station.fromDB(entry)).toList();
  }

  @override
  Future<Station> getStationDetail(String id) async {
    if (_database == null){
      throw Exception("Nao inicializou a base de dados");
    }

    List result = await _database!.rawQuery("SELECT * FROM station WHERE id = ?", [id]);
    var station = Station.fromDB(result.first);

    List reportsDB = await _database!.rawQuery("SELECT * FROM incident_report WHERE stationId = ?", [id]);
    List<IncidentReport> reports = reportsDB.map((entry) => IncidentReport.fromDB(entry)).toList();

    station.reports = reports;
    return station;
  }




  // Insert
  @override
  Future<void> insertStation(Station station) async{
    if (_database == null){
      throw Exception("Nao inicializou a base de dados");
    }

    List results = await _database!.rawQuery("SELECT * FROM station WHERE id = ?", [station.id]);
    if(results.isNotEmpty){

      station.favorite = results.first["favorite"] == 1;

      await _database!.update(
        "station",
        station.toDB(),
        where: "id = ?",
        whereArgs: [station.id],
      );
    }else{
      await _database!.insert("station", station.toDB());
    }
  }

  @override
  Future<void> attachIncident(String id, IncidentReport report) async{
    if (_database == null){
      throw Exception("Nao inicializou a base de dados");
    }

    await _database!.insert(
      "incident_report",
      {
        "stationId": id,
        "timestamp": report.timestamp.toIso8601String(),
        "rate": report.rate,
        "notes": report.notes,
        "type": report.type.name,
      },
    );
  }








  // Funçoes adicionais
  Future<List<Map<String, dynamic>>> getAllReportsWithStation() async {
    if (_database == null) {
      throw Exception("Nao inicializou a base de dados");
    }

    List<Map<String, dynamic>> result = await _database!.rawQuery(
        "SELECT incident_report.*, station.name as stationName "
            "FROM incident_report "
            "JOIN station ON incident_report.stationId = station.id"
    );
    return result;
  }

  Future<void> updateFavorite(String stationId, bool favorite) async {
    if (_database == null) {
      throw Exception("Nao inicializou a base de dados");
    }

    print("UPDATE $stationId -> $favorite");


    await _database!.update(
      "station",
      {
        "favorite": favorite ? 1 : 0,
      },
      where: "id = ?",
      whereArgs: [stationId],
    );
  }

  Future<List<Station>> getFavoriteStations() async {
    if (_database == null) {
      throw Exception("Nao inicializou a base de dados");
    }

    List result = await _database!.rawQuery(
        "SELECT * FROM station WHERE favorite = 1"
    );

    return result.map((entry) => Station.fromDB(entry)).toList();
  }
}

模型

import 'dart:core';

import 'incident_report.dart';

class Station {

  final String id;
  final String name;
  final double latitude, longitude;
  final String lineName;
  double distance;
  bool favorite = false;
  List<IncidentReport> reports;

  // Construtor
  Station({
    required this.id,
    required this.name,
    required this.latitude,
    required this.longitude,
    required this.lineName,
    this.distance = -1,
    this.favorite = false,
    List<IncidentReport>? reports,
  }) : reports = reports ?? [];







  // Maps para mover os dados
  // JSON
  factory Station.fromJSON(Map<String, dynamic> json){
    return Station(
        id: json["stop_id"],
        name: json["stop_name"],
        latitude: double.parse(json["stop_lat"].toString()),
        longitude: double.parse(json["stop_lon"].toString()),
        lineName: json["linha"],
    );
  }


  // Base de dados
  factory Station.fromDB(Map<String, dynamic> db){
    return Station(
      id: db["id"],
      name: db["name"],
      latitude: db["latitude"],
      longitude: db["longitude"],
      lineName: db["lineName"],
      favorite: db["favorite"] == 1 ? true : false
    );
  }

  Map<String, dynamic> toDB (){
    return {
      "id": id,
      "name": name,
      "latitude": latitude,
      "longitude": longitude,
      "lineName": lineName,
      "favorite": favorite ? 1 : 0
    };
  }

}

收藏

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

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

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

  @override
  State<FavoritosPage> createState() => _FavoritosPageState();
}

class _FavoritosPageState extends State<FavoritosPage> {

  @override
  Widget build(BuildContext context) {

    MetroRepository repositorio = MetroRepository(
      local: context.read<SqfliteMetroDataSource>(),
      remote: context.read<HttpMetroDataSource>(),
      connectivityService: context.read<ConnectivityModule>(),
    );

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.orange.shade700,
        scrolledUnderElevation: 0,
        title: const Text(
          "Favoritos",
          style: TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.w700,
          ),
        ),
      ),

      body: FutureBuilder<List<Station>>(
        future: repositorio.getFavoriteStations(),
        builder: (_, snapshot) {

          if (snapshot.connectionState != ConnectionState.done) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return Center(
              child: Text("${snapshot.error}"),
            );
          }

          final favorites = snapshot.data ?? [];

          if (favorites.isEmpty) {
            return const Center(
              child: Text(
                "Não existem estações favoritas.",
                style: TextStyle(fontSize: 16),
              ),
            );
          }

          return ListView.builder(
            itemCount: favorites.length,
            itemBuilder: (context, index) {

              final station = favorites[index];

              return Container(
                padding: const EdgeInsets.all(6),

                child: ListTile(
                  tileColor: Colors.white,
                  contentPadding: const EdgeInsets.all(14),

                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(20),
                    side: BorderSide(
                      color: Colors.orange.shade100,
                      width: 1,
                    ),
                  ),

                  title: Text(
                    station.name,
                    style: TextStyle(
                      fontSize: 20,
                      color: Colors.grey.shade800,
                    ),
                  ),

                  subtitle: Text(
                    "Linha ${station.lineName}",
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.orange.shade700,
                    ),
                  ),

                  trailing: IconButton(
                    icon: const Icon(
                      Icons.favorite,
                      color: Colors.red,
                    ),
                    onPressed: () async {

                      await repositorio.updateFavorite(
                        station.id,
                        false,
                      );

                      setState(() {});
                    },
                  ),

                  onTap: () {
                    Navigator.of(context)
                        .push(
                      MaterialPageRoute(
                        builder: (_) => StationDetailPage(
                          stationId: station.id,
                          stationName: station.name,
                        ),
                      ),
                    )
                        .then((_) {
                      setState(() {});
                    });
                  },
                ),
              );
            },
          );
        },
      ),
    );
  }
}

列表

import 'package:cmproject/location_module.dart';
import 'package:cmproject/screens/station_detail_page.dart';
import 'package:flutter/material.dart';
import 'package:cmproject/data/metro_repository.dart';
import 'package:cmproject/models/station.dart';
import 'package:geolocator/geolocator.dart';
import 'package:provider/provider.dart';
import '../connectivity_module.dart';
import '../data/http_metro_datasource.dart';
import '../data/sqflite_metro_datasource.dart';


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

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

class _ListaPageState extends State<ListaPage> {
  // Filtros
  String _pesquisaNome = "";
  bool _crescente = true;

  @override
  Widget build (BuildContext context){
    // Repositorio
    MetroRepository repositorio = MetroRepository(local: context.read<SqfliteMetroDataSource>(), remote: context.read<HttpMetroDataSource>(), connectivityService: context.read<ConnectivityModule>());
    LocationModule locationModule = context.read<LocationModule>();

    // Aplica os filtros
    Future<List<Station>> getFuture() async {
      final stations = _pesquisaNome.isNotEmpty
          ? await repositorio.getStationsByName(_pesquisaNome)
          : await repositorio.getAllStations();

      return addDistanceAndSort(stations, _crescente);
    }








    // return
    return Scaffold(

      // Key para os testes
      key: Key("list-screen"),


      // App bar
      appBar: AppBar(
        // BackGround e centalizaçao
        backgroundColor: Colors.orange.shade700,
        scrolledUnderElevation: 0,
        centerTitle: false,

        // Titulo
        title: const Text(
          "Estações",
          style: TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.w700,
            color: Colors.white,
          ),
        ),
      ),


      // Corpo
      body: Column(
        children: [


          // Pesquisar estaçao pelo nome
          Container(
            color: Colors.orange.shade700,
            padding: const EdgeInsets.fromLTRB(9, 7, 9, 9),
            child: Row(
              children: [
                buildStationNameField(),
                buildOrdenacaoDistancia(),
              ],
            ),
          ),




          // As estaçoes pesquisadas ou globais
          Expanded(
            child: FutureBuilder(
                future: getFuture(),
                builder: (_, snapshot){
                  if(snapshot.connectionState != ConnectionState.done){
                    return Center(child: CircularProgressIndicator(),);
                  }else{
                    if (snapshot.hasError){
                      return Center(child: Text("${snapshot.error}"),);
                    }else{
                      if(snapshot.data!.isEmpty){
                        return buildSemDados();
                      }

                      return buildListStationsView(snapshot.data ?? [], locationModule, repositorio);
                    }
                  }
                }
            ),
          ),
        ],
      ),
    );
  }


  // Contruir Lista de estaçoes ou avisar que nao existem estacoes
  // Mensagem de nao encontar estacoes
  Widget buildSemDados(){
    return Center(child: Text("Não foi possível obter as estações de metro. Verifique a conectividade e volte a tentar"),);
  }
  // Contrui a lista com as estaçoes fornecidas
  Widget buildListStationsView(List<Station> stations, LocationModule locationModule, MetroRepository repositorio) {

    return ListView.builder(
      // Key para testes
      key: const Key('list-view'),



      // Lista
      itemCount: stations.length,
      itemBuilder: (context, index) {
        final station = stations[index];

        return Container(
          padding: const EdgeInsets.fromLTRB(6,6,6,6),

          child: ListTile(
            tileColor: Colors.white,
            contentPadding: const EdgeInsets.all(14),


            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20),
              side: BorderSide(color: Colors.orange.shade100, width: 1)
            ),


            title: Text(station.name, style: TextStyle(fontSize: 20, color: Colors.grey.shade800)),
            subtitle: Text("Linha ${station.lineName}", style: TextStyle(fontSize: 16, color: Colors.orange.shade700)),

            trailing: IconButton(
              icon: Icon(
                station.favorite
                    ? Icons.favorite
                    : Icons.favorite_border,
                color: station.favorite
                    ? Colors.red
                    : Colors.grey,
              ),
              onPressed: () async {
                setState(() {
                  station.favorite = !station.favorite;
                });

                await repositorio.updateFavorite(
                  station.id,
                  station.favorite,
                );
              },
            ),
            // As distancias, irao ser calculadas cada vez que o utilizador escolhe a pagina
            leading: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  Icons.place,
                  color: station.distance < 0 ? Colors.grey : Colors.orange,
                ),
                Text(
                  station.distance < 0
                      ? "N/A"
                      : station.distance < 1000
                      ? "${station.distance.toStringAsFixed(0)} m"
                      : "${(station.distance / 1000).toStringAsFixed(1)} km",
                  style: TextStyle(
                    fontSize: 12,
                    color: station.distance < 0 ? Colors.grey : Colors.black,
                  ),
                ),
              ],
            ),


            onTap: () {
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (_) => StationDetailPage(stationId: station.id, stationName: station.name,))).then((_) {
                  setState(() {});
              });
            },
          ),

        );
      },
    );
  }




  // Filtros
  Widget buildStationNameField() {
    return Expanded(
      child: TextField(
        style: const TextStyle(color: Colors.white),
        decoration: buildInputDecorationSearch(),
        onChanged: (value) {
          setState(() {
            _pesquisaNome = value;
          });
        },
      ),
    );
  }

  Row buildOrdenacaoDistancia() {
    return Row(
      children: [

        Checkbox(
          value: _crescente,
          checkColor: Colors.orange.shade700,
          activeColor: Colors.white,
          side: const BorderSide(color: Colors.white),

          onChanged: (value) {
            setState(() {
              _crescente = value ?? true;
            });
          },

        ),

        Icon(
          _crescente ? Icons.arrow_upward : Icons.arrow_downward,
          color: Colors.white,
          size: 18,
        ),

        const SizedBox(width: 4),

        Text(
          _crescente ? "Crescente" : "Decrescente",
          style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
        ),

      ],
    );
  }

















  // Decoracoes
  InputDecoration buildInputDecorationSearch() {
    return InputDecoration(

      hintText: 'Pesquisar estação...',
      hintStyle: const TextStyle(color: Colors.white),

      filled: true,
      fillColor: Colors.white.withOpacity(0.15),

      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: BorderSide.none,
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: const BorderSide(color: Colors.orange, width: 1.5),
      ),

      labelText: 'Pesquisar estação',
      prefixIcon: const Icon(Icons.search),
    );
  }







  // Calcular a distancia do utilizador para a estacao de metro X
  Future<List<Station>> addDistanceAndSort(List<Station> stations, bool crescente) async {
    late Position position;

    try {
      position = await Geolocator.getCurrentPosition().timeout(const Duration(seconds: 9));
    }
    catch (_) {
      return stations;
    }

    for (final station in stations) {
      station.distance = Geolocator.distanceBetween(
        position.latitude,
        position.longitude,
        station.latitude,
        station.longitude,
      );
    }

    stations.sort((a, b) {
      return crescente
          ? a.distance.compareTo(b.distance)
          : b.distance.compareTo(a.distance);
    });

    return stations;
  }
}

解决方案

在你在更改收藏图标后调用setState() 时,setState实际上是在内部再次调用build() 方法(也就是说setState() 告诉Flutter代码变脏,现在是重新构建的时候),因此在执行setState之后,build会被调用,而你已经调用了

future: repositorio.getFavoriteStations(),

在FutureBuilder中,一旦UI构建完成,它会再次加载整个列表,因此避免在build中调用任何API

同时也尝试使用Provider来替代setState,Provider在应用的状态管理方面更实用。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章