Skip to content

Commit 01acd64

Browse files
committed
v3.8.0
1. added new examples: groq_ai_ex, dio_download_ex, timeline_ex 2. add reusable class/functions: like picking a value with a dropdown btn and chat thread. 3. fix bug in firebase chatroom and IAP example 4. upgrade packages
1 parent e81c500 commit 01acd64

31 files changed

+909
-468
lines changed

Diff for: .fvm/fvm_config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"flutterSdkVersion": "stable"
2+
"flutterSdkVersion": "3.19.6"
33
}

Diff for: .fvmrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"flutter": "3.19.6"
3+
}

Diff for: .github/workflows/flutter-actions.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Set up Flutter
3535
uses: subosito/flutter-action@v2
3636
with:
37-
channel: stable
37+
channel: 3.19.6
3838
cache: true
3939

4040
- run: flutter pub get
@@ -71,7 +71,7 @@ jobs:
7171
- name: Set up Flutter
7272
uses: subosito/flutter-action@v2
7373
with:
74-
channel: stable
74+
channel: 3.19.6
7575
cache: true
7676

7777
- run: flutter pub get

Diff for: .gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,7 @@ app.*.symbols
220220
!**/ios/**/default.perspectivev3
221221
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
222222
!/dev/ci/**/Gemfile.lock
223-
!.vscode/settings.json
223+
!.vscode/settings.json
224+
225+
# FVM Version Cache
226+
.fvm/

Diff for: .vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"java.configuration.updateBuildConfiguration": "automatic",
3-
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable"
3+
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable",
4+
"dart.flutterSdkPath": ".fvm/versions/3.19.6"
45
}

Diff for: lib/main.dart

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'package:device_preview_screenshot/device_preview_screenshot.dart';
22
import 'package:firebase_core/firebase_core.dart';
33
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
4+
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
5+
import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart';
6+
import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart';
47
import 'package:flutter/foundation.dart';
58
import 'package:flutter/material.dart';
69
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -18,6 +21,13 @@ Future<void> main() async {
1821
if (kIsMobileOrWeb) {
1922
await Firebase.initializeApp(
2023
options: DefaultFirebaseOptions.currentPlatform);
24+
FirebaseUIAuth.configureProviders([
25+
GoogleProvider(
26+
clientId:
27+
'785184947614-k4q21aq3rmasodkrj5gjs9qtqtkp89tt.apps.googleusercontent.com'),
28+
EmailAuthProvider(),
29+
AppleProvider(),
30+
]);
2131
// Pass all uncaught errors from the framework to Crashlytics.
2232
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
2333
await MobileAds.instance.initialize();

Diff for: lib/my_app_routes.dart

+40-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'constants.dart';
77
import 'home_page.dart';
88
import 'my_route.dart';
99
import 'routes/about.dart';
10+
import 'routes/aiml_groq_ex.dart';
1011
import 'routes/animation_animated_builder_ex.dart';
1112
import 'routes/animation_animated_container_ex.dart';
1213
import 'routes/animation_animated_icons_ex.dart';
@@ -37,6 +38,7 @@ import 'routes/charts_new_heatmap_calendar_ex.dart';
3738
import 'routes/charts_pie_chart_ex.dart';
3839
import 'routes/charts_radar_chart_ex.dart';
3940
import 'routes/charts_time_series_ex.dart';
41+
import 'routes/charts_timelines_ex.dart';
4042
import 'routes/feature_device_preview.dart';
4143
import 'routes/feature_store_secrets.dart';
4244
import 'routes/firebase_chatroom_ex.dart';
@@ -84,7 +86,8 @@ import 'routes/nav_nav_drawer_header_ex.dart';
8486
import 'routes/nav_pageselector_ex.dart';
8587
import 'routes/nav_routes_ex.dart';
8688
import 'routes/nav_tabs_ex.dart';
87-
import 'routes/networking_chatgpt_ex.dart';
89+
import 'routes/aiml_chatgpt_ex.dart';
90+
import 'routes/networking_dio_download_ex.dart';
8891
import 'routes/networking_googlebooks_ex.dart';
8992
import 'routes/networking_hacker_news_ex.dart';
9093
import 'routes/networking_rest_api_fetch_ex.dart';
@@ -1074,6 +1077,16 @@ const kMyAppRoutesAdvanced = <MyRouteGroup>[
10741077
title: 'Radar Chart',
10751078
links: {'pub.dev': 'https://pub.dev/packages/flutter_radar_chart'},
10761079
),
1080+
MyRoute(
1081+
child: TimeLinesExample(),
1082+
sourceFilePath: 'lib/routes/charts_timelines_ex.dart',
1083+
title: 'Timelines',
1084+
description: 'Easily build UI to represent event timelines.',
1085+
links: {
1086+
'pub.dev': 'https://github.com/sawin0/timelines_plus',
1087+
'demo': 'https://chulwoo.dev/timelines/#'
1088+
},
1089+
),
10771090
],
10781091
),
10791092
MyRouteGroup(
@@ -1111,12 +1124,11 @@ const kMyAppRoutesAdvanced = <MyRouteGroup>[
11111124
links: {'Hacker News API': 'https://github.com/HackerNews/API'},
11121125
),
11131126
MyRoute(
1114-
child: ChatGptExample(),
1115-
sourceFilePath: 'lib/routes/networking_chatgpt_ex.dart',
1116-
title: "ChatGPT",
1117-
description: 'Interact with ChatGPT in Flutter',
1118-
links: {'pub.dev': 'https://pub.dev/packages/chat_gpt_sdk'},
1119-
),
1127+
child: DioDownloadExample(),
1128+
sourceFilePath: 'lib/routes/networking_dio_download_ex.dart',
1129+
title: "Dio download file",
1130+
links: {'pub.dev': 'https://pub.dev/packages/dio'},
1131+
)
11201132
],
11211133
),
11221134
MyRouteGroup(
@@ -1133,7 +1145,7 @@ const kMyAppRoutesAdvanced = <MyRouteGroup>[
11331145
},
11341146
),
11351147
MyRoute(
1136-
child: FlutterFireLoginUiExample(),
1148+
child: FirebaseAuthUiExample(),
11371149
sourceFilePath: 'lib/routes/firebase_flutterfire_loginui_ex.dart',
11381150
title: 'FlutterFire login UI',
11391151
description: 'Login/profile UI from FlutterFire, recommended.',
@@ -1164,6 +1176,12 @@ const kMyAppRoutesAdvanced = <MyRouteGroup>[
11641176
"Google I/O'17 video": 'https://www.youtube.com/watch?v=w2TcYP8qiRI',
11651177
},
11661178
),
1179+
],
1180+
),
1181+
MyRouteGroup(
1182+
groupName: 'AI/ML',
1183+
icon: Icon(Icons.auto_awesome),
1184+
routes: <MyRoute>[
11671185
MyRoute(
11681186
child: GoogleMLKitExample(),
11691187
sourceFilePath: 'lib/routes/firebase_mlkit_ex.dart',
@@ -1175,6 +1193,20 @@ const kMyAppRoutesAdvanced = <MyRouteGroup>[
11751193
'https://developers.google.com/ml-kit/vision/text-recognition',
11761194
},
11771195
),
1196+
MyRoute(
1197+
child: ChatGptExample(),
1198+
sourceFilePath: 'lib/routes/aiml_chatgpt_ex.dart',
1199+
title: "ChatGPT",
1200+
description: 'Interact with ChatGPT in Flutter',
1201+
links: {'pub.dev': 'https://pub.dev/packages/chat_gpt_sdk'},
1202+
),
1203+
MyRoute(
1204+
sourceFilePath: 'lib/routes/aiml_groq_ex.dart',
1205+
child: GroqExample(),
1206+
title: 'Groq Chat',
1207+
description: 'Chat with open-source models provided by Groq',
1208+
links: {'pub.dev': 'https://pub.dev/packages/groq'},
1209+
),
11781210
],
11791211
),
11801212
];

Diff for: lib/my_app_settings.dart

+75
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:convert';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_riverpod/flutter_riverpod.dart';
35
import 'package:path_provider/path_provider.dart';
@@ -160,3 +162,76 @@ class MyAppSettings extends ChangeNotifier {
160162
notifyListeners();
161163
}
162164
}
165+
166+
/// Extend the sharedPreference to support maps.
167+
/// TODO: examine the logic.
168+
/// TODO: can we use StateProvider to watch changes? cf. https://riverpod.dev/docs/concepts/scopes#initialization-of-synchronous-provider-for-async-apis
169+
extension SharedPreferencesMapExtension on SharedPreferences {
170+
/// Set a map in SharedPreferences
171+
Future<void> setMap(String key, Map<String, dynamic> map) async {
172+
final jsonString = jsonEncode(map);
173+
await setString(key, jsonString);
174+
}
175+
176+
/// Get a map from SharedPreferences
177+
Map<String, dynamic>? getMap(String key) {
178+
final jsonString = getString(key) ?? '{}';
179+
try {
180+
final obj = jsonDecode(jsonString);
181+
if (obj is Map<String, dynamic>) return obj;
182+
return null;
183+
} catch (e) {
184+
return null;
185+
}
186+
}
187+
188+
/// Add or update an entry in a map in SharedPreferences
189+
Future<void> updateMapEntry(String key, String mapKey, dynamic value) async {
190+
final map = getMap(key);
191+
if (map != null) {
192+
map[mapKey] = value;
193+
await setMap(key, map);
194+
}
195+
}
196+
197+
/// Remove an entry from a map in SharedPreferences
198+
Future<void> removeMapEntry(String key, String mapKey) async {
199+
final map = getMap(key);
200+
if (map != null) {
201+
map.remove(mapKey);
202+
await setMap(key, map);
203+
}
204+
}
205+
206+
/// Insert a set of key-value pairs into a map in SharedPreferences
207+
Future<void> insertMapEntries(
208+
String key, Map<String, dynamic> entries) async {
209+
final map = getMap(key);
210+
if (map != null) {
211+
map.addAll(entries);
212+
await setMap(key, map);
213+
}
214+
}
215+
216+
/// Remove a set of key-value pairs from a map in SharedPreferences
217+
Future<void> removeMapEntries(String key, Iterable<String> keys) async {
218+
final map = getMap(key);
219+
if (map != null) {
220+
for (final key in keys) {
221+
map.remove(key);
222+
}
223+
await setMap(key, map);
224+
}
225+
}
226+
227+
/// Check if a key exists in a map in SharedPreferences
228+
bool containsMapEntry(String key, String mapKey) {
229+
final map = getMap(key);
230+
return map != null && map.containsKey(mapKey);
231+
}
232+
233+
/// Clear a map in SharedPreferences
234+
Future<void> clearMap(String key) async {
235+
await setString(key, '{}');
236+
}
237+
}

Diff for: lib/my_route.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ class MyRoute extends StatelessWidget {
122122
PopupMenuButton(
123123
itemBuilder: (context) {
124124
return <PopupMenuItem>[
125-
for (final MapEntry<String, String> titleAndLink in this.links.entries)
125+
for (final MapEntry<String, String> titleAndLink
126+
in this.links.entries)
126127
PopupMenuItem(
127128
child: ListTile(
128129
title: Text(titleAndLink.key),

Diff for: lib/routes/networking_chatgpt_ex.dart renamed to lib/routes/aiml_chatgpt_ex.dart

+9-75
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
55
import 'package:tuple/tuple.dart';
66

77
import '../my_app_settings.dart';
8+
import 'aiml_groq_ex.dart';
89

910
class ChatGptExample extends ConsumerStatefulWidget {
1011
const ChatGptExample({super.key});
@@ -22,7 +23,6 @@ class _ChatGptExampleState extends ConsumerState<ChatGptExample> {
2223
enableLog: true,
2324
);
2425
final _textController = TextEditingController();
25-
final _scrollController = ScrollController();
2626
bool _pendingResponse = false;
2727
//! List of (message:string, isMe:bool) pairs to represent the conversation.
2828
final _messages = <Tuple2<String, bool>>[];
@@ -51,16 +51,13 @@ class _ChatGptExampleState extends ConsumerState<ChatGptExample> {
5151
return Column(
5252
children: [
5353
Flexible(
54-
child: Scrollbar(
55-
controller: _scrollController,
56-
thumbVisibility: true,
57-
child: ListView.builder(
58-
padding: const EdgeInsets.all(8.0),
59-
reverse: true,
60-
itemBuilder: (_, i) =>
61-
_buildMessageTile(_messages[i].item1, _messages[i].item2),
62-
itemCount: _messages.length,
63-
),
54+
child: ListView.builder(
55+
padding: const EdgeInsets.all(8.0),
56+
reverse: true,
57+
//! MyMessageBubbleTile is defined in aiml_groq_ex.dart.
58+
itemBuilder: (_, i) => MyMessageBubbleTile(
59+
message: _messages[i].item1, isMe: _messages[i].item2),
60+
itemCount: _messages.length,
6461
),
6562
),
6663
if (_pendingResponse)
@@ -73,19 +70,7 @@ class _ChatGptExampleState extends ConsumerState<ChatGptExample> {
7370
Divider(height: 1.0),
7471
_buildTextComposer(),
7572
Divider(height: 1.0),
76-
Row(
77-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
78-
children: [
79-
Text(
80-
'${ref.watch(mySettingsProvider).chatGptTurns} free turns left'),
81-
TextButton.icon(
82-
label: Text('More quota'),
83-
icon: Icon(Icons.emoji_events),
84-
onPressed: () => Navigator.of(context)
85-
.pushNamed('/monetization_rewarded_ad_ex'),
86-
),
87-
],
88-
)
73+
MyAiChatQuotaBar(),
8974
],
9075
);
9176
}
@@ -122,57 +107,6 @@ class _ChatGptExampleState extends ConsumerState<ChatGptExample> {
122107
});
123108
}
124109

125-
Widget _buildMessageTile(String message, bool isMe) {
126-
final avatar = isMe
127-
? CircleAvatar(
128-
backgroundColor: Colors.blueGrey,
129-
child: Text('Me'),
130-
)
131-
: CircleAvatar(
132-
backgroundColor: Colors.blue,
133-
child: Icon(CommunityMaterialIcons.robot),
134-
);
135-
136-
final msgBubble = Container(
137-
margin: isMe
138-
? EdgeInsets.only(
139-
top: 8.0,
140-
bottom: 8.0,
141-
left: 80.0,
142-
)
143-
: EdgeInsets.only(
144-
top: 8.0,
145-
bottom: 8.0,
146-
),
147-
padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
148-
decoration: BoxDecoration(
149-
color: isMe ? Colors.grey[300] : Colors.blue[100],
150-
borderRadius: isMe
151-
? BorderRadius.only(
152-
topLeft: Radius.circular(15.0),
153-
bottomLeft: Radius.circular(15.0),
154-
)
155-
: BorderRadius.only(
156-
topRight: Radius.circular(15.0),
157-
bottomRight: Radius.circular(15.0),
158-
),
159-
),
160-
child: Text(
161-
message,
162-
style: TextStyle(
163-
color: isMe ? Colors.black : Colors.black87,
164-
fontSize: 16.0,
165-
),
166-
textAlign: isMe ? TextAlign.right : TextAlign.left,
167-
),
168-
);
169-
return ListTile(
170-
leading: isMe ? null : avatar,
171-
trailing: isMe ? avatar : null,
172-
title: msgBubble,
173-
);
174-
}
175-
176110
Widget _buildTextComposer() {
177111
return IconTheme(
178112
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),

0 commit comments

Comments
 (0)