0

This is my code:

  class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
  }

  class _HomePageState extends State<HomePage> {
  late Future<ImageProvider> userImageFuture;
  LoggedUser loggedUser = Cache.getLoggedUser();
  
  @override
  void initState() {
    userImageFuture = Buttons.getUserImage(loggedUser.Id, context);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).backgroundColor,
      body: SingleChildScrollView(
        child: SafeArea(
          child: Column(
            children: <Widget>[
              Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.only(top: 15, left: 15),
                    child: FutureBuilder<ImageProvider>(
                      future: userImageFuture,
                      builder: (BuildContext context,
                          AsyncSnapshot<ImageProvider> snapshot) {
                        return CircleAvatar(
                          radius: 20,
                          foregroundImage: snapshot.data,
                          backgroundImage: Settings.DefaultUserImage,
                        );
                      },
                    ),
                  ),

                  ...

                ],
              ),
              
              ...

            ],
          ),
        ),
      ),
    );
  }
}

The problem is that FutureBuilder runs twice. It is supposed to fetch user profile picture from async method from another class:

class Buttons {
    static Future<ImageProvider> getUserImage(
      int userId, BuildContext context) async {
    
    ...

    return image;
  }
}

I followed this instructions. I edited my code and as far as I understand it from the docs:

The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.

I am relatively new to Flutter but I am convinced that this Future is not created at the same time as FutureBuilder.

Any possible solutions? I would be very grateful, because I've spent too much time on this. Thanks.

jsfrz
  • 99
  • 9
  • 1
    First, your super.initState() is in the wrong place. It ALWAYS goes as the first step. Second, the FutureBuilder will re-run up to 60 times per second. This is the nature of the build() method. Other answers have reported your flaws because of that, so I'll refer you to them. – Randal Schwartz Jul 09 '22 at 16:26
  • @RandalSchwartz so you are saying this is the nature of build() method. For this app I am using a web API call in my **userImageFuture** and I wouldn't like to call my API more times than it is necessary: `[07/09/2022 17:32:51] (118) Image for user '1' requsted. [07/09/2022 17:32:51] (118) Image for user '1' requsted.` It is possible to do it differently without FutureBuilder? – jsfrz Jul 09 '22 at 17:36
  • 1
    userImageFuture is set once in initState. When it is complete, it should not be triggered again. This is the nature of futures. I'm not sure where 'Image for user 1 requested' is coming from. – Randal Schwartz Jul 09 '22 at 19:42
  • @RandalSchwartz that is a log from my API. The request is called twice. – jsfrz Jul 09 '22 at 19:53
  • 1
    Then it's something in code you're not showing, or the widget above this one is forcing this widget to get rebuilt from scratch. – Randal Schwartz Jul 09 '22 at 19:55
  • 1
    put a debugPrint in the initState. If it prints twice, your problem is the widget holding this widget – Randal Schwartz Jul 09 '22 at 19:56
  • @RandalSchwartz found the bug. Thanks for your assistance. – jsfrz Jul 09 '22 at 20:31
  • ‍♂️You're most welcome. What was the nature of the bug? – Randal Schwartz Jul 09 '22 at 22:17
  • @RandalSchwartz condition if user is cached (logged in) was in LoginPage's build. If user was logged in, it returned widget (HomePage) instead of pushing into HomePage page. Basically a build method in another build method. A thing I am not exactly proud of. – jsfrz Jul 09 '22 at 22:38

1 Answers1

0

You should check for the snapshot's status in the builder method. Currently you are returning the CircleAvatar immediately, but in fact for the first run it is more than likely that the future is not completed yet.

You should follow steps like these in every case when you use FutureBuilder:

builder: (context, snapshot) {
  if (snapshot.connectionState != ConnectionState.done) {
    // means the future is not completed yet, display
    // a progress indicator for example
    return const Center(child: CircularProgressIndicator());
  }
  
  if (snapshot.hasError) {
    // there can be an error even if the future is completed,
    // display an error message or whatever you like to do
    // to handle the error
    return const Center(child: Text('Error'));
  }

  if (!snapshot.hasData) {
    // even a completed, errorless future can contain
    // no data, for example there is no matching result,
    // you can skip this check if you can manage
    // missing or null in `snapshot.data`, but generally
    // it is the recommended way
    // (if you check source code, you will see that
    // hasData is simply checking data against null value:
    // bool get hasData => data != null;)
    return const Center(child: Text('No data'));
  }
  
  // now you are good to go
  return CircleAvatar(
    radius: 20,
    foregroundImage: snapshot.data,
    backgroundImage: Settings.DefaultUserImage,
  );
}

And you don't really need:

builder: (BuildContext context, AsyncSnapshot<ImageProvider> snapshot)

You can simply use this, the types will be inferred by Dart:

builder: (context, snapshot)
Peter Koltai
  • 8,296
  • 2
  • 10
  • 20