⚒️

【Rails / Flutter】Rails API をバックエンド、 Flutter をフロントエンドとする構成を作る Part2

2024/06/30に公開

前回に引き続き、 Rails をバックエンドに、 Flutter をフロントエンドにした構成を作ってみたいと思います。
インフラ構成に Terraform を使い、 API 通信には GraphQL を採用し、これまで単体の勉強しかしたことがなかったものを組み合わせてみることにしました。

さっそく、始めましょう!

1. DBのセットアップ

gem のインストール

まず、必要な gem をインストールします。 Gemfileに以下の行を追加します。

Gemfile
gem 'ridgepole', '~> 2.0.0' # 必須ではないが、DBマイグレーションに便利なので利用

ジェムをインストールします。

docker-compose run web bundle install

ridgepole のセットアップ

Schemafile.rb を作成し、スキーマ定義を書いていきます。

db/Schemafile.rb
# frozen_string_literal: true

create_table "habits", charset: "utf8mb4", collation: "utf8mb4_bin", force: :cascade do |t|
  t.string :name,         null: false, default: ""
  t.string :description,               default: ""

  t.datetime :created_at, null: false, precision: 6
  t.datetime :updated_at, null: false, precision: 6

  t.index :updated_at, name: "index_habits_on_updated_at"
end

ridgepole apply を使ってデータベースのマイグレーションをします。

docker-compose run web ridgepole -c config/database.yml --apply -f db/Schemafile.rb

モデルの設定

habit.rb
class Habit < ApplicationRecord
  validates :name, presence: true
  validates :description, presence: true
end

これで習慣を扱う Habit モデルが用意できました。

2. Rails 側で GraphQL を導入

GraphQL のセットアップ

まず、 Rails アプリケーションに GraphQL を導入します。
graphql 関連ジェムをインストールするために Gemfile に以下の行を追加します。

gem 'graphql', '~> 2.3.5'

group :development do
  gem 'graphiql-rails'
  gem 'sprockets-rails'
end

次に、以下のコマンドを実行してジェムをインストールします。

docker-compose run web bundle install

GraphQL スキーマの作成

次に、GraphQL スキーマを作成します。
以下のコマンドを実行して、GraphQL スキーマを生成します。

docker-compose run web rails generate graphql:install

これにより、 GraphQL スキーマファイルが生成されます。
次に、モデルと対応するタイプを作成します。
今回は、習慣を管理するアプリケーションを例に Habit モデル作ります。

docker-compose run web rails generate graphql:object Habit

生成された app/graphql/types/habit_type.rb ファイルを以下のように編集します。

# frozen_string_literal: true

module Types
  class HabitType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :description, String
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

GraphQL クエリの作成

次に、習慣一覧を取得するためのクエリを作成します。
app/graphql/types/query_type.rb ファイルを以下のように編集します。

# frozen_string_literal: true

module Types
  class QueryType < Types::BaseObject
    # すべてのHabitsを取得
    # {
    #   habits(userId: 1) {
    #     id
    #     name
    #     description
    #   }
    # }
    field :habits, [Types::HabitType], null: false

    def habits()
      Habit.all.order(:updated_at)
    end
  end
end

GraphQL エンドポイントの用意

GraphqlController を作成します。

graphql_controller.rb
# frozen_string_literal: true

class GraphqlController < ApplicationController
  # If accessing from outside this domain, nullify the session
  # This allows for outside API access while preventing CSRF attacks,
  # but you'll have to authenticate your user separately
  # protect_from_forgery with: :null_session

  def execute
    variables = prepare_variables(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      request: request
    }
    result = AppSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue StandardError => e
    raise e unless Rails.env.development?
    handle_error_in_development(e)
  end

  private

  # Handle variables in form data, JSON body, or a blank value
  def prepare_variables(variables_param)
    case variables_param
    when String
      if variables_param.present?
        JSON.parse(variables_param) || {}
      else
        {}
      end
    when Hash
      variables_param
    when ActionController::Parameters
      variables_param.to_unsafe_hash # GraphQL-Ruby will validate name and type of incoming variables.
    when nil
      {}
    else
      raise ArgumentError, "Unexpected parameter: #{variables_param}"
    end
  end

  def handle_error_in_development(e)
    logger.error e.message
    logger.error e.backtrace.join("\n")

    render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
  end
end

ルーティングも設定します。

routes.rb
post "/graphql", to: "graphql#execute"

これで /graphql に対して、クエリやミューテーションが実行できるようになりました!

GraphiQL の設定

開発環境で GraphQL をテストするために、 GraphiQL を設定します。
config/routes.rb ファイルに以下の行を追加します。

if Rails.env.development?
  mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end

これで、http://localhost:3000/graphiql から GraphiQL インターフェースにアクセスできるようになります。

GraphiQL は左の枠にクエリやミューテーションを入力すると、右に結果が出力されます。
わざわざ curl や外部ツールを使わなくていいので便利ですね。

3. Flutter プロジェクトの作成

Flutter のインストール方法

まず最初に、Flutter をインストールします。各プラットフォームに対応したインストールガイドに従ってください。

新規 Flutter プロジェクトの作成

次に、新しい Flutter プロジェクトを作成します。ターミナルを開き、以下のコマンドを実行してください。

flutter create my_flutter_app
cd my_flutter_app

これで、新しい Flutter プロジェクトが作成されました。次に、GraphQL クライアントの設定を行います。

4. GraphQL クライアントの設定

Flutter で GraphQL クライアントを使用するためのパッケージインストール

Flutter で GraphQL クライアントを使用するために、graphql_flutter パッケージをインストールします。
pubspec.yaml ファイルに以下の依存関係を追加します。

graphql_flutter: ^5.1.2

次に、以下のコマンドを実行してパッケージをインストールします。

flutter pub get

GraphQL クライアントの設定

次に、lib/main.dart ファイルを開き、 GraphQL クライアントの設定を行います。

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

void main() async {
  final HttpLink httpLink = HttpLink(
    'http://<your-rails-api-url>/graphql',
  );

  ValueNotifier<GraphQLClient> client = ValueNotifier(
    GraphQLClient(
      link: httpLink,
      cache: GraphQLCache(store: InMemoryStore()),
    ),
  );

  runApp(MyApp(client: client));
}

class MyApp extends StatelessWidget {
  final ValueNotifier<GraphQLClient> client;

  MyApp({required this.client});

  
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: MaterialApp(
        title: 'Flutter GraphQL Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomeScreen(),
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter GraphQL Demo'),
      ),
      body: Center(
        child: Text('Hello World'),
      ),
    );
  }
}

これで、GraphQL クライアントの基本設定が完了しました。

5. GraphQLクエリを搭載したページの作成

画面レイアウトの作成

HomeScreen を別ファイルに切り出して lib/home_screen.dart を作成し、以下のコードを追加します。

home_screen.dart
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  
  Widget build(BuildContext context) {
    String fetchHabits = r"""
      query FetchHabits() {
        habits() {
          id
          name
          description
        }
      }
    """;

    return Scaffold(
      appBar: AppBar(
        title: const Text('習慣トラッカー'),
        backgroundColor: Colors.blue[800],
      ),
      body: Query(
        options: QueryOptions(
          document: gql(fetchHabits),
        ),
        builder: (QueryResult result, {FetchMore? fetchMore, refetch}) {
          if (result.hasException) {
            // エラーが発生した場合にエラーメッセージを表示
            print(result.exception.toString());
            return const Center(child: Text("何らかの理由で Habit の取得に失敗しました。"));
          }

          if (result.isLoading) {
            // ロード中はインジケータを表示
            return const Center(child: CircularProgressIndicator());
          }

          // データを取得し、habits リストが空かどうかをチェック
          List habits = result.data != null && result.data!['habits'] != null ? result.data!['habits'] : [];

          if (habits.isEmpty) {
            // データが空の場合は、適切なメッセージを表示
            return const Center(child: Text('データが見つかりませんでした'));
          }

          // データが存在する場合は、ListViewを表示
          return ListView.builder(
            itemCount: habits.length,
            itemBuilder: (context, index) {
              final habit = habits[index];
              return HabitCard(habit: habit);
            },
          );
        },
      ),
    );
  }
}

class HabitCard extends StatelessWidget {
  final dynamic habit;
  const HabitCard({super.key, required this.habit});

  
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.blue[100],
          child: Icon(Icons.run_circle, color: Colors.blue[800]),
        ),
        title: Text(habit['name']),
        subtitle: const Text('Day 10 of 1'),
        trailing: Checkbox(
          value: false,
          onChanged: (bool? value) {
            // 必要に応じて何かしら処理
          },
        ),
        onTap: () {
          // 必要に応じて何かしら処理
        },
      ),
    );
  }
}

これで、基本的な画面レイアウトが完成しました。

6. Rails API との接続と動作確認

作成した Rails API との接続設定

前回の記事で作成した Rails API のエンドポイントを使用して、 Flutter アプリからデータを取得します。HttpLink の URL を、実際の Rails API の URL に置き換えます。

データ取得と表示の確認

Flutter アプリを起動し、記事一覧が正しく取得されて表示されることを確認します。
クエリが正常に動作している場合、リストビューに記事のタイトルと内容が表示されます。

7. まとめと次のステップ

この記事では、 Flutter アプリを作成し、 GraphQL を使用して Rails API バックエンドと接続する方法を紹介しました。

次の記事では、 GraphQL ミューテーションを使った、ユーザー登録などを実装していこうと思います!

Discussion