关于flutter:使用-BLOC-模式构建你的-Flutter-项目

57次阅读

共计 9268 个字符,预计需要花费 24 分钟才能阅读完成。

原文: 应用 BLOC 模式构建你的 Flutter 我的项目

嗨伙计!我带着另一篇对于 Flutter 的全新文章回来了。这一次,将探讨和示范“如何构建 Flutter 我的项目”。这样你就能够轻松地保护、扩大和测试你的 Flutter 我的项目。在深入实际主题之前,我想分享一个小故事,对于为什么应该专一于为我的项目构建一个牢靠的架构。

更新 :本文的 第 2 篇 第 3 篇 已公布,对以后设计进行了一些更改,以解决一些问题并展现一些惊人的实现。链接在这里。

第 3 篇

Compile time Dependency Injection in Flutter

第 4 篇

Integration and Unit testing in Flutter

为什么须要结构化你的我的项目?

“2015 年,曾几何时,我过后是一名竞技程序员(Hackerearth 个人简介),同时也在学习 Android 利用程序开发。作为一名竞技程序员,我只关怀程序的输入和效率,素来没有思考过结构化我的程序和我的项目。这种趋势和编码格调也反映在我的 Android 我的项目中。我正在以一个竞技程序员的思维模式编写 Android 应用程序。一开始,所有都很好。因为只有我本人在我的项目上工作,没有老板给我提需要,不须要增加新性能或更改现有性能。然而,当我开始在一家初创公司工作并为他们构建 Android 应用程序时,我总是花很多工夫去批改程序中的现有性能。不仅如此,我甚者在构建应用程序的过程中引入了 Bugs。所有这些问题的根本原因是:‘我从来不遵循任何的架构模式,或者从未结构化我的我的项目’。随着工夫的流逝,我开始理解软件世界,并胜利的把我本人从一个竞技程序员转变成了一个软件工程师。现在,当启动一个新我的项目时,我的次要关注点是为我的项目构建一个松软的构造,或者架构。这帮忙我成为一名优良的、熟能生巧的软件工程师。😄”

完结我无聊的故事😅,让咱们回归本文的正题:“应用 BLOC 模式构建你的 Flutter 我的项目”。

咱们的指标

构建一个非常简单的 app,有一个页面,页面内蕴含一个网格列表。列表项是从服务端获取的。列表的内容是 The Movies DB 站点中的热门电影。

Note: 在持续之前,你应该曾经理解 Widgets,how to make a network call in Flutter,并具备 Dart 相干常识的中级程度。本文有点长,并附带了大量其余资源的链接,不便你进一步浏览相干的主题。

让咱们开始表演吧. 😍

在间接进入代码之前,先展现一下 BLOC 架构的视觉体验。咱们将遵循这个架构构建 app。

上图展现了数据如何从 UI 流向 Data Layer,以及如何从 Data Layer 流向 UI。BLOC 不会持有 UI 中 Widgets 的援用。UI 仅会监听来自 BLOC 的变动。让咱们做一个小问答来了解这个图:

1. 什么是 BLOC 模式?

它是 Google 开发人员举荐的 Flutter 状态管理系统。它从我的项目的核心地位拜访数据,有助于治理状态。

2. 我能够将此架构与其余任何架构相关联吗?

当然能够。MVP 和 MVVM 就是一些很好的例子。惟一会扭转的是:BLOC 将被 MVVM 中的 ViewModel 所代替。

3. BLOC 的底层是什么?或者在一个中央治理状态的外围是什么?

底层是 STREAMS 或 REACTIVE 形式。一般来说,数据将以流的模式从 BLOC 流向 UI 或从 UI 流向 BLOC。如果你从未据说过流,请浏览 Stack Overflow 的答复。

心愿小问答局部能打消你的疑虑。如果须要进一步解释或有其余问题,能够在上面评论或间接通过 LinkedIn 与我分割。

开始应用 BLOC 模式构建我的项目

1. 首先新建一个我的项目,革除 main.dart 文件中的所有代码。在终端中输出以下命令:

flutter create myProjectName

2. 在 main.dart 文件中写下以下代码:

import 'package:flutter/material.dart';
import 'src/app.dart'
void main() {void main() {runApp(App);
  }
}

3. 在 lib 包下创立一个 src 包,在 src 包中创立一个文件并将其命名为 app.dart,将以下代码复制粘贴到 app.dart 文件中。

import 'package:flutter/material.dart';
import 'ui/movie_list.dart';

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(theme: ThemeData.dark(),
        home: Scaffold(body: MovieList(),
        ),
      );
  }
}

4. 在 src 包下创立一个新包,并将其命名为 resources

当初创立几个新包,即 blocs、models、resources 和 ui,如下图所示,而后咱们设置我的项目的骨架:

blocs 包将寄存 BLOC 实现的相干文件。models 包将寄存 POJO 类,或从服务器获取的 JSON 模型类。资源包将蕴含存储库类和网络调用实现类。resources 包将存放数据存储库类和负责网络调用的实现类。ui 包将寄存用户可见的 UI 页面。

5. 最初一件事,咱们须要增加一个第三方库 RxDart。关上 pubspec.yaml,增加 rxdart: ^0.18.0,如下所示:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  rxdart: ^0.18.0
  http: ^0.12.0+1

sync 你的我的项目,或在终端中键入以下命令。须要在 project 根目录中执行此命令。

flutter packages get

6. 曾经实现了 project 的骨架搭建,当初开始解决我的项目的底层逻辑,即 网络层。首先,理解一下行将应用的服务端 API。点击 link,进入电影网站数据库 API 页面。实现注册,并从设置页面获取你的 API key。咱们将从上面的 url 获取数据:

http://api.themoviedb.org/3/movie/popular?api_key=”your\_api\_key”

将你的 API key 放到下面的 url 中并点击这个 url (删除双引号),你能够看到相似上面的 JSON 返回数据:

{
  "page": 1,
  "total_results": 19772,
  "total_pages": 989,
  "results": [
    {
      "vote_count": 6503,
      "id": 299536,
      "video": false,
      "vote_average": 8.3,
      "title": "Avengers: Infinity War",
      "popularity": 350.154,
      "poster_path": "\/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg",
      "original_language": "en",
      "original_title": "Avengers: Infinity War",
      "genre_ids": [
        12,
        878,
        14,
        28
      ],
      "backdrop_path": "\/bOGkgRGdhrBYJSLpXaxhXVstddV.jpg",
      "adult": false,
      "overview": "As the Avengers and their allies have continued to protect the world from threats too large for any one hero to handle, a new danger has emerged from the cosmic shadows: Thanos. A despot of intergalactic infamy, his goal is to collect all six Infinity Stones, artifacts of unimaginable power, and use them to inflict his twisted will on all of reality. Everything the Avengers have fought for has led up to this moment - the fate of Earth and existence itself has never been more uncertain.",
      "release_date": "2018-04-25"
    },

7. 为网络返回的这种数据类型创立数据模型或 POJO 类。在 models 包下创立一个新的文件,命名为 item\_model.dart,并复制上面的代码到文件中:

class ItemModel {
  int _page;
  int _total_results;
  int _total_pages;
  List<_Result> _results = [];

  ItemModel.fromJson(Map<String, dynamic> parsedJson) {print(parsedJson['results'].length);
    _page = parsedJson['page'];
    _total_results = parsedJson['total_results'];
    _total_pages = parsedJson['total_pages'];
    List<_Result> temp = [];
    for (int i = 0; i < parsedJson['results'].length; i++) {_Result result = _Result(parsedJson['results'][i]);
      temp.add(result);
    }
    _results = temp;
  }

  List<_Result> get results => _results;

  int get total_pages => _total_pages;

  int get total_results => _total_results;

  int get page => _page;
}

class _Result {
  int _vote_count;
  int _id;
  bool _video;
  var _vote_average;
  String _title;
  double _popularity;
  String _poster_path;
  String _original_language;
  String _original_title;
  List<int> _genre_ids = [];
  String _backdrop_path;
  bool _adult;
  String _overview;
  String _release_date;

  _Result(result) {_vote_count = result['vote_count'];
    _id = result['id'];
    _video = result['video'];
    _vote_average = result['vote_average'];
    _title = result['title'];
    _popularity = result['popularity'];
    _poster_path = result['poster_path'];
    _original_language = result['original_language'];
    _original_title = result['original_title'];
    for (int i = 0; i < result['genre_ids'].length; i++) {_genre_ids.add(result['genre_ids'][i]);
    }
    _backdrop_path = result['backdrop_path'];
    _adult = result['adult'];
    _overview = result['overview'];
    _release_date = result['release_date'];
  }

  String get release_date => _release_date;

  String get overview => _overview;

  bool get adult => _adult;

  String get backdrop_path => _backdrop_path;

  List<int> get genre_ids => _genre_ids;

  String get original_title => _original_title;

  String get original_language => _original_language;

  String get poster_path => _poster_path;

  double get popularity => _popularity;

  String get title => _title;

  double get vote_average => _vote_average;

  bool get video => _video;

  int get id => _id;

  int get vote_count => _vote_count;
}

心愿你能够将此文件和服务端返回的 JSON 进行隐射。如果不是这样,你须要晓得的是咱们最关怀的是 Results 类中的 poster_path 属性,咱们将在主页面中显示所有热门电影的海报(posters)。fromJson() 办法用来获取解码后的 JSON,并将 JSON 数据映射到正确的变量中。

8. 当初解决网络申请。在 resources 包下新建一个文件,命名为 movie\_api\_provider.dart,复制上面的代码到文件中,稍后会进行解释:

import 'dart:async';
import 'package:http/http.dart' show Client;
import 'dart:convert';
import '../models/item_model.dart';

class MovieApiProvider {Client client = Client();
  final _apiKey = 'your_api_key';

  Future<ItemModel> fetchMovieList() async {print("entered");
    final response = await client
        .get("http://api.themoviedb.org/3/movie/popular?api_key=$_apiKey");
    print(response.body.toString());
    if (response.statusCode == 200) {
      // If the call to the server was successful, parse the JSON
      return ItemModel.fromJson(json.decode(response.body));
    } else {
      // If that call was not successful, throw an error.
      throw Exception('Failed to load post');
    }
  }
}

Note:将 moive\_api\_provider.dart 文件中的 _apiKey 的值替换为你的 API key,否则将不能申请到数据。

fetchMovieList() 办法用来向服务端 API 发动网络申请。如果网络申请胜利,将返回一个 Feature ItemModel 对象;否则,将抛出一个异样。

9. 在 resource 包下创立一个新的文件,命名为 repository.dart。复制上面的代码到文件中:

import 'dart:async';
import 'movie_api_provider.dart';
import '../models/item_model.dart';

class Repository {final moviesApiProvider = MovieApiProvider();

  Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList();
}

文件中导入了 movie\_api\_provider.dart,并调用了 fetchMovieList() 办法。Repository 类是数据流向 BLOC 的中心点。

10. 上面的局部略微有点简单,实现 bloc 逻辑。在 blocs 包下新建一个文件,命名为 movies_bloc.dart。复制上面的代码到文件中,前面会具体解释代码:

import '../resources/repository.dart';
import 'package:rxdart/rxdart.dart';
import '../models/item_model.dart';

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();
  }
}

final bloc = MoviesBloc();

导入 RxDart package import 'package:rxdart/rxdart.dart';,这会将 RxDart 相干的所有办法和类导入到文件中。在 MoviesBloc 类中创立一个 Repository 对象,用来拜访 fetchAllMovies() 办法。创立一个 PublishSubject 对象,它的职责是:以流的模式将增加到其中的 ItemModel 对象(从服务端获取的数据模型类)传递给 UI。为了将 ItemModel 对象作为流传递,须要创立另一个办法 allMovies(),返回类型是 Observable(如果你不理解 Observables,请观看此视频)。文件的最初创立了一个 bloc 对象,这样不便 UI 以单例的形式拜访 MoviesBloc 类。

如果你不晓得什么是响应式编程,请看这个简略的阐明。简略来说,只有服务端有新的数据返回,咱们就必须更新 UI。为了简化更新工作,让 UI 监听来自 MoviesBloc 的数据变动,并相应的更新所展现的内容。这种对数据的监听,能够通过应用 RxDart 实现。

11. 这是最初局部了,在 UI 包下创立一个文件,命名为 movie\_list.dart。复制上面的代码到文件中:

import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';

class MovieList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {bloc.fetchAllMovies();
    return Scaffold(
      appBar: AppBar(title: Text('Popular Movies'),
      ),
      body: StreamBuilder(
        stream: bloc.allMovies,
        builder: (context, AsyncSnapshot<ItemModel> snapshot) {if (snapshot.hasData) {return buildList(snapshot);
          } else if (snapshot.hasError) {return Text(snapshot.error.toString());
          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }

  Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
    return GridView.builder(
        itemCount: snapshot.data.results.length,
        gridDelegate:
            new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
        itemBuilder: (BuildContext context, int index) {
          return Image.network(
            'https://image.tmdb.org/t/p/w185${snapshot.data
                .results[index].poster_path}',
            fit: BoxFit.cover,
          );
        });
  }
}

有意思的是,这个类没有应用 StatefulWidget,而是应用了一个 StreamBuilder,它能够像 StatefulWidget 一样实现更新 UI。

这里须要指出的一点是,我在 build 办法中进行了网络申请调用。这是不应该的,因为 build(context) 办法会被调用屡次。但因为文章变得越来越长,也越来越简单,为了放弃简略,这里依然在 build(context) 办法中调用网络申请。后续我会更新这篇文章,以一种更好的形式进行网络调用。

正如我所说的,MoviesBloc 类将新数据作为流传递。为了解决流,有一个很好的内置类,即 StreamBuilder,它将监听传入的流并相应地更新 UI。StreamBuilder 须要一个 stream 参数,这里传递的是 MovieBloc 的 allMovies() 办法,因为 allMovies() 返回一个流。当有数据流过去,StreamBuilder 将应用最新的数据从新渲染 widget,这些数据中蕴含 ItemModel 对象。当然,你能够应用任何的 Widget 展现数据对象中的任何数据(这是你的额创造力就展示进去了)。这里应用一个 GridView 显示 ItemModel 对象列表中的所有海报。最终产品的输入如下:

到了文章的开端,伙计们,你们能保持到最初真是太好了,心愿你们喜爱这篇文章。如果你有任何疑难或问题,请通过 LinkedIn 或 Twitter 分割我。请观赏这篇文章,不要吝惜你的掌声和评论。

如果你须要残缺的源码,请拜访这个工程项目的 githut repository

看看我的其余文章

Effective BLoC pattern

When Firebase meets BLoC Pattern

正文完
 0