skip to Main Content

I’m starting to get pretty frustrated with Dart’s async

All I want is lazily load some value and assign it to a property in my class:

Future<String> getMeSomeBar() async => "bar";
class Foo {
  late String bar = await getMeSomeBar();
}

this doesn’t work, of course.

But what really drives me mad is that I know Kotlin has a runBlocking function that is designed for cases exactly like that and there just doesn’t seem to be a Dart equivalent or pattern that would allow me to do the same.

At least not one that I’ve found, so far.

I can’t even do a basic spin-lock in Dart because there’s no way to check for the state of a Future

So because getMeSomeBar is outside of my control, I need to makeFoo.bar a Future<String>.

Which means I have to make whatever function accesses Foo.bar an async function.

Which means I have to make whatever functions accesses that function, an async function.

Which means I have to make whatever functions accesses those function async too.

Which means …

You get the point. In the end, the whole bloody app is going to be asynchronous simply because I seem to have absolutely no way to block a thread/isolate until a Future is completed.

That can’t be the solution?!

Is there a way to avoid this?

(No, using then doesn’t help because then you’re either waiting for the completion of that Future – which again, you seem to only be able to do in an async function – or you’re gambling on your Futures’ returning "in time" – which is just plain stupid.)

2

Answers


  1. Not sure if this is a scalable solution for you, but you could delay building your Foo class until the completion of a Future<T>, effectively creating an asynchronous factory of Future<Foo>.

    You could then (1) use that Future<Foo> in a FutureBuilder widget that listens for Future<Foo> to resolve before building your real layout, or (2) you could use your own Completer<Foo> in a stateful widget to update the state when done.

    Also: if you have to await several Futures to build Foo, you can use Future.all(...) in your Foo.fromAsyncCall Future-factory.

    Here’s an example:

    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          debugShowCheckedModeBanner: false,
          // home: Example1(),
          home: Example2(),
        );
      }
    }
    
    // a standin for some object
    Future<String> getMeSomeBar() async {
      // just an arbitrary delay
      await Future.delayed(const Duration(seconds: 3));
      return 'bar';
    }
    
    // a standin for some class
    class Foo1 {
      final String bar;
    
      const Foo1(this.bar);
    
      // You can create a function that will build your object upon the completion
      //  of a future
      static Future<Foo1> fromAsyncCall([dynamic someArgs]) => Future(() async {
            // you can access someArgs in here, if you need to
            String bar = await getMeSomeBar();
            return Foo1(bar);
          });
    }
    
    // EX.1 - You could try to use a FutureBuilder listening for the completion of
    //  the future
    class Example1 extends StatefulWidget {
      const Example1({super.key});
    
      @override
      State<Example1> createState() => _Example1State();
    }
    
    class _Example1State extends State<Example1> {
      late Future<Foo1> foo1;
    
      @override
      void initState() {
        super.initState();
        foo1 = Foo1.fromAsyncCall();
      }
    
      @override
      void dispose() {
        foo1.ignore();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) => Scaffold(
            body: Center(
              child: FutureBuilder(
                future: foo1,
                builder: (BuildContext context, AsyncSnapshot<Foo1> snapshot) {
                  if (snapshot.hasData) {
                    return Text(
                      snapshot.data!.bar,
                      style: TextStyle(
                        color: Colors.green,
                      ),
                    );
                  }
                  if (snapshot.hasError) {
                    return Text(
                      snapshot.error?.toString() ?? 'Error',
                      style: TextStyle(
                        color: Colors.red,
                      ),
                    );
                  }
                  return CircularProgressIndicator.adaptive();
                },
              ),
            ),
          );
    }
    
    // EX.2 - You could do it yourself by listening for a Completer. Unfortunately,
    //  the future value that the completer wraps is a Future<T> so you can't
    //  directly access the future's inner value/error here - you'd have the same
    //  problem
    class Example2 extends StatefulWidget {
      const Example2({super.key});
    
      @override
      State<Example2> createState() => _Example2State();
    }
    
    class _Example2State extends State<Example2> {
      final Completer<void> _completer = Completer();
      Foo1? _value;
      Object? _error;
    
      @override
      void initState() {
        super.initState();
        Foo1.fromAsyncCall().then((Foo1 value) {
          if (_completer.isCompleted || !mounted) return;
          setState(() {
            _completer.complete();
            _value = value;
          });
        }).catchError((Object error, StackTrace _) {
          if (_completer.isCompleted || !mounted) return;
          setState(() {
            _completer.completeError(error);
            _error = error;
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        Widget? child;
        if (_completer.isCompleted) {
          if (_value != null) {
            child = Text(
              _value!.bar,
              style: TextStyle(
                color: Colors.green,
              ),
            );
          }
          if (_error != null) {
            child = Text(
              _error.toString(),
              style: TextStyle(
                color: Colors.red,
              ),
            );
          }
        }
        child ??= CircularProgressIndicator.adaptive();
    
        return Scaffold(
          body: Center(
            child: child,
          ),
        );
      }
    }
    
    Login or Signup to reply.
  2. Alternatively, you could try to alter your class in such a way where it can complete itself in some form of late-valued fields. I wouldn’t recommend that because if you try to access bar before it has completed it could throw an error.

    class Foo2 {
      final Completer<void> _completer = Completer();
      List<void Function(String)>? _callbacks;
    
      bool get isCompleted => _completer.isCompleted;
    
      late final String bar;
    
      Foo2({
        required FutureOr<String> bar,
        Iterable<void Function(String)>? onCompletedCallbacks,
      }) {
        _callbacks = onCompletedCallbacks?.toList();
        _completer.future.then((value) {
          if (_callbacks == null) return;
          for (void Function(String) callback in _callbacks!) {
            callback(this.bar);
          }
          _callbacks = null; // forget the callbacks now: we are done
        });
        if (bar is Future<String>) {
          bar.then((value) {
            this.bar = value;
            _completer.complete();
          });
          return;
        }
        this.bar = bar;
        _completer.complete();
      }
    
      void addListener(void Function(String) callback) {
        if (isCompleted) {
          callback(bar);
          return;
        }
        _callbacks ??= [];
        _callbacks!.add(callback);
      }
    
      void removeListener(void Function(String) callback) {
        _callbacks?.remove(callback);
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search