当我只是想要图标时,Flutter的 FutureBuilder会重新构建整个页面
我正在开发一个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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。