美文网首页
使用BLOC模式构建Flutter项目(二)

使用BLOC模式构建Flutter项目(二)

作者: 渣渣曦 | 来源:发表于2019-07-23 23:35 被阅读0次

    1、首先解决上一章的两个缺陷,第一个是在MoviesBloc类里建立的dispose()方法来关闭streams以避免内存泄露,但未在任何地方引用所以会导致内存泄露。另一个缺陷是把网络调用放在build方法里相当危险。
    当前MovieList类是一个StatelessWidget,所有属性生成后不可变更,因此不适合网络请求,也不能调用bloc的dispose方法,因此转换访类为StatfulWidget,把网络调用放在initState()里,bloc的dispose()放入StatefulWidget的dispose()中。
    用以下代码替换当前movie_list.dart代码:

    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].posterPath}',
                  fit: BoxFit.cover,
                ),
              );
            });
      }
    }
    

    上面代码把bloc.fetchAllMovies()调用放入了initState(),block.dispose()放入了MovieListState类的dispose()中。运行该程序不会发生改变,但也不会发生多次网络请求和内存泄露。
    提示:任何网络和数据库调用都不要放到build方法类中。
    完成后进行app的下一步设计流程


    image.png

    流程使用述语:
    1)、Movie List Screen:可视化用户界面看所有图片的grid list。
    2)、Movie List Bloc:从repository获取数据后传送到Movie List Screen的桥。
    3)、Movie Detail Screen:从主页跳转明细页面。
    4)、Repository:数据流控制点。
    5)、API provider:网络调用接口。
    2、单例(Single Instance)和作用域实例(Scoped Instance)
    可以通过两种方式(单例和作用域实例)把BLoC类引入各自的用户界面,单例可以在app里的任何部分进行引用;而作用域实例被限制访问仅限关联用户界面引用,如下图:


    image.png
    如上图,把BLoC放入InheritedWidget,然后包装用户界面其中的widget即可访问BLoC,无上级节点不可以访问BLoC。
    增加detail用户界面
    在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),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    

    3、导航
    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].posterPath}',
                    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].backdropPath,
              description: data.results[index].overview,
              releaseDate: data.results[index].releaseDate,
              voteAverage: data.results[index].voteAverage.toString(),
              movieId: data.results[index].id,
            );
          }),
        );
      }
    }
    

    下一步为明细页面加入预告片,通过下面的链接获取JSON响应文本:
    https://api.themoviedb.org/3/movie/299536/videos?api_key=802b2c4b88ea1183e50e6b285a27696e
    首先建立POJO类,在models包中创建trailer_model.dart,代码如下:

    class TrailerModel  {
      int id;
      List<Results> results;
    
      TrailerModel({this.id, this.results});
    
      TrailerModel.fromJson(Map<String, dynamic> json) {
        id = json['id'];
        if (json['results'] != null) {
          results = new List<Results>();
          json['results'].forEach((v) {
            results.add(new Results.fromJson(v));
          });
        }
      }
    
      Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = new Map<String, dynamic>();
        data['id'] = this.id;
        if (this.results != null) {
          data['results'] = this.results.map((v) => v.toJson()).toList();
        }
        return data;
      }
    }
    
    class Results {
      String id;
      String iso6391;
      String iso31661;
      String key;
      String name;
      String site;
      int size;
      String type;
    
      Results(
          {this.id,
          this.iso6391,
          this.iso31661,
          this.key,
          this.name,
          this.site,
          this.size,
          this.type});
    
      Results.fromJson(Map<String, dynamic> json) {
        id = json['id'];
        iso6391 = json['iso_639_1'];
        iso31661 = json['iso_3166_1'];
        key = json['key'];
        name = json['name'];
        site = json['site'];
        size = json['size'];
        type = json['type'];
      }
    
      Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = new Map<String, dynamic>();
        data['id'] = this.id;
        data['iso_639_1'] = this.iso6391;
        data['iso_3166_1'] = this.iso31661;
        data['key'] = this.key;
        data['name'] = this.name;
        data['site'] = this.site;
        data['size'] = this.size;
        data['type'] = this.type;
        return data;
      }
    }
    

    创建movie_api_provider.dart实现(implement)网络调用请求,内容如下:

    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)方法作用是转换网络请求响应JSON文本到TrailerModel对象,并返回Future<TrailerModel>。
    更新repository.dart文件,增加新的网络实现,代码如下:

    import 'dart:async';
    import 'movie_api_provider.dart';
    import '../models/item_model.dart';
    import '../models/trailer_model.dart';
    
    class Repository {
      final moviesApiProvider = MovieApiProvider();
    
      Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList();
    
      Future<TrailerModel> fetchTrailers(int movieId) => moviesApiProvider.fetchTrailer(movieId);
    }
    

    接下来实现BLoC作用域实例,在blocs包中创建movie_detail_bloc.dart和movie_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,通过of(context)访问bloc。context作为of(context)属于InheritedWidget。
    movie_detail_bloc.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主要是通过两个或多个Subjects获取最终结果。意思就是在对一些数据操作后把从一个Subject数据传递到另一个。
    在此应用程序中movieId增加到名为_movieId的PublishSubject,然后把movieId送到ScanStreamTransformer中调用预告片API,并把返回结果通过pipe发送到_trailers的BehaviorSubject。如下图所示:


    image.png

    最后一步是使movieDetailBloc访问MovieDetail。更新movie_list.dart的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,
              ),
            );
          }),
        );
      }
    }
    

    代码中MaterialPageRoute返回MovieDetailBlocProvider(InheritedWidget),其中包含子组件MovieDetail。因此MovieDetailBloc类能访问明细页面和其下所有组件。
    最后,修改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模式构建Flutter项目(二)

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