【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜後編・ViewModelと画面の実装〜
IT技術
はじめに
この記事は、 【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜前編・API通信の実装〜 の続きとなります。
まだ前編をご覧になってない方は、そちらからご覧ください。
後編の本記事では、前編で実装した Qiita の記事一覧を取得する API を使って、 画面と API 通信を繋ぐ ViewModelの実装、そして実際の画面の実装を行っていきます!
ViewModel の実装
まず MVVM の VM にあたる、 ViewModel を実装していきましょう。
ViewModel は、画面の状態や持つべきデータ(Model)や、画面(View)側から呼び出されるデータのやり取りの処理などをまとめ、 View と Model を繋ぐ責務を持ちます。
今回実装するのは、記事一覧画面の ViewModel です。
view_model ディレクトリを作成して、その中に article_list_view_model.dart ファイルを作成し、以下のように記述しましょう。
1import 'package:flutter/widgets.dart';
2import 'package:flutter_qiita_retrofit/article.dart';
3import 'package:flutter_qiita_retrofit/article_repository.dart';
4import 'package:flutter_qiita_retrofit/article_repository_impl.dart';
5
6class ArticleListViewModel with ChangeNotifier {
7 final ArticleRepository repository = ArticleRepositoryImpl();
8 late List<Article> articles;
9
10 ArticleListViewModel() {
11 this.articles = [];
12 fetchArticles();
13 }
14
15 Future<void> fetchArticles() async {
16 await repository.fetchArticles().then((result) {
17 result.when(success: (articles) {
18 this.articles = articles;
19 notifyListeners();
20 }, failure: (error) {
21 print(error.message);
22 });
23 });
24 }
25}
15行目の fetchArticles メソッドで、前編で実装したリポジトリクラスを通じて記事一覧を取得しています。
このメソッドは、 API 通信とその結果を待つ非同期処理になるので async キーワードで宣言し、 非同期通信の結果を待つ処理に await キーワードを記述します。
返ってきたレスポンスクラスの Result が、 Success なのか Failure なのかで処理を分岐しているのがわかりますね。
このように、使う側にも記述しやすく、読む側にも通信処理がわかりやすいコードになるわけです。
さて、19行目に notifyListeners() という馴染みのないメソッドが呼ばれていますね。
この ArticleListViewModel は ChangeNotifier というクラスを継承しています。
このクラスに notifyListeners() メソッドが定義されていて、このメソッドを呼び出すことによって任意のタイミングで View 側に ViewModel が保持しているプロパティに変更があったことを通知することができます。
この通知を View 側でどう受け取るのかは、 View 側の実装で解説していきますね!
記事表示 WebView の実装
それではいよいよ画面の実装です!
「記事表示 WebView 」「記事一覧表示画面」の2つを作っていきます。
まず、記事表示 WebView を作っていきます。
WebView とは Flutter における Web ページ表示用の View です。
記事モデル(Article) には記事のURL(url)が含まれていますので、それを使ってアプリから記事の内容をブラウザ表示することができます。
ツールバーには記事タイトルと「←」ボタンが表示され、記事のリンクをブラウザ表示する画面になっています。
それでは早速作っていきましょう!
webview_flutter ライブラリの追加
Flutter で WebView を扱う際には webview_flutter という外部ライブラリが必要になるため、追加しましょう。
pubspec.yaml に以下のように記述します。
1dependencies:
2 # 〜省略〜
3 webview_flutter:
そして、お馴染みとなった以下のコマンドでライブラリをインストールします。
1flutter pub get
View の実装
view ディレクトリを作成し、 article_web_view.dart ファイルを作成して、以下のように記述しましょう。
1import 'package:flutter/material.dart';
2import 'package:flutter_qiita_retrofit/model/article.dart';
3import 'package:webview_flutter/webview_flutter.dart';
4
5class ArticleDetailWebView extends StatelessWidget {
6 final Article article;
7
8 ArticleDetailWebView({Key? key, required this.article}) : super(key: key);
9
10 @override
11 Widget build(BuildContext context) {
12 return MaterialApp(
13 home: Scaffold(
14 appBar: AppBar(
15 leading: BackButton(
16 onPressed: () {
17 Navigator.pop(context);
18 },
19 ),
20 title: Text(article.title),
21 ),
22 body: Center(
23 child: WebView(initialUrl: article.url),
24 ),
25 ),
26 );
27 }
28}
記事一覧に表示するアイテム(ListTile)の実装
記事一覧画面は ListView を使って表示しますので、リストで繰り返し表示される要素(ListTile)を実装する必要があります。
今回は、以下のような ListTile を作っていきます。
左から投稿者のプロフィール画像・記事タイトルと並ぶシンプルな構成です
view ディレクトリ内に article_list_tile.dart ファイルを作成し、以下のように記述しましょう。
1import 'package:flutter/material.dart';
2import 'package:flutter_qiita_retrofit/model/article.dart';
3import 'package:flutter_qiita_retrofit/view/article_detail_web_view.dart';
4
5class ArticleListTile extends StatelessWidget {
6 ArticleListTile({required this.article});
7
8 final Article article;
9
10 @override
11 Widget build(BuildContext context) {
12 return ListTile(
13 leading: ClipOval(
14 child: Image.network(article.user.profileImageUrl,
15 width: 44,
16 height: 44, errorBuilder: (context, exception, stacktrace) {
17 return Text('Q');
18 }),
19 ),
20 title: Text(article.title),
21 onTap: () {
22 Navigator.push(
23 context,
24 MaterialPageRoute(
25 builder: (context) => ArticleDetailWebView(article: article)));
26 },
27 );
28 }
29}
13行目の ClipOval ウィジェットは、 child で持つウィジェットを円形にして表示することができます。
14行目の Image.network() メソッドは、第1引数で指定したURLから画像を取得します。
URLがエラーなどで取得できない場合は、下の errorBuilder で代替のウィジェットを設定できます。
ListTile には onTap というタップ時の処理を記述することができ、 Navigator.push メソッドで先ほど作成した ArticleDetailWebView を新たな画面で開く仕組みになっています。
記事一覧画面(ListView)の実装
それでは、先ほど作成した ArticleListTile を利用して、記事一覧を表示する ListView を作成しましょう。
Provider パッケージの追加
ViewModel で登場した notifyListeners() メソッドは、この画面に変更の通知を送ります。
この通知を受け取るために、provider という外部パッケージを使用しますので追加します。
1dependencies:
2 # ~省略~
3 provider:
そしてもうしつこいかもしれませんが(笑)、以下のコマンドでライブラリをインストールします。
1flutter pub get
View の実装
view ディレクトリ内に、article_list_view.dart ファイルを作成して以下のように記述します。
1import 'package:flutter/material.dart';
2import 'package:flutter_qiita_retrofit/view/article_list_tile.dart';
3import 'package:flutter_qiita_retrofit/view_model/article_list_view_model.dart';
4import 'package:provider/provider.dart';
5
6class ArticleListView extends StatelessWidget {
7 final viewModel = ArticleListViewModel();
8
9 @override
10 Widget build(BuildContext context) {
11 return Scaffold(
12 appBar: AppBar(
13 title: const Text('Qiita Articles'),
14 ),
15 body: ChangeNotifierProvider(
16 create: (context) => viewModel,
17 child: Consumer<ArticleListViewModel>(
18 builder: (context, viewModel, _) {
19 return RefreshIndicator(
20 child: _buildListView(viewModel),
21 onRefresh: () => viewModel.fetchArticles());
22 },
23 ),
24 ),
25 );
26 }
27
28 Widget _buildListView(ArticleListViewModel viewModel) {
29 return ListView.separated(
30 padding: EdgeInsets.symmetric(vertical: 8),
31 itemBuilder: (context, index) {
32 return ArticleListTile(article: viewModel.articles[index]);
33 },
34 separatorBuilder: (context, index) => Divider(),
35 itemCount: viewModel.articles.length);
36 }
37}
上から順を追って説明していきますね。
1 body: ChangeNotifierProvider(
2 create: (context) => viewModel,
3 child: Consumer<ArticleListViewModel>(
4 builder: (context, viewModel, _) {
ChangeNotifierProvider は、これでウィジェットをラップすることで、create で渡した関数で返されるインスタンスの状態の変更を、child 内の Consumer に通知することができます。
あとは Consumer 内の builder でウィジェットを構成すればOKです。
これで ArticleListViewModel の notifyListeners() が呼ばれ変更が通知された際に、 Consumer 内のウィジェットが更新されるようになります。
1 return RefreshIndicator(
2 child: _buildListView(viewModel),
3 onRefresh: () => viewModel.fetchArticles());
続くコードでウィジェットの構成を書いています。
RefreshIndicator は、「画面を下に引っ張って更新」を実現するウィジェットです。
アプリなどで更新する際によく見かけると思います。
使い方は簡単で、child に表示するウィジェットを書き、onRefresh に引っ張って更新する際に行うメソッドを渡すだけです。
child のウィジェットは下の _buildListView メソッドで返すようにしているので、その中身を見ていきましょう。
1 Widget _buildListView(ArticleListViewModel viewModel) {
2 return ListView.separated(
3 padding: EdgeInsets.symmetric(vertical: 8),
4 itemBuilder: (context, index) {
5 return ArticleListTile(article: viewModel.articles[index]);
6 },
7 separatorBuilder: (context, index) => Divider(),
8 itemCount: viewModel.articles.length);
9 }
10}
ListView.separated() メソッドで ListView を生成して return しています。
これは、ListView の各要素間に設定する境界の View を separatorBuilder で設定することができるメソッドで、それ以外は基本的に ListView.builder() メソッドと同じです。
Flutter の SDK には、Divider という境界線のウィジェットがデフォルトで入っていますので、今回はそれを separatorBuilder に設定しています。
もちろん自前で境界線用のウィジェットを作り、それを設定することも可能ですよ!
アプリの起動画面設定
お疲れ様です!
これで画面の実装は全て完了しました!
ただ、このままだと起動時表示される画面はプロジェクト作成時のサンプルアプリのままなので、実装した画面を表示するように設定を行います。
既に存在している main.dart ファイルを以下のように書き換えましょう。
1import 'package:flutter/material.dart';
2import 'package:flutter_qiita_retrofit/view/article_list_view.dart';
3
4void main() {
5 runApp(MyApp());
6}
7
8class MyApp extends StatelessWidget {
9 // This widget is the root of your application.
10 @override
11 Widget build(BuildContext context) {
12 return MaterialApp(
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: ArticleListView(),
17 );
18 }
19}
これで本当の本当にお疲れ様でした!全ての作業が終了です。
それでは実際にビルドして動作を確認してみましょう!
このように動いていれば成功です!
↓のリポジトリにアプリのソースコードを置いているので、もし動かない場合は参考にしてくださいね。
https://github.com/yoshiyasuko/flutter_qiita_retrofit
さいごに
前後編にわたって、「チュートリアル終わったけどもう1ステップレベルアップしたい!」と思う初心者向けに記事を執筆してみましたがいかがでしたでしょうか?
僭越ながら「この記事に沿って読んで実装していけば Flutter の API 通信周りの基礎を学べるハンズオン」をテーマに書かせていただきました。
API 通信は、モバイルアプリ開発にあたって必須となってくる技術ですし、習得できた時に自分のレベルアップを実感しやすい分野だと思います。
これを機に、ぜひ Flutter のみならずモバイルアプリ全体に興味を持っていただいて、楽しいアプリ開発ライフを送っていただきたいと思っています!
弊社、モバイルアプリエンジニア採用強化中ですので、ぜひぜひ腕を磨いてご応募くださいませ!
ご覧いただき、ありがとうございました!
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「クレヨンしんちゃんは人生のマニュアル」が口癖なモバイルアプリとバックエンドやってる人