<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Emmanuel David Tuksa]]></title><description><![CDATA[I'm Emmanuel Tuksa, a software engineer sharing lessons from building user-focused apps. I write about what I learn, problems I solve, and tools that help create better products. Tips here. Stay tuned]]></description><link>https://detuksa.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 10:44:40 GMT</lastBuildDate><atom:link href="https://detuksa.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Ultimate Guide to Native macOS Menus in Flutter: Why mac_menu_bar Changes Everything]]></title><description><![CDATA[Building a Flutter app for macOS? Your menu bar experience is about to get a serious upgrade.

Hey Flutter developers!
Let me ask you something: Have you ever spent hours fighting with Flutter's PlatformMenuBar, trying to make your macOS app's menus ...]]></description><link>https://detuksa.com/the-ultimate-guide-to-native-macos-menus-in-flutter-why-macmenubar-changes-everything</link><guid isPermaLink="true">https://detuksa.com/the-ultimate-guide-to-native-macos-menus-in-flutter-why-macmenubar-changes-everything</guid><category><![CDATA[Flutter]]></category><category><![CDATA[packages]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Swift]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Emmanuel David Tuksa]]></dc:creator><pubDate>Mon, 12 Jan 2026 10:02:41 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Building a Flutter app for macOS? Your menu bar experience is about to get a serious upgrade.</p>
</blockquote>
<p>Hey Flutter developers!</p>
<p>Let me ask you something: Have you ever spent hours fighting with Flutter's <code>PlatformMenuBar</code>, trying to make your macOS app's menus feel <em>just right</em>? Have you found yourself recreating the entire menu structure just to add one tiny item? Or worse, have you given up on custom clipboard handling because intercepting standard menu actions seemed impossible?</p>
<p>If you nodded along to any of these, you're not alone. I've been there. We've all been there.</p>
<p>But what if I told you there's a better way? A way that makes working with macOS menus feel natural, powerful, and dare I say... <em>fun</em>?</p>
<p>Let me introduce you to <a target="_blank" href="https://pub.dev/packages/mac_menu_bar"><strong>mac_menu_bar</strong></a> – a Flutter plugin that completely transforms how you work with macOS menus. By the end of this article, you'll understand why this isn't just another plugin, but a game-changer for macOS Flutter development.</p>
<p>Grab your favorite beverage, and let's dive in.</p>
<h2 id="heading-the-platformmenubar-problem-and-why-its-worse-than-you-think">The PlatformMenuBar Problem (And Why It's Worse Than You Think)</h2>
<p>Let's be honest about Flutter's built-in <code>PlatformMenuBar</code>. It's not <em>bad</em> – it's actually quite innovative. The declarative API fits Flutter's philosophy beautifully. You define your menus in code, and Flutter handles the platform-specific rendering.</p>
<p>Sounds perfect, right?</p>
<p><strong>Wrong.</strong></p>
<p>Here's what happens when you actually try to build a professional macOS app with <code>PlatformMenuBar</code>:</p>
<h3 id="heading-problem-1-the-rebuild-everything-trap">Problem #1: The "Rebuild Everything" Trap</h3>
<p>Picture this: You're building a document editor. You want to add a single "Export to PDF" option to the File menu. Simple enough, right?</p>
<p><strong>With PlatformMenuBar, you write this:</strong></p>
<pre><code class="lang-dart">PlatformMenuBar(
  menus: [
    PlatformMenu(
      label: <span class="hljs-string">'File'</span>,
      menus: [
        <span class="hljs-comment">// Wait, I need ALL the standard items first...</span>
        PlatformMenuItem(
          label: <span class="hljs-string">'New'</span>,
          shortcut: SingleActivator(LogicalKeyboardKey.keyN, meta: <span class="hljs-keyword">true</span>),
          onSelected: _onNew,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Open...'</span>,
          shortcut: SingleActivator(LogicalKeyboardKey.keyO, meta: <span class="hljs-keyword">true</span>),
          onSelected: _onOpen,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Open Recent'</span>,
          <span class="hljs-comment">// Oh god, this needs a submenu...</span>
          menus: [
            <span class="hljs-comment">// Do I really have to manage recent files myself?</span>
          ],
        ),
        PlatformMenuDivider(),
        PlatformMenuItem(
          label: <span class="hljs-string">'Close'</span>,
          shortcut: SingleActivator(LogicalKeyboardKey.keyW, meta: <span class="hljs-keyword">true</span>),
          onSelected: _onClose,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Save'</span>,
          shortcut: SingleActivator(LogicalKeyboardKey.keyS, meta: <span class="hljs-keyword">true</span>),
          onSelected: _onSave,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Save As...'</span>,
          shortcut: SingleActivator(
            LogicalKeyboardKey.keyS,
            meta: <span class="hljs-keyword">true</span>,
            shift: <span class="hljs-keyword">true</span>,
          ),
          onSelected: _onSaveAs,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Revert to Saved'</span>,
          onSelected: _onRevert,
        ),
        PlatformMenuDivider(),
        <span class="hljs-comment">// FINALLY! My custom item!</span>
        PlatformMenuItem(
          label: <span class="hljs-string">'Export to PDF...'</span>,
          shortcut: SingleActivator(
            LogicalKeyboardKey.keyE,
            meta: <span class="hljs-keyword">true</span>,
            shift: <span class="hljs-keyword">true</span>,
          ),
          onSelected: _onExportPDF,
        ),
        PlatformMenuDivider(),
        PlatformMenuItem(
          label: <span class="hljs-string">'Page Setup...'</span>,
          onSelected: _onPageSetup,
        ),
        PlatformMenuItem(
          label: <span class="hljs-string">'Print...'</span>,
          shortcut: SingleActivator(LogicalKeyboardKey.keyP, meta: <span class="hljs-keyword">true</span>),
          onSelected: _onPrint,
        ),
      ],
    ),
    <span class="hljs-comment">// And this is just the FILE menu!</span>
    <span class="hljs-comment">// I haven't even started on Edit, View, Window, Help...</span>
  ],
)
</code></pre>
<p><strong>That's 60+ lines of code just to add ONE menu item.</strong></p>
<p>And here's the kicker: Miss one standard item, and your app feels broken to Mac users. Forget the keyboard shortcut for Print? Users notice. No "Recent Files" submenu? Feels amateur.</p>
<p>You're not building features anymore. You're maintaining boilerplate.</p>
<h3 id="heading-problem-2-standard-actions-are-locked-away">Problem #2: Standard Actions Are Locked Away</h3>
<p>Let me share a real scenario that drove me crazy:</p>
<p>I was building a code editor. I wanted Copy to include syntax highlighting information in the clipboard – you know, so when you paste into Slack or a word processor, the code formatting is preserved.</p>
<p>Seems reasonable, right? It's 2024. Every professional Mac app does this.</p>
<p><strong>But with PlatformMenuBar?</strong> There's literally no way to intercept the standard Edit menu Copy action. You can create your <em>own</em> Copy menu item, but the standard system one? Untouchable.</p>
<h3 id="heading-the-hidden-problem-user-expectations">The "Hidden" Problem: User Expectations</h3>
<p>Here's the thing that keeps me up at night: <strong>Mac users have expectations.</strong></p>
<p>When they open your app, they expect:</p>
<ul>
<li><p>A fully-featured File menu with New, Open, Save, Recent Files</p>
</li>
<li><p>An Edit menu with Undo, Redo, Cut, Copy, Paste, Select All</p>
</li>
<li><p>Standard keyboard shortcuts that work everywhere</p>
</li>
<li><p>Context-appropriate enabled/disabled states</p>
</li>
<li><p>Smooth, native performance</p>
</li>
</ul>
<p>With <code>PlatformMenuBar</code>, meeting these expectations means writing hundreds of lines of boilerplate code. And the moment you need to customize something? You're in for a world of pain.</p>
<p><strong>There has to be a better way.</strong></p>
<h2 id="heading-enter-macmenubar-a-paradigm-shift">Enter mac_menu_bar: A Paradigm Shift</h2>
<p>Okay, enough complaining. Let's talk solutions.</p>
<p>The <a target="_blank" href="https://pub.dev/packages/mac_menu_bar"><strong>mac_menu_bar</strong></a> plugin takes a fundamentally different approach. Instead of <em>recreating</em> the macOS menu system in Flutter, it <em>embraces</em> it.</p>
<p>Think about it: macOS already has a perfect menu system. It's mature, performant, accessible, and users understand it. Why fight against it?</p>
<h3 id="heading-the-philosophy-work-with-the-platform-not-against-it">The Philosophy: Work <em>With</em> the Platform, Not Against It</h3>
<p>Here's the core insight that makes <a target="_blank" href="https://pub.dev/packages/mac_menu_bar">mac_menu_bar</a> special:</p>
<blockquote>
<p><strong>Your Flutter app runs inside a macOS application that already has a menu bar. Instead of replacing it, enhance it.</strong></p>
</blockquote>
<p>This single philosophical shift changes everything.</p>
<p>Let me show you that same example from earlier:</p>
<p><strong>With</strong> <a target="_blank" href="https://pub.dev/packages/mac_menu_bar"><strong>mac_menu_bar</strong></a><strong>:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
  menuId: <span class="hljs-string">'File'</span>,
  itemId: <span class="hljs-string">'export_pdf'</span>,
  title: <span class="hljs-string">'Export to PDF...'</span>,
  shortcut: <span class="hljs-keyword">const</span> SingleActivator(
    LogicalKeyboardKey.keyE,
    meta: <span class="hljs-keyword">true</span>,
    shift: <span class="hljs-keyword">true</span>,
  ),
);

MacMenuBar.setMenuItemSelectedHandler((itemId) {
  <span class="hljs-keyword">if</span> (itemId == <span class="hljs-string">'export_pdf'</span>) _onExportPDF();
});
</code></pre>
<p><strong>That's it. 14 lines.</strong></p>
<p>The File menu? Already there with all the standard items. The positioning? macOS handles it. The accessibility? Built-in. The native feel? Perfect.</p>
<p>You just added your custom item to the <em>existing</em> menu structure. Like a native macOS app would.</p>
<p>When I built mac_menu_bar, I started with a simple question: "Why are we rebuilding what macOS already does perfectly?"</p>
<p>That question led to a fundamental insight:</p>
<p><strong>macOS menus work beautifully. We shouldn't replace them – we should enhance them.</strong></p>
<p>It's like the difference between:</p>
<ul>
<li><p>Building a house from scratch vs. renovating a room</p>
</li>
<li><p>Replacing your car's engine vs. adding a turbocharger</p>
</li>
<li><p>Rewriting Linux vs. writing a kernel module</p>
</li>
</ul>
<p>Why rebuild the entire system when you can extend it intelligently? This philosophy guided every design decision in mac_menu_bar.</p>
<h2 id="heading-feature-deep-dive-what-makes-this-special">Feature Deep-Dive: What Makes This Special</h2>
<p>Let's break down the features that make mac_menu_bar incredible. And I mean <em>really</em> break them down, with examples that show you the power.</p>
<h3 id="heading-feature-1-seamless-menu-integration">Feature 1: Seamless Menu Integration</h3>
<p><strong>The Promise:</strong> Add items to existing macOS menus without recreating them.</p>
<p><strong>Why It Matters:</strong> This is the headline feature, and it's revolutionary.</p>
<p><strong>Real Example:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Add to the File menu</span>
<span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
  menuId: <span class="hljs-string">'File'</span>,
  itemId: <span class="hljs-string">'quick_export'</span>,
  title: <span class="hljs-string">'Quick Export'</span>,
  shortcut: <span class="hljs-keyword">const</span> SingleActivator(
    LogicalKeyboardKey.keyE,
    meta: <span class="hljs-keyword">true</span>,
  ),
);

<span class="hljs-comment">// Add to the Edit menu</span>
<span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
  menuId: <span class="hljs-string">'Edit'</span>,
  itemId: <span class="hljs-string">'transform'</span>,
  title: <span class="hljs-string">'Transform Selection...'</span>,
  shortcut: <span class="hljs-keyword">const</span> SingleActivator(
    LogicalKeyboardKey.keyT,
    meta: <span class="hljs-keyword">true</span>,
    shift: <span class="hljs-keyword">true</span>,
  ),
);

<span class="hljs-comment">// Add to the View menu</span>
<span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
  menuId: <span class="hljs-string">'View'</span>,
  itemId: <span class="hljs-string">'toggle_sidebar'</span>,
  title: <span class="hljs-string">'Toggle Sidebar'</span>,
  shortcut: <span class="hljs-keyword">const</span> SingleActivator(
    LogicalKeyboardKey.keyB,
    meta: <span class="hljs-keyword">true</span>,
    alt: <span class="hljs-keyword">true</span>,
  ),
);
</code></pre>
<p>Each of these items appears in the <em>native</em> macOS menu, alongside the standard items that macOS provides. Your users see a complete, professional menu structure without you writing hundreds of lines of code.</p>
<p><strong>The Technical Magic:</strong> Behind the scenes, mac_menu_bar finds the existing NSMenu object (the native macOS menu) and adds your NSMenuItem to it. It's direct manipulation of native macOS UI components, which means:</p>
<ul>
<li><p>Zero Flutter rendering overhead</p>
</li>
<li><p>Perfect native appearance</p>
</li>
<li><p>Full accessibility support</p>
</li>
<li><p>System-level keyboard shortcut handling</p>
</li>
</ul>
<h3 id="heading-feature-2-standard-menu-action-interception">Feature 2: Standard Menu Action Interception</h3>
<p><strong>The Promise:</strong> Hook into Cut, Copy, Paste, and Select All to customize their behavior.</p>
<p><strong>Why It Matters:</strong> This unlocks professional-level features like custom clipboard formats, smart pasting, analytics, and validation.</p>
<p><strong>Real Example: Rich Text Editor</strong></p>
<pre><code class="lang-dart">MacMenuBar.onPaste(() <span class="hljs-keyword">async</span> {
      debugPrint(<span class="hljs-string">'Paste menu item selected'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    });
    MacMenuBar.onSelectAll(() <span class="hljs-keyword">async</span> {
      debugPrint(<span class="hljs-string">'Select all selected'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    });
    MacMenuBar.onCut(() <span class="hljs-keyword">async</span> {
      debugPrint(<span class="hljs-string">'Cut menu item selected'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    });
    MacMenuBar.onCopy(() <span class="hljs-keyword">async</span> {
      debugPrint(<span class="hljs-string">'Copy menu item selected'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    });
</code></pre>
<p><strong>The Power Move:</strong> Notice how we return <code>true</code> when we handle the action, and <code>false</code> when we want the system to handle it? This is brilliant because:</p>
<ul>
<li><p>You have full control when you need it</p>
</li>
<li><p>You defer to the system when you don't</p>
</li>
<li><p>Users get consistent behavior</p>
</li>
<li><p>You can make runtime decisions about handling</p>
</li>
</ul>
<h3 id="heading-feature-3-unlimited-submenu-nesting">Feature 3: Unlimited Submenu Nesting</h3>
<p><strong>The Promise:</strong> Create complex menu hierarchies with any level of nesting.</p>
<p><strong>Why It Matters:</strong> Professional apps need organized, discoverable features.</p>
<p><strong>Real Example: Developer Tools Menu</strong></p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; _setupDevToolsMenu() <span class="hljs-keyword">async</span> {
  <span class="hljs-comment">// Create main Developer menu</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addSubmenu(
    parentMenuId: <span class="hljs-string">'main'</span>,
    submenuId: <span class="hljs-string">'developer'</span>,
    title: <span class="hljs-string">'Developer'</span>,
  );

  <span class="hljs-comment">// Add direct items</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'developer'</span>,
    itemId: <span class="hljs-string">'toggle_console'</span>,
    title: <span class="hljs-string">'Toggle Console'</span>,
    shortcut: <span class="hljs-keyword">const</span> SingleActivator(
      LogicalKeyboardKey.keyJ,
      meta: <span class="hljs-keyword">true</span>,
      alt: <span class="hljs-keyword">true</span>,
    ),
  );

  <span class="hljs-comment">// Create Debug submenu</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addSubmenu(
    parentMenuId: <span class="hljs-string">'developer'</span>,
    submenuId: <span class="hljs-string">'debug'</span>,
    title: <span class="hljs-string">'Debug'</span>,
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'debug'</span>,
    itemId: <span class="hljs-string">'start_debug'</span>,
    title: <span class="hljs-string">'Start Debugging'</span>,
    shortcut: <span class="hljs-keyword">const</span> SingleActivator(LogicalKeyboardKey.f5),
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'debug'</span>,
    itemId: <span class="hljs-string">'step_over'</span>,
    title: <span class="hljs-string">'Step Over'</span>,
    shortcut: <span class="hljs-keyword">const</span> SingleActivator(LogicalKeyboardKey.f10),
  );

  <span class="hljs-comment">// Create Profiling submenu under Debug</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addSubmenu(
    parentMenuId: <span class="hljs-string">'debug'</span>,
    submenuId: <span class="hljs-string">'profiling'</span>,
    title: <span class="hljs-string">'Profiling'</span>,
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'profiling'</span>,
    itemId: <span class="hljs-string">'start_cpu_profile'</span>,
    title: <span class="hljs-string">'Start CPU Profiling'</span>,
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'profiling'</span>,
    itemId: <span class="hljs-string">'start_memory_profile'</span>,
    title: <span class="hljs-string">'Start Memory Profiling'</span>,
  );

  <span class="hljs-comment">// Create Test submenu</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addSubmenu(
    parentMenuId: <span class="hljs-string">'developer'</span>,
    submenuId: <span class="hljs-string">'testing'</span>,
    title: <span class="hljs-string">'Testing'</span>,
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'testing'</span>,
    itemId: <span class="hljs-string">'run_all_tests'</span>,
    title: <span class="hljs-string">'Run All Tests'</span>,
    shortcut: <span class="hljs-keyword">const</span> SingleActivator(
      LogicalKeyboardKey.keyT,
      meta: <span class="hljs-keyword">true</span>,
      shift: <span class="hljs-keyword">true</span>,
    ),
  );

  <span class="hljs-comment">// Even deeper nesting - Unit Tests under Testing</span>
  <span class="hljs-keyword">await</span> MacMenuBar.addSubmenu(
    parentMenuId: <span class="hljs-string">'testing'</span>,
    submenuId: <span class="hljs-string">'unit_tests'</span>,
    title: <span class="hljs-string">'Unit Tests'</span>,
  );

  <span class="hljs-keyword">await</span> MacMenuBar.addMenuItem(
    menuId: <span class="hljs-string">'unit_tests'</span>,
    itemId: <span class="hljs-string">'run_widget_tests'</span>,
    title: <span class="hljs-string">'Run Widget Tests'</span>,
  );
}
</code></pre>
<p><strong>Result:</strong> A professional, organized menu structure:</p>
<pre><code class="lang-plaintext">Developer
├── Toggle Console          Cmd+Alt+J
├── Debug
│   ├── Start Debugging     F5
│   ├── Step Over          F10
│   └── Profiling
│       ├── Start CPU Profiling
│       └── Start Memory Profiling
└── Testing
    ├── Run All Tests       Cmd+Shift+T
    └── Unit Tests
        └── Run Widget Tests
</code></pre>
<p><strong>Why This Matters:</strong> Complex applications need organized features. Good menu organization improves discoverability and user productivity. Mac users expect this level of organization in professional apps.</p>
<h2 id="heading-community-and-support">Community and Support</h2>
<p>The plugin is:</p>
<ul>
<li><p><strong>Well-documented</strong>: Comprehensive README with examples</p>
</li>
<li><p><strong>Thoroughly tested</strong>: Full test coverage with unit and integration tests</p>
</li>
<li><p><strong>Production-ready</strong>: Used in real-world applications</p>
</li>
<li><p><strong>Actively maintained</strong>: Regular updates and bug fixes</p>
</li>
<li><p><strong>Open source</strong>: MIT licensed, contributions welcome</p>
</li>
</ul>
<h2 id="heading-the-bottom-line">The Bottom Line</h2>
<p>If you're building a serious macOS app with Flutter, <strong>mac_menu_bar</strong> isn't just nice to have – it's essential. It gives you:</p>
<ul>
<li><p><strong>Native macOS integration</strong> that <code>PlatformMenuBar</code> can't match</p>
</li>
<li><p><strong>Dynamic menu management</strong> without widget rebuilds</p>
</li>
<li><p><strong>Standard menu action interception</strong> for custom behavior</p>
</li>
<li><p><strong>Type-safe keyboard shortcuts</strong> with full IDE support</p>
</li>
<li><p><strong>Professional menu structures</strong> with unlimited nesting</p>
</li>
<li><p><strong>Better performance</strong> through targeted updates</p>
</li>
<li><p><strong>Cleaner code</strong> with intuitive APIs</p>
</li>
</ul>
<p>Whether you're building a text editor, database tool, creative app, or productivity software, <strong>mac_menu_bar</strong> lets you create menu experiences that feel truly native to macOS – because they <em>are</em> native.</p>
<p>Your users will notice the difference. Your codebase will thank you. And you'll wonder how you ever managed without it.</p>
<h2 id="heading-get-started-today">Get Started Today</h2>
<pre><code class="lang-bash">flutter pub add mac_menu_bar
</code></pre>
<p>Then check out the <a target="_blank" href="https://pub.dev/packages/mac_menu_bar">documentation</a> and start building menus the way they were meant to be built on macOS. If you want to contribute checkout the repository <a target="_blank" href="https://github.com/DeTuksa/mac_menu_bar">here</a><a target="_blank" href="https://github.com/DeTuksa/mac_menu_bar">.</a></p>
<p>Your Flutter app deserves menus that feel at home on the Mac. <strong>mac_menu_bar</strong> makes it happen.</p>
<hr />
<p><em>Have you used mac_menu_bar in your project? Share your experience in the comments below!</em></p>
]]></content:encoded></item><item><title><![CDATA[Implementing Drag and Drop in Flutter with super_drag_and_drop]]></title><description><![CDATA[Drag and drop is a fundamental interaction pattern in modern applications, allowing users to intuitively move content between different areas of your app or even between applications. While Flutter provides basic drag and drop capabilities, the super...]]></description><link>https://detuksa.com/implementing-drag-and-drop-in-flutter-with-superdraganddrop</link><guid isPermaLink="true">https://detuksa.com/implementing-drag-and-drop-in-flutter-with-superdraganddrop</guid><category><![CDATA[Flutter]]></category><category><![CDATA[Drag & Drop]]></category><dc:creator><![CDATA[Emmanuel David Tuksa]]></dc:creator><pubDate>Thu, 08 Jan 2026 10:33:20 GMT</pubDate><content:encoded><![CDATA[<p>Drag and drop is a fundamental interaction pattern in modern applications, allowing users to intuitively move content between different areas of your app or even between applications. While Flutter provides basic drag and drop capabilities, the <a target="_blank" href="https://pub.dev/packages/super_drag_and_drop"><code>super_drag_and_drop</code></a> package offers a more robust, cross-platform solution that handles the complexities of different operating systems and web browsers.</p>
<h2 id="heading-why-superdraganddrop">Why super_drag_and_drop?</h2>
<p>The <a target="_blank" href="https://pub.dev/packages/super_drag_and_drop"><code>super_drag_and_drop</code></a> package provides several advantages over Flutter's built-in drag and drop widgets:</p>
<ul>
<li><p><strong>Cross-platform consistency</strong>: Works seamlessly across iOS, Android, macOS, Windows, Linux, and web</p>
</li>
<li><p><strong>Native integration</strong>: Supports dragging files from outside your app (file managers, other applications)</p>
</li>
<li><p><strong>Rich format support</strong>: Handles various data formats including files, images, text, and custom formats</p>
</li>
<li><p><strong>Modern API</strong>: Clean, intuitive API that follows Flutter's widget patterns</p>
</li>
</ul>
<h2 id="heading-getting-started">Getting Started</h2>
<p>First, add the package to your <code>pubspec.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">dependencies:</span>
  <span class="hljs-attr">super_drag_and_drop:</span> <span class="hljs-string">^0.9.1</span>
  <span class="hljs-attr">cross_file:</span> <span class="hljs-string">^0.3.4+2</span>
  <span class="hljs-attr">mime:</span> <span class="hljs-string">^2.0.0</span>
</code></pre>
<h2 id="heading-core-concepts">Core Concepts</h2>
<h3 id="heading-dropregion">DropRegion</h3>
<p>The <code>DropRegion</code> widget is the foundation of the drop functionality. It wraps any widget and makes it capable of receiving dropped content:</p>
<pre><code class="lang-dart">DropRegion(
  formats: [Formats.fileUri, Formats.png, Formats.jpeg],
  onDropOver: (event) =&gt; DropOperation.copy,
  onPerformDrop: (event) <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Handle the dropped content</span>
  },
  child: YourWidget(),
)
</code></pre>
<h3 id="heading-formats">Formats</h3>
<p>The package provides built-in format definitions for common types:</p>
<ul>
<li><p><code>Formats.fileUri</code> - File URIs from the file system</p>
</li>
<li><p><code>Formats.png</code>, <code>Formats.jpeg</code> - Image formats</p>
</li>
<li><p><code>Formats.plainText</code> - Text content</p>
</li>
<li><p>And other formats like <code>Formats.epub</code>, <code>Formats.md</code></p>
</li>
</ul>
<h3 id="heading-drop-events">Drop Events</h3>
<p>The package provides several callbacks to handle different stages of the drop operation:</p>
<ul>
<li><p><code>onDropOver</code>: Called when dragged content hovers over the region</p>
</li>
<li><p><code>onPerformDrop</code>: Called when content is actually dropped</p>
</li>
<li><p><code>onDropEnter</code>: Called when drag enters the region</p>
</li>
<li><p><code>onDropExit</code>: Called when drag leaves the region</p>
</li>
</ul>
<h2 id="heading-practical-implementation-file-drop-handler">Practical Implementation: File Drop Handler</h2>
<p>Let's build a practical drag and drop handler for file attachments, similar to what you might find in a chat application:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DragAndDropHandler</span> </span>{
  <span class="hljs-keyword">const</span> DragAndDropHandler({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onAttachments,
    <span class="hljs-keyword">this</span>.onDragEnter,
    <span class="hljs-keyword">this</span>.onDragExit,
  });

  <span class="hljs-keyword">final</span> <span class="hljs-keyword">void</span> <span class="hljs-built_in">Function</span>(<span class="hljs-built_in">List</span>&lt;FileData&gt; attachments) onAttachments;
  <span class="hljs-keyword">final</span> VoidCallback? onDragEnter;
  <span class="hljs-keyword">final</span> VoidCallback? onDragExit;

  Widget buildDropRegion({<span class="hljs-keyword">required</span> Widget child}) {
    <span class="hljs-keyword">return</span> DropRegion(
      formats: [
        Formats.fileUri,
        Formats.png,
        Formats.jpeg,
        Formats.pdf,
      ],
      hitTestBehavior: HitTestBehavior.deferToChild,
      onDropOver: (event) =&gt; DropOperation.copy,
      onPerformDrop: (event) <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">await</span> _handleDrop(event);
      },
      onDropEnter: (_) =&gt; onDragEnter?.call(),
      onDropLeave: (_) =&gt; onDragExit?.call(),
      child: child,
    );
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _handleDrop(PerformDropEvent event) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> items = event.session.items;

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> item <span class="hljs-keyword">in</span> items) {
      <span class="hljs-keyword">if</span> (item.dataReader != <span class="hljs-keyword">null</span>) {
        <span class="hljs-comment">// Handle file URI (desktop platforms)</span>
        item.dataReader!.getValue(Formats.fileUri, (uri) <span class="hljs-keyword">async</span> {
          <span class="hljs-keyword">if</span> (uri != <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">final</span> file = <span class="hljs-keyword">await</span> _processFile(uri);
            <span class="hljs-keyword">if</span> (file != <span class="hljs-keyword">null</span>) {
              onAttachments([file]);
            }
          }
        });

        <span class="hljs-comment">// Handle direct file data (web platform)</span>
        <span class="hljs-keyword">if</span> (item.dataReader!.canProvide(Formats.png)) {
          item.dataReader!.getFile(Formats.png, (file) <span class="hljs-keyword">async</span> {
            <span class="hljs-keyword">await</span> _processWebFile(file);
          });
        }
      }
    }
  }

  Future&lt;FileData?&gt; _processFile(<span class="hljs-built_in">Uri</span> uri) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> path = uri.toFilePath();
      <span class="hljs-keyword">final</span> file = XFile(path);
      <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> file.readAsBytes();
      <span class="hljs-keyword">final</span> mimeType = lookupMimeType(path, headerBytes: bytes);

      <span class="hljs-keyword">return</span> FileData(
        bytes: bytes,
        name: file.name,
        mimeType: mimeType ?? <span class="hljs-string">'application/octet-stream'</span>,
      );
    } <span class="hljs-keyword">catch</span> (e) {
      debugPrint(<span class="hljs-string">'Error processing file: <span class="hljs-subst">$e</span>'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }
  }

  Future&lt;<span class="hljs-keyword">void</span>&gt; _processWebFile(DataReaderFile file) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> stream = file.getStream();
    <span class="hljs-keyword">final</span> chunks = <span class="hljs-keyword">await</span> stream.toList();
    <span class="hljs-keyword">final</span> bytes = Uint8List.fromList(
      chunks.expand((e) =&gt; e).toList(),
    );

    <span class="hljs-keyword">final</span> mimeType = lookupMimeType(
      file.fileName ?? <span class="hljs-string">''</span>,
      headerBytes: bytes,
    ) ?? <span class="hljs-string">'application/octet-stream'</span>;

    <span class="hljs-keyword">final</span> fileData = FileData(
      bytes: bytes,
      name: file.fileName ?? <span class="hljs-string">'dropped_file'</span>,
      mimeType: mimeType,
    );

    onAttachments([fileData]);
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FileData</span> </span>{
  <span class="hljs-keyword">final</span> Uint8List bytes;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> name;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> mimeType;

  FileData({
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.bytes,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.name,
    <span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.mimeType,
  });
}
</code></pre>
<h2 id="heading-platform-specific-considerations">Platform-Specific Considerations</h2>
<h3 id="heading-web-vs-desktop">Web vs Desktop</h3>
<p>The package handles platform differences internally, but you need to be aware of how files are accessed:</p>
<p><strong>Desktop platforms</strong> (Windows, macOS, Linux):</p>
<ul>
<li><p>Use <code>Formats.fileUri</code> to get file paths</p>
</li>
<li><p>Access files directly from the file system</p>
</li>
<li><p>More straightforward file handling</p>
</li>
</ul>
<p><strong>Web platform</strong>:</p>
<ul>
<li><p>Cannot access file paths directly due to security restrictions</p>
</li>
<li><p>Must use <code>getFile()</code> method and stream the content</p>
</li>
<li><p>Process files as byte streams</p>
</li>
</ul>
<pre><code class="lang-dart"><span class="hljs-keyword">if</span> (!UniversalPlatform.isWeb) {
  <span class="hljs-comment">// Desktop: Use file URI</span>
  item.dataReader!.getValue(Formats.fileUri, (uri) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> path = uri.toFilePath();
    <span class="hljs-comment">// Process file from path</span>
  });
} <span class="hljs-keyword">else</span> {
  <span class="hljs-comment">// Web: Stream file content</span>
  item.dataReader!.getFile(format, (file) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> stream = file.getStream();
    <span class="hljs-keyword">final</span> bytes = <span class="hljs-keyword">await</span> stream
      .expand((chunk) =&gt; chunk)
      .toList();
    <span class="hljs-comment">// Process bytes</span>
  });
}
</code></pre>
<h2 id="heading-visual-feedback">Visual Feedback</h2>
<p>Visual feedback is crucial for creating an intuitive drag and drop experience. Users need clear indicators that show when content can be dropped, when they’re hovering over a valid drop zone, and when the drop operation completes successfully or fails.</p>
<p>The simplest form of feedbck is changing the appearance of your drop zone when users drag content over it. This can be achieved using the <code>onDropEnter</code> and <code>onDropExit</code> callbacks:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DropZone</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  State&lt;DropZone&gt; createState() =&gt; _DropZoneState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_DropZoneState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">DropZone</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _isDragging = <span class="hljs-keyword">false</span>;

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> DragAndDropHandler(
      onDragEnter: () =&gt; setState(() =&gt; _isDragging = <span class="hljs-keyword">true</span>),
      onDragExit: () =&gt; setState(() =&gt; _isDragging = <span class="hljs-keyword">false</span>),
      onAttachments: (files) {
        setState(() =&gt; _isDragging = <span class="hljs-keyword">false</span>);
        <span class="hljs-comment">// Handle files</span>
      },
    ).buildDropRegion(
      child: AnimatedContainer(
        duration: <span class="hljs-built_in">Duration</span>(milliseconds: <span class="hljs-number">200</span>),
        decoration: BoxDecoration(
          color: _isDragging 
            ? Colors.blue.withOpacity(<span class="hljs-number">0.1</span>)
            : Colors.transparent,
          border: Border.all(
            color: _isDragging 
              ? Colors.blue 
              : Colors.grey,
            width: _isDragging ? <span class="hljs-number">2</span> : <span class="hljs-number">1</span>,
          ),
          borderRadius: BorderRadius.circular(<span class="hljs-number">8</span>),
        ),
        child: Center(
          child: Text(
            _isDragging 
              ? <span class="hljs-string">'Drop files here'</span> 
              : <span class="hljs-string">'Drag files here'</span>,
          ),
        ),
      ),
    );
  }
}
</code></pre>
<h2 id="heading-mime-type-detection">MIME Type Detection</h2>
<p>Accurate MIME type detection is essential for properly handling different file types in your application. The MIME type determines how files should be processed, displayed, and stored. Flutter provides the <code>mime</code> packages for this purpose, but you’ll often need additional logic to handle edge cases.</p>
<h3 id="heading-why-mime-types-matter">Why MIME Types Matter</h3>
<p>MIME types serve several critical purposes:</p>
<ul>
<li><p>File validation: Ensures users can only upload supported file types</p>
</li>
<li><p>Icon selection: Display appropriate icons for different file types</p>
</li>
<li><p>Processing logic: Route files to the correct handlers (images to image processors, PDFs to document viewers, etc.)</p>
</li>
<li><p>Security: Prevent execution of potentially dangerous file types</p>
</li>
<li><p>API communication: Many backend APIs require accurate MIME types for file uploads</p>
</li>
</ul>
<h3 id="heading-basic-mime-type-detection">Basic MIME Type Detection</h3>
<p>The <code>mime</code> package's <code>lookupMimeType</code> function uses both file extensions and file content (magic numbers) to determine the MIME type:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:mime/mime.dart'</span>;

<span class="hljs-built_in">String</span> getMimeType(<span class="hljs-built_in">String</span> filePath, Uint8List bytes) {
  <span class="hljs-comment">// First attempt: use both filename and content</span>
  <span class="hljs-keyword">final</span> mimeType = lookupMimeType(
    filePath,
    headerBytes: bytes,
  );

  <span class="hljs-keyword">return</span> mimeType ?? <span class="hljs-string">'application/octet-stream'</span>;
}
</code></pre>
<h3 id="heading-handling-edge-cases">Handling Edge Cases</h3>
<p>Many files don't have standard extensions, or their content doesn't match typical patterns. You need custom logic for these cases:</p>
<pre><code class="lang-dart"><span class="hljs-built_in">String</span> detectMimeType(<span class="hljs-built_in">String</span> path, Uint8List bytes) {
  <span class="hljs-comment">// Try to detect from file extension and content</span>
  <span class="hljs-built_in">String?</span> mimeType = lookupMimeType(path, headerBytes: bytes);

  <span class="hljs-comment">// Fallback for markdown files</span>
  <span class="hljs-keyword">if</span> (mimeType == <span class="hljs-keyword">null</span> || mimeType == <span class="hljs-string">'application/octet-stream'</span>) {
    <span class="hljs-keyword">final</span> <span class="hljs-keyword">extension</span> = path.split(<span class="hljs-string">'.'</span>).last.toLowerCase();
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">extension</span> == <span class="hljs-string">'md'</span> || <span class="hljs-keyword">extension</span> == <span class="hljs-string">'markdown'</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-string">'text/markdown'</span>;
    }
  }

  <span class="hljs-keyword">return</span> mimeType ?? <span class="hljs-string">'application/octet-stream'</span>;
}
</code></pre>
<h2 id="heading-error-handling">Error Handling</h2>
<p>Robust error handling is critical for drag and drop operations because they involve file system access, network operations, and user interactions that can fail in numerous ways. A well-designed error handling strategy improves user experience and makes debugging easier.</p>
<h3 id="heading-common-error-scenarios">Common Error Scenarios</h3>
<p>File operations can fail for many reasons:</p>
<ul>
<li><p><strong>Permission errors</strong>: User lacks permission to read the file</p>
</li>
<li><p><strong>File not found</strong>: File was deleted or moved during the operation</p>
</li>
<li><p><strong>Network errors</strong>: File is on a network drive that became unavailable</p>
</li>
<li><p><strong>Memory errors</strong>: File is too large to load into memory</p>
</li>
<li><p><strong>Format errors</strong>: File content doesn't match its extension</p>
</li>
<li><p><strong>Encoding errors</strong>: File uses an unsupported character encoding</p>
</li>
<li><p><strong>Concurrent access</strong>: Another process is using the file</p>
</li>
<li><p><strong>Corruption</strong>: File data is corrupted or incomplete</p>
</li>
</ul>
<p>Always wrap file operations in try-catch blocks:</p>
<pre><code class="lang-dart">Future&lt;<span class="hljs-keyword">void</span>&gt; handleDrop(PerformDropEvent event) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> items = event.session.items;

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> item <span class="hljs-keyword">in</span> items) {
      <span class="hljs-keyword">if</span> (item.dataReader != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">await</span> processItem(item);
      }
    }
  } <span class="hljs-keyword">catch</span> (e, stackTrace) {
    debugPrint(<span class="hljs-string">'Error handling drop: <span class="hljs-subst">$e</span>'</span>);
    debugPrint(<span class="hljs-string">'Stack trace: <span class="hljs-subst">$stackTrace</span>'</span>);
    <span class="hljs-comment">// Show error to user</span>
  }
}
</code></pre>
<h2 id="heading-testing">Testing</h2>
<p>The package allows you to expose drop handling logic for testing:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DragAndDropHandler</span> </span>{
  <span class="hljs-meta">@visibleForTesting</span>
  Future&lt;FileData?&gt; handleDroppedFile(<span class="hljs-built_in">Uri</span> data) =&gt; _handleDroppedFile(data);

  Future&lt;FileData?&gt; _handleDroppedFile(<span class="hljs-built_in">Uri</span> data) <span class="hljs-keyword">async</span> {
    <span class="hljs-comment">// Implementation</span>
  }
}

<span class="hljs-comment">// In tests</span>
test(<span class="hljs-string">'handles dropped file correctly'</span>, () <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> handler = DragAndDropHandler(
    onAttachments: (files) =&gt; receivedFiles = files,
  );

  <span class="hljs-keyword">final</span> uri = <span class="hljs-built_in">Uri</span>.file(<span class="hljs-string">'/path/to/test.png'</span>);
  <span class="hljs-keyword">final</span> result = <span class="hljs-keyword">await</span> handler.handleDroppedFile(uri);

  expect(result, isNotNull);
  expect(result!.mimeType, <span class="hljs-string">'image/png'</span>);
});
</code></pre>
<h2 id="heading-complete-example">Complete Example</h2>
<p>Here's a complete example of a file upload area with drag and drop:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FileUploadArea</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">Function</span>(<span class="hljs-built_in">List</span>&lt;FileData&gt;) onFilesAdded;

  <span class="hljs-keyword">const</span> FileUploadArea({<span class="hljs-keyword">required</span> <span class="hljs-keyword">this</span>.onFilesAdded});

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

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_FileUploadAreaState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">FileUploadArea</span>&gt; </span>{
  <span class="hljs-built_in">bool</span> _isDragging = <span class="hljs-keyword">false</span>;
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">List</span>&lt;FileData&gt; _files = [];

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> handler = DragAndDropHandler(
      onAttachments: (files) {
        setState(() {
          _files.addAll(files);
          _isDragging = <span class="hljs-keyword">false</span>;
        });
        widget.onFilesAdded(files);
      },
      onDragEnter: () =&gt; setState(() =&gt; _isDragging = <span class="hljs-keyword">true</span>),
      onDragExit: () =&gt; setState(() =&gt; _isDragging = <span class="hljs-keyword">false</span>),
    );

    <span class="hljs-keyword">return</span> handler.buildDropRegion(
      child: Container(
        padding: EdgeInsets.all(<span class="hljs-number">32</span>),
        decoration: BoxDecoration(
          border: Border.all(
            color: _isDragging ? Colors.blue : Colors.grey,
            width: <span class="hljs-number">2</span>,
            style: BorderStyle.solid,
          ),
          borderRadius: BorderRadius.circular(<span class="hljs-number">12</span>),
          color: _isDragging 
            ? Colors.blue.withOpacity(<span class="hljs-number">0.05</span>)
            : <span class="hljs-keyword">null</span>,
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              Icons.cloud_upload,
              size: <span class="hljs-number">64</span>,
              color: _isDragging ? Colors.blue : Colors.grey,
            ),
            SizedBox(height: <span class="hljs-number">16</span>),
            Text(
              _isDragging 
                ? <span class="hljs-string">'Drop files here'</span>
                : <span class="hljs-string">'Drag and drop files here'</span>,
              style: TextStyle(
                fontSize: <span class="hljs-number">18</span>,
                fontWeight: FontWeight.w500,
              ),
            ),
            <span class="hljs-keyword">if</span> (_files.isNotEmpty) ...[
              SizedBox(height: <span class="hljs-number">24</span>),
              ..._files.map((file) =&gt; ListTile(
                leading: Icon(Icons.insert_drive_file),
                title: Text(file.name),
                subtitle: Text(file.mimeType),
              )),
            ],
          ],
        ),
      ),
    );
  }
}
</code></pre>
<h2 id="heading-best-practices">Best Practices</h2>
<ol>
<li><p><strong>Support multiple formats</strong>: Include all relevant formats your app can handle</p>
</li>
<li><p><strong>Provide clear visual feedback</strong>: Let users know when they're dragging over a valid drop zone</p>
</li>
<li><p><strong>Handle errors gracefully</strong>: File operations can fail for many reasons</p>
</li>
<li><p><strong>Consider platform differences</strong>: Web and desktop handle files differently</p>
</li>
<li><p><strong>Test thoroughly</strong>: Test with various file types and sizes</p>
</li>
<li><p><strong>Optimize for performance</strong>: Don't block the UI while processing large files</p>
</li>
<li><p><strong>Validate file types</strong>: Check MIME types and extensions before processing</p>
</li>
<li><p><strong>Set size limits</strong>: Prevent users from dropping excessively large files</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The <a target="_blank" href="https://pub.dev/packages/super_drag_and_drop"><code>super_drag_and_drop</code></a> package provides a robust, cross-platform solution for implementing drag and drop in Flutter applications. By understanding the platform differences and following best practices, you can create intuitive file handling experiences that work seamlessly across all platforms. The key is to handle both the desktop file URI approach and the web streaming approach, provide clear visual feedback, and always handle errors gracefully.</p>
]]></content:encoded></item><item><title><![CDATA[ValueNotifier and setState]]></title><description><![CDATA[Introduction
In flutter, we know that everything is a widget. The widgets can be divided into two; Stateless Widgets and Stateful Widgets. While the Stateless widget cannot be modified or changed once it has been built due to the fact that it does no...]]></description><link>https://detuksa.com/valuenotifier-and-setstate-41702964405b</link><guid isPermaLink="true">https://detuksa.com/valuenotifier-and-setstate-41702964405b</guid><dc:creator><![CDATA[Emmanuel David Tuksa]]></dc:creator><pubDate>Wed, 07 Sep 2022 10:46:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688386353268/98dede5f-e50c-4480-8260-d675dc086322.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688386353268/98dede5f-e50c-4480-8260-d675dc086322.jpeg" alt /></p>
<h4 id="heading-introduction"><strong>Introduction</strong></h4>
<p>In flutter, we know that everything is a widget. The widgets can be divided into two; <strong>Stateless Widgets</strong> and <strong>Stateful Widgets</strong>. While the Stateless widget cannot be modified or changed once it has been built due to the fact that it does not have any internal state and must be initialised again to reflect changes, the Stateful widget can be modified during it’s life-cycle without having to be reinitialised again.</p>
<p>Stateful widgets can be rebuilt by calling <code>**setState**</code>. This callback notifies the framework that the internal state of the object has been changed. However, it is not <a target="_blank" href="https://docs.flutter.dev/perf/best-practices"><em>best practice</em></a> to rebuild the entire widget and just rebuilding the what we want to update is a more efficient way of going about things. In this article, we would be looking at how to update parts of the UI without having to call <code>**setState**</code> and rebuild the entire widget tree.</p>
<p>ChangeNotifier is a simple class which provides change notification to its listeners. ValueNotifier is a a special type of class that extends a <code>ChangeNotifier</code>. When value is replaced with something that is not equal to the old value, this class notifies its listeners. Changes to the value can be listened to by either <code>**ValueListenableBuilder**</code> or <code>**AnimatedBuilder**</code> . Using a ValueNotifier and ValueListenableBuilder eliminates unnecessary rebuilds of the widget as the only update that’ll occur would be for the ValueListenableBuilder.</p>
<p>Below is an example of how to use a substitute <code>setState</code> with the ChangeNotifier.</p>
<blockquote>
<p>Create a New Flutter Project</p>
</blockquote>
<p>Open your terminal, navigate to the folder you’ll like to create a new project and run the following command</p>
<p><code>flutter create ***project_name***</code></p>
<p>Your <code>main.dart</code> file should look somewhat like the code below:</p>
<p>import 'package:flutter/material.dart';  </p>
<p>void main() {<br />  runApp(const MyApp());<br />}  </p>
<p>class MyApp extends StatelessWidget {<br />  const MyApp({Key? key}) : super(key: key);  </p>
<p>  // This widget is the root of your application.<br />  @override<br />  Widget build(BuildContext context) {<br />    return MaterialApp(<br />      title: 'Flutter Demo',<br />      theme: ThemeData(<br />        primarySwatch: Colors.<em>blue</em>,<br />      ),<br />      home: const MyHomePage(title: 'Flutter Demo Home Page'),<br />    );<br />  }<br />}  </p>
<p>class MyHomePage extends StatefulWidget {<br />  const MyHomePage({Key? key, required this.title}) : super(key: key);  </p>
<p>  final String title;  </p>
<p>  @override<br />  State createState() =&gt; _MyHomePageState();<br />}  </p>
<p>class _MyHomePageState extends State {<br />  int _counter = 0;  </p>
<p>  void _incrementCounter() {<br />    setState(() {<br />      _counter++;<br />    });<br />  }  </p>
<p>  @override<br />  Widget build(BuildContext context) {<br />    return Scaffold(<br />      appBar: AppBar(<br />        title: Text(widget.title),<br />      ),<br />      body: Center(<br />        child: Column(<br />          mainAxisAlignment: MainAxisAlignment.center,<br />          children: [<br />            const Text(<br />              'You have pushed the button this many times:',<br />            ),<br />            Text(<br />              '$_counter',<br />              style: Theme.<em>of</em>(context).textTheme.headline4,<br />            ),<br />          ],<br />        ),<br />      ),<br />      floatingActionButton: FloatingActionButton(<br />        onPressed: _incrementCounter,<br />        tooltip: 'Increment',<br />        child: const Icon(Icons.<em>add</em>),<br />      ),<br />    );<br />  }<br />}</p>
<p>This uses <code>setState</code> to update the value of counter, thereby rebuilding the widget each time the value is changed. We’ll look at a different approach to this using the <code>ValueNotifier</code> .</p>
<blockquote>
<p>Change <code>_counter</code> Type</p>
</blockquote>
<p>Change<br /><code>int _counter = 0;</code><br />to<br /><code>final _counter = ValueNotifier&lt;int&gt;(0);</code></p>
<p>Now <code>_counter</code> is a type of ValueNotifier.</p>
<blockquote>
<p>Edit <code>_incrementCounter</code> method</p>
</blockquote>
<p>Replace the content of the <code>_incrementCounter</code> with the code below</p>
<p>void _incrementCounter() {<br />  _counter.value++;<br />}</p>
<blockquote>
<p>Listen to Changes</p>
</blockquote>
<p>Finally, we would wrap the text widget with the <code>**ValueListenableBuilder**</code> to listen to the changes on <code>_counter</code> .</p>
<p>Replace:</p>
<p>Text(<br />  '$_counter',<br />  style: Theme.<em>of</em>(context).textTheme.headline4,<br />),</p>
<p>With:</p>
<p>ValueListenableBuilder(<br />  valueListenable: _counter,<br />  builder: (context, value, _) {<br />    return Text(<br />      '$value',<br />      style: Theme.<em>of</em>(context).textTheme.headline4,<br />    );<br />  }<br />),</p>
<p>With this new method of updated <code>_counter</code> , whenever there is a change to the value of <code>_counter</code> only the <code>**ValueListenableBuilder**</code> is updated, hereby avoiding any unnecessary rebuild. This method would be efficient for more complex applications and improves the performance of your flutter application.</p>
<p><strong>Conclusion</strong></p>
<p>This draws us to the end of our short journey on ValueNotifier and setState flutter, I hope it has been really helpful so far. If you have any more questions or want to engage feel free to connect with me on <a target="_blank" href="http://twitter.com/dt_emmy"><strong>Twitter</strong></a> or <a target="_blank" href="https://www.linkedin.com/in/emmanuel-david-b5a49b158"><strong>LinkedIn</strong></a>. Do appreciate the article with some claps and comments.</p>
<p>If you want the complete code, here is a link to the <a target="_blank" href="https://github.com/DeTuksa/ValueNotifierExample">github repository</a> of the project.</p>
]]></content:encoded></item><item><title><![CDATA[Biometric Authentication in Flutter]]></title><description><![CDATA[Introduction
Authentication simply means verifying beyond a doubt that a person is who they say they are. Biometric authentication performs this verification by checking distinctive biological or behavioral characteristics. While building application...]]></description><link>https://detuksa.com/biometric-authentication-in-flutter-841fced942e5</link><guid isPermaLink="true">https://detuksa.com/biometric-authentication-in-flutter-841fced942e5</guid><dc:creator><![CDATA[Emmanuel David Tuksa]]></dc:creator><pubDate>Fri, 22 Apr 2022 10:46:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688386360942/6d913698-730f-4a63-ac72-0a62db481572.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4 id="heading-introduction">Introduction</h4>
<p>Authentication simply means verifying beyond a doubt that a person is who they say they are. Biometric authentication performs this verification by checking distinctive biological or behavioral characteristics. While building applications that contain sensitive information and users data, it is important to make it secure and add layers of security, one of such being the biometric authentication. In this article we would go over how to setup biometric authentication in a flutter application.</p>
<blockquote>
<p>Create your flutter project</p>
</blockquote>
<p>This can be achieved by running:</p>
<p><code>flutter create project name</code></p>
<blockquote>
<p>Add the loca_auth package</p>
</blockquote>
<p>Add <code>local_auth: ^1.1.10</code> to your pubspec.yaml file. This package is the responsible for the biometric authentication.</p>
<p>After adding the package, run <code>flutter pub get</code> to install the package.</p>
<p>Also change your MainActivity.kt file to a Fragment Activity:</p>
<p>import io.flutter.embedding.android.FlutterFragmentActivity<br />import io.flutter.embedding.engine.FlutterEngine<br />import io.flutter.plugins.GeneratedPluginRegistrant<br />import android.view.WindowManager.LayoutParams  </p>
<p>class MainActivity: FlutterFragmentActivity() {<br />    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {<br />        GeneratedPluginRegistrant.registerWith(flutterEngine)<br />    }<br />}</p>
<p>Next is to add this to AndroidManifest.xml</p>
<p>&lt;uses-permission android:name="android.permission.USE_FINGERPRINT"/</p>
<blockquote>
<p>Walk through</p>
</blockquote>
<p>The first thing to do is to initialize the <code>LocalAuthentication</code> variable.</p>
<p><code>final LocalAuthentication localAuthentication = LocalAuthentication();</code></p>
<p>Next thing to do is to check if the device has biometrics support or not. The localAuthentication variable we declared has a future boolean property <code>canCheckBiometrics</code> that returns true if biometrics is supported and false if not. We can acheive this check with the code below</p>
<p>Future getBiometricSupport() async {<br />  bool hasSupport = false;<br />  try {<br />    hasSupport = await localAuthentication.canCheckBiometrics;<br />  } catch(e) {<br />    print(e);<br />  }<br />  return hasSupport;<br />}</p>
<p>After checking if the device has biometrics or not, the next thing to do is to authenticate user using biometrics if it is enabled. The code below can be used to achieve this.</p>
<p>Future authenticateUser() async {<br />  bool authenticated = false;<br />  bool biometric = await getBiometricSupport();<br />  setState(() {<br />    hasBiometric = biometric;<br />  });<br />  if (biometric) {<br />    try {<br />      authenticated = await localAuthentication.authenticate(<br />        localizedReason: 'Authenticate User',<br />        useErrorDialogs: true,<br />        stickyAuth: true,<br />        sensitiveTransaction: true,<br />        biometricOnly: true<br />      );<br />    } catch (e) {<br />      print(e);<br />    }<br />  } else {<br />    ScaffoldMessenger.<em>of</em>(context).showSnackBar(errorSnackBar);<br />    setState(() {<br />      authenticated = false;<br />    });<br />  }<br />  return authenticated;<br />}</p>
<p>This function can be used either on init or on tap of a button depending on the user experience the application desires. With that setup we are all good to go.</p>
<h4 id="heading-conclusion">Conclusion</h4>
<p>This draws us to the end of our short journey on biometric authentication in flutter, I hope it has been really helpful so far. If you have any more questions or want to engage feel free to connect with me on <a target="_blank" href="http://twitter.com/dt_emmy"><strong>Twitter</strong></a> or <a target="_blank" href="https://www.linkedin.com/in/emmanuel-david-b5a49b158"><strong>LinkedIn</strong></a>. Do appreciate the article with some claps and comments.</p>
<p>If you want the complete code, here is a link to the <a target="_blank" href="https://github.com/DeTuksa/biometric-auth">github repository</a> of the project.</p>
]]></content:encoded></item><item><title><![CDATA[Gesture Detector in Row- Flutter]]></title><description><![CDATA[In flutter, GestureDetector is a commonly used widget. It is simply a widget that detects gestures. It can be used to wrap other widgets that you want to be clickable. This enables us to create custom buttons and custom clickable widgets. However, wh...]]></description><link>https://detuksa.com/gesture-detector-in-row-flutter-a07198a0b68a</link><guid isPermaLink="true">https://detuksa.com/gesture-detector-in-row-flutter-a07198a0b68a</guid><dc:creator><![CDATA[Emmanuel David Tuksa]]></dc:creator><pubDate>Wed, 13 Apr 2022 07:30:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688386369078/b598e626-d0d3-419a-837c-6fc8c100e045.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In flutter, <em>GestureDetector</em> is a commonly used widget. It is simply a widget that detects gestures. It can be used to wrap other widgets that you want to be clickable. This enables us to create custom buttons and custom clickable widgets. However, when a Row is wrapped with a <em>GestureDetector</em> the <em>GestureDetector</em> detects clicks on only the children of the Row and not the entire Row itself. Confused? Take a look at the image below.</p>
<p>On the first <em>Row</em>, when the white space between “Press this Text” and “Or press this Text” is clicked, no action is carried out. However, on the second <em>Row</em> when the white space is clicked the action is carried out. In this article, we’ll be looking at how to make the entire <em>Row</em> clickable and not just children of the <em>Row</em>.</p>
<blockquote>
<p><strong>1.</strong> create your flutter project</p>
</blockquote>
<p>This can be achieved by running</p>
<p><code>flutter create project_name</code></p>
<blockquote>
<p><strong>2.</strong> Add this to your main.dart file</p>
</blockquote>
<p>import 'package:flutter/material.dart';  </p>
<p>void main() {<br />  runApp(const MyApp());<br />}  </p>
<p>class MyApp extends StatelessWidget {<br />  const MyApp({Key? key}) : super(key: key);  </p>
<p>  @override<br />  Widget build(BuildContext context) {<br />    return MaterialApp(<br />      title: 'Row in Gesture Detector',<br />      theme: ThemeData(<br />        primarySwatch: Colors.<em>blue</em>,<br />      ),<br />      debugShowCheckedModeBanner: false,<br />      home: const MyHomePage(title: 'Row in Gesture Detector'),<br />    );<br />  }<br />}  </p>
<p>class MyHomePage extends StatefulWidget {<br />  const MyHomePage({Key? key, required this.title}) : super(key: key);  </p>
<p>  final String title;  </p>
<p>  @override<br />  State createState() =&gt; _MyHomePageState();<br />}  </p>
<p>class _MyHomePageState extends State {  </p>
<p>  @override<br />  Widget build(BuildContext context) {<br />    return Scaffold();<br />  }<br />}</p>
<blockquote>
<p><strong>3.</strong> Create the increment method to test the gesture behavior</p>
</blockquote>
<p>Add this to the <code>MyHomePageState</code> class</p>
<p>int _counter = 0;  </p>
<p>void _incrementCounter() {<br />  setState(() {<br />    _counter++;<br />  });<br />}</p>
<blockquote>
<p><strong>4.</strong> Create the Row and wrap with Gesture Detector</p>
</blockquote>
<p>Add this to the Scaffold body</p>
<p>return Scaffold(<br />  appBar: AppBar(<br />    title: Text(widget.title),<br />  ),<br />  body: Center(<br />    child: Column(<br />      mainAxisAlignment: MainAxisAlignment.center,<br />      children: [<br />        GestureDetector(<br />          onTap: _incrementCounter,<br />          child: Row(<br />            mainAxisSize: MainAxisSize.max,<br />            mainAxisAlignment: MainAxisAlignment.spaceAround,<br />            crossAxisAlignment: CrossAxisAlignment.center,<br />            children: const [<br />              Text(<br />                'Press this Text'<br />              ),<br />              Text(<br />                'Or press this Text'<br />              )<br />            ],<br />          ),<br />        ),<br />        const SizedBox(height: 80,),<br />        GestureDetector(<br />          behavior: HitTestBehavior.opaque,<br />          onTap: _incrementCounter,<br />          child: Row(<br />            mainAxisSize: MainAxisSize.max,<br />            mainAxisAlignment: MainAxisAlignment.spaceAround,<br />            crossAxisAlignment: CrossAxisAlignment.center,<br />            children: const [<br />              Text(<br />                  'Press white space'<br />              ),<br />              Text(<br />                  'Press white space'<br />              )<br />            ],<br />          ),<br />        ),<br />        const SizedBox(height: 60,),<br />        Text(<br />          '$_counter',<br />          style: Theme.<em>of</em>(context).textTheme.headline4,<br />        ),<br />      ],<br />    ),<br />  ),<br />);</p>
<p>If you notice in the code, we have two sepeate <em>Rows</em> wrapped with <em>GestureDetector</em>. One has <em>behavior</em> set while the other doesn’t. The <em>GestureDetector</em> with <em>behavior</em> set is the one that enables the whole Row clickable and I’ll be explaining that shortly.<br />The <em>GestureDetector</em> widget has a field <em>behavior</em> that determines how the <em>GestureDetector</em> acts when hit. The <em>behavior</em> field is of type <em>HitTestBehavior</em>. By default, the <em>behavior</em> field is set to <em>HitTestBehavior.deferToChild</em> which ensures that the <em>GestureDetector</em> receives events only when one of the children of its target is being hit. That sometimes might not be a good user experience. Setting the <em>behavior</em> field to <em>HitTestBehavior.Opaque</em> enables the <em>GestureDetector</em> to receive events as long as the hit was received within the target’s bounds.</p>
<h4 id="heading-conclusion">Conclusion</h4>
<p>We have reached the end of this article. I hope it has been helpful so far. If you have any more questions or want to engage feel free to connect with me on <a target="_blank" href="http://twitter.com/dt_emmy"><strong>Twitter</strong></a> or <a target="_blank" href="https://www.linkedin.com/in/emmanuel-david-b5a49b158"><strong>LinkedIn</strong></a>. Do appreciate the article with some claps and comments.</p>
<p>If you want the complete code, here is a link to the <a target="_blank" href="https://github.com/DeTuksa/Row-In-GestureDetector">github repository</a> of the project.</p>
]]></content:encoded></item></channel></rss>