Utilizzare FutureBuilder in Flutter

Mattepuffo's logo
Utilizzare FutureBuilder in Flutter

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!


Condividi

Commentami!