使用BLoC构建您的Flutter项目之二

作者: 小菜鸟程序媛 | 来源:发表于2019-05-25 19:08 被阅读7次

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

    上一篇文章中已经使用了BLoC模式实现了项目的构建,这篇文章中将会对上次的项目进行优化。

    这篇文章主要覆盖的主题:

    1. 解决架构中的设计缺陷
    2. Single Instance与Scoped Instance(对 BLoC的访问)
    3. Navigation
    4. RxDart’s Transformers

    当前架构中的设计缺陷

    第一个缺陷就是,在MovieBloc类中创建了一个dispose方法,该方法是用来关闭流以防导致内存溢出。我们创建了这个方法,但是从来没有调用过,这将会导致内存溢出。

    另一个缺陷就是在build方法中进行网络调用。

    现在MovieList是StatelessWidget,而StatelessWidget是只要将其添加到Widget树中,之后所有属性都是不可变的,而build方法时入口,由于配置更改,可以被多次调用。所以该方法不适合网络调用。而StatelessWidget中也没有一个适合调用dispose方法的地方。

    StatefulWidget方法提供了initStatedispose方法。initState方法用来分配资源,而dispose方法用来在界面回收的之前处理掉这些资源。

    首先我们将MovieList从StatelessWidget转换成StatefulWidget,接着在initState中调用网络请求方法,接着在dispose方法中调用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,
                ),
              );
            });
      }
    }
    

    新功能实现

    我们添加一个新界面用来显示电影详情。我们重新设计一下app的流程。

    image.png

    上面的图很简单,但是我们还是来解释一下:

    1. MovieList Screen:电影列表界面
    2. MovieList Bloc:这是一个从Repository获取数据并传递到UI界面的桥梁。
    3. MovieDetail Screen: 用来显示从列表选择的电影的详情。
    4. Repository:用来控制数据流的中心
    5. API provider:用来控制所有的网络请求

    Single Instance vs Scoped Instance

    正如你所看到的,两个Screen都可以访问各自的BLoC类,我们可以通过下面的两种方式将这些BLoC类暴露给各自的Screen。单例是指将BLoC类的单个引用(Singleton)暴露给Screen。可以从应用程序的任何部分访问此类型的BLoC类。任何Screen都可以使用单实例BLoC类。

    但是Scoped Instance BLoC类具有有限的访问权限。它只与Screen相关联。


    2.png

    正如上图看到的,只有Screen本身以及其下面两个自定的widget可以访问bloc。我们使用InheritedWidget将BLoC包裹起来。InheritedWidget将包装Screen,让Screen组件和其里面的自定义的组件可以访问Bloc。Screen的父组件都不能访问该Bloc。

    Single Instance只适合用在小型的项目中,如果你开发的是一个复杂的项目,那么Scoped Widget将会是你的首选。

    添加详情界面

    当点击列表中的其中一个item时,将会跳转到该电影的详情界面,用户可以看到电影的详细信息,然后一些数据将从列表界面传到详情界面,预告片从服务器加载。

    在创建文件之前,我们要遵从上一篇文章中提到的项目结构。首先在ui包中创建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中如果你想从一个界面跳转到另一个界面的话,使用Navigator类,我们在movie_list.dart中实现导航的逻辑。

    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方法中。

    接着我们去服务器上获取预告片信息:
    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包中创建一个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;
    }
    

    接下来在movie_api_provider.dart文件中实现网络请求。

    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)方法就是我们执行网络请求然后将返回的信息转换成TrailerModel对象,并返回Future<TrailerModel>。

    接着在Repository类中添加网络调用实现。接下来让我们使用Scoped Instance方案来实现功能。在bloc包中创建一个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;
      }
    }
    

    此类实现了InheritedWidget,并通过(Context)的of方法来放问bloc。

    接下来编写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请求,然后返回预告片列表。用到了RxDart的Transformers

    Transformers

    Transformers是用来连接两个或多个Subject并获得最终的结果。如果想要对数据进行一些操作后,从一个Subject转换成另一个Subject的话,我们将使用Transformers对传入的第一个Subject进行一些操作之后传递给下一个Subject。

    在我们的项目中,我们将movieId添加给_movieId,他是一个PublishSubject的类型,我们将_movieId传递给ScanStreamTransformer,然后ScanStreamTransformer将进行网络调用,最后把结果传递给_trailers,_trailers是一个BehaviorSubject。

    3.png

    最后一步就是通过MovieDetail访问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,
              ),
            );
          }),
        );
      }
    }
    

    我们最终返回了一个包裹MovieDetailMovieDetailBlocProvider,这样MovieDetailBloc就可以被MovieDetail组件以及子组件所访问了。

    接下来就是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()中,原因在这里,最后是项目地址

    自己的话

    这两篇文章原作者通过一步步深入和优化的方式向我们介绍了Flutter项目架构和分层的东西,看了之后,让我有种豁然开朗的感觉,瞬间知道该怎么动手开发一个Flutter项目了。

    相关文章

      网友评论

        本文标题:使用BLoC构建您的Flutter项目之二

        本文链接:https://www.haomeiwen.com/subject/mmkzzqtx.html