COMPANY SERVICE STAFF BLOG NEWS CONTACT

STAFF BLOG

スタッフブログ

TECHNICAL

テクログ

2024.03.15

FlutterでGemini Proを使ってみた

テクログ

はじめに

お久しぶりです!
のりさんです。

先月 Flutter/Dart のアップデート(Flutter3.19・Dart3.3)があったようですね。
まだベータ版ですが「Google AI Dart SDK」も公開されたようです。

今回は Google AI Dart SDK のベータ版を使って
GoogleAIのGeminiを組み込んだFlutterのアプリを作ってみました。

環境

  • Windows10 Pro
  • Visual Studio Code
  • Flutter 3.19 / Dart 3.3

APIキーの準備

実装を始める前に
今回FlutterからGoogle AI Dart SDKを利用したいので、APIキーを準備しておきます。

① Google AI Studioにログイン
 https://aistudio.google.com/
(ログインにはGoogleアカウントが必要です)

② APIキーの取得
 今回FlutterからGoogle AI Dart SDKを利用したいので
 Google AI Studio画面にある「Get API Key」からAPIキーを作成します。

新規プロジェクトの作成

①Flutter/Dartのバージョンを最新化
 自分が使用している環境は、少し古いバージョンだったので最新版にアップデートしました。
 (今回Flutter:3.19・Dart:3.3でアプリ開発をします)

②Flutter新規プロジェクトの作成
 以下のflutterコマンドを実行してFlutterプロジェクトを作成します。
 flutter create ai_app

パッケージの追加

プロジェクト作成が終わったら、必要なパッケージを追加して実装していきます。

  • google_generative_ai:Google AI Dart SDK
  • grouped_list:グループ化リスト
  • flutter_spinkit:待ち時間アニメーション
  • intl:日付書式設定

flutterコマンド:
 flutter pub add google_generative_ai grouped_list flutter_spinkit intl

完成アプリ

完成したアプリは以下のようになりました!

下部にあるテキストボックスに質問を入力して送信することで
Gemini Proと会話ができるようになりました!

それではソースコードをみていきたいと思います。

ソースコード

今回作成したファイルは以下3つです。

  • chat_message.dart:チャットメッセージ
  • chat_user.dart:チャットユーザー
  • main.dart:アプリ ※APIキー部分は要設定

①lib/chat_message.dartを作成します

import 'package:flutter/material.dart';
import 'package:sample/chat_user.dart';

@immutable
class ChatMessage implements Comparable {
  final ChatUser user;
  final Widget message;
  final DateTime sendDate;

  const ChatMessage({
    required this.user,
    required this.message,
    required this.sendDate,
  });

  @override
  int compareTo(other) {
    return sendDate.compareTo(other.sendDate);
  }
}

②lib/chat_user.dartを作成します

import 'package:flutter/material.dart';

@immutable
class ChatUser {
  final String name;
  final Color color;
  final Color messageColor;
  final Color messageBackgroundColor;
  final Icon? icon;

  const ChatUser({
    required this.name,
    required this.color,
    required this.messageColor,
    required this.messageBackgroundColor,
    this.icon,

  });
}

③lib/main.dartを修正します ※要APIキー設定

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:grouped_list/grouped_list.dart';
import 'package:intl/intl.dart';
import 'package:sample/chat_message.dart';
import 'package:sample/chat_user.dart';
import 'package:google_generative_ai/google_generative_ai.dart';

void main() {
  runApp(const MyApp());
}

const apiKey = "【ここにAPIキーを設定します】";

const myUser = ChatUser(
  name: "ユーザー",
  color: Colors.white,
  messageColor: Colors.black,
  messageBackgroundColor: Color.fromARGB(255, 251, 255, 214),
  icon: Icon(Icons.person, color: Color.fromARGB(255, 56, 143, 82))
);
const partnerUser = ChatUser(
  name: "Gemini Pro",
  color: Colors.white,
  messageColor: Colors.black,
  messageBackgroundColor: Color.fromARGB(255, 239, 251, 248),
  icon: Icon(Icons.person, color: Color.fromARGB(189, 52, 132, 236))
);

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _scrollController = ScrollController();
  final _textController = TextEditingController();
  late ChatSession _chat;
  late GenerativeModel _model;

  @override
  void initState() {
    _model = GenerativeModel(
        model: 'gemini-pro',
        apiKey: apiKey,
    );
    _chat = _model.startChat();
    super.initState();
  }

  final List<ChatMessage> _messages = [];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Google AI Dart SDK (Gemini Pro)',
      theme: ThemeData(primarySwatch: Colors.red, canvasColor: Colors.transparent),
      home: Scaffold(
        appBar: AppBar(title: const Text('Google AI Dart SDK (Gemini Pro)')),
        backgroundColor: const Color.fromARGB(189, 52, 132, 236),
        body: Column(
          children: [
            Expanded(
              child: Scrollbar(
                controller: _scrollController,
                thumbVisibility: true,
                child: GroupedListView<ChatMessage, DateTime>(
                    controller: _scrollController,
                    elements: _messages,
                    order: GroupedListOrder.DESC,
                    sort: true,
                    reverse: true,
                    floatingHeader: true,
                    useStickyGroupSeparators: true,
                    padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
                    groupBy: (ChatMessage m) => DateTime(m.sendDate.year, m.sendDate.month, m.sendDate.day),
                    groupHeaderBuilder: (ChatMessage m) {
                      return Row(
                        crossAxisAlignment: CrossAxisAlignment.center,
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: DecoratedBox(
                                decoration: const BoxDecoration(color: Color.fromARGB(190, 18, 87, 177), borderRadius: BorderRadius.all(Radius.circular(8.0))),
                                child: Padding(
                                  padding:const EdgeInsets.all(6),
                                  child: Text(
                                    DateFormat('yyyy年MM月dd日').format(m.sendDate),
                                    textAlign: TextAlign.center,
                                    style: const TextStyle(color: Colors.white, fontSize: 12),
                                  ),
                                ),
                              ),
                          )
                        ]
                        );
                    },
                    itemBuilder: (context, m) {
                      var isPartner = (m.user == partnerUser);
                      return Row(
                        mainAxisAlignment: isPartner ? MainAxisAlignment.start : MainAxisAlignment.end,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                            Column(                           
                            mainAxisSize: MainAxisSize.min,
                            crossAxisAlignment: isPartner ? CrossAxisAlignment.start : CrossAxisAlignment.end,
                            children: [
                              Row(
                                children: [
                                  if(m.user.icon != null) CircleAvatar(radius: 10, backgroundColor: Colors.white, child: Icon(m.user.icon!.icon, size: 16, color: m.user.icon!.color)),
                                  Padding(padding: const EdgeInsets.only(left: 3, right: 3), child: Text(m.user.name, style: TextStyle(color: m.user.color))),
                                ],
                              ),
                              Row(
                                crossAxisAlignment: CrossAxisAlignment.end,
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  Padding(
                                    padding: const EdgeInsets.all(8.0),
                                    child: Card(
                                          elevation: 3,
                                          color: m.user.messageBackgroundColor,
                                          shadowColor: Colors.black45,
                                          shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(18.0))),
                                          margin: const EdgeInsets.symmetric(horizontal:0, vertical: 0),
                                          child: Padding(
                                            padding: const EdgeInsets.only(top: 6.0, bottom: 6.0, left: 15.0, right: 15.0),
                                            child: m.message,
                                          ),
                                        ),
                                  ),
                                ],
                              ),
                            ],
                          ),
                        ],
                      );
                    },
                )
              ),
            ),

            Container(
              color: Colors.grey[200],
              padding: const EdgeInsets.only(bottom: 15, left: 50, right: 50, top: 15),
              child: TextField(
                    controller: _textController,
                    minLines: 1,
                    maxLines: 5,
                    decoration: InputDecoration(
                      filled: true,
                      fillColor: Colors.white,
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                      border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24)),
                      focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(24)), borderSide: BorderSide(color: Colors.blue, width: 2)),
                      suffixIcon: IconButton(
                        onPressed: () async {
                          if (_textController.text.isEmpty) return;
                          final sendText = _textController.text;
                          setState(() {
                            final message = ConstrainedBox(
                              constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 100),
                              child: SelectableText(sendText)
                            );
                            _messages.add(ChatMessage(user: myUser, message: message, sendDate: DateTime.now()));
                            _textController.clear();
                          });
                          await Future.delayed(const Duration(milliseconds: 100));
                          setState(() {
                            final spinkit = SpinKitThreeBounce(
                              itemBuilder: (context, index) => const DecoratedBox(decoration: BoxDecoration(color: Color.fromARGB(255, 28, 108, 212))),
                              size: 16,
                            );
                            final futureBuilder = FutureBuilder(
                              builder: (context, snapshot) {
                                if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
                                  return ConstrainedBox(
                                    constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 100),
                                    child: SelectableText(snapshot.data!)
                                  );
                                }
                                if (snapshot.connectionState == ConnectionState.waiting) {
                                  return spinkit;
                                }
                                if (snapshot.hasError) {
                                  return Text("エラーが発生しました(${snapshot.error!})");
                                }
                                return const Text("エラーが発生しました");
                              },
                              future: Future.delayed(const Duration(milliseconds: 500), () async {
                                final response = await _chat.sendMessage(Content.text(sendText));
                                return response.text ?? "エラーが発生しました"; //
                              }),
                            );
                            _messages.add(ChatMessage(user: partnerUser, message: futureBuilder, sendDate: DateTime.now()));
                          });
                        },
                        icon: const Icon(Icons.send, color: Colors.blue),
                      ),
                    ),
                  ),
              ),
          ],
        ),
      ),
    );
  }
}

おわりに

今回はFlutterからgemini proを利用してみました。
簡単に作れたので、アプリに組み込みやすそうです。

今回はテキストのみですが、画像+テキストで送信できるようなので
そのあたりも今後みてみたいと思います。

それではまた!

この記事を書いた人

のりさん

入社年2014年

出身地東京都

業務内容開発

特技・趣味水泳・筋トレ・旅行

テクログに関する記事一覧

TOP