在movie資料夾下新增「bloc」資料夾並用bloc generator產生MovieBloc的模板。
fluttube
└───lib
│ └───login
│ └───movie
│ │ └───bloc
│ │ │ └───bloc.dart
│ │ │ └───movie_bloc.dart
│ │ │ └───movie_event.dart
│ │ │ └───movie_state.dart
│ │ └───model
│ │ │ └───movie_list.dart
│ │ └───movie_api_provider.dart
│ │ └───movie_repository.dart
│ └───register
│ ...
│ └─── validators.dart
我把State分成以下幾種:
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import '../model/movie_list.dart';
@immutable
abstract class MovieState extends Equatable {
MovieState([List props = const []]) : super(props);
}
class NowPlayingMovieState extends MovieState {
final MovieList movieList;
NowPlayingMovieState({@required this.movieList}) : super([movieList]);
@override
String toString() {
return "NowPlayingMovieState";
}
}
class PopularMovieState extends MovieState {
final MovieList movieList;
PopularMovieState({@required this.movieList}) : super([movieList]);
@override
String toString() {
return "PopularMovieState";
}
}
class TopRatedMovieState extends MovieState {
final MovieList movieList;
TopRatedMovieState({@required this.movieList}) : super([movieList]);
@override
String toString() {
return "TopRatedMovieState";
}
}
class InitMovieState extends MovieState {
@override
String toString() {
return "InitMovieState";
}
}
class LoadingMovie extends MovieState {
@override
String toString() {
return "LoadingMovie";
}
}
class FailedFetchData extends MovieState {
@override
String toString() {
return "FailedFetchData";
}
}
比較要注意的是前面三個State都有儲存取回來的電影清單,UI就可以直接從State拿然後顯示出來。
Event只有三種:
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@immutable
abstract class MovieEvent extends Equatable {
MovieEvent([List props = const []]) : super(props);
}
class FetchNowPlaying extends MovieEvent {
final String region;
FetchNowPlaying({@required this.region}) : super([region]);
@override
String toString() {
return "Fetch NowPlaying Movie List {region: $region}";
}
}
class FetchPopular extends MovieEvent {
final String region;
FetchPopular({@required this.region}) : super([region]);
@override
String toString() {
return "Fetch Popular Movie List {region: $region}";
}
}
class FetchTopRated extends MovieEvent {
final String region;
FetchTopRated({@required this.region}) : super([region]);
@override
String toString() {
return "Fetch TopRated Movie List {region: $region}";
}
}
每個Event都有要求輸入region
,之後就可以讓使用者選擇要顯示哪個國家的電影清單。
取得電影清單的event需要呼叫MovieRepository內的method,所以在建構子的地方要代入MovieRepository的實體。其他一樣是要修改initialState以及設計mapEventToState。
import 'dart:async';
import 'package:bloc/bloc.dart';
import '../movie.dart';
import 'package:meta/meta.dart';
class MovieBloc extends Bloc<MovieEvent, MovieState> {
final MovieRepository _movieRepository;
MovieBloc({@required MovieRepository movieRepository})
: assert(movieRepository != null),
_movieRepository = movieRepository;
@override
MovieState get initialState => InitMovieState();
@override
Stream<MovieState> mapEventToState(
MovieEvent event,
) async* {
if (event is FetchNowPlaying) {
yield* _mapFetchNowPlayingToState(region: event.region);
} else if (event is FetchPopular) {
yield* _mapFetchPopularToState(region: event.region);
} else if (event is FetchTopRated) {
yield* _mapFetchTopRatedToState(region: event.region);
}
}
Stream<MovieState> _mapFetchNowPlayingToState({String region}) async* {
yield LoadingMovie();
try {
final _movieList =
await _movieRepository.fetchNowPlayingMovieList(region: region);
print(_movieList.results.length);
yield NowPlayingMovieState(movieList: _movieList);
} catch (_) {
yield FailedFetchData();
}
}
Stream<MovieState> _mapFetchPopularToState({String region}) async* {
yield LoadingMovie();
try {
final _movieList =
await _movieRepository.fetchPopularMovieList(region: region);
yield PopularMovieState(movieList: _movieList);
} catch (_) {
yield FailedFetchData();
}
}
Stream<MovieState> _mapFetchTopRatedToState({String region}) async* {
yield LoadingMovie();
try {
final _movieList =
await _movieRepository.fetchTopRatedMovieList(region: region);
yield TopRatedMovieState(movieList: _movieList);
} catch (_) {
yield FailedFetchData();
}
}
}
在movie資料夾下新增「movie.dart」
貼上以下程式碼:
export 'bloc/bloc.dart';
export 'model/movie_list.dart';
export 'movie_repository.dart';
測試一下MovieBloc有正確的運作,開啟home.dart貼上以下程式碼
import 'package:flutter/material.dart';
import '../authentication_bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttube/movie/movie.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () =>
BlocProvider.of<AuthenticationBloc>(context).dispatch(LoggedOut()),
label: Text("Logout"),
icon: Icon(Icons.arrow_back),
),
body: MovieList(),
);
}
}
class MovieList extends StatefulWidget {
@override
_MovieListState createState() => _MovieListState();
}
class _MovieListState extends State<MovieList> {
MovieBloc _movieBloc;
final MovieRepository _movieRepository = MovieRepository();
@override
void initState() {
_movieBloc = MovieBloc(movieRepository: _movieRepository);
_movieBloc.dispatch(FetchPopular(region: 'TW'));
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _movieBloc,
builder: (context, state) {
if (state is LoadingMovie) {
return Center(
child: CircularProgressIndicator(),
);
} else if (state is FailedFetchData) {
return Center(
child: Text('Failed'),
);
} else if (state is InitMovieState) {
return Center(
child: Text('Init Movie'),
);
}
return buildList(state.movieList);
},
);
}
}
Widget buildList(movieList) {
return GridView.builder(
itemCount: movieList.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${movieList.results[index].posterPath}',
fit: BoxFit.cover,
),
));
});
}
由於是第四次實作bloc了,今天迅速的完成MovieBloc的實作,並且在bloc中使用MovieRepository呼叫API取得電影清單。
以及在HomePage用簡單的GridView顯示電影海報圖片測試bloc確實有從API取得電影清單資訊。
我想現在這個時間點切入「Test」是個不錯的點,之後幾天我會開始介紹Flutter的測試框架以及如何在Flutter寫測試。
完整程式碼在這裡-> FlutTube Github