<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Codexlancers</title>
    <description>The latest articles on DEV Community by Codexlancers (@codexlancers).</description>
    <link>https://dev.to/codexlancers</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3857287%2Fa8e4508f-a0b0-447b-a06a-9bc5efa2ebf5.jpeg</url>
      <title>DEV Community: Codexlancers</title>
      <link>https://dev.to/codexlancers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codexlancers"/>
    <language>en</language>
    <item>
      <title>From Design to Flutter Code: Using Google Stitch MCP with Antigravity AI</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/from-design-to-flutter-code-using-google-stitch-mcp-with-antigravity-ai-4bmn</link>
      <guid>https://dev.to/codexlancers/from-design-to-flutter-code-using-google-stitch-mcp-with-antigravity-ai-4bmn</guid>
      <description>&lt;p&gt;Design-to-code workflows are becoming increasingly efficient with the introduction of the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;. By connecting Google Stitch with Antigravity AI, developers can allow AI agents to access design context directly and generate Flutter implementations with significantly less manual effort.&lt;/p&gt;

&lt;p&gt;In this article, we'll walk through the complete setup process and demonstrate how to generate Flutter UI code directly from Google Stitch designs using MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) is an open standard that enables AI applications to securely connect with external tools and data sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits include:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Direct access to design context
&lt;/li&gt;
&lt;li&gt;Reduced manual design interpretation
&lt;/li&gt;
&lt;li&gt;Faster UI implementation
&lt;/li&gt;
&lt;li&gt;Improved code consistency
&lt;/li&gt;
&lt;li&gt;Better collaboration between designers and developers
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use Google Stitch with Antigravity AI?
&lt;/h2&gt;

&lt;p&gt;Google Stitch provides AI-powered design generation, while Antigravity AI can consume that design context through MCP and generate implementation code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits
&lt;/h3&gt;

&lt;p&gt;✅ Faster design-to-development workflow&lt;br&gt;
✅ Reduced UI implementation time&lt;br&gt;
✅ Better design accuracy&lt;br&gt;
✅ Reusable Flutter components&lt;br&gt;
✅ Production-ready code generation&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fezai4w3afp13khs017xh.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fezai4w3afp13khs017xh.jpeg" alt="Architecture Overview" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdavvzxy11r80gz2faoeq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdavvzxy11r80gz2faoeq.jpeg" alt=" " width="674" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bswqdn7r1ofqhwrxddy.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bswqdn7r1ofqhwrxddy.jpeg" alt=" " width="391" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwmmrxwc4f6kja3rfzog.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwmmrxwc4f6kja3rfzog.jpeg" alt=" " width="705" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jzr6s40g82vpzv9v2gz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jzr6s40g82vpzv9v2gz.jpeg" alt=" " width="800" height="386"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google Stitch
│
▼
MCP Server
│
▼
Antigravity AI
│
▼
Flutter Code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1: Generate UI Design in Google Stitch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open Google Stitch.&lt;/li&gt;
&lt;li&gt;Generate your mobile application design.&lt;/li&gt;
&lt;li&gt;Select all screens you want to export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finalize navigation flow before exporting
&lt;/li&gt;
&lt;li&gt;Use consistent design patterns
&lt;/li&gt;
&lt;li&gt;Keep component naming organized
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Export MCP Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Export&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;MCP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Set Up MCP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose your preferred IDE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After selecting Antigravity, Google Stitch provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP Configuration
&lt;/li&gt;
&lt;li&gt;API Key
&lt;/li&gt;
&lt;li&gt;Connection Details
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy the generated configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure MCP in Antigravity AI
&lt;/h3&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent Panel
└── Three Dot Menu
└── MCP Servers
└── Manage MCP Servers
└── View Raw Config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the Stitch MCP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stitch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serverUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://stitch.googleapis.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"X-Goog-Api-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR-API-KEY"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace: &lt;code&gt;YOUR-API-KEY&lt;/code&gt; with the API key provided by Google Stitch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 4: Verify the Connection
&lt;/h3&gt;

&lt;p&gt;After saving the configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restart Antigravity if needed.&lt;/li&gt;
&lt;li&gt;Open MCP Servers.&lt;/li&gt;
&lt;li&gt;Verify that Stitch appears in the configured server list.&lt;/li&gt;
&lt;li&gt;Confirm the connection status is active.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Server Not&amp;nbsp;Visible
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Restart the Antigravity service.&lt;/li&gt;
&lt;li&gt;Verify that the server is running successfully.&lt;/li&gt;
&lt;li&gt;Check logs for startup errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Authentication Error
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ensure your API key is correct.&lt;/li&gt;
&lt;li&gt;Confirm that the key has not expired.&lt;/li&gt;
&lt;li&gt;Verify environment variables are properly configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Connection Failure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check your internet connection.&lt;/li&gt;
&lt;li&gt;Verify firewall or proxy settings.&lt;/li&gt;
&lt;li&gt;Ensure the target endpoint is reachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Invalid Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Validate your JSON configuration file.&lt;/li&gt;
&lt;li&gt;Check for missing commas, brackets, or quotation marks.&lt;/li&gt;
&lt;li&gt;Use a JSON validator before restarting the service.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: Most startup issues can be resolved by validating the configuration file and restarting the Antigravity server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Generate Flutter UI&amp;nbsp;Code
&lt;/h2&gt;

&lt;p&gt;Once the MCP connection is established, Antigravity AI can access the Stitch design context.&lt;br&gt;
Example prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the selected Google Stitch mobile app screens in Flutter.

Requirements:
- Use GetX for state management
- Follow Material Design guidelines
- Create reusable widgets
- Maintain responsive layouts
- Use proper folder structure
- Generate clean production-ready code
- Match the Stitch design closely

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI agent will analyze the design through MCP and generate Flutter code accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Development Workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design Creation
      │
      ▼
Google Stitch
      │
      ▼
Export MCP
      │
      ▼
Antigravity AI
      │
      ▼
Flutter Code Generation
      │
      ▼
Developer Review
      │
      ▼
Production App

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;During testing, the following practices produced the best results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Export complete screen flows instead of individual screens.&lt;/li&gt;
&lt;li&gt;Provide detailed implementation requirements.&lt;/li&gt;
&lt;li&gt;Request reusable widgets and proper architecture.&lt;/li&gt;
&lt;li&gt;Review generated code before production deployment.&lt;/li&gt;
&lt;li&gt;Use GetX modules for scalable project organization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating Google Stitch with Antigravity AI through MCP, Flutter developers can significantly accelerate the design-to-code process. Instead of manually translating designs into code, developers can focus on refining business logic and user experience while AI handles much of the UI implementation work.&lt;/p&gt;

&lt;p&gt;If you're already using Flutter and GetX, this workflow can dramatically reduce UI development time while maintaining consistency with your original designs.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Advanced PDF Toolkit for Flutter</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/advanced-pdf-toolkit-for-flutter-lkn</link>
      <guid>https://dev.to/codexlancers/advanced-pdf-toolkit-for-flutter-lkn</guid>
      <description>&lt;p&gt;A premium, high-performance, native PDF viewer plugin for Flutter. It renders PDFs smoothly via Flutter &lt;code&gt;PlatformViews&lt;/code&gt; directly on top of platform-native PDF frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt; - &lt;a href="https://developer.android.com/reference/android/graphics/pdf/PdfRenderer" rel="noopener noreferrer"&gt;PdfRenderer&lt;/a&gt; for high-performance page rendering, combined with &lt;a href="https://github.com/TomRoush/PdfBox-Android" rel="noopener noreferrer"&gt;pdfbox-android&lt;/a&gt; for text search and bookmark/outline extraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt; - &lt;a href="https://developer.apple.com/documentation/pdfkit" rel="noopener noreferrer"&gt;PDFKit&lt;/a&gt; for full native rendering, text search, and outline support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin exposes a powerful Dart-side controller and an optional, fully-customizable built-in toolbar for common viewer interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Open PDFs from assets, network URLs, local file paths, raw bytes (&lt;code&gt;Uint8List&lt;/code&gt;), or base64-encoded strings&lt;/li&gt;
&lt;li&gt;Built-in toolbar with page navigation, zoom controls, search, bookmarks, page thumbnails, and a theme toggle - each shown/hidden independently&lt;/li&gt;
&lt;li&gt;Programmatic control via &lt;code&gt;FlutterPdfToolkitController&lt;/code&gt;: jump to page, next / previous page, zoom in / out / reset, search, bookmarks, dark mode, page thumbnails&lt;/li&gt;
&lt;li&gt;Horizontal or vertical scrolling, single-page or continuous page modes&lt;/li&gt;
&lt;li&gt;Pinch-to-zoom and double-tap-to-zoom on &lt;strong&gt;both&lt;/strong&gt; Android and iOS&lt;/li&gt;
&lt;li&gt;Initial page and initial zoom level configuration&lt;/li&gt;
&lt;li&gt;Password-protected PDF support, with an automatic unlock dialog&lt;/li&gt;
&lt;li&gt;Dark mode toggle at the Flutter UI level&lt;/li&gt;
&lt;li&gt;Text search with result highlighting and bookmark/outline support on &lt;strong&gt;both&lt;/strong&gt; Android and iOS&lt;/li&gt;
&lt;li&gt;Page thumbnails rendered natively from the PDF and shown in a grid for quick navigation&lt;/li&gt;
&lt;li&gt;Built-in "Sign document" toolbar action: draw a signature, position and resize it on the current page, and stamp it onto the PDF natively via &lt;code&gt;FlutterPdfToolkit.signPdf()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Built-in "Reorder pages" toolbar action: drag page thumbnails into a new order and save the rearranged PDF natively via &lt;code&gt;FlutterPdfToolkit.reorderPages()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Built-in "Delete pages" toolbar action: tap page thumbnails to mark them for removal and save the trimmed PDF natively via &lt;code&gt;FlutterPdfToolkit.removePages()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Built-in "Add pages" toolbar action: pick where to insert new pages from a page-thumbnail grid, then insert PDF and/or image files supplied by the host app natively via &lt;code&gt;FlutterPdfToolkit.insertPages()&lt;/code&gt; and &lt;code&gt;FlutterPdfToolkit.imagesToPdf()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Built-in "Add image" toolbar action: pick an image supplied by the host app, position and resize it on the current page, and stamp it onto the PDF natively via &lt;code&gt;FlutterPdfToolkit.addImageToPdf()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Merge multiple PDF files into a single document natively via &lt;code&gt;FlutterPdfToolkit.mergePdfs()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Split a PDF into multiple documents by page range natively via &lt;code&gt;FlutterPdfToolkit.splitPdf()&lt;/code&gt;, with &lt;code&gt;FlutterPdfToolkit.getPdfPageCount()&lt;/code&gt; to read a PDF's page count&lt;/li&gt;
&lt;li&gt;Save or share a PDF file via &lt;code&gt;FlutterPdfToolkit.downloadPdf()&lt;/code&gt; - saves to the Downloads folder on Android, or opens the share sheet on iOS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform Support
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Android (API 21 / Android 5.0)&lt;/strong&gt;&lt;br&gt;
Rendering: PdfRenderer + pdfbox-android&lt;br&gt;
Features: Search and bookmarks via pdfbox-android&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS (iOS 13.0)&lt;/strong&gt;&lt;br&gt;
Rendering: PDFKit&lt;br&gt;
Features: Full search and bookmark/outline support&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Unsupported Platforms&lt;br&gt;
Web, desktop, and macOS are not implemented. FlutterPdfToolkit renders via AndroidView/UiKitView, so usage outside Android/iOS will fail at runtime.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Flutter &lt;code&gt;&amp;gt;=1.17.0&lt;/code&gt; and Dart SDK &lt;code&gt;^3.11.5&lt;/code&gt; (as declared in &lt;code&gt;pubspec.yaml&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Android: &lt;code&gt;minSdkVersion 21&lt;/code&gt; or higher (raise it in &lt;code&gt;android/app/build.gradle&lt;/code&gt; if your app currently targets lower).&lt;/li&gt;
&lt;li&gt;iOS: deployment target &lt;code&gt;13.0&lt;/code&gt; or higher (set &lt;code&gt;platform&amp;nbsp;:ios, '13.0'&lt;/code&gt; in your &lt;code&gt;ios/Podfile&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Add the dependency to your &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter_pdf_toolkit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No additional native setup is required - the plugin registers its platform views automatically on Android and iOS.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you load PDFs from the network, make sure your app has internet access permission (already enabled by default for Android, and allowed by default on iOS unless you've restricted App Transport Security).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_pdf_toolkit/flutter_pdf_toolkit.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PDF Viewer'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  With a Controller
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;FlutterPdfToolkitController&lt;/code&gt; lets you drive the viewer programmatically and read its current state (page number, zoom level, bookmarks, search results, etc.).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PdfScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_PdfScreenState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_PdfScreenState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PdfScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkitController&lt;/span&gt; &lt;span class="n"&gt;_controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkitController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PDF Viewer'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;actions:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;skip_next&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'assets/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;_controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PDF Sources
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PdfSource&lt;/code&gt; is a sealed class with a factory constructor for each supported input type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From an asset bundled with your app&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'assets/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'assets/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;bundle:&lt;/span&gt; &lt;span class="n"&gt;myAssetBundle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// From a network URL, optionally with custom headers (e.g. auth tokens)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'https://example.com/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;'https://example.com/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Bearer &amp;lt;token&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// From an absolute local file path&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/storage/emulated/0/Download/sample.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// From raw bytes (e.g. downloaded or generated in-memory)&lt;/span&gt;
&lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Uint8List&lt;/span&gt;

&lt;span class="c1"&gt;// From a base64-encoded string (data URLs such as "data:application/pdf;base64,..." are also accepted)&lt;/span&gt;
&lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64PdfString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64PdfString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;password:&lt;/span&gt; &lt;span class="s"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally, asset, network, bytes, and base64 sources are written to a temporary file before being handed to the native viewer; file path sources are passed through as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  FlutterPdfToolkit widget reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core properties
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; (PdfSource) - &lt;strong&gt;Required&lt;/strong&gt;. The PDF to display.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;controller&lt;/code&gt; (FlutterPdfToolkitController?) - Controller to drive the viewer. If omitted, internal controller is created.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Toolbar controls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;showToolbar&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show/hide the built-in toolbar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showSearch&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show search icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showBookmarks&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show bookmarks icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showThumbnails&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show page thumbnails.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showZoomControls&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show zoom controls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showPageControls&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show prev/next buttons.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showThemeToggle&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show light/dark toggle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Document editing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;showSignature&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show "Sign document" icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onSigned&lt;/code&gt; (void Function(String)?) - Called with signed file path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showReorder&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show "Reorder pages" icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onPagesReordered&lt;/code&gt; (void Function(String)?) - Called with reordered file path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showDeletePages&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show "Delete pages" icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onPagesRemoved&lt;/code&gt; (void Function(String)?) - Called with trimmed file path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showAddPages&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show "Add pages" icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onAddPagesRequested&lt;/code&gt; (Future?&amp;gt; Function()?) - Return file paths to insert.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;showAddImage&lt;/code&gt; (bool) = &lt;code&gt;true&lt;/code&gt; - Show "Add image" icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onAddImageRequested&lt;/code&gt; (Future Function()?) - Return JPEG/PNG bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Styling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;darkMode&lt;/code&gt; (bool) = &lt;code&gt;false&lt;/code&gt; - Initial dark mode state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toolbarBackgroundColor&lt;/code&gt; (Color?) - Toolbar background.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toolbarForegroundColor&lt;/code&gt; (Color?) - Icon/text color.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backgroundColor&lt;/code&gt; (Color?) - Background during load/error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialZoomLevel&lt;/code&gt; (double) = &lt;code&gt;1.0&lt;/code&gt; - Zoom level on load.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;initialPage&lt;/code&gt; (int) = &lt;code&gt;1&lt;/code&gt; - Starting page (1-indexed).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;password&lt;/code&gt; (String?) - Password for encrypted PDFs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scrollDirection&lt;/code&gt; (Axis) = &lt;code&gt;Axis.vertical&lt;/code&gt; - Vertical or horizontal scrolling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;singlePage&lt;/code&gt; (bool) = &lt;code&gt;false&lt;/code&gt; - One page at a time vs continuous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If the toolbar is hidden (&lt;code&gt;showToolbar: false&lt;/code&gt;), drive the viewer using a &lt;code&gt;FlutterPdfToolkitController&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controller reference (FlutterPdfToolkitController)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FlutterPdfToolkitController&lt;/code&gt; extends &lt;code&gt;ChangeNotifier&lt;/code&gt;, so you can listen to it directly or wrap the viewer in an &lt;code&gt;AnimatedBuilder&lt;/code&gt; / &lt;code&gt;ListenableBuilder&lt;/code&gt; to rebuild your own UI in response to state changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  FlutterPdfToolkitController reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  State (read-only)
&lt;/h3&gt;

&lt;p&gt;Observable properties of the current document and viewer state.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pageNumber&lt;/code&gt; (int) - Current page (1-indexed).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageCount&lt;/code&gt; (int) - Total number of pages in the document.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zoomLevel&lt;/code&gt; (double) - Current zoom level.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isReady&lt;/code&gt; (bool) - Whether the native view has finished initializing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;errorMessage&lt;/code&gt; (String?) - Non-null if document failed to load.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bookmarks&lt;/code&gt; (List) - Outline/bookmarks from platform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;searchText&lt;/code&gt; (String?) - The active search query, if any.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;searchResultCount&lt;/code&gt; (int) - Number of matches for active search.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;currentSearchResultIndex&lt;/code&gt; (int) - Index (1-based) of highlighted match.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Actions
&lt;/h3&gt;

&lt;p&gt;Methods to control the viewer and document interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jumpToPage(int pageNumber)&lt;/code&gt; - Jump to page (1-indexed).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nextPage()&lt;/code&gt; - Go to next page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;previousPage()&lt;/code&gt; - Go to previous page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Zoom
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zoomIn([double step = 0.25])&lt;/code&gt; - Increase zoom by step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zoomOut([double step = 0.25])&lt;/code&gt; - Decrease zoom by step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resetZoom()&lt;/code&gt; - Reset to default zoom.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;search(String text, {bool caseSensitive = false}) - Search the document.&lt;/li&gt;
&lt;li&gt;clearSearch() - Clear active search and highlights.&lt;/li&gt;
&lt;li&gt;nextSearchResult() - Jump to next match.&lt;/li&gt;
&lt;li&gt;previousSearchResult() - Jump to previous match.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;requestBookmarks()&lt;/code&gt; - Re-request bookmark/outline list.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setDarkMode(bool enabled)&lt;/code&gt; - Toggle dark mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getPageThumbnail(int pageNumber, {int width = 200})&lt;/code&gt; - Returns PNG thumbnail as Uint8List.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  PdfBookmarkItem
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PdfBookmarkItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Password-Protected PDFs
&lt;/h2&gt;

&lt;p&gt;There are two ways to supply a password:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;password&lt;/code&gt; directly to &lt;code&gt;FlutterPdfToolkit&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/path/to/protected.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;password:&lt;/span&gt; &lt;span class="s"&gt;'samplefiles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Attach a password to a &lt;code&gt;PdfSource.base64&lt;/code&gt; source:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64PdfString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;password:&lt;/span&gt; &lt;span class="s"&gt;'samplefiles'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no password is supplied (or the supplied password is incorrect), the widget shows a built-in "&lt;strong&gt;Password Required&lt;/strong&gt;" dialog and prompts the user to enter the password. The dialog re-displays with an "Incorrect password" error if the entered password is rejected, and can be cancelled to abort loading the document.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Text search is supported on both Android and iOS.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;On iOS, search uses &lt;code&gt;PDFKit's&lt;/code&gt; native text search.&lt;/li&gt;
&lt;li&gt;On Android, the page text is extracted with &lt;code&gt;pdfbox-android&lt;/code&gt; (cached after first use) and matched case-insensitively.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Matches are highlighted on the page and the view automatically scrolls to the first/selected match.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;controller.search('query')&lt;/code&gt;, &lt;code&gt;nextSearchResult()&lt;/code&gt;, &lt;code&gt;previousSearchResult()&lt;/code&gt;, and &lt;code&gt;clearSearch()&lt;/code&gt; to drive search programmatically. The toolbar reflects the active query and match count when present.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Bookmarks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bookmarks (PDF outline entries) are populated automatically on view creation via &lt;code&gt;requestBookmarks()&lt;/code&gt; and exposed through &lt;code&gt;controller.bookmarks&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tapping the bookmarks icon in the toolbar opens a bottom sheet listing all bookmarks; tapping an entry jumps to that page.&lt;/li&gt;
&lt;li&gt;Supported on &lt;strong&gt;both Android and iOS&lt;/strong&gt; - Android extracts the document outline with &lt;code&gt;pdfbox-android&lt;/code&gt;, while iOS reads it via &lt;code&gt;PDFKit&lt;/code&gt;'s &lt;code&gt;outlineRoot&lt;/code&gt;. If a PDF has no outline/bookmarks defined, &lt;code&gt;bookmarks&lt;/code&gt; will be empty on either platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Page Thumbnails
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tapping the thumbnails icon in the toolbar opens a grid of page previews; tapping a thumbnail jumps to that page.&lt;/li&gt;
&lt;li&gt;Each thumbnail is rendered natively (via &lt;code&gt;PDFKit&lt;/code&gt; on iOS and &lt;code&gt;PdfRenderer&lt;/code&gt; on Android) and fetched lazily as the grid is shown, then cached for the lifetime of the document.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;controller.getPageThumbnail(pageNumber, width: 200)&lt;/code&gt; to fetch a thumbnail yourself, e.g. to build a custom page picker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Signing PDFs
&lt;/h2&gt;

&lt;p&gt;Tapping the "Sign document" icon (the pen/draw icon) in the toolbar opens a built-in flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A dialog where the user draws their signature with a finger/stylus (powered by &lt;code&gt;SignaturePad&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A bottom sheet showing a preview of the current page, where the user can drag and resize the signature before confirming placement.&lt;/li&gt;
&lt;li&gt;The signature is stamped onto the current page natively and the viewer reloads the signed copy. &lt;code&gt;onSigned&lt;/code&gt; is called with the path to the new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hide the built-in action with &lt;code&gt;showSignature: false&lt;/code&gt; if you want to build your own signing UI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SignaturePad&lt;/code&gt; and &lt;code&gt;SignaturePadController&lt;/code&gt; are exported so you can build a custom signature-capture UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SignaturePadController&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SignaturePadController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SignaturePadState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;padKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SignaturePadState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;SignaturePad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;padKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Later, e.g. when the user taps "Done":&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pngBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;padKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentState&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;toPngBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;toPngBytes()&lt;/code&gt; renders the drawn strokes to a transparent PNG. Call &lt;code&gt;controller.clear()&lt;/code&gt; to reset the pad and &lt;code&gt;controller.isEmpty&lt;/code&gt; to check whether anything has been drawn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stamp a signature (or any PNG image) onto a PDF page yourself with &lt;code&gt;FlutterPdfToolkit.signPdf()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;signedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;sourcePath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/signed.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;signatureImageBytes:&lt;/span&gt; &lt;span class="n"&gt;pngBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;pageNumber:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1-indexed&lt;/span&gt;
  &lt;span class="nl"&gt;xRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// left edge, as a fraction of the page width&lt;/span&gt;
  &lt;span class="nl"&gt;yRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// top edge, as a fraction of the page height&lt;/span&gt;
  &lt;span class="nl"&gt;widthRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// signature width, as a fraction of the page width&lt;/span&gt;
  &lt;span class="nl"&gt;heightRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// signature height, as a fraction of the page height&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signing parameters
&lt;/h2&gt;

&lt;p&gt;Parameters for the PDF signing operation. The signature image is stamped onto the specified page at the given position and size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sourcePath&lt;/code&gt; (String) - Absolute file path of the PDF to sign.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the signed PDF should be written.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signatureImageBytes&lt;/code&gt; (Uint8List) - PNG image to stamp onto the page, ideally with transparent background.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageNumber&lt;/code&gt; (int) - 1-indexed page number to stamp the signature onto.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;xRatio&lt;/code&gt; / &lt;code&gt;yRatio&lt;/code&gt; (double) - Top-left position of signature as fraction (0.0–1.0) of page width/height.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;widthRatio&lt;/code&gt; / &lt;code&gt;heightRatio&lt;/code&gt; (double) - Size of signature as fraction (0.0–1.0) of page width/height.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if signing failed. Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android (appended to the page's content stream) and &lt;code&gt;CoreGraphics&lt;/code&gt; on iOS (the page is redrawn into a new PDF context with the signature composited on top) - so &lt;code&gt;sourcePath&lt;/code&gt; must be a local file path and existing vector content is preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reordering Pages
&lt;/h2&gt;

&lt;p&gt;Tapping the "Reorder pages" icon (the swap icon) in the toolbar opens a built-in flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A bottom sheet shows every page as a thumbnail in a grid.&lt;/li&gt;
&lt;li&gt;The user presses and holds a page, then drags it to a new position. Each tile shows a badge with its current position in the new order.&lt;/li&gt;
&lt;li&gt;Tapping "Save order" rewrites the document with the pages in their new order natively, and the viewer reloads the rearranged copy. &lt;code&gt;onPagesReordered&lt;/code&gt; is called with the path to the new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Hide the built-in action with &lt;code&gt;showReorder: false&lt;/code&gt; if you want to build your own reordering UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reorder (and optionally drop) pages yourself with &lt;code&gt;FlutterPdfToolkit.reorderPages()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;reorderedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reorderPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/reordered.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;pageOrder:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// moves page 3 to the front, drops the rest&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reordering parameters
&lt;/h3&gt;

&lt;p&gt;Parameters for the PDF page reordering operation. Reorder pages, drop unwanted pages, or duplicate pages in the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; (String) - Absolute file path of the PDF to reorder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the reordered PDF should be written.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageOrder&lt;/code&gt; (List) - 1-based page numbers from source PDF, in desired output order. Omit a page number to drop that page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if reordering failed. Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android and &lt;code&gt;PDFKit&lt;/code&gt; on iOS - so &lt;code&gt;path&lt;/code&gt; must be a local file path. Use &lt;code&gt;getPdfPageCount()&lt;/code&gt; to determine valid page numbers beforehand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;p&gt;Keep pages 1, 3, 5 in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageOrder:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reverse page order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageOrder:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop page 2, keep others:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageOrder:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Removing Pages
&lt;/h2&gt;

&lt;p&gt;Tapping the "Delete pages" icon (the trash icon) in the toolbar opens a built-in flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A bottom sheet shows every page as a thumbnail in a grid.&lt;/li&gt;
&lt;li&gt;The user taps pages to mark them for removal. Selected pages are highlighted with a checkmark.&lt;/li&gt;
&lt;li&gt;Tapping "Delete N pages" rewrites the document without the selected pages natively, and the viewer reloads the trimmed copy. &lt;code&gt;onPagesRemoved&lt;/code&gt; is called with the path to the new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Hide the built-in action with &lt;code&gt;showDeletePages: false&lt;/code&gt; if you want to build your own page-removal UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remove pages yourself with &lt;code&gt;FlutterPdfToolkit.removePages()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;trimmedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removePages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/trimmed.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;pageNumbers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// removes pages 2 and 4&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Removing pages parameters
&lt;/h3&gt;

&lt;p&gt;Parameters for the PDF page deletion operation. Remove unwanted pages from the document.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; (String) - Absolute file path of the PDF to trim.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the trimmed PDF should be written.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageNumbers&lt;/code&gt; (List) - 1-based page numbers from source PDF to remove.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;p&gt;Delete pages 2 and 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageNumbers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete first page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageNumbers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete last 3 pages (10–12 of 12-page doc):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;pageNumbers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if removal failed (including if &lt;code&gt;pageNumbers&lt;/code&gt; covers every page in the document). Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android and &lt;code&gt;PDFKit&lt;/code&gt; on iOS - so &lt;code&gt;path&lt;/code&gt; must be a local file path. Use &lt;code&gt;getPdfPageCount()&lt;/code&gt; to determine valid page numbers beforehand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Pages
&lt;/h3&gt;

&lt;p&gt;Tapping the "Add pages" icon (the document-plus icon) in the toolbar opens a built-in flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A bottom sheet shows every page as a thumbnail in a grid, plus a "Start" tile. Tap a page to insert the new pages after it (or tap "Start" to insert them before page 1).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onAddPagesRequested&lt;/code&gt; is called - your app should show its own file and/or image picker here and return the chosen local file paths, in the order they should be inserted. Return null or an empty list to cancel.&lt;/li&gt;
&lt;li&gt;Any image files (&lt;code&gt;.jpg&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.jpeg&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.png&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.heic&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.heif&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.webp&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.bmp&lt;/code&gt;,&amp;nbsp;&lt;code&gt;.gif&lt;/code&gt;) are converted to single-page PDFs and combined with any PDF files, preserving the given order. The combined pages are inserted at the chosen position natively, and the viewer reloads the updated copy. &lt;code&gt;onPagesAdded&lt;/code&gt; is called with the path to the new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;showAddPages defaults to true, but the action requires onAddPagesRequested to be provided - without it, tapping the icon shows a message that adding pages isn't configured. Set &lt;code&gt;showAddPages: false&lt;/code&gt; if you want to build your own add-pages UI instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Insert another PDF's pages yourself with &lt;code&gt;FlutterPdfToolkit.insertPages()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;updatedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insertPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;insertPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/pages_to_insert.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/updated.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;afterPageNumber:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// insert after page 2 (0 = before page 1)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Insert pages parameters
&lt;/h2&gt;

&lt;p&gt;Parameters for the PDF page insertion operation. Insert pages from one PDF into another at a specific position.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; (String) - Absolute file path of the PDF to insert pages into.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;insertPath&lt;/code&gt; (String) - Absolute file path of the PDF whose pages should be inserted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the updated PDF should be written.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;afterPageNumber&lt;/code&gt; (int) - 1-based page number to insert after. 0 inserts before page 1. Document's page count appends at end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;p&gt;Insert at beginning (before page 1):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;afterPageNumber:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Insert after page 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;afterPageNumber:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append at end (5-page document):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;afterPageNumber:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if insertion failed. Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android and &lt;code&gt;PDFKit&lt;/code&gt; on iOS - so both &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;insertPath&lt;/code&gt; must be local file paths.&lt;/p&gt;

&lt;p&gt;Convert images into a PDF (one page per image) with &lt;code&gt;FlutterPdfToolkit.imagesToPdf()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pagesPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;imagesToPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;imagePaths:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'/path/to/photo1.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'/path/to/photo2.png'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/photos.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;imagePaths&lt;/code&gt; (List) - Absolute file paths of the images to convert, in page order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the resulting PDF should be written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if conversion failed. Each page is sized to match its source image. Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android and &lt;code&gt;CoreGraphics&lt;/code&gt; on iOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding images to a&amp;nbsp;PDF
&lt;/h2&gt;

&lt;p&gt;Tapping the "Add image" icon (the image-plus icon) in the toolbar opens a built-in flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;onAddImageRequested&lt;/code&gt; is called - your app should show its own image picker (e.g. via the &lt;code&gt;image_picker&lt;/code&gt; package) and return the chosen image's bytes (JPEG or PNG). Return &lt;code&gt;null&lt;/code&gt; to cancel.&lt;/li&gt;
&lt;li&gt;A bottom sheet shows a preview of the current page, where the user can drag and resize the image before confirming placement.&lt;/li&gt;
&lt;li&gt;The image is stamped onto the current page natively and the viewer reloads the updated copy. &lt;code&gt;onImageAdded&lt;/code&gt; is called with the path to the new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;showAddImage&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;, but the action requires &lt;code&gt;onAddImageRequested&lt;/code&gt; to be provided - without it, tapping the icon shows a message that adding images isn't configured. Set &lt;code&gt;showAddImage: false&lt;/code&gt; if you want to build your own add-image UI instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stamp an image onto a PDF page yourself with &lt;code&gt;FlutterPdfToolkit.addImageToPdf()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;updatedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addImageToPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;sourcePath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/with_image.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;imageBytes:&lt;/span&gt; &lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// JPEG or PNG bytes&lt;/span&gt;
  &lt;span class="nl"&gt;pageNumber:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1-indexed&lt;/span&gt;
  &lt;span class="nl"&gt;xRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// left edge, as a fraction of the page width&lt;/span&gt;
  &lt;span class="nl"&gt;yRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// top edge, as a fraction of the page height&lt;/span&gt;
  &lt;span class="nl"&gt;widthRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// image width, as a fraction of the page width&lt;/span&gt;
  &lt;span class="nl"&gt;heightRatio:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// image height, as a fraction of the page height&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  File paths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sourcePath&lt;/code&gt; (String) - Absolute file path of the PDF to modify.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the updated PDF should be written.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Image content
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;imageBytes&lt;/code&gt; (Uint8List) - JPEG or PNG image to stamp onto the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Page targeting
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pageNumber&lt;/code&gt; (int) - 1-indexed page number to stamp the image onto.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Position and&amp;nbsp;size
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xRatio&lt;/code&gt; / &lt;code&gt;yRatio&lt;/code&gt; (double) - Top-left position of the image as a fraction (0.0–1.0) of page width/height.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;widthRatio&lt;/code&gt; / &lt;code&gt;heightRatio&lt;/code&gt; (double) - Size of the image as a fraction (0.0–1.0) of page width/height.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns &lt;code&gt;outputPath&lt;/code&gt; on success, or &lt;code&gt;null&lt;/code&gt; if adding the image failed. Runs natively - &lt;code&gt;pdfbox-android&lt;/code&gt; on Android (appended to the page's content stream) and &lt;code&gt;CoreGraphics&lt;/code&gt; on iOS (the page is redrawn into a new PDF context with the image composited on top) - so sourcePath must be a local file path and existing vector content is preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging, Splitting, and Downloading PDFs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FlutterPdfToolkit&lt;/code&gt; exposes several static helper methods that don't require a viewer instance - they call straight into the native platform code via a shared method channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging PDFs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;mergedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mergePdfs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;paths:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'/path/to/first.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'/path/to/second.pdf'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nl"&gt;outputPath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output/merged.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mergedPath&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Open it in the viewer, e.g.:&lt;/span&gt;
  &lt;span class="c1"&gt;// FlutterPdfToolkit(source: PdfSource.filePath(mergedPath));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;paths&lt;/code&gt; (List) - Absolute file paths of the PDFs to merge, in the order they should appear in the output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputPath&lt;/code&gt; (String) - Absolute file path where the merged PDF should be written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Returns the outputPath on success, or null if merging failed (e.g. an invalid or unreadable input path). The merge is performed natively - PDFKit on iOS and pdfbox-android on Android - so all input paths must be local file paths. If a source is an asset, network URL, or in-memory bytes, write it to a temporary file first (see PDF sources).&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting a PDF's page&amp;nbsp;count
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pageCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPdfPageCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the total number of pages, or &lt;code&gt;null&lt;/code&gt; if the file could not be read. Useful for working out valid page ranges before calling &lt;code&gt;splitPdf()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting a&amp;nbsp;PDF
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splitPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;outputDirectory:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/output'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;pageRanges:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// pages 1-3 -&amp;gt; split_1.pdf&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// page 4    -&amp;gt; split_2.pdf&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// pages 5-10 -&amp;gt; split_3.pdf&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// e.g. open one of the resulting files in the viewer:&lt;/span&gt;
    &lt;span class="c1"&gt;// FlutterPdfToolkit(source: PdfSource.filePath(path));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;path&lt;/code&gt; (String) - Absolute file path of the PDF to split.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputDirectory&lt;/code&gt; (String) - Absolute path of the directory where split files are written. Directory is created automatically if it doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageRanges&lt;/code&gt; (List&amp;gt;) - One &lt;code&gt;[startPage, endPage]&lt;/code&gt; pair per output file, 1-based and inclusive (e.g., &lt;code&gt;[1, 3]&lt;/code&gt; = pages 1–3).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputFileNamePrefix&lt;/code&gt;(String) -Optional prefix for output file names. Defaults to &lt;code&gt;'split'&lt;/code&gt;. Files are named &lt;code&gt;&amp;lt;prefix&amp;gt;_&amp;lt;n&amp;gt;.pdf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Returns the list of output file paths, in the same order as pageRanges, or null if splitting failed (e.g. an invalid path or an out-of-range page number). Like mergePdfs(), this runs natively via PDFKit on iOS and pdfbox-android on Android, and path must be a local file path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Downloading / sharing a&amp;nbsp;PDF
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterPdfToolkit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;downloadPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;sourcePath:&lt;/span&gt; &lt;span class="s"&gt;'/path/to/merged.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;fileName:&lt;/span&gt; &lt;span class="s"&gt;'merged_document.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sourcePath&lt;/code&gt; (String) - Absolute path of the PDF file to save or share.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fileName&lt;/code&gt; (String) - Suggested file name for the saved or shared copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Android, the file is copied into the device's public Downloads folder under &lt;code&gt;fileName&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On iOS, the system share sheet is opened so the user can save the file to Files, AirDrop it, etc.&lt;/p&gt;

&lt;p&gt;Returns &lt;code&gt;true&lt;/code&gt; if the save/share action completed successfully, false otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zoom
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pinch-to-zoom is supported on both platforms via the built-in scroll/zoom view.&lt;/li&gt;
&lt;li&gt;Double-tapping the page toggles between the default zoom level and 2x zoom, centered on the tap location, on both Android and iOS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;controller.zoomIn()&lt;/code&gt;, &lt;code&gt;zoomOut()&lt;/code&gt;, and &lt;code&gt;resetZoom()&lt;/code&gt; drive zoom programmatically; the toolbar's zoom buttons call these directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dark Mode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;darkMode&lt;/code&gt; sets the initial state of the native viewer's color scheme.&lt;/li&gt;
&lt;li&gt;The built-in toolbar includes a sun/moon toggle (&lt;code&gt;showThemeToggle&lt;/code&gt;) that calls &lt;code&gt;controller.setDarkMode(...)&lt;/code&gt; and rebuilds the toolbar's icon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;controller.setDarkMode(bool enabled)&lt;/code&gt; can be called at any time - including before the native view is ready, in which case it is applied once the view finishes initializing.&lt;/li&gt;
&lt;li&gt;Dark mode only affects the PDF viewer's chrome/background at the native level; it does not alter the content of the PDF itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only Android and iOS are currently supported - there is no web, desktop, or macOS implementation.&lt;/li&gt;
&lt;li&gt;Search is case-insensitive on both platforms; advanced query syntax (regex, whole-word, etc.) is not supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building PDF functionality in Flutter shouldn't require multiple plugins, complex native integrations, or performance trade-offs. With &lt;strong&gt;flutter_pdf_toolkit&lt;/strong&gt;, you get a powerful all-in-one solution for viewing, searching, editing, signing, and managing PDFs using native rendering on Android and iOS. If your app works with documents, this package can help you deliver a faster, smoother, and more professional user experience with minimal effort. 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>pdf</category>
    </item>
    <item>
      <title>🚀 How to Create EXE Installer for a Flutter Windows App Using Inno Setup</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Tue, 14 Jul 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/how-to-create-exe-installer-for-a-flutter-windows-app-using-inno-setup-a16</link>
      <guid>https://dev.to/codexlancers/how-to-create-exe-installer-for-a-flutter-windows-app-using-inno-setup-a16</guid>
      <description>&lt;p&gt;Flutter makes it easy to build Windows desktop applications, but distributing your application can become messy if you share the entire release folder containing multiple files and directories.&lt;/p&gt;

&lt;p&gt;Instead of sending users a folder like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyApp.exe
flutter_windows.dll
data/
other dependency files...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can package everything into a single installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexInstaller.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this guide, you'll learn how to create a professional Windows installer for your Flutter desktop application using Inno Setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before getting started, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flutter installed and configured for Windows development&lt;/li&gt;
&lt;li&gt;A Flutter Windows application&lt;/li&gt;
&lt;li&gt;Inno Setup installed&lt;/li&gt;
&lt;li&gt;A Windows release build generated using:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter build windows &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your release files will be generated in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/windows/x64/runner/Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Use Inno Setup?
&lt;/h2&gt;

&lt;p&gt;Inno Setup allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Package your Flutter application into a single installer&lt;/li&gt;
&lt;li&gt;✅ Create desktop and Start Menu shortcuts&lt;/li&gt;
&lt;li&gt;✅ Install application files automatically&lt;/li&gt;
&lt;li&gt;✅ Add custom icons&lt;/li&gt;
&lt;li&gt;✅ Configure installation paths&lt;/li&gt;
&lt;li&gt;✅ Provide a professional installation experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Launch Inno Setup
&lt;/h2&gt;

&lt;p&gt;Open Inno Setup after installation.&lt;/p&gt;

&lt;p&gt;Select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a new script file using the Script Wizard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then click OK.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F556elanezjrxlwsrf0ij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F556elanezjrxlwsrf0ij.png" alt="Launch Inno&amp;nbsp;Setup" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Enter Application Information
&lt;/h2&gt;

&lt;p&gt;Fill in your application's basic information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application Name&lt;/li&gt;
&lt;li&gt;Application Version&lt;/li&gt;
&lt;li&gt;Publisher Name&lt;/li&gt;
&lt;li&gt;Publisher Website (Optional)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyr9vbzjn10sq9d6pe5i8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyr9vbzjn10sq9d6pe5i8.png" alt="Enter Application Information" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Configure the Installation Folder
&lt;/h2&gt;

&lt;p&gt;Choose where the application should be installed.&lt;/p&gt;

&lt;p&gt;Recommended settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application destination base folder:
Program Files Folder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter your application name as the folder name.&lt;br&gt;
You may optionally allow users to change the installation location.&lt;br&gt;
Click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flro2gkkpuu1oy6zwr00s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flro2gkkpuu1oy6zwr00s.png" alt="Configure the Installation Folder" width="529" height="398"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Add Flutter Release Files
&lt;/h2&gt;

&lt;p&gt;This is the most important step.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/windows/x64/runner/Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to include the required Flutter release files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Select the Main Executable
&lt;/h3&gt;

&lt;p&gt;Click Browse and choose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexApp.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes the application's primary executable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Additional Files
&lt;/h3&gt;

&lt;p&gt;Click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add File(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter_windows.dll
CodexApp.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add the Data Folder
&lt;/h3&gt;

&lt;p&gt;Click:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44ozocmmbak5jo7ed9cy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44ozocmmbak5jo7ed9cy.png" alt="Add Flutter Release&amp;nbsp;Files" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every Flutter Windows application is different.&lt;/p&gt;

&lt;p&gt;Some apps may contain additional DLL files, native libraries, AI models, assets, or plugin resources.&lt;/p&gt;

&lt;p&gt;If your application uses packages that generate extra files in the Release folder, include those files and folders in your installer as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Configure the Data Folder Destination
&lt;/h2&gt;

&lt;p&gt;After adding the data folder:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the folder entry.&lt;/li&gt;
&lt;li&gt;Click Edit.&lt;/li&gt;
&lt;li&gt;Set:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Destination Subfolder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data\*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the Flutter assets are installed correctly.&lt;/p&gt;

&lt;p&gt;Without this step, your application may fail to load resources after installation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsww2emira12fweg39pu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsww2emira12fweg39pu5.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5hkppvd01gsw9abzuyt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5hkppvd01gsw9abzuyt.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 6: File Association (Optional)
&lt;/h2&gt;

&lt;p&gt;If your application opens custom file types, configure them here.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Most Flutter applications don't require file associations.&lt;/p&gt;

&lt;p&gt;Leave this unchecked and click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F525gfj2suuk3u0d3n8lp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F525gfj2suuk3u0d3n8lp.png" alt=" " width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 7: Create Application Shortcuts
&lt;/h2&gt;

&lt;p&gt;Recommended settings:&lt;/p&gt;

&lt;p&gt;✅ Create a Start Menu shortcut&lt;br&gt;
✅ Allow users to create a Desktop shortcut&lt;/p&gt;

&lt;p&gt;This improves accessibility for end users.&lt;/p&gt;

&lt;p&gt;Click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25nec2womzywm30kc2vo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25nec2womzywm30kc2vo.png" alt=" " width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 8: Choose Installation Mode
&lt;/h2&gt;

&lt;p&gt;Select:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Administrative Install Mode
(Install for all users)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs the application for all users on the computer.&lt;/p&gt;

&lt;p&gt;Click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvp8wowdtk04b5rhfy9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvp8wowdtk04b5rhfy9v.png" alt=" " width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Configure Compiler Settings
&lt;/h2&gt;

&lt;p&gt;This step controls how the final installer will be generated.&lt;/p&gt;

&lt;p&gt;Configure:&lt;/p&gt;

&lt;h3&gt;
  
  
  Output Folder
&lt;/h3&gt;

&lt;p&gt;Choose where the installer should be generated.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Installers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installer Name
&lt;/h3&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexInstaller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Icon (Optional)
&lt;/h3&gt;

&lt;p&gt;You can specify a custom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.ico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;file for your installer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Password Protection (Optional)
&lt;/h3&gt;

&lt;p&gt;You may also password-protect the installer if needed.&lt;/p&gt;

&lt;p&gt;Click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0g5m6p6txxkd0gb6jrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0g5m6p6txxkd0gb6jrm.png" alt=" " width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Generate the Installer Script
&lt;/h2&gt;

&lt;p&gt;Continue through the remaining wizard pages and click Finish.&lt;/p&gt;

&lt;p&gt;Inno Setup will ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would you like to save the script before compiling?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select Yes to save the installer script. This allows you to quickly rebuild or update the installer whenever you release a new version of your Flutter Windows application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnykwsh6fxvmvanemix2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnykwsh6fxvmvanemix2.png" alt=" " width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Build the Installer
&lt;/h2&gt;

&lt;p&gt;Inno Setup will compile your script and package all required files.&lt;/p&gt;

&lt;p&gt;Once the compilation completes successfully, your output folder will contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexInstaller.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwl22e29t47ry39h8wem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwl22e29t47ry39h8wem.png" alt=" " width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Installer
&lt;/h2&gt;

&lt;p&gt;Before distributing your installer:&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Installation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Install on a clean machine or VM.&lt;/li&gt;
&lt;li&gt;Launch the application.&lt;/li&gt;
&lt;li&gt;Verify assets load correctly.&lt;/li&gt;
&lt;li&gt;Confirm shortcuts work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verify Uninstallation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open Control Panel.&lt;/li&gt;
&lt;li&gt;Uninstall the application.&lt;/li&gt;
&lt;li&gt;Confirm files are removed properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Issues and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  App Opens But Shows a Blank Screen
&lt;/h3&gt;

&lt;p&gt;Usually caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing data folder&lt;/li&gt;
&lt;li&gt;Incorrect destination subfolder configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure the data folder is installed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application Folder\data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Missing DLL Errors
&lt;/h3&gt;

&lt;p&gt;Ensure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flutter_windows.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is included in the installer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assets Not Loading
&lt;/h3&gt;

&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;has been added recursively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Folder Structure
&lt;/h2&gt;

&lt;p&gt;Your release folder should look similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Release/
│
├── MyApp.exe
├── flutter_windows.dll
├── data/
├── other dependency files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After packaging with Inno Setup, users only need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CodexInstaller.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Inno Setup is one of the easiest and most reliable tools for packaging Flutter Windows applications into a professional installer.&lt;/p&gt;

&lt;p&gt;With just a few minutes of setup, Inno Setup can transform your Flutter Windows release files into a professional installer that users can install with a few clicks.&lt;/p&gt;

&lt;p&gt;Whether you're distributing applications internally, sharing software with clients, or preparing for a public release, creating a proper installer provides a much better user experience than distributing raw release files.&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
    </item>
    <item>
      <title>Building AI-Powered .NET Applications Without Learning Python</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Fri, 10 Jul 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/building-ai-powered-net-applications-without-learning-python-334p</link>
      <guid>https://dev.to/codexlancers/building-ai-powered-net-applications-without-learning-python-334p</guid>
      <description>&lt;p&gt;For years, AI felt like something only Python developers could build.&lt;/p&gt;

&lt;p&gt;Every tutorial started with Jupyter notebooks, TensorFlow, and dozens of packages that backend developers had never heard of.&lt;/p&gt;

&lt;p&gt;Today, that's no longer true.&lt;/p&gt;

&lt;p&gt;If you're already building applications with ASP.NET Core, AI is becoming another capability you can add to your application rather than an entirely new field to learn.&lt;/p&gt;

&lt;p&gt;The good news is that Microsoft has built an ecosystem that allows developers to stay inside C# while adding intelligent features to their applications.&lt;/p&gt;

&lt;p&gt;In this article, we'll look at three approaches every .NET developer should know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ML.NET for training custom models&lt;/li&gt;
&lt;li&gt;ONNX for consuming existing models&lt;/li&gt;
&lt;li&gt;Azure AI for plug-and-play intelligence&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Option 1: Build Your Own Models with ML.NET
&lt;/h2&gt;

&lt;p&gt;ML.NET allows developers to train and consume machine learning models entirely inside .NET.&lt;/p&gt;

&lt;p&gt;Install it first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.ML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Define your data model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SentimentData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SentimentPrediction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;PredictedLabel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Probability&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Score&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Train a model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ML&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MLContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadFromTextFile&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SentimentData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"data.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hasHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;separatorChar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transforms&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FeaturizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Features"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SentimentData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinaryClassification&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trainers&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SdcaLogisticRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;labelColumnName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;featureColumnName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Features"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"sentiment_model.zip"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Make a prediction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatePredictionEngine&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="n"&gt;SentimentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SentimentPrediction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SentimentData&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"I love this product"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;$"Prediction: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PredictedLabel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Option 2: Use ONNX Models
&lt;/h2&gt;

&lt;p&gt;Sometimes training a model is unnecessary.&lt;/p&gt;

&lt;p&gt;You simply want to use one that already exists.&lt;/p&gt;

&lt;p&gt;Install the runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.ML.OnnxRuntime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load an ONNX model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ML.OnnxRuntime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ML.OnnxRuntime.Tensors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InferenceSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"model.onnx"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tensor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DenseTensor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3f&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;NamedOnnxValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFromTensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is great when performance matters or when data scientists already provide trained models.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 3: Add AI Without Building Models
&lt;/h2&gt;

&lt;p&gt;Sometimes developers overcomplicate AI.&lt;/p&gt;

&lt;p&gt;You don't always need machine learning.&lt;/p&gt;

&lt;p&gt;Sometimes you simply need intelligent features.&lt;/p&gt;

&lt;p&gt;Azure AI provides services for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OCR&lt;/li&gt;
&lt;li&gt;Translation&lt;/li&gt;
&lt;li&gt;Speech recognition&lt;/li&gt;
&lt;li&gt;Sentiment analysis&lt;/li&gt;
&lt;li&gt;Document extraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.AI.TextAnalytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TextAnalyticsClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzureKeyCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnalyzeSentiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"The service was excellent."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sentiment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No training required.&lt;/p&gt;

&lt;p&gt;Just consume the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Choose Between Them
&lt;/h2&gt;

&lt;p&gt;Over time, I started using a simple rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ML.NET&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When my application needs to learn from my own data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ONNX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a trained model already exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure AI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I simply need intelligent features quickly.&lt;/p&gt;

&lt;p&gt;You don't have to pick only one.&lt;/p&gt;

&lt;p&gt;Many applications combine all three.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest mindset shift is realizing that AI is no longer a separate world.&lt;/p&gt;

&lt;p&gt;It's becoming another layer of software development.&lt;/p&gt;

&lt;p&gt;Just like databases.&lt;/p&gt;

&lt;p&gt;Just like caching.&lt;/p&gt;

&lt;p&gt;Just like Docker.&lt;/p&gt;

&lt;p&gt;As .NET developers, we no longer have to leave our ecosystem to build intelligent applications.&lt;/p&gt;

&lt;p&gt;And honestly, that's probably the most exciting part.&lt;/p&gt;

&lt;p&gt;The barrier to entry has never been lower.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Make Flutter Web Apps Fully Responsive Across All Screen Sizes</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/how-to-make-flutter-web-apps-fully-responsive-across-all-screen-sizes-515p</link>
      <guid>https://dev.to/codexlancers/how-to-make-flutter-web-apps-fully-responsive-across-all-screen-sizes-515p</guid>
      <description>&lt;p&gt;Building for the web with Flutter is powerful - but making it truly responsive across mobile, tablet, and desktop is where most apps struggle.&lt;/p&gt;

&lt;p&gt;At our team, we've seen one consistent pattern:&lt;/p&gt;

&lt;p&gt;👉 Apps don't fail because of features - they fail because of layout breakdown on different screen sizes.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through how we approach &lt;strong&gt;fully responsive Flutter Web design&lt;/strong&gt;, using production-ready patterns that scale across devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Responsiveness Matters in Flutter Web
&lt;/h2&gt;

&lt;p&gt;Unlike mobile apps, Flutter Web runs on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small mobile screens (320px–480px)&lt;/li&gt;
&lt;li&gt;Tablets (600px–1024px)&lt;/li&gt;
&lt;li&gt;Laptops (1024px–1440px)&lt;/li&gt;
&lt;li&gt;Large desktops (1440px+)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your UI is not adaptive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buttons overflow&lt;/li&gt;
&lt;li&gt;Text becomes unreadable&lt;/li&gt;
&lt;li&gt;Layout breaks on wide screens&lt;/li&gt;
&lt;li&gt;Users drop off instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of designing for one screen, we design for &lt;strong&gt;breakpoints + flexible layouts&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define Clear Breakpoints for All Screen Sizes
&lt;/h2&gt;

&lt;p&gt;The first step in any responsive system is defining consistent screen breakpoints. Without this, layouts become unpredictable and hard to maintain.&lt;/p&gt;

&lt;p&gt;We usually divide screens into three categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mobile&lt;/li&gt;
&lt;li&gt;Tablet&lt;/li&gt;
&lt;li&gt;Desktop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us a clean structure to base all UI decisions on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScreenSizes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;mobile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tablet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;desktop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once breakpoints are defined, they act as the foundation of the entire responsive system.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build a Central Responsive Helper
&lt;/h2&gt;

&lt;p&gt;Instead of writing MediaQuery conditions everywhere, we centralize all logic into a single helper class.&lt;/p&gt;

&lt;p&gt;This keeps the code clean, reusable, and scalable across large projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Responsive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ScreenSizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isTablet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ScreenSizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tablet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScreenSizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDesktop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScreenSizes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tablet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach ensures consistency across the entire app.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Avoid Fixed Layouts - Use Flexible Widgets
&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes in Flutter Web is using fixed widths and heights.&lt;/p&gt;

&lt;p&gt;This breaks UI instantly on different screen sizes.&lt;/p&gt;

&lt;p&gt;Instead, we rely on flexible widgets like Expanded and Flexible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;flex:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;flex:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;green&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures layouts naturally adjust to available space.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use LayoutBuilder for Real-Time Responsiveness
&lt;/h2&gt;

&lt;p&gt;Instead of guessing device size, we let Flutter tell us the actual constraints using LayoutBuilder.&lt;/p&gt;

&lt;p&gt;This is one of the most reliable ways to build adaptive UIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;LayoutBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MobileView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TabletView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DesktopView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern allows us to fully control UI per screen type.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Design Adaptive Navigation Systems
&lt;/h2&gt;

&lt;p&gt;Navigation is one of the most overlooked parts of responsive design.&lt;/p&gt;

&lt;p&gt;A single navigation style does not work across all screen sizes.&lt;/p&gt;

&lt;p&gt;We follow this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mobile → Bottom Navigation&lt;/li&gt;
&lt;li&gt;Tablet → Navigation Rail&lt;/li&gt;
&lt;li&gt;Desktop → Sidebar Navigation
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;buildNavigation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Responsive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BottomNavigationBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;items:&lt;/span&gt; &lt;span class="p"&gt;[...]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NavigationRail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;extended:&lt;/span&gt; &lt;span class="n"&gt;Responsive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDesktop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;destinations:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;NavigationRailDestination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Home'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures usability across all devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use GridView for Adaptive Layouts
&lt;/h2&gt;

&lt;p&gt;Instead of fixed columns, we use adaptive grids that automatically adjust based on screen width.&lt;/p&gt;

&lt;p&gt;This removes the need for manual layout tuning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;GridView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;gridDelegate:&lt;/span&gt; &lt;span class="n"&gt;SliverGridDelegateWithMaxCrossAxisExtent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;maxCrossAxisExtent:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;crossAxisSpacing:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;mainAxisSpacing:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Item &lt;/span&gt;&lt;span class="si"&gt;$index&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach ensures the UI always looks balanced.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Implement Responsive Typography
&lt;/h2&gt;

&lt;p&gt;Typography often gets ignored, but it plays a huge role in readability across devices.&lt;/p&gt;

&lt;p&gt;We scale font sizes based on screen width.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;getFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply it consistently across the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"Responsive Design"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fontSize:&lt;/span&gt; &lt;span class="n"&gt;getFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Avoid Fixed Width Containers
&lt;/h2&gt;

&lt;p&gt;Fixed-width containers break layouts on smaller screens and waste space on larger ones.&lt;/p&gt;

&lt;p&gt;Instead, we use fluid layouts with constraints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;constraints:&lt;/span&gt; &lt;span class="n"&gt;BoxConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;maxWidth:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mobile → full width usage&lt;/li&gt;
&lt;li&gt;Desktop → centered readable layout&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Structure Code for Scalability
&lt;/h2&gt;

&lt;p&gt;Responsiveness is not just UI - it's architecture.&lt;/p&gt;

&lt;p&gt;We always structure our projects in a way that scales:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/responsive
  breakpoints.dart
  screen_helper.dart

/screens
  mobile/
  tablet/
  desktop/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we use a simple switch system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Responsive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isMobile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;MobileHome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Responsive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isTablet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;TabletHome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DesktopHome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps code clean, maintainable, and production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes in Flutter Web Responsiveness
&lt;/h2&gt;

&lt;p&gt;Here are some mistakes we often see while building Flutter Web apps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Overusing MediaQuery
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;MediaQuery&lt;/code&gt; everywhere makes code messy and hard to manage. It's better to centralize responsiveness logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Designing Only for Mobile
&lt;/h3&gt;

&lt;p&gt;Many apps are built for mobile first and later stretched for web, which breaks layouts on larger screens.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Using Fixed Widths
&lt;/h3&gt;

&lt;p&gt;Hardcoded sizes don't adapt well. Always prefer flexible layouts over fixed dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ignoring Desktop UI
&lt;/h3&gt;

&lt;p&gt;A lot of apps simply scale up mobile screens instead of designing proper desktop layouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Poor Typography Scaling
&lt;/h3&gt;

&lt;p&gt;Same font size across all devices hurts readability. Text should adapt to screen size.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Not Testing Real Screens
&lt;/h3&gt;

&lt;p&gt;Only testing in emulator is not enough - real devices often reveal layout issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Responsive design in Flutter Web is not about adding a few conditions - it's about building a &lt;strong&gt;system that scales by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At our team, the goal is simple:&lt;/p&gt;

&lt;p&gt;"If a UI breaks on any screen, the architecture is wrong - not the screen."&lt;/p&gt;

&lt;p&gt;If you follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Breakpoints&lt;/li&gt;
&lt;li&gt;Adaptive layouts&lt;/li&gt;
&lt;li&gt;Flexible widgets&lt;/li&gt;
&lt;li&gt;Grid-based scaling&lt;/li&gt;
&lt;li&gt;Device-aware navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you'll already be ahead of 80% of Flutter web apps today.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>webdev</category>
      <category>responsive</category>
    </item>
    <item>
      <title>Flutter + WebRTC + Firebase: Build a Real-Time Video App</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Tue, 07 Jul 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/flutter-webrtc-firebase-build-a-real-time-video-app-34e0</link>
      <guid>https://dev.to/codexlancers/flutter-webrtc-firebase-build-a-real-time-video-app-34e0</guid>
      <description>&lt;p&gt;&lt;em&gt;How we built a cross-platform video calling app with camera, mic, screen sharing, live chat, polls, and emoji reactions using WebRTC, Flutter, and Firebase.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why WebRTC?&lt;/strong&gt;&lt;br&gt;
WebRTC (Web Real-Time Communication) is an open-source technology that enables browsers and mobile applications to exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audio&lt;/li&gt;
&lt;li&gt;Video&lt;/li&gt;
&lt;li&gt;Arbitrary data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;directly between users.&lt;br&gt;
The biggest advantage?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peer-to-Peer Communication&lt;/strong&gt;&lt;br&gt;
Instead of routing every video frame through your servers, WebRTC allows devices to communicate directly whenever possible.&lt;br&gt;
That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower latency&lt;/li&gt;
&lt;li&gt;Better privacy&lt;/li&gt;
&lt;li&gt;Reduced infrastructure costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it raises an important question:&lt;br&gt;
How do two devices on different networks actually find each other?&lt;br&gt;
That question is where the real WebRTC journey begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding WebRTC: The Phone Call&amp;nbsp;Analogy&lt;br&gt;
WebRTC has two completely separate parts:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;1. Signaling&lt;/strong&gt;&lt;br&gt;
Helps peers discover each other and exchange connection information. WebRTC doesn't provide this. You build it yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Media/Data Transport&lt;/strong&gt;&lt;br&gt;
Once peers know how to connect, WebRTC handles the actual communication.&lt;br&gt;
Think of it like making a phone call.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📖 Phone book → Signaling&lt;/li&gt;
&lt;li&gt;☎️ Actual conversation → Peer Connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The phone book doesn't hear the conversation. It only helps establish it. The Firebase Firestore acts as the phone book.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The WebRTC Handshake&lt;/strong&gt;&lt;br&gt;
Whenever someone joins a meeting, this sequence occurs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fnorkgxtkgrsikm0f26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fnorkgxtkgrsikm0f26.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking Down the Terminology&lt;br&gt;
SDP (Session Description Protocol)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A text description of what each peer supports.&lt;br&gt;
It contains information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supported codecs&lt;/li&gt;
&lt;li&gt;Video resolutions&lt;/li&gt;
&lt;li&gt;Audio capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The caller creates an Offer. The receiver responds with an Answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ICE Candidates&lt;/strong&gt;&lt;br&gt;
Possible network routes that peers can use to communicate.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local IP addresses&lt;/li&gt;
&lt;li&gt;Public IP addresses discovered through STUN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebRTC automatically selects the best working path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STUN Servers&lt;/strong&gt;&lt;br&gt;
STUN servers answer one simple question:&lt;br&gt;
"How does the internet see me?" They help devices discover their public IP addresses. Google provides free STUN servers that are perfect for learning and prototypes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the&amp;nbsp;app&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Capturing Camera and Microphone&lt;/strong&gt;&lt;br&gt;
Everything begins with obtaining media access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_cameraStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;facingMode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ideal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ideal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. Until real users show up.&lt;br&gt;
What if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Camera permission is denied?&lt;/li&gt;
&lt;li&gt;No webcam exists?&lt;/li&gt;
&lt;li&gt;Another app is already using the camera?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, you need fallbacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Creating the Peer Connection&lt;/strong&gt;&lt;br&gt;
The heart of WebRTC is: RTCPeerConnection&amp;nbsp;&lt;br&gt;
This object represents the live connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_peerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPeerConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iceServers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stun:stun.l.google.com:19302&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stun:stun1.l.google.com:19302&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;STUN vs&amp;nbsp;TURN&lt;/strong&gt;&lt;br&gt;
One important production consideration:&lt;br&gt;
&lt;strong&gt;STUN&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct peer-to-peer&lt;/li&gt;
&lt;li&gt;Free&lt;/li&gt;
&lt;li&gt;Low latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TURN&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relays media through a server&lt;/li&gt;
&lt;li&gt;Works behind restrictive NATs&lt;/li&gt;
&lt;li&gt;Costs bandwidth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;STUN is enough for demos.&lt;br&gt;
TURN is required for production reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Signaling with Firebase Firestore&lt;/strong&gt;&lt;br&gt;
This is the part tutorials often skip. WebRTC doesn't tell you how to exchange Offers and Answers.&lt;br&gt;
Many implementations use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Socket.IO&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;We choose: Firebase Firestore&lt;/strong&gt;&lt;br&gt;
Because Firestore provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Realtime listeners&lt;/li&gt;
&lt;li&gt;Serverless infrastructure&lt;/li&gt;
&lt;li&gt;Minimal backend maintenanceon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: The ICE Candidate Race Condition&lt;/strong&gt;&lt;br&gt;
Every WebRTC developer eventually encounters this bug. ICE candidates often arrive before the remote description has been set.&lt;/p&gt;

&lt;p&gt;If you process them immediately:  WebRTC throws errors.&lt;br&gt;
The solution: Buffer them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;_remoteDescriptionSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;_pendingCandidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the remote description is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;_flushPendingCandidates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Data&amp;nbsp;Channels&lt;/strong&gt;&lt;br&gt;
Most people think WebRTC only handles video. It doesn't.&lt;/p&gt;

&lt;p&gt;It also supports:&lt;br&gt;
RTCDataChannel&lt;br&gt;
A peer-to-peer data pipeline.&lt;br&gt;
we used it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💬 Chat&lt;/li&gt;
&lt;li&gt;😄 Emoji reactions&lt;/li&gt;
&lt;li&gt;📊 Live polls&lt;/li&gt;
&lt;li&gt;🔇 Mic status&lt;/li&gt;
&lt;li&gt;📷 Camera status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sendReaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;'chat'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
 &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;'reaction'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
 &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;'poll'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chat messages never touch a server. They're encrypted and delivered directly between peers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Mute, Camera, and Screen&amp;nbsp;Sharing&lt;/strong&gt;&lt;br&gt;
Real meetings aren't static.&lt;br&gt;
Users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mute microphones&lt;/li&gt;
&lt;li&gt;Disable cameras&lt;/li&gt;
&lt;li&gt;Share screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Muting is surprisingly simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Screen sharing is even cooler. Instead of rebuilding the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;replaceTrack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenTrack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remote user seamlessly sees your screen. No renegotiation required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Reality of Production WebRTC&lt;/strong&gt;&lt;br&gt;
This is where the real work begins. A demo can be built quickly. A product is much harder.&lt;/p&gt;

&lt;p&gt;Here are some of the problems we had to solve while building a production-ready implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peer-Left Detection&lt;/strong&gt;&lt;br&gt;
Users don't always leave gracefully. They close browser tabs, force-stop the app, or lose connectivity.&lt;/p&gt;

&lt;p&gt;We relied on Firestore room state to detect this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;roomSubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;roomRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;snapshots&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'calleeJoined'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;leaveCall&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The other participant left the meeting."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without proper cleanup, cameras remain active and rooms become unusable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconnection Logic&lt;/strong&gt;&lt;br&gt;
Mobile networks are unreliable. Users switch between Wi-Fi and cellular networks all the time.&lt;/p&gt;

&lt;p&gt;Instead of assuming a stable connection, we watched the ICE connection state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;_peerConnection&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;onIceConnectionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;RTCIceConnectionState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RTCIceConnectionStateDisconnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;attemptReconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;RTCIceConnectionState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RTCIceConnectionStateFailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;restartIce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed connection shouldn't automatically end the call. Sometimes it just needs another chance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle Management&lt;/strong&gt;&lt;br&gt;
Users minimize apps. Browsers close. Operating systems kill background processes.&lt;/p&gt;

&lt;p&gt;We used Flutter lifecycle callbacks to release resources safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;didChangeAppLifecycleState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppLifecycleState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AppLifecycleState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;leaveCall&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ignoring lifecycle events often leads to stuck cameras and orphaned sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Renderer Management&lt;/strong&gt;&lt;br&gt;
RTCVideoRenderer instances are expensive. Creating and disposing them repeatedly can cause black screens.&lt;/p&gt;

&lt;p&gt;Instead, initialize them once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;localRenderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteRenderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Reuse throughout the call&lt;/span&gt;
&lt;span class="n"&gt;localRenderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;remoteRenderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remoteStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dispose them only after the meeting ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camera Hardware&amp;nbsp;Timing&lt;/strong&gt;&lt;br&gt;
Stopping camera tracks immediately doesn't always release hardware properly.&lt;/p&gt;

&lt;p&gt;We introduced a small delay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;localStream&lt;/span&gt;
    &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;getVideoTracks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, some devices kept the camera indicator active even after leaving the call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ICE Candidate Race Conditions&lt;/strong&gt;&lt;br&gt;
ICE candidates can arrive before the remote description is set. Processing them too early causes exceptions.&lt;/p&gt;

&lt;p&gt;Buffer first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_remoteDescriptionSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;pendingCandidates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addIceCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Flush&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt; &lt;span class="nl"&gt;later:&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pendingCandidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;addIceCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;pendingCandidates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tiny piece of logic eliminated one of the most common WebRTC bugs. A WebRTC demo is 20% of the work. The remaining 80% is handling edge cases.&lt;br&gt;
&lt;strong&gt;WebRTC User Limits: A Quick&amp;nbsp;Guide&lt;/strong&gt;&lt;br&gt;
"How many users can WebRTC support?" It depends on your architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1-on-1 Calls (P2P) - Perfect for two users: low latency, minimal infrastructure.&lt;/li&gt;
&lt;li&gt;Small Groups (Mesh) - Each participant connects to all others. Works for 3–6 users; connections grow rapidly beyond that.&lt;/li&gt;
&lt;li&gt;Large Meetings (SFU) - A server forwards each user's stream to others. Supports dozens or hundreds of participants with lower CPU and bandwidth usage, but requires server infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When to use what:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1–2 users → Pure P2P&lt;/li&gt;
&lt;li&gt;3–6 users → Mesh (use with caution)&lt;/li&gt;
&lt;li&gt;6+ users → SFU&lt;/li&gt;
&lt;li&gt;Large webinars → SFU with recording and simulcast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Architecture Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb163plhv3uao9u1w9neu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb163plhv3uao9u1w9neu.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Firebase only handles signaling.Media flows directly between peers.&lt;br&gt;
That separation is the essence of WebRTC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should You Use&amp;nbsp;WebRTC?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Yes, if you're building:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Video conferencing&lt;/li&gt;
&lt;li&gt;Audio calling&lt;/li&gt;
&lt;li&gt;Collaborative applications&lt;/li&gt;
&lt;li&gt;Peer-to-peer file sharing&lt;/li&gt;
&lt;li&gt;Realtime gaming experiences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Plan ahead&amp;nbsp;for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TURN infrastructure&lt;/li&gt;
&lt;li&gt;Reconnection strategies&lt;/li&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;Group-call scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you move beyond one-to-one calls, you'll eventually need an SFU.&lt;br&gt;
But for MVPs and real-world one-to-one applications:&lt;br&gt;
Flutter + WebRTC + Firebase is an incredibly powerful combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;WebRTC handles media, not signaling.&lt;/li&gt;
&lt;li&gt;Firebase Firestore makes an excellent serverless signaling layer.&lt;/li&gt;
&lt;li&gt;Offer → Answer → ICE is the universal handshake.&lt;/li&gt;
&lt;li&gt;Data channels are massively underrated.&lt;/li&gt;
&lt;li&gt;Edge cases are where production engineering happens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;we'd love to hear your experiences building with WebRTC. Drop your questions in the comments.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Running Local AI Models in .NET with Ollama (Step-by-Step Guide)</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Fri, 03 Jul 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/building-ai-powered-net-applications-without-learning-python-2m3o</link>
      <guid>https://dev.to/codexlancers/building-ai-powered-net-applications-without-learning-python-2m3o</guid>
      <description>&lt;p&gt;&lt;em&gt;Build AI-powered ASP.NET Core applications without relying entirely on cloud providers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you've been experimenting with AI recently, your journey probably looked something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up for an AI provider&lt;/li&gt;
&lt;li&gt;Generate an API key&lt;/li&gt;
&lt;li&gt;Send a prompt&lt;/li&gt;
&lt;li&gt;Get a response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's simple, fast, and honestly, it's amazing how quickly you can build something useful.&lt;/p&gt;

&lt;p&gt;But once AI moves beyond a proof of concept, a different set of questions starts to appear.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much will this cost at scale?&lt;/li&gt;
&lt;li&gt;Do we really want sensitive data leaving our infrastructure?&lt;/li&gt;
&lt;li&gt;What happens if we hit API limits?&lt;/li&gt;
&lt;li&gt;Do we want our internal tools to depend entirely on an external service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly where local AI becomes interesting.&lt;/p&gt;

&lt;p&gt;Recently, I started experimenting with Ollama and was surprised by how easy it has become to run AI models locally and integrate them into a regular ASP.NET Core application.&lt;/p&gt;

&lt;p&gt;The best part?&lt;/p&gt;

&lt;p&gt;From a .NET developer's perspective, it mostly feels like integrating another HTTP service.&lt;/p&gt;

&lt;p&gt;In this guide, we'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Ollama&lt;/li&gt;
&lt;li&gt;Download and run a local model&lt;/li&gt;
&lt;li&gt;Integrate it into ASP.NET Core&lt;/li&gt;
&lt;li&gt;Build a simple AI-powered endpoint&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Run AI Models Locally?
&lt;/h2&gt;

&lt;p&gt;Cloud AI is fantastic.&lt;/p&gt;

&lt;p&gt;I'm not here to replace it.&lt;/p&gt;

&lt;p&gt;But local AI solves a different set of problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Privacy
&lt;/h3&gt;

&lt;p&gt;Many business applications work with sensitive information.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal documentation&lt;/li&gt;
&lt;li&gt;Support tickets&lt;/li&gt;
&lt;li&gt;Customer records&lt;/li&gt;
&lt;li&gt;Application logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sending that information to an external AI provider may introduce security, compliance, or governance concerns.&lt;/p&gt;

&lt;p&gt;Running models locally keeps everything inside your own infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Predictable Costs
&lt;/h3&gt;

&lt;p&gt;Cloud providers charge based on usage.&lt;/p&gt;

&lt;p&gt;That works well initially, but costs can grow quickly as applications scale.&lt;/p&gt;

&lt;p&gt;Local AI removes the per-request pricing model entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fewer Dependencies
&lt;/h3&gt;

&lt;p&gt;Your application no longer depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internet connectivity&lt;/li&gt;
&lt;li&gt;External outages&lt;/li&gt;
&lt;li&gt;API rate limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs on your own machine or server.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Ollama?
&lt;/h2&gt;

&lt;p&gt;Ollama is a tool that allows developers to run Large Language Models (LLMs) locally.&lt;/p&gt;

&lt;p&gt;Instead of manually configuring machine learning environments, Ollama handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model downloads&lt;/li&gt;
&lt;li&gt;Runtime management&lt;/li&gt;
&lt;li&gt;Memory handling&lt;/li&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once installed, interacting with AI becomes as simple as making an HTTP request.&lt;/p&gt;

&lt;p&gt;Popular models include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Best Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;llama3&lt;/td&gt;
&lt;td&gt;General AI tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral&lt;/td&gt;
&lt;td&gt;Fast responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codellama&lt;/td&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma&lt;/td&gt;
&lt;td&gt;Lightweight applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1: Install Ollama
&lt;/h2&gt;

&lt;p&gt;Download Ollama from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://ollama.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download a model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama run llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it something simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain dependency injection in ASP.NET Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get a response, you're ready to go.&lt;/p&gt;

&lt;p&gt;By default, Ollama exposes a local API at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what our ASP.NET Core application will communicate with.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Create an ASP.NET Core API
&lt;/h2&gt;

&lt;p&gt;Create a new project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new webapi &lt;span class="nt"&gt;-n&lt;/span&gt; LocalAIApi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recommended structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Controllers
Services
Models
Program.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep your AI logic separate from controllers.&lt;/p&gt;

&lt;p&gt;It'll make your application easier to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Register HttpClient
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAiService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OllamaService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Create an AI Service
&lt;/h2&gt;

&lt;p&gt;Interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IAiService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OllamaService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAiService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OllamaService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llama3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"api/generate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OllamaResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OllamaResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, AI integration starts looking very similar to integrating any third-party API.&lt;/p&gt;

&lt;p&gt;That's probably the biggest surprise when working with Ollama for the first time.&lt;/p&gt;

&lt;p&gt;Most of the complexity is already handled for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Expose an AI Endpoint
&lt;/h2&gt;

&lt;p&gt;Request model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Prompt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/ai"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AiController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IAiService&lt;/span&gt; &lt;span class="n"&gt;_aiService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AiController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAiService&lt;/span&gt; &lt;span class="n"&gt;aiService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_aiService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"generate"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AiRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Prompt is required."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_aiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API can now expose AI functionality without relying on a cloud provider.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   ↓
ASP.NET Core API
   ↓
AI Service Layer
   ↓
Ollama HTTP API
   ↓
Local LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping AI behind a dedicated service layer makes it easier to swap providers later if needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is Local AI Always Better?
&lt;/h2&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Cloud AI is still the better choice in many scenarios.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Use cloud AI when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need state-of-the-art reasoning models&lt;/li&gt;
&lt;li&gt;You have thousands of concurrent users&lt;/li&gt;
&lt;li&gt;You need globally distributed infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use local AI when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Privacy matters&lt;/li&gt;
&lt;li&gt;Cost control matters&lt;/li&gt;
&lt;li&gt;You're building internal business tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In reality, many teams will likely use a hybrid approach.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A few years ago, running large language models felt like something only machine learning engineers could do.&lt;/p&gt;

&lt;p&gt;Today, tools like Ollama have changed that.&lt;/p&gt;

&lt;p&gt;As a .NET developer, integrating local AI is often just another HTTP integration.&lt;/p&gt;

&lt;p&gt;And that's what makes this so exciting.&lt;/p&gt;

&lt;p&gt;Local AI isn't replacing cloud AI.&lt;/p&gt;

&lt;p&gt;It's simply giving developers another architectural option.&lt;/p&gt;

&lt;p&gt;For internal tools, private assistants, documentation search, and AI-powered business applications, that option is becoming increasingly practical every day.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Flutter Firebase Remote Config: Control Your App Without Releasing Again</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Thu, 02 Jul 2026 08:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/flutter-firebase-remote-config-control-your-app-without-releasing-again-2kjk</link>
      <guid>https://dev.to/codexlancers/flutter-firebase-remote-config-control-your-app-without-releasing-again-2kjk</guid>
      <description>&lt;p&gt;There's a moment every developer hits. You ship an app and then immediately realize :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I should hide this feature"&lt;/li&gt;
&lt;li&gt;"This API needs to change"&lt;/li&gt;
&lt;li&gt;"We need a quick banner or message"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But now you're stuck waiting for users to update the app.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;Firebase Remote Config&lt;/strong&gt; quietly becomes one of the most powerful tools in your Flutter toolkit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;Most apps today aren't &lt;strong&gt;build once and forget&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They evolve constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Features roll out gradually&lt;/li&gt;
&lt;li&gt;Experiments need testing&lt;/li&gt;
&lt;li&gt;Bugs need quick mitigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a remote control system, every small change becomes:&lt;br&gt;
Build → Upload → Review → Wait → Hope users update&lt;br&gt;
That's slow and sometimes risky.&lt;/p&gt;

&lt;p&gt;Remote Config flips that completely.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 The Idea
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding values in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showNewUI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You move that control to the cloud.&lt;/p&gt;

&lt;p&gt;Now it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showNewUI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"show_new_ui"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And suddenly…&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👉 You can turn features ON/OFF from Firebase&lt;/li&gt;
&lt;li&gt;👉 You can change behavior without redeploying&lt;/li&gt;
&lt;li&gt;👉 You can react instantly to real-world issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Actually Helps in Real Apps
&lt;/h2&gt;

&lt;p&gt;Let's not talk theory - here's where it actually shines:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Emergency Fixes
&lt;/h3&gt;

&lt;p&gt;API breaking? Feature crashing?&lt;br&gt;
Just disable it remotely instead of pushing a hotfix build.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Feature Flags
&lt;/h3&gt;

&lt;p&gt;Roll out a feature to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10% users first&lt;/li&gt;
&lt;li&gt;Then 50%&lt;/li&gt;
&lt;li&gt;Then everyone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All without touching the code again.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Maintenance Mode
&lt;/h3&gt;

&lt;p&gt;Add a config like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;is_app_under_maintenance: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app can instantly switch to a maintenance screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A/B Testing
&lt;/h3&gt;

&lt;p&gt;Try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two different home screens&lt;/li&gt;
&lt;li&gt;Different button colours&lt;/li&gt;
&lt;li&gt;Different pricing UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure what works best.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter Integration
&lt;/h2&gt;

&lt;p&gt;The setup is simple - but how you structure it matters more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Initialize once
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseRemoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConfigSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;RemoteConfigSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;fetchTimeout:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;minimumFetchInterval:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hours:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Always define defaults
&lt;/h3&gt;

&lt;p&gt;This is your safety net.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDefaults&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="s"&gt;"show_new_ui"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"is_app_under_maintenance"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 If fetch fails and you don't define defaults → your app can break.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fetch &amp;amp; activate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchAndActivate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 This does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetches latest values from Firebase&lt;/li&gt;
&lt;li&gt;Activates them immediately in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Use it like normal data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"is_app_under_maintenance"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaintenanceScreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Cleaner Way
&lt;/h2&gt;

&lt;p&gt;If you directly access Remote Config everywhere… things get messy fast.&lt;/p&gt;

&lt;p&gt;A better approach is to centralize it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseRemoteConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;isMaintenance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;_remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"is_app_under_maintenance"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;showNewUI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;_remote&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"show_new_ui"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your UI stays clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isMaintenance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaintenanceScreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚠️ Things That Will Save You Later
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Don't treat it like real-time
&lt;/h3&gt;

&lt;p&gt;Remote Config is not instant.&lt;/p&gt;

&lt;p&gt;It works on fetch intervals - so don't use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live chat&lt;/li&gt;
&lt;li&gt;Real-time counters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Be careful with fetch frequency
&lt;/h3&gt;

&lt;p&gt;During development: &lt;code&gt;minimumFetchInterval: Duration.zero&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In production:&lt;br&gt;
&lt;code&gt;minimumFetchInterval: Duration(hours: 12)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Otherwise, Firebase will throttle your requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Never store sensitive data
&lt;/h3&gt;

&lt;p&gt;This is not for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;Secrets&lt;/li&gt;
&lt;li&gt;Tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything here can be exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Defaults are not optional
&lt;/h3&gt;

&lt;p&gt;If fetch fails and you didn't define defaults → your app can break.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 When You Should Actually Use It
&lt;/h2&gt;

&lt;p&gt;Use Remote Config when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want flexibility after release&lt;/li&gt;
&lt;li&gt;You run experiments&lt;/li&gt;
&lt;li&gt;You need quick toggles for features&lt;/li&gt;
&lt;li&gt;You want safer rollouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need real-time updates&lt;/li&gt;
&lt;li&gt;You're storing secure data&lt;/li&gt;
&lt;li&gt;You're passing large payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  💡 Final Take
&lt;/h2&gt;

&lt;p&gt;If you're already using Firebase and not using Remote Config You're honestly missing out on one of the easiest wins in Flutter development.&lt;/p&gt;

&lt;p&gt;Start small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add one flag&lt;/li&gt;
&lt;li&gt;Control one UI change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And once you see it working You won't want to go back.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
    </item>
    <item>
      <title>How to Implement Biometric Authentication in a Flutter App (The Right Way)</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Wed, 01 Jul 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/how-to-implement-biometric-authentication-in-a-flutter-app-the-right-way-lp0</link>
      <guid>https://dev.to/codexlancers/how-to-implement-biometric-authentication-in-a-flutter-app-the-right-way-lp0</guid>
      <description>&lt;p&gt;In today's world, security is no longer optional - it's expected. Whether it's a fintech app, a fitness tracker, or an internal company tool, users want fast and secure access without the hassle of remembering passwords.&lt;/p&gt;

&lt;p&gt;That's exactly where biometric authentication comes in.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through how we &lt;strong&gt;implement biometric authentication in a Flutter app&lt;/strong&gt;, the practical approach we follow in production, and the common mistakes developers often make (and how to avoid them).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Biometric Authentication?
&lt;/h2&gt;

&lt;p&gt;Before jumping into implementation, let's quickly understand why it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster login experience (no typing passwords)&lt;/li&gt;
&lt;li&gt;More secure than traditional authentication&lt;/li&gt;
&lt;li&gt;Native support across Android &amp;amp; iOS&lt;/li&gt;
&lt;li&gt;Better user trust and retention&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We Use in Flutter
&lt;/h2&gt;

&lt;p&gt;To implement biometric authentication, we rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local_auth&lt;/code&gt; package (official Flutter plugin)&lt;/li&gt;
&lt;li&gt;Native biometric APIs under the hood (Face ID, Touch ID, Fingerprint)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Add Dependency
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;local_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^3.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Platform Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Android Setup
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;android/app/src/main/AndroidManifest.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-permission&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.USE_BIOMETRIC"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also ensure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;uses-feature&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.hardware.fingerprint"&lt;/span&gt; &lt;span class="na"&gt;android:required=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ iOS Setup
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;ios/Runner/Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSFaceIDUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;We use Face ID to authenticate you securely&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Without this, Face ID will NOT work and your app may crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Implement Biometric Logic
&lt;/h2&gt;

&lt;p&gt;Here's how we structure it in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:local_auth/local_auth.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BiometricService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;LocalAuthentication&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;/// Check if device supports biometrics&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isBiometricAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;canCheckBiometrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canCheckBiometrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDeviceSupported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDeviceSupported&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;canCheckBiometrics&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isDeviceSupported&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Biometric availability error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Get available biometric types (fingerprint, face, etc.)&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BiometricType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getAvailableBiometrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAvailableBiometrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Error fetching biometrics: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Authenticate user (with optional fallback to device PIN/password)&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;biometricOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isAvailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;isBiometricAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Biometric not available on this device'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isAuthenticated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;localizedReason:&lt;/span&gt; &lt;span class="s"&gt;'Please authenticate to continue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;options:&lt;/span&gt; &lt;span class="n"&gt;AuthenticationOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;biometricOnly:&lt;/span&gt; &lt;span class="n"&gt;biometricOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;stickyAuth:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;useErrorDialogs:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Authentication error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Use It in UI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;BiometricService&lt;/span&gt; &lt;span class="n"&gt;biometricService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BiometricService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loginWithBiometrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isAuthenticated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;biometricService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ScaffoldMessenger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showSnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;content:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Authentication successful'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ScaffoldMessenger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;showSnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SnackBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;content:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Authentication failed. Please try again'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Works for Face ID &amp;amp; Fingerprint (No Extra Code Needed)
&lt;/h2&gt;

&lt;p&gt;One thing we really like about this approach is that we don't have to write separate logic for different biometric types.&lt;/p&gt;

&lt;p&gt;The same implementation works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fingerprint (Android)&lt;/li&gt;
&lt;li&gt;Face ID (iOS)&lt;/li&gt;
&lt;li&gt;Touch ID (older iOS devices)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;local_auth&lt;/code&gt; package automatically detects and uses the available biometric method on the device.&lt;/p&gt;

&lt;p&gt;So whether the user is using Face ID or fingerprint - the flow remains exactly the same from our side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Actually Add in Production Apps
&lt;/h2&gt;

&lt;p&gt;In real apps, we don't stop at just authentication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Store a flag (biometric enabled/disabled) using secure storage&lt;/li&gt;
&lt;li&gt;✅ Provide fallback (PIN or password)&lt;/li&gt;
&lt;li&gt;✅ Show a toggle in settings&lt;/li&gt;
&lt;li&gt;✅ Auto-trigger biometrics on app launch (if enabled)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistakes We've Seen While Implementing This
&lt;/h2&gt;

&lt;p&gt;This is where most implementations go wrong 👇&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Not Handling Unsupported Devices
&lt;/h3&gt;

&lt;p&gt;Many developers assume biometrics will always be available.&lt;/p&gt;

&lt;p&gt;👉 Always check:&lt;/p&gt;

&lt;p&gt;✅ canCheckBiometrics&lt;br&gt;
✅ isDeviceSupported&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Ignoring iOS Permission Setup
&lt;/h3&gt;

&lt;p&gt;Missing NSFaceIDUsageDescription is one of the most common issues.&lt;/p&gt;

&lt;p&gt;👉 Result: App crash or silent failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Not Providing a Fallback Option
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Biometrics can fail due to:&lt;/li&gt;
&lt;li&gt;Wet fingers&lt;/li&gt;
&lt;li&gt;Face not recognized&lt;/li&gt;
&lt;li&gt;Sensor issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Always provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PIN and Password fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Forcing Biometric Without User Consent
&lt;/h3&gt;

&lt;p&gt;Never enable biometrics by default.&lt;/p&gt;

&lt;p&gt;👉 Always:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask user permission&lt;/li&gt;
&lt;li&gt;Let them enable/disable in settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Poor Error Handling
&lt;/h3&gt;

&lt;p&gt;Most developers just return false on failure.&lt;/p&gt;

&lt;p&gt;👉 Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle exceptions properly&lt;/li&gt;
&lt;li&gt;Show meaningful messages to users&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Not Testing Edge Cases
&lt;/h3&gt;

&lt;p&gt;Biometric flows behave differently when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App goes to background&lt;/li&gt;
&lt;li&gt;Device is locked/unlocked&lt;/li&gt;
&lt;li&gt;Multiple failed attempts occur&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Always test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background/foreground transitions&lt;/li&gt;
&lt;li&gt;Lock screen scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Using Biometrics as the Only Security Layer
&lt;/h3&gt;

&lt;p&gt;Biometric authentication should &lt;strong&gt;not replace backend authentication&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 Always combine with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure tokens&lt;/li&gt;
&lt;li&gt;Backend validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things That Helped Us Get This Right
&lt;/h2&gt;

&lt;p&gt;From our experience, this is what works best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep biometric logic separate (service class)&lt;/li&gt;
&lt;li&gt;Always provide fallback authentication&lt;/li&gt;
&lt;li&gt;Respect user choice and privacy&lt;/li&gt;
&lt;li&gt;Test on real devices (not just emulators)&lt;/li&gt;
&lt;li&gt;Keep UX smooth and non-intrusive&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Biometric authentication is powerful - but only when implemented thoughtfully.&lt;/p&gt;

&lt;p&gt;It's not just about adding a fingerprint or Face ID button. It's about creating a &lt;strong&gt;secure, seamless, and reliable user experience&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When done right, it can significantly improve both &lt;strong&gt;security and user satisfaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're building a Flutter app and want to level up your authentication flow, this is one feature you shouldn't skip.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>biometric</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Implementing Google Authentication in Flutter Without Firebase: Using the Latest google_sign_in (7.2.0)</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Tue, 30 Jun 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/implementing-google-authentication-in-flutter-without-firebase-using-the-latest-googlesignin-1gea</link>
      <guid>https://dev.to/codexlancers/implementing-google-authentication-in-flutter-without-firebase-using-the-latest-googlesignin-1gea</guid>
      <description>&lt;p&gt;There's a point in almost every Flutter project where authentication comes into the picture.&lt;/p&gt;

&lt;p&gt;And more often than not, the default answer is:&lt;/p&gt;

&lt;p&gt;"Just use Firebase."&lt;/p&gt;

&lt;p&gt;We've done that too. It works well.&lt;/p&gt;

&lt;p&gt;But in one of our recent projects, we already had a backend handling authentication. We didn't need Firebase - we just needed &lt;strong&gt;Google Sign-In as a login option&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we went with a direct approach using Google OAuth.&lt;/p&gt;

&lt;p&gt;What we didn't expect?&lt;/p&gt;

&lt;p&gt;Most tutorials online were outdated.&lt;/p&gt;

&lt;p&gt;If you're using the latest &lt;code&gt;google_sign_in&lt;/code&gt; (7.2.0), things have changed quite a bit.&lt;/p&gt;

&lt;p&gt;This guide is based on what actually worked for us in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Didn't Use Firebase
&lt;/h2&gt;

&lt;p&gt;In our case, Firebase would have added unnecessary complexity.&lt;/p&gt;

&lt;p&gt;We already had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend handling users&lt;/li&gt;
&lt;li&gt;Our own JWT/session system&lt;/li&gt;
&lt;li&gt;APIs ready for auth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All we needed was:&lt;br&gt;
👉 A way to let users log in with Google&lt;br&gt;
👉 A secure way to verify them on our backend&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the Flow Looks Like (High-Level)
&lt;/h2&gt;

&lt;p&gt;The flow is still simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User taps "Continue with Google"&lt;/li&gt;
&lt;li&gt;Google account picker opens&lt;/li&gt;
&lt;li&gt;User selects an account&lt;/li&gt;
&lt;li&gt;We receive an ID Token&lt;/li&gt;
&lt;li&gt;We send that token to our backend&lt;/li&gt;
&lt;li&gt;Backend verifies it with Google&lt;/li&gt;
&lt;li&gt;User is logged in / created&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing Firebase-specific here.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Google Cloud Setup (Don't Skip This)
&lt;/h2&gt;

&lt;p&gt;Before writing code, make sure this part is correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a project in Google Cloud Console&lt;/li&gt;
&lt;li&gt;Configure OAuth Consent Screen&lt;/li&gt;
&lt;li&gt;Create OAuth Client IDs:&lt;/li&gt;
&lt;li&gt;Android&lt;/li&gt;
&lt;li&gt;iOS&lt;/li&gt;
&lt;li&gt;Web (important for backend verification)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Things we double-check every time:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correct package name / bundle ID&lt;/li&gt;
&lt;li&gt;SHA-1 fingerprint added (Android)&lt;/li&gt;
&lt;li&gt;Using the correct client ID in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anything is wrong here, your code will look fine… but sign-in will fail silently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Add Dependency
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;google_sign_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^7.2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 3: Implementation
&lt;/h2&gt;

&lt;p&gt;New Working Version (7.2.0)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:google_sign_in/google_sign_in.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GoogleSignIn&lt;/span&gt; &lt;span class="n"&gt;_googleSignIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GoogleSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;signInWithGoogle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Step 1: Initialize (Required in v7+)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_googleSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;serverClientId:&lt;/span&gt; &lt;span class="s"&gt;'YOUR_WEB_CLIENT_ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Get this from Google Cloud Console&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 2: Trigger Sign-In&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GoogleSignInAccount&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_googleSignIn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 3: Fetch Tokens&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GoogleSignInAuthentication&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authentication&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 4: Validate Token&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ID Token is null"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 5: Send token to backend&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ID Token: &lt;/span&gt;&lt;span class="si"&gt;$idToken&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access Token: &lt;/span&gt;&lt;span class="si"&gt;$accessToken&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Google Sign-In Error: &lt;/span&gt;&lt;span class="si"&gt;$error&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: What We Send to Backend
&lt;/h2&gt;

&lt;p&gt;We only send: idToken&lt;br&gt;
Not accessToken. Not profile data.&lt;br&gt;
Just the ID token.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Backend Verification (Where Security Actually Happens)
&lt;/h2&gt;

&lt;p&gt;This step is not optional.&lt;/p&gt;

&lt;p&gt;Here's a simple Node.js example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OAuth2Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google-auth-library&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyGoogleToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyIdToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token is issued by Google&lt;/li&gt;
&lt;li&gt;Token is not tampered&lt;/li&gt;
&lt;li&gt;User identity is valid&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistakes We've Seen
&lt;/h2&gt;

&lt;p&gt;This is the part that saves hours of debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Forgetting initialize()
&lt;/h3&gt;

&lt;p&gt;In v7+, if you skip this:&lt;br&gt;
👉 Authentication will fail&lt;br&gt;
👉 Or behave inconsistently&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Using Wrong Client ID
&lt;/h3&gt;

&lt;p&gt;You must use:&lt;br&gt;
👉 Web Client ID in serverClientId&lt;br&gt;
Not Android / iOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Skipping Backend Verification
&lt;/h3&gt;

&lt;p&gt;This is a security flaw.&lt;br&gt;
Anyone can fake a token if you trust it directly from the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Not Handling Errors Properly
&lt;/h3&gt;

&lt;p&gt;Don't just print(error).&lt;/p&gt;

&lt;p&gt;At least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show a snackbar&lt;/li&gt;
&lt;li&gt;Handle cancellation gracefully&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Expecting Old Code to Work
&lt;/h3&gt;

&lt;p&gt;A lot of developers copy older snippets and assume something is wrong with Flutter.&lt;/p&gt;

&lt;p&gt;It's not.&lt;br&gt;
The API changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Confusing ID Token vs Access Token
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;idToken → authentication (you need this)&lt;/li&gt;
&lt;li&gt;accessToken → Google APIs (optional)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We Added in Production
&lt;/h2&gt;

&lt;p&gt;The basic flow works, but in real apps we also add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silent login (check if user already signed in)&lt;/li&gt;
&lt;li&gt;Proper loading states&lt;/li&gt;
&lt;li&gt;Retry handling for network issues&lt;/li&gt;
&lt;li&gt;Backend session management (JWT)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When This Approach Makes Sense
&lt;/h2&gt;

&lt;p&gt;We use this method when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We already have a backend&lt;/li&gt;
&lt;li&gt;We want full control over authentication&lt;/li&gt;
&lt;li&gt;Firebase would be overkill&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Google Sign-In without Firebase is not a workaround - it's actually the more direct and flexible approach.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;google_sign_in 7.2.0&lt;/code&gt;, things have clearly evolved.&lt;/p&gt;

&lt;p&gt;Once you understand the new flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialize&lt;/li&gt;
&lt;li&gt;Authenticate&lt;/li&gt;
&lt;li&gt;Get token&lt;/li&gt;
&lt;li&gt;Verify on backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;it becomes straightforward again.&lt;/p&gt;

&lt;p&gt;If these are right, your implementation is solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Last Thing
&lt;/h2&gt;

&lt;p&gt;Most tutorials haven't caught up with the 7.2.0 changes yet.&lt;/p&gt;

&lt;p&gt;So if you've been stuck trying to make old code work - it's not you.&lt;/p&gt;

&lt;p&gt;It's the docs lagging behind real-world usage.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>googleauth</category>
    </item>
    <item>
      <title>The Complete Guide to Firebase Security Rules: Why They Matter and how to Write Them</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/the-complete-guide-to-firebase-security-rules-why-they-matter-and-how-to-write-them-2cbf</link>
      <guid>https://dev.to/codexlancers/the-complete-guide-to-firebase-security-rules-why-they-matter-and-how-to-write-them-2cbf</guid>
      <description>&lt;p&gt;When building modern web and mobile applications, backend security is often the most stressful piece of the puzzle. Traditional architectures require you to spin up a custom server, write middleware, and handle authentication routing just to protect a single database row.&lt;/p&gt;

&lt;p&gt;Firebase flips this model on its head. By allowing client applications to talk &lt;em&gt;directly&lt;/em&gt; to backend services like Firestore and Cloud Storage, it drastically speeds up development. But this architectural shift introduces a critical question: &lt;strong&gt;If the client can touch the database directly, what stops a user from modifying someone else's data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;Firebase Security Rules&lt;/strong&gt;. Let's break down what they are, why they are non-negotiable for production apps, and exactly how to implement them across different Firebase services.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Firebase Security, and Why Is It&amp;nbsp;Crucial?
&lt;/h2&gt;

&lt;p&gt;Firebase Security Rules are server-side access control configurations that sit directly between your client app and your cloud data. They act as an invisible, structural shield. Every single incoming read or write request is intercepted by Firebase and evaluated against these rules. If the request doesn't meet your criteria, Firebase rejects it entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Can't Ignore&amp;nbsp;Them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Client is Insecure&lt;/strong&gt;: No matter how secure your Flutter, React, or native mobile code feels, an advanced user can decompile your app package, extract your Firebase configuration keys, and make direct calls to your endpoints via tools like Postman or custom scripts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Integrity&lt;/strong&gt;: Security rules do more than just check user identity - they validate &lt;em&gt;schemas&lt;/em&gt;. They ensure strings aren't passed into number fields, field lengths don't exceed limits, and mandatory fields aren't missing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Billing Protection&lt;/strong&gt;: Leaving your database wide open allows malicious actors or rogue bots to scrape your data continuously, executing millions of reads that can instantly maximize your billing tier.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which Firebase Services Use Security&amp;nbsp;Rules?
&lt;/h2&gt;

&lt;p&gt;Firebase applies individual security rule structures across three core backend services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Firestore&lt;/strong&gt;: Flexible, scalable NoSQL cloud database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime Database&lt;/strong&gt;: Original, low-latency document-store database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Storage&lt;/strong&gt;: Built for storing and serving user-generated content like images, audio, and videos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at two practical, real-world examples for each service to see how these rules look in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cloud Firestore Examples
&lt;/h2&gt;

&lt;p&gt;Firestore rules leverage granular path matching and custom conditions to control access down to specific document fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example A: User-Owned Data Only (CRUD Isolation)
&lt;/h3&gt;

&lt;p&gt;In a social or note-taking application, a user should only be able to view, edit, or delete notes that they created. This rule matches the user's logged-in unique ID (&lt;code&gt;request.auth.uid&lt;/code&gt;) against the document's path variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/notes/{noteId} {
      // Allow access only if the authenticated user matches the path ID
      allow read, write: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example B: Strict Content and Type Validation
&lt;/h3&gt;

&lt;p&gt;If you run an e-commerce platform or inventory app, any authenticated user can browse products, but only incoming data that passes formatting checks can be added to the catalog.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      // Anyone can read product details
      allow read: if true;

      // Enforce data integrity during creation
      allow create: if request.auth != null 
        &amp;amp;&amp;amp; request.resource.data.price is int 
        &amp;amp;&amp;amp; request.resource.data.price &amp;gt; 0
        &amp;amp;&amp;amp; request.resource.data.title.size() &amp;lt;= 50;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;request.resource.data&lt;/code&gt;: Points to the map data of the document as it will look if the write succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Realtime Database&amp;nbsp;Examples
&lt;/h2&gt;

&lt;p&gt;Realtime Database rules use a unique, hierarchical JSON structure rather than matching specific service paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example A: Chat Room - Read Allowed, Write Restrained
&lt;/h3&gt;

&lt;p&gt;In a global chat application, any user who is logged in should be able to read the history of a chat room, but they can only post messages that carry their own UID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"public_chats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"$roomId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;".read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth != null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"$messageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;".write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth != null &amp;amp;&amp;amp; newData.child('senderId').val() === auth.uid"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example B: Preventing Data Overwrites (Immutable Records)
&lt;/h3&gt;

&lt;p&gt;For critical logging data, financial audits, or transaction histories, you want users to be able to submit a log once, but prevent anyone from editing or deleting it later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transaction_logs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"$logId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;".read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth != null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Allow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;creation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;doesn't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;yet;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;prevent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;modifications&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;".write"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth != null &amp;amp;&amp;amp; !data.exists() &amp;amp;&amp;amp; newData.exists()"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Cloud Storage&amp;nbsp;Examples
&lt;/h2&gt;

&lt;p&gt;Firestore security parameters do not automatically safeguard your file assets. Cloud Storage handles rules separately to regulate asset uploads and size caps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example A: Restricting Upload File Sizes and MIME&amp;nbsp;Types
&lt;/h3&gt;

&lt;p&gt;To keep your storage costs under control and prevent bad actors from executing Denial of Service (DoS) attacks via massive files, you can restrict uploads to specific image file formats under 5MB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service firebase.storage {
  match /b/{bucket}/o {
    match /profile_pictures/{userId}/{allPaths=**} {
      // Profiles are publicly readable
      allow read: if true;

      // Uploads must belong to the user, be an image, and be under 5MB
      allow write: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId
                   &amp;amp;&amp;amp; request.resource.size &amp;lt; 5 * 1024 * 1024
                   &amp;amp;&amp;amp; request.resource.contentType.matches('image/.*');
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example B: Private Business Document Multi-User Access
&lt;/h3&gt;

&lt;p&gt;Imagine an app where multiple users belong to an organization account. They need access to a shared bucket of secure PDFs, while outside users are completely blocked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service firebase.storage {
  match /b/{bucket}/o {
    match /organizations/{orgId}/documents/{docId} {
      // Allow access if the user's custom auth token contains the matching organization ID
      allow read, write: if request.auth != null 
                         &amp;amp;&amp;amp; request.auth.token.organizationId == orgId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices for Writing Firebase Security&amp;nbsp;Rules
&lt;/h2&gt;

&lt;p&gt;As your application grows, security rules can quickly become difficult to manage. Following a few best practices can help keep your Firebase project secure and maintainable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start With Deny-By-Default
&lt;/h3&gt;

&lt;p&gt;Instead of allowing broad access and restricting it later, begin with restrictive rules and explicitly grant permissions where needed. This reduces the risk of accidentally exposing sensitive data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never Trust Client-Side Validation
&lt;/h3&gt;

&lt;p&gt;Validating forms in your app improves user experience, but it does not provide security. Attackers can bypass your UI entirely and send requests directly to Firebase. Always enforce critical validations in your security rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow the Principle of Least Privilege
&lt;/h3&gt;

&lt;p&gt;Users should only have access to the data and operations they absolutely need. Avoid granting collection-wide write access when document-level access is sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Custom Claims for&amp;nbsp;Roles
&lt;/h3&gt;

&lt;p&gt;For applications with administrators, moderators, or organization-based permissions, use Firebase Authentication custom claims instead of storing roles directly in client-accessible documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Rules Before Deployment
&lt;/h3&gt;

&lt;p&gt;Firebase provides the Emulator Suite to test security rules locally. Verify both successful and failed requests to ensure your rules behave exactly as expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Rules Under Version&amp;nbsp;Control
&lt;/h3&gt;

&lt;p&gt;Treat security rules like application code. Review changes carefully, track them in Git, and include them in your deployment process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Writing Firebase Security Rules can feel like extra overhead when you are trying to ship features quickly, but they are the literal foundation of your app's security model. Treat your security rules with the same respect as your frontend architecture - write them early, test them using the local Firebase Emulator Suite, and track them under version control.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>security</category>
    </item>
    <item>
      <title>Flutter Animations for Beginners: What You Can Build Without Plugins</title>
      <dc:creator>Codexlancers</dc:creator>
      <pubDate>Fri, 26 Jun 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/codexlancers/flutter-animations-for-beginners-what-you-can-build-without-plugins-p6c</link>
      <guid>https://dev.to/codexlancers/flutter-animations-for-beginners-what-you-can-build-without-plugins-p6c</guid>
      <description>&lt;p&gt;Animations are often where apps start to feel &lt;em&gt;alive&lt;/em&gt; instead of just functional.&lt;br&gt;
And if you're just getting started with Flutter, it's easy to assume:&lt;br&gt;
"I'll need a bunch of animation libraries to make things look good."&lt;/p&gt;

&lt;p&gt;We used to think the same.&lt;br&gt;
But in reality, Flutter already gives you a powerful set of built-in animation tools - no plugins required.&lt;/p&gt;

&lt;p&gt;In this blog, we'll walk through &lt;strong&gt;real UI animations we've built in production apps&lt;/strong&gt;, using only Flutter's default capabilities.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Avoid Plugins (At First)?
&lt;/h2&gt;

&lt;p&gt;From our experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plugins increase dependency risk&lt;/li&gt;
&lt;li&gt;Updates can break things unexpectedly&lt;/li&gt;
&lt;li&gt;Debugging becomes harder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flutter's built-in animations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;li&gt;Performant&lt;/li&gt;
&lt;li&gt;Fully customizable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly more than enough for most use cases.&lt;/p&gt;
&lt;h2&gt;
  
  
  Animations You Can Build Without Plugins
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Button Click Animation (Scale Effect)
&lt;/h3&gt;

&lt;p&gt;On tap, the button slightly scales down and then smoothly returns to its original size.&lt;/p&gt;

&lt;p&gt;It's a subtle effect, but it plays a big role - without feedback like this, users can feel unsure if their action was registered. This small touch makes the UI feel responsive and polished.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buttons&lt;/li&gt;
&lt;li&gt;Cards&lt;/li&gt;
&lt;li&gt;Any tappable UI
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnimatedButton&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_AnimatedButtonState&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_AnimatedButtonState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_AnimatedButtonState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AnimatedButton&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onTapDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onTapUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTapDown:&lt;/span&gt; &lt;span class="n"&gt;_onTapDown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onTapUp:&lt;/span&gt; &lt;span class="n"&gt;_onTapUp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedScale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;scale:&lt;/span&gt; &lt;span class="n"&gt;_scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tap Me"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;style:&lt;/span&gt; &lt;span class="n"&gt;TextStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;white&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Expand / Collapse Cards
&lt;/h3&gt;

&lt;p&gt;When tapped, the container smoothly expands or collapses to reveal or hide content.&lt;/p&gt;

&lt;p&gt;Rather than content appearing abruptly, this creates a more natural flow, making the UI easier to read and noticeably more polished.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Settings&lt;/li&gt;
&lt;li&gt;Dashboards
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExpandableCard&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_ExpandableCardState&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_ExpandableCardState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_ExpandableCardState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpandableCard&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isExpanded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isExpanded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isExpanded&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="n"&gt;isExpanded&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tap to Expand"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isExpanded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"More content here..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Image Zoom (Hero Animation)
&lt;/h3&gt;

&lt;p&gt;The image smoothly moves from one screen to another, as if it's the same element transitioning between pages.&lt;/p&gt;

&lt;p&gt;This creates a seamless experience and makes the app feel more polished.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Profile pictures&lt;/li&gt;
&lt;li&gt;Product previews&lt;/li&gt;
&lt;li&gt;Galleries
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;tag:&lt;/span&gt; &lt;span class="s"&gt;"imageHero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://via.placeholder.com/150"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Hero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;tag:&lt;/span&gt; &lt;span class="s"&gt;"imageHero"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://via.placeholder.com/600"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Fade-In Screens
&lt;/h3&gt;

&lt;p&gt;Instead of instantly appearing, the screen fades in smoothly.&lt;/p&gt;

&lt;p&gt;This removes the harshness of sudden transitions and creates a more pleasant, polished user experience.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarding&lt;/li&gt;
&lt;li&gt;Splash to home&lt;/li&gt;
&lt;li&gt;First load screens
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FadeScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_FadeScreenState&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_FadeScreenState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_FadeScreenState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FadeScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AnimatedOpacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;opacity:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Smooth List Insert / Remove
&lt;/h3&gt;

&lt;p&gt;When items are added or removed, they animate smoothly instead of just appearing or disappearing.&lt;/p&gt;

&lt;p&gt;This makes the changes feel more natural and easier to follow.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat apps&lt;/li&gt;
&lt;li&gt;Todo lists&lt;/li&gt;
&lt;li&gt;Notifications
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;AnimatedList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;initialItemCount:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SizeTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;sizeFactor:&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ListTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Page Transitions (Custom Navigation)
&lt;/h3&gt;

&lt;p&gt;This controls how screens transition when navigating between them.&lt;/p&gt;

&lt;p&gt;While default transitions work fine, custom ones can make the experience feel smoother and more intentional.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen navigation&lt;/li&gt;
&lt;li&gt;Onboarding flows
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;PageRouteBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;pageBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;___&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NextScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nl"&gt;transitionsBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FadeTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;opacity:&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Toggle UI Animations
&lt;/h3&gt;

&lt;p&gt;The switch smoothly slides and updates its color when toggled.&lt;/p&gt;

&lt;p&gt;This clear visual feedback helps users instantly understand the current state, making interactions feel more intuitive.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Settings&lt;/li&gt;
&lt;li&gt;Filters&lt;/li&gt;
&lt;li&gt;Feature toggles
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToggleAnimation&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_ToggleAnimationState&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_ToggleAnimationState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_ToggleAnimationState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ToggleAnimation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;isOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isOn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;AnimatedContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;isOn&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;green&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;isOn&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;centerRight&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;centerLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;radius:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Tween Animation (Advanced Control)
&lt;/h3&gt;

&lt;p&gt;This approach lets you animate values such as size, position, or opacity from one state to another.&lt;/p&gt;

&lt;p&gt;It offers much finer control, making it ideal for cases where built-in animation widgets don't quite meet your needs.&lt;/p&gt;

&lt;p&gt;Where to use?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom animations&lt;/li&gt;
&lt;li&gt;Complex UI interactions
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TweenExample&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_TweenExampleState&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_TweenExampleState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_TweenExampleState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TweenExample&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;SingleTickerProviderStateMixin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;Animation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;vsync:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Tween&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;begin:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;end:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AnimatedBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;animation:&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="n"&gt;animation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Mistakes Beginners Make
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overusing animations
&lt;/h3&gt;

&lt;p&gt;Just because you &lt;em&gt;can&lt;/em&gt; animate something doesn't mean you should.&lt;/p&gt;

&lt;p&gt;If everything is moving, nothing really stands out - and the UI starts to feel noisy instead of smooth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making animations too slow
&lt;/h3&gt;

&lt;p&gt;Long animations often feel frustrating instead of smooth.&lt;/p&gt;

&lt;p&gt;In most cases, anything above ~500ms starts to feel laggy and hurts the experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  No consistency in motion
&lt;/h3&gt;

&lt;p&gt;If every screen uses a different animation style, the app starts to feel disconnected.&lt;/p&gt;

&lt;p&gt;Consistency in motion is just as important as consistency in colors or fonts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring performance
&lt;/h3&gt;

&lt;p&gt;Animations can get expensive if not handled carefully.&lt;/p&gt;

&lt;p&gt;Avoid rebuilding heavy widgets unnecessarily while animations are running - it can quickly impact performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're just starting with Flutter animations, don't jump to plugins.&lt;/p&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AnimatedContainer&lt;/li&gt;
&lt;li&gt;AnimatedOpacity&lt;/li&gt;
&lt;li&gt;Hero&lt;/li&gt;
&lt;li&gt;AnimatedList&lt;/li&gt;
&lt;li&gt;Tween + AnimationController&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These alone can help you build &lt;strong&gt;smooth, production-level UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From our experience, mastering these basics gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better control&lt;/li&gt;
&lt;li&gt;Cleaner code&lt;/li&gt;
&lt;li&gt;More confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly most apps don't need more than this.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>animation</category>
    </item>
  </channel>
</rss>
