Bloc、Flutter 和导航

Bloc, Flutter and Navigation

所以和大多数人一样,我是 Bloc 的新手,我会颤抖、飞镖和绕过我的头。我用谷歌搜索,浏览了这里的帖子,但没有找到真正的答案。

所以这是关于使用 bloc 和 flutter 进行导航的。以登录为例。所以有一个登录页面后面有一个集团,在某个时候有人按下按钮登录。

所以我们可以在执行验证的 bloc 中调用一个函数。我认为这违反了严格的方法,但我看到有人这样做。但是,如果登录成功,您如何导航到下一个屏幕?你不应该在一个集团中导航?

但是,如果该登录页面使用 StreamBuilder 来更改状态,那么您也不能在构建器中添加导航,可以吗?你不能 return 导航,你 return 小部件。

initstate 是您可以导航的地方,但是您可以在 initstate 中有一个流生成器来侦听 bloc 中的状态变化吗?

现在有点混乱,但我坚持不懈,因为这是应该前进的方向...

谢谢 保罗

为了让 BLoC 成为 前进的道路的神话:没有完美的处理状态的方法。 每个状态管理体系结构都比其他问题更好地解决了一些问题;总有 trade-offs,在决定架构时了解它们很重要。

一般来说,好的架构是实用的:它是可缩放和可扩展的,同时只需要很少的开销。 由于人们对实用性的看法各不相同,架构总是涉及意见,所以请对以下内容持保留态度,因为我将阐述我对如何为您的应用采用 BLoC 的个人看法。

BLoC 是 Fl​​utter 中一种非常有前途的状态管理方法,因为有一个标志性成分:流。 它们允许将 UI 从业务逻辑中分离出来,并且它们与 Flutter-ish 方法配合得很好,一旦它们过时就重建整个小部件子树。 所以很自然地,与 BLoC 之间的每次通信都应该使用流,对吗?

+----+  Stream   +------+
| UI | --------> | BLoC |
|    | <-------- |      |
+----+   Stream  +------+

嗯,有点。

要记住的重要一点是,状态管理架构是达到目的的一种手段;您不应该只是为了做事而做事,而应该保持开放的心态并仔细评估每个选项的优缺点。 我们将 BLoC 与 UI 分开的原因是 BLoC 不需要关心 UI 的结构——它只是提供一些简单的流,而数据发生的任何事情都是 UI的责任。

但是,虽然流已被证明是将信息从 BLoC 传输到 UI 的绝佳方式,但它们在另一个方向上增加了不必要的开销: Streams 旨在传输连续的数据流(它甚至在名称中也是如此),但大多数时候,UI 只需要触发 BLoC 中的单个事件。这就是为什么有时您会看到一些 Stream<void>s 或类似的 hacky 解决方案¹,只是为了严格遵守 BLoC-y 做事方式。

此外,如果我们基于来自 BLoC 的流推送新路由,BLoC 基本上会控制 UI 流——但拥有直接控制 UI 和业务的代码逻辑正是我们试图阻止的东西!

这就是为什么一些开发人员(包括我)完全放弃 stream-based 解决方案并采用自定义方式从 UI 触发 BLoC 中的事件。 就个人而言,我只是使用方法调用(通常是 return Futures)来触发 BLoC 的事件:

+----+   method calls    +------+
| UI | ----------------> | BLoC |
|    | <---------------- |      |
+----+   Stream, Future  +------+

在这里,BLoC returns Streams 的数据是 "live" 和 Futures 作为方法调用的答案。

让我们看看您的示例如何解决:

  • BLoC 可以提供 Stream<bool> 用户是否登录,甚至 Stream<Account>,其中 Account 包含用户的帐户信息。
  • BLoC 还可以提供异步 Future<void> signIn(String username, String password) 方法,如果登录成功,return 什么都没有,否则会抛出错误。
  • UI 可以自行处理输入管理,并在按下登录按钮后触发如下内容:
try {
  setState(() => _isLoading = true); // This could display a loading spinner of sorts.
  await Bloc.of(context).signIn(_usernameController.text, _passwordController.text);
  Navigator.of(context).pushReplacement(...); // Push logged in screen.
} catch (e) {
  setState(() => _isLoading = false);
  // TODO: Display the error on the screen.
}

这样,您就可以很好地分离关注点:

  • BLoC 实际上只是做了它应该做的事情——处理业务逻辑(在本例中,是让用户登录)。
  • UI 只关心两件事:
    • 显示来自 Stream
    • 的用户数据
    • 通过在 BLoC 中触发用户操作并根据结果执行 UI 操作来对用户操作做出反应。²

最后,我想指出的是,这只是一种可能的解决方案,它是通过尝试在复杂应用程序中处理状态的不同方式来随着时间的推移而演变的。 了解关于状态管理如何工作的不同观点很重要,因此我鼓励您更深入地研究该主题,也许可以通过观看来自 Google [=87 的 "Pragmatic State Management in Flutter" session =].

编辑:刚刚在 Brian Egan's architecture samples 中找到了这个架构,它被称为 "Simple BLoC"。如果你想了解不同的架构,我真的建议你看看 repo。


¹ 尝试向 BLoC 操作提供多个参数时,它变得更加丑陋 – 因为那样你需要定义一个包装器 class 只是为了将其传递给 Stream。

² 我 do 承认它在启动应用程序时变得有点难看:你需要某种启动画面来检查 BLoC 的流并将用户重定向到根据他们是否登录的适当屏幕。出现该规则的例外是因为用户执行了一个操作——启动应用程序——但 Flutter 框架不允许我们直接挂钩到那个(至少据我所知,至少不是很优雅)。

BlocListener 是您可能需要的小部件。如果状态更改为(例如)LoginSuccess,则块侦听器可以调用通常的 Navigate.of(context)。你可以找到一个 example of BlocListener in action near the bottom of this page.

另一种选择是将回调传递给事件。

 BlocProvider.of<MyBloc>(context).add(MyEvent(
              data: data,
              onSuccess: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) {
                    return HomePage();
                  }),
                );
              }));

首先:如果没有业务逻辑,则无需访问 YourBloc class。

但有时一些用户的 activity 需要在 Bloc class 中执行一些逻辑,然后 Bloc class 必须决定下一步做什么:只需重建小部件显示对话框 甚至导航到下一条路线。在这种情况下,您必须发送一些 State 到 UI 才能完成操作。 那么又出现了一个问题:Bloc发送State显示toast时widgets怎么办?

这是整个故事的主要问题。

很多答案和文章推荐使用flutter_block. This library has BlocBuilder and BlocListener。使用这些 classes,您可以解决一些问题,但不能解决 100% 的问题。

在我的例子中,我使用了 BlocConsumer 来管理 BlocBuilderBlocListener 并提供了管理状态的绝妙方法。

来自文档:

BlocConsumer<BlocA, BlocAState>(
  listenWhen: (previous, current) {
    // return true/false to determine whether or not
    // to invoke listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  buildWhen: (previous, current) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

正如您在 BlocConsumer 中看到的那样,您可以过滤状态:您可以轻松定义状态以重建小部件和状态以显示一些弹出窗口或导航到下一个屏幕。

下面Github中felangel提到的issue,我们可以使用BlocListner来达到这个目的

BlocListener(
    bloc: BlocProvider.of<DataBloc>(context),
    listener: (BuildContext context, DataState state) {
        if (state is Success) {              
            Navigator.of(context).pushNamed('/details');
        }              
    },
    child: BlocBuilder(
        bloc: BlocProvider.of<DataBloc>(context),
        builder: (BuildContext context, DataState state) {        
            if (state is Initial) {
                return Text('Press the Button');
            }
            if (state is Loading) {
                return CircularProgressIndicator();
            }  
            if (state is Success) {
                return Text('Success');
            }  
            if (state is Failure) {
                return Text('Failure');
            }
        },
    }
)