flutterriverpodflutter-futurebuilder

Why ref.read(myProvider.notifier).getData() need to use initState become a late Future variable then could been use in future?


I am the new guy who learn flutter about 4 months, and I can't understand this:

There is two code:

Code 1.

late Future _placesFuture;

@override
void initState() {
  _placesFuture = ref.read(placesProvider.notifier).getData();
  super.initState();
}

FutureBuilder(
  future: _placesFuture,
  builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting
      ? const Center(
          child: CircularProgressIndicator(),
        )
      : places.isEmpty
          ? const Center(
              child: Text('No item here.'),
            )
          : ListView.builder(
              itemBuilder: (context, index) => PlaceItem(places[index]),
              itemCount: places.length,
            ),
)

Code 2.

FutureBuilder(
  future: ref.read(placesProvider.notifier).getData(),
  builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting
      ? const Center(
          child: CircularProgressIndicator(),
        )
      : places.isEmpty
          ? const Center(
              child: Text('No item here.'),
            )
          : ListView.builder(
              itemBuilder: (context, index) => PlaceItem(places[index]),
              itemCount: places.length,
            ),
)

They only Code 1 can work, Code 2 will always been ConnectionState.waiting.

I don't know why I must initState _placesFuture first and then use _placesFuture in future.

Why could not just put ref.read(placesProvider.notifier).getData() in the future?

Thanks πŸ™πŸ™πŸ™

I'm try to Google and ask to ChatGPT, but I still feel confused.

The whole code:

// places_provider.dart
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as path;
import 'package:sqflite/sqflite.dart' as sql;

import '../models/place.dart';

Future<sql.Database> _getDb() async {
  final dbPath = await sql.getDatabasesPath();
  final db = await sql.openDatabase(
    path.join(dbPath, 'places.db'),
    onCreate: (db, version) {
      return db.execute('CREATE TABLE places(id TEXT PRIMARY KEY, title TEXT, image TEXT, lat REAL, lng REAL, address TEXT)');
    },
    version: 1,
  );
  return db;
}

class PlacesNotifier extends StateNotifier<List<Place>> {
  PlacesNotifier() : super([]);

  Future<void> removePlace(Place place) async {
    final db = await _getDb();
    db.delete('places', where: 'id = ?', whereArgs: [place.id]);
    if (state.contains(place)) {
      state = state.where((element) => element.id != place.id).toList();
    }
  }

  Future<void> addPlace(Place place) async {
    final db = await _getDb();

    db.insert('places', {
      'id': place.id,
      'title': place.title,
      'image': place.image.path,
      'lat': place.location.lat,
      'lng': place.location.lng,
      'address': place.location.address,
    });

    state = [place, ...state];
  }

  Future<void> getData() async {
    final db = await _getDb();
    final data = await db.query('places');
    final p = data
        .map(
          (e) => Place(
            id: e['id'] as String,
            title: e['title'] as String,
            image: File(e['image'] as String),
            location: PlaceLocation(
              lat: e['lat'] as double,
              lng: e['lng'] as double,
              address: e['address'] as String,
            ),
          ),
        )
        .toList();

    state = p;
  }
}

final placesProvider = StateNotifierProvider<PlacesNotifier, List<Place>>(
  (ref) {
    return PlacesNotifier();
  },
);

// places_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers/places_provider.dart';
import '../models/place.dart';
import '../widgets/place_item.dart';
import './new_place_screen.dart';

class PlacesScreen extends ConsumerStatefulWidget {
  const PlacesScreen({super.key});

  @override
  ConsumerState<PlacesScreen> createState() => _PlacesScreenState();
}

class _PlacesScreenState extends ConsumerState<PlacesScreen> {
  late Future _placesFuture;
  @override
  void initState() {
    _placesFuture = ref.read(placesProvider.notifier).getData();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final places = ref.watch(placesProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('ζˆ‘ηš„ζ‰€ζœ‰η§˜ε―†εŸΊεœ°'),
        actions: [
          IconButton(
              onPressed: () async {
                final p = await Navigator.of(context).push<Place>(MaterialPageRoute(
                  builder: (context) => const NewPlaceScreen(),
                ));
                ref.read(placesProvider.notifier).addPlace(p!);
              },
              icon: const Icon(Icons.add))
        ],
      ),
      body: FutureBuilder(
        future: _placesFuture,
        builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting
            ? const Center(
                child: CircularProgressIndicator(),
              )
            : places.isEmpty
                ? const Center(
                    child: Text('尚ζœͺζ·»εŠ δ»»δ½•εœ°ι»žγ€‚'),
                  )
                : ListView.builder(
                    itemBuilder: (context, index) => PlaceItem(places[index]),
                    itemCount: places.length,
                  ),
      ),
    );
  }
}

or view in GitHub.


Solution

  • In general, you should always use the style like Code 1. Because if you put the request in FutureBuilder, it will make another call to the database Whenever the Widget rebuild.

    In your Code 2, because ref.watch(placesProvider); is called with the ref of PlacesScreen, PlacesScreen will rebuild whenever placesProvider updates, and calling ref.read(placesProvider.notifier).getData() in FutureBuilder will cause a delayed update placesProvider and trigger a rebuild of PlacesScreen which also rebuild FutureBuilder and call ref.read(placesProvider.notifier).getData(); again.