iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 16
1
Mobile Development

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

【Flutter基礎概念與實作】 Day16–使用SharedPreference記下帳號、接上TMDb API

今天來實作接上movie API,取得近期上映電影清單。不過在開始之前我們先幫LoignForm加一個簡單的功能——記錄使用者帳號。

SharedPreference

sharedPreference能將簡單的資料存放在硬碟內,採用key-value的方式做存取。
在Flutter要使用sharedPreference非常簡單,首先先下載「shared_preferences」套件。
在pubspec.yaml dependencies處添加 shared_preferences: ^0.5.3+4安裝最新版本。

LoginForm

要增加的程式碼並不多。

  1. 引入shared_preferences套件
    import 'package:shared_preferences/shared_preferences.dart';
  2. 在_LoginFormState增加區域變數,紀錄checkbox狀態。
    bool _rememberAccount;
  3. 新增以下四個method,用來存取sharedPreferences內的值
_loadCheckbox() async{
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _rememberAccount = prefs.getBool('remember_account') ?? false;
    });
  }

  _saveCheckout(value) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool('remember_account', value);
  }

  _loadAccount() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    setState(() {
      _emailController.text = prefs.getString('account') ?? "";
    });
  }

  _saveAccount(account) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString('account', account);
  }
  1. 在initState()取得先前的紀錄資料
void initState() {
    _loginBloc = BlocProvider.of<LoginBloc>(context);
    _emailController.addListener(_onEmailChanged);
    _passwordController.addListener(_onPasswordChanged);
    _loadCheckbox();
    _loadAccount();
    super.initState();
  }
  1. 增加CheckboxListTile在密碼輸入欄後,讓使用者勾選是否要記錄他的帳號
CheckboxListTile(
            value: _rememberAccount ?? false,
            onChanged: (value){
                setState(() {
                    _rememberAccount = value;
                });
                _saveCheckout(value);
            },
            title: Text('記住我的帳號'),
            controlAffinity: ListTileControlAffinity.leading,
)
  1. 在BlocListener的「isSuccess」處加上以下程式碼,當checkbox被勾選就把信箱紀錄起來。
if (state.isSuccess) {
          if(_rememberAccount)
            _saveAccount(_emailController.text);
          BlocProvider.of<AuthenticationBloc>(context).dispatch(LoggedIn());
        }

The Movie Database

處理完記錄帳號的功能,回到今天的正題來串接TMDb API吧,TMDb的API是免費的除了提供電影資訊外它也提供名人和TV Show的資料可以使用。

註冊帳號

到Settings/API申請API Key

測試你的API Key

在瀏覽器網址輸入https://api.themoviedb.org/3/movie/now_playing?api_key=[Your API KEY]&page=1&region=TW,把你的API Key替換進去,應該會看到如下圖的畫面,這些就是TMDb回傳的資料,我們在App裡面就要轉換它們並存成Movie的物件。

如果你覺得這樣看太亂不舒服,可以使用Firefox測試,Firefox偵測到是json格式就會自動的整理排版。

API Document

可以到TMDb的API Document查看如何使用他們的API,也能直接在網頁上進行測試,測試好直接複製它們產生的網址之後就可以使用網址取得資料。
這次會使用到以下幾種電影清單類型:

可以先在網頁上玩看看~

MovieList

新增「movie」資料夾在裡面新增「model」資料夾,最後新增「movie_list.dart」。
路徑:lib/movie/model/movie_list.dart
這裡我們要自己建立MovieList類別,還沒有class概念的人可以參考Day5的教學。

class MovieList {
  int _page;
  int _totalResults;
  int _totalPages;
  List<_Item> _results = [];

  MovieList.fromJson(Map<String, dynamic> parsedJson) {
    _page = parsedJson['page'];
    _totalPages = parsedJson['total_pages'];
    _totalResults = parsedJson['total_results'];

    List<_Item> temp = [];
    for (int i=0; i < parsedJson['results'].length; i++){
      _Item item = _Item(parsedJson['results'][i]);
      temp.add(item);
    }
    _results = temp;
  }

  List<_Item> get movieList => _results;
  int get totalPages => _totalPages;
  int get totalResults => _totalResults;
  int get page => _page;
}

class _Item {
  var _popularity;
  int _voteCount;
  int _id;
  bool _video;
  var _voteAverage;
  String _title;
  String _posterPath;
  String _backdropPath;
  String _language;
  String _overview;
  String _releaseDate;

  _Item(result) {
    _popularity = result['popularity'];
    _voteCount = result['vote_count'];
    _id = result['id'];
    _video = result['video'];
    _voteAverage = result['vote_average'] + 0.0;
    _title = result['title'];
    _posterPath = result['poster_path'];
    _backdropPath = result['backdrop_path'];
    _language = result['original_language'];
    _overview = result['overview'];
    _releaseDate = result['release_date'];
  }

  String get popularity => _popularity;
  int get voteCount => _voteCount;
  int get id => _id;
  bool get video => _video;
  double get voteAverage => _voteAverage;
  String get title => _title;
  String get posterPath => _posterPath;
  String get backdropPath => _backdropPath;
  String get language => _language;
  String get overview => _overview;
  String get releaseDate => _releaseDate;
}

雖然程式碼很長,不過概念很簡單,在這個檔案中我們創造兩個class:

  • MovieList:解析從API取得的Movie List Json data
  • _item:解析單部電影的Json data

還記得Day5提過的name constructor嗎?
MovieList.fromJson(Map<String, dynamic> parsedJson)就是一個name constructor,它能接收從Json data decode來的Map object用來初始化MovieList Object。

Movie Api Provider

在movie資料夾下新增movie_api_provider.dart

由於在這個檔案中我們要使用到http來呼叫IMDb API,所以先到pubspec.yaml增加「http」套件的引用

http: ^0.12.0+2

下載好後來看程式碼吧。

import 'package:http/http.dart';
import 'dart:async';
import 'dart:convert';
import 'model/movie_list.dart';
import 'movie_api_key.dart';

class MovieApiProvider {
  final Client _client;
  String _apiKey = movie_api_key;
  final _baseUrl = 'http://api.themoviedb.org/3/movie';
  MovieApiProvider({Client client})
      : _client = client ?? Client();

  Future<MovieList> fetchPopularMovieList({String region = 'TW'}) async {
    final response = await _client
        .get("$_baseUrl/popular?api_key=$_apiKey&page=1&region=$region");
    if (response.statusCode == 200) {
      return MovieList.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to get popular movie list.');
    }
  }

  Future<MovieList> fetchNowPlayingMovieList({String region = 'TW'}) async {
    final response = await _client
        .get("$_baseUrl/now_playing?api_key=$_apiKey&page=1&region=$region");
    if (response.statusCode == 200) {
      return MovieList.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to get now playing movie list.');
    }
  }

  Future<MovieList> fetchTopRatedMovieList({String region = 'TW'}) async {
    final response = await _client
        .get("$_baseUrl/top_rated?api_key=$_apiKey&page=1&region=$region");
    if (response.statusCode == 200) {
      return MovieList.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to get top rated movie list.');
    }
  }
}

你的api_key可以像我一樣放在另一個檔案「movie_api_key.dart」,裡面只有一行程式碼const String movie_api_key = "your_api_key";

建構子需要client參數是為了之後測試方便才加的,目前先不用理會它。

這3個method使用http的Get傳送Request和Server索取前面提到的「Now Playing」、「Popular」和「Top Rated」電影清單。接收到Response後確認statusCode,假如是200代表成功取得資料。
其他statusCode對應的訊息可以在這裡查詢。

Movie Repository

在movie資料夾下新增movie_repository.dart

import 'dart:async';
import 'movie_api_provider.dart';
import 'model/movie_list.dart';

class MovieRepository {
  final _movieApiProvider = MovieApiProvider();

  Future<MovieList> fetchPopularMovieList({String region = 'TW'}) =>
      _movieApiProvider.fetchPopularMovieList(region: region);

  Future<MovieList> fetchNowPlayingMovieList({String region = 'TW'}) =>
      _movieApiProvider.fetchNowPlayingMovieList(region: region);

  Future<MovieList> fetchTopRatedMovieList({String region = 'TW'}) =>
      _movieApiProvider.fetchTopRatedMovieList(region: region);
}

其實只是多包裝一層Repository class,這是因為使用MovieRepository的人不需要知道我們是如何呼叫TMDb API的。

之後實作的MovieBloc如果要使用到Movie API就直接呼叫MovieRepository而不是MovieApiProvider。

今天的目錄架構

fluttube
└───lib
│   └───movie
│   │   └───model
│   │   │   └───movie_list.dart
│   │   └───movie_api_provider.dart
│   │   └───movie_repository.dart
│   └───main.dart
│   ...
│   └─── validators.dart

今日總結

今天前半部幫登入介面新增了記錄帳號的功能,也順便學習如何使用SharedPreference套件(雖然也可以使用SharedPreference來記錄使用者的密碼,不過礙於安全性就不推薦了)。

後半部認識了TMDb API該如何設定和使用。根據回傳的Json Data建立了MovieList Class。
並在App中新增了MovieApiProvider和MovieRepository來串接API,之後就能直接使用MovieRepository內的method取得MovieList。

明天老樣子來實作MovieBloc吧,這已經是實作的第四個Bloc應該很熟悉了迅速完成它吧。

完整程式碼在這裡-> FlutTube Github


上一篇
【Flutter基礎概念與實作】 Day15–實作Register Bloc、Firebase Authentication
下一篇
【Flutter基礎概念與實作】 Day17–實作Movie Bloc
系列文
Flutter---Google推出的跨平台框架,Android、iOS一起搞定30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
blantt
iT邦新手 5 級 ‧ 2020-11-04 18:37:54

您好:
我覺得您的專案看起很棒,想研究一下,
請問目前一個地方在 movie_api_key.dart,
專案說找不到這個dart,請問這來源是什麼呢? 好像也不是外來的

文章中有提到呦

你的api_key可以像我一樣放在另一個檔案「movie_api_key.dart」,裡面只有一行程式碼const String movie_api_key = "your_api_key";

blantt iT邦新手 5 級 ‧ 2020-11-05 16:56:34 檢舉

可以嘍!! 感謝!

我要留言

立即登入留言