如何在 Flutter 中将 Stream 转换为 Listenable?

How to convert a Stream to a Listenable in Flutter?

我正在尝试弄清楚如何利用 Firebase 的 onAuthStateChanges() 流在 go_router 包的 refreshListenable 参数中用作 Listenable,以便在 authState 更改时重定向. 此外,我正在使用 flutter_riverpod 进行状态管理。

到目前为止,我的代码如下所示:

我创建了一个简单的 AuthService class(缩小到最重要的部分):

abstract class BaseAuthService {
  Stream<bool> get isLoggedIn;
  Future<bool> signInWithEmailAndPassword({ required String email, required String password });
}

class AuthService implements BaseAuthService {
  final Reader _read;

  const AuthService(this._read);

  FirebaseAuth get auth => _read(firebaseAuthProvider);

  @override
  Stream<bool> get isLoggedIn => auth.authStateChanges().map((User? user) => user != null);

  @override
  Future<bool> signInWithEmailAndPassword({ required String email, required String password }) async {
    try {
      await auth.signInWithEmailAndPassword(email: email, password: password);
      return true;
    } on FirebaseAuthException catch (e) {
      ...
    } catch (e) {
      ...
    }

    return false;
  }

接下来我创建了这些提供程序:

final firebaseAuthProvider = Provider.autoDispose<FirebaseAuth>((ref) => FirebaseAuth.instance);

final authServiceProvider = Provider.autoDispose<AuthService>((ref) => AuthService(ref.read));

如前所述,我想以某种方式监听这些 authChanges 并将它们传递给路由器:

final router = GoRouter(
    refreshListenable: ???
    redirect: (GoRouterState state) {
        bool isLoggedIn = ???
        
        if (!isLoggedIn && !onAuthRoute) redirect to /signin;
    }
)

我真的不知道如何使用 riverpod 执行此操作,但我认为使用 riverpod 不需要上下文。使用 Provider 我会做这样的事情:

  // Somewhere in main.dart I register my dependencies with Provider:

      Provider(
        create: (context) =>  AuthService(//pass whatever),
     // ...

  // Somewhere in my *stateful* App Widget' State:
  // ...
  late ValueListenable<bool> isLoggedInListenable;

  @override
  void initState(){
    // locate my authService Instance
    final authService = context.read<AuthService>();
    // as with anything that uses a stream, you need some kind of initial value
    // "convert" the stream to a value listenable
    isLoggedInListenable = authService.isLoggedIn.toValueListenable(false);
    super.initState();
  }

  @override
  Widget build(BuildContext context){
    final router = GoRouter(
      refreshListenable: isLoggedInListenable,
      redirect: (GoRouterState state) {
        bool isLoggedIn = isLoggedInListenable.value;
      
        if (!isLoggedIn && !onAuthRoute) //redirect to /signin;
        // ...
      }
    );
    return MaterialApp.router(
      // ...
    );
  }

这是将流“转换”为 ValueListenable 的扩展

extension StreamExtensions<T> on Stream<T> {
  ValueListenable<T> toValueNotifier(
    T initialValue, {
    bool Function(T previous, T current)? notifyWhen,
  }) {
    final notifier = ValueNotifier<T>(initialValue);
    listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }

  // Edit: added nullable version
  ValueListenable<T?> toNullableValueNotifier{
    bool Function(T? previous, T? current)? notifyWhen,
  }) {
    final notifier = ValueNotifier<T?>(null);
    listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }

  Listenable toListenable() {
    final notifier = ChangeNotifier();
    listen((_) {
      // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
      notifier.notifyListeners();
    });
    return notifier;
  }
}

之所以有效,是因为 ValueListenable 是一个 Listenable!与 ChangeNotifier 相同(它也只是保存数据)。

在您的情况下,如果您可以在声明路由器之前获取您的 authService 实例,则可以将流转换为可侦听的,然后使用它。确保它是小部件状态的一部分,否则您可能会收集通知程序垃圾。另外,我添加了一个 notifyWhen 方法,以防您想按条件过滤通知。在这种情况下不需要,ValueNotifier 只会在值实际更改时通知。

再补充一点,对于使用 flutter_bloc 的人来说,此扩展也适用:

extension BlocExtensions<T> on BlocBase<T> {
  Listenable asListenable() {
    final notifier = ChangeNotifier();
    stream.listen((_) {
      // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
      notifier.notifyListeners();
    });
    return notifier;
  }

  ValueListenable<T> asValueListenable({
    BlocBuilderCondition? notifyWhen,
  }) {
    final notifier = ValueNotifier<T>(state);
    stream.listen((value) {
      if (notifyWhen == null || notifyWhen(notifier.value, value)) {
        notifier.value = value;
      }
    });
    return notifier;
  }
}

根据go_router文档,您可以简单地使用以下方法: https://gorouter.dev/redirection#refreshing-with-a-stream

GoRouterRefreshStream(_fooBloc.stream)