<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ AISprint  - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ AISprint  - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:24:49 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/aisprint/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Medical Chatbot with Flutter and Gemini: A Beginner’s Guide ]]>
                </title>
                <description>
                    <![CDATA[ In today's digital age, the demand for accessible and accurate health information is higher than ever. Leveraging the power of artificial intelligence, we can create intelligent chatbots that provide reliable health-related guidance. This beginner's ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-medical-chatbot-with-flutter-and-gemini/</link>
                <guid isPermaLink="false">684c61db3b494a20ba49fddb</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AISprint  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ chatbot ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Fri, 13 Jun 2025 17:37:31 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749830721631/4675d1f6-ad64-46a3-86e1-ce8a2c84323f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In today's digital age, the demand for accessible and accurate health information is higher than ever. Leveraging the power of artificial intelligence, we can create intelligent chatbots that provide reliable health-related guidance.</p>
<p>This beginner's guide will walk you through building a powerful and specialized medical chatbot using Flutter and Google's Gemini API. The chatbot will be able to receive input from various modalities like text, audio, camera, files, and a gallery, and it will be strictly confined to answering health-related questions.</p>
<h3 id="heading-table-of-contents">Table of Contents:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-power-of-ai-in-healthcare">The Power of AI in Healthcare</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-your-development-environment">How to Set Up Your Development Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-code-implementation-and-explanation">Code Implementation and Explanation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-important-considerations-and-future-enhancements">Important Considerations and Future Enhancements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-screenshots">Screenshots and Completed Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-the-power-of-ai-in-healthcare">The Power of AI in Healthcare</h2>
<p>AI-powered chatbots are transforming various industries, and healthcare is no exception. They offer a scalable and efficient way to disseminate information, answer frequently asked questions, and even provide initial assessments. By focusing on health-related queries, our chatbot will act as a specialized assistant, providing concise and accurate information to users.</p>
<h3 id="heading-core-technologies">Core Technologies</h3>
<p>We’ll build our medical chatbot using the following key technologies:</p>
<ul>
<li><p><strong>Flutter:</strong> Google's UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase.<strong><sup>1</sup></strong> Its rich set of widgets and expressive UI make it ideal for creating engaging chat interfaces.</p>
</li>
<li><p><strong>Google Gemini API:</strong> Google's most capable and flexible AI model. Gemini is multimodal, meaning it can process and understand different types of information, including text, images, audio, and video. This capability is crucial for our chatbot to handle diverse user inputs.</p>
</li>
<li><p><code>flutter_ai_toolkit</code>: A Flutter package that provides a set of AI chat-related widgets and an abstract LLM provider API, simplifying the integration of AI models into your Flutter app. It offers out-of-the-box support for Gemini.</p>
</li>
<li><p><code>google_generative_ai</code>: The official Dart package for interacting with Google's Generative AI models (Gemini).</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-your-development-environment">How to Set Up Your Development Environment</h2>
<p>Before we dive into the code, make sure you have Flutter installed and configured on your system. If not, follow the <a target="_blank" href="https://flutter.dev/docs/get-started/install">official Flutter installation guide here</a>.</p>
<h3 id="heading-get-your-gemini-api-key">Get Your Gemini API Key</h3>
<p>To interact with the Gemini API, you need an API key. This key authenticates your application and allows it to send requests to the Gemini model.</p>
<p>Here's how to get your Gemini API key:</p>
<ol>
<li><p><strong>Go to Google AI Studio:</strong> Open your web browser and navigate to <a target="_blank" href="https://aistudio.google.com/">https://aistudio.google.com/</a>.</p>
</li>
<li><p><strong>Log in with your Google account:</strong> If you're not already logged in, you'll be prompted to sign in with your Google account.</p>
</li>
<li><p><strong>Click "Get API key in Google AI Studio":</strong> On the Google AI Studio homepage, you'll see a prominent button with this text. Click it.</p>
</li>
<li><p><strong>Review and approve terms of service:</strong> A pop-up will appear asking you to consent to the Google APIs Terms of Service and Gemini API Additional Terms of Service. Read them carefully, check the necessary boxes, and click "Continue."</p>
</li>
<li><p><strong>Create your API key:</strong> You'll now have the option to "Create API key in new project" or "Create API key in existing project." Choose the one that suits your needs. Your API key will be auto-generated.</p>
</li>
<li><p><strong>Copy your API key:</strong> <strong>Crucially, copy this API key immediately and store it securely.</strong> It will not be shown again. <strong>Do NOT hardcode your API key directly into your production code, especially for client-side applications.</strong> For development purposes, we will use it directly in our <code>MedicalChatScreen</code> for simplicity, but for a real-world application, consider using environment variables or a secure backend to manage your API key.</p>
</li>
</ol>
<h3 id="heading-add-dependencies-pubspecyaml">Add Dependencies (<code>pubspec.yaml</code>)</h3>
<p>Open your <code>pubspec.yaml</code> file (located at the root of your Flutter project) and add the following dependencies under <code>dependencies</code>:</p>
<pre><code class="lang-bash">dependencies:
  flutter:
    sdk: flutter
  flutter_ai_toolkit: ^0.6.8
  google_generative_ai: ^0.4.6
</code></pre>
<p>After adding these, run <code>flutter pub get</code> in your terminal to fetch the packages.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Our project will have a simple structure:</p>
<ul>
<li><p><code>lib/main.dart</code>: The entry point of our Flutter application.</p>
</li>
<li><p><code>lib/screens/chat.dart</code>: Contains the main chat interface for our medical chatbot.</p>
</li>
</ul>
<h2 id="heading-code-implementation-and-explanation">Code Implementation and Explanation</h2>
<p>Let's break down the provided code and understand each part.</p>
<h4 id="heading-libmaindart"><code>lib/main.dart</code></h4>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:ai_demo/screens/chat.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  runApp(<span class="hljs-keyword">const</span> MyApp());
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MyApp({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Medical ChatBot'</span>,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: <span class="hljs-keyword">const</span> MedicalChatScreen(),
    );
  }
}
</code></pre>
<p>Here’s what’s going on in this code:</p>
<ul>
<li><p><code>import 'package:ai_demo/screens/chat.dart';</code>: This line imports the <code>chat.dart</code> file from the <code>screens</code> folder. This is where our <code>MedicalChatScreen</code> widget is defined.</p>
</li>
<li><p><code>import 'package:flutter/material.dart';</code>: This imports the fundamental Flutter Material Design widgets, essential for building the UI.</p>
</li>
<li><p><code>void main() { runApp(const MyApp()); }</code>: This is the entry point of every Flutter application. <code>runApp()</code> takes a widget as an argument and makes it the root of the widget tree.</p>
</li>
<li><p><code>class MyApp extends StatelessWidget</code>: <code>MyApp</code> is the root widget of our application. <code>StatelessWidget</code> means its properties don't change over time.</p>
</li>
<li><p><code>Widget build(BuildContext context)</code>: This method is where the UI of the <code>MyApp</code> widget is built.</p>
</li>
<li><p><code>MaterialApp</code>: This widget provides the basic Material Design visual structure for a Flutter app.</p>
<ul>
<li><p><code>title: 'Medical ChatBot'</code>: Sets the title of the application, which might be displayed in the device's task switcher or browser tab.</p>
</li>
<li><p><code>theme: ThemeData(...)</code>: Defines the visual theme of the application.</p>
<ul>
<li><code>colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)</code>: Generates a color scheme based on a primary "seed" color (<code>Colors.deepPurple</code>), ensuring a consistent and harmonious look across the app.</li>
</ul>
</li>
<li><p><code>home: const MedicalChatScreen()</code>: Sets the initial screen of the application to our <code>MedicalChatScreen</code> widget.</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-libscreenschatdart"><code>lib/screens/chat.dart</code></h4>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:google_generative_ai/google_generative_ai.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MedicalChatScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> MedicalChatScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  State&lt;MedicalChatScreen&gt; createState() =&gt; _MedicalChatScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MedicalChatScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MedicalChatScreen</span>&gt; </span>{
  <span class="hljs-built_in">String</span> apiKey = <span class="hljs-string">""</span>; <span class="hljs-comment">// IMPORTANT: Replace with your actual Gemini API Key</span>

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-comment">// It's a good practice to load the API key from a secure source</span>
    <span class="hljs-comment">// rather than hardcoding it, especially for production apps.</span>
    <span class="hljs-comment">// For this demo, we'll keep it simple.</span>
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        automaticallyImplyLeading: <span class="hljs-keyword">false</span>,
        title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">"Medical ChatBot"</span>),
      ),
      body: LlmChatView(
        suggestions: <span class="hljs-keyword">const</span> [
          <span class="hljs-string">"I've been feeling dizzy lately. What now?"</span>,
          <span class="hljs-string">"How do I know if I need to see a doctor?"</span>,
          <span class="hljs-string">"What should I eat to boost my immunity?"</span>
        ],
        style: LlmChatViewStyle(
          backgroundColor: Colors.white,
          chatInputStyle: ChatInputStyle(
            hintText: <span class="hljs-string">"Enter your message"</span>,
            decoration: <span class="hljs-keyword">const</span> BoxDecoration().copyWith(
              borderRadius: BorderRadius.circular(<span class="hljs-number">50</span>),
            ),
          ),
        ),
        provider: GeminiProvider(
          model: GenerativeModel(
            model: <span class="hljs-string">"gemini-2.0-flash"</span>,
            apiKey: apiKey,
            systemInstruction: Content.system(
              <span class="hljs-string">"You are a professional medical health assistant. Only respond to health and medically related questions and make them concise and straight to the point without too much explanation."</span>
                  <span class="hljs-string">"If a question is unrelated to health or medicine, politely inform the user that you can only answer medical-related queries."</span>,
            ),
          ),
        ),
        welcomeMessage:
        <span class="hljs-string">"Hello👋 I’m here to help with your medical questions. Please tell me how I can assist you."</span>
      ),
    );
  }
}
</code></pre>
<p>What’s going on in <code>chat.dart</code>:</p>
<ul>
<li><p><code>import 'package:flutter/material.dart';</code>: Imports Material Design widgets.</p>
</li>
<li><p><code>import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';</code>: Imports the <code>flutter_ai_toolkit</code> package, which provides the <code>LlmChatView</code> and <code>GeminiProvider</code>.</p>
</li>
<li><p><code>import 'package:google_generative_ai/google_generative_ai.dart';</code>: Imports the <code>google_generative_ai</code> package, which allows us to interact with the Gemini model.</p>
</li>
<li><p><code>class MedicalChatScreen extends StatefulWidget</code>: Our chat screen is a <code>StatefulWidget</code> because its <code>apiKey</code> and potentially other chat-related states might change.</p>
</li>
<li><p><code>_MedicalChatScreenState createState() =&gt; _MedicalChatScreenState();</code>: Creates the mutable state for this widget.</p>
</li>
<li><p><code>String apiKey = "";</code>: <strong>This is where you need to paste your Gemini API key.</strong> Replace <code>""</code> with the actual key you obtained from Google AI Studio. For example: <code>String apiKey = "YOUR_GEMINI_API_KEY_HERE";</code>.</p>
<ul>
<li><strong>Security note:</strong> As mentioned before, hardcoding API keys is not recommended for production applications. Consider using environment variables, a secrets management service (like Firebase Remote Config or Google Cloud Secret Manager), or a backend server to handle API requests securely.</li>
</ul>
</li>
<li><p><code>initState()</code>: This method is called once when the widget is inserted into the widget tree. It's a good place for initial setup. In this case, it's empty but serves as a placeholder for potential future initialization like loading the API key securely.</p>
</li>
<li><p><code>Scaffold</code>: Implements the basic Material Design visual layout structure.</p>
<ul>
<li><p><code>appBar</code>: Displays a top app bar.</p>
<ul>
<li><p><code>backgroundColor: Colors.white</code>: Sets the background color of the app bar to white.</p>
</li>
<li><p><code>automaticallyImplyLeading: false</code>: Prevents Flutter from automatically adding a back button if this screen is pushed onto a navigation stack.</p>
</li>
<li><p><code>title: const Text("Medical ChatBot")</code>: Sets the title text of the app bar.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>body: LlmChatView(...)</code>: This is the core widget from the <code>flutter_ai_toolkit</code> that provides the chat UI and handles the interaction with the LLM.</p>
<ul>
<li><p><code>suggestions: const [...]</code>: Provides a list of initial suggested prompts to the user when the chat is empty. These prompts guide the user on the types of questions the chatbot can answer.</p>
</li>
<li><p><code>style: LlmChatViewStyle(...)</code>: Customizes the appearance of the chat view.</p>
<ul>
<li><p><code>backgroundColor: Colors.white</code>: Sets the background color of the chat area.</p>
</li>
<li><p><code>chatInputStyle: ChatInputStyle(...)</code>: Styles the text input field.</p>
<ul>
<li><p><code>hintText: "Enter your message"</code>: Placeholder text in the input field.</p>
</li>
<li><p><code>decoration: const BoxDecoration().copyWith(borderRadius: BorderRadius.circular(50))</code>: Styles the input field with rounded corners.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>provider: GeminiProvider(...)</code>: This is where we configure our Gemini model as the AI provider for the <code>LlmChatView</code>.</p>
<ul>
<li><p><code>model: GenerativeModel(...)</code>: Creates an instance of the Gemini model.</p>
<ul>
<li><p><code>model: "gemini-2.0-flash"</code>: Specifies the particular Gemini model to use. "gemini-2.0-flash" is a lightweight and fast model suitable for many applications. Other models like "gemini-pro" are also available, offering different capabilities and costs.</p>
</li>
<li><p><code>apiKey: apiKey</code>: Passes your obtained Gemini API key to the model.</p>
</li>
<li><p><code>systemInstruction: Content.system(...)</code>: <strong>This is crucial for defining the chatbot's persona and limitations.</strong> It's a system message sent to the Gemini model at the beginning of the conversation (and potentially with every turn, depending on the implementation details of <code>flutter_ai_toolkit</code> and <code>google_generative_ai</code>).</p>
<ul>
<li><p><code>"You are a professional medical health assistant. Only respond to health and medical-related questions and make them concise and straight to the point without too much explanation."</code>: This is the primary instruction. It tells Gemini to act as a medical assistant and to be precise in its health-related responses.</p>
</li>
<li><p><code>"If a question is unrelated to health or medicine, politely inform the user that you can only answer medical-related queries."</code>: This instruction ensures that the chatbot stays within its defined scope and doesn't hallucinate or provide irrelevant answers to non-medical questions, which is vital for a specialized health bot.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>welcomeMessage: "Hello👋 I’m here to help with your medical questions. Please tell me how I can assist you."</code>: A friendly message displayed to the user when they first open the chat screen, setting the context for the conversation.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-how-to-handle-multi-modal-inputs">How to Handle Multi-Modal Inputs</h3>
<p>The <code>flutter_ai_toolkit</code> package, when used with <code>GeminiProvider</code>, intrinsically supports multi-modal inputs. The <code>LlmChatView</code> automatically provides UI elements for:</p>
<ul>
<li><p><strong>Text input:</strong> The standard text field for typing messages.</p>
</li>
<li><p><strong>Audio input:</strong> A microphone icon will typically be present, allowing users to record voice messages that are then transcribed and sent to Gemini.</p>
</li>
<li><p><strong>Camera input:</strong> A camera icon will allow users to take a photo and send it to the chatbot. Gemini can then process the image and provide a response.</p>
</li>
<li><p><strong>File input:</strong> An attachment icon (often a paperclip) will enable users to select files (like documents or images) from their device to send to the chatbot.</p>
</li>
<li><p><strong>Gallery input:</strong> Similar to file input, but specifically for selecting images or videos from the device's photo gallery.</p>
</li>
</ul>
<p>The <code>flutter_ai_toolkit</code> abstracts away the complexities of handling these different input types, converting them into a format that the <code>google_generative_ai</code> package and subsequently the Gemini model can understand and process. For instance, images are sent as <code>ImagePart</code> within the <code>Content</code> object, and audio might be transcribed to text before being sent, or sent as <code>AudioPart</code> if the model supports direct audio input.</p>
<p>The <code>systemInstruction</code> we set for the <code>GenerativeModel</code> is crucial here. While the <code>flutter_ai_toolkit</code> handles the input, the Gemini model's ability to understand various modalities in the context of health questions depends on its training and our clear instructions.</p>
<p>For example, if a user uploads an image of a rash, the system instruction helps guide Gemini to interpret it from a medical perspective (though it's important to remember that an AI chatbot is not a substitute for professional medical diagnosis).</p>
<h3 id="heading-how-to-run-your-chatbot">How to Run Your Chatbot</h3>
<ol>
<li><p><strong>Replace</strong> <code>apiKey</code>: In <code>lib/screens/chat.dart</code>, replace <code>""</code> with your actual Gemini API key.</p>
</li>
<li><p><strong>Run the application:</strong> In your terminal, navigate to your project's root directory and run: <strong>Bash</strong></p>
<pre><code class="lang-bash"> flutter run
</code></pre>
</li>
</ol>
<p>This will launch the application on a connected device or emulator. You should see the "Medical ChatBot" app with the welcome message and suggested prompts. Try typing some health-related questions, and also experiment with the multi-modal input options (microphone, camera, attachment icon) if your device/emulator supports them.</p>
<h2 id="heading-important-considerations-and-future-enhancements">Important Considerations and Future Enhancements</h2>
<ul>
<li><p><strong>API key security:</strong> Just to reiterate the importance of not hardcoding API keys in production. For a deployed app, consider using environment variables, backend services, or Flutter's build configurations to inject the API key securely.</p>
</li>
<li><p><strong>Error handling:</strong> The current code doesn't explicitly show error handling for API calls. In a real application, you'd want to handle network errors, invalid API keys, or rate limits gracefully. The <code>flutter_ai_toolkit</code> and <code>google_generative_ai</code> packages provide mechanisms for this.</p>
</li>
<li><p><strong>User Experience (UX):</strong></p>
<ul>
<li><p><strong>Loading indicators:</strong> Show a loading indicator while the AI is generating a response.</p>
</li>
<li><p><strong>Input validation:</strong> For certain inputs (for example, file types), you might want to add client-side validation.</p>
</li>
<li><p><strong>Clearance/history:</strong> Implement features to clear chat history or save past conversations.</p>
</li>
</ul>
</li>
<li><p><strong>Medical disclaimer:</strong> Crucially, any medical chatbot should include a prominent disclaimer stating that it is not a substitute for professional medical advice, diagnosis, or treatment. It should always advise users to consult with a qualified healthcare professional for any health concerns.</p>
</li>
<li><p><strong>Privacy and data security:</strong> When dealing with health-related information, data privacy is paramount. Ensure your application complies with relevant regulations (for example, HIPAA in the U.S., GDPR in Europe) and that user data is handled securely. The Gemini API has its own data policies you should review.</p>
</li>
<li><p><strong>Advanced system instructions:</strong> For a more sophisticated medical chatbot, you could expand the <code>systemInstruction</code> to include specific medical knowledge domains, preferred response formats (for example, always list bullet points for symptoms), or even direct the AI to ask clarifying questions.</p>
</li>
<li><p><strong>Tool use/function calling:</strong> Gemini supports tool use (function calling), allowing the AI to interact with external services. For a health bot, this could mean:</p>
<ul>
<li><p>Looking up drug information from a database.</p>
</li>
<li><p>Finding nearby clinics or pharmacies.</p>
</li>
<li><p>Accessing up-to-date medical research.</p>
</li>
<li><p>This would require more complex setup with backend functions that the AI can call.</p>
</li>
</ul>
</li>
<li><p><strong>Speech-to-Text (STT) and Text-to-Speech (TTS):</strong> While <code>flutter_ai_toolkit</code> handles audio input, you might want more fine-grained control over STT and TTS services for improved voice interaction.</p>
</li>
<li><p><strong>Image processing and medical imaging:</strong> For truly advanced medical applications, you might integrate specialized image processing libraries for analyzing medical images (for example, X-rays, MRIs), but this is a complex domain requiring expert knowledge and regulatory compliance. Our current setup allows Gemini to interpret images, but it relies on Gemini's general vision capabilities.</p>
</li>
</ul>
<h2 id="heading-screenshots">Screenshots</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749131323671/5768237e-2f4b-4c6b-aae6-23486dc8bb46.png" alt="5768237e-2f4b-4c6b-aae6-23486dc8bb46" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can check out the full project here: <a target="_blank" href="https://github.com/Atuoha/ai_medical_assistant">https://github.com/Atuoha/ai_medical_assistant</a></p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>You've now successfully built a foundational medical chatbot using Flutter and Google Gemini! This application demonstrates how to integrate a powerful AI model with a multi-modal user interface, while also enforcing specific behavioral constraints (health-only questions).</p>
<p>By extending this base with robust error handling, enhanced UX, and potentially advanced AI features like tool use, you can create even more sophisticated and valuable healthcare applications.</p>
<p>Remember to always prioritize user safety and data privacy when developing AI solutions in the medical domain.</p>
<h3 id="heading-flutter-and-dart-packages"><strong>Flutter and Dart Packages:</strong></h3>
<ul>
<li><p><code>flutter_ai_toolkit</code>:</p>
<ul>
<li><p>Pub.dev page: <a target="_blank" href="https://pub.dev/packages/flutter_ai_toolkit">https://pub.dev/packages/flutter_ai_toolkit</a></p>
</li>
<li><p>Flutter Documentation (AI Toolkit): <a target="_blank" href="https://docs.flutter.dev/ai-toolkit">https://docs.flutter.dev/ai-toolkit</a></p>
</li>
</ul>
</li>
<li><p><code>google_generative_ai</code>:</p>
<ul>
<li>Pub.dev page: <a target="_blank" href="https://www.google.com/search?q=https://pub.dev/packages/google_generative_ai">https://pub.dev/packages/google_generative_ai</a></li>
</ul>
</li>
</ul>
<p><strong>Google Gemini API and AI Studio:</strong></p>
<ul>
<li><p>Google AI Studio: <a target="_blank" href="https://aistudio.google.com/">https://aistudio.google.com/</a></p>
</li>
<li><p>Get a Gemini API Key (Google AI for Developers): <a target="_blank" href="https://ai.google.dev/gemini-api/docs/api-key">https://ai.google.dev/gemini-api/docs/api-key</a></p>
</li>
<li><p>Gemini API Documentation (Google AI for Developers): <a target="_blank" href="https://ai.google.dev/api">https://ai.google.dev/api</a> (General API documentation)</p>
</li>
</ul>
<p><strong>Flutter Documentation:</strong></p>
<ul>
<li>Flutter Official Website (Installation Guide): <a target="_blank" href="https://flutter.dev/docs/get-started/install">https://flutter.dev/docs/get-started/install</a></li>
</ul>
<p><strong>General Concepts (for further reading):</strong></p>
<ul>
<li><p><strong>Material Design:</strong> <a target="_blank" href="https://m3.material.io/">https://m3.material.io/</a> (For understanding Flutter's UI principles)</p>
</li>
<li><p><strong>Large Language Models (LLMs):</strong> A broad topic, but understanding the basics of how LLMs work will enhance comprehension of the <code>systemInstruction</code> and model behavior.</p>
</li>
<li><p><strong>Multimodal AI:</strong> Research on multimodal AI provides context for why Gemini can handle various input types (text, image, audio, and so on).</p>
</li>
<li><p><strong>API Key Security Best Practices:</strong> For production applications, it's crucial to understand secure API key management (for example, environment variables, secret management services). A good starting point would be general security best practices for API keys.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI-Powered Cooking Assistant with Flutter and Gemini ]]>
                </title>
                <description>
                    <![CDATA[ After soaking in everything shared at GoogleIO, I can’t lie – I feel supercharged! From What’s New in Flutter to Building Agentic Apps with Flutter and Firebase AI Logic, and the deep dive into How Flutter Makes the Most of Your Platforms, it felt li... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-an-ai-powered-cooking-assistant-with-flutter-and-gemini/</link>
                <guid isPermaLink="false">68388c4c973615b7f6864e63</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter-aware ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AISprint  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Thu, 29 May 2025 16:33:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748533427117/1c8c2384-c6a3-4ad8-ab40-1eee65b2c914.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>After soaking in everything shared at <a target="_blank" href="https://www.youtube.com/playlist?list=PLOU2XLYxmsIL4mCDJICu2vLPNw-zdcGAt">GoogleIO</a>, I can’t lie – I feel supercharged! From <a target="_blank" href="https://io.google/2025/explore/pa-keynote-12">What’s New in Flutter</a> to <a target="_blank" href="https://io.google/2025/explore/technical-session-6">Building Agentic Apps with Flutter and Firebase AI Logic</a>, and the deep dive into <a target="_blank" href="https://io.google/2025/explore/technical-session-25">How Flutter Makes the Most of Your Platforms</a>, it felt like plugging directly into the Matrix of dev power.</p>
<p>But the absolute showstopper for me? David’s presentation using <a target="_blank" href="https://firebase.studio/">Firebase Studio</a> and <a target="_blank" href="https://builder.io">Builder.io</a> was a masterpiece. I’ve already checked it out, and it’s every bit as awesome as it looked. Pair that with everything Gemini is shipping... and wow. We’re entering a whole new era of app development.</p>
<p>Artificial Intelligence (AI) is no longer a futuristic concept – it's an integral part of our daily lives, transforming how we interact with technology and the world around us.</p>
<p>From personalized recommendations on streaming platforms to intelligent assistants that manage our schedules, AI's applications are vast and ever-expanding. Its ability to process massive datasets, identify patterns, and make informed decisions is revolutionizing industries from healthcare to finance…and now, even cooking!</p>
<p>At the forefront of this AI revolution are powerful platforms like <strong>Google's Vertex AI</strong> and <strong>Gemini</strong>. Vertex AI is a unified machine learning platform that lets you build, deploy, and scale ML models faster and more efficiently. It provides a comprehensive suite of tools for the entire ML workflow, from data preparation to model deployment and monitoring. Think of it as your all-in-one workshop for crafting intelligent systems.</p>
<p>Gemini, on the other hand, is Google's most capable and flexible AI model. It's a multimodal large language model (LLM), meaning it can understand and process information across various modalities – text, images, audio, and more. This makes Gemini incredibly versatile, enabling it to handle complex tasks that require a nuanced understanding of different types of data. For developers, Gemini opens up a world of possibilities for creating highly intelligent and intuitive applications.</p>
<p>Complementing these powerful AI models is <strong>Firebase AI Studio</strong>, a suite of tools within Firebase designed to simplify the integration of AI capabilities into your applications. It streamlines the process of connecting your app to Gemini models, making it easier to leverage the power of generative AI without getting bogged down in complex infrastructure.</p>
<h3 id="heading-building-an-ai-powered-cooking-assistant-with-flutter-and-gemini">Building an AI-Powered Cooking Assistant with Flutter and Gemini</h3>
<p>In this article, I'll demonstrate how I leveraged the combined power of Gemini and Flutter to build an AI-powered cooking assistant.</p>
<p>Fueled by a recent burst of culinary curiosity, I decided to try building an app (Snap2Chef) that could identify any food item from a photo or voice command, provide a detailed recipe, give step-by-step cooking instructions, and even link me to a relevant YouTube video for visual guidance.</p>
<p>Whether I’m exploring new dishes or trying to whip up a meal with what I have on hand, this app powered by Gemini makes the cooking experience smarter and more accessible.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To make the most of this guide, ensure you have the following prerequisites in place (not mandatory):</p>
<ul>
<li><p><strong>Flutter Development Environment:</strong> You should have a working Flutter development setup, including the Flutter SDK, a compatible IDE (like VS Code or Android Studio), and configured emulators or physical devices for testing.</p>
</li>
<li><p><strong>Basic to Intermediate Flutter Knowledge:</strong> Familiarity with Flutter's widget tree, state management (for example, <code>StatefulWidget</code>, <code>setState</code>), asynchronous programming (<code>Future</code>, <code>async/await</code>), and handling user input is essential.</p>
</li>
<li><p><strong>Google Cloud Project and API Key:</strong> You'll need an active Google Cloud project with the Vertex AI API and Gemini API enabled. Ensure you have an API key generated and ready to use. While we'll use it directly in the app for demonstration, <strong>for production applications, it's highly recommended to use a secure backend to proxy your requests to Google's APIs.</strong></p>
</li>
<li><p><strong>Basic Understanding of REST APIs:</strong> Knowing how HTTP requests (GET, POST) and JSON data work will be beneficial, though the <code>google_generative_ai</code> package abstracts much of this.</p>
</li>
<li><p><strong>Assets Configuration:</strong> If you're using a local placeholder image (<code>placeholder.png</code> in <code>assets/images/</code>), ensure your <code>pubspec.yaml</code> file is correctly configured to include this asset.</p>
</li>
</ul>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-how-to-get-your-gemini-api-key">How to Get Your Gemini API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-your-flutter-project-and-dependencies">Set Up Your Flutter Project and Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-core-folder">1. <code>core</code> Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-the-infrastructure-folder">2. The <code>infrastructure</code> Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-the-presentation-folder">3. The presentation Folder</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-permissions-ensuring-app-functionality-and-user-privacy">Permissions: Ensuring App Functionality and User Privacy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-assets-managing-application-resources">Assets: Managing Application Resources</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-app-icons-customizing-your-applications-identity">App Icons: Customizing Your Application's Identity</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-splash-screen-the-first-impression">Splash Screen: The First Impression</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-screenshots-from-the-app">Screenshots from the App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ol>
<h2 id="heading-how-to-get-your-gemini-api-key"><strong>How to Get Your Gemini API Key</strong></h2>
<p>To use the Gemini model, you'll need an API key. You can obtain one by following these steps:</p>
<ol>
<li><p>Go to <a target="_blank" href="https://aistudio.google.com/app/apikey">Google AI Studio</a>.</p>
</li>
<li><p>Sign in with your Google account.</p>
</li>
<li><p>Click on "Get API key" or "Create API key in new project."</p>
</li>
<li><p>Copy the generated API key.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748068929897/6e05ea8a-b80b-4bef-90c7-0ffddafa4965.png" alt="6e05ea8a-b80b-4bef-90c7-0ffddafa4965" class="image--center mx-auto" width="1840" height="718" loading="lazy"></p>
<p><strong>Important Security Note:</strong></p>
<p>In the provided HomeScreen code, the API key is directly embedded as String apiKey = "";. This is not a secure practice for production applications. Hardcoding API keys directly into your client-side code (like a Flutter app) exposes them to reverse engineering and potential misuse.</p>
<p>To secure your API keys in a Flutter application, I highly recommend referring to my article: <a target="_blank" href="https://www.freecodecamp.org/news/how-to-secure-mobile-apis-in-flutter/">How to Secure Mobile APIs in Flutter</a>. This article covers various best practices, including:</p>
<ul>
<li><p>Using environment variables or build configurations.</p>
</li>
<li><p>Storing keys in secure local storage (though still client-side).</p>
</li>
<li><p>Proxying API requests through a backend server to truly hide your API key.</p>
</li>
<li><p>Using Firebase Extensions or Cloud Functions for server-side logic that interacts with AI models, without exposing the key to the client.</p>
</li>
</ul>
<p>For this tutorial, we'll keep it simple, but always prioritize API security in your real-world projects!</p>
<h2 id="heading-set-up-your-flutter-project-and-dependencies"><strong>Set Up Your Flutter Project and Dependencies</strong></h2>
<p>To begin, let's create a new Flutter project and set up the necessary dependencies in your <code>pubspec.yaml</code> file.</p>
<p>First, create a new Flutter project by running:</p>
<pre><code class="lang-bash">flutter create snap2chef
<span class="hljs-built_in">cd</span> snap2chef
</code></pre>
<p>Now, open <code>pubspec.yaml</code> and add the following dependencies:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">flutter:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">google_generative_ai:</span> <span class="hljs-string">^0.4.7</span>
  <span class="hljs-attr">permission_handler:</span> <span class="hljs-string">^12.0.0+1</span>
  <span class="hljs-attr">file_picker:</span> <span class="hljs-string">^10.1.9</span>
  <span class="hljs-attr">image_cropper:</span> <span class="hljs-string">^9.1.0</span>
  <span class="hljs-attr">image_picker:</span> <span class="hljs-string">^1.1.2</span>
  <span class="hljs-attr">path_provider:</span> <span class="hljs-string">^2.1.5</span>
  <span class="hljs-attr">fluttertoast:</span> <span class="hljs-string">^8.2.12</span>
  <span class="hljs-attr">gap:</span> <span class="hljs-string">^3.0.1</span>
  <span class="hljs-attr">iconsax:</span> <span class="hljs-string">^0.0.8</span>
  <span class="hljs-attr">dotted_border:</span> <span class="hljs-string">^2.1.0</span>
  <span class="hljs-attr">youtube_player_flutter:</span> <span class="hljs-string">^9.1.1</span>
  <span class="hljs-attr">flutter_markdown:</span> <span class="hljs-string">^0.7.7+1</span>
  <span class="hljs-attr">loader_overlay:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">flutter_spinkit:</span> <span class="hljs-string">^5.2.1</span>
  <span class="hljs-attr">cached_network_image:</span> <span class="hljs-string">^3.4.1</span>
  <span class="hljs-attr">flutter_native_splash:</span> <span class="hljs-string">^2.4.4</span>
  <span class="hljs-attr">flutter_launcher_icons:</span> <span class="hljs-string">^0.14.3</span>
  <span class="hljs-attr">speech_to_text:</span> <span class="hljs-string">^7.0.0</span>

<span class="hljs-attr">dev_dependencies:</span>
  <span class="hljs-attr">flutter_test:</span>
    <span class="hljs-attr">sdk:</span> <span class="hljs-string">flutter</span>
  <span class="hljs-attr">flutter_lints:</span> <span class="hljs-string">^5.0.0</span>
  <span class="hljs-attr">build_runner:</span> <span class="hljs-string">^2.4.13</span>
</code></pre>
<p>After adding the dependencies, run <code>flutter pub get</code> in your terminal to fetch them:</p>
<pre><code class="lang-bash">flutter pub get
</code></pre>
<h2 id="heading-project-structure"><strong>Project Structure</strong></h2>
<p>We'll organize our project into three main folders (with various subfolders) to maintain a clean and scalable architecture:</p>
<ul>
<li><p><code>core</code>: Contains core functionalities, utilities, and shared components.</p>
</li>
<li><p><code>infrastructure</code>: Manages external services, data handling, and business logic.</p>
</li>
<li><p><code>presentation</code>: Houses the UI layer, including screens, widgets, and components.</p>
</li>
<li><p><code>main.dart</code>: The entry point of our Flutter application.</p>
</li>
</ul>
<p>Let's dive into the details of each folder.</p>
<h3 id="heading-1-the-core-folder">1. The <code>core</code> Folder</h3>
<p>The <code>core</code> folder will contain <code>extensions</code>, <code>constants</code>, and <code>shared</code> utilities.</p>
<h5 id="heading-the-extensions-folder">The <code>extensions</code> <strong>Folder</strong></h5>
<p>This directory will hold extension methods that add new functionalities to existing classes.</p>
<p><code>format_to_mb.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">extension</span> ByTeToMegaByte <span class="hljs-keyword">on</span> <span class="hljs-built_in">int</span> {
  <span class="hljs-built_in">int</span> formatToMegaByte() {
    <span class="hljs-built_in">int</span> bytes = <span class="hljs-keyword">this</span>;
    <span class="hljs-keyword">return</span> (bytes / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>)).ceil();
  }
}
</code></pre>
<p>This extension on the int type (integers) provides a convenient method <code>formatToMegaByte()</code>. When called on an integer representing bytes, it converts that byte value into megabytes. The division by <code>(1024 * 1024)</code> converts bytes to megabytes, and <code>.ceil()</code> rounds the result up to the nearest whole number. This is useful for displaying file sizes in a more human-readable format.</p>
<p><code>loading.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:loader_overlay/loader_overlay.dart'</span>;

<span class="hljs-keyword">extension</span> LoaderOverlayExtension <span class="hljs-keyword">on</span> BuildContext {
  <span class="hljs-keyword">void</span> showLoader() {
    loaderOverlay.<span class="hljs-keyword">show</span>();
  }

  <span class="hljs-keyword">void</span> hideLoader() {
    loaderOverlay.<span class="hljs-keyword">hide</span>();
  }
}
</code></pre>
<p>This extension on <code>BuildContext</code> simplifies the process of showing and hiding a global loading overlay in your Flutter application. It leverages the loader_overlay package.</p>
<ul>
<li><p><code>showLoader()</code>: Calls <code>loaderOverlay.show()</code> to display the loading indicator.</p>
</li>
<li><p><code>hideLoader()</code>: Calls <code>loaderOverlay.hide()</code> to dismiss the loading indicator. These extensions make it easy to control the loader from any widget that has access to a <code>BuildContext</code>.</p>
</li>
</ul>
<p><code>to_file.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;

<span class="hljs-keyword">extension</span> ToFile <span class="hljs-keyword">on</span> Future&lt;XFile?&gt; {
  Future&lt;File?&gt; toFile() =&gt; then((xFile) =&gt; xFile?.path).then(
        (filePath) =&gt; filePath != <span class="hljs-keyword">null</span> ? File(filePath) : <span class="hljs-keyword">null</span>,
      );
}
</code></pre>
<p>This extension is designed to convert an XFile object (typically obtained from the image_picker package) into a dart:io File object.</p>
<ul>
<li><p>It operates on a <code>Future&lt;XFile?&gt;</code>, meaning it expects a future that might resolve to an <code>XFile</code> or <code>null</code>.</p>
</li>
<li><p><code>then((xFile) =&gt; xFile?.path)</code>: If <code>xFile</code> is not null, it extracts the file's path. Otherwise, it passes <code>null</code>.</p>
</li>
<li><p><code>then((filePath) =&gt; filePath != null ? File(filePath) : null)</code>: If a <code>filePath</code> is available, it creates a <code>File</code> object from it. Otherwise, it returns <code>null</code>. This is a concise way to handle the asynchronous conversion of a picked image or video <code>XFile</code> into a <code>File</code> object that can be used for further operations like displaying or uploading.</p>
</li>
</ul>
<p><code>to_file2.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:path_provider/path_provider.dart'</span>;

<span class="hljs-keyword">extension</span> XFileExtension <span class="hljs-keyword">on</span> XFile {
  Future&lt;File&gt; toFile() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> readAsBytes();
    <span class="hljs-keyword">final</span> tempDir = <span class="hljs-keyword">await</span> getTemporaryDirectory();
    <span class="hljs-keyword">final</span> tempFile = File(<span class="hljs-string">'<span class="hljs-subst">${tempDir.path}</span>/<span class="hljs-subst">${<span class="hljs-keyword">this</span>.name}</span>'</span>);
    <span class="hljs-keyword">await</span> tempFile.writeAsBytes(bytes);
    <span class="hljs-keyword">return</span> tempFile;
  }
}
</code></pre>
<p>This extension on XFile provides a more robust way to convert an XFile to a dart:io file. This is particularly useful when you need to write the XFile's content to a temporary location.</p>
<ul>
<li><p><code>await readAsBytes()</code>: Reads the content of the <code>XFile</code> as a list of bytes.</p>
</li>
<li><p><code>final tempDir = await getTemporaryDirectory()</code>: Gets the path to the temporary directory on the device using <code>path_provider</code>.</p>
</li>
<li><p><code>final tempFile = File('${tempDir.path}/${this.name}')</code>: Creates a new <code>File</code> object in the temporary directory with the original name of the <code>XFile</code>.</p>
</li>
<li><p><code>await tempFile.writeAsBytes(bytes)</code>: Writes the bytes read from the <code>XFile</code> into the newly created temporary file.</p>
</li>
<li><p><code>return tempFile</code>: Returns the newly created <code>File</code> object. This is particularly useful when you're working with <code>XFile</code>s that might not have a readily accessible file path on the device, or if you need to ensure the file is persistently available for further processing, such as cropping.</p>
</li>
</ul>
<h4 id="heading-the-constants-folder">The <code>constants</code> Folder</h4>
<p>This directory will hold static values and enumerations used throughout the app.</p>
<p><code>enums/record_source.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> RecordSource { camera, gallery }
</code></pre>
<p>This is a simple enumeration (enum) named <code>RecordSource</code>. It defines two possible values: <code>camera</code> and <code>gallery</code>. This enum is used to represent the source from which an image or video is picked, providing a clear and type-safe way to differentiate between capturing from the camera and selecting from the device's gallery.</p>
<p><code>enums/status.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">enum</span> Status { success, error }
</code></pre>
<p>This is another straightforward enumeration named <code>Status</code>. It defines <code>success</code> and <code>error</code> as its possible values. This enum is commonly used to indicate the outcome of an operation or a process, providing a standardized way to convey status information, for example, for toast messages.</p>
<p><code>app_strings.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// ignore_for_file: constant_identifier_names</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppStrings</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_MODEL = <span class="hljs-string">'gemini-2.0-flash'</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> APP_SUBTITLE =  <span class="hljs-string">"Capture a photo or use your voice to get step-by-step guidance on how to prepare your favorite dishes or snacks"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> APP_TITLE = <span class="hljs-string">"Your Personal AI Recipe Guide"</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_TEXT_PART = <span class="hljs-string">"You are a recipe ai expert. Generate a recipe based on this image, include recipe name, preparation steps, and a public YouTube video demonstrating the preparation step. Output the YouTube video URL on a new line prefixed with 'YouTube Video URL: ', it should be a https URL and the image URL on a new line prefixed with 'Image URL: ' and it should be a https URL too."</span>
      <span class="hljs-string">"If the image is not a food, snacks or drink, politely inform the user that you can only answer recipe queries and ask them to close and upload a food/snack/drink image."</span>;

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> AI_AUDIO_PART =
  <span class="hljs-string">"You are a recipe ai expert. Generate a recipe based on this text, include recipe name, preparation steps. I'd also love for you to show me any valid image online relating to this food/drink/snack and a public YouTube video demonstrating the preparation step.If the text doesn't contain things related to a food, snacks or drink, politely inform the user that you can only answer recipe queries and ask them to close and upload a food/snack/drink image. Output the YouTube video URL on a new line prefixed with 'YouTube Video URL: ', it should be a https URL and the image URL on a new line prefixed with 'Image URL: ' and it should be a https URL too, The text is: "</span>;

}
</code></pre>
<p>This class <code>AppStrings</code> centralizes all the static string constants used throughout the application. This approach helps in managing strings effectively, making them easily modifiable and preventing typos.</p>
<ul>
<li><p><code>AI_MODEL</code>: Specifies the Gemini model to be used, in this case, <code>gemini-2.0-flash</code>.</p>
</li>
<li><p><code>APP_SUBTITLE</code> and <code>APP_TITLE</code>: Define the main titles and subtitles for the app's UI.</p>
</li>
<li><p><code>AI_TEXT_PART</code>: This is a crucial string that serves as the prompt for the Gemini model <strong>when an image is provided</strong>. It instructs the AI to act as a recipe expert, generate a recipe including the name and steps, and provide a YouTube video. It also includes a fallback message if the image isn't food-related.</p>
</li>
<li><p><code>AI_AUDIO_PART</code>: Similar to <code>AI_TEXT_PART</code>, but this prompt is used when <strong>audio input is provided</strong>. It also instructs the AI to generate a recipe, include a relevant online image, and a YouTube video, with specific formatting requirements for the URLs. This prompt will be concatenated with the transcribed text from the user's voice input.</p>
</li>
</ul>
<p><code>app_color.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppColors</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> primaryColor = Color(<span class="hljs-number">0xFF7E57C2</span>);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> litePrimary = Color(<span class="hljs-number">0xFFEDE7F6</span>);
  <span class="hljs-keyword">static</span> Color errorColor = <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xFFEA5757</span>);
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> Color grey =
  Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">170</span>, <span class="hljs-number">170</span>, <span class="hljs-number">170</span>);

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> Color lighterGrey =
  Color.fromARGB(<span class="hljs-number">255</span>, <span class="hljs-number">204</span>, <span class="hljs-number">204</span>, <span class="hljs-number">204</span>);
}
</code></pre>
<p>The <code>AppColors</code> class centralizes all the custom color definitions used in the application. This makes it easy to maintain a consistent color scheme throughout the UI and allows for quick global changes to the app's theme. Each static constant represents a specific color with its hexadecimal value or RGB value.</p>
<h4 id="heading-the-shared-folder">The <code>shared</code> Folder</h4>
<p>This directory will contain shared utility classes.</p>
<p><code>image_picker_helper.dart</code>:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:developer'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:file_picker/file_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span> <span class="hljs-keyword">show</span> immutable;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_picker/image_picker.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:permission_handler/permission_handler.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/to_file.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/to_file2.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../presentation/components/toast_info.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../constants/enums/status.dart'</span>;

<span class="hljs-meta">@immutable</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePickerHelper</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ImagePicker _imagePicker = ImagePicker();

  <span class="hljs-keyword">static</span> Future&lt;PickedFileWithInfo?&gt; pickImageFromGallery2() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> isGranted = <span class="hljs-keyword">await</span> Permission.photos.isGranted;
    <span class="hljs-keyword">if</span> (!isGranted) {
      <span class="hljs-keyword">await</span> Permission.photos.request();
      toastInfo(
          msg: <span class="hljs-string">"You didn't allow access"</span>, status: Status.error);
    }
    <span class="hljs-keyword">final</span> pickedFile =
    <span class="hljs-keyword">await</span> _imagePicker.pickImage(source: ImageSource.gallery);
    <span class="hljs-keyword">if</span> (pickedFile != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> file = <span class="hljs-keyword">await</span> pickedFile.toFile();
      log(pickedFile.name.split(<span class="hljs-string">"."</span>).join(<span class="hljs-string">","</span>));
      <span class="hljs-keyword">return</span> PickedFileWithInfo(file: file, fileName: pickedFile.name);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }
  }

  <span class="hljs-keyword">static</span> Future&lt;FilePickerResult?&gt; pickFileFromGallery() =&gt;
      FilePicker.platform.pickFiles(
          type: FileType.custom,
          allowedExtensions: [<span class="hljs-string">"pdf"</span>, <span class="hljs-string">"doc"</span>, <span class="hljs-string">"docx"</span>, <span class="hljs-string">"png"</span>, <span class="hljs-string">"jpg"</span>, <span class="hljs-string">"jpeg"</span>]);

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; pickImageFromGallery() =&gt;
      _imagePicker.pickImage(source: ImageSource.gallery).toFile();

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; takePictureFromCamera() =&gt;
      _imagePicker.pickImage(source: ImageSource.camera).toFile();

  <span class="hljs-keyword">static</span> Future&lt;File?&gt; pickVideoFromGallery() =&gt;
      _imagePicker.pickVideo(source: ImageSource.gallery).toFile();

  <span class="hljs-keyword">static</span> Future&lt;FilePickerResult?&gt; pickSinglePDFFileFromGallery() =&gt;
      FilePicker.platform
          .pickFiles(type: FileType.custom, allowedExtensions: [<span class="hljs-string">"pdf"</span>]);
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PickedFileWithInfo</span> </span>{
  <span class="hljs-keyword">final</span> File file;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> fileName;

  PickedFileWithInfo({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.file, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.fileName});
}

PlatformFile? file;
</code></pre>
<p>The <code>ImagePickerHelper</code> class provides static methods for picking various types of files (images, videos, documents) from the device's gallery or camera, with integrated permission handling.</p>
<ul>
<li><p><code>_imagePicker</code>: An instance of <code>ImagePicker</code> for interacting with the device's image and video picking functionalities.</p>
</li>
<li><p><code>pickImageFromGallery2()</code>:</p>
<ul>
<li><p><strong>Permission handling</strong>: Checks if photo gallery permission is granted using <code>permission_handler</code>. If not, it requests the permission and displays a toast message if denied.</p>
</li>
<li><p><strong>Image picking</strong>: Uses <code>_imagePicker.pickImage(source: ImageSource.gallery)</code> to let the user select an image from the gallery.</p>
</li>
<li><p><strong>Conversion</strong>: If an image is picked, it converts the <code>XFile</code> to a <code>File</code> object using the <code>toFile()</code> extension.</p>
</li>
<li><p><strong>Logging</strong>: Logs the file name for debugging.</p>
</li>
<li><p><strong>Return value</strong>: Returns a <code>PickedFileWithInfo</code> object containing the <code>File</code> and <code>fileName</code>.</p>
</li>
</ul>
</li>
<li><p><code>pickFileFromGallery()</code>: Uses <code>file_picker</code> to allow picking various file types (PDF, Doc, Docx, PNG, JPG, JPEG) from the gallery.</p>
</li>
<li><p><code>pickImageFromGallery()</code>: A simpler method to pick an image from the gallery, directly returning a <code>Future&lt;File?&gt;</code> using the <code>toFile()</code> extension.</p>
</li>
<li><p><code>takePictureFromCamera()</code>: Captures an image using the device's camera and returns a <code>Future&lt;File?&gt;</code>.</p>
</li>
<li><p><code>pickVideoFromGallery()</code>: Picks a video from the gallery and returns a <code>Future&lt;File?&gt;</code>.</p>
</li>
<li><p><code>pickSinglePDFFileFromGallery()</code>: Specifically picks a single PDF file from the gallery.</p>
</li>
<li><p><code>PickedFileWithInfo</code> class: A simple data class to hold both the <code>File</code> object and its <code>fileName</code>.</p>
</li>
</ul>
<p>This helper class centralizes all file picking logic, making it reusable and easier to manage permissions and different picking scenarios.</p>
<h3 id="heading-2-the-infrastructure-folder">2. The <code>infrastructure</code> Folder</h3>
<p>This folder handles the logic for interacting with external services and processing data.</p>
<h5 id="heading-imageuploadcontrollerdart"><code>image_upload_controller.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_cropper/image_cropper.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/enums/record_source.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/shared/image_picker_helper.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../presentation/widgets/image_picker_component.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageUploadController</span> </span>{
  <span class="hljs-comment">/// <span class="markdown">crop image</span></span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _cropImage(
      File? selectedFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,
      ) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> croppedFile = <span class="hljs-keyword">await</span> ImageCropper().cropImage(
        sourcePath: selectedFile.path,
        compressFormat: ImageCompressFormat.jpg,
        compressQuality: <span class="hljs-number">100</span>,
        uiSettings: [
          AndroidUiSettings(
            toolbarTitle: <span class="hljs-string">'Crop Image'</span>,
            toolbarColor: AppColors.primaryColor,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.square,
            lockAspectRatio: <span class="hljs-keyword">false</span>,
            statusBarColor: AppColors.primaryColor,
            activeControlsWidgetColor: AppColors.primaryColor,
            aspectRatioPresets: [
              CropAspectRatioPreset.original,
              CropAspectRatioPreset.square,
              CropAspectRatioPreset.ratio4x3,
              CropAspectRatioPresetCustom(),
            ],
          ),
          IOSUiSettings(
            title: <span class="hljs-string">'Crop Image'</span>,
            aspectRatioPresets: [
              CropAspectRatioPreset.original,
              CropAspectRatioPreset.square,
              CropAspectRatioPreset.ratio4x3,
              CropAspectRatioPresetCustom(),
            ],
          ),
        ],
      );
      assignCroppedImage(croppedFile);
    }
  }

  <span class="hljs-comment">// /// pick image from camera and gallery</span>
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> imagePicker(
      RecordSource recordSource,
      Completer? completer,
      BuildContext context,
      <span class="hljs-built_in">Function</span> setFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,
      ) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (recordSource == RecordSource.gallery) {
      <span class="hljs-keyword">final</span> pickedFile = <span class="hljs-keyword">await</span> ImagePickerHelper.pickImageFromGallery();
      <span class="hljs-keyword">if</span> (pickedFile == <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">return</span>;
      }
      completer?.complete(pickedFile.path);
      <span class="hljs-keyword">if</span> (!context.mounted) {
        <span class="hljs-keyword">return</span>;
      }
      setFile(pickedFile);

      <span class="hljs-keyword">if</span> (context.mounted) {
        Navigator.of(context).pop();
      }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (recordSource == RecordSource.camera) {
      <span class="hljs-keyword">final</span> pickedFile = <span class="hljs-keyword">await</span> ImagePickerHelper.takePictureFromCamera();
      <span class="hljs-keyword">if</span> (pickedFile == <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">return</span>;
      }

      completer?.complete(pickedFile.path);
      <span class="hljs-keyword">if</span> (!context.mounted) {
        <span class="hljs-keyword">return</span>;
      }
      setFile(pickedFile);
      <span class="hljs-comment">// crop image</span>
      _cropImage(pickedFile, assignCroppedImage);

      <span class="hljs-keyword">if</span> (context.mounted) {
        Navigator.of(context).pop();
      }
    }
  }

  <span class="hljs-comment">/// <span class="markdown">modal for selecting file source</span></span>
  <span class="hljs-keyword">static</span> Future showFilePickerButtonSheet(BuildContext context, Completer? completer,
      <span class="hljs-built_in">Function</span> setFile,
      <span class="hljs-built_in">Function</span> assignCroppedImage,) {
    <span class="hljs-keyword">return</span> showModalBottomSheet(
      shape: <span class="hljs-keyword">const</span> RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(<span class="hljs-number">35</span>),
          topRight: Radius.circular(<span class="hljs-number">35</span>),
        ),
      ),
      context: context,
      builder: (context) {
        <span class="hljs-keyword">return</span> SingleChildScrollView(
          child: Container(
            padding: <span class="hljs-keyword">const</span> EdgeInsets.fromLTRB(<span class="hljs-number">10</span>, <span class="hljs-number">14</span>, <span class="hljs-number">15</span>, <span class="hljs-number">20</span>),
            child: Column(
              children: [
                Container(
                  height: <span class="hljs-number">4</span>,
                  width: <span class="hljs-number">50</span>,
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.only(top: <span class="hljs-number">5</span>),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                    color: <span class="hljs-keyword">const</span> Color(<span class="hljs-number">0xffE4E4E4</span>),
                  ),
                ),
                Padding(
                  padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">10.0</span>),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      GestureDetector(
                        onTap: () =&gt; Navigator.of(context).pop(),
                        child: <span class="hljs-keyword">const</span> Align(
                          alignment: Alignment.topRight,
                          child: Icon(Icons.close, color: Colors.grey),
                        ),
                      ),
                      <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Select Image Source'</span>,
                        style: TextStyle(
                          color: AppColors.primaryColor,
                          fontSize: <span class="hljs-number">16</span>,
                          fontWeight: FontWeight.w600,
                        ),
                      ),
                      <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
                      ImagePickerTile(
                        title: <span class="hljs-string">'Capture from Camera'</span>,
                        subtitle: <span class="hljs-string">'Take a live snapshot'</span>,
                        icon: Iconsax.camera,
                        recordSource: RecordSource.camera,
                        completer: completer,
                        context: context,
                        setFile: setFile,
                        assignCroppedImage: assignCroppedImage,
                      ),
                      <span class="hljs-keyword">const</span> Divider(color: Color(<span class="hljs-number">0xffE4E4E4</span>)),
                      ImagePickerTile(
                        title: <span class="hljs-string">'Upload from Gallery'</span>,
                        subtitle: <span class="hljs-string">'Select image from gallery'</span>,
                        icon: Iconsax.gallery,
                        recordSource: RecordSource.gallery,
                        completer: completer,
                        context: context,
                        setFile: setFile,
                        assignCroppedImage: assignCroppedImage,
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CropAspectRatioPresetCustom</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">CropAspectRatioPresetData</span> </span>{
  <span class="hljs-meta">@override</span>
  (<span class="hljs-built_in">int</span>, <span class="hljs-built_in">int</span>)? <span class="hljs-keyword">get</span> data =&gt; (<span class="hljs-number">2</span>, <span class="hljs-number">3</span>);

  <span class="hljs-meta">@override</span>
  <span class="hljs-built_in">String</span> <span class="hljs-keyword">get</span> name =&gt; <span class="hljs-string">'2x3 (customized)'</span>;
}
</code></pre>
<p>The <code>ImageUploadController</code> class manages the process of picking and optionally cropping images before they are used in the application.</p>
<ul>
<li><p><code>_cropImage(File? selectedFile, Function assignCroppedImage)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles the image cropping functionality using the <code>image_cropper</code> package.</p>
</li>
<li><p>It takes a <code>selectedFile</code> (the image to be cropped) and a <code>Function assignCroppedImage</code> (a callback to update the UI with the cropped image).</p>
</li>
<li><p><code>ImageCropper().cropImage(...)</code> opens the cropping UI. It's configured with various UI settings for both Android and iOS, including <code>toolbarColor</code>, <code>aspectRatioPresets</code>, and more, to ensure a consistent and branded experience.</p>
</li>
<li><p><code>CropAspectRatioPresetCustom()</code>: This is a custom class that implements <code>CropAspectRatioPresetData</code> to define a specific cropping aspect ratio (2x3 in this case), providing more flexibility than the built-in presets.</p>
</li>
<li><p>Once cropped, the <code>croppedFile</code> is passed to the <code>assignCroppedImage</code> callback.</p>
</li>
</ul>
</li>
<li><p><code>imagePicker(RecordSource recordSource, Completer? completer, BuildContext context, Function setFile, Function assignCroppedImage)</code>:</p>
<ul>
<li><p>This <strong>static method</strong> is the core logic for initiating image picking from either the camera or gallery.</p>
</li>
<li><p>It takes a <code>recordSource</code> (from the <code>RecordSource</code> enum), an optional <code>completer</code> (likely for handling asynchronous operations outside the UI), the current <code>context</code>, <code>setFile</code> (a callback to set the picked file in the UI), and <code>assignCroppedImage</code> (the callback for cropped images).</p>
</li>
<li><p><strong>Gallery Selection (</strong><code>RecordSource.gallery</code>):</p>
<ul>
<li><p>It calls <code>ImagePickerHelper.pickImageFromGallery()</code> to get the selected image.</p>
</li>
<li><p>If a file is picked, it completes the <code>completer</code>, calls <code>setFile</code> to update the UI, and then pops the bottom sheet.</p>
</li>
</ul>
</li>
<li><p><strong>Camera Capture (</strong><code>RecordSource.camera</code>):</p>
<ul>
<li><p>It calls <code>ImagePickerHelper.takePictureFromCamera()</code> to capture an image.</p>
</li>
<li><p>Similar to gallery selection, it completes the <code>completer</code>, calls <code>setFile</code>, and then importantly, it calls <code>_cropImage</code> to allow the user to crop the newly captured image before it's fully used.</p>
</li>
<li><p>Finally, it pops the bottom sheet.</p>
</li>
</ul>
</li>
<li><p><code>context.mounted</code> checks are included to ensure that UI updates only happen if the widget is still in the widget tree, preventing errors.</p>
</li>
</ul>
</li>
<li><p><code>showFilePickerButtonSheet(...)</code>:</p>
<ul>
<li><p>This <strong>static method</strong> displays a modal bottom sheet, providing the user with options to select an image source (Camera or Gallery).</p>
</li>
<li><p>It uses <code>showModalBottomSheet</code> to present a nicely styled sheet with rounded corners.</p>
</li>
<li><p>Inside the sheet, it displays a draggable indicator and two <code>ImagePickerTile</code> widgets (presumably a custom widget for displaying each option) for "Capture from Camera" and "Upload from Gallery."</p>
</li>
<li><p>When an <code>ImagePickerTile</code> is tapped, it internally calls the <code>imagePicker</code> method with the corresponding <code>RecordSource</code>.</p>
</li>
</ul>
</li>
</ul>
<p>In summary, <code>ImageUploadController</code> acts as a central orchestrator for image acquisition, offering options to pick from the gallery or camera, and integrating robust image cropping capabilities – all while ensuring a smooth user experience through UI callbacks and modal interactions.</p>
<h5 id="heading-recipecontrollerdart"><code>recipe_controller.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:cached_network_image/cached_network_image.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_markdown/flutter_markdown.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:google_generative_ai/google_generative_ai.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/loading.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:youtube_player_flutter/youtube_player_flutter.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/app_strings.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../core/constants/enums/status.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../presentation/components/toast_info.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RecipeController</span> </span>{
  <span class="hljs-comment">// send image to gemini</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _sendImageToGemini(
      File? selectedFile,
      GenerativeModel model,
      BuildContext context,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    toastInfo(msg: <span class="hljs-string">"Obtaining recipe and preparations"</span>, status: Status.success);

    <span class="hljs-keyword">if</span> (selectedFile == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> selectedFile.readAsBytes();

    <span class="hljs-keyword">final</span> prompt = TextPart(AppStrings.AI_TEXT_PART);
    <span class="hljs-keyword">final</span> image = DataPart(<span class="hljs-string">'image/jpeg'</span>, bytes);

    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> model.generateContent([
      Content.multi([prompt, image]),
    ]);

    <span class="hljs-keyword">if</span> (context.mounted) {
      _displayRecipe(
        response.text,
        context,
        selectedFile,
        removeFile,
        removeText,
      );
    }
  }

  <span class="hljs-comment">// send audio text prompt</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; _sendAudioTextPrompt(
      GenerativeModel model,
      BuildContext context,
      <span class="hljs-built_in">String</span> transcribedText,
      File? selectedFile,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    toastInfo(msg: <span class="hljs-string">"Obtaining recipe and preparations"</span>, status: Status.success);

    <span class="hljs-keyword">final</span> prompt = <span class="hljs-string">'<span class="hljs-subst">${AppStrings.AI_AUDIO_PART}</span> <span class="hljs-subst">${transcribedText.trim()}</span>.'</span>;
    <span class="hljs-keyword">final</span> content = [Content.text(prompt)];
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> model.generateContent(content);

    <span class="hljs-keyword">if</span> (context.mounted) {
      _displayRecipe(
        response.text,
        context,
        selectedFile,
        removeFile,
        removeText,
      );
    }
  }

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> _displayRecipe(
      <span class="hljs-built_in">String?</span> recipeText,
      BuildContext context,
      File? selectedFile,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">Function</span> removeText,
      ) {
    <span class="hljs-keyword">if</span> (recipeText == <span class="hljs-keyword">null</span> || recipeText.isEmpty) {
      recipeText = <span class="hljs-string">"No recipe could be generated or parsed from the response."</span>;
    }
    <span class="hljs-built_in">String</span> workingRecipeText = recipeText;

    <span class="hljs-built_in">String?</span> videoId;
    <span class="hljs-built_in">String?</span> extractedImageUrl;

    <span class="hljs-keyword">final</span> youtubeLineRegex = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'YouTube Video URL:\s*(https?:\/\/\S+)'</span>, caseSensitive: <span class="hljs-keyword">false</span>);
    <span class="hljs-keyword">final</span> youtubeMatch = youtubeLineRegex.firstMatch(recipeText);
    <span class="hljs-keyword">if</span> (youtubeMatch != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">final</span> youtubeUrl = youtubeMatch.group(<span class="hljs-number">1</span>);
      <span class="hljs-keyword">final</span> ytIdRegex = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'v=([\w-]{11})'</span>);
      <span class="hljs-keyword">final</span> ytIdMatch = ytIdRegex.firstMatch(youtubeUrl ?? <span class="hljs-string">''</span>);
      <span class="hljs-keyword">if</span> (ytIdMatch != <span class="hljs-keyword">null</span>) {
        videoId = ytIdMatch.group(<span class="hljs-number">1</span>);
      }
      workingRecipeText = workingRecipeText.replaceAll(youtubeMatch.group(<span class="hljs-number">0</span>)!, <span class="hljs-string">''</span>).trim();
    }

    <span class="hljs-keyword">final</span> imageLine = <span class="hljs-built_in">RegExp</span>(<span class="hljs-string">r'Image URL:\s*(https?:\/\/\S+\.(?:png|jpe?g|gif|webp|bmp|svg))'</span>);
    <span class="hljs-keyword">final</span> imageMatch = imageLine.firstMatch(recipeText);
    <span class="hljs-keyword">if</span> (imageMatch != <span class="hljs-keyword">null</span>) {
      extractedImageUrl = imageMatch.group(<span class="hljs-number">1</span>);
      workingRecipeText = workingRecipeText.replaceAll(imageMatch.group(<span class="hljs-number">0</span>)!, <span class="hljs-string">''</span>).trim();
    }

    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Extracted Image URL: <span class="hljs-subst">$extractedImageUrl</span>"</span>);
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Extracted Video ID: <span class="hljs-subst">$videoId</span>"</span>);

    <span class="hljs-built_in">String?</span> cleanedRecipeText = workingRecipeText;

    showDialog(
      barrierDismissible: <span class="hljs-keyword">false</span>,
      context: context,
      builder: (BuildContext dialogContext) {
        YoutubePlayerController? ytController;

        <span class="hljs-keyword">if</span> (videoId != <span class="hljs-keyword">null</span>) {
          ytController = YoutubePlayerController(
            initialVideoId: videoId,
            flags: <span class="hljs-keyword">const</span> YoutubePlayerFlags(
              autoPlay: <span class="hljs-keyword">false</span>,
              mute: <span class="hljs-keyword">false</span>,
              disableDragSeek: <span class="hljs-keyword">false</span>,
              loop: <span class="hljs-keyword">false</span>,
              isLive: <span class="hljs-keyword">false</span>,
              forceHD: <span class="hljs-keyword">false</span>,
              enableCaption: <span class="hljs-keyword">true</span>,
            ),
          );
        }

        <span class="hljs-keyword">return</span> AlertDialog(
          title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Generated Recipe'</span>),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                selectedFile != <span class="hljs-keyword">null</span>
                    ? Container(
                  height: <span class="hljs-number">150</span>,
                  width: <span class="hljs-built_in">double</span>.infinity,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                    border: Border.all(color: AppColors.primaryColor),
                    image: DecorationImage(
                      image: FileImage(File(selectedFile.path)),
                      fit: BoxFit.cover,
                    ),
                  ),
                )
                    :  extractedImageUrl != <span class="hljs-keyword">null</span>
                    ? ClipRRect(
                  borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
                  child: CachedNetworkImage(
                    imageUrl: extractedImageUrl,
                    height: <span class="hljs-number">150</span>,
                    width: <span class="hljs-built_in">double</span>.infinity,
                    fit: BoxFit.cover,
                    placeholder: (context, url) =&gt;
                        Image.asset(<span class="hljs-string">'assets/images/placeholder.png'</span>, fit: BoxFit.cover),
                    errorWidget: (context, url, error) =&gt;
                        Image.asset(<span class="hljs-string">'assets/images/placeholder.png'</span>, fit: BoxFit.cover),
                  ),
                )
                    : <span class="hljs-keyword">const</span> SizedBox.shrink(),
                Gap(<span class="hljs-number">16</span>),
                MarkdownBody(
                  data: cleanedRecipeText,
                  styleSheet: MarkdownStyleSheet(
                    h1: <span class="hljs-keyword">const</span> TextStyle(
                      fontSize: <span class="hljs-number">24</span>,
                      fontWeight: FontWeight.bold,
                      color: Colors.deepPurple,
                    ),
                    h2: <span class="hljs-keyword">const</span> TextStyle(
                      fontSize: <span class="hljs-number">20</span>,
                      fontWeight: FontWeight.bold,
                    ),
                    strong: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),

                <span class="hljs-keyword">if</span> (videoId != <span class="hljs-keyword">null</span> &amp;&amp; ytController != <span class="hljs-keyword">null</span>) ...[
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">16</span>),
                  YoutubePlayer(
                    controller: ytController,
                    showVideoProgressIndicator: <span class="hljs-keyword">true</span>,
                    progressIndicatorColor: AppColors.primaryColor,
                    progressColors: <span class="hljs-keyword">const</span> ProgressBarColors(
                      playedColor: AppColors.primaryColor,
                      handleColor: Colors.amberAccent,
                    ),
                    onReady: () {
                      <span class="hljs-comment">// Controller is ready</span>
                    },
                  ),
                ],
              ],
            ),
          ),
          actions: &lt;Widget&gt;[
            TextButton(
              onPressed: () {
                ytController?.dispose();
                Navigator.of(dialogContext).pop();
                <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
                  removeFile();
                } <span class="hljs-keyword">else</span> {
                  removeText();
                }
              },
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Close'</span>),
            ),
          ],
        );
      },
    );
  }

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> sendRequest(
      BuildContext context,
      File? selectedFile,
      GenerativeModel model,
      <span class="hljs-built_in">Function</span> removeFile,
      <span class="hljs-built_in">String</span> transcribedText,
      <span class="hljs-built_in">Function</span> removeText,
      ) <span class="hljs-keyword">async</span> {
    context.showLoader();
    toastInfo(msg: <span class="hljs-string">"Processing..."</span>, status: Status.success);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">await</span> _sendImageToGemini(
          selectedFile,
          model,
          context,
          removeFile,
          removeText,
        );
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (transcribedText.isNotEmpty) {
        <span class="hljs-keyword">await</span> _sendAudioTextPrompt(
          model,
          context,
          transcribedText,
          selectedFile,
          removeFile,
          removeText,
        );
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">if</span> (kDebugMode) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">'Error sending request: <span class="hljs-subst">$e</span>'</span>);
      }
      toastInfo(msg: <span class="hljs-string">"Error sending request:<span class="hljs-subst">$e</span> "</span>, status: Status.error);
    } <span class="hljs-keyword">finally</span> {
      <span class="hljs-keyword">if</span> (context.mounted) {
        context.hideLoader();
      }
    }
  }
}
</code></pre>
<p>The <code>RecipeController</code> class is responsible for interacting with the Gemini AI model to generate recipes and then display these recipes to the user, complete with parsed YouTube video links and potentially extracted image URLs.</p>
<ul>
<li><p><code>_sendImageToGemini(File? selectedFile, GenerativeModel model, BuildContext context, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles sending an image to the Gemini model.</p>
</li>
<li><p>It displays a "Processing..." toast message.</p>
</li>
<li><p>It reads the <code>selectedFile</code> (the image) as bytes.</p>
</li>
<li><p>It creates a <code>TextPart</code> from <code>AppStrings.AI_TEXT_PART</code> (our image-based AI prompt) and a <code>DataPart</code> for the image bytes.</p>
</li>
<li><p><code>model.generateContent([Content.multi([prompt, image])])</code>: This is where the magic happens! It sends both the text prompt and the image data to the Gemini model for generation.</p>
</li>
<li><p>Upon receiving a response, it calls <code>_displayRecipe</code> to show the generated recipe to the user.</p>
</li>
<li><p><code>context.mounted</code> check ensures the context is still valid before attempting UI updates.</p>
</li>
</ul>
</li>
<li><p><code>_sendAudioTextPrompt(GenerativeModel model, BuildContext context, String transcribedText, File? selectedFile, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> handles sending transcribed audio text to the Gemini model.</p>
</li>
<li><p>It constructs a full prompt by concatenating <code>AppStrings.AI_AUDIO_PART</code> with the <code>transcribedText</code>.</p>
</li>
<li><p><code>model.generateContent([Content.text(prompt)])</code>: It sends only the text prompt to the Gemini model.</p>
</li>
<li><p>Similar to the image method, it calls <code>_displayRecipe</code> with the generated text.</p>
</li>
</ul>
</li>
<li><p><code>_displayRecipe(String? recipeText, BuildContext context, File? selectedFile, Function removeFile, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>private static method</strong> is responsible for parsing the AI's response and displaying it in a modal dialog.</p>
</li>
<li><p><strong>Error handling</strong>: If <code>recipeText</code> is null or empty, it provides a default message.</p>
</li>
<li><p><strong>Extracting YouTube video URL</strong>: It uses a <code>RegExp</code> (<code>youtubeLineRegex</code>) to find a line in the <code>recipeText</code> that matches the "YouTube Video URL: https://..." pattern. If found, it extracts the full URL and then another <code>RegExp</code> (<code>ytIdRegex</code>) to get the YouTube video ID. The extracted video URL text is then removed from <code>workingRecipeText</code> to clean the displayed recipe.</p>
</li>
<li><p><strong>Extracting image URL</strong>: Similarly, it uses another <code>RegExp</code> (<code>imageLine</code>) to extract an image URL from the <code>recipeText</code>. The extracted image URL text is also removed.</p>
</li>
<li><p><strong>Debug printing</strong>: Prints the extracted URLs for debugging.</p>
</li>
<li><p><code>showDialog</code>: Presents an <code>AlertDialog</code> to the user.</p>
<ul>
<li><p><code>YoutubePlayerController</code>: If a <code>videoId</code> was extracted, it initializes a <code>YoutubePlayerController</code> from the <code>Youtubeer_flutter</code> package, configured with basic flags (for example, <code>autoPlay: false</code>).</p>
</li>
<li><p><strong>Recipe display</strong>:</p>
<ul>
<li><p>If an <code>selectedFile</code> (image taken by the user) is present, it displays that image.</p>
</li>
<li><p>Otherwise, if an <code>extractedImageUrl</code> was found in the AI's response, it uses <code>CachedNetworkImage</code> to display that image. This is particularly useful for text-based queries where Gemini might suggest an image.</p>
</li>
<li><p><code>MarkdownBody</code>: Uses <code>flutter_markdown</code> to render the <code>cleanedRecipeText</code> (after removing the YouTube and Image URLs) as Markdown, allowing for rich text formatting (for example, bolding, headings) directly from the AI's response.</p>
</li>
<li><p><code>YoutubePlayer</code>: If a <code>videoId</code> and <code>ytController</code> are available, it embeds the YouTube video player directly into the dialog, with customizable progress bar colors.</p>
</li>
</ul>
</li>
<li><p><strong>"Close" button</strong>: Disposes the <code>ytController</code> (important for resource management), pops the dialog, and calls either <code>removeFile()</code> or <code>removeText()</code> to clear the input fields based on what was used for the query.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>sendRequest(BuildContext context, File? selectedFile, GenerativeModel model, Function removeFile, String transcribedText, Function removeText)</code>:</p>
<ul>
<li><p>This <strong>public static method</strong> is the entry point for sending requests to the Gemini model.</p>
</li>
<li><p><code>context.showLoader()</code>: Displays a loading overlay using our custom extension.</p>
</li>
<li><p><code>toastInfo(msg: "Processing...", status: Status.success)</code>: Shows a toast message.</p>
</li>
<li><p><strong>Conditional logic</strong>:</p>
<ul>
<li><p>If <code>selectedFile</code> is not null, it calls <code>_sendImageToGemini</code>.</p>
</li>
<li><p>Otherwise, if <code>transcribedText</code> is not empty, it calls <code>_sendAudioTextPrompt</code>.</p>
</li>
</ul>
</li>
<li><p><strong>Error handling</strong>: Uses a <code>try-catch</code> block to gracefully handle any errors during the AI request, logging them in debug mode and showing an error toast to the user.</p>
</li>
<li><p><code>finally</code> Block: Ensures <code>context.hideLoader()</code> is always called, regardless of success or error, to dismiss the loading indicator.</p>
</li>
</ul>
</li>
</ul>
<p>In essence, <code>RecipeController</code> orchestrates the entire process of sending user input (image or voice), communicating with the Gemini AI, parsing its intelligent response, and beautifully presenting it to the user with interactive elements like YouTube videos and relevant images.</p>
<h3 id="heading-3-the-presentation-folder">3. The <code>presentation</code> Folder</h3>
<p>This folder contains all the UI-related code.</p>
<h5 id="heading-screenshomescreendart"><code>screens/home_screen.dart</code>:</h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:google_generative_ai/google_generative_ai.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:image_cropper/image_cropper.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/core/extensions/format_to_mb.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/recipe_controller.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:speech_to_text/speech_recognition_result.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:speech_to_text/speech_to_text.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_strings.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/enums/status.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../components/toast_info.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/glowing_microphone.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/image_previewer.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/query_text_box.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../widgets/upload_container.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> HomeScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; </span>{
  File? selectedFile;
  Completer? completer;
  <span class="hljs-built_in">String?</span> fileName;
  <span class="hljs-built_in">int?</span> fileSize;
  <span class="hljs-keyword">late</span> GenerativeModel _model;
  <span class="hljs-built_in">String</span> apiKey = <span class="hljs-string">""</span>; <span class="hljs-comment">// &lt;--- REPLACE WITH YOUR ACTUAL API KEY</span>
  <span class="hljs-keyword">final</span> TextEditingController _query = TextEditingController();
  <span class="hljs-keyword">final</span> SpeechToText _speechToText = SpeechToText();
  <span class="hljs-built_in">bool</span> _speechEnabled = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">String</span> _lastWords = <span class="hljs-string">''</span>;
  <span class="hljs-built_in">bool</span> isRecording = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">bool</span> isDoneRecording = <span class="hljs-keyword">false</span>;

  <span class="hljs-keyword">void</span> removeText() {
    setState(() {
      _query.clear();
      isDoneRecording = <span class="hljs-keyword">false</span>;
      _lastWords = <span class="hljs-string">""</span>;
    });
    _query.clear();
  }

  <span class="hljs-keyword">void</span> setKeyword(<span class="hljs-built_in">String</span> prompt) {
    <span class="hljs-keyword">if</span> (prompt.isEmpty) {
      toastInfo(msg: <span class="hljs-string">"You didn't say anything!"</span>, status: Status.error);
      setState(() {
        isDoneRecording = <span class="hljs-keyword">false</span>;
        isRecording = <span class="hljs-keyword">false</span>;
      });
      <span class="hljs-keyword">return</span>;
    }

    setState(() {
      _lastWords = <span class="hljs-string">""</span>;
      isRecording = <span class="hljs-keyword">false</span>;
      _query.text = prompt;
      isDoneRecording = <span class="hljs-keyword">true</span>;
    });
  }

  <span class="hljs-keyword">void</span> _initSpeech() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      _speechEnabled = <span class="hljs-keyword">await</span> _speechToText.initialize(
        onStatus: (status) =&gt; debugPrint(<span class="hljs-string">'Speech status: <span class="hljs-subst">$status</span>'</span>),
        onError: (error) =&gt; debugPrint(<span class="hljs-string">'Speech error: <span class="hljs-subst">$error</span>'</span>),
      );
      <span class="hljs-keyword">if</span> (!_speechEnabled) {
        toastInfo(
          msg: <span class="hljs-string">"Microphone permission not granted or speech not available."</span>,
          status: Status.error,
        );
      }
      setState(() {});
    } <span class="hljs-keyword">catch</span> (e) {
      debugPrint(<span class="hljs-string">"Speech initialization failed: <span class="hljs-subst">$e</span>"</span>);
    }
  }

  <span class="hljs-keyword">void</span> _startListening() <span class="hljs-keyword">async</span> {
    setState(() {
      isRecording = <span class="hljs-keyword">true</span>;
    });
    <span class="hljs-keyword">if</span> (!_speechEnabled) {
      toastInfo(msg: <span class="hljs-string">"Speech not initialized yet."</span>, status: Status.error);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">await</span> _speechToText.listen(onResult: _onSpeechResult);
    setState(() {});
  }

  <span class="hljs-keyword">void</span> _stopListening() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">await</span> _speechToText.stop();
    setKeyword(_lastWords);
    setState(() {});
  }

  <span class="hljs-keyword">void</span> _onSpeechResult(SpeechRecognitionResult result) {
    setState(() {
      _lastWords = result.recognizedWords;
    });
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> Replace "YOUR_API_KEY" with your actual Gemini API Key</span>
    <span class="hljs-comment">// Refer to https://www.freecodecamp.org/news/how-to-secure-mobile-apis-in-flutter/ for API key security.</span>
    apiKey = <span class="hljs-string">"YOUR_API_KEY"</span>; <span class="hljs-comment">// Secure this!</span>
    _model = GenerativeModel(model: AppStrings.AI_MODEL, apiKey: apiKey);
    _initSpeech();
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _query.dispose();
    _speechToText.cancel(); <span class="hljs-comment">// Cancel listening to prevent resource leaks</span>
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-keyword">void</span> assignCroppedImage(CroppedFile? croppedFile) {
    <span class="hljs-keyword">if</span> (croppedFile != <span class="hljs-keyword">null</span>) {
      setState(() {
        selectedFile = File(croppedFile.path);
      });
    }
  }

  <span class="hljs-keyword">void</span> setFile(File? pickedFile) {
    setState(() {
      selectedFile = pickedFile;
      fileName = pickedFile?.path.split(<span class="hljs-string">'/'</span>).last;
      fileSize = pickedFile?.lengthSync().formatToMegaByte();
    });
  }

  <span class="hljs-keyword">void</span> removeFile() {
    setState(() {
      selectedFile = <span class="hljs-keyword">null</span>;
      fileSize = <span class="hljs-keyword">null</span>;
    });
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    Size size = MediaQuery.sizeOf(context);

    <span class="hljs-keyword">return</span> Scaffold(
      floatingActionButton: selectedFile != <span class="hljs-keyword">null</span> || _query.text.isNotEmpty
          ? FloatingActionButton.extended(
        onPressed: () =&gt; RecipeController.sendRequest(
          context,
          selectedFile,
          _model,
          removeFile,
          _query.text,
          removeText,
        ),
        backgroundColor: AppColors.primaryColor,
        icon: <span class="hljs-keyword">const</span> Icon(Iconsax.send_1, color: Colors.white),
        label: <span class="hljs-keyword">const</span> Text(
          <span class="hljs-string">"Send Request"</span>,
          style: TextStyle(color: Colors.white),
        ),
      )
          : <span class="hljs-keyword">null</span>,
      body: Padding(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">18.0</span>),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                AppStrings.APP_TITLE,
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Colors.black,
                  fontWeight: FontWeight.w500,
                  fontSize: <span class="hljs-number">16</span>,
                ),
              ),
              Text(
                AppStrings.APP_SUBTITLE,
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: AppColors.grey,
                  fontSize: <span class="hljs-number">15</span>,
                  fontWeight: FontWeight.w300,
                ),
              ),
              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
              <span class="hljs-keyword">if</span> (!isDoneRecording)
                !isRecording
                    ? selectedFile != <span class="hljs-keyword">null</span>
                    ? ImagePreviewer(
                  size: size,
                  pickedFile: selectedFile,
                  removeFile: removeFile,
                  context: context,
                  completer: completer,
                  setFile: setFile,
                  assignCroppedImage: assignCroppedImage,
                )
                    : GestureDetector(
                  onTap: () =&gt;
                      ImageUploadController.showFilePickerButtonSheet(
                        context,
                        completer,
                        setFile,
                        assignCroppedImage,
                      ),
                  child: UploadContainer(
                    title: <span class="hljs-string">'an image of a food or snack'</span>,
                    size: size,
                  ),
                )
                    : SizedBox.shrink(),
              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),

              <span class="hljs-keyword">if</span> (selectedFile == <span class="hljs-keyword">null</span>) ...[
                <span class="hljs-keyword">if</span> (!isDoneRecording) ...[
                  Text(
                    <span class="hljs-string">"or record your voice"</span>,
                    style: TextStyle(
                      color: AppColors.grey,
                      fontSize: <span class="hljs-number">16</span>,
                      fontWeight: FontWeight.w200,
                    ),
                  ),
                  Center(
                    child: GestureDetector(
                      onTap: () {
                        <span class="hljs-keyword">if</span> (!_speechEnabled) {
                          toastInfo(
                            msg: <span class="hljs-string">"Speech recognition not ready yet."</span>,
                            status: Status.error,
                          );
                          <span class="hljs-keyword">return</span>;
                        }
                        <span class="hljs-keyword">if</span> (_speechToText.isNotListening) {
                          _startListening();
                        } <span class="hljs-keyword">else</span> {
                          _stopListening();
                        }
                      },
                      child: GlowingMicButton(
                        isListening: !_speechToText.isNotListening,
                      ),
                    ),
                  ),
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                  Container(
                    padding: EdgeInsets.all(<span class="hljs-number">16</span>),
                    child: Text(
                      _speechToText.isListening
                          ? _lastWords
                          : _speechEnabled
                          ? <span class="hljs-string">'Tap the microphone to start listening...'</span>
                          : <span class="hljs-string">'Speech not available'</span>,
                    ),
                  ),
                  <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">10</span>),
                ],

                isDoneRecording
                    ? QueryTextBox(query: _query)
                    : SizedBox.shrink(),
              ],

              <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">20</span>),
              selectedFile != <span class="hljs-keyword">null</span> || _query.text.isNotEmpty
                  ? GestureDetector(
                onTap: () {
                  <span class="hljs-keyword">if</span> (selectedFile != <span class="hljs-keyword">null</span>) {
                    removeFile();
                  } <span class="hljs-keyword">else</span> {
                    removeText();
                  }
                },
                child: CircleAvatar(
                  backgroundColor: AppColors.primaryColor,
                  radius: <span class="hljs-number">30</span>,
                  child: Icon(Iconsax.close_circle, color: Colors.white),
                ),
              )
                  : SizedBox.shrink(),
            ],
          ),
        ),
      ),
    );
  }
}
</code></pre>
<p>The <code>HomeScreen</code> is the main user interface of our AI cooking assistant application. It manages the state for image selection, voice input, and triggers the AI recipe generation.</p>
<ul>
<li><p><strong>State variables</strong>:</p>
<ul>
<li><p><code>selectedFile</code>: Stores the <code>File</code> object of the image picked by the user.</p>
</li>
<li><p><code>completer</code>: A <code>Completer</code> object, often used for asynchronous operations to signal completion.</p>
</li>
<li><p><code>fileName</code>, <code>fileSize</code>: Store details about the selected image.</p>
</li>
<li><p><code>_model</code>: An instance of <code>GenerativeModel</code> from the <code>google_generative_ai</code> package, which is our interface to the Gemini API.</p>
</li>
<li><p><code>apiKey</code>: <strong>Crucially, this is where you'll insert your Gemini API key.</strong> Remember the security warning above!</p>
</li>
<li><p><code>_query</code>: A <code>TextEditingController</code> for the text input field, which will display the transcribed voice input.</p>
</li>
<li><p><code>_speechToText</code>: An instance of <code>SpeechToText</code> for handling voice recognition.</p>
</li>
<li><p><code>_speechEnabled</code>: A boolean indicating if speech recognition is initialized and available.</p>
</li>
<li><p><code>_lastWords</code>: Stores the most recently recognized words from speech.</p>
</li>
<li><p><code>isRecording</code>: A boolean to track if voice recording is active.</p>
</li>
<li><p><code>isDoneRecording</code>: A boolean to track if a voice recording has been completed and transcribed.</p>
</li>
</ul>
</li>
<li><p><strong>Methods</strong>:</p>
<ul>
<li><p><code>removeText()</code>: Clears the text input field (<code>_query</code>), resets <code>isDoneRecording</code> and <code>_lastWords</code> to clear any previous voice input.</p>
</li>
<li><p><code>setKeyword(String prompt)</code>: Sets the <code>_query</code> text to the <code>prompt</code> (transcribed voice), and updates <code>isRecording</code> and <code>isDoneRecording</code> states. It also provides a toast message if the prompt is empty.</p>
</li>
<li><p><code>_initSpeech()</code>: Initializes the <code>SpeechToText</code> plugin. It requests microphone permission and sets <code>_speechEnabled</code> based on the initialization success. If permissions are not granted, it shows an error toast.</p>
</li>
<li><p><code>_startListening()</code>: Starts the speech recognition listener. Sets <code>isRecording</code> to <code>true</code>.</p>
</li>
<li><p><code>_stopListening()</code>: Stops the speech recognition listener and calls <code>setKeyword</code> with the <code>_lastWords</code> to finalize the transcribed text.</p>
</li>
<li><p><code>_onSpeechResult(SpeechRecognitionResult result)</code>: Callback method for <code>SpeechToText</code> that updates <code>_lastWords</code> with the recognized words as speech recognition progresses.</p>
</li>
<li><p><code>initState()</code>: Called when the widget is inserted into the widget tree. It initializes the <code>_model</code> with the Gemini API key and model name, and calls <code>_initSpeech()</code> to set up voice recognition.</p>
</li>
<li><p><code>dispose()</code>: Called when the widget is removed from the widget tree. It disposes of the <code>_query</code> controller and cancels the <code>_speechToText</code> listener to prevent memory leaks.</p>
</li>
<li><p><code>assignCroppedImage(CroppedFile? croppedFile)</code>: Callback function passed to <code>ImageUploadController</code> to update <code>selectedFile</code> with the path of the newly cropped image.</p>
</li>
<li><p><code>setFile(File? pickedFile)</code>: Callback function passed to <code>ImageUploadController</code> to update <code>selectedFile</code> with the picked image, and also extracts its <code>fileName</code> and <code>fileSize</code> using our custom extension.</p>
</li>
<li><p><code>removeFile()</code>: Clears the <code>selectedFile</code> and <code>fileSize</code> states, effectively removing the displayed image.</p>
</li>
</ul>
</li>
<li><p><code>build(BuildContext context)</code> Method – UI Layout:</p>
<ul>
<li><p><code>FloatingActionButton.extended</code>: This button, labeled "Send Request," becomes visible only when an image (<code>selectedFile</code>) is chosen OR when there's text in the query box (<code>_query.text.isNotEmpty</code>). Tapping it triggers <code>RecipeController.sendRequest</code> with the relevant input.</p>
</li>
<li><p><strong>App title and subtitle</strong>: Displays the main headings using <code>AppStrings</code>.</p>
</li>
<li><p><strong>Image upload/preview section</strong>:</p>
<ul>
<li><p>If <code>!isDoneRecording</code> (meaning no voice input has been finalized) and <code>!isRecording</code> (not currently recording voice):</p>
<ul>
<li><p>If <code>selectedFile</code> is not null, it shows an <code>ImagePreviewer</code> widget to display the chosen image with an option to remove it.</p>
</li>
<li><p>Otherwise (no image selected), it displays an <code>UploadContainer</code> which acts as a tappable area to trigger <code>ImageUploadController.showFilePickerButtonSheet</code> for picking an image.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Voice input section</strong>:</p>
<ul>
<li><p>This section (<code>if (selectedFile == null) ...</code>) only appears if no image is selected, providing an alternative input method.</p>
</li>
<li><p>If <code>!isDoneRecording</code>, it shows a "or record your voice" text and a <code>GlowingMicButton</code>.</p>
<ul>
<li><p>Tapping the <code>GlowingMicButton</code> toggles speech recognition (<code>_startListening</code> / <code>_stopListening</code>).</p>
</li>
<li><p>A <code>Text</code> widget displays the current speech recognition status or <code>_lastWords</code> as they are transcribed.</p>
</li>
</ul>
</li>
<li><p>If <code>isDoneRecording</code> (meaning voice input has been finalized), it shows a <code>QueryTextBox</code> which displays the transcribed text, allowing for review before sending the request.</p>
</li>
</ul>
</li>
<li><p><strong>Clear input button</strong>: A <code>CircleAvatar</code> with a close icon appears when either an image is selected or text is present in the query. Tapping it calls <code>removeFile()</code> or <code>removeText()</code> to clear the respective input.</p>
</li>
</ul>
</li>
</ul>
<p>Overall, <code>HomeScreen</code> intelligently adapts its UI based on user input (image or voice) and orchestrates the interaction with the <code>ImageUploadController</code> for image handling and the <code>RecipeController</code> for AI recipe generation.</p>
<h4 id="heading-the-components-folder">The <code>components</code> Folder</h4>
<p>This folder contains smaller, reusable UI elements.</p>
<h5 id="heading-toastinfodart"><code>toast_info.dart</code></h5>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:fluttertoast/fluttertoast.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>; <span class="hljs-comment">// Import for MaterialColor/Colors</span>

<span class="hljs-keyword">void</span> toastInfo({
  <span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> msg,
  <span class="hljs-keyword">required</span> Status status,
}) {
  Fluttertoast.showToast(
    msg: msg,
    toastLength: Toast.LENGTH_SHORT,
    gravity: ToastGravity.BOTTOM,
    timeInSecForIosWeb: <span class="hljs-number">1</span>,
    backgroundColor: status == Status.success ? AppColors.primaryColor : AppColors.errorColor,
    textColor: Colors.white,
    fontSize: <span class="hljs-number">16.0</span>,
  );
}
</code></pre>
<p>The <code>toastInfo</code> function provides a convenient way to display brief, non-intrusive messages (toasts) to the user, typically for feedback like "success" or "error" messages.</p>
<p>It takes two required parameters:</p>
<ul>
<li><p><code>msg</code>: The message string to be displayed in the toast.</p>
</li>
<li><p><code>status</code>: An enum of type <code>Status</code> (<code>success</code> or <code>error</code>) which determines the background color of the toast.</p>
</li>
</ul>
<p><code>Fluttertoast.showToast(...)</code> is the core function from the <code>fluttertoast</code> package that displays the toast.</p>
<ul>
<li><p><code>toastLength</code>: Sets the duration the toast is visible (short).</p>
</li>
<li><p><code>gravity</code>: Positions the toast at the bottom of the screen.</p>
</li>
<li><p><code>timeInSecForIosWeb</code>: Duration for web/iOS.</p>
</li>
<li><p><code>backgroundColor</code>: Dynamically set to <code>AppColors.primaryColor</code> for success and <code>AppColors.errorColor</code> for errors, providing visual cues to the user.</p>
</li>
<li><p><code>textColor</code>: Sets the text color to white.</p>
</li>
<li><p><code>fontSize</code>: Sets the font size of the toast message.</p>
</li>
</ul>
<p>This function centralizes toast message display, ensuring consistency in appearance and behavior throughout the app.</p>
<h4 id="heading-the-widgets-folder">The <code>widgets</code> Folder</h4>
<p>The application's user interface is constructed using a series of well-defined, reusable Flutter widgets. Each widget serves a specific purpose, contributing to the overall functionality and aesthetic of Snap2Chef.</p>
<p>1. <code>glowing_microphone.dart</code>:</p>
<p>This widget creates an animated microphone button that visually indicates when the application is actively listening for speech input.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GlowingMicButton</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">bool</span> isListening;

  <span class="hljs-keyword">const</span> GlowingMicButton({<span class="hljs-keyword">super</span>.key, <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.isListening});

  <span class="hljs-meta">@override</span>
  State&lt;GlowingMicButton&gt; createState() =&gt; _GlowingMicButtonState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_GlowingMicButtonState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">GlowingMicButton</span>&gt;
    <span class="hljs-title">with</span> <span class="hljs-title">SingleTickerProviderStateMixin</span> </span>{
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> AnimationController _controller;
  <span class="hljs-keyword">late</span> <span class="hljs-keyword">final</span> Animation&lt;<span class="hljs-built_in">double</span>&gt; _animation;

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _controller = AnimationController(
      vsync: <span class="hljs-keyword">this</span>,
      duration: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">2</span>),
    );

    _animation = Tween&lt;<span class="hljs-built_in">double</span>&gt;(begin: <span class="hljs-number">0.0</span>, end: <span class="hljs-number">25.0</span>).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );

    <span class="hljs-keyword">if</span> (widget.isListening) {
      _controller.repeat(reverse: <span class="hljs-keyword">true</span>);
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> didUpdateWidget(<span class="hljs-keyword">covariant</span> GlowingMicButton oldWidget) {
    <span class="hljs-keyword">super</span>.didUpdateWidget(oldWidget);

    <span class="hljs-keyword">if</span> (widget.isListening &amp;&amp; !_controller.isAnimating) {
      _controller.repeat(reverse: <span class="hljs-keyword">true</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!widget.isListening &amp;&amp; _controller.isAnimating) {
      _controller.stop();
    }
  }

  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> dispose() {
    _controller.dispose();
    <span class="hljs-keyword">super</span>.dispose();
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> SizedBox(
      width: <span class="hljs-number">100</span>, <span class="hljs-comment">// Enough space for the full glow</span>
      height: <span class="hljs-number">100</span>,
      child: Stack(
        alignment: Alignment.center,
        children: [
          <span class="hljs-keyword">if</span> (widget.isListening)
            AnimatedBuilder(
              animation: _animation,
              builder: (_, __) {
                <span class="hljs-keyword">return</span> Container(
                  width: <span class="hljs-number">60</span> + _animation.value,
                  height: <span class="hljs-number">60</span> + _animation.value,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: AppColors.primaryColor.withOpacity(<span class="hljs-number">0.15</span>),
                  ),
                );
              },
            ),
          CircleAvatar(
            backgroundColor: AppColors.primaryColor,
            radius: <span class="hljs-number">30</span>,
            child: Icon(
              widget.isListening ? Iconsax.stop_circle : Iconsax.microphone,
              color: Colors.white,
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>GlowingMicButton</code> (StatefulWidget): This is a <code>StatefulWidget</code> because it needs to manage its own animation state. It takes a <code>final bool isListening</code> property, which dictates whether the microphone should display a glowing animation or remain static.</li>
</ul>
<ul>
<li><p><code>_GlowingMicButtonState</code> (State with <code>SingleTickerProviderStateMixin</code>):</p>
<ul>
<li><p><code>SingleTickerProviderStateMixin</code>: This mixin is crucial for providing a <code>Ticker</code> to an <code>AnimationController</code>. A <code>Ticker</code> essentially drives the animation forward, linking it to the frame callbacks, ensuring smooth animation performance.</p>
</li>
<li><p><code>_controller</code> (AnimationController): Manages the animation. It's initialized with <code>vsync: this</code> (from <code>SingleTickerProviderStateMixin</code>) and a <code>duration</code> of 2 seconds.</p>
</li>
<li><p><code>_animation</code> (Animation&lt;double&gt;): Defines the range of values the animation will produce. Here, a <code>Tween&lt;double&gt;(begin: 0.0, end: 25.0)</code> is used with a <code>CurvedAnimation</code> (specifically <code>Curves.easeOut</code>) to create a smooth, decelerating effect as the glow expands.</p>
</li>
<li><p><code>initState()</code>: When the widget is first created, the <code>AnimationController</code> and <code>Animation</code> are initialized. If <code>isListening</code> is initially <code>true</code>, the animation is set to <code>repeat(reverse: true)</code> to make the glow pulse in and out continuously.</p>
</li>
<li><p><code>didUpdateWidget()</code>: This lifecycle method is called when the widget's configuration (its properties) changes. It checks if <code>isListening</code> has changed and starts or stops the animation accordingly. This ensures the animation dynamically responds to changes in the <code>isListening</code> state from its parent.</p>
</li>
<li><p><code>dispose()</code>: Crucially, the <code>_controller.dispose()</code> method is called here to release the resources held by the animation controller when the widget is removed from the widget tree, preventing memory leaks.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>SizedBox</code>: Provides a fixed size (100x100) for the button, ensuring enough space for the glowing effect.</p>
</li>
<li><p><code>Stack</code>: Allows layering widgets on top of each other.</p>
<ul>
<li><p><code>if (widget.isListening) AnimatedBuilder(...)</code>: This conditional renders the glowing effect <em>only</em> when <code>isListening</code> is <code>true</code>.</p>
<ul>
<li><p><code>AnimatedBuilder</code>: Rebuilds its child whenever the <code>_animation</code> changes value.</p>
</li>
<li><p>Inside <code>AnimatedBuilder</code>, a <code>Container</code> is used to create the circular glow. Its <code>width</code> and <code>height</code> are dynamically increased by <code>_animation.value</code>, creating the expanding effect. The <code>color</code> is <code>AppColors.primaryColor</code> with <code>0.15</code> opacity, giving it a subtle glow.</p>
</li>
</ul>
</li>
<li><p><code>CircleAvatar</code>: This is the main microphone button.</p>
<ul>
<li><p><code>backgroundColor</code> is <code>AppColors.primaryColor</code>.</p>
</li>
<li><p><code>radius</code> is <code>30</code>.</p>
</li>
<li><p>The <code>child</code> is an <code>Icon</code> from the <code>Iconsax</code> package, dynamically changing between <code>Iconsax.stop_circle</code> (when listening) and <code>Iconsax.microphone</code> (when not listening). The icon color is white.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>2. <code>image_picker_component.dart</code></p>
<p>This widget provides a reusable <code>ListTile</code> interface for users to select images from either the camera or the gallery.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/cupertino.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/enums/record_source.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePickerTile</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ImagePickerTile({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.subtitle,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.icon,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.recordSource,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.completer,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.context,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.setFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.assignCroppedImage,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> subtitle;
  <span class="hljs-keyword">final</span> IconData icon;
  <span class="hljs-keyword">final</span> RecordSource recordSource;
  <span class="hljs-keyword">final</span> Completer? completer;
  <span class="hljs-keyword">final</span> BuildContext context;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> setFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> assignCroppedImage;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> ListTile(
      leading: CircleAvatar(
        backgroundColor: AppColors.litePrimary,
        child: Padding(
          padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">3.0</span>),
          child: Center(
            child: Icon(icon, color: AppColors.primaryColor, size: <span class="hljs-number">20</span>),
          ),
        ),
      ),
      title: Text(title, style: <span class="hljs-keyword">const</span> TextStyle(color: Colors.black)),
      subtitle: Text(
        subtitle,
        style: <span class="hljs-keyword">const</span> TextStyle(fontSize: <span class="hljs-number">14</span>, color: Colors.grey),
      ),
      trailing: <span class="hljs-keyword">const</span> Icon(
        CupertinoIcons.chevron_right,
        size: <span class="hljs-number">20</span>,
        color: Color(<span class="hljs-number">0xffE4E4E4</span>),
      ),
      onTap: () {
        ImageUploadController.imagePicker(
          recordSource,
          completer,
          context,
          setFile,
          assignCroppedImage,
        );
      },
    );
  }
}
</code></pre>
<ul>
<li><p><code>ImagePickerTile</code> (StatelessWidget): This is a <code>StatelessWidget</code> because it simply renders content based on its immutable properties and triggers an external function (<code>ImageUploadController.imagePicker</code>) when tapped.</p>
</li>
<li><p><strong>Properties:</strong> It takes several <code>final</code> properties to make it highly customizable:</p>
<ul>
<li><p><code>title</code> and <code>subtitle</code>: Text for the main and secondary lines of the list tile.</p>
</li>
<li><p><code>icon</code>: The <code>IconData</code> to display as the leading icon.</p>
</li>
<li><p><code>recordSource</code>: An enum (<code>RecordSource</code>) likely indicating if the image should be picked from the camera or gallery.</p>
</li>
<li><p><code>completer</code>: A <code>Completer</code> object, often used for asynchronous operations to signal when a task is complete.</p>
</li>
<li><p><code>context</code>: The <code>BuildContext</code> to allow the <code>ImageUploadController</code> to show dialogs or navigate.</p>
</li>
<li><p><code>setFile</code>: A <code>Function</code> callback to update the selected image file in the parent widget.</p>
</li>
<li><p><code>assignCroppedImage</code>: A <code>Function</code> callback to handle the result of any image cropping operation.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>ListTile</code>: A standard Flutter widget used to arrange elements in a single row.</p>
<ul>
<li><p><code>leading</code>: Displays a <code>CircleAvatar</code> with a light primary background color, containing the specified <code>icon</code> in the primary color. This creates a visually appealing icon button on the left.</p>
</li>
<li><p><code>title</code>: Displays the <code>title</code> text in black.</p>
</li>
<li><p><code>subtitle</code>: Displays the <code>subtitle</code> text in grey with a font size of 14, providing additional descriptive information.</p>
</li>
<li><p><code>trailing</code>: Shows a <code>CupertinoIcons.chevron_right</code> (right arrow) icon, common for indicating navigation or actionable items in a list.</p>
</li>
<li><p><code>onTap</code>: This is the primary interaction point. When the <code>ListTile</code> is tapped, it calls the static method <code>ImageUploadController.imagePicker</code>, passing all the necessary parameters. This centralizes the image picking logic within <code>ImageUploadController</code>, making the <code>ImagePickerTile</code> purely a UI component.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>3. <code>image_previewer.dart</code></p>
<p>This widget is responsible for displaying a previously picked image and offering options to 'Edit' (re-pick) or 'Remove' the image.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:async'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:snap2chef/infrastructure/image_upload_controller.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImagePreviewer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> ImagePreviewer({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.size,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.pickedFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.removeFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.context,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.completer,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.setFile,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.assignCroppedImage,
  });

  <span class="hljs-keyword">final</span> Size size;
  <span class="hljs-keyword">final</span> File? pickedFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> removeFile;
  <span class="hljs-keyword">final</span> BuildContext context;
  <span class="hljs-keyword">final</span> Completer? completer;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> setFile;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span> assignCroppedImage;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Container(
      height: size.height * <span class="hljs-number">0.13</span>,
      width: <span class="hljs-built_in">double</span>.infinity,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
        <span class="hljs-comment">// border: Border.all(</span>
        <span class="hljs-comment">//   color: AppColors.borderColor,</span>
        <span class="hljs-comment">// ),</span>
        image: DecorationImage(
          image: FileImage(
            File(pickedFile!.path),
          ),
          fit: BoxFit.cover,
        ),
      ),
      child: Stack(
        children: [
          Container(
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(<span class="hljs-number">0.3</span>),
              borderRadius: BorderRadius.circular(<span class="hljs-number">7</span>),
            ),
          ),
          <span class="hljs-comment">// Centered content</span>
          Center(
            child: Wrap(
              crossAxisAlignment: WrapCrossAlignment.center,
              spacing: <span class="hljs-number">20</span>,
              children: [
                GestureDetector(
                  onTap: () {
                    ImageUploadController.showFilePickerButtonSheet(context,completer,setFile,assignCroppedImage);
                  },
                  child: Column(
                    children: [
                      Icon(
                        Iconsax.edit_2,
                        size: <span class="hljs-number">20</span>,
                        color: Colors.white,
                      ),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Edit'</span>,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: <span class="hljs-number">15</span>,
                        ),
                      )
                    ],
                  ),
                ),
                GestureDetector(
                  onTap: () {
                    removeFile();
                  },
                  child: Column(
                    children: [
                      Icon(
                        Iconsax.note_remove,
                        color: Colors.white,
                        size: <span class="hljs-number">20</span>,
                      ),
                      <span class="hljs-keyword">const</span> Text(
                        <span class="hljs-string">'Remove'</span>,
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: <span class="hljs-number">15</span>,
                        ),
                      )
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>ImagePreviewer</code> (StatelessWidget): Similar to <code>ImagePickerTile</code>, this is a <code>StatelessWidget</code> that displays content and triggers callbacks.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><p><code>size</code>: The <code>Size</code> of the parent widget, used to calculate the <code>height</code> of the preview container proportionally.</p>
</li>
<li><p><code>pickedFile</code>: A <code>File?</code> representing the image file to be displayed. It's nullable, implying that this widget might only show if a file has been picked.</p>
</li>
<li><p><code>removeFile</code>: A <code>Function</code> callback to handle the removal of the currently displayed image.</p>
</li>
<li><p><code>context</code>, <code>completer</code>, <code>setFile</code>, <code>assignCroppedImage</code>: These are passed through to the <code>ImageUploadController</code> when the 'Edit' action is triggered, similar to the <code>ImagePickerTile</code>.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>Container</code>: The primary container for the image preview.</p>
<ul>
<li><p><code>height</code>: Set to 13% of the screen height, providing a responsive size.</p>
</li>
<li><p><code>width</code>: <code>double.infinity</code> to take full available width.</p>
</li>
<li><p><code>decoration</code>:</p>
<ul>
<li><p><code>borderRadius</code>: Applies rounded corners to the container.</p>
</li>
<li><p><code>image: DecorationImage(...)</code>: This is where the magic happens. It displays the <code>pickedFile</code> as a background image for the container.</p>
<ul>
<li><p><code>FileImage(File(pickedFile!.path))</code>: Creates an image provider from the local file path. The <code>!</code> (null assertion operator) implies <code>pickedFile</code> is expected to be non-null when this widget is displayed.</p>
</li>
<li><p><code>fit: BoxFit.cover</code>: Ensures the image covers the entire container, potentially cropping parts of it.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Stack</code>: Layers content on top of the image.</p>
<ul>
<li><p><code>Container</code> (Overlay): A semi-transparent black <code>Container</code> is placed on top of the image (<code>Colors.black.withOpacity(0.3)</code>) to create a darkened overlay. This improves the readability of the white text and icons placed over the image.</p>
</li>
<li><p><code>Center</code>: Centers the action buttons horizontally and vertically within the overlay.</p>
</li>
<li><p><code>Wrap</code>: Arranges the 'Edit' and 'Remove' buttons horizontally with a <code>spacing</code> of 20. <code>WrapCrossAlignment.center</code> aligns them vertically within the <code>Wrap</code>.</p>
</li>
<li><p><code>GestureDetector</code> (for 'Edit'):</p>
<ul>
<li><p><code>onTap</code>: Calls <code>ImageUploadController.showFilePickerButtonSheet</code>, allowing the user to re-select or change the image. This method likely presents a bottom sheet with options to pick from the camera or gallery, similar to how the initial image picking works.</p>
</li>
<li><p>Its child is a <code>Column</code> containing an <code>Iconsax.edit_2</code> icon and an 'Edit' text, both in white.</p>
</li>
</ul>
</li>
<li><p><code>GestureDetector</code> (for 'Remove'):</p>
<ul>
<li><p><code>onTap</code>: Calls the <code>removeFile()</code> callback, which would typically clear the selected <code>pickedFile</code> in the parent state, causing this previewer to disappear or revert to an upload state.</p>
</li>
<li><p>Its child is a <code>Column</code> containing an <code>Iconsax.note_remove</code> icon and a 'Remove' text, both in white.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>4. <code>query_text_box.dart</code></p>
<p>This widget provides a styled <code>TextFormField</code> for multi-line text input, typically used for user queries or notes.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">QueryTextBox</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> QueryTextBox({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> TextEditingController query,
  }) : _query = query;

  <span class="hljs-keyword">final</span> TextEditingController _query;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> TextFormField(
      controller: _query,
      maxLines: <span class="hljs-number">4</span>,
      autofocus: <span class="hljs-keyword">true</span>,
      decoration: InputDecoration(
        hintStyle: TextStyle(color: AppColors.lighterGrey),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: BorderSide(color: Colors.grey.shade400),
        ),
        focusedBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: <span class="hljs-keyword">const</span> BorderSide(
            color: AppColors.primaryColor,
            width: <span class="hljs-number">2.0</span>,
          ),
        ),
        enabledBorder: OutlineInputBorder(
          borderRadius: BorderRadius.circular(<span class="hljs-number">12.0</span>),
          borderSide: BorderSide(color: Colors.grey.shade300),
        ),
        contentPadding: <span class="hljs-keyword">const</span> EdgeInsets.symmetric(
          vertical: <span class="hljs-number">12.0</span>,
          horizontal: <span class="hljs-number">16.0</span>,
        ),
      ),
      style: <span class="hljs-keyword">const</span> TextStyle(
        fontSize: <span class="hljs-number">14.0</span>,
        color: Colors.black,
      ),
      keyboardType: TextInputType.multiline,
      textInputAction: TextInputAction.newline,
    );
  }
}
</code></pre>
<ul>
<li><code>QueryTextBox</code> (StatelessWidget): A <code>StatelessWidget</code> that renders a text input field. It takes a <code>TextEditingController</code> as a required parameter, allowing external control over the text field's content.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><code>_query</code> (TextEditingController): The controller linked to the <code>TextFormField</code>. This allows retrieving the text, setting initial text, and listening for changes.</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>TextFormField</code>: The core input widget.</p>
<ul>
<li><p><code>controller: _query</code>: Binds the <code>TextEditingController</code> to this field.</p>
</li>
<li><p><code>maxLines: 4</code>: Allows the text field to expand up to 4 lines before becoming scrollable.</p>
</li>
<li><p><code>autofocus: true</code>: Automatically focuses the text field when the screen loads, bringing up the keyboard.</p>
</li>
<li><p><code>decoration: InputDecoration(...)</code>: Defines the visual styling of the input field.</p>
<ul>
<li><p><code>hintStyle</code>: Sets the color of the hint text to <code>AppColors.lighterGrey</code>.</p>
</li>
<li><p><code>border</code>: Defines the default border when the field is not focused or enabled, with rounded corners and a light grey border.</p>
</li>
<li><p><code>focusedBorder</code>: Defines the border style when the field is actively focused by the user. It uses <code>AppColors.primaryColor</code> with a wider stroke (<code>width: 2.0</code>) to provide a clear visual indicator of focus.</p>
</li>
<li><p><code>enabledBorder</code>: Defines the border style when the field is enabled but not focused, using a slightly darker grey.</p>
</li>
<li><p><code>contentPadding</code>: Adds internal padding within the text field for better spacing of the text.</p>
</li>
</ul>
</li>
<li><p><code>style</code>: Sets the font size to 14.0 and color to black for the entered text.</p>
</li>
<li><p><code>keyboardType: TextInputType.multiline</code>: Configures the keyboard to be suitable for multi-line text input, often providing a "return" key that creates a new line.</p>
</li>
<li><p><code>textInputAction: TextInputAction.newline</code>: Specifies that pressing the "Done" or "Enter" key on the keyboard should insert a new line.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>5. <code>upload_container.dart</code></p>
<p>This widget creates a visually distinct "dotted border" container, typically used as a tappable area to trigger file upload or selection actions.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dotted_border/dotted_border.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:gap/gap.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:iconsax/iconsax.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../core/constants/app_colors.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UploadContainer</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> UploadContainer({
    <span class="hljs-keyword">super</span>.key,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.size,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.title,
  });

  <span class="hljs-keyword">final</span> Size size;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> title;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> DottedBorder(
      color: AppColors.primaryColor,
      radius: <span class="hljs-keyword">const</span> Radius.circular(<span class="hljs-number">15</span>),
      borderType: BorderType.RRect,
      strokeWidth: <span class="hljs-number">1</span>,
      child: SizedBox(
        height: size.height * <span class="hljs-number">0.13</span>,
        width: <span class="hljs-built_in">double</span>.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              height: <span class="hljs-number">70</span>,
              width: <span class="hljs-number">60</span>,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: AppColors.litePrimary,
              ),
              child: Padding(
                padding: <span class="hljs-keyword">const</span> EdgeInsets.all(<span class="hljs-number">13.0</span>),
                child: Icon(
                  Iconsax.document_upload,
                  color: AppColors.primaryColor,
                ),
              ),
            ),
            <span class="hljs-keyword">const</span> Gap(<span class="hljs-number">5</span>),
            RichText(
              text: TextSpan(
                text: <span class="hljs-string">'Click to select '</span>,
                style: TextStyle(
                  color: AppColors.primaryColor,
                ),
                children: [
                  TextSpan(
                    text: title,
                    style: TextStyle(
                      color: Color(<span class="hljs-number">0xff555555</span>),
                    ),
                  )
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<ul>
<li><code>UploadContainer</code> (StatelessWidget): A <code>StatelessWidget</code> primarily for visual presentation, indicating an upload zone.</li>
</ul>
<ul>
<li><p><strong>Properties:</strong></p>
<ul>
<li><p><code>size</code>: The <code>Size</code> of the parent, used to determine the container's height proportionally.</p>
</li>
<li><p><code>title</code>: A <code>String</code> to be displayed as part of the "Click to select [title]" message.</p>
</li>
</ul>
</li>
<li><p><code>build()</code> Method:</p>
<ul>
<li><p><code>DottedBorder</code>: This package provides the visual dotted border effect.</p>
<ul>
<li><p><code>color: AppColors.primaryColor</code>: The color of the dotted line.</p>
</li>
<li><p><code>radius: const Radius.circular(15)</code>: Applies rounded corners to the dotted border.</p>
</li>
<li><p><code>borderType: BorderType.RRect</code>: Specifies that the border should follow a rounded rectangle shape.</p>
</li>
<li><p><code>strokeWidth: 1</code>: Sets the thickness of the dotted line.</p>
</li>
</ul>
</li>
<li><p><code>SizedBox</code>: Defines the internal dimensions of the area within the dotted border, taking up 13% of the screen height and full width.</p>
</li>
<li><p><code>Column</code>: Arranges the icon and text vertically, centered within the <code>SizedBox</code>.</p>
<ul>
<li><p><code>Container</code> (Icon background): A circular container with <code>AppColors.litePrimary</code> background holds the upload icon.</p>
<ul>
<li><code>Iconsax.document_upload</code>: The icon signifying an upload action, colored with <code>AppColors.primaryColor</code>.</li>
</ul>
</li>
<li><p><code>Gap(5)</code>: From the <code>gap</code> package, this provides a small vertical space (5 pixels) between the icon and the text.</p>
</li>
<li><p><code>RichText</code>: Allows for different styles within a single text block.</p>
<ul>
<li><p><code>TextSpan(text: 'Click to select ', ...)</code>: The first part of the message, styled with <code>AppColors.primaryColor</code>.</p>
</li>
<li><p><code>children: [TextSpan(text: title, ...)]</code>: The second part of the message, which is the <code>title</code> property passed to the widget, styled in a darker grey. This structure allows "Click to select " to be consistently styled while the <code>title</code> (for example, "image", "document") can have a different appearance.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-summary-of-code-implementation">Summary of Code Implementation</h3>
<p>We've covered a significant amount of ground in this part of the article, transforming our basic Flutter application into a powerful AI-powered recipe guide. We started by setting up the core UI, then delved into integrating the <code>google_generative_ai</code> package to communicate with Google's Gemini models for both image and voice input.</p>
<p>We implemented robust logic for:</p>
<ul>
<li><p><strong>Image input:</strong> Capturing images from the camera or gallery, cropping them, and sending them to the <code>gemini</code> model.</p>
</li>
<li><p><strong>Voice input:</strong> Recording audio and preparing the groundwork for transcription before sending text to the <code>gemini</code> model.</p>
</li>
<li><p><strong>Dynamic content display:</strong> Skillfully parsing the AI's response to extract and present not just the recipe text, but also embedding YouTube instructional videos and even relevant images, all within a beautifully formatted dialog using <code>flutter_markdown</code> and <code>cached_network_image</code>. We also ensured proper lifecycle management for our media players.</p>
</li>
</ul>
<p>This highlights how easily you can leverage advanced AI capabilities like multimodal understanding and natural language generation within your Flutter applications. By building on these concepts, you can create truly interactive and intelligent user experiences.</p>
<p>Now that we have the core logic in place for capturing input, communicating with the AI, and displaying its rich responses, we need to ensure that our application can actually access the necessary device features.</p>
<h2 id="heading-permissions-ensuring-app-functionality-and-user-privacy"><strong>Permissions: Ensuring App Functionality and User Privacy</strong></h2>
<p>For a Flutter application to interact with system features like the camera, microphone, or file storage, it must declare specific permissions in both its Android and iOS manifests. These declarations inform the operating system about the app's requirements and, for sensitive permissions, prompt the user for consent at runtime.</p>
<h3 id="heading-android1-permissions-in-androidappsrcmainandroidmanifestxml"><strong>Android<sup>1</sup> Permissions (in</strong> <code>android/app/src/main/AndroidManifest.xml</code>)</h3>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">manifest</span> <span class="hljs-attr">xmlns:android</span>=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.RECORD_AUDIO"</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.CAMERA"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.INTERNET"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.READ_EXTERNAL_STORAGE"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">manifest</span>&gt;</span>
</code></pre>
<p>Here’s what’s going on:</p>
<ul>
<li><p><code>&lt;uses-permission android:name="android.permission.RECORD_AUDIO"/&gt;</code>: This permission is necessary for the application to access the device's microphone and record audio. It's crucial for any speech recognition or voice input features, like the <code>GlowingMicButton</code> implies.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.CAMERA" /&gt;</code>: Grants the application access to the device's camera. This is essential for features that allow users to take photos, such as those enabled by <code>ImagePickerTile</code> or <code>ImagePreviewer</code>.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.INTERNET" /&gt;</code>: This is a fundamental permission required for almost any modern application that connects to the internet. It allows the app to send and receive data from web services, like interacting with the Gemini API, Firebase, or Vertex AI.</p>
</li>
<li><p><code>&lt;uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /&gt;</code>: Allows the application to read files from the device's shared external storage (for example, photos saved in the gallery). This is necessary when picking existing images from the gallery. For newer Android versions (Android 10+), scoped storage might change how this works, but for reading user-selected media, this declaration is still relevant. For writing to external storage, <code>WRITE_EXTERNAL_STORAGE</code> would also be needed.</p>
</li>
</ul>
<h3 id="heading-ios-permissions-in-iosrunnerinfoplist"><strong>iOS Permissions (in</strong> <code>ios/Runner/Info.plist</code><strong>)</strong></h3>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">plist</span> <span class="hljs-meta-keyword">PUBLIC</span> <span class="hljs-meta-string">"-//Apple//DTD PLIST 1.0//EN"</span> <span class="hljs-meta-string">"http://www.apple.com/DTDs/PropertyList-1.0.dtd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">plist</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>io.flutter.embedded_views_preview<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSSpeechRecognitionUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>We need access to recognize your speech.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSCameraUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to the camera to capture photos and videos.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSMicrophoneUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to the microphone for audio recording.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSPhotoLibraryUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs access to your photo library.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSPhotoLibraryAddUsageDescription<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>This app needs permission to save photos to your photo library.<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSAppTransportSecurity<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>NSAllowsArbitraryLoads<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">plist</span>&gt;</span>
</code></pre>
<p>Here’s what’s going on:</p>
<p>iOS permissions are declared in the <code>Info.plist</code> file using specific keys (<code>NS...UsageDescription</code>) and require a user-facing string explaining why the permission is needed. This string is displayed to the user when the app requests the permission.</p>
<ul>
<li><p><code>&lt;key&gt;io.flutter.embedded_views_preview&lt;/key&gt;&lt;true/&gt;</code>: This key is often added when using Flutter plugins that integrate native UI components (for example, camera previews, webviews). It enables a preview of embedded native views during development.</p>
</li>
<li><p><code>&lt;key&gt;NSSpeechRecognitionUsageDescription&lt;/key&gt;&lt;string&gt;We need access to recognize your speech.&lt;/string&gt;</code>: This is the privacy description for speech recognition services (for example, Apple's built-in speech recognizer). It's crucial for features like voice input to work.</p>
</li>
<li><p><code>&lt;key&gt;NSCameraUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to the camera to capture photos and videos.&lt;/string&gt;</code>: The privacy description for camera access. This is required for capturing images via the camera, as used in the image picking functionality.</p>
</li>
<li><p><code>&lt;key&gt;NSMicrophoneUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to the microphone for audio recording.&lt;/string&gt;</code>: The privacy description for microphone access. Necessary for recording audio for speech input.</p>
</li>
<li><p><code>&lt;key&gt;NSPhotoLibraryUsageDescription&lt;/key&gt;&lt;string&gt;This app needs access to your photo library.&lt;/string&gt;</code>: The privacy description for reading from the user's photo library. This is required when picking existing images or videos from the gallery.</p>
</li>
<li><p><code>&lt;key&gt;NSPhotoLibraryAddUsageDescription&lt;/key&gt;&lt;string&gt;This app needs permission to save photos to your photo library.&lt;/string&gt;</code>: The privacy description for writing to the user's photo library. This would be needed if the app captures photos/videos and saves them directly to the device's gallery.</p>
</li>
<li><p><code>&lt;key&gt;NSAppTransportSecurity&lt;/key&gt;&lt;dict&gt;&lt;key&gt;NSAllowsArbitraryLoads&lt;/key&gt;&lt;true/&gt;&lt;/dict&gt;</code>: This section relates to Apple's App Transport Security (ATS). By default, ATS enforces secure connections (HTTPS). Setting <code>NSAllowsArbitraryLoads</code> to <code>true</code> (as shown here) <em>disables</em> this enforcement, allowing the app to make insecure HTTP connections. While useful during development or for interacting with specific legacy APIs, it's generally <strong>not recommended for production apps</strong> due to security implications. For production, you should ideally configure specific exceptions or ensure all network requests use HTTPS.</p>
</li>
</ul>
<h2 id="heading-assets-managing-application-resources"><strong>Assets: Managing Application Resources</strong></h2>
<p>Assets are files bundled with your application and are accessible at runtime. This typically includes images, fonts, audio files, and more.</p>
<p>In this application, we have an <code>assets</code> folder, and inside it, an <code>images</code> subfolder.</p>
<pre><code class="lang-dart">assets/
└── images/
    ├── placeholder.png
    └── app_logo.png
</code></pre>
<ul>
<li><p><code>placeholder.png</code>: This image is typically used as a temporary visual cue when actual content (like an image being loaded or picked) is not yet available. It provides a better user experience than a blank space.</p>
</li>
<li><p><code>app_logo.png</code>: This is the primary logo of the application. It's used for various purposes, including the app icon and the splash screen.</p>
</li>
</ul>
<p>To ensure Flutter knows about these assets and bundles them with the application, you need to declare them in your <code>pubspec.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter:</span>
  <span class="hljs-attr">uses-material-design:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">assets:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">assets/images/</span> <span class="hljs-comment"># This line tells Flutter to include all files in the assets/images/ directory</span>
</code></pre>
<h2 id="heading-app-icons-customizing-your-applications-identity"><strong>App Icons: Customizing Your Application's Identity</strong></h2>
<p>Flutter applications use the <code>flutter_launcher_icons</code> package to simplify the process of generating app icons for different platforms and resolutions. This ensures your app has a consistent and professional look on both Android and iOS devices.</p>
<h3 id="heading-pubspecyaml-configuration-for-flutterlaunchericons"><code>pubspec.yaml</code> Configuration for <code>flutter_launcher_icons</code></h3>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter_icons:</span>
  <span class="hljs-attr">android:</span> <span class="hljs-string">"launcher_icon"</span>
  <span class="hljs-attr">ios:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">image_path:</span> <span class="hljs-string">"assets/images/app_logo.png"</span>
  <span class="hljs-attr">remove_alpha_ios:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">adaptive_icon_background:</span> <span class="hljs-string">"#FFFFFF"</span>
  <span class="hljs-attr">adaptive_icon_foreground:</span> <span class="hljs-string">"assets/images/app_logo.png"</span>
</code></pre>
<p>Here’s what’s happening:</p>
<ul>
<li><p><code>flutter_icons:</code>: This is the root key for the <code>flutter_launcher_icons</code> package configuration.</p>
</li>
<li><p><code>android: "launcher_icon"</code>: Specifies that Android launcher icons should be generated. <code>"launcher_icon"</code> is the default and usually sufficient.</p>
</li>
<li><p><code>ios: true</code>: Enables the generation of iOS app icons.</p>
</li>
<li><p><code>image_path: "assets/images/app_logo.png"</code>: This is the absolute path to your source image file that will be used to generate the icons. It's crucial that this path is correct and points to a high-resolution square image.</p>
</li>
<li><p><code>remove_alpha_ios: true</code>: For iOS, this option removes the alpha channel from the icon. iOS icons typically do not use an alpha channel for transparency.</p>
</li>
<li><p><code>adaptive_icon_background: "#FFFFFF"</code>: This is specific to Android Adaptive Icons (introduced in Android 8.0 Oreo). It defines the background layer of the adaptive icon. Here, it's set to white (<code>#FFFFFF</code>).</p>
</li>
<li><p><code>adaptive_icon_foreground: "assets/images/app_logo.png"</code>: This defines the foreground layer of the adaptive icon. It uses the <code>app_logo.png</code> again, which will be masked and scaled by the Android system.</p>
</li>
</ul>
<h3 id="heading-generating-app-icons"><strong>Generating App Icons</strong></h3>
<p>After configuring <code>pubspec.yaml</code>, you need to run the following commands in your terminal:</p>
<p>First, run <code>dart run flutter_launcher_icons:generate</code>. This command generates a configuration file (often named <code>flutter_launcher_icons.yaml</code> or similar, or directly processes the <code>pubspec.yaml</code>) which <code>flutter_launcher_icons</code> uses.</p>
<p><em>Correction</em>: The prompt mentions "generate a config file and setup the image path to the path of the app_logo.png then run dart run flutter_launcher_icons to generate the assets". It seems <code>flutter_launcher_icons:generate</code> might be an older or specific command, the typical usage is to run <code>flutter_launcher_icons</code> directly after setting <code>image_path</code> in <code>pubspec.yaml</code>. For the given configuration, the <code>image_path</code> is already set in <code>pubspec.yaml</code>.</p>
<p>Then, run <code>dart run flutter_launcher_icons</code>. This command executes the <code>flutter_launcher_icons</code> package, which takes the <code>image_path</code> specified in <code>pubspec.yaml</code> and generates all the necessary icon files at various resolutions for both Android and iOS, placing them in the correct native project directories.</p>
<h2 id="heading-splash-screen-the-first-impression"><strong>Splash Screen: The First Impression</strong></h2>
<p>A splash screen (or launch screen) is the first screen users see when they open your app. It provides a branded experience while the app initializes resources. The <code>flutter_native_splash</code> package simplifies creating native splash screens for Flutter apps.</p>
<h3 id="heading-pubspecyaml-configuration-for-flutternativesplash"><code>pubspec.yaml</code> Configuration for <code>flutter_native_splash</code></h3>
<pre><code class="lang-yaml"><span class="hljs-attr">flutter_native_splash:</span>
  <span class="hljs-attr">color:</span> <span class="hljs-string">"#FFFFFF"</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">assets/images/app_logo.png</span>
  <span class="hljs-attr">android:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">android_gravity:</span> <span class="hljs-string">center</span>
  <span class="hljs-attr">fullscreen:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">ios:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>Here’s what’s happening:</p>
<ul>
<li><p><code>flutter_native_splash:</code>: The root key for the <code>flutter_native_splash</code> package configuration.</p>
</li>
<li><p><code>color: "#FFFFFF"</code>: Sets the background color of the splash screen. Here, it's set to white.</p>
</li>
<li><p><code>image: assets/images/app_logo.png</code>: Specifies the path to the image that will be displayed on the splash screen. In this case, it's the application's logo.</p>
</li>
<li><p><code>android: true</code>: Enables splash screen generation for Android.</p>
</li>
<li><p><code>android_gravity: center</code>: For Android, this centers the splash image on the screen.</p>
</li>
<li><p><code>fullscreen: true</code>: Makes the splash screen appear in fullscreen mode, without status or navigation bars.</p>
</li>
<li><p><code>ios: true</code>: Enables splash screen generation for iOS.</p>
</li>
</ul>
<h3 id="heading-generating-the-splash-screen"><strong>Generating the Splash Screen</strong></h3>
<p>After configuring <code>pubspec.yaml</code>, run the following command in your terminal: <code>dart run flutter_native_splash:create</code>. It processes the configuration and generates the native splash screen files (for example, launch images, drawables) in the respective Android and iOS project folders, ensuring they are properly integrated into the native launch process.</p>
<h2 id="heading-screenshots-from-the-app"><strong>Screenshots from the App</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748068995235/d84ad92d-a686-43ee-a34c-89f2d6bf7e17.png" alt="d84ad92d-a686-43ee-a34c-89f2d6bf7e17" class="image--center mx-auto" width="1818" height="1240" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748069008469/f5fecee8-93dd-46ef-92ae-bd8c5413b3a7.png" alt="f5fecee8-93dd-46ef-92ae-bd8c5413b3a7" class="image--center mx-auto" width="1353" height="1200" loading="lazy"></p>
<p>Keep in mind that the output quality can vary depending on the AI model you’re using. The same applies to YouTube links and image URLs – sometimes they work perfectly, and other times they may not. So if something doesn’t work as expected, it’s not necessarily on your end.</p>
<p>Also, remember there are so many ways to achieve this and you don’t necessarily use to use this method. I’ll provide some other resources you can check out below. You can use <code>systemInstructions</code> instead of defining constraints in text the way I did it.</p>
<p><strong>Here’s the completed project:</strong> <a target="_blank" href="https://github.com/Atuoha/snap2chef_ai">https://github.com/Atuoha/snap2chef_ai</a></p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>I hope this comprehensive breakdown has given you a clear understanding of the "Snap2Chef" application's structure, UI components, and underlying configurations. May your coding journey be filled with creativity and successful implementations.</p>
<p>Happy coding!</p>
<h2 id="heading-references">References</h2>
<p>Here are some references for the key technologies and packages used in this application:</p>
<h3 id="heading-flutter-packages">Flutter Packages</h3>
<ul>
<li><p><code>flutter/material.dart</code>: The core Flutter Material Design package.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://api.flutter.dev/flutter/material/material-library.html">Flutter API Docs - material library</a></li>
</ul>
</li>
<li><p><code>iconsax/iconsax.dart</code>: A custom icon set for Flutter.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://www.google.com/search?q=https://pub.dev/packages/iconsax">pub.dev - iconsax</a></li>
</ul>
</li>
<li><p><code>gap/gap.dart</code>: A simple package for adding spacing between widgets.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/gap">pub.dev - gap</a></li>
</ul>
</li>
<li><p><code>dotted_border/dotted_border.dart</code>: A Flutter package to draw a dotted border around any widget.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/dotted_border">pub.dev - dotted_border</a></li>
</ul>
</li>
<li><p><code>flutter/cupertino.dart</code>: The core Flutter Cupertino (iOS-style) widgets package.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://api.flutter.dev/flutter/cupertino/cupertino-library.html">Flutter API Docs - cupertino library</a></li>
</ul>
</li>
<li><p><code>flutter_launcher_icons</code>: A package for generating application launcher icons.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/flutter_launcher_icons">pub.dev - flutter_launcher_icons</a></li>
</ul>
</li>
<li><p><code>flutter_native_splash</code>: A package for generating native splash screens.</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/flutter_native_splash">pub.dev - flutter_native_splash</a></li>
</ul>
</li>
<li><p><code>image_picker</code> (Implicitly used by <code>ImageUploadController</code>): A Flutter plugin for picking images from the image library, or taking new photos with the camera. (Though not directly imported in the provided snippets, <code>ImageUploadController</code> likely uses this or a similar package).</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/image_picker">pub.dev - image_picker</a></li>
</ul>
</li>
<li><p><code>image_cropper</code> (Implicitly used by <code>ImageUploadController</code>): A Flutter plugin for cropping images. (Likely used in conjunction with <code>image_picker</code> for <code>assignCroppedImage</code>).</p>
<ul>
<li><strong>Reference:</strong> <a target="_blank" href="https://pub.dev/packages/image_cropper">pub.dev - image_cropper</a></li>
</ul>
</li>
</ul>
<h3 id="heading-apis-and-platforms"><strong>APIs and Platforms</strong></h3>
<ul>
<li><p><strong>Gemini API</strong>: Google's family of generative AI models.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://www.google.com/search?q=https://ai.google.dev/gemini">Google AI Gemini API</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://cloud.google.com/gemini/docs">Google Cloud - Gemini API Documentation</a></p>
</li>
</ul>
</li>
<li><p><strong>Firebase</strong>: Google's comprehensive app development platform.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://firebase.google.com/">Firebase Official Website</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://firebase.google.com/docs">Firebase Documentation</a></p>
</li>
<li><p><strong>Firebase Console/Studio</strong>: The web-based interface for managing Firebase projects.</p>
</li>
</ul>
</li>
<li><p><strong>Vertex AI</strong>: Google Cloud's machine learning platform.</p>
<ul>
<li><p><strong>Reference:</strong> <a target="_blank" href="https://cloud.google.com/vertex-ai">Google Cloud - Vertex AI</a></p>
</li>
<li><p><strong>Documentation:</strong> <a target="_blank" href="https://cloud.google.com/vertex-ai/docs">Google Cloud - Vertex AI Documentation</a></p>
</li>
</ul>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
