Blocking wait on future OUTSIDE of async functions

50 views Asked by At

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

There are 2 answers

0
Joeseph Schmoe On

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,
      ),
    );
  }
}
0
Joeseph Schmoe On

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);
  }
}