2024.03.15
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
はじめに
お久しぶりです!
のりさんです。
先月 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を利用してみました。
簡単に作れたので、アプリに組み込みやすそうです。
今回はテキストのみですが、画像+テキストで送信できるようなので
そのあたりも今後みてみたいと思います。
それではまた!