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

8
我正在尝试弄清楚如何利用Firebase的onAuthStateChanges()流作为refreshListenable参数中的可监听对象,以便在身份验证状态更改时重定向,go_router包提供了这个功能。此外,我使用flutter_riverpod进行状态管理。
到目前为止,我的代码看起来像这样:
我创建了一个简单的AuthService类(缩小到最重要的部分):
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;
    }
)

创建一个继承ChangeNotifier的类,并在您的Stream.authState更改时调用notifyListeners - 使用Stream.listen来监听该流中的任何新事件。 - pskink
@pskink,但是由于我正在使用Riverpod的ChangeNotifierProvider,所以我无法在GoRouter构造函数内读取此Notifier。 - Florian Leeser
但是你有一个“Stream”,不是吗?如果是这样,你可以监听该流。 - pskink
@pskink 是的,我能够听取该流并在authState更改时通知听众,但正如我所说,不知何故我必须将ChangeNotifier提供给GoRouter构造函数,在那里我没有上下文或引用可以参考和调用类似于context.read()的东西。 - Florian Leeser
@FlorianLeeser 你好) 你找到解决方案了吗?请在这里提供它,谢谢! - Constantine
很抱歉,@Konstantin,我没有。似乎没有办法完成这个任务。 - Florian Leeser
2个回答

14

根据 go_router 的文档,您可以使用以下方法:

https://gorouter.dev/redirection#refreshing-with-a-stream
GoRouterRefreshStream(_fooBloc.stream)

更新:2022年9月20日

此类已从go_router 5.0中删除,并且在go_router中没有提供替代类。原因是此类与路由无关,不应包含在go_router中。要迁移使用此类的现有应用程序,可以将该类复制到其代码中并直接拥有实现。

import 'dart:async';

import 'package:flutter/foundation.dart';

class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Stream<dynamic> stream) {
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen(
          (dynamic _) => notifyListeners(),
        );
  }

  late final StreamSubscription<dynamic> _subscription;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

这是最好的选择! - Florian Leeser
有没有使用Firebase身份验证的完整示例? - Johannes Pertl

3

我不太清楚如何使用Riverpod来做到这一点,但是我认为您不需要使用context就可以使用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的实例,您可以将流(stream)转换为可监听对象(listenable)并使用它。确保它是小部件的状态的一部分,否则您可能会得到通知器被垃圾回收的错误。此外,我添加了一个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;
  }
}

这个工作得非常好!!!我非常感激! 还有一个问题:是否有可能将null作为initialValue传递给转换器?因为在开始时,Auth-Stream不会产生任何内容,直到它知道用户是否已登录,对吧?如果是这样的话,我想在加载完成之前显示一种“闪屏”效果。否则,它会在几毫秒内显示LoginScreen。 - Florian Leeser
问题显然出现在我在Web上输入路径时。例如,当调用/home时,一切都会重新加载并显示SignInScreen片刻,因为Listenable在开始时产生了false。 - Florian Leeser
1
@Florian Lesser 刚刚编辑了它并添加了一个可空版本,因此您可以安全地调用它而不需要任何初始值,并且它应该最初为 null。 - nosmirck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接