How I Published My Flutter App to the Ubuntu Snap Store

How I Published My Flutter App to the Ubuntu Snap Store

A complete walkthrough of bringing a Flutter desktop app to the Ubuntu Snap Store — from first build to final approval, including every obstacle along the way.

15 min read
Updated June 1, 2026

If you've built a Flutter app and want to distribute it on Linux, the Ubuntu Snap Store is one of the most accessible channels — it's built into Ubuntu and reaches millions of users. But getting a non-trivial Flutter app published involves more than running snapcraft and uploading the result.

This article walks you through how I published Huda, a full-featured Islamic companion app, to the Snap Store. Along the way, I ran into twelve significant issues — from native library resolution to backend migration to store review — and I'll share exactly how each one was resolved.

Whether you're publishing your first Flutter desktop app or debugging Snap confinement issues, this guide has something for you.

Setting Up the Linux Build

Flutter's Linux desktop support has matured significantly, but your app still needs to compile and run natively on an x86_64 Linux system. The first step is making sure flutter build linux completes successfully.

For Huda, the app relied on several native libraries and platform plugins that worked perfectly on Android and iOS but had no Linux equivalents out of the box. The first challenge appeared immediately.

Handling Native Dependencies

Resolving libmpv for Audio Playback

Huda uses media_kit for Quran audio playback. On Windows, the media_kit_libs_windows_audio package bundles everything needed. On Linux, there's no such convenience — you need the system's libmpv library.

The error:

Exception: Cannot find libmpv at the usual places.

The fix was straightforward: add media_kit_libs_linux to pubspec.yaml and install the system runtime library:

sudo apt install libmpv2

This tells the Dart FFI layer where to find the shared library at runtime.

The Snap Environment Breaks Library Resolution

After installing libmpv, the error persisted — but only when launching from VS Code. This was confusing until I realized that VS Code itself is installed as a Snap package.

Snap-sandboxed processes have an altered dynamic linker environment. DynamicLibrary.open('libmpv.so.2') relies on dlopen() resolving names through the system's ldconfig cache, but inside a Snap environment, that resolution silently fails.

The solution was to bypass name-based resolution entirely:

lib/core/bootstrap/bootstrap.dart
String? _findLinuxLibmpv() {
  final paths = [
    '/usr/lib/x86_64-linux-gnu/libmpv.so.2',
    '/usr/lib/libmpv.so.2',
    '/usr/lib64/libmpv.so.2',
  ];
  for (final p in paths) {
    if (File(p).existsSync()) return p;
  }
  return null;
}

By probing well-known absolute filesystem paths and passing the result directly to JustAudioMediaKit.ensureInitialized(), we bypass dlopen name resolution completely.

The librsvg Symbol Conflict

Even after resolving the path, loading libmpv caused a crash:

Failed to load dynamic library: undefined symbol: rsvg_handle_get_intrinsic_size_in_pixels

The root cause was subtle: VS Code's Snap bundles an older version of librsvg (2.47.0), and the Snap launcher injects this into the dynamic linker search path. When libmpv loads its dependency chain (libmpv → libavcodec → librsvg), it picks up the Snap's outdated version instead of the system's version 2.60.0, which is the first to export the required symbol.

The fix was applied at the C++ level in the GTK runner:

linux/runner/main.cc
#include <dlfcn.h>
 
int main(int argc, char** argv) {
  // Pre-load system librsvg before Flutter initializes
  dlopen("librsvg-2.so.2", RTLD_NOW | RTLD_GLOBAL);
 
  // ... Flutter initialization
}

By loading the system's librsvg into the global symbol table first, the correct symbols are already resolved when libavcodec later tries to load them. The ${CMAKE_DL_LIBS} linkage was also added to CMakeLists.txt to provide dlopen portably.

Platform Compatibility

Once the native audio pipeline worked, the next set of issues came from platform-specific features that simply don't exist on Linux.

Local Notifications

Flutter's flutter_local_notifications plugin threw an error because LinuxInitializationSettings was missing:

Invalid argument(s): Linux settings must be set when targeting Linux platform.

A quick addition to the initialization code fixed it:

lib/core/services/notification_services.dart
final settings = InitializationSettings(
  android: androidSettings,
  iOS: iosSettings,
  macOS: macOSSettings,
  linux: LinuxInitializationSettings(
    defaultActionName: 'Open notification',
  ),
);

Migrating from Firebase to Supabase

This was the biggest change. Firebase has no Linux supportfirebase_core, cloud_firestore, and firebase_storage all lack Linux implementations. The build either fails or crashes at startup.

The solution was a complete backend migration to Supabase, which works on all platforms including Linux:

  • Firestore collection writes → Supabase table inserts (app_feedback, error_reports)
  • Firebase Storage uploads → Supabase Storage bucket uploads
  • Firebase.initializeApp()Supabase.initialize()

This was a significant effort but resulted in a cleaner, more portable architecture. As a side effect, the Quran font download logic in qcf_font_service.dart had to be rewritten to use Dio HTTP requests against Supabase public storage URLs instead of Firebase Storage references.

Gracefully Handling Missing Linux APIs

The geolocator plugin doesn't implement permission-check or settings-opening APIs on Linux. Calling them throws MissingPluginException. Linux desktop apps also don't have a system location-settings screen.

The solution used platform guards:

if (PlatformUtils.isLinux) {
  // Skip permission checks — assume always granted
  // Hide "Open Settings" button
  // Show "Search Manually" instead
}

A ManualLocationSearchDialog backed by the existing Nominatim geocoding service lets Linux users set their location by searching for a city name.

Similarly, the Memorization tab (which relies on microphone access for audio recording) was conditionally excluded on Linux by reducing the DefaultTabController length from 4 to 3.

Packaging with Snapcraft

With the app running on Linux, the next phase was packaging it as a Snap for distribution.

Setting the Window Icon

Flutter's Linux shell doesn't set a window icon automatically. The app launched with the generic GTK icon.

The fix was adding gtk_window_set_icon_from_file() in the GTK runner:

linux/runner/my_application.cc
// Check debug vs release path for the icon
if (g_file_test("assets/dev/huda_center.png", G_FILE_TEST_EXISTS)) {
  gtk_window_set_icon_from_file(window, "assets/dev/huda_center.png", NULL);
} else {
  gtk_window_set_icon_from_file(window,
    "data/flutter_assets/assets/dev/huda_center.png", NULL);
}

Staging Libraries Inside the Snap

Inside the strict-confinement sandbox, the Snap cannot access the host system's /usr/lib. The libmpv library must be staged inside the snap itself.

snap/snapcraft.yaml
parts:
  huda:
    plugin: nil
    stage-packages:
      - libmpv2

Snapcraft automatically stages libmpv2 and all its declared apt dependencies inside the snap's usr/lib/x86_64-linux-gnu/. The _findLinuxLibmpv() helper already probes absolute paths, so it correctly finds the staged copy at $SNAP/usr/lib/x86_64-linux-gnu/libmpv.so.2.

After staging libmpv, a new crash appeared:

libblas.so.3: cannot open shared object file: No such file or directory

On Debian/Ubuntu, libblas3 and liblapack3 install their .so files under architecture-specific subdirectories and create top-level symlinks via update-alternatives. But Snapcraft stages packages verbatim without running update-alternatives, so the top-level symlinks are never created.

The fix was an override-prime script in snapcraft.yaml:

snap/snapcraft.yaml
override-prime: |
  craftctl default
  cd usr/lib/x86_64-linux-gnu
  ln -sf blas/libblas.so.3 libblas.so.3
  ln -sf lapack/liblapack.so.3 liblapack.so.3

Additionally, librsvg2-2 was removed from stage-packages because staging it conflicted with the GNOME content snap's own librsvg, breaking the gdk-pixbuf SVG loader.

Store Submission and Review

With the Snap building and running correctly, I uploaded it to the Snap Store. The automated pipeline accepted the upload but immediately flagged it for manual human review:

Issues while processing snap:
- human review required due to 'deny-connection' constraint

The snapcraft.yaml declares slots for D-Bus (claiming the com.aw.huda namespace) and MPRIS (global media player controls). Because these interfaces request system-level privileges, the automated pipeline flags them with a deny-connection security constraint.

The resolution was to submit a manual review request on the Snapcraft Forum under the store-requests > privileged-interfaces subcategory. The request included:

  • App description and purpose
  • Upstream repository status
  • Technical justification for why D-Bus and MPRIS are required (continuous audio playback, hardware media key support)

After review and approval, the snap was published and is now available at snapcraft.io/huda.

Key Takeaways

  1. Test outside your IDE's sandbox. If your IDE is a Snap (like VS Code), library resolution behaves differently than in a bare terminal. Always test with flutter run from a non-sandboxed shell.

  2. Audit every plugin for Linux support. Most Flutter plugins are mobile-first. Check each dependency's platform support matrix and have fallbacks ready.

  3. Snap confinement means self-contained packaging. Don't assume system libraries are available. Stage everything your app needs inside the snap.

  4. update-alternatives doesn't run in snaps. If your dependencies rely on symlinks created by Debian package scripts, you'll need to create them manually in override-prime.

  5. Privileged interfaces require human review. If your app uses D-Bus, MPRIS, or other system-level interfaces, budget time for the forum review process.

  6. Backend portability matters. Firebase's lack of Linux support forced a full migration. If you plan to ship on Linux, choose a backend that works everywhere from the start.


Frequently Asked Questions

How do I publish a Flutter app to the Ubuntu Snap Store?

Build your Flutter Linux desktop app with flutter build linux, create a snapcraft.yaml that packages the build output, run snapcraft to produce the .snap file, then upload it with snapcraft upload. If your app uses privileged interfaces, you may need to request manual review on the Snapcraft Forum.

Why can't my Flutter app find libmpv on Linux?

Flutter uses DynamicLibrary.open() which relies on dlopen() for name resolution. If you're running inside a Snap environment (e.g., VS Code installed as a Snap), the dynamic linker environment is altered. Use absolute paths to the library instead of relying on name resolution.

Does Firebase work with Flutter on Linux?

No. Firebase FlutterFire packages only support Android, iOS, macOS, and Web. For Linux desktop apps, consider alternatives like Supabase, which provides similar functionality with full cross-platform support.

How do I fix "deny-connection" errors when uploading to the Snap Store?

This happens when your snap declares interfaces that require system-level privileges (like D-Bus or MPRIS). Submit a manual review request on the Snapcraft Forum with a technical justification for why each interface is needed.

What libraries do I need to stage in my Flutter snap?

Any native library your app depends on that isn't provided by the GNOME extension. Common examples include libmpv2 for media playback. Check the snap's runtime logs for "cannot open shared object file" errors to identify missing libraries.

Setting Up the Linux Build