使用BLoC構(gòu)建您的Flutter項(xiàng)目之二

原文地址:https://medium.com/flutterpub/architect-your-flutter-project-using-bloc-pattern-part-2-d8dd1eca9ba5

上一篇文章中已經(jīng)使用了BLoC模式實(shí)現(xiàn)了項(xiàng)目的構(gòu)建,這篇文章中將會(huì)對(duì)上次的項(xiàng)目進(jìn)行優(yōu)化指孤。

這篇文章主要覆蓋的主題:

  1. 解決架構(gòu)中的設(shè)計(jì)缺陷
  2. Single Instance與Scoped Instance(對(duì) BLoC的訪問(wèn))
  3. Navigation
  4. RxDart’s Transformers

當(dāng)前架構(gòu)中的設(shè)計(jì)缺陷

第一個(gè)缺陷就是列牺,在MovieBloc類中創(chuàng)建了一個(gè)dispose方法,該方法是用來(lái)關(guān)閉流以防導(dǎo)致內(nèi)存溢出。我們創(chuàng)建了這個(gè)方法划纽,但是從來(lái)沒(méi)有調(diào)用過(guò)脆侮,這將會(huì)導(dǎo)致內(nèi)存溢出。

另一個(gè)缺陷就是在build方法中進(jìn)行網(wǎng)絡(luò)調(diào)用勇劣。

現(xiàn)在MovieList是StatelessWidget靖避,而StatelessWidget是只要將其添加到Widget樹(shù)中,之后所有屬性都是不可變的比默,而build方法時(shí)入口幻捏,由于配置更改,可以被多次調(diào)用命咐。所以該方法不適合網(wǎng)絡(luò)調(diào)用粘咖。而StatelessWidget中也沒(méi)有一個(gè)適合調(diào)用dispose方法的地方嗤攻。

StatefulWidget方法提供了initStatedispose方法寡喝。initState方法用來(lái)分配資源,而dispose方法用來(lái)在界面回收的之前處理掉這些資源。

首先我們將MovieList從StatelessWidget轉(zhuǎn)換成StatefulWidget情龄,接著在initState中調(diào)用網(wǎng)絡(luò)請(qǐng)求方法,接著在dispose方法中調(diào)用bloc的dispose方法目木。

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

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    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 GridTile(
            child: Image.network(
              'https://image.tmdb.org/t/p/w185${snapshot.data
                  .results[index].poster_path}',
              fit: BoxFit.cover,
            ),
          );
        });
  }
}

新功能實(shí)現(xiàn)

我們添加一個(gè)新界面用來(lái)顯示電影詳情蜕企。我們重新設(shè)計(jì)一下app的流程。

image.png

上面的圖很簡(jiǎn)單路呜,但是我們還是來(lái)解釋一下:

  1. MovieList Screen:電影列表界面
  2. MovieList Bloc:這是一個(gè)從Repository獲取數(shù)據(jù)并傳遞到UI界面的橋梁迷捧。
  3. MovieDetail Screen: 用來(lái)顯示從列表選擇的電影的詳情。
  4. Repository:用來(lái)控制數(shù)據(jù)流的中心
  5. API provider:用來(lái)控制所有的網(wǎng)絡(luò)請(qǐng)求

Single Instance vs Scoped Instance

正如你所看到的胀葱,兩個(gè)Screen都可以訪問(wèn)各自的BLoC類漠秋,我們可以通過(guò)下面的兩種方式將這些BLoC類暴露給各自的Screen。單例是指將BLoC類的單個(gè)引用(Singleton)暴露給Screen抵屿∏旖酰可以從應(yīng)用程序的任何部分訪問(wèn)此類型的BLoC類。任何Screen都可以使用單實(shí)例BLoC類轧葛。

但是Scoped Instance BLoC類具有有限的訪問(wèn)權(quán)限搂抒。它只與Screen相關(guān)聯(lián)。


2.png

正如上圖看到的尿扯,只有Screen本身以及其下面兩個(gè)自定的widget可以訪問(wèn)bloc求晶。我們使用InheritedWidget將BLoC包裹起來(lái)。InheritedWidget將包裝Screen衷笋,讓Screen組件和其里面的自定義的組件可以訪問(wèn)Bloc芳杏。Screen的父組件都不能訪問(wèn)該Bloc。

Single Instance只適合用在小型的項(xiàng)目中辟宗,如果你開(kāi)發(fā)的是一個(gè)復(fù)雜的項(xiàng)目爵赵,那么Scoped Widget將會(huì)是你的首選。

添加詳情界面

當(dāng)點(diǎn)擊列表中的其中一個(gè)item時(shí)慢蜓,將會(huì)跳轉(zhuǎn)到該電影的詳情界面亚再,用戶可以看到電影的詳細(xì)信息,然后一些數(shù)據(jù)將從列表界面?zhèn)鞯皆斍榻缑娉柯眨A(yù)告片從服務(wù)器加載氛悬。

在創(chuàng)建文件之前,我們要遵從上一篇文章中提到的項(xiàng)目結(jié)構(gòu)耘柱。首先在ui包中創(chuàng)建movie_detail.dart文件如捅。

import 'package:flutter/material.dart';

class MovieDetail extends StatefulWidget {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetail({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  State<StatefulWidget> createState() {
    return MovieDetailState(
      title: title,
      posterUrl: posterUrl,
      description: description,
      releaseDate: releaseDate,
      voteAverage: voteAverage,
      movieId: movieId,
    );
  }
}

class MovieDetailState extends State<MovieDetail> {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetailState({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        bottom: false,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                expandedHeight: 200.0,
                floating: false,
                pinned: true,
                elevation: 0.0,
                flexibleSpace: FlexibleSpaceBar(
                    background: Image.network(
                  "https://image.tmdb.org/t/p/w500$posterUrl",
                  fit: BoxFit.cover,
                )),
              ),
            ];
          },
          body: Padding(
            padding: const EdgeInsets.all(10.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Container(margin: EdgeInsets.only(top: 5.0)),
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
                Row(
                  children: <Widget>[
                    Icon(
                      Icons.favorite,
                      color: Colors.red,
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 1.0, right: 1.0),
                    ),
                    Text(
                      voteAverage,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 10.0, right: 10.0),
                    ),
                    Text(
                      releaseDate,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                  ],
                ),
                Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
                Text(description),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Navigation

Flutter中如果你想從一個(gè)界面跳轉(zhuǎn)到另一個(gè)界面的話,使用Navigator類,我們?cè)?strong>movie_list.dart中實(shí)現(xiàn)導(dǎo)航的邏輯调煎。

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

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    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 GridTile(
            child: InkResponse(
              enableFeedback: true,
              child: Image.network(
                'https://image.tmdb.org/t/p/w185${snapshot.data
                    .results[index].poster_path}',
                fit: BoxFit.cover,
              ),
              onTap: () => openDetailPage(snapshot.data, index),
            ),
          );
        });
  }

  openDetailPage(ItemModel data, int index) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return 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,
        );
      }),
    );
  }
}

具體的邏輯在openDetailPage方法中镜遣。

接著我們?nèi)シ?wù)器上獲取預(yù)告片信息:
https://api.themoviedb.org/3/movie/<movie_id>/videos?api_key=your_api_key,我們需要傳入movie_id和api_key士袄。下面是api返回的信息:

{
  "id": 299536,
  "results": [
    {
      "id": "5a200baa925141033608f5f0",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "6ZfuNTqbHE8",
      "name": "Official Trailer",
      "site": "YouTube",
      "size": 1080,
      "type": "Trailer"
    },
    {
      "id": "5a200bcc925141032408d21b",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "sAOzrChqmd0",
      "name": "Action...Avengers: Infinity War",
      "site": "YouTube",
      "size": 720,
      "type": "Clip"
    },
    {
      "id": "5a200bdd0e0a264cca08d39f",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "3VbHg5fqBYw",
      "name": "Trailer Tease",
      "site": "YouTube",
      "size": 720,
      "type": "Teaser"
    },
    {
      "id": "5a7833440e0a26597f010849",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "pVxOVlm_lE8",
      "name": "Big Game Spot",
      "site": "YouTube",
      "size": 1080,
      "type": "Teaser"
    },
    {
      "id": "5aabd7e69251413feb011276",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "QwievZ1Tx-8",
      "name": "Official Trailer #2",
      "site": "YouTube",
      "size": 1080,
      "type": "Trailer"
    },
    {
      "id": "5aea2ed2c3a3682bf7001205",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "LXPaDL_oILs",
      "name": "\"Legacy\" TV Spot",
      "site": "YouTube",
      "size": 1080,
      "type": "Teaser"
    },
    {
      "id": "5aea2f3e92514172a7001672",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "PbRmbhdHDDM",
      "name": "\"Family\" Featurette",
      "site": "YouTube",
      "size": 1080,
      "type": "Featurette"
    }
  ]
}

我們需要在model包中創(chuàng)建一個(gè)trailer_model.dart文件悲关。

class TrailerModel {
  int _id;
  List<_Result> _results = [];

  TrailerModel.fromJson(Map<String, dynamic> parsedJson) {
    _id = parsedJson['id'];
    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 id => _id;
}

class _Result {
  String _id;
  String _iso_639_1;
  String _iso_3166_1;
  String _key;
  String _name;
  String _site;
  int _size;
  String _type;

  _Result(result) {
    _id = result['id'];
    _iso_639_1 = result['iso_639_1'];
    _iso_3166_1 = result['iso_3166_1'];
    _key = result['key'];
    _name = result['name'];
    _site = result['site'];
    _size = result['size'];
    _type = result['type'];
  }

  String get id => _id;

  String get iso_639_1 => _iso_639_1;

  String get iso_3166_1 => _iso_3166_1;

  String get key => _key;

  String get name => _name;

  String get site => _site;

  int get size => _size;

  String get type => _type;
}

接下來(lái)在movie_api_provider.dart文件中實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求谎僻。

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

class MovieApiProvider {
  Client client = Client();
  final _apiKey = '802b2c4b88ea1183e50e6b285a27696e';
  final _baseUrl = "http://api.themoviedb.org/3/movie";

  Future<ItemModel> fetchMovieList() async {
    final response = await client.get("$_baseUrl/popular?api_key=$_apiKey");
    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');
    }
  }

  Future<TrailerModel> fetchTrailer(int movieId) async {
    final response =
        await client.get("$_baseUrl/$movieId/videos?api_key=$_apiKey");

    if (response.statusCode == 200) {
      return TrailerModel.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to load trailers');
    }
  }
}

fetchTrailer(movie_id)方法就是我們執(zhí)行網(wǎng)絡(luò)請(qǐng)求然后將返回的信息轉(zhuǎn)換成TrailerModel對(duì)象,并返回Future<TrailerModel>寓辱。

接著在Repository類中添加網(wǎng)絡(luò)調(diào)用實(shí)現(xiàn)艘绍。接下來(lái)讓我們使用Scoped Instance方案來(lái)實(shí)現(xiàn)功能。在bloc包中創(chuàng)建一個(gè)movie_detail_bloc.dartmovie_detail_bloc_provider.dart文件秫筏。

下面是movie_detail_bloc_provider.dart中的代碼:

import 'package:flutter/material.dart';
import 'movie_detail_bloc.dart';
export 'movie_detail_bloc.dart';

class MovieDetailBlocProvider extends InheritedWidget {
  final MovieDetailBloc bloc;

  MovieDetailBlocProvider({Key key, Widget child})
      : bloc = MovieDetailBloc(),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_) {
    return true;
  }

  static MovieDetailBloc of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(MovieDetailBlocProvider)
            as MovieDetailBlocProvider)
        .bloc;
  }
}

此類實(shí)現(xiàn)了InheritedWidget诱鞠,并通過(guò)(Context)的of方法來(lái)放問(wèn)bloc。

接下來(lái)編寫movie_detail.dart文件这敬。

import 'dart:async';

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

class MovieDetailBloc {
  final _repository = Repository();
  final _movieId = PublishSubject<int>();
  final _trailers = BehaviorSubject<Future<TrailerModel>>();

  Function(int) get fetchTrailersById => _movieId.sink.add;
  Observable<Future<TrailerModel>> get movieTrailers => _trailers.stream;

  MovieDetailBloc() {
    _movieId.stream.transform(_itemTransformer()).pipe(_trailers);
  }

  dispose() async {
    _movieId.close();
    await _trailers.drain();
    _trailers.close();
  }

  _itemTransformer() {
    return ScanStreamTransformer(
      (Future<TrailerModel> trailer, int id, int index) {
        print(index);
        trailer = _repository.fetchTrailers(id);
        return trailer;
      },
    );
  }
}

上面的邏輯就是將movieId傳遞給api請(qǐng)求航夺,然后返回預(yù)告片列表。用到了RxDart的Transformers崔涂。

Transformers

Transformers是用來(lái)連接兩個(gè)或多個(gè)Subject并獲得最終的結(jié)果阳掐。如果想要對(duì)數(shù)據(jù)進(jìn)行一些操作后,從一個(gè)Subject轉(zhuǎn)換成另一個(gè)Subject的話堪伍,我們將使用Transformers對(duì)傳入的第一個(gè)Subject進(jìn)行一些操作之后傳遞給下一個(gè)Subject锚烦。

在我們的項(xiàng)目中觅闽,我們將movieId添加給_movieId,他是一個(gè)PublishSubject的類型帝雇,我們將_movieId傳遞給ScanStreamTransformer,然后ScanStreamTransformer將進(jìn)行網(wǎng)絡(luò)調(diào)用蛉拙,最后把結(jié)果傳遞給_trailers,_trailers是一個(gè)BehaviorSubject尸闸。

3.png

最后一步就是通過(guò)MovieDetail訪問(wèn)MovieDetailBloc,為此我們需要更新openDetailPage()方法。

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

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    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 GridTile(
            child: InkResponse(
              enableFeedback: true,
              child: Image.network(
                'https://image.tmdb.org/t/p/w185${snapshot.data
                    .results[index].poster_path}',
                fit: BoxFit.cover,
              ),
              onTap: () => openDetailPage(snapshot.data, index),
            ),
          );
        });
  }

  openDetailPage(ItemModel data, int index) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return 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,
          ),
        );
      }),
    );
  }
}

我們最終返回了一個(gè)包裹MovieDetailMovieDetailBlocProvider孕锄,這樣MovieDetailBloc就可以被MovieDetail組件以及子組件所訪問(wèn)了吮廉。

接下來(lái)就是movie_detail.dart類了

import 'dart:async';

import 'package:flutter/material.dart';
import '../blocs/movie_detail_bloc_provider.dart';
import '../models/trailer_model.dart';

class MovieDetail extends StatefulWidget {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetail({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  State<StatefulWidget> createState() {
    return MovieDetailState(
      title: title,
      posterUrl: posterUrl,
      description: description,
      releaseDate: releaseDate,
      voteAverage: voteAverage,
      movieId: movieId,
    );
  }
}

class MovieDetailState extends State<MovieDetail> {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetailBloc bloc;

  MovieDetailState({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

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

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        bottom: false,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context,
              bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                expandedHeight: 200.0,
                floating: false,
                pinned: true,
                elevation: 0.0,
                flexibleSpace: FlexibleSpaceBar(
                    background: Image.network(
                      "https://image.tmdb.org/t/p/w500$posterUrl",
                      fit: BoxFit.cover,
                    )),
              ),
            ];
          },
          body: Padding(
            padding: const EdgeInsets.all(10.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Container(margin: EdgeInsets.only(top: 5.0)),
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Row(
                  children: <Widget>[
                    Icon(
                      Icons.favorite,
                      color: Colors.red,
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 1.0,
                          right: 1.0),
                    ),
                    Text(
                      voteAverage,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 10.0,
                          right: 10.0),
                    ),
                    Text(
                      releaseDate,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                  ],
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Text(description),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Text(
                  "Trailer",
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                StreamBuilder(
                  stream: bloc.movieTrailers,
                  builder:
                      (context, AsyncSnapshot<Future<TrailerModel>> snapshot) {
                    if (snapshot.hasData) {
                      return FutureBuilder(
                        future: snapshot.data,
                        builder: (context,
                            AsyncSnapshot<TrailerModel> itemSnapShot) {
                          if (itemSnapShot.hasData) {
                            if (itemSnapShot.data.results.length > 0)
                              return trailerLayout(itemSnapShot.data);
                            else
                              return noTrailer(itemSnapShot.data);
                          } else {
                            return Center(child: CircularProgressIndicator());
                          }
                        },
                      );
                    } else {
                      return Center(child: CircularProgressIndicator());
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget noTrailer(TrailerModel data) {
    return Center(
      child: Container(
        child: Text("No trailer available"),
      ),
    );
  }

  Widget trailerLayout(TrailerModel data) {
    if (data.results.length > 1) {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
          trailerItem(data, 1),
        ],
      );
    } else {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
        ],
      );
    }
  }

  trailerItem(TrailerModel data, int index) {
    return Expanded(
      child: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.all(5.0),
            height: 100.0,
            color: Colors.grey,
            child: Center(child: Icon(Icons.play_circle_filled)),
          ),
          Text(
            data.results[index].name,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}

在這里,我們將初始化bloc的代碼放在了didChangeDependencies()中畸肆,原因在這里宦芦,最后是項(xiàng)目地址

自己的話

這兩篇文章原作者通過(guò)一步步深入和優(yōu)化的方式向我們介紹了Flutter項(xiàng)目架構(gòu)和分層的東西,看了之后轴脐,讓我有種豁然開(kāi)朗的感覺(jué)调卑,瞬間知道該怎么動(dòng)手開(kāi)發(fā)一個(gè)Flutter項(xiàng)目了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末大咱,一起剝皮案震驚了整個(gè)濱河市恬涧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碴巾,老刑警劉巖溯捆,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異厦瓢,居然都是意外死亡提揍,警方通過(guò)查閱死者的電腦和手機(jī)啤月,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)劳跃,“玉大人顽冶,你說(shuō)我怎么就攤上這事∈厶迹” “怎么了强重?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)贸人。 經(jīng)常有香客問(wèn)我间景,道長(zhǎng),這世上最難降的妖魔是什么艺智? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任倘要,我火速辦了婚禮,結(jié)果婚禮上十拣,老公的妹妹穿的比我還像新娘封拧。我一直安慰自己,他們只是感情好夭问,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布泽西。 她就那樣靜靜地躺著,像睡著了一般缰趋。 火紅的嫁衣襯著肌膚如雪捧杉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天秘血,我揣著相機(jī)與錄音味抖,去河邊找鬼。 笑死灰粮,一個(gè)胖子當(dāng)著我的面吹牛仔涩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粘舟,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼熔脂,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蓖乘?” 一聲冷哼從身側(cè)響起锤悄,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘉抒,沒(méi)想到半個(gè)月后零聚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年隶症,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了政模。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚂会,死狀恐怖淋样,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胁住,我是刑警寧澤趁猴,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站彪见,受9級(jí)特大地震影響儡司,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜余指,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一捕犬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酵镜,春花似錦碉碉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至缸濒,卻和暖如春足丢,著一層夾襖步出監(jiān)牢的瞬間粱腻,已是汗流浹背庇配。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绍些,地道東北人捞慌。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像柬批,于是被迫代替她去往敵國(guó)和親啸澡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容