前回の続き。
-
FlutterでTwitterクライアント作成④タイムラインをListTileで装飾
前回の続き。 今回はタイムラインの表示をTwitterっぽくします。 目次1 Flutter標準のUIを使用する1.1 tweetをリスト形式で表示する1.1.1 LinkedHashMapの追加1. ...
続きを見る
今回は結構な大改造になりました。
いろいろ手探りでやってるので仕方なし。
目次
カスタムウィジェットを作成する
前回はListTileを使いましたが、これだとタイムラインのUIを満たせないので独自ウィジェットを作成します。
ちなみにmaterial UIのソースはSDKの中に入ってるので、list_tile.dartをコピーすれば拡張できるみたいです。
でもこいつを読み解くのがしんどそうだったので今回は自作します。
最終的な完成図はこんな感じです。
※わかりやすくするために背景に色を付けてます
custom_card.dartを作成する
どうでもいい…くないんですけど、Flutterの構成ってどうしたらいいのかよくわかりませんね。
BLoCとかpackage:providerとか言いますが、これらを理解してからやろうとするとちっとも前に進まないので、今回は超我流パターンで進めています。
ということで、lib/ui/の下にcustom_card.dartを作りました。
他から呼び出されたときにカスタムウィジェットを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
import 'package:flutter/material.dart'; class CustomCard { Widget createCard(String cardId, String screenName, String profileImageUrlHttps, String text) { Widget widget; switch (cardId) { case 'text1': widget = _BaseCard(screenName, profileImageUrlHttps, text); break; default: widget = _BaseCard(screenName, profileImageUrlHttps, text); } return widget; } } class _BaseCard extends StatelessWidget { final String _screenName; final String _profileImageUrlHttps; final String _text; _BaseCard( this._screenName, this._profileImageUrlHttps, this._text, ); @override Widget build(BuildContext context) { return Card( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded( flex: 1, child: Column( children: <Widget>[ Container( color: Colors.orange[300], padding: EdgeInsets.all(4.0), child: CircleAvatar( backgroundImage: NetworkImage(_profileImageUrlHttps) ), ) ], ), ), Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( color:Colors.teal[200], child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text(_screenName), Text(_text), ], ), ) ], ), ), ], ), ); } } |
実際に叩かれるメソッド
カード用の情報をもらって、特定のカードを返すためのメソッド。
cardIdによって返すカードを変える想定です。
1 2 3 4 5 6 7 8 9 10 11 |
Widget createCard(String cardId, String screenName, String profileImageUrlHttps, String text) { Widget widget; switch (cardId) { case 'text1': widget = _BaseCard(screenName, profileImageUrlHttps, text); break; default: widget = _BaseCard(screenName, profileImageUrlHttps, text); } return widget; } |
返却されるStatelessWidget
実際に返却されるウィジェット_BaseCard。
将来的にこいつを継承していろいろ作る予定です。
引数を受け取るために、クラス変数を用意しておきます。
1 2 3 4 5 6 7 8 9 10 |
class _BaseCard extends StatelessWidget { final String _screenName; final String _profileImageUrlHttps; final String _text; _BaseCard( this._screenName, this._profileImageUrlHttps, this._text, ); |
RowとColumnを組み合わせてCardを作ってます。
インスペクターで見るとこんな感じ。
Row or Columnの中身を比率で分割するExpanded
ユーザーアイコンとテキスト部分を分けるために、ExpandedでRowの中身を1:5で分割しています。
右側はさらにColumnを入れてます。
こちらは比率指定不要なのでExpandedなし。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Expanded( flex: 1, child: Column( children: <Widget>[ Container( color: Colors.orange[300], padding: EdgeInsets.all(4.0), child: CircleAvatar( backgroundImage: NetworkImage(_profileImageUrlHttps) ), ) ], ), ), Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( color:Colors.teal[200], child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text(_screenName), Text(_text), ], ), ) ], ), ), |
ちなみに、中身に合わせて収縮するFlexibleというのもあります。
詳細はこちらの説明を参照。
子要素の並べ方を指定するAxisAlignment
RowやColumnはそのままだと全部中央寄せになってしまいます。
調整するにはmainAxisAlignmentまたはcrossAxisAlignmentを指定します。
AxisAlignmentのわかりやすい説明はこちら。
んで、これがcrossAxisAlignmentを指定しなかった場合。
- ユーザーアイコンを上に寄せたい
- screenNameを左に寄せたい
- textを左に寄せたい
ということで、それぞれcrossAxisAlignment.startを指定した結果がこう。
これでとりあえずウィジェットは完成です。
Twitter API接続用のモデルを作成する
home_route.dartの中でごちゃごちゃ処理しすぎるのもよくないので、API接続部分を切り出してtwitter_api_model.dartとしました。
home_route.dartの修正内容は後で書きます。
twitter_api_model.dartを作成する
lib/model/直下に作ります。
中身はこれだけ。
接続先(path)とパラメータ(param)をもらってAPIにGETで接続し、jsonDecodeしてから返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import 'package:twitter_1user/twitter_1user.dart'; import 'dart:convert'; class TwitterApiModel { Future GetJson(String path, Map param) async { final String apiKey = 'Twitter Developerで取得したAPI Key'; final String apiSecret = 'Twitter Developerで取得したAPI secret key'; final String accessToken = 'Twitter Developerで取得したAccess token'; final String accessSecret = 'Twitter Developerで取得したAccess token secret'; Twitter twitter = new Twitter(apiKey, apiSecret, accessToken, accessSecret); return jsonDecode(await twitter.request('GET', path, param)); } } |
カードのリストを返すモデルを作成する
home_route.dartの中でごちゃごちゃ(略)API名を指定したらカードリストを返してくれるモデルを作りました。
twitter_card_model.dartを作成する
MVVMっぽく考えたらmodelでいいはず。
ってことで、lib/model/twitter_card_model.dartを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import '../ui/custom_card.dart'; import '../model/twitter_api_model.dart'; class TwitterCardModel { Future<List> CreateCardList(String apiName) async{ List _cardList = []; final _tweets = await GetTweetsJson(apiName); CustomCard customCard = CustomCard(); for (var i = 0; i < _tweets['screenName'].length; i++) { _cardList.add(customCard.createCard( 'text1', _tweets['screenName'][i], _tweets['profileImageUrlHttps'][i], _tweets['text'][i], )); } return _cardList; } Future<Map> GetTweetsJson(String apiName) async{ String _userTimelinePath = 'statuses/home_timeline.json'; Map<String, String> _userTimelineParam; switch (apiName) { case 'home_timeline': _userTimelinePath = 'statuses/home_timeline.json'; _userTimelineParam = { 'count': '10', 'include_entities': 'true', }; break; default: _userTimelinePath = 'statuses/home_timeline.json'; _userTimelineParam = { 'count': '10', 'include_entities': 'true', }; } Map<String, List<String>> _tweets = { 'screenName':[], 'profileImageUrlHttps':[], 'text':[], }; final jsonData = await TwitterApiModel().GetJson(_userTimelinePath, _userTimelineParam); for (int i = 0; i < jsonData.length; i++){ _tweets['screenName'].add(jsonData[i]['user']['screen_name']); _tweets['profileImageUrlHttps'].add(jsonData[i]['user']['profile_image_url_https']); _tweets['text'].add(jsonData[i]['text']); } return _tweets; } } |
作成したファイル2つをインポート
先ほど作成したcustom_card.dartとtwitter_api_model.dartを追加します。
1 2 |
import '../ui/custom_card.dart'; import '../model/twitter_api_model.dart'; |
APIを指定して該当のカードリストを返すメソッド
引数で受け取ったapiNameをGetTweetsJsonに渡してAPIでデータを取得します。
さらに、取得したJsonをCustomCard().createCardに渡してカードリストを作成します。
今はカードの種類が1つしかない(text1)ですが、画像付きカードなどが増えたらここに分岐を追加してカードIDを切り替える予定です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Future<List> CreateCardList(String apiName) async{ List _cardList = []; final _tweets = await GetTweetsJson(apiName); CustomCard customCard = CustomCard(); for (var i = 0; i < _tweets['screenName'].length; i++) { _cardList.add(customCard.createCard( 'text1', _tweets['screenName'][i], _tweets['profileImageUrlHttps'][i], _tweets['text'][i], )); } return _cardList; } |
APIに接続してJsonから取得したデータを詰めて返すメソッド
Jsonの取得と必要なデータの収集をこの中で行っています。
対象がタイムラインでもツイートの検索結果でも取得する内容は同じだと思われるので、接続先とパラメータだけ変えるようにしてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
Future<Map> GetTweetsJson(String apiName) async{ String _userTimelinePath = 'statuses/home_timeline.json'; Map<String, String> _userTimelineParam; switch (apiName) { case 'home_timeline': _userTimelinePath = 'statuses/home_timeline.json'; _userTimelineParam = { 'count': '10', 'include_entities': 'true', }; break; default: _userTimelinePath = 'statuses/home_timeline.json'; _userTimelineParam = { 'count': '10', 'include_entities': 'true', }; } Map<String, List<String>> _tweets = { 'screenName':[], 'profileImageUrlHttps':[], 'text':[], }; final jsonData = await TwitterApiModel().GetJson(_userTimelinePath, _userTimelineParam); for (int i = 0; i < jsonData.length; i++){ _tweets['screenName'].add(jsonData[i]['user']['screen_name']); _tweets['profileImageUrlHttps'].add(jsonData[i]['user']['profile_image_url_https']); _tweets['text'].add(jsonData[i]['text']); } return _tweets; } |
home_route.dartを修正する
周囲をいろいろいじったので、home_route.dartも合わせます。
全体はこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import 'package:flutter/material.dart'; import '../header.dart'; import '../model/twitter_card_model.dart'; class Home extends StatefulWidget { @override _Home createState() => _Home(); } class _Home extends State<Home> { final String headerTitle = 'ホーム'; List _cardList = List<TwitterCardModel>(); @override void initState() { super.initState(); _load(); setState(() {}); } Future<void> _load() async { _cardList = await TwitterCardModel().CreateCardList('home_timeline'); } @override Widget build(BuildContext context) { return Scaffold( appBar: Header(headerTitle: headerTitle), body: FutureBuilder( future: _load(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { return ListView.builder( itemCount: _cardList.length, itemBuilder: (BuildContext context, int index) { return _cardList[index]; } ); } else { return Center(child: Container( child: CircularProgressIndicator(), ), ); } } ), ); } } |
importを整理する
いろいろと外に切り出したので、不要なものがいくつか。
以下のように修正します。
1 2 3 4 5 6 |
import 'package:flutter/material.dart'; // 削除 import 'package:twitter_1user/twitter_1user.dart'; // 削除 import 'dart:convert'; // 削除 import 'dart:collection'; import '../header.dart'; import '../model/twitter_card_model.dart'; // 追加する |
_loadでカードリストを取得する
以前は_loadの中でTwitterAPIに接続していましたが、今回はカードリストを受け取るだけ。
カードリストを受け取る変数の用意
TwitterCardModelの戻り値を受け取るための_cardListを用意。
後で代入するのでfinalはつけません。
_tweetsと_timelineDataはもういらないので削除。
1 2 3 4 5 |
class _Home extends State<Home> { final String headerTitle = 'ホーム'; // 削除 LinkedHashMap _tweets = LinkedHashMap(); // 削除 String _timelineData = ''; List _cardList = List<TwitterCardModel>(); |
initStateにsetStateを追加
以前は_load()の中で実行していたsetStateですが、外に出します。
1 2 3 4 5 6 |
@override void initState() { super.initState(); _load(); setState(() {}); // 追加 } |
_load()でCreateCardListを実行
Jsonを取得するためにやっていた処理は全部削除。
CreateCardListを実行するコードを1行だけ追加します。
この処理をsetStateの中でやろうとするとasyncがないとダメだよ!と怒られます。
なのでここでは_cardListの値だけを更新して、setStateは_load()の後で実行してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Future<void> _load() async { //ここから final String apiKey = 'Twitter Developerで取得したAPI Key'; final String apiSecret = 'Twitter Developerで取得したAPI secret key'; final String accessToken = 'Twitter Developerで取得したAccess token'; final String accessSecret = 'Twitter Developerで取得したAccess token secret'; final String userTimelinePath = 'statuses/home_timeline.json'; Twitter twitter = new Twitter(apiKey, apiSecret, accessToken, accessSecret); final res = jsonDecode(await twitter.request('GET', userTimelinePath, {'count': '30'})); setState(() { for (int i = 0; i < res.length; i++){ _tweets[i] = { 'screenName' : res[i]['user']['screen_name'], 'profileImageUrlHttps' : res[i]['user']['profile_image_url_https'], 'text' : res[i]['text'], }; } }); // ここまで削除 _cardList = await TwitterCardModel().CreateCardList('home_timeline'); } |
FutureBuilderで取得したカードを非同期で表示する
_cardListの取得は非同期なので、画面を表示した時点ではまだ描画するデータがありません。
FutureBuilderを使ってデータ取得中はローディングアイコンを表示し、取得が終わったらカードリストを描画するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@override Widget build(BuildContext context) { return Scaffold( appBar: Header(headerTitle: headerTitle), body: FutureBuilder( // ListView.builderをFutureBuilderでくるむ future: _load(), // データを取得しているメソッドを指定 builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { return ListView.builder( itemCount: _cardList.length, itemBuilder: (BuildContext context, int index) { return _cardList[index]; } ); } else { return Center(child: // ローディングアイコン Container( child: CircularProgressIndicator(), ), ); } } ), ); } |
ローディングアイコンは用意されてるものを呼ぶだけ
Flutterではローディング用ウィジェットが2種類用意されています。
- LinearProgressIndicator:線状のローディングウィジェット
- CircularProgressIndicator:円状のローディングウィジェット
画面の表示待ちはCircularProgressIndicatorかなーと思います。
ついでにheader.dartをuiに移動しておく
せっかくuiフォルダ作ったので、lib直下にあったheader.dartをlib/ui/下に移動しました。
各routesのimport部分を修正しておきます。
本当はroot.dart内のBottomNavigationBarもフッターとして切り出したい…けど、力尽きたのでまた今度。
全部できたらビルドしよう
ここまで出来たらようやく動かせます。
画像などがないのでローディングアイコンは一瞬しか出ません。
背景色を消すとこんな感じ。
なんとなーくそれっぽくなりました。
カスタマイズは続く
今回、ようやくTailListの内容に追いついたくらいですね。
ここからまだまだカスタマイズしないといけません。
長くなったので、ボタン追加したりなんだりは次回で。
-
FlutterでTwitterクライアント作成⑥タイムラインの各種ボタンを追加する
前回の続き。 コードを読み直してたら、クソ実装というか謎実装してることに気づいたので直しました。 なんでその実装にしようとしたのか全く思い出せない。 過去の自分は別の人。 目次1 CustomCard ...
続きを見る
ここまでのコードはGitHubで。