FutureBuilder gives an unexpected result and slowing UI down

Viewed 295

It is a first statefull widget

bool _isPressed = false;
...

ElevatedButton(
  child: const Text('Run long calculations'),
  onPressed: () {
    setState(() {
      _isPressed = !_isPressed;
    });
  },
),
_isPressed ? const Result() : Container(),
...

and Result widget with its builds function returns

FutureBuilder<String>(
  future: _process(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: LinearProgressIndicator(),
      );
    } else {
      if (snapshot.error != null) {
        return const Center(
          child: Text('An error occurred'),
        );
      } else {
        return Text('${snapshot.data}');
      }
    }
  },
);

Future<String> _process() async {
    await argon2.hashPasswordString('dummy text', salt: Salt.newSalt()); // long calculations
    return 'dummy result';
}

Why the FutureBuilder does not render LinearProgressIndicator before it render final text? Actualy, the LinearProgressIndicator is rendered for a very small amount of time before final text rendered, but there is something wrong with it, because the circular indicator should spin much longer.

_process() seems to slow down the application and that's why the progress indicator does not spin. But how can it be if the result of the computation is Future and the code awaits for it...

7 Answers

I think its better to change your conditions like below . based on flutter Doc

          if (snapshot.hasData) {
             // data
             return Text('${snapshot.data}');
          } else if (snapshot.hasError) {
            // error
          } else {
            // CircularProgressIndicator
             return SizedBox(
                child: CircularProgressIndicator(),
                width: 60,
                height: 60,
              );
          }

If this answer does not help you and you think have a UI freeze because of heavy task in _process() method you should do the process task in separate Isolate.

The problem is with this line:

 if (snapshot.connectionState == ConnectionState.waiting)

You see, ConnectionState.waiting is used when there is no connection yet, for example when a stream has no value.

Here is what each connection state is:

  • Active
    • after an asyncronous computation started, but before it ends.
  • None
    • When there is no asyncronous computation at all (for example, the future is None on a future builder)
  • Done
    • After the asyncronous computation has ended
  • Waiting
    • Before the asynchronous computation begins

So when you check if the connection state is waiting, the value is true for a split second and then the connection state switches to active, here is what your if statement should look like:

if (snapshot.connectionState == ConnectionState.active)

MohandeRr has suggested the impmentation flutter docs has used, but i usually do it like this

if (snapshot.connectionState != ConnectionState.done) {
  return const Center(
    child: LinearProgressIndicator(),
  );
}

if (snapshot.hasError) {
  return const Center(
    child: Text('An error occurred'),
  );
}

return Text('${snapshot.data}');
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  bool _isPressed = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Demo"),
        actions: [
          TextButton(
            child: const Text(
              'Press',
              style: TextStyle(
                color: Colors.white,
              ),
            ),
            onPressed: () {
              setState(() {
                _isPressed = !_isPressed;
              });
            },
          )
        ],
      ),
      body: _isPressed
          ? FutureBuilder<String>(
              future: process(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Center(
                    child: Text(snapshot.data ?? ""),
                  );
                } else if (snapshot.hasError) {
                  return const Center(
                    child: Text('An error occurred'),
                  );
                } else {
                  return const Center(
                    child: LinearProgressIndicator(),
                  );
                }
              },
            )
          : const Center(
              child: Text("Hidden"),
            ),
    );
  }

  Future<String> process() async {
    await Future.delayed(const Duration(seconds: 3));
    return "Hello World";
  }
}

There are 2 problems at play here:

  1. You are creating a new Future (_process()) for every build loop. You need to put that in your state and reuse/clear it appropriately.

  2. You are using the wrong ConnectionState check - snapshot.connectionState != ConnectionState.done is probably what you need

Your code is fine, if you replace the _getHash body with just a Future.delayed() the progress indicator shows fine. Hence the problem is in hashPasswordString. If you look at the implementation of this function you'll notice that in fact it is synchronous.

So the quick fix would be - create a static function like that:

static String _calculateHash(String input) {
    final result = argon2.hashPasswordStringSync(input,
        salt: Salt.newSalt(), iterations: 256, parallelism: 8);
    return result.hexString;
  }

and use it with the compute function:

  Future<String> _hash() {
    return compute(_calculateHash, 'input text');
    // this is not needed anymore
    // DArgon2Result result = await argon2.hashPasswordString('input text',
    //     salt: Salt.newSalt(), iterations: 256, parallelism: 8);
    // return result.hexString;
  }

  static String _calculateHash(String input) {
    final result = argon2.hashPasswordStringSync(input,
        salt: Salt.newSalt(), iterations: 256, parallelism: 8);
    return result.hexString;
  }

The long and proper fix - create a PR for the dargon2_flutter package.

Related