Examples
Complete code examples showing how to integrate expo-ai-kit into real applications.
Simple Chat App
iOS A complete chat application with streaming responses, message history, and proper error handling.
Chat Context Hook
First, create a custom hook to manage the AI session and conversation state:
1 import { useState, useCallback, useRef, useEffect } from 'react'; 2 import { 3 isAvailable, 4 prepareModel, 5 createSession, 6 sendMessage, 7 Session, 8 } from 'expo-ai-kit'; 9 10 interface Message { 11 id: string; 12 role: 'user' | 'assistant'; 13 content: string; 14 timestamp: Date; 15 } 16 17 export function useChat() { 18 const [messages, setMessages] = useState<Message[]>([]); 19 const [isLoading, setIsLoading] = useState(false); 20 const [isAvailableState, setIsAvailableState] = useState<boolean | null>(null); 21 const [streamingContent, setStreamingContent] = useState(''); 22 const sessionRef = useRef<Session | null>(null); 23 24 // Check availability and prepare model on mount 25 useEffect(() => { 26 async function init() { 27 const available = await isAvailable(); 28 setIsAvailableState(available); 29 30 if (available) { 31 await prepareModel(); 32 sessionRef.current = await createSession({ 33 systemPrompt: 'You are a helpful, friendly assistant. Keep responses concise.', 34 }); 35 } 36 } 37 init(); 38 39 return () => { 40 sessionRef.current?.close(); 41 }; 42 }, []); 43 44 const sendUserMessage = useCallback(async (text: string) => { 45 if (!sessionRef.current || isLoading) return; 46 47 const userMessage: Message = { 48 id: Date.now().toString(), 49 role: 'user', 50 content: text, 51 timestamp: new Date(), 52 }; 53 54 setMessages((prev) => [...prev, userMessage]); 55 setIsLoading(true); 56 setStreamingContent(''); 57 58 try { 59 const response = await sendMessage(sessionRef.current, { 60 message: text, 61 onToken: (token) => { 62 setStreamingContent((prev) => prev + token); 63 }, 64 }); 65 66 const assistantMessage: Message = { 67 id: (Date.now() + 1).toString(), 68 role: 'assistant', 69 content: response.text, 70 timestamp: new Date(), 71 }; 72 73 setMessages((prev) => [...prev, assistantMessage]); 74 } catch (error) { 75 console.error('Chat error:', error); 76 // Optionally add error message to chat 77 } finally { 78 setIsLoading(false); 79 setStreamingContent(''); 80 } 81 }, [isLoading]); 82 83 const clearChat = useCallback(async () => { 84 setMessages([]); 85 await sessionRef.current?.clearHistory(); 86 }, []); 87 88 return { 89 messages, 90 isLoading, 91 isAvailable: isAvailableState, 92 streamingContent, 93 sendMessage: sendUserMessage, 94 clearChat, 95 }; 96 }
Chat Screen Component
The main chat screen that uses the hook:
1 import React, { useState, useRef } from 'react'; 2 import { 3 View, 4 FlatList, 5 TextInput, 6 TouchableOpacity, 7 Text, 8 StyleSheet, 9 KeyboardAvoidingView, 10 Platform, 11 } from 'react-native'; 12 import { useChat } from '../hooks/useChat'; 13 import { MessageBubble } from '../components/MessageBubble'; 14 15 export function ChatScreen() { 16 const [inputText, setInputText] = useState(''); 17 const flatListRef = useRef<FlatList>(null); 18 const { 19 messages, 20 isLoading, 21 isAvailable, 22 streamingContent, 23 sendMessage, 24 clearChat, 25 } = useChat(); 26 27 if (isAvailable === null) { 28 return ( 29 <View style={styles.centered}> 30 <Text>Checking AI availability...</Text> 31 </View> 32 ); 33 } 34 35 if (!isAvailable) { 36 return ( 37 <View style={styles.centered}> 38 <Text style={styles.unavailableTitle}>AI Not Available</Text> 39 <Text style={styles.unavailableText}> 40 On-device AI requires an iPhone 15 Pro or newer with iOS 26.0+ 41 </Text> 42 </View> 43 ); 44 } 45 46 const handleSend = () => { 47 if (inputText.trim() && !isLoading) { 48 sendMessage(inputText.trim()); 49 setInputText(''); 50 } 51 }; 52 53 // Combine messages with streaming content for display 54 const displayMessages = [...messages]; 55 if (streamingContent) { 56 displayMessages.push({ 57 id: 'streaming', 58 role: 'assistant', 59 content: streamingContent, 60 timestamp: new Date(), 61 }); 62 } 63 64 return ( 65 <KeyboardAvoidingView 66 style={styles.container} 67 behavior={Platform.OS === 'ios' ? 'padding' : undefined} 68 keyboardVerticalOffset={90} 69 > 70 <FlatList 71 ref={flatListRef} 72 data={displayMessages} 73 keyExtractor={(item) => item.id} 74 renderItem={({ item }) => ( 75 <MessageBubble 76 role={item.role} 77 content={item.content} 78 isStreaming={item.id === 'streaming'} 79 /> 80 )} 81 contentContainerStyle={styles.messageList} 82 onContentSizeChange={() => flatListRef.current?.scrollToEnd()} 83 /> 84 85 <View style={styles.inputContainer}> 86 <TextInput 87 style={styles.input} 88 value={inputText} 89 onChangeText={setInputText} 90 placeholder="Type a message..." 91 multiline 92 maxLength={1000} 93 editable={!isLoading} 94 /> 95 <TouchableOpacity 96 style={[styles.sendButton, isLoading && styles.sendButtonDisabled]} 97 onPress={handleSend} 98 disabled={isLoading || !inputText.trim()} 99 > 100 <Text style={styles.sendButtonText}> 101 {isLoading ? '...' : 'Send'} 102 </Text> 103 </TouchableOpacity> 104 </View> 105 </KeyboardAvoidingView> 106 ); 107 } 108 109 const styles = StyleSheet.create({ 110 container: { 111 flex: 1, 112 backgroundColor: '#fff', 113 }, 114 centered: { 115 flex: 1, 116 justifyContent: 'center', 117 alignItems: 'center', 118 padding: 20, 119 }, 120 unavailableTitle: { 121 fontSize: 18, 122 fontWeight: '600', 123 marginBottom: 8, 124 }, 125 unavailableText: { 126 textAlign: 'center', 127 color: '#666', 128 }, 129 messageList: { 130 padding: 16, 131 paddingBottom: 8, 132 }, 133 inputContainer: { 134 flexDirection: 'row', 135 padding: 12, 136 borderTopWidth: 1, 137 borderTopColor: '#e5e5e5', 138 alignItems: 'flex-end', 139 }, 140 input: { 141 flex: 1, 142 borderWidth: 1, 143 borderColor: '#e5e5e5', 144 borderRadius: 20, 145 paddingHorizontal: 16, 146 paddingVertical: 10, 147 maxHeight: 100, 148 fontSize: 16, 149 }, 150 sendButton: { 151 marginLeft: 8, 152 backgroundColor: '#6366f1', 153 borderRadius: 20, 154 paddingHorizontal: 20, 155 paddingVertical: 10, 156 }, 157 sendButtonDisabled: { 158 backgroundColor: '#c7c7c7', 159 }, 160 sendButtonText: { 161 color: '#fff', 162 fontWeight: '600', 163 }, 164 });
Message Component
A simple message bubble component:
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
interface MessageBubbleProps {
role: 'user' | 'assistant';
content: string;
isStreaming?: boolean;
}
export function MessageBubble({ role, content, isStreaming }: MessageBubbleProps) {
const isUser = role === 'user';
return (
<View style={[styles.container, isUser ? styles.userContainer : styles.assistantContainer]}>
<View style={[styles.bubble, isUser ? styles.userBubble : styles.assistantBubble]}>
<Text style={[styles.text, isUser && styles.userText]}>
{content}
{isStreaming && <Text style={styles.cursor}>|</Text>}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginVertical: 4,
},
userContainer: {
alignItems: 'flex-end',
},
assistantContainer: {
alignItems: 'flex-start',
},
bubble: {
maxWidth: '80%',
borderRadius: 16,
paddingHorizontal: 14,
paddingVertical: 10,
},
userBubble: {
backgroundColor: '#6366f1',
},
assistantBubble: {
backgroundColor: '#f3f4f6',
},
text: {
fontSize: 16,
lineHeight: 22,
},
userText: {
color: '#fff',
},
cursor: {
color: '#6366f1',
},
});This example includes all the essentials: session management, streaming UI, loading states, and error handling. Adapt it to your app's design system.
AI Writing Assistant
iOS A simple writing assistant that helps improve or expand text:
import { useState } from 'react';
import { createSession, sendMessage, isAvailable } from 'expo-ai-kit';
type Action = 'improve' | 'expand' | 'summarize' | 'simplify';
const prompts: Record<Action, string> = {
improve: 'Improve this text for clarity and professionalism:',
expand: 'Expand on this text with more detail:',
summarize: 'Summarize this text concisely:',
simplify: 'Simplify this text for a general audience:',
};
export function useWritingAssistant() {
const [isProcessing, setIsProcessing] = useState(false);
const [result, setResult] = useState('');
async function processText(text: string, action: Action) {
if (!await isAvailable()) {
throw new Error('AI not available');
}
setIsProcessing(true);
setResult('');
try {
const session = await createSession();
const response = await sendMessage(session, {
message: `${prompts[action]}\n\n${text}`,
onToken: (token) => {
setResult((prev) => prev + token);
},
});
await session.close();
return response.text;
} finally {
setIsProcessing(false);
}
}
return { processText, isProcessing, result };
}
// Usage in a component
function TextEditor() {
const [text, setText] = useState('');
const { processText, isProcessing, result } = useWritingAssistant();
const handleImprove = async () => {
const improved = await processText(text, 'improve');
setText(improved);
};
// ... render UI with buttons for each action
}Smart Reply Suggestions
iOS Generate quick reply suggestions for messages:
import { useState, useCallback } from 'react';
import { createSession, sendMessage, isAvailable } from 'expo-ai-kit';
export function useSmartReplies() {
const [suggestions, setSuggestions] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const generateReplies = useCallback(async (incomingMessage: string) => {
if (!await isAvailable()) return;
setIsLoading(true);
setSuggestions([]);
try {
const session = await createSession({
systemPrompt: `Generate 3 brief, natural reply options for the following message.
Format: Return only the replies, one per line, no numbers or bullets.
Keep each reply under 50 characters.`,
});
const response = await sendMessage(session, {
message: incomingMessage,
});
// Parse the response into individual suggestions
const replies = response.text
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0 && line.length < 100)
.slice(0, 3);
setSuggestions(replies);
await session.close();
} catch (error) {
console.error('Failed to generate replies:', error);
} finally {
setIsLoading(false);
}
}, []);
return { suggestions, isLoading, generateReplies };
}
// Usage
function MessageView({ message }: { message: string }) {
const { suggestions, isLoading, generateReplies } = useSmartReplies();
useEffect(() => {
generateReplies(message);
}, [message]);
return (
<View>
<Text>{message}</Text>
{isLoading ? (
<Text>Generating suggestions...</Text>
) : (
<View style={styles.suggestions}>
{suggestions.map((suggestion, i) => (
<TouchableOpacity
key={i}
onPress={() => sendReply(suggestion)}
style={styles.chip}
>
<Text>{suggestion}</Text>
</TouchableOpacity>
))}
</View>
)}
</View>
);
}Error Handling Patterns
Robust error handling for production apps:
import { isAvailable, createSession, sendMessage, Session } from 'expo-ai-kit';
export class AIError extends Error {
constructor(
message: string,
public code: 'UNAVAILABLE' | 'SESSION_ERROR' | 'MESSAGE_ERROR' | 'TIMEOUT',
public cause?: Error
) {
super(message);
this.name = 'AIError';
}
}
export async function withAISession<T>(
fn: (session: Session) => Promise<T>,
options?: { timeout?: number }
): Promise<T> {
const timeout = options?.timeout ?? 60000;
// Check availability
const available = await isAvailable();
if (!available) {
throw new AIError(
'On-device AI is not available on this device',
'UNAVAILABLE'
);
}
let session: Session | null = null;
try {
session = await createSession();
// Add timeout wrapper
const result = await Promise.race([
fn(session),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new AIError('Request timed out', 'TIMEOUT')),
timeout
)
),
]);
return result;
} catch (error) {
if (error instanceof AIError) throw error;
throw new AIError(
'Failed to process AI request',
'MESSAGE_ERROR',
error as Error
);
} finally {
await session?.close();
}
}
// Usage
async function getAIResponse(prompt: string) {
try {
return await withAISession(async (session) => {
const response = await sendMessage(session, { message: prompt });
return response.text;
});
} catch (error) {
if (error instanceof AIError) {
switch (error.code) {
case 'UNAVAILABLE':
// Show device requirements message
break;
case 'TIMEOUT':
// Offer to retry
break;
default:
// Generic error message
break;
}
}
throw error;
}
}These examples demonstrate patterns, not complete apps. Adapt them to your specific needs, UI framework, and state management solution.