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,
),
],
),
);
}
}
网友评论