Blog

MediaStore in Flutter

7 July 2021
mediastore-in-flutter

Using platform channels to provide access to MediaStore API from Dart.

 

Intro

This article will guide you through the process of creating a simple flutter app that allows you to draw on a canvas. Then we will add the ability for this app to save those images to external storage volume using MediaStore API which allows to retrieve and update media files. Files saved this way are discoverable e.g. by system file explorer and will survive in case your app gets uninstalled. MediaStore API was added in API level 1 which makes it available in all Android devices!

To use this API we will need to implement a portion of code both in the host platform (in this case Android) and in Dart using a mechanism called Platform Channels.

mobile

App for drawing a masterpiece on canvas

As a first step, we will create the aforementioned Flutter app, allowing users to create simple drawings. It will contain two button controls, one for picking the colour of the paintbrush and the second for clearing the canvas. Of course, the rest of the space will be devoted to the canvas, which will allow user to draw by pressing and moving a finger.

To track user’s gestures, we will use GestureDetector widget, which provides access to onPanDown/Update/Cancel/End callbacks. Each track traversed by a finger will be recorded using Path class from dart:ui. We will store all tracks with their colours as a list in state class to be able to perform drawing and display the graphic.

To draw to the canvas directly, we will take advantage of CustomPainter, to which all the tracks with their colours will be provided.

Tapping on the change colour button will open the dialogue with ColorPicker from flutter_colorpicker library (using version 0.4.0). It will allow user to change colour while diving deeper into the masterpiece.

As the main topic of this article is related to platform channels, I will not go into further details regarding the implementation but only leave a link to the repo containing the work done until this point: https://github.com/Dartek12/flutter_media_store/tree/step_1.

Our tiny graphical Flutter editor
Our tiny graphical Flutter editor

 

Calling system API from Dart

Whenever you want to use platform-specific code which was not provided with the Flutter framework or third-party plugin provider, you will have to turn to platform channels. It is a system that allows executing Java/Kotlin on Android or Objective-C/Swift on iOS, which relies on passing messages to the hosting platform. Communication is done in an asynchronous manner so that your user interface stays responsive.

To send a message to the hosting platform, you will need to create an instance of MethodChannel in dart code, passing its name to the constructor. In corresponding native code, you will also have to register MethodChannel on Android and FlutterMethodChannel on iOS using the same name.

Then you send a message at the Dart side calling invokeMethod with the method name and the list of arguments of supported types and use registered handler at host platform side to handle the message (including calling native APIs) and produce the result.

That was quite a general description, but no worries — examples are coming!

two people are programming

How are we going to use MediaStore?

The journey from our canvas to saving a file to the MediaStore will consist of few steps:

  • saving canvas contents to a temporary file,
  • passing file path to method channel,
  • handle a file on the Android side by adding it to the MediaStore collection.

That’s it. Let’s get started!

Step 1. Saving canvas contents to temporary media file.

Using PictureRecorder object we will record everything we do to the canvas so that we can save operations result to the file. We assign global key to the container widget holding the canvas to read its size. Having all of that the next step is to save it to temporary PNG file with the help of path_providerpath and uuid which are open source packages:

Future<File> _saveCanvasToTemporaryFile() async {
  final recorder = PictureRecorder();
  final RenderBox box = _containerKey.currentContext!.findRenderObject() as RenderBox;
  final size = box.size;
  final rect = Rect.fromLTRB(0, 0, size.width, size.height);
  final canvas = Canvas(recorder, rect);
  canvas.drawRect(rect, Paint()..color = Colors.white);
  _drawOnCanvas(canvas, size: size, paint: _paint, paths: _paths);
  final picture = recorder.endRecording();
  final image = await picture.toImage(size.width.toInt(), size.height.toInt());
  final bytes = await image.toByteData(format: ImageByteFormat.png);
  final identifier = Uuid().v4();
  final fileName = '$identifier.png';
  final directory = await path_provider.getTemporaryDirectory();
  final filePath = path.join(directory.path, fileName);
  return await File(filePath).writeAsBytes(bytes!.buffer.asUint8List());
}

Step 2. Passing file path to method channel.

Let’s create Dart class for encapsulating communication with the host platform containg single method for adding item with file path and name.

class MediaStore {
  static const _channel = MethodChannel('flutter_media_store');

  Future<void> addItem({required File file, required String name}) async {
    await _channel.invokeMethod('addItem', {'path': file.path, 'name': name});
  }
}

It was that easy!

Step 3. Handling file at Android side.

Now comes the toughest part. We will add handlers at Android side of the app. We need to override configureFlutterEngine method in our host activity in order to register method channel:

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(
        flutterEngine.dartExecutor.binaryMessenger,
        "flutter_media_store"
    ).setMethodCallHandler { call, result ->
        when (call.method) {
            "addItem" -> {
                addItem(call.argument("path")!!, call.argument("name")!!)
                result.success(null)
            }
        }
    }
}

Now we are ready to implement addItem method. We will use content resolver object to insert content values into the appropriate media store volume, which in result returns URI.

Then we will open output stream to that URI so that we can write contents to it (which will be copied from our temporaral storage created in Flutter).

private fun addItem(path: String, name: String) {
    val extension = MimeTypeMap.getFileExtensionFromUrl(path)
    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)!!

    val collection = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, name)
        put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + getString(R.string.app_name))
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
    }

    val resolver = applicationContext.contentResolver
    val uri = resolver.insert(collection, values)!!

    try {
        resolver.openOutputStream(uri).use { os ->
            File(path).inputStream().use { it.copyTo(os!!) }
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.clear()
            values.put(MediaStore.Images.Media.IS_PENDING, 0)
            resolver.update(uri, values, null, null)
        }
    } catch (ex: IOException) {
        Log.e("MediaStore", ex.message, ex)
    }
}

Beware: If this is run on Android 9.0 or a lower device, then you will have to request permissions before saving a file to the MediaStore!

Last step

Finally we can connect everything. Add button control to the top bar which will invoke previously written code with an example implementation of:

void _saveImage() async {
  File tempFile = await _saveCanvasToTemporaryFile();
  await _saveFileToMediaStore(tempFile);
  await tempFile.delete();
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('File saved!')));
}

Results

Now we can play a bit with our app and use save button to see if it’s working properly.

Our masterpiece
Our masterpiece

 

Let’s check the File Explorer and… Success!
Let’s check the File Explorer and… Success!

Success! We managed to draw a masterpiece and saved it in MediaStore!

 

Summary

In this article, we have created a simple Flutter app with extraordinary capabilities. We have learnt how to draw to the canvas, save it to the file and then insert it into MediaStore using the power of platform channels.

As one could notice, for now our platform channel contains public interface for inserting only images and cannot save other types of media. That’s okay for us because it was our intent. But MediaStore is capable of adding media data like video or audio. We can pretty easily extend existing code by adding public interface for inserting those types of media and handling them at Android side to be able to reuse this piece of software in other Flutter apps.

Although we managed all of it by ourselves this time, that does not have to be the case. You can always use pigeon package, which allows you to generate automatically the code responsible for interaction between Flutter and Host Platform. And if you are interested in the internals and possible performance improvements of platform channels, I strongly recommend reading this great article: https://medium.com/flutter/improving-platform-channel-performance-in-flutter-e5b4e5df04af.

The final code can be found here: https://github.com/Dartek12/flutter_media_store

Share

Bartosz Biernacki
Bartosz Biernacki
Mobile developer at Evertop. Constant learner and home-bird.
Read my other articles

logo stopka
Copyright ©2021, Evertop Sp. z o.o. | Privacy policy
scroll to top