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:

hooks/useChat.tstypescript
1import { useState, useCallback, useRef, useEffect } from 'react';
2import {
3 isAvailable,
4 prepareModel,
5 createSession,
6 sendMessage,
7 Session,
8} from 'expo-ai-kit';
9
10interface Message {
11 id: string;
12 role: 'user' | 'assistant';
13 content: string;
14 timestamp: Date;
15}
16
17export 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:

screens/ChatScreen.tsxtypescript
1import React, { useState, useRef } from 'react';
2import {
3 View,
4 FlatList,
5 TextInput,
6 TouchableOpacity,
7 Text,
8 StyleSheet,
9 KeyboardAvoidingView,
10 Platform,
11} from 'react-native';
12import { useChat } from '../hooks/useChat';
13import { MessageBubble } from '../components/MessageBubble';
14
15export 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
109const 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:

components/MessageBubble.tsxtypescript
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:

components/WritingAssistant.tsxtypescript
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:

hooks/useSmartReplies.tstypescript
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:

utils/aiHelpers.tstypescript
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.