In my previous article, we looked at the Google AI Dart SDK and how we can use it in Flutter/Dart applications. I promised to provide a sample application in the upcoming article, so here I am today. We will develop a storytelling application in Flutter using the Google Generative AI package. So, without further ado, let's start.
An AI Storytelling App
Let's make a small storyteller app using Flutter and Gemini AI. In this app, we will provide the prompt and image to Gemini AI, which will respond with a story based on the image, as shown in the app flow diagram below.
![Flow diagram]()
Now, join me in completing these steps to build it.
Step 1. Installation
Follow the steps below to enable Google Gemini in your Flutter project.
Get an API key: You can head over to https://aistudio.google.com/app/apikey and create a Gemini API key.
Add the dependency: You need to add the google_generative_ai dependency to the pubspec.yaml file in your flutter project.
dependencies:
google_generative_ai: ^0.2.0
Add the assets: You must include the image assets folder in your pubspec.yaml file.
assets:
- images/
Run the Flutter pub and get after that.
Step 2. Folder Structure
Create a folder structure similar to the one shown below, and put all of your images in the image folder.
![Folder structure]()
Step 3. Add Your API Key
Add the Gemini API key to the utils/constants.dart file.
const String apiKey = 'YOUR_API_KEY';
Replace the YOUR_API_KEY with your actual Gemini API key.
Step 4. Add Images Resources
In the utils/images.dart file, I've centralized all of the asset image paths.
class ImageResources {
static const String elephantAndTailor = 'images/elephant-and-the-tailor.jpg';
static const String freeBird = 'images/free_bird.jpeg';
static const String lionAndMouse = 'images/lion_and_mouse.jpeg';
static const String lionAndRabbit = 'images/lion_and_rabbit.jpeg';
static const String lostAndFound = 'images/lost_and_found.jpeg';
static const String wolfWolf = 'images/wolf_wolf.jpeg';
static const String sweetGrapes = 'images/sweet_grapes.jpeg';
static const String literateAndIliterate = 'images/Literate_and-_lliterate.jpeg';
}
Step 5. Create a Home Page (pages/home_page.dart).
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:storyteller/utils/images.dart';
import 'package:storyteller/pages/story_page.dart';
// HomePage is a StatelessWidget that displays a grid of images.
// Each image can be clicked to navigate to a StoryPage.
class HomePage extends StatelessWidget {
// List of image resources to be displayed in the grid.
List<String> images = [
ImageResources.sweetGrapes,
ImageResources.lionAndMouse,
ImageResources.freeBird,
ImageResources.lostAndFound,
ImageResources.elephantAndTailor,
ImageResources.wolfWolf,
ImageResources.lionAndRabbit,
ImageResources.literateAndIliterate
];
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
title: const Text('Storyteller'), // AppBar with the title 'Storyteller'
),
body: GridView.builder(
padding: const EdgeInsets.all(8.0), // Padding around the grid
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // number of items per row
crossAxisSpacing: 8, // spacing between items horizontally
mainAxisSpacing: 8, // spacing between items vertically
),
itemCount: images.length, // The number of items in the grid is the number of images
itemBuilder: (context, index) {
// For each item, create an InkWell widget that navigates to a StoryPage when tapped.
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StoryPage(imagePath: images[index])), // Pass the image path to the StoryPage
);
},
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(images[index]), // Display the image
fit: BoxFit.cover, // Make the image cover the entire grid item
),
borderRadius: BorderRadius.circular(10), // Round the corners of the grid items
),
),
);
},
),
);
}
}
Home Page displays a grid of images using the GridView.builder widget. Each grid item is an InkWell widget that, when tapped, navigates to a StoryPage with the corresponding image.
Now, call the HomePage widget to main.dart.
import 'package:flutter/material.dart';
import 'package:storyteller/pages/home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: HomePage(),
);
}
}
Step 6. Create a Story Page (pages/story_page.dart).
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:storyteller/utils/constants.dart';
import 'dart:typed_data';
// StoryPage is a StatefulWidget that displays a story generated from an image.
class StoryPage extends StatefulWidget {
final String imagePath; // Path to the image that the story is based on
const StoryPage({super.key, required this.imagePath});
@override
State<StoryPage> createState() => _StoryPageState();
}
class _StoryPageState extends State<StoryPage> {
List<String> story = []; // List of strings that make up the story
bool isLoading = true; // Whether the story is currently being loaded
// Load an image from the assets and return its bytes
Future<Uint8List> loadImageAssetBytes(String path) async {
ByteData data = await rootBundle.load(path);
return data.buffer.asUint8List();
}
// Compose a story based on the image at widget.imagePath
void composeStory() async {
story.clear();
final model = GenerativeModel(model: 'gemini-pro-vision', apiKey: apiKey);
final prompt = 'Generate a story behind this image?';
final lionBytes = await loadImageAssetBytes(widget.imagePath);
final content = [
Content.multi([TextPart(prompt), DataPart('image/jpeg', lionBytes)])
];
final responses = model.generateContentStream(content);
await for (final response in responses) {
story.add(response.text!.trim());
}
setState(() {
isLoading = false; // The story has been loaded
});
}
@override
void initState() {
super.initState();
composeStory(); // Compose the story when the widget is initialized
}
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverAppBar(
expandedHeight: size.height * 0.4,
floating: false,
pinned: true,
iconTheme: IconThemeData(
color: innerBoxIsScrolled ? Colors.black : Colors.white),
flexibleSpace: FlexibleSpaceBar(
background: Image.asset(
widget.imagePath,
fit: BoxFit.cover,
),
),
),
];
},
body: isLoading
? const Center(child: CircularProgressIndicator()) // Show a loading spinner while the story is being loaded
: ListView.builder(
padding: const EdgeInsets.all(8.0),
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: story.length, // The number of items in the list is the number of strings in the story
itemBuilder: (context, index) {
return Text(
story[index], // Display each string in the story as a separate item in the list
style: Theme.of(context).textTheme.bodyLarge,
);
},
),
),
);
}
}
The story page takes an image path as a parameter. The page uses Google's Generative AI to generate a story based on the provided image.
- loadImageAssetBytes function loads the image from the provided path as a byte array.
- composeStory function clears the existing story, initializes the generative model with the API key, and sets a prompt. It then loads the image bytes and generates a content stream. The generated story is added to the story list. Once the story is generated, it sets isLoading to false to indicate that it is ready to display.
- initState function calls composeStory to generate the story when the widget is initialized.
![Storyteller app]()
![Story]()
Conclusion
In this post, we explored how to use the Gemini API with the help of the Google generative AI package to build a storyteller application in Flutter that generates a story based on an image. You can construct various types of applications utilizing Gemini with Flutter. For example,
- Summarise lengthy texts and capture their key points.
- Build smart chatbots that resemble human conversations
- A visual search engine that allows users to upload pictures and receive descriptions
- And many more
If you found this article helpful, feel free to connect with me on LinkedIn and say hi. Stay tuned for the next article.
GitHub SourceCode