🐦

FlutterでTwitter API v2.0のFiltered Streamを使用する方法

2022/07/11に公開

概要

以前以下の記事でTwitter API v2.0Volume StreamFlutterフレームワークで簡単に使用する方法を紹介しました。

https://zenn.dev/kato_shinya/articles/how-to-use-twitter-volume-stream-in-flutter

当記事ではタイトルにもあるように、FlutterFiltered Streamを簡単に使用する方法を紹介していきます。

Filtered Streamとは

Filtered Streamは、以前別の記事で紹介したVolume Streamとよく似た、Twitter API v2.0で提供されているストリーミングエンドポイントです。

Volume Streamとの違い

しかし、Volume Streamがリアルタイムでツイートされた全てのツイートに対するストリーミングをサポートしている一方で、Filtered Streamは特定のルールを定義することで、ストリーミングで取得できるツイートをフィルタリングできる点が大きな違いになります。

この仕様により、Volume Streamでは実現できない、より特定の範囲でフィルタリングされたコンテンツへのプログラム的な操作が可能になります。

本記事を読みすすめるにあたって

  • DartFlutterに関する基本的な知識があることが望ましいです。
  • 本記事ではVSCodeを使用していきます。
  • Twitter APIを使用するためにBearerトークンTwitter Developerで取得しておいてください。

参考資料

https://www.itti.jp/web-direction/how-to-apply-for-twitter-api/

使用ライブラリ

Filtered Streamの使用方法を紹介するにあたって、以下のライブラリを使用していきます。

twitter_api_v2

  • twitter_api_v2

https://github.com/twitter-dart/twitter-api-v2

https://pub.dev/packages/twitter_api_v2

Flutterプロジェクトの作成

まずは、空のFlutterプロジェクトを作成しましょう。先の推奨事項でも触れたように本記事ではVSCodeを使用していきますが、VSCode以外のエディタを使用している方は、使用しているエディタに合わせた作成方法を優先していただいて構いません。

ひとまず、以下のようにFlutterプロジェクトを作成しました。

Flutter: New Project

VSCodeを使用していて、Flutterの拡張機能をインストールしている場合は、コマンドパレットから簡単にFlutterの空プロジェクトを生成することができます。

スクリーンショット 2022-07-10 18 34 43

テンプレート種別の選択

生成するテンプレートの種別は「Application」を選択しました。

スクリーンショット 2022-07-10 18 39 21

任意のプロジェクト名を入力

プロジェクト名はひとまず「example」としましたが、任意の名前で構いません。

スクリーンショット 2022-07-10 18 40 37

生成されたディレクトツリー

そして、上記の工程で生成された以下のディレクトリ構造を基本として、twitter_api_v2を使用してFiltered Streamの実装方法を紹介していきます。

.
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
│   ├── app
│   │   ├── build.gradle
│   │   └── src
│   │       ├── debug
│   │       │   └── AndroidManifest.xml
│   │       ├── main
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── java
│   │       │   │   └── io
│   │       │   │       └── flutter
│   │       │   │           └── plugins
│   │       │   │               └── GeneratedPluginRegistrant.java
│   │       │   ├── kotlin
│   │       │   │   └── com
│   │       │   │       └── example
│   │       │   │           └── example
│   │       │   │               └── MainActivity.kt
│   │       │   └── res
│   │       │       ├── drawable
│   │       │       │   └── launch_background.xml
│   │       │       ├── drawable-v21
│   │       │       │   └── launch_background.xml
│   │       │       ├── mipmap-hdpi
│   │       │       │   └── ic_launcher.png
│   │       │       ├── mipmap-mdpi
│   │       │       │   └── ic_launcher.png
│   │       │       ├── mipmap-xhdpi
│   │       │       │   └── ic_launcher.png
│   │       │       ├── mipmap-xxhdpi
│   │       │       │   └── ic_launcher.png
│   │       │       ├── mipmap-xxxhdpi
│   │       │       │   └── ic_launcher.png
│   │       │       ├── values
│   │       │       │   └── styles.xml
│   │       │       └── values-night
│   │       │           └── styles.xml
│   │       └── profile
│   │           └── AndroidManifest.xml
│   ├── build.gradle
│   ├── example_android.iml
│   ├── gradle
│   │   └── wrapper
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── local.properties
│   └── settings.gradle
├── example.iml
├── ios
│   ├── Flutter
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   ├── Generated.xcconfig
│   │   ├── Release.xcconfig
│   │   └── flutter_export_environment.sh
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   │   ├── AppIcon.appiconset
│   │   │   │   ├── Contents.json
│   │   │   │   ├── Icon-App-1024x1024@1x.png
│   │   │   │   ├── Icon-App-20x20@1x.png
│   │   │   │   ├── Icon-App-20x20@2x.png
│   │   │   │   ├── Icon-App-20x20@3x.png
│   │   │   │   ├── Icon-App-29x29@1x.png
│   │   │   │   ├── Icon-App-29x29@2x.png
│   │   │   │   ├── Icon-App-29x29@3x.png
│   │   │   │   ├── Icon-App-40x40@1x.png
│   │   │   │   ├── Icon-App-40x40@2x.png
│   │   │   │   ├── Icon-App-40x40@3x.png
│   │   │   │   ├── Icon-App-60x60@2x.png
│   │   │   │   ├── Icon-App-60x60@3x.png
│   │   │   │   ├── Icon-App-76x76@1x.png
│   │   │   │   ├── Icon-App-76x76@2x.png
│   │   │   │   └── Icon-App-83.5x83.5@2x.png
│   │   │   └── LaunchImage.imageset
│   │   │       ├── Contents.json
│   │   │       ├── LaunchImage.png
│   │   │       ├── LaunchImage@2x.png
│   │   │       ├── LaunchImage@3x.png
│   │   │       └── README.md
│   │   ├── Base.lproj
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── GeneratedPluginRegistrant.h
│   │   ├── GeneratedPluginRegistrant.m
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   │   ├── contents.xcworkspacedata
│   │   │   └── xcshareddata
│   │   │       ├── IDEWorkspaceChecks.plist
│   │   │       └── WorkspaceSettings.xcsettings
│   │   └── xcshareddata
│   │       └── xcschemes
│   │           └── Runner.xcscheme
│   └── Runner.xcworkspace
│       ├── contents.xcworkspacedata
│       └── xcshareddata
│           ├── IDEWorkspaceChecks.plist
│           └── WorkspaceSettings.xcsettings
├── lib
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml
└── test
    └── widget_test.dart

使用するライブラリをインストールしよう

以下のコマンドを実行して使用するライブラリのインストールを行いましょう。

pubspec.yamlに定義を追加

まず、以下のコマンドでpubspec.yamlへ依存ライブラリの定義を追加します。

flutter pub add twitter_api_v2

ライブラリのインストール

次に、以下のコマンドも実行してインストールを完了させます。

flutter pub get

インストール後の構成

ここまでの工程でプロジェクト直下にあるpubspec.yamlに以下の定義が追加されていれば成功です。

dependencies:
  flutter:
    sdk: flutter
  twitter_api_v2: ^4.0.0

使用するエンドポイントについて

ライブラリのインストールは先の工程で完了しましたが、実際に実装をしていく前に使用するエンドポイントに関する紹介をしておきます。

公式で提供されている仕様

Filtered Streamを使用する際には、公式のTwitter API v2.0から以下の3つのエンドポイントが提供されています。

エンドポイント メソッド 説明
/2/tweets/search/stream GET 任意のルールでフィルタリングされたストリームに接続します。
/2/tweets/search/stream/rules GET 定義された任意のルールを取得することができます。
/2/tweets/search/stream/rules POST フィルタリングを行うルールを追加または削除することができます。

twitter_api_v2で提供している仕様

Filtered Streamの機能を使用していくにあたって上記のエンドポイントを使用していきますが、今回使用するライブラリであるtwitter_api_v2では、以下のマトリクスにあるメソッドでそれぞれのエンドポイントをサポートしています。

エンドポイント クラス メソッド 説明
GET /2/tweets/search/stream TweetsService connectFilteredStream 任意のルールでフィルタリングされたストリームに接続します。
GET /2/tweets/search/stream/rules TweetsService lookupFilteringRules 定義された任意のルールを返却します。
POST /2/tweets/search/stream/rules TweetsService createFilteringRules フィルタリングを行うルールを追加します。
POST /2/tweets/search/stream/rules TweetsService destroyFilteringRules フィルタリングを行うルールを削除します。

上記の2つのマトリクスを比較するとわかりますが、公式で提供されているPOST /2/tweets/search/stream/rulesエンドポイントを、twitter_api_v2ではエンドポイントの役割を明確にするために追加(create)削除(destroy)に分割しています。その点が、公式で提供されているTwitter API v2.0twitter_api_v2の大きな違いになります。

フィルタリングルールを画面操作で追加してみよう

Filtered Streamを使用する際は、先に紹介したエンドポイントを使用して、あらかじめ一つ以上のフィルタリングルールを追加しておく必要があります。そのため、まずはフィルタリングルールを追加する処理を実装してみましょう。

libフォルダ直下にあるmain.dartを以下のように修正してください。

main
import 'package:flutter/material.dart';

// twitter_api_v2を使用するためには以下のimportだけで大丈夫です。
import 'package:twitter_api_v2/twitter_api_v2.dart';

void main() {
  runApp(MaterialApp(
    home: const AddRuleView(),
    theme: ThemeData(useMaterial3: true),
  ));
}

class AddRuleView extends StatefulWidget {
  const AddRuleView({Key? key}) : super(key: key);

  
  State<AddRuleView> createState() => _AddRuleViewState();
}

class _AddRuleViewState extends State<AddRuleView> {
  final _twitter = TwitterApi(bearerToken: 'Bearerトークンを渡してください。');

  final _addingRule = TextEditingController();

  String _addedRule = 'NONE';

  
  Widget build(BuildContext context) => Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(30),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              TextField(controller: _addingRule),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () async {
                  // 入力されたルールを追加します。
                  // 追加したルールデータが返却されます。
                  final result =
                      await _twitter.tweets.createFilteringRules(
                    rules: [
                      FilteringRuleParam(value: _addingRule.text),
                    ],
                  );

                  // 追加したルールのIDから再検索をして、
                  // 本当に入力したルールが登録されたのか確認してみます。
                  final addedRule =
                      await _twitter.tweets.lookupFilteringRules(
                    ruleIds: [result.data.first.id],
                  );

                  super.setState(() {
                    _addedRule = addedRule.data.first.value;
                  });
                },
                child: const Text('ルール追加'),
              ),
              const SizedBox(height: 50),
              Text(
                '追加されたルール: $_addedRule',
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
      );
}

実行する際の注意

先に実装したアプリを実行する前に、Twitter Developerで取得したBearerトークンをTwitterApiオブジェクトの引数として渡すことを忘れないでください

final _twitter = TwitterApi(bearerToken: 'Bearerトークンを渡してください。');

Flutterアプリの実行

上記の処理にBearerトークンを設定した後に、flutter runコマンドを使用してアプリを起動すると以下のような出力になります。

Simulator Screen Shot - iPhone 13 Pro Max - 2022-07-10 at 19 59 53

先に実装したこの画面では、画面中央に位置するテキストフィールドに入力したルールを「ルール追加」ボタンが押下された際に追加し、追加されたルールを画面下部のテキストに表示するようになっています。実際に、先に実装した処理が想定したとおりに機能するのか試してみましょう。

画面中央のテキストフィールドに任意の文字列を入力し、「ルール追加」ボタンを押下してください。

Simulator Screen Shot - iPhone 13 Pro Max - 2022-07-11 at 08 28 41

先の実装で画面下部に配置したTextウィジェットへ、テキストフィールドに入力した文字列が出力されれば成功です。

フィルタリングルールを画面操作で削除してみよう

先の実装でルールの追加を行えるようになりましたので、今回は逆に追加したルールを検索して削除する機能を実装してみましょう。

機能拡張するための準備

ここからは複数の画面を横断的に確認しやすいように、タブ形式で画面を作成していきます。そのため、まずは先に実装したAddRuleViewクラスをadd_rule_view.dartファイルとして分割してください。

add_rule_view.dart
import 'package:flutter/material.dart';

// twitter_api_v2を使用するためには以下のimportだけで大丈夫です。
import 'package:twitter_api_v2/twitter_api_v2.dart';

class AddRuleView extends StatefulWidget {
  const AddRuleView({Key? key}) : super(key: key);

  
  State<AddRuleView> createState() => _AddRuleViewState();
}

class _AddRuleViewState extends State<AddRuleView> {
  final _twitter = TwitterApi(bearerToken: 'Bearerトークンを渡してください。');

  final _addingRule = TextEditingController();

  String _addedRule = 'NONE';

  
  Widget build(BuildContext context) => Scaffold(
        body: Padding(
          padding: const EdgeInsets.all(30),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              TextField(controller: _addingRule),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () async {
                  // 入力されたルールを追加します。
                  // 追加したルールデータが返却されます。
                  final result =
                      await _twitter.tweets.createFilteringRules(
                    rules: [
                      FilteringRuleParam(value: _addingRule.text),
                    ],
                  );

                  // 追加したルールのIDから再検索をして、
                  // 本当に入力したルールが登録されたのか確認してみます。
                  final addedRule =
                      await _twitter.tweets.lookupFilteringRules(
                    ruleIds: [result.data.first.id],
                  );

                  super.setState(() {
                    _addedRule = addedRule.data.first.value;
                  });
                },
                child: const Text('ルール追加'),
              ),
              const SizedBox(height: 50),
              Text(
                '追加されたルール: $_addedRule',
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
        ),
      );
}

次に、main.dartを以下のように修正してください。

main
import 'package:example/add_rule_view.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: DefaultTabController(
      length: 1,
      child: Scaffold(
        appBar: AppBar(
          bottom: const TabBar(
            tabs: [
              Tab(
                child: Text(
                  'ルール追加',
                  style: TextStyle(color: Colors.black),
                ),
              ),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            AddRuleView(),
          ],
        ),
      ),
    ),
    theme: ThemeData(useMaterial3: true),
  ));
}

ここまで実装が完了したらflutter runで、実装した画面の出力を確認してみましょう。以下のように出力されれば成功です。

Simulator Screen Shot - iPhone 13 Pro Max - 2022-07-11 at 10 51 10

ルール管理画面の作成

ここまでで画面拡張をする準備ができましたので、早速追加したルールを検索して削除する機能を持つ管理画面を作成してみましょう。

manage_rule_view.dartファイルを新規作成し、そのファイルを以下のように修正してください。

manage_rule_view
import 'package:flutter/material.dart';
import 'package:twitter_api_v2/twitter_api_v2.dart';

class ManageRuleView extends StatefulWidget {
  const ManageRuleView({Key? key}) : super(key: key);

  
  State<ManageRuleView> createState() => _ManageRuleViewState();
}

class _ManageRuleViewState extends State<ManageRuleView> {
  final _twitter = TwitterApi(bearerToken: 'Bearerトークンを渡してください。');

  
  Widget build(BuildContext context) => Scaffold(
        body: Padding(
          padding: const EdgeInsets.fromLTRB(70, 30, 70, 30),
          child: FutureBuilder(
            // lookupFilteringRulesメソッドで追加済みのルールを検索します。
            // 特定のルールIDを引数として渡さない場合は全検索になります。
            future: _twitter.tweets.lookupFilteringRules(),
            builder: (_, AsyncSnapshot snapshot) {
              if (!snapshot.hasData) {
                return const Center(child: CircularProgressIndicator());
              }

              final rules = snapshot.data;

              return ListView.builder(
                itemCount: rules.data.length,
                itemBuilder: (__, int index) {
                  return ListTile(
                    title: Text(rules.data[index].value),
                    trailing: ElevatedButton(
                      onPressed: () async {
                        // destroyFilteringRulesメソッドにルールIDを引数として渡すことで、
                        // IDに紐づくルールを削除することができます。
                        await _twitter.tweets.destroyFilteringRules(
                          ruleIds: [rules.data[index].id],
                        );

                        super.setState(() {});
                      },
                      child: const Text('削除'),
                    ),
                  );
                },
              );
            },
          ),
        ),
      );
}

次に、main.dartを以下のように修正してください。

main
import 'package:example/add_rule_view.dart';
import 'package:example/manage_rule_view.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: DefaultTabController(
      // タブの数を修正するのを忘れないでください。
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          bottom: const TabBar(
            tabs: [
              Tab(
                child: Text(
                  'ルール追加',
                  style: TextStyle(color: Colors.black),
                ),
              ),
              // ルール管理のタブを追加します。
              Tab(
                child: Text(
                  'ルール管理',
                  style: TextStyle(color: Colors.black),
                ),
              ),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            AddRuleView(),
            // ルール管理画面を追加します。
            ManageRuleView(),
          ],
        ),
      ),
    ),
    theme: ThemeData(useMaterial3: true),
  ));
}

Filtered Streamに接続してみよう

さて、ここまででルールの追加削除、そして参照の方法がわかりました。
次は、先の方法で追加したルールをもとにFiltered Streamに接続する方法を紹介します。

先ほどと同じ要領でfiltered_stream_view.dartファイルを新規作成し、以下のように修正してください。

filtered_stream_view
import 'package:flutter/material.dart';
import 'package:twitter_api_v2/twitter_api_v2.dart';

class FilteredStreamView extends StatefulWidget {
  const FilteredStreamView({Key? key}) : super(key: key);

  
  State<FilteredStreamView> createState() => _FilteredStreamViewState();
}

class _FilteredStreamViewState extends State<FilteredStreamView> {
  final _twitter = TwitterApi(bearerToken:'Bearerトークンを渡してください。');

  final _tweets = <TweetData>[];

  late Future<TwitterStreamResponse<FilteredStreamResponse>> _stream;

  
  void initState() {
    super.initState();

    // 15分間に50回のリクエストのみが許可されるため、
    // あらかじめStreamを取得しておきます。
    _stream = _twitter.tweets.connectFilteredStream();
  }

  
  Widget build(BuildContext context) => Scaffold(
        body: FutureBuilder(
          future: _stream,
          builder: (_, AsyncSnapshot snapshot) {
            if (!snapshot.hasData) {
              return const Center(child: CircularProgressIndicator());
            }

            final stream = snapshot.data.stream;

            return StreamBuilder(
              // データが流れてくるたびに再描画されます。
              stream: stream,
              builder: (__, AsyncSnapshot snapshot) {
                if (!snapshot.hasData) {
                  return const Center(child: CircularProgressIndicator());
                }

                _tweets.add(snapshot.data.data);

                return ListView.builder(
                  itemCount: _tweets.length,
                  itemBuilder: (___, int index) {
                    return Card(
                      child: ListTile(
                        title: Text(_tweets[index].id),
                        subtitle: Text(_tweets[index].text),
                      ),
                    );
                  },
                );
              },
            );
          },
        ),
      );
}

次に、main.dartも以下のように修正してください。

main
import 'package:example/add_rule_view.dart';
import 'package:example/manage_rule_view.dart';
import 'package:flutter/material.dart';

import 'filtered_stream_view.dart';

void main() {
  runApp(MaterialApp(
    home: DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          bottom: const TabBar(
            tabs: [
              Tab(
                child: Text(
                  'ルール追加',
                  style: TextStyle(color: Colors.black),
                ),
              ),
              Tab(
                child: Text(
                  'ルール管理',
                  style: TextStyle(color: Colors.black),
                ),
              ),
              Tab(
                child: Text(
                  'ストリーム',
                  style: TextStyle(color: Colors.black),
                ),
              ),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            AddRuleView(),
            ManageRuleView(),
            FilteredStreamView(),
          ],
        ),
      ),
    ),
    theme: ThemeData(useMaterial3: true),
  ));
}

Flutterアプリの実行

ここまで修正が完了したら、先ほどまでの工程と同様にTwitterApiオブジェクトにBearerトークンを引数として渡すことを確認して、flutter runコマンドでFlutterアプリを起動してみましょう。

ルールを追加した状態で新しく追加した「ストリーム」タブを開くと、追加したルールでフィルタリングされたツイートを取得できていることが確認できます。

ezgif-3-12eaab07c8

最後に

ここまでの紹介で、twitter_api_v2を使用してFiltered StreamFlutterアプリケーションを統合する方法がわかったかと思います。

しかし、この記事で紹介したFiltered Streamの仕様はほんの一部分でしかなく、より高度なフィルタリングルールを作成することができます。もちろん、twitter_api_v2もこの高度な仕様をサポートしていますので、公式で提供されているフィルタリングルールのシンタックスに関しては以下のページを参考にしてください。

また、これは将来的な話になりますが、このフィルタリングルールをより簡単に構築できる仕様をtwitter_api_v2の後のリリースでサポートしていく予定です。

https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/integrate/build-a-rule

この記事で作成したFlutterアプリケーションは以下のリポジトリで公開していますので、cloneして自由に使用していただいて構いません。

https://github.com/myConsciousness/example-filtered-stream-with-flutter

貢献者の募集

twitter_api_v2オープンソースですのでどのような方でも開発に貢献することができます。開発リポジトリの公用語は英語にしていますが、日本人の方々も大歓迎ですのでお気軽にIssuePull Requestを作成してください。

また、このライブラリが役に立った場合にGitHub の開発リポジトリスターを付けることや、Pub.devいいねを付けることもよろしくお願いします。これは twitter_api_v2 の開発コミュニティを活性化するためにとても大きな意味があります。

もしなにか疑問がある場合は開発リポジトリのディスカッションにでもスレッドを立てていただければと思います。

https://github.com/twitter-dart/twitter-api-v2

https://pub.dev/packages/twitter_api_v2

スポンサーの募集

オープンソース開発をサポートしてくださるスポンサーを募集しています。少額($1)からの寄付も可能ですので、以下のリンクから是非ご支援ください。

https://github.com/sponsors/myconsciousness

また、この記事にバッジを贈っていただくことでも支援は可能です。

GitHubで編集を提案

Discussion