Flutter FutureProvider Execute build twice

Viewed 501

I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?

class ReportsPage extends StatelessWidget {
  const ReportsPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureProvider<List<ReportModel>>(
      create: (_) async => ReportsProvider().loadReportData(1),
      initialData: null,
      catchError: (_, __) => null,
      child: const ReportWidg()
    );
  }
}

class ReportWidg extends StatelessWidget {
  const ReportWidg();

  @override
  Widget build(BuildContext context) {
    print("Execute Build");
    final reportList = Provider.of<List<ReportModel>>(context);
    if (reportList == null) {
      return Center(child: CircularProgressIndicator());
    } else if (reportList.isEmpty) {
      return Center(child: Text("Det finns inga rapporter."));
    }

    print(reportList.length);
    return Container();
  }
}
4 Answers

Im relativly new to flutter but I think its because StatelessWidget is @immutable, which means whenever something changes it needs to rebuild itself.

At first build there is async calling made and ReportWidg() is rendered.

Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".

In object-oriented and functional programming, an immutable object (unchangeable object) is an object whose state cannot be modified after it is created. ... This is in contrast to a mutable object (changeable object), which can be modified after it is created

or am I wrong ?

I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.

In your case you should use Consumer, i.e.

FutureProvider<List<ReportModel>(
  create: (_) => ...,
  child: Consumer<List<ReportModel>(
    builder: (_, reportList, __) {
      return reportList == null ?
        CircularProgressIndicator() :
        ReportWidg(reportList);
    }
  ),
),

But in this case you must to refactor your ReportWidg.

You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data

If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()

In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet

Widget build(BuildContext context) {
    print("Execute Build");
    final reportList = Provider.of<List<ReportModel>>(context);
    print("reportList ${reportList.toString()}");
    if (reportList == null) {
      print("reportList is null");
      return Center(child: CircularProgressIndicator());
    } else if (reportList.isEmpty) {
      return Center(child: Text("Empty"));
    }

    return Scaffold(
      body: ListView.builder(
          itemCount: reportList.length,

working demo

enter image description here

full code

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

class ReportModel {
  String title;

  ReportModel({this.title});
}

class ReportsProvider with ChangeNotifier {
  Future<List<ReportModel>> loadReportData(int no) async {
    await Future.delayed(Duration(seconds: 5), () {});
    return Future.value([
      ReportModel(title: "1"),
      ReportModel(title: "2"),
      ReportModel(title: "3")
    ]);
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ReportsPage(),
    );
  }
}

class ReportsPage extends StatelessWidget {
  const ReportsPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureProvider<List<ReportModel>>(
        create: (_) async => ReportsProvider().loadReportData(1),
        initialData: null,
        catchError: (_, __) => null,
        child: const ReportWidg());
  }
}

class ReportWidg extends StatelessWidget {
  const ReportWidg();

  @override
  Widget build(BuildContext context) {
    print("Execute Build");
    final reportList = Provider.of<List<ReportModel>>(context);
    print("reportList ${reportList.toString()}");
    if (reportList == null) {
      print("reportList is null");
      return Center(child: CircularProgressIndicator());
    } else if (reportList.isEmpty) {
      return Center(child: Text("Empty"));
    }

    return Scaffold(
      body: ListView.builder(
          itemCount: reportList.length,
          itemBuilder: (context, index) {
            return Card(
                elevation: 6.0,
                child: Padding(
                  padding: const EdgeInsets.only(
                      top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
                  child: Text(reportList[index].title.toString()),
                ));
          }),
    );
  }
}
Related