Utilizzare FutureBuilder in Flutter
FutureBuilder è un widget per Flutter che può essere molto comodo quando dobbiamo riempire dei widget con operazioni asincrone.
Ad esempio prendendo i dati da una API.
In questo articolo vediamo come usarlo.
L'url da cui reperiamo i dati è: https://www.mattepuffo.com/api/book/get.php.
Questo, invece, è il model:
import 'package:flutter/foundation.dart';
class Books with ChangeNotifier {
List<Book> books;
Books({
required this.books,
});
factory Books.fromJson(Map<String, dynamic> json) => Books(
books: List<Book>.from(json["books"].map((x) => Book.fromJson(x))),
);
}
class Book with ChangeNotifier {
final int? id;
final String? title;
final int? authorId;
final String? author;
final int? editorId;
final String? editor;
final double? price;
final String? isbn;
final String? note;
final int? scaffale;
final DateTime? dataAggiunta;
Book({
this.id,
this.title,
this.authorId,
this.author,
this.editorId,
this.editor,
this.price,
this.isbn,
this.note,
this.scaffale,
this.dataAggiunta,
});
factory Book.fromJson(Map<String, dynamic> json) => Book(
id: json["id"],
title: json["title"],
authorId: json["author_id"],
author: json["author"],
editorId: json["editor_id"],
editor: json["editor"],
price: json["price"]?.toDouble(),
isbn: json["isbn"],
note: json["note"],
scaffale: json["scaffale"],
dataAggiunta: DateTime.parse(json["data_aggiunta"]),
);
}
Poi creiamo un service che ha due metodi:
- uno per prendere i dati remoti
- l'altro per effettuare una ricerca sulla ListView
import 'dart:convert';
import 'package:book_flutter/utils/utils.dart';
import 'package:http/http.dart' as http;
import '../models/book.dart';
class BookService {
Future<List<Book>> getAll() async {
final url = Uri.parse('${Utils.basePathBook}get.php');
final response = await http.get(url);
final Books books = Books.fromJson(json.decode(response.body));
List<Book> items = books.books;
return items;
}
Future<List<Book>> cerca(Future<List<Book>> items, String testo) async {
List<Book> tmpList = await items;
return testo.isEmpty
? items
: Future.value(List.from(tmpList.where((el) =>
el.title!.toLowerCase().contains(testo.toLowerCase()) ||
el.author!.toLowerCase().contains(testo.toLowerCase()))));
}
}
Questa la schermata:
import 'package:book_flutter/models/book.dart';
import 'package:book_flutter/services/book_service.dart';
import 'package:book_flutter/utils/utils.dart';
import 'package:book_flutter/widgets/main_menu_widget.dart';
import 'package:flutter/material.dart';
import '../widgets/book_item_widget.dart';
class BooksScreen extends StatefulWidget {
const BooksScreen({super.key});
@override
State<BooksScreen> createState() => _BooksScreenState();
}
class _BooksScreenState extends State<BooksScreen> {
final _utils = Utils();
final _searchController = TextEditingController();
final _bookService = BookService();
late Future<List<Book>> _items;
late Future<List<Book>> _filterItems;
@override
void initState() {
super.initState();
_items = _loadItems();
_filterItems = _items;
if (_utils.isMobile()) {
_utils.checkConnetcion();
}
}
@override
void dispose() {
super.dispose();
_searchController.dispose();
}
Future<List<Book>> _loadItems() async {
return _bookService.getAll();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MP Book'),
),
drawer: const MainMenu(),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: (value) {
setState(() {
_filterItems = _bookService.cerca(_items, value);
});
},
controller: _searchController,
decoration: const InputDecoration(
labelText: "Cerca...",
hintText: "Cerca...",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
),
),
),
Expanded(
child: RefreshIndicator(
displacement: 150,
backgroundColor: Colors.black38,
strokeWidth: 3,
triggerMode: RefreshIndicatorTriggerMode.onEdge,
onRefresh: () async {
_searchController.text = '';
await Future.delayed(const Duration(milliseconds: 1500));
},
child: FutureBuilder<List<Book>>(
future: _filterItems,
builder: (context, initialData) {
if (initialData.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (initialData.hasError) {
return Center(
child: Text(
initialData.error.toString(),
),
);
}
if (initialData.data!.isEmpty) {
return const Center(
child: Text('Nessun elemento trovato!'),
);
}
return ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: initialData.data!.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (ctx, i) => BookItem(
book: initialData.data![i],
),
);
},
),
),
),
],
),
);
}
}
Come vedete nel FutureBuilder andiamo anche a controllare che i dati ci siano e che non ci siano errori.
Abbiamo anche un widget temporaneo per il loading.
BookItem è un widget che rappresenta il "record" singolo, ed questo:
import 'package:book_flutter/models/book.dart';
import 'package:flutter/material.dart';
import '../screens/book_screen.dart';
class BookItem extends StatelessWidget {
const BookItem({super.key, required this.book});
final Book book;
@override
Widget build(BuildContext context) {
void _showSnackBar() {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
"CANCELLATO!",
),
duration: const Duration(seconds: 5),
action: SnackBarAction(
label: "Annulla",
onPressed: () {
print("ANNULLATO");
},
),
),
);
}
void _del() {
_showSnackBar();
}
Widget _confDialog() {
return AlertDialog(
title: const Text('ATTENZIONE?'),
content: const Text(
'Sicuro di voler cancellare il libro?',
),
actions: <Widget>[
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
child: const Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
);
}
return Dismissible(
key: UniqueKey(),
background: Container(
color: Colors.redAccent,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 6),
child: const Icon(
Icons.delete,
color: Colors.white,
size: 40,
),
),
confirmDismiss: (direction) {
return showDialog(context: context, builder: (ctx) => _confDialog());
},
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
_del();
}
if (direction == DismissDirection.startToEnd) {
print('ALTRA AZIONE');
}
},
child: Column(
children: [
ListTile(
leading: CircleAvatar(
radius: 20,
child: Padding(
padding: const EdgeInsets.all(6),
child: FittedBox(
child: Text(
'€ ${book.price}',
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
),
title: Text(
(book.title ?? ""),
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(book.author ?? ""),
trailing: Wrap(
spacing: 10,
children: <Widget>[
IconButton(
icon: const Icon(Icons.remove_red_eye),
color: Colors.purple,
onPressed: () => {
Navigator.of(context).pushNamed(
BookScreen.routeName,
arguments: book.id,
)
},
),
IconButton(
icon: const Icon(Icons.delete),
color: Theme.of(context).colorScheme.error,
onPressed: () => {
showDialog(
context: context,
builder: (ctx) => _confDialog(),
).then(
(value) => {
if (value)
{
_del(),
},
},
),
},
),
],
),
),
const SizedBox(
height: 5,
),
],
),
);
}
}
Per completezza questo è il main.dart:
import 'dart:core';
import 'package:flutter/material.dart';
import './screens/books_screen.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'MP Book',
theme: ThemeData(
fontFamily: 'Raleway',
primarySwatch: Colors.amber,
textTheme: const TextTheme(
headlineLarge: TextStyle(fontWeight: FontWeight.bold),
bodyLarge: TextStyle(
fontSize: 14.0,
fontFamily: 'Hind',
color: Colors.black,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
),
),
initialRoute: '/',
routes: {
'/': (ctx) => const BooksScreen(),
},
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (ctx) => const BooksScreen(),
);
},
);
}
}
Ci stanno anche alcune dipendenze da soddisfare se usate esattamente questo codice:
- page_transition
- http
Enjoy!
dart flutter http futurebuilder listview
Commentami!