宮沢賢治のオールナイトニッポン

アプリ開発/プログラミング学習における厄介事を垂れ流します。flutter/dart/python...

カーソルを検知してテキストに下線をつける【Flutter Web】 / how to detect mouse hover on text and then make text with underline.

EDIT : 2021/6/16

Flutter 2.2からTextSpanのパラメータにonEnter/onExitパラメータが追加されました。これでマウスホバー時のテキストの制御ができます。。。

ベンリィ 😊

stackoverflow.com


必要な情報

  • どうやってマウスホバーを検知するか?/ how to detect mouse hover on flutter web?

stackoverflow.com

ホバー検知のWidgetがリリースされたのが一ヶ月前とからしい。タイミング良かった :)

  • テキストに下線をつけるには?/ how to get text with underline?

stackoverflow.com

そしてこんなかんじで、対象のテキストへのマウスホバー時のハンドラをonEnterにあてる

マウスホバーが外れた時のハンドラはonExit

boolのコントローラーで状態管理。ホバー時にはTextDecoration.underlineをあてる。

class MouseHoverDetectableText extends HookWidget {
  const MouseHoverDetectableText(this.text);
  final String text;

  @override
  Widget build(BuildContext context) {
    final hover = useState(false);
    return GestureDetector(
      child: MouseRegion(
        onEnter: (_) => hover.value = true,
        onExit: (_) => hover.value = false,
        child: Text(
          text,
          style: TextStyle(
            decoration: hover.value ? TextDecoration.underline : null,
            color: Colors.grey,
            fontSize: 12,
          ),
        ),
      ),
      onTap: () {},
    );
  }
}

動かすとこんな感じになる 😊

FontWeight, Colorの変化などもこれ使えば簡単そうですね。

f:id:teruponn56:20210615231733g:plain

Twitter APIを触る 【Recent Search/Trends】/ dabbling with Twitter API 【Dart】

Twitter API

Twitter API docs -> https://developer.twitter.com/en/docs/twitter-api

あまり人気のないTwitter APIですが、触ってみようと思います。

今回は Recent Search/Trends の2つの機能です。

Twitter API key取得に関しては下記のブログを参考にしました。 意外とすんなり取得できたのでやってみてください!

www.itti.jp

Recent Search

ニュースアプリとかで、【Twitterの反応】みたいなロジック、よく見ますよね? こんなの。

f:id:teruponn56:20210522213926j:plain f:id:teruponn56:20210522213909j:plain

...... ところで、

みんなのガッキーがお嫁に行きましたね ;)

おめでとう!ガッキー!!!!!!!

Twitterキーワード検索のなんちゃってクライアントアプリみたいなものを作ります。

通常は

  • バックエンドでAPI叩いてデータをフロント用に整形
  • -> DB保存
  • -> リクエストを受け取りフロントに流す
  • -> 時間経過トリガーとかでバックエンド側でTwitterデータ再取得
  • -> ...

という流れだと思うのですが、今回はフロントでAPI叩いて整形しています。

センシティブなアカウントの情報を取得しようとすると、パラメータを設定しても、nullが帰ってくるようですね。;)

下記のようにほしいデータをクエリに仕込んでUriオブジェクトを作成します。ほしいデータはenumでまとめました。

userID、 tweetID、 名前、ユーザーネーム(@以下)、Tweet本文、いいね数、リツイート数、リプライ数、アイコンのurlくらいあればそれっぽく作れるはずです。

final url = Uri.https(
      'api.twitter.com',
      '/2/tweets/search/recent',
      <String, dynamic>{
        'query': searchWords,
        'expansions': RecentSearchParams.authorId.asString,
        'tweet.fields': [
          RecentSearchParams.createdAt.asString,
          RecentSearchParams.text.asString,
          RecentSearchParams.publicMetrics.asString,
        ].join(','),
        'user.fields': [
          RecentSearchParams.id.asString,
          RecentSearchParams.name.asString,
          RecentSearchParams.userName.asString,
          RecentSearchParams.profileImageUrl.asString,
        ].join(','),
        'start_time': yesterday,
      },
    );

先程のUriと認証情報を仕込んでhttp GETします。

final result = await http.get(
      url,
      headers: {'Authorization': 'Bearer ${oauthToken.accessToken}'},
    ).catchError((dynamic e) {
      print('ERROR:$e');
    });

あとはかえってきたjsonをほぐして、欲しいデータを抜き出します。

いろいろいじったところ、ざっくり下記のようになりました。

f:id:teruponn56:20210522214028g:plain

Twitter Trends by Location and Webview

Trendsのほうは設定できるパラメータはほとんどありません。 必須のパラメータはwoiedです。

  • WOIED(Where On Earth IDentifier) 32ビットの一意の参照識別子、らしいです。

これで地域を指定します。 Recent Search 同様にUriオブジェクトを作成します。パラメータに受け取ったwoiedをあてます。

final url = Uri.https(
      'api.twitter.com',
      '/1.1/trends/place.json',
      <String, dynamic>{
        'id': woied,
      },
    );

Trends画面の流れとしては、地域を選択して、上位50のトレンドを取得、タップするとそのトレンド検索の画面(webview)に移行します。

Trendsのほうはこんなかんじで、地域選択してトレンドをタップするとwebview画面に移行します。 webview機能はそこまで充実していない感じですが、表示するだけとか、表示してひと手間とかだったら、さっくりいけそうな感じはしますね :)

f:id:teruponn56:20210522214205g:plain

以上の内容はGitHubに公開してあります。:)

これから Twitter APIのマイクロプロジェクト含め、更新していくので見てやってください。:)

github.com

アプリからGDrive上にフォルダ作成、ファイルをアップロードする 【Google Drive API v3】

開発中のアプリでデータをCSV出力する機能を実装しました。出力先はGoogle Driveのアプリ専用フォルダです。

流れとしては、

  • csvString作成
  • 出力先のフォルダがGDrive上にあるか確認
  • (なければ、フォルダ作成)
  • csvファイルをGDrive上にアップロード

という感じになります。

csvStringの作成はいろいろな方法がありますが、僕はアプリ内のデータオブジェクトを下記のパッケージでcsvStringへ変換しました。 https://pub.dev/packages/csv

また、ここで記述した内容は API権限スコープ https://www.googleapis.com/auth/drive.file のもとで行っています。

フォルダがあるかチェック -> ここでいう id はファイルやフォルダ固有のIDのことです。ファイルやフォルダ作成時にAPIから返ってくるIDを保存しておき、次回以降はそのフォルダIDを指定してやり、アプリフォルダ以下にファイルを作成していきます。

アカウント変更などの影響で,IDは持っているが現在参照しているアカウント上にはフォルダが存在しない場合があるので、フォルダ名とIDでチェックします。

  Future<bool> fileExistsOnGDrive(String fileName, String id) async {
    final client = GoogleHttpClient(await googleSignInAccount.authHeaders);
    final drive = googleApis.DriveApi(client);
    await drive.files
        .list(spaces: 'drive', $fields: 'files(id)', q: 'name = $fileName')
        .then((value) {
      if (value != null && value.files.length > 0) {
        for (var file in value.files) {
          if (file.id == id) {
            return true;
          }
        }
      }
    });
    return false;
  }

listでとれる情報は上記スコープのもとでは、アプリが作成したファイル/フォルダのみになります。

list メソッドの q パラメータ作成について -> https://developers.google.com/drive/api/v3/ref-search-terms#drive_properties

アプリフォルダ作成

    final googleApisFolder = googleApis.File();
    googleApisFolder.name = csvFolderName;
    googleApisFolder.mimeType = 'application/vnd.google-apps.folder'; // フォルダとして指定
    final response = await drive.files.create(googleApisFolder);
    final String folderID = response.id;

    await setString(key, folderID); 
    // 以降は保存したIDを指定して,フォルダ以下にファイルを作成

Google DriveにおけるmimeType について -> https://developers.google.com/drive/api/v3/mime-types

アップロード

    final csvFileToUpload = googleApis.File();
    final csvFileOnLocal = File(csvPath);

    csvFileToUpload.parents = [folderID]; // 保存しておいたフォルダIDを指定する。
    csvFileToUpload.name = path.basename(csvPath);

    final response = await drive.files.create(
        csvFileToUpload,
        uploadMedia: googleApis.Media(file.openRead(), file.lengthSync()),
      );

以上がざっくりとしたフォルダ作成 -> ファイルアップロードの流れでした。 listcreateのパラメータをもっと読むと、さらに便利な使い方、リファクタが可能かと思います。

Google Drive API Docsを初めて見たときは情報量に圧倒されましたが、丁寧にわかりやすく、かつ情報を網羅しており(docsなのであたりまえ?)、非常にためになりました。

参考 :

drive.files.list -> https://developers.google.com/drive/api/v3/reference/files/list

drive.files.create -> https://developers.google.com/drive/api/v3/reference/files/create

パラメータとしてのアンダースコア (_)/ Underscore as the parameter in【Dart】

dartのコードを見ていると下記のように引数にアンダースコアが置かれている場面にちょくちょく出くわす。

...
testFunc(_) {
    ...
}
...

なにこれ?と思いつつも特に困らなかったので、というか自分で使う場面に出くわさなかったので詳しく調べなかったのだが、ブログのネタとして調べることにしました。

結論からすると、「明示的に、パラメータを使わないことを表している」ということらしい。

dart.dev

f:id:teruponn56:20210331165338p:plain

なるほど、たしかにthenメソッドとか使うときに値を受け取っても使わないことってありますよね。

明示的に、このパラメータは使わないよ、って表現したほうがわかりやすい :)

Shared Preferencesのファイルはどこにあるのか? / Where are Shared Preferences Stored in 【Flutter/Android】?

開発中のアプリにおいて、userの入力したデータをすべてひっくるめて、Google Driveにバックアップする機能を実装しようとしています。

そのなかでShared Preferencesのデータがどこに保存されているのか確認する必要がありました。調べていくと下記のSO記事を見つけました。

stackoverflow.com

いわく、/data/data/PACKAGE NAME/shared_prefs/FlutterSharedPreferences.xml Androidの場合はここにあるよ、と。確かめてみます。 適当なデータをShared Preferences経由で保存し、下記のようにデータにアクセスします。

final file = File(
        '/data/data/MY PACKAGE NAME/shared_prefs/FlutterSharedPreferences.xml');
    String contents = await file.readAsString();

    if (contents != null)
      print(
          'This is /data/data/MY PACKAGE NAME/shared_prefs/FlutterSharedPreferences.xml -> ...\n$contents');
    else
      print('its NULL');

ありました!!!StringList形式と通常のStringのデータがあります。 f:id:teruponn56:20210323131204p:plain 同様にStethoパッケージを使って、Chrome Dev Toolsでも確認をしてみます。はい、もちろん同様のデータが確認できます。StringListのデータだけ、なんか不明な文字列に変換されている仕様なんですよね。最初見たときは、なんか文字化けしてるとおもいました。 :p f:id:teruponn56:20210323131359p:plain

SHA-1 Key取得の方法 / How to get SHA-1 Key on【Win10】

※一番下にGoogle Developers公式のSHA-1 Key確認方法があります。それでつまづいたら、下記の内容を順に確認してみてください :)

FlutterとFireBase連携の際にSHA-1 Keyが必要になりました。備忘録として残します。

VScodeで開発をしているのですが、ちょっと手こずりました。 最初は下記の記事を参考に進めました。

stackoverflow.com gladlew signingReportをプロジェクトのディレクトリでRunさせて、とあります。

ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. と怒られました。

うん、JavaJDKのダウンロードをしていない。 次に見つけたのが下記のYouTube動画です。

www.youtube.com keytool -list -v -keystore C:\Users\USER NAME\.android\debug.keystore -alias androiddebugkey -storepass android -keypass android

このコマンドをC:\Users\USER NAME で走らせるようです。

しかしkeytoolコマンドが認識されません。

そんなこんなで、keytoolが認識されない問題で、下記のSO記事にたどり着きました。

stackoverflow.com C:\Program Files\Java\jdk1******\bin から走らせて、とのことです。JDKをダウンロードして、走らせます。

コマンド keytool は見つかりませんでしたが、現在の場所に存在します。Windows PowerShell は、既定では、現在の場所からコマンドを読み込みません。このコマンドを信頼する場合は、".\keytool" と 入力してください

そうします。

不正なオプション: ...NAME\.android\debug.keystore

pathが正しく認識されていません。"..."で囲います。....いけました😊

f:id:teruponn56:20210322170228p:plain
SHA-1 Key

EDIT:

FireBaseのAndroid登録の画面でTipsとしてSHA-1 Keyの確認方法が詳しく出ていました。下記のリンクです。これが一番わかり易いですね!

developers.google.com

インジケータつきのカルーセルスライダー/Carousel Slider with Indicator 【Flutter】

ルーセルスライダーにインデックスインジケータ(?)を実装したい。

開発中のカルーセルスライダーをいじる機会があったので記録に残します。 下記のパッケージはすごく充実していて、お手軽にカルーセルを実装できます。

pub.dev

f:id:teruponn56:20210321121112p:plain

上記のデモの実装は下記になります。インジケータ部分において、現在表示されていもののインデックスが黒丸で表現されています。

class _CarouselWithIndicatorState extends State<CarouselWithIndicatorDemo> {
  int _current = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Carousel with indicator demo')),
      body: Column(
          children: [
            CarouselSlider(
              items: imageSliders,
              options: CarouselOptions(
                  autoPlay: true,
                  enlargeCenterPage: true,
                  aspectRatio: 2.0,
                  onPageChanged: (index, reason) {
                    setState(() {
                      _current = index;
                    });
                  }
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: imgList.map((url) {
                int index = imgList.indexOf(url);
                return Container(
                  width: 8.0,
                  height: 8.0,
                  margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: _current == index
                        ? Color.fromRGBO(0, 0, 0, 0.9)
                        : Color.fromRGBO(0, 0, 0, 0.4),
                  ),
                );
              }).toList(),
            ),
          ]
      ),
    );
  }
}

グレーや黒の丸で表現されている上記のインジケータをいじり、インデックスの数字で表現をしたいと思います。 上記コードのインジケータ実装部ではRow()の子要素としてContainer()で丸を表現したものを返しているだけでしたが、下記のようにStackでまとめて、丸の上からIndexの数字を重ねてやります。

Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: imgList.map((url) {
            int index = imgList.indexOf(url);
            return Stack(children: [
              Container(
                width: 36,
                height: 36,
                margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 2.0),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _current == index ? Colors.blue : Colors.grey[350],
                ),
              ),
              Container(
                  margin: EdgeInsets.fromLTRB(2, 13, 2, 7),
                  width: 36,
                  height: 36,
                  child: Text('${index + 1}',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                          color: Colors.black,
                          fontSize: 24,
                          fontWeight: FontWeight.bold)))
            ]);
          }).toList(),
        )

こんなかんじで実装できました☺

f:id:teruponn56:20210601130833g:plain

下記のレポジトリにあるので、見てやってください😊 github.com