DEV Community

Cover image for I Built a Media Library for Flet Because Real Mobile Apps Need Native APIs
Faizan gondal
Faizan gondal

Posted on

I Built a Media Library for Flet Because Real Mobile Apps Need Native APIs

GitHub: https://github.com/fazi-gondal/Flet-media-library
PyPI: https://pypi.org/project/flet-media-library/

I’ve been building mobile applications with Flet, and one thing becomes obvious pretty quickly when you move beyond basic apps:

Python and Flet are great for building the application itself, but eventually you need to talk to the operating system.

Media handling is a good example.

As soon as an app needs to browse photos, display videos, save downloads to the gallery, record audio, manage files, or react to changes in the device's media library, you start dealing with platform-specific APIs, permissions, scoped storage, MediaStore, PhotoKit, thumbnails, and a lot of edge cases.

I didn't want to solve those problems separately in every Flet project.

So I built flet-media-library.

It is an open-source Flet service extension that gives Flet applications a Python API for working with the device media library on Android and iOS.

The current release is v1.1.2.


The problem I was trying to solve

Imagine you're building a Flet application that downloads videos.

The download itself isn't particularly interesting:

URL
 ↓
download
 ↓
temporary file
Enter fullscreen mode Exit fullscreen mode

The real problem starts afterward.

Where should that video go?

Users expect it to appear in something like:

Movies/MyApp/
Enter fullscreen mode Exit fullscreen mode

and show up in the system gallery.

The same problem appears with other applications.

A camera app needs to save captured media.

A voice recorder needs to save recordings to Music.

A media editor needs to read assets and save the output.

A gallery app needs to query thousands of files efficiently.

A file manager may need to rename or move media.

And all of this has to work within the platform's permission and storage model.

That's the part I wanted to make reusable.


What is flet-media-library?

flet-media-library is a Flet Service that exposes media-library functionality directly to Python.

A basic setup looks like this:

import flet as ft

from flet_media_library import MediaLibrary


async def main(page: ft.Page):
    media = MediaLibrary()

    page.services.append(media)
    page.update()

    status = await media.request_permissions([
        "image",
        "video",
    ])

    if not status.all_granted and not status.any_limited:
        page.add(ft.Text("Media permission was not granted"))
        return

    result = await media.get_assets(
        media_type="image",
        limit=20,
        sort_by="date_added",
        sort_order="desc",
    )

    for asset in result.items:
        print(asset.display_name)


if __name__ == "__main__":
    ft.run(main)
Enter fullscreen mode Exit fullscreen mode

The important part for me is that the application code stays Python.

You don't have to build your application around photo_manager types or write platform-specific media logic throughout your UI code.


What it currently supports

The library covers most of the things you'd expect from a reusable media service.

Permissions

You can check and request permissions for images, videos, and audio independently.

status = await media.check_permissions([
    "image",
    "video",
    "audio",
])
Enter fullscreen mode Exit fullscreen mode

The result contains per-type states such as:

granted
limited
denied
denied_forever
restricted
unknown
Enter fullscreen mode Exit fullscreen mode

That matters on newer Android versions because media permissions are no longer just one big switch.

The project also exposes the limited-access picker and a helper for opening the application settings.


Querying albums and assets

You can query device albums:

albums = await media.get_albums(media_type="image")

for album in albums:
    print(album.name, album.asset_count)
Enter fullscreen mode Exit fullscreen mode

And then query assets with pagination and sorting:

page = await media.get_assets(
    media_type="video",
    limit=30,
    offset=0,
    sort_by="date_added",
    sort_order="desc",
)
Enter fullscreen mode Exit fullscreen mode

The query API supports:

  • image, video, audio, or all media
  • album filtering
  • pagination
  • date added
  • date modified
  • filename
  • size
  • duration
  • MIME filtering where supported
  • date-range filtering

That makes it practical for a real gallery rather than just a small demo with ten files.


Thumbnails became more important than I expected

One of the first things you notice when building a media gallery is that loading original files everywhere is a bad idea.

The package provides a normal thumbnail API:

thumb = await media.get_thumbnail(
    asset.id,
    width=150,
    height=150,
    quality=85,
)
Enter fullscreen mode Exit fullscreen mode

That works nicely with Flet:

ft.Image(
    src_base64=thumb,
    width=150,
    height=150,
)
Enter fullscreen mode Exit fullscreen mode

But I also added another API:

path = await media.get_thumbnail_path(
    asset.id,
    width=160,
    height=160,
)
Enter fullscreen mode Exit fullscreen mode

This writes the thumbnail into a local cache and returns the path.

You can then use:

ft.Image(
    src=path,
    width=160,
    height=160,
)
Enter fullscreen mode Exit fullscreen mode

The reason for having both is pretty simple.

Base64 is convenient.

But when you're scrolling through a large gallery, continuously moving image data through the Python/Flutter boundary isn't something I wanted to rely on.

So the path-based API is there specifically for gallery-scale rendering.


Saving media to the system gallery

This is probably the part most Flet media applications will care about.

For images:

await media.save_image(
    "/path/to/photo.jpg",
    file_name="photo.jpg",
    relative_path="Pictures/MyApp",
)
Enter fullscreen mode Exit fullscreen mode

For videos:

await media.save_video(
    "/path/to/video.mp4",
    file_name="video.mp4",
    relative_path="Movies/MyApp",
)
Enter fullscreen mode Exit fullscreen mode

The point is that these aren't just application-private files anymore.

They are inserted into the platform's media library.

On Android, the public destinations include locations such as:

DCIM/
Pictures/
Movies/
Music/
Enter fullscreen mode Exit fullscreen mode

depending on the type of media and the API being used.

This is particularly useful for downloaders, camera apps, recorders, editors, and other applications that create media for the user.


Audio exposed an interesting platform gap

While working on this, I ran into one of the reasons native extensions are sometimes unavoidable.

The upstream Flutter media-library functionality I was using didn't provide everything I needed for Android audio saving.

So I added a custom Android Kotlin implementation using MediaStore.

That gives the Python API:

await media.save_audio(
    "/path/to/recording.wav",
    file_name="voice_note.wav",
    relative_path="Music/FletMediaLibrary",
)
Enter fullscreen mode Exit fullscreen mode

This is currently an Android-only capability.

I prefer exposing that limitation explicitly rather than pretending Android and iOS have exactly the same capabilities.

That's also why the library has capability discovery.


Platform capability discovery

You can ask the library what the current platform supports:

capabilities = await media.get_capabilities()

print(capabilities)
Enter fullscreen mode Exit fullscreen mode

The response can tell you things such as:

platform
supports_audio_save
supports_move
supports_rename
supports_copy
supports_mime_filter
supports_limited_access
supports_thumbnail_path
supports_change_notify
android_sdk
Enter fullscreen mode Exit fullscreen mode

So instead of scattering platform checks everywhere, an application can do something like:

if capabilities["supports_rename"]:
    # show rename action
Enter fullscreen mode Exit fullscreen mode

For cross-platform Flet applications, that can make the UI much cleaner.


Rename and move are where mobile storage gets interesting

Reading media is one thing.

Changing it is another.

The library provides:

await media.rename_asset(
    asset_id,
    "new_name.mp4",
)
Enter fullscreen mode Exit fullscreen mode

and:

await media.move_asset(
    asset_id,
    "Movies/Archive",
)
Enter fullscreen mode Exit fullscreen mode

On Android, these operations are implemented through native media APIs and have to respect the rules introduced by scoped storage.

Depending on the Android version and operation, the system may ask the user to confirm the modification.

That's expected.

One thing I learned while building this is that it's better to work with the platform's security model than to try to work around it.

That's also why the project intentionally avoids requiring broad:

MANAGE_EXTERNAL_STORAGE
Enter fullscreen mode Exit fullscreen mode

access for normal media-library operations.


Live media changes

Another feature that became useful in the demo is change notifications.

Suppose your Flet gallery is open and another application creates a photo.

Your application shouldn't necessarily have to be manually restarted or rebuilt to know something changed.

You can subscribe to changes:

def on_change(event):
    print(
        event.change_type,
        event.asset_id,
        event.media_type,
    )


media.on_change = on_change

await media.start_change_notify()
Enter fullscreen mode Exit fullscreen mode

The event can represent:

added
modified
removed
other
Enter fullscreen mode Exit fullscreen mode

The other case is intentional.

The underlying platform doesn't always give you perfectly precise change information, so I'd rather expose that honestly than invent precision that isn't really there.


The implementation

The project is packaged as an actual Flet extension.

The Python side exposes the public API.

The Flutter side implements the service.

photo_manager is used as the primary media-library backend for common functionality such as querying, thumbnails, saving and deletion.

Then Android Kotlin fills specific gaps where native handling is needed.

So the important boundary is roughly:

Flet / Python application
        ↓
MediaLibrary service
        ↓
Flutter extension
        ↓
photo_manager
        ↓
Android native MediaStore where required
Enter fullscreen mode Exit fullscreen mode

The Python package owns its own models such as:

MediaAsset
MediaAlbum
MediaAssetPage
MediaPermissionStatus
MediaChangeEvent
Enter fullscreen mode Exit fullscreen mode

That means the application isn't tightly coupled to the underlying Flutter package.

I think that's an important detail when building an extension that other projects may depend on.


Why not just use filesystem access?

That was one of the questions I kept coming back to during development.

A phone isn't just a normal desktop filesystem.

Modern Android deliberately separates application storage and shared media.

iOS also has its own photo-library model.

So a solution based on:

open(...)
os.listdir(...)
shutil.move(...)
Enter fullscreen mode Exit fullscreen mode

doesn't automatically give you correct integration with the user's gallery.

You also have to think about:

  • media indexing
  • permissions
  • scoped storage
  • limited access
  • user confirmation
  • public media directories
  • platform-specific identifiers
  • external media changes

That's why I built this around the media-library APIs themselves, rather than treating the problem as generic filesystem access.


The demo application

I also didn't want the repository to contain only a library with a few isolated code snippets.

There is a full demo application in:

examples/media_library_demo/
Enter fullscreen mode Exit fullscreen mode

It acts as both a showcase and a test harness.

It includes:

Gallery

Browse photos, videos, and audio.

Albums

Load actual device albums.

Thumbnails

Render image and video thumbnails.

Playback

Play video and audio from inside the application.

Camera

Capture photos and record videos.

Microphone

Record audio and save it to Android's Music collection.

File management

Move, rename, copy, and delete media.

Live updates

Listen for media-library changes.

Smoke testing

Run a sequence of checks against the core APIs on a real device.

That last part is especially useful for native extensions.

There are a lot of things that can work in a desktop development environment and then behave differently on a real Android device.


A real-world use case: a Flet video downloader

This is probably the easiest way to explain why I wanted this library to exist.

Suppose we're building a Flet video downloader.

The downloader creates:

/cache/download.mp4
Enter fullscreen mode Exit fullscreen mode

Then:

await media.save_video(
    "/cache/download.mp4",
    file_name="download.mp4",
    relative_path="Movies/MyDownloader",
)
Enter fullscreen mode Exit fullscreen mode

Now the file is part of the device media library.

The same service can then be used to query and display it.

So the application doesn't need one system for downloading and another completely separate system for managing gallery content.

The workflow becomes:

Download
   ↓
Temporary file
   ↓
MediaLibrary
   ↓
MediaStore / PhotoKit
   ↓
System media library
Enter fullscreen mode Exit fullscreen mode

That's the kind of reusable building block I wanted.


Another use case: camera and recording apps

The same thing works for camera applications.

For example:

Camera capture
      ↓
temporary file
      ↓
save_image / save_video
      ↓
system gallery
Enter fullscreen mode Exit fullscreen mode

And for an Android voice recorder:

microphone
      ↓
WAV recording
      ↓
save_audio
      ↓
Music/FletMediaLibrary
Enter fullscreen mode Exit fullscreen mode

The included demo application exercises both flows.


Error handling

Native APIs fail.

Permissions can be denied.

Files can disappear.

Users can cancel system dialogs.

Some operations simply aren't available on a specific platform.

So instead of returning a giant collection of ambiguous values, the package defines typed exceptions such as:

PermissionRequiredError
PermissionDeniedError
UnsupportedError
AssetNotFoundError
AlbumNotFoundError
InvalidArgumentError
PlatformError
Enter fullscreen mode Exit fullscreen mode

For example:

from flet_media_library import UnsupportedError

try:
    await media.rename_asset(asset_id, "new_name.mp4")
except UnsupportedError:
    print("Rename is not supported here")
Enter fullscreen mode Exit fullscreen mode

This is a small detail, but it makes the API much easier to integrate into an actual application.


What I learned while building it

The interesting part of this project wasn't writing a few methods around a media plugin.

The real work was dealing with the differences between:

Python
Flutter
Android
iOS
MediaStore
PhotoKit
Flet's extension system
Enter fullscreen mode Exit fullscreen mode

and keeping those differences from leaking into every part of the application.

A few things became especially clear during development.

Native capability gaps are normal

A cross-platform Flutter package can cover most of the common functionality and still leave important platform-specific gaps.

That's where an extension can add value.

Mobile storage is not a normal filesystem problem

Especially on newer Android versions.

Trying to treat everything as unrestricted file access leads to the wrong abstraction.

Performance matters in media applications

Generating thumbnails is easy.

Generating thousands of thumbnails efficiently is a different problem.

That's why the path-based thumbnail API was worth adding.

Platform differences should be explicit

Rather than hiding limitations, I prefer capability detection and clear exceptions.

Testing on a real device matters

Permissions, MediaStore behavior, recording paths, confirmation dialogs, and filesystem locations are all areas where a real device tells you much more than a desktop test.


Installing it

The package is available on PyPI:

pip install flet-media-library
Enter fullscreen mode Exit fullscreen mode

Or with uv:

uv add flet-media-library
Enter fullscreen mode Exit fullscreen mode

Then add it to your project:

[project]
dependencies = [
    "flet>=1.0.0",
    "flet-media-library>=1.1.2",
]
Enter fullscreen mode Exit fullscreen mode

The package includes the Flutter extension so the native integration can be included when building the Flet application.


Project status

The current release is:

flet-media-library 1.1.2
Enter fullscreen mode Exit fullscreen mode

The project is MIT licensed and currently targets Android and iOS.

Recent work has included:

  • per-media-type permissions
  • limited-access handling
  • cached thumbnail paths
  • capability discovery
  • date-range filtering
  • Android audio saving
  • Android move and rename support
  • improved native concurrency handling
  • demo recording fixes
  • automated Android demo APK builds
  • a larger integration-test/demo harness

There is still work to do.

The project roadmap includes broader device verification, more native integration tests, Dart-side tests around the media backend, and an optional original-file-path API for playback-related scenarios.


Final thoughts

I built flet-media-library because I kept running into the same problem:

Flet makes application development surprisingly productive, but serious mobile applications eventually need native platform capabilities.

Media handling is one of those areas.

Instead of every Flet developer separately figuring out permissions, MediaStore, PhotoKit, thumbnails, public storage, media mutations, and change notifications, I'd rather have those capabilities available as a reusable service.

That's what this project is trying to be.

Not a replacement for Flet.

Not a new UI framework.

Just a practical media-library layer that lets you keep writing your application in Python while still using the native media APIs underneath.

And because it's open source, you can inspect the implementation, use it in your own projects, open issues, and contribute improvements.

If you're building a Flet gallery, video downloader, camera app, audio recorder, media editor, or another media-heavy mobile application, this may save you from rebuilding the same native integration yourself.

Top comments (0)