Creating a Generative UI Mobile App Using Flutter, Gemini, and the genui Package
Subtitle: (event name / date) Location: (venue)
Hi! I'm Jeo
- From Davao · Software Developer
- Svelte · React · Firebase · TypeScript
- I love Mobile Apps — Flutter, Android, React Native
- Tech Volunteer @ GDG Davao
Disclaimer: Developer's POV. Focus is on how it works, not pixel-perfect design.
[IMG PLACEHOLDER: Speaker headshot]
In This Talk...
- Traditional UI with AI vs Generative UI
- What
genuiis — and how it works - Building a dynamic quiz app, step by step
Chapter One
In Traditional UIs…
- UIs come pre-built — the AI has no say in how responses are presented
- Output is always text inside a fixed widget
- Even if the AI is flexible, the experience isn't
[IMG PLACEHOLDER: Classic chat UI — AI response as plain text inside a Card]
Generative UI — AI generates not just content, but the entire UX
| Traditional UIs | Generative UIs |
|---|---|
| Developer pre-designs every screen | AI selects the right UI for the context |
| AI output wrapped in a fixed widget | AI output is the widget |
| UI is a container for AI output | UI is part of the AI output |
[IMG PLACEHOLDER: Side-by-side — text response vs interactive quiz card rendered by genui]
Dynamic Quiz App
- User types any topic — Gemini generates questions on the fly
- Questions render as interactive widgets — not text
- 6 formats: Multiple Choice · True/False · Fill in the Blank · Order the Steps · Word Bank · Slider
- Immediate feedback + explanations on reveal
[IMG PLACEHOLDER: App screenshot — mix of card types generated for "Flutter Basics"]
Chapter Two
Dependencies
# pubspec.yaml
dependencies:
genui: ^0.9.0
google_generative_ai: ^0.4.0
json_schema_builder: ^0.1.3
flutter_dotenv: ^5.0.2API key in .env (add to .gitignore, declare as asset):
GEMINI_API_KEY=your_key_here
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await loadEnv();
runApp(const QuizApp());
}[IMG PLACEHOLDER: Google AI Studio "Get API key" page]
A CatalogItem = one schema + one widget
CatalogItem(
name: 'MultipleChoiceCard', // Gemini uses this name
dataSchema: S.object( ... ), // tells Gemini what JSON to produce
widgetBuilder: (context) { // turns that JSON into a Flutter widget
final data = context.data as Map<String, Object?>;
return MultipleChoiceCard( ... );
},
)genui injects the schema into the system prompt automatically — no manual prompt engineering.
Each card type gets its own schema
// Multiple choice
dataSchema: S.object(properties: {
'question': S.string(),
'choices': S.list(items: S.object(properties: {
'id': S.string(), 'text': S.string(),
})),
'correctChoiceId': S.string(),
'explanation': S.string(),
}, required: ['question', 'choices', 'correctChoiceId']),
// True / False
dataSchema: S.object(properties: {
'statement': S.string(),
'isTrue': S.boolean(),
'explanation': S.string(),
}, required: ['statement', 'isTrue']),
// Fill in the blank
dataSchema: S.object(properties: {
'statement': S.string(),
'correctAnswer': S.string(),
'hint': S.string(),
}, required: ['statement', 'correctAnswer']),Register all card types in one place
// lib/widgets/quiz_catalog.dart
Catalog buildQuizCatalog({
required void Function(bool isCorrect) onAnswered,
}) {
return BasicCatalogItems.asCatalog().copyWith(newItems: [
multipleChoiceCardItem(onAnswered),
trueFalseCardItem(onAnswered),
fillInBlankCardItem(onAnswered),
orderTheStepsCardItem(onAnswered),
wordBankCardItem(onAnswered),
sliderCardItem(onAnswered),
]);
}BasicCatalogItems— genui's built-in text/markdown widgets.copyWith(newItems: [...])— add your custom types on top- To add a new format: create its file, add one line here
void _startQuiz(String topic) {
final catalog = buildQuizCatalog(
onAnswered: (isCorrect) => setState(() {
_answered++;
if (isCorrect) _correct++;
}),
);
// genui reads your schemas and builds the system prompt
final promptBuilder = PromptBuilder.chat(
catalog: catalog,
systemPromptFragments: [_systemFragments(topic)],
);
final systemPrompt = promptBuilder.systemPromptJoined();
final model = GenerativeModel(
model: 'models/gemini-2.0-flash',
apiKey: getApiKey(),
systemInstruction: Content.text(systemPrompt),
);
_surfaceController = SurfaceController(catalogs: [catalog]);
late A2uiTransportAdapter transport;
transport = A2uiTransportAdapter(
onSend: (message) async {
final response = model.generateContentStream(
[Content.text(message.text)],
);
await for (final chunk in response) {
transport.addChunk(chunk.text ?? '');
}
},
);
_conversation = Conversation(
controller: _surfaceController!,
transport: transport,
);
}String _systemFragments(String topic) =>
'You are a quiz generator for "$topic". '
'Output all 5 questions at once without waiting. '
'Keep explanations short (1 sentence). Mix difficulty.';Listen for surfaces → render one at a time with Surface
// Listen for Gemini emitting a new component
_conversation!.events.listen((event) {
if (event is ConversationSurfaceAdded(:final surfaceId)) {
setState(() => _surfaceIds.add(surfaceId));
}
});
// Kick off the quiz
_conversation!.sendRequest(
ChatMessage.user('Give me quiz questions about: $topic. Mix the formats.'),
);// Show one card at a time — genui picks the right widget automatically
Surface(
key: ValueKey(_surfaceIds[_currentIndex]),
surfaceContext: _surfaceController!.contextFor(_surfaceIds[_currentIndex]),
)
// After answering, a button advances to the next card
FilledButton.icon(
onPressed: _nextQuestion,
label: Text(_currentIndex >= _surfaceIds.length - 1
? 'See Results'
: 'Next Question'),
)[IMG PLACEHOLDER: App screenshot — single card shown with Next Question button]
User types topic
↓
Conversation.sendRequest()
↓
A2uiTransportAdapter → Gemini 2.0 Flash
↓ (streams JSON component objects)
genui parses stream → ConversationSurfaceAdded
↓
SurfaceController matches CatalogItem
↓
Surface widget → your widgetBuilder → Flutter card
[IMG PLACEHOLDER: Clean visual diagram of the above flow]
Let's try it
Topics to try:
- "Flutter Basics"
- "World War II"
- "The Human Body"
Ask the audience for a topic!
[IMG PLACEHOLDER: QR code to GitHub repo]
- Traditional AI UIs are still static — the AI fills content, but the experience is fixed
- Generative UI flips that — the AI decides what to render, not just what to say
- A2UI is the protocol;
genuiis the Flutter implementation — backend-agnostic by design - A
CatalogItemis the unit of Generative UI — one schema tells Gemini what to produce, one widget builder renders it - You write normal Flutter widgets —
genuihandles the bridge between LLM output and your UI - In ~200 lines of Dart, we got 4 interactive question formats, real-time streaming, and zero manual JSON parsing
Thanks!
genui—pub.dev/packages/genui- Google AI Studio —
aistudio.google.com - Source code —
[IMG PLACEHOLDER: QR code]
Questions?
Jeo · GitHub: jeoooo · GDG Davao
[IMG PLACEHOLDER: GDG Davao logo]