在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