Last active
November 17, 2025 00:02
-
-
Save romanejaquez/0317cb3f930181fe9018831b4a4e8ea7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:convert'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:finger_painter/finger_painter.dart'; | |
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | |
| import 'package:firebase_core/firebase_core.dart'; | |
| import 'package:firebase_ai/firebase_ai.dart'; | |
| import 'package:flutter_riverpod/legacy.dart'; | |
| import 'package:myapp/firebase_options.dart'; | |
| import 'dart:ui' as ui; | |
| // root widget | |
| void main() async { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| // initialize Firebase | |
| await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); | |
| runApp(const ProviderScope(child: GeminiPictionaryApp())); | |
| } | |
| class GeminiPictionaryApp extends StatelessWidget { | |
| const GeminiPictionaryApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const MaterialApp(home: GeminiPictionaryHome()); | |
| } | |
| } | |
| class GeminiPictionaryHome extends StatelessWidget { | |
| const GeminiPictionaryHome({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar( | |
| backgroundColor: Colors.white, | |
| elevation: 0, | |
| centerTitle: true, | |
| title: const Row( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| Icon(Icons.collections, color: Colors.purple), | |
| SizedBox(width: 16), | |
| Text('Pictionary!', style: TextStyle(color: Colors.purple)), | |
| ], | |
| ), | |
| ), | |
| body: const Center( | |
| child: Column( | |
| children: [ | |
| PaintingSurface(), | |
| PaintingValidationControls(), | |
| PaintingValidator(), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| final painterControllerProvider = Provider<PainterController>((ref) { | |
| return PainterController() | |
| ..setPenType(PenType.paintbrush) | |
| ..setStrokeColor(Colors.black) | |
| ..setMinStrokeWidth(3) | |
| ..setMaxStrokeWidth(10) | |
| ..setBlurSigma(0.0) | |
| ..setBlendMode(ui.BlendMode.srcOver); | |
| }); | |
| class PaintingSurface extends ConsumerStatefulWidget { | |
| const PaintingSurface({super.key}); | |
| @override | |
| ConsumerState<PaintingSurface> createState() => _PaintingSurfaceState(); | |
| } | |
| class _PaintingSurfaceState extends ConsumerState<PaintingSurface> { | |
| late PainterController painterController; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| initializePainterController(); | |
| } | |
| void initializePainterController() async { | |
| painterController = ref.read(painterControllerProvider); | |
| ByteData bgImg = await rootBundle.load('assets/bgimg.png'); | |
| painterController.setBackgroundImage(bgImg.buffer.asUint8List()); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Painter( | |
| controller: painterController, | |
| backgroundColor: Colors.white, | |
| size: Size(MediaQuery.sizeOf(context).width, 300), | |
| child: const ColoredBox(color: Colors.white), | |
| ); | |
| } | |
| } | |
| class PaintingValidationControls extends ConsumerWidget { | |
| const PaintingValidationControls({super.key}); | |
| @override | |
| Widget build(BuildContext context, WidgetRef ref) { | |
| return Padding( | |
| padding: const EdgeInsets.all(24), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| ElevatedButton.icon( | |
| onPressed: () async { | |
| await ref.read(paintingServiceProvider).validatePicture(); | |
| }, | |
| icon: const Icon(Icons.image), | |
| label: const Text('Validate Picture'), | |
| ), | |
| const SizedBox(width: 24), | |
| ElevatedButton.icon( | |
| icon: const Icon(Icons.clear), | |
| onPressed: () async { | |
| ref.read(paintingServiceProvider).clear(); | |
| }, | |
| label: const Text('Clear'), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class ImageResultPayload { | |
| final String name; | |
| final Uint8List imgBytes; | |
| const ImageResultPayload({required this.name, required this.imgBytes}); | |
| } | |
| final imageResultNotifier = StateProvider<ImageResultPayload?>((ref) => null); | |
| class GeminiPaintingService { | |
| final Ref ref; | |
| const GeminiPaintingService(this.ref); | |
| Future<void> validatePicture() async { | |
| try { | |
| // get the drawn image as as sequence of bytes | |
| Uint8List imgBytes = ref.read(painterControllerProvider).getImageBytes()!; | |
| // create an instance of type GenerativeModel through the Firebase AI Logic SDK | |
| // specifying the type of model and type of response expected from Gemini | |
| final googleAIInstance = FirebaseAI.googleAI(); | |
| GenerativeModel model = googleAIInstance.generativeModel( | |
| model: 'gemini-2.5-flash', | |
| generationConfig: GenerationConfig( | |
| responseMimeType: 'application/json', | |
| responseSchema: Schema.object( | |
| properties: {"guessedImage": Schema.string()}, | |
| ), | |
| ), | |
| ); | |
| // assemble a message that contains | |
| // that contains a TextPart y un InlineDataPart, with a mimeType of 'image/png' | |
| final content = Content.multi([ | |
| // text part (prompt) | |
| TextPart( | |
| ''' | |
| You are a Pictionary player. Given the provided image, identify its content | |
| and respond in JSON format with the following properties: | |
| { "guessedImage": the name of the drawn image } | |
| '''), | |
| // image part | |
| InlineDataPart('image/png', imgBytes), | |
| ]); | |
| var response = await model.generateContent([content]); | |
| var jsonPayload = json.decode(response.text!); | |
| ref.read(imageResultNotifier.notifier).state = ImageResultPayload( | |
| name: jsonPayload['guessedImage'], | |
| imgBytes: imgBytes, | |
| ); | |
| } on Exception catch (e) { | |
| debugPrint(e.toString()); | |
| } | |
| } | |
| void clear() { | |
| ref.read(painterControllerProvider).clearContent(clearColor: Colors.white); | |
| ref.read(imageResultNotifier.notifier).state = null; | |
| } | |
| } | |
| final paintingServiceProvider = Provider<GeminiPaintingService>((ref) { | |
| return GeminiPaintingService(ref); | |
| }); | |
| class PaintingValidator extends ConsumerWidget { | |
| const PaintingValidator({super.key}); | |
| @override | |
| Widget build(BuildContext context, WidgetRef ref) { | |
| final imgPayload = ref.watch(imageResultNotifier); | |
| if (imgPayload == null) { | |
| return const SizedBox.shrink(); | |
| } | |
| return Container( | |
| margin: const EdgeInsets.all(20), | |
| padding: const EdgeInsets.all(20), | |
| decoration: BoxDecoration( | |
| color: Colors.white, | |
| borderRadius: BorderRadius.circular(20), | |
| ), | |
| child: Row( | |
| children: [ | |
| Image.memory(imgPayload.imgBytes, width: 200, height: 200), | |
| const SizedBox(width: 24), | |
| Expanded( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| 'Guessed image:', | |
| style: TextStyle(fontSize: 20, color: Colors.purpleAccent), | |
| ), | |
| Text( | |
| imgPayload.name, | |
| style: const TextStyle( | |
| fontSize: 30, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment