前回の続き。
-
FlutterでTwitterクライアント作成⑤カスタムウィジェットでListTileっぽく
前回の続き。 今回は結構な大改造になりました。 いろいろ手探りでやってるので仕方なし。 目次1 カスタムウィジェットを作成する1.1 custom_card.dartを作成する1.1.1 実際に叩かれ ...
続きを見る
コードを読み直してたら、クソ実装というか謎実装してることに気づいたので直しました。
なんでその実装にしようとしたのか全く思い出せない。
過去の自分は別の人。
CustomCard().createCardに渡すデータ形式の変更
渡すデータが増えてきたのと、何やら扱いにくい実装になってたので修正します。
_tweetsをListに変更する
元の作りがこう。
1 2 3 |
_tweets['screenName'].add(jsonData[i]['user']['screen_name']); _tweets['profileImageUrlHttps'].add(jsonData[i]['user']['profile_image_url_https']); _tweets['text'].add(jsonData[i]['text']); |
で、これを使う時はこう。
1 2 3 4 5 6 7 8 |
for (var i = 0; i < _tweets['screenName'].length; i++) { _cardList.add(customCard.createCard( 'text1', _tweets['screenName'][i], _tweets['profileImageUrlHttps'][i], _tweets['text'][i], )); } |
なんでこんなアホな作りしてんだよ!!
って自分でツッコミました。
超無駄。
ということで、リストの1要素にtweetの情報が全部入ってる状態に修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Future<List> GetTweetsJson(String apiName) async{ // 戻り値をListに変更 ・・・ List _tweets = []; // 入れ物をListに変更 for (int i = 0; i < jsonData.length; i++){ final String _timeText = ConvertText().TwitterTimetamp(jsonData[i]['created_at']); _tweets.add( // Listの中にMapを入れる { 'screenName': jsonData[i]['user']['screen_name'], 'profileImageUrlHttps': jsonData[i]['user']['profile_image_url_https'], 'text': jsonData[i]['text'], } ); } return _tweets; } |
_tweetsを使うところも修正。
_tweetsの1要素を渡すだけなのでかなりスッキリしました。
1 2 3 4 5 6 7 8 9 10 |
Future<List> CreateCardList(String apiName) async{ List _cardList = []; final _tweets = await GetTweetsJson(apiName); CustomCard customCard = CustomCard(); for (var i = 0; i < _tweets.length; i++) { // _tweetsの長さを指定 _cardList.add(customCard.createCard('text1', _tweets[i])); // tweet情報をまとめて渡す } return _cardList; } |
さらにさらに、呼び出されるcreateCardも引数をMapに変更。
_BaseCardには渡されたMapをそのまま渡すだけにします。
1 2 3 4 5 6 7 8 9 10 11 |
Widget_BaseCard createCard(String cardId, Map tweetData) { Widget widget; switch (cardId) { case 'text1': widget = _BaseCard(tweetData); break; default: widget = _BaseCard(tweetData); } return widget; } |
_BaseCard側もMapを受け取るようにして
1 2 3 4 5 6 |
class _BaseCard extends StatelessWidget { final Map _tweetData; _BaseCard( this._tweetData, ); |
実際に使う時はKeyを指定して使います。
1 2 3 4 5 6 7 8 9 10 |
@override Widget build(BuildContext context) { ・・・ Container( color: Colors.orange[300], padding: EdgeInsets.all(4.0), child: CircleAvatar( backgroundImage: NetworkImage(_tweetData['profileImageUrlHttps']) ), ・・・ |
Header部分を修正する
Headerと言ってるのはこの部分。
今はscreenNameしか表示してないので、name(id部分)、投稿時間、アローボタンを追加します。
Header用ウィジェットを作成する
_BaseCardの中に直接書くと可読性が落ちるので外に切り出します。
_Headerクラスの作成
_Headerクラスを作って_tweetDataをもらうようにします。
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 |
class _Header extends StatelessWidget { Map _tweetData; _Header( this._tweetData, ); @override Widget build(BuildContext context) { return Row( children: <Widget>[ Expanded( flex: 10, child: Row( children: <Widget>[ Flexible( flex: 10, child: Text( _tweetData['name'], style: TextStyle( fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), ), Text( ' @' + _tweetData['screenName'], style: TextStyle( color: Colors.black54, ), overflow: TextOverflow.ellipsis, ), Text( ' ' + _tweetData['timeText'], style: TextStyle( color: Colors.black54, ), overflow: TextOverflow.ellipsis, ), ], ) ), Expanded( flex: 1, child: IconButton( icon: Icon( Icons.keyboard_arrow_down ), onPressed: () {}, ), ), ], ); } } |
構造はこんな感じ。
screenNameが一番優先されるみたいなので、screenNameのみFlexibleで囲んでます。
_Headerの呼び出し
_BaseCardのbuildで_Headerを呼び出します。
Containerでくるんでわかりやすく色を付けました。
同じようにTextもContainerで色付け。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class _BaseCard extends StatelessWidget { ・・・ @override Widget build(BuildContext context) { ・・・ child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // 削除 Text(_screenName), // 削除 Text(_text), Container( // Header color: Colors.yellow[200], child: _Header(_tweetData), ), Container( // Text color: Colors.blue[200], child: Text(_tweetData['text']), ), |
_tweetsに情報を追加する
新しくname、timeTextという値を使うようにしたので、これらをAPIから取得します。
_timeTextは投稿時間によって1分とか1日とかの表示にしたいので、別クラスを作って処理します。
1 2 3 4 5 6 7 8 9 10 11 12 |
for (int i = 0; i < jsonData.length; i++){ final String _timeText = ConvertText().TwitterTimetamp(jsonData[i]['created_at']); // 追加 _tweets.add( { 'timeText': _timeText, // 追加 'name': jsonData[i]['user']['name'], // 追加 'screenName': jsonData[i]['user']['screen_name'], 'profileImageUrlHttps': jsonData[i]['user']['profile_image_url_https'], 'text': jsonData[i]['text'], } ); } |
ConvertTextクラスを作成する
utilというフォルダを作って、その下にconvert_text.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 |
import 'package:intl/intl.dart'; class ConvertText { String TwitterTimetamp(String timeText) { DateTime _nowJst = DateTime.now().toLocal(); DateTime _createdAt = DateFormat('EEE MMM d hh:mm:ss +0000 yyyy','en_US').parse(timeText, true); DateTime _createdAtJst = _createdAt.add(Duration(hours: 9)); Duration difference = _nowJst.difference(_createdAt); if (difference.inSeconds <= 59) { return '${difference.inSeconds}秒'; } if (difference.inMinutes <= 59) { return '${difference.inMinutes}分'; } if (difference.inHours <= 23) { return '${difference.inHours}時間'; } if (difference.inDays <= 6) { return '${difference.inDays}日'; } if (7 <= difference.inDays && difference.inDays <= 30) { return '${_createdAtJst.month}月${_createdAtJst.day}日'; } return '${_createdAtJst.year}年${_createdAtJst.month}月${_createdAtJst.day}日'; } } |
flutter_localizationsの追加
intl.dartはflutter_localizationsに含まれてます。
pubspec.yamlに2行追加します。
1 2 3 4 5 6 7 8 9 10 11 |
dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 http: any twitter_1user: ^1.0.1 flutter_localizations: // 追加 sdk: flutter // 追加 |
時間のparse
Twitterのcreated_atを見ると、こんな文字列が入ってます。
1 |
"created_at": "Wed Oct 10 20:19:24 +0000 2018", |
こいつをpurseしてDateTimeに変換します。
1 |
DateTime _createdAt = DateFormat('EEE MMM d hh:mm:ss +0000 yyyy','en_US').parse(timeText, true); |
+0000の部分はタイムゾーンを表すそうで。
これだとUTCですね。
リファレンスを読んだらZZZZZでいいんじゃないの?と思ったんですがなぜかエラーに。
今回は端末の設定が日本語であっても全部UTCなので、そのまま+0000としてます。
日本時間に変換
ここちょっとよくわかってないんですが。
現在時刻の日本時間をとる場合は.toLocal()でできます。
1 |
DateTime _nowJst = DateTime.now().toLocal(); |
でも、_createdAtは.toLocal()しても日本時間にならないんですよね。
しょうがないので、9時間足してやります。
1 2 3 |
DateTime _nowJst = DateTime.now().toLocal(); DateTime _createdAt = DateFormat('EEE MMM d hh:mm:ss +0000 yyyy','en_US').parse(timeText, true); DateTime _createdAtJst = _createdAt.add(Duration(hours: 9)); |
時間の差分を取って表示するテキストを設定
DartはDateTimeを直接四則演算することができません。
今回のように時間の差分をとる場合はdifferenceを使います。
ここで注意すべきなのは、differenceに使うのは9時間足した_createdAtJstを使ってはいけないということ。
_createdAtJstを使うと、differenceの結果が9時間ずれてしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Duration difference = _nowJst.difference(_createdAt); if (difference.inSeconds <= 59) { return '${difference.inSeconds}秒'; } if (difference.inMinutes <= 59) { return '${difference.inMinutes}分'; } if (difference.inHours <= 23) { return '${difference.inHours}時間'; } if (difference.inDays <= 6) { return '${difference.inDays}日'; } if (7 <= difference.inDays && difference.inDays <= 30) { return '${_createdAtJst.month}月${_createdAtJst.day}日'; } return '${_createdAtJst.year}年${_createdAtJst.month}月${_createdAtJst.day}日'; } |
年、月などはローカライズするとき用のやり方がありますが、長くなりそうなので後日。
ここまでを保存するとこんな感じ。
ここで言っているFooterはこれ。
一番右側のツイートアクティビティを取得するAPIは非公開らしいので、今回は省略します。
custom_card.dartに_FooterButtonsというクラスを追加しました。
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 |
class _FooterButtons extends StatelessWidget { Map _tweetData; _FooterButtons( this._tweetData, ); @override Widget build(BuildContext context) { return ButtonBar( buttonPadding: EdgeInsets.all(0.0), alignment: MainAxisAlignment.spaceAround, children: <Widget>[ Row( children: <Widget>[ IconButton( icon: Icon(Icons.chat_bubble_outline), iconSize: 18.0, onPressed: () {}, ), Text(''), ], ), Row( children: <Widget>[ IconButton( icon: Icon(Icons.cached), iconSize: 18.0, onPressed: () {}, ), Text(_tweetData['retweetCount'].toString()), ], ), Row( children: <Widget>[ IconButton( icon: Icon(Icons.favorite_border), iconSize: 18.0, onPressed: () {}, ), Text(_tweetData['favoriteCount'].toString()), ], ), IconButton( icon: Icon(Icons.share), iconSize: 18.0, onPressed: () {}, ), ], ); } |
ButtonBarにボタンを追加
ButtonBarはボタンを水平方向に並べるためのウィジェットです。
水平方向に余白がなければ縦に並べてくれるとか。
別にRowの中に並べてもいいんですが、せっかくなのでButtonBarを使ってみました。
でもlayoutBehaviorが効かなくてサイズの調整が全然うまくいかず。。。
とりあえずアイコンサイズをそれぞれ指定して、いいね数とリツイート数を表示できるようにTextも追加しました。
リプライ数を取得するAPIは非公開らしいです。
1 2 3 4 5 6 7 8 9 10 |
Row( children: <Widget>[ IconButton( icon: Icon(Icons.cached), iconSize: 18.0, onPressed: () {}, ), Text(_tweetData['favoriteCount'].toString()), ], ), |
_tweetsに情報を追加する
いいね数とリツイート数をAPIから取得します。
_tweetsにfavoriteCountとretweetCountを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
for (int i = 0; i < jsonData.length; i++){ final String _timeText = ConvertText().TwitterTimetamp(jsonData[i]['created_at']); _tweets.add( { 'timeText': _timeText, 'name': jsonData[i]['user']['name'], 'screenName': jsonData[i]['user']['screen_name'], 'profileImageUrlHttps': jsonData[i]['user']['profile_image_url_https'], 'text': jsonData[i]['text'], 'favoriteCount': jsonData[i]['favorite_count'], 'retweetCount': jsonData[i]['retweet_count'], } ); |
ここまでを保存するとこう。
GIGAZINEのtweetを公式で確認。
時間やいいねの数がちゃんと合ってます。
まだまだ足りないタイムライン
見た目はかなりそれっぽくなりましたが、まだ画像もリツイートも表示できません。
タイムラインってかなり作りこまれてるんですね。。。
公式とまったく同じものは作れない
リプライの数なんかがそうですが、APIが非公開になっている情報があります。
まぁ、全部公開しちゃったら公式アプリいらなくなっちゃうかもしれないですからねぇ。
画像についてはAPIで取得できるので、次回タイムラインに表示してみます。
と思ったけどいろいろ修正があったのでそっちを先に。
-
FlutterでTwitterクライアント作成⑦細かい修正いろいろ
前回の続き。 タイムラインに画像を表示する!! 予定でしたが、その前にいろいろと細かい修正が必要そうだったのでまとめて。 目次1 いいね・リツイートが0の時はラベルを空欄にする1.1 三項演算子を追加 ...
続きを見る
ここまでのコードはこちら。
※いいね数とリツイート数を逆にしてたので修正しました