Skip to content

Commit 69db04c

Browse files
committed
Implement Gemini Chat app
1 parent d538c30 commit 69db04c

28 files changed

+782
-60
lines changed

ios/Flutter/Debug.xcconfig

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
12
#include "Generated.xcconfig"

ios/Flutter/Release.xcconfig

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
12
#include "Generated.xcconfig"

ios/Podfile

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Uncomment this line to define a global platform for your project
2+
# platform :ios, '12.0'
3+
4+
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5+
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6+
7+
project 'Runner', {
8+
'Debug' => :debug,
9+
'Profile' => :release,
10+
'Release' => :release,
11+
}
12+
13+
def flutter_root
14+
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15+
unless File.exist?(generated_xcode_build_settings_path)
16+
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17+
end
18+
19+
File.foreach(generated_xcode_build_settings_path) do |line|
20+
matches = line.match(/FLUTTER_ROOT\=(.*)/)
21+
return matches[1].strip if matches
22+
end
23+
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24+
end
25+
26+
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27+
28+
flutter_ios_podfile_setup
29+
30+
target 'Runner' do
31+
use_frameworks!
32+
use_modular_headers!
33+
34+
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35+
target 'RunnerTests' do
36+
inherit! :search_paths
37+
end
38+
end
39+
40+
post_install do |installer|
41+
installer.pods_project.targets.each do |target|
42+
flutter_additional_ios_build_settings(target)
43+
end
44+
end

lib/main.dart

+3-49
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
3-
import 'package:riverpod_annotation/riverpod_annotation.dart';
4-
part 'main.g.dart';
5-
6-
@riverpod
7-
class Counter extends _$Counter {
8-
@override
9-
int build() {
10-
return 0;
11-
}
12-
13-
increment() {
14-
return state++;
15-
}
16-
}
3+
import 'package:riverpod_gemini/screens/chat_screen.dart';
174

185
void main() {
196
runApp(const ProviderScope(child: MyApp()));
@@ -26,48 +13,15 @@ class MyApp extends StatelessWidget {
2613
@override
2714
Widget build(BuildContext context) {
2815
return MaterialApp(
29-
title: 'Riverpod Demo',
16+
title: 'Riverpod Gemini Demo',
3017
theme: ThemeData(
3118
colorScheme: ColorScheme.fromSeed(
3219
seedColor: Colors.deepPurple,
3320
brightness: Brightness.dark,
3421
),
3522
useMaterial3: true,
3623
),
37-
home: const MyHomePage(),
38-
);
39-
}
40-
}
41-
42-
class MyHomePage extends ConsumerWidget {
43-
const MyHomePage({super.key});
44-
45-
@override
46-
Widget build(BuildContext context, WidgetRef ref) {
47-
return Scaffold(
48-
appBar: AppBar(
49-
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
50-
title: const Text("Riverpod Demo"),
51-
),
52-
body: Center(
53-
child: Column(
54-
mainAxisAlignment: MainAxisAlignment.center,
55-
children: <Widget>[
56-
const Text(
57-
'You have pushed the button this many times:',
58-
),
59-
Text(
60-
ref.watch(counterProvider).toString(),
61-
style: Theme.of(context).textTheme.headlineMedium,
62-
),
63-
],
64-
),
65-
),
66-
floatingActionButton: FloatingActionButton(
67-
onPressed: () => ref.read(counterProvider.notifier).increment(),
68-
tooltip: 'Increment',
69-
child: const Icon(Icons.add),
70-
),
24+
home: const ChatScreen(title: 'Flutter + Generative AI'),
7125
);
7226
}
7327
}

lib/providers/api_key_provider.dart

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'package:riverpod_annotation/riverpod_annotation.dart';
2+
3+
part 'api_key_provider.g.dart';
4+
5+
@Riverpod(keepAlive: true)
6+
class ApiKey extends _$ApiKey {
7+
@override
8+
String? build() {
9+
return null;
10+
}
11+
12+
void setApiKey(String value) {
13+
state = value;
14+
}
15+
}

lib/main.g.dart renamed to lib/providers/api_key_provider.g.dart

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/providers/chat_provider.dart

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:google_generative_ai/google_generative_ai.dart';
2+
import 'package:riverpod_annotation/riverpod_annotation.dart';
3+
import 'package:riverpod_gemini/providers/api_key_provider.dart';
4+
5+
part 'chat_provider.g.dart';
6+
7+
@Riverpod(keepAlive: true)
8+
ChatSession chatSession(ChatSessionRef ref) {
9+
return GenerativeModel(
10+
model: 'gemini-pro',
11+
apiKey: ref.watch(apiKeyProvider) ?? '',
12+
).startChat();
13+
}
14+
15+
@riverpod
16+
class ChatMessage extends _$ChatMessage {
17+
@override
18+
Future<String> build() async {
19+
return '';
20+
}
21+
22+
Future<void> send(String text) async {
23+
state = const AsyncValue.loading();
24+
state = await AsyncValue.guard(() async {
25+
final res =
26+
await ref.read(chatSessionProvider).sendMessage(Content.text(text));
27+
if (res.text == null) {
28+
throw Exception('Empty response');
29+
}
30+
return res.text!;
31+
});
32+
if (state is! AsyncError) {
33+
ref.invalidate(chatContentsProvider);
34+
}
35+
}
36+
}
37+
38+
@riverpod
39+
List<ChatContent> chatContents(ChatContentsRef ref) {
40+
return ref
41+
.watch(chatSessionProvider)
42+
.history
43+
.map((content) => ChatContent.fromContent(content))
44+
.toList();
45+
}
46+
47+
class ChatContent {
48+
final String role;
49+
final String text;
50+
51+
ChatContent(this.role, this.text);
52+
53+
factory ChatContent.fromContent(Content content) {
54+
return ChatContent(
55+
content.role ?? '',
56+
content.parts.whereType<TextPart>().map<String>((e) => e.text).join(''),
57+
);
58+
}
59+
}

lib/providers/chat_provider.g.dart

+53
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/screens/chat_screen.dart

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:riverpod_gemini/providers/api_key_provider.dart';
4+
import 'package:riverpod_gemini/widgets/api_key_widget.dart';
5+
import 'package:riverpod_gemini/widgets/chat_input_widget.dart';
6+
import 'package:riverpod_gemini/widgets/chat_list_widget.dart';
7+
8+
class ChatScreen extends ConsumerWidget {
9+
const ChatScreen({super.key, required this.title});
10+
11+
final String title;
12+
13+
@override
14+
Widget build(BuildContext context, WidgetRef ref) {
15+
return Scaffold(
16+
appBar: AppBar(
17+
title: Text(title),
18+
),
19+
body: switch (ref.watch(apiKeyProvider)) {
20+
_? => const ChatWidget(),
21+
_ => const ApiKeyWidget(),
22+
},
23+
);
24+
}
25+
}
26+
27+
class ChatWidget extends StatelessWidget {
28+
const ChatWidget({super.key});
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
return const Padding(
33+
padding: EdgeInsets.all(8.0),
34+
child: Column(
35+
mainAxisAlignment: MainAxisAlignment.center,
36+
crossAxisAlignment: CrossAxisAlignment.start,
37+
children: [
38+
Expanded(child: ChatListWidget()),
39+
Padding(
40+
padding: EdgeInsets.symmetric(vertical: 25, horizontal: 15),
41+
child: ChatInputBoxWidget(),
42+
),
43+
],
44+
),
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)