iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
1
Mobile Development

Flutter---Google推出的跨平台框架,Android、iOS一起搞定系列 第 17

【Flutter基礎概念與實作】 Day17–實作Movie Bloc

  • 分享至 

  • xImage
  •  

今天又要來實作Bloc啦,基本概念都和前面相同,就速速帶過吧。

MovieBloc

在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

Movie State

我把State分成以下幾種:

  • NowPlayingMovieState:顯示上映中電影清單
  • PopularMovieState:顯示熱門電影清單
  • TopRatedMovieState:顯示高分電影清單
  • InitMovieState:初始狀態
  • LoadingMovieState:取得電影清單中
  • FailedFetchData:遇到錯誤的狀態
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拿然後顯示出來。

Movie Event

Event只有三種:

  • FetchNowPlaying:取得上映中電影清單
  • FetchPopular:取得熱門電影清單
  • FetchTopRated:取得高分電影清單
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,之後就可以讓使用者選擇要顯示哪個國家的電影清單。

Movie Bloc

取得電影清單的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資料夾下新增「movie.dart」
貼上以下程式碼:

export 'bloc/bloc.dart';
export 'model/movie_list.dart';
export 'movie_repository.dart';

HomePage

測試一下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


上一篇
【Flutter基礎概念與實作】 Day16–使用SharedPreference記下帳號、接上TMDb API
下一篇
【Flutter基礎概念與實作】 Day18–Flutter測試框架以及Mockito Package使用範例介紹
系列文
Flutter---Google推出的跨平台框架,Android、iOS一起搞定30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言