Sign In Flow

How to relay the state of an asynchronous action in the UI while it occurs via "mutations."

/// Provides a [Random].
// NOTE: encapsulating this enables easy implementation switching later on.
final Capsule<Random> randomCapsule = capsule((use) => Random());

/// Provides a function that signs the user in.
final Capsule<Future<void> Function(String, String)> signInAction = capsule((use) {
  // NOTE: we have this `use` call up top (before it is actually needed)
  // because you must never use a CapsuleHandle across an asynchronous gap
  // (which includes in a function callback (below) or after an await).
  // But don't worry, ReArch will give you a reminder if you ever forget.
  final random = use(randomCapsule);

  return (email, password) {
    // Dummy implementation that is fallible
    return Future.delayed(const Duration(seconds: 1), () {
      if (random.nextBool()) throw Exception('Sign in failed');
    });
  };
});

class SignInForm extends RearchConsumer {
  const SignInForm({super.key});

  Widget build(BuildContext context, WidgetHandle use) {
    final (email, setEmail) = use.state('');
    final (password, setPassword) = use.state('');

    final (state: signInState, mutate: mutateSignIn, clear: _) =
        use.mutation<void>();
    final rawSignIn = use(signInAction);
    void signIn() => mutateSignIn(rawSignIn(email, password));

    return Column(
      children: [
        TextField(onChanged: setEmail),
        TextField(onChanged: setPassword),
        ElevatedButton.icon(
          icon: const Icon(Icons.person_rounded),
          onPressed: signInState is AsyncLoading ? null : signIn,
          label: const Text('Sign In'),
        ),
        switch (signInState) {
          null => const SizedBox.shrink(), // no sign-in attempted yet
          AsyncData<void>() => const Text('Signed In'),
          AsyncLoading<void>() => const CircularProgressIndicator(),
          AsyncError<void>(:final error) => Text('$error'),
        },
      ],
    );
  }
}