[译] 在 flutter 中高效地使用 BLoC 模式

朋友们,我有好长一段时间没有写过 flutter 相关的文章了。在完成了两篇关于 BLoC 模式的文章以后,我花了一些时间,分析了社区对于这种模式的使用状况,在回答了一些关于 BLoC 模式实现的一些问题以后,我发现你们对于 BLoC 模式存在不少疑惑。因此,我构思了一套方法,你们按照这一套方法来作,就能够正确地实现 BLoC 模式了,这会帮助开发人员在实现的时候避免犯下一些常见的错误。因此,我今天向你们介绍一下在使用 BLoC 模式时必需要遵循的 8 个黄金点html

前提

我心目中的读者,应该知道 BLoC 模式是什么,或者使用模式建立了一个应用(至少作过 CTRL + CCTRL + V)。若是你是第一次听到 BLoC 这个词,那么下面三篇文章能够很好地帮助你理解这个模式。前端

  1. 使用 BLoC 模式构建 Flutter 项目第一部分第二部分android

  2. 当 Firebase 遇到了 BLoC 模式ios

和 BLoC 相遇的故事

我知道,BLoC 模式是一个很难去理解和实现的模式。我看过了不少开发人员的帖子,询问 哪里是学习 BLoC 模式的最佳资源呢?读完了不一样的帖子和评论以后,我以为你们在理解这个问题的阻碍有如下几点。git

  1. 响应式地思考。github

  2. 努力了解须要建立多少 BLoC 文件。编程

  3. 惧怕这个模式会形成代码复杂度的提高。后端

  4. 不知道 stream 在何时会被处理掉。安全

  5. 什么是 BLoC 模式的完整形式?(这是一个业务逻辑组件)网络

  6. 更多其余的缘由……

可是今天我要列出一些最为重要的点,这些点能够帮助你更加自信及有效地实现 BLoC 模式。如今,就让咱们赶快看看有哪些很棒的点。

每个页面都有其本身的 BLoC

这是须要记住的最重要的一个点。每当你建立了一个新的页面,例如登陆页,注册页,我的资料页等涉及到数据处理的页面的时候,你必需要为其 建立一个新的 BLoC。不要将全局 BLoC 用于处理应用中的全部页面。你可能会认为,若是咱们有一个全局的 BLoC,就能够轻松地处理跨页面的数据了。这很很差,由于你的库应当将这些公共数据提供给 BLoC。BLoC 仅仅是获取数据而且将其注入到页面中,来向用户展现。

左图是正确的使用模式

每一个 BLoC 必需要有一个 dispose() 方法

这一点比较直接。你建立的每一个 BLoC 都应该有一个 dispose() 方法。这个方法是你清理或者关闭你建立的全部 stream 的位置。下面是一个 dispose() 的简单的例子。

class MoviesBloc {
  final _repository = Repository();
  final _moviesFetcher = PublishSubject<ItemModel>();

  Observable<ItemModel> get allMovies => _moviesFetcher.stream;

  fetchAllMovies() async {
    ItemModel itemModel = await _repository.fetchAllMovies();
    _moviesFetcher.sink.add(itemModel);
  }

  dispose() {
    _moviesFetcher.close();
  }
}
复制代码

不要在 BLoC 中使用 StatelessWidget

每当你想要建立一个传递数据到 BLoC 或者从 BLoC 中获取数据的页面的时候,请使用 StatefulWidget 。使用 StatefulWidget 相比于使用 StatelessWidget 的最大优势在于 StatefulWidget 中的生命周期方法。在文章的后面,咱们会讨论在使用 BLoC 模式时须要覆盖的两个最重要的方法。StatelessWidget 很适合制做页面的小的静态部分,例如显示图像或者是硬编码的文本。若是你想要看看怎么用 StatelessWidget 来实现 BLoC 模式,请看上面推荐的文章的 第一部分,而在第二部分中,我讲述了本身为何要从 StatelessWidget 迁移到 StatefulWidget

重写 didChangeDependencies() 来初始化 BLoC

若是你须要在初始化的时候须要一个 context 来初始化 BLoC 对象,那么这个方法就是在 StatefulWidget 中须要重写的最重要的方法。你能够将其视为初始化方法(最好仅用于 BLoC 的初始化)。你或许会说,咱们有 initState() 方法,那么为何咱们要使用 didChangeDependencies() 方法。文档里面清楚地提到,从 didChangeDependencies() 调用 BuildContext.inheritFromWidgetOfExactType 是安全的。下面是使用这个方法的一个简单的例子:

@override
  void didChangeDependencies() {
    bloc = MovieDetailBlocProvider.of(context);
    bloc.fetchTrailersById(movieId);
    super.didChangeDependencies();
  }
复制代码

重写 dispose() 方法来销毁 BLoC

就和有一个初始化方法同样,咱们还有一个方法,来处理掉咱们在 BLoC 中建立的链接。dispose() 方法是调用与该页面相连的对应的 BLoC 的 dispose() 方法的最佳位置。每当你离开页面的时候,须要调用这个方法(实际上就是StatefulWidget被处理掉的时候)。如下是该方法的一个小例子:

@override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }
复制代码

只有须要处理复杂逻辑的时候,才使用 RxDart

若是你以前使用过 BLoC 模式的话,那么你必定据说过 [RxDart](https://github.com/ReactiveX/rxdart) 库。这个库是 Google Dart 的响应式函数式编程库,它只是一个包装器,用来包装 Dart 提供的 Stream API。我建议你仅在须要处理,相似于连接多个网络请求这样的复杂逻辑时,才使用这个库。对于一些简单的实现,使用 Dart 语言提供的 Stream API 就足够了,由于这个 API 已经很是成熟了。下面我添加了一个 BLoC,它使用了 Stream API 而不是 RxDart 库,这样会让操做变得很是简单,咱们不须要额外的库来实现一样的事情:

import 'dart:async';

class Bloc {

  //Our pizza house
  final order = StreamController<String>();

  //Our order office
  Stream<String> get orderOffice => order.stream.transform(validateOrder);

  //Pizza house menu and quantity
  static final _pizzaList = {
    "Sushi": 2,
    "Neapolitan": 3,
    "California-style": 4,
    "Marinara": 2
  };

  //Different pizza images
  static final _pizzaImages = {
    "Sushi": "http://pngimg.com/uploads/pizza/pizza_PNG44077.png",
    "Neapolitan": "http://pngimg.com/uploads/pizza/pizza_PNG44078.png",
    "California-style": "http://pngimg.com/uploads/pizza/pizza_PNG44081.png",
    "Marinara": "http://pngimg.com/uploads/pizza/pizza_PNG44084.png"
  };


  //Validate if pizza can be baked or not. This is John
  final validateOrder =
      StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {
    if (_pizzaList[order] != null) {
      //pizza is available
      if (_pizzaList[order] != 0) {
        //pizza can be delivered
        sink.add(_pizzaImages[order]);
        final quantity = _pizzaList[order];
        _pizzaList[order] = quantity-1;
      } else {
        //out of stock
        sink.addError("Out of stock");
      }
    } else {
      //pizza is not in the menu
      sink.addError("Pizza not found");
    }
  });

  //This is Mia
  void orderItem(String pizza) {
    order.sink.add(pizza);
  }
}
复制代码

使用 PublishSubject 代替 BehaviorSubject

对于那些在 Flutter 项目中使用 RxDart 库的人来讲,这一点会更加地明确。BehaviorSubject 是一个特殊的 StreamController,它会捕获到已经添加到 controller 的最新项,而且将其做为新的 listener 的第一个事件触发。即便你在 BehaviorSubject 上调用 close() 或者 drain(),它仍然会保留最后一项,而且在这个 listener 被订阅的时候触发。若是开发人员不了解这个功能,这有可能会变成一场噩梦。而 PublishSubject 不会存储最后一项,更加适合于大多数状况。在这个项目中,能够查看 BehaviorSubject 的功能。运行应用程序,而且跳转到 'Add Goal' 页面,在表单中输入详细信息,而且跳转回来。如今,再次访问 'Add Goal' 页面,你就会发现表单里已经预先填写了你以前输入的数据。若是你和我同样懒,那么能够看我下面附上的视频:

Goals App Demo

正确地使用 BLoC Providers

在我说这一点以前,请看下面的代码片(第 9 行和第 10 行)。

import 'package:flutter/material.dart';
import 'ui/login.dart';
import 'blocs/goals_bloc_provider.dart';
import 'blocs/login_bloc_provider.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LoginBlocProvider(
      child: GoalsBlocProvider(
        child: MaterialApp(
          theme: ThemeData(
            accentColor: Colors.black,
            primaryColor: Colors.amber,
          ),
          home: Scaffold(
            appBar: AppBar(
              title: Text(
                "Goals",
                style: TextStyle(color: Colors.black),
              ),
              backgroundColor: Colors.amber,
              elevation: 0.0,
            ),
            body: LoginScreen(),
          ),
        ),
      ),
    );
  }
}

复制代码

你能够清楚地看到,多个 BLoC Provider 是嵌套的。这时候,那么你必定会担忧,若是继续在同一个链中添加更多的 BLoC,会致使一场噩梦,你可能会得出 BLoC 模式没法扩展的结论。可是,让我告诉你,当你须要在 Widget 树中访问多个 BLoC 的时候,可能会有一种特殊的状况(BLoC 只保存应用程序所须要的 UI 配置),所以,对于这种状况,上述的嵌套是彻底没问题的。可是我建议你在大多数的状况下,仍是要避免这种嵌套的,而且只在实际须要的地方提供 BLoC。所以,好比当你须要导航到新的页面的时候,能够像这样使用 BLoC Provider:

openDetailPage(ItemModel data, int index) {
    final page = MovieDetailBlocProvider(
      child: MovieDetail(
        title: data.results[index].title,
        posterUrl: data.results[index].backdrop_path,
        description: data.results[index].overview,
        releaseDate: data.results[index].release_date,
        voteAverage: data.results[index].vote_average.toString(),
        movieId: data.results[index].id,
      ),
    );
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return page;
      }),
    );
  }
复制代码

这样,MovieDetailBlocProvider 就不会为整个组件树,而是会为 MovieDetail 页面提供 BLoC。你能够看到,我将 MovieDetailScreen 存储在一个新的 final variable 中,来避免每次在 MovieDetailScreen 中打开或者关闭键盘的时候,都会从新建立 MovieDetailScreen 的问题。

尚未结束

虽然这里是本文的结尾了,但并非这个主题的结尾。我也会在这个有关优化 BLoC 模式的文集中不断添加新的想法,从而继续丰富它的内容。我但愿这些想法能够帮助你更好地实现 BLoC 模式。Keep learning and keep coding :)。若是你喜欢这篇文章,能够经过点赞来表达你的爱。

有任何疑问,请在 LinkedIn 与我联系,或者在 Twitter 上关注我。我会尽我所能解决你的问题。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索