Platform-Specific Code With Flutter Method Channel: Getting Started

Learn how to communicate with some platform-specific code with Flutter method channels and extend the functionality of the Flutter application. By Wilberforce Uwadiegwu.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Fetching all Images From Android

Still in MainActivity, declare another function that will execute the query and return the resultant cursor:

private fun getCursor(limit: Int): Cursor? {
    //1
    val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    val projection = arrayOf(MediaStore.Images.Media._ID)

    //2
    return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
        val sort = "${MediaStore.Images.ImageColumns.DATE_MODIFIED} DESC LIMIT $limit"
        contentResolver.query(uri, projection, null, null, sort)
    } else {
        //3
        val args = Bundle().apply {
            putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
            putStringArray(
                ContentResolver.QUERY_ARG_SORT_COLUMNS,
                arrayOf(MediaStore.Images.ImageColumns.DATE_MODIFIED)
            )
            putInt(
                ContentResolver.QUERY_ARG_SORT_DIRECTION,
                ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
            )
        }
        contentResolver.query(uri, projection, args, null)
    }
}

Here's what the code above does:

  1. Declares the uri and projection to get image id column from external storage.
  2. Executes the query API for devices having SDK versions of Android earlier than Android 11.
  3. Executes the query API for devices having an SDK version of Android 11 or higher.

Finally, replace the dummy code in getPhotos() with:

if (queryLimit == 0 || !hasStoragePermission()) return

lifecycleScope.launch(Dispatchers.IO) {
    val ids = mutableListOf<String>()
    val cursor = getCursor(queryLimit)
    cursor?.use {
        while (cursor.moveToNext()) {
            val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val long = cursor.getLong(columnIndex)
            ids.add(long.toString())
        }
    }
    methodResult?.success(ids)
}

While Coroutines was used to execute the query on a background thread, the cursor was iterated over to get the id of each image.

Reading Image Bytes on Android

To read the image bytes for a given id, you'll first get the Uri for the image. Then, you'll request the bytes with Glide, an Android image-loading library.

Write the getImageBytes function above getCursor():

private fun getImageBytes(uri: Uri?, width: Int, height: Int, onComplete: (ByteArray) -> Unit) {
    lifecycleScope.launch(Dispatchers.IO) {
        try {
            val r = Glide.with(this@MainActivity)
                .`as`(ByteArray::class.java)
                .load(uri)
                .submit(width, height).get()
            onComplete(r)
        } catch (t: Throwable) {
            onComplete(byteArrayOf())
        }
    }
}

The instructions above load the image with the uri and invoke onComplete() with the resultant bytes.

Finally, replace the dummy code in fetchImage():

// 1
val id = (args["id"] as String).toLong()
val width = (args["width"] as Double).toInt()
val height = (args["height"] as Double).toInt()

// 2
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
getImageBytes(uri, width, height) {
    result.success(it)
}

Here's what this code does:

  1. Reads the image properties from the arguments passed from the Dart end.
  2. Generates a Uri with the ID and loads the image for that Uri.

Run the Flutter project on Android, and you should have a similar experience:

Images of dogs appearing and being selected

Consuming Method Calls From Host Platforms in Flutter

Host platforms can invoke a method on Flutter, and Flutter can listen for incoming method invocations and parse the method names and arguments, just like you did for Swift and Kotlin above. This is another way in which the Event Channel differs from the Method Channel, because the events in the Event Channel are unidirectional.

Android can send the method call to Flutter like this:

val param = mapOf(Pair("param1", "value1"), Pair("param2", "value2"))
methodChannel.invokeMethod("doSomething", param)

Swift on iOS can send it like this:

var param = ["param1": "value1", "param2": "value2"]
methodChannel.invokeMethod("doSomething", arguments: param)

In both examples above, invokeMethod() supports a third argument, which is the result Flutter returns.

methodChannel.setMethodCallHandler((call) async {
  switch (call.method) {
    case 'doSomething':
      return doSomething(call.arguments);
    default:
      throw PlatformException(code: '1', message: 'Not Implemented');
  }
});

Congrats on completing this tutorial!

Where to Go From Here?

The completed project contains the full code used in this tutorial. It's named final in the zipped file you downloaded earlier. You can still download it by clicking Download Materials at the top or bottom of this tutorial.

In this tutorial, you learned how to communicate between Flutter and the host platform via the Method Channel. To improve your knowledge, you can also implement the camera and video features. When the user taps the camera icon, call up the host platform to capture an image and return it to Flutter. The same goes for the video icon, but capture and return video data instead.

Check out the official doc on Writing custom platform-specific code and video on Packages and Plugins to learn how to develop a plugin that uses platform channel to talk to the pressure sensor on Android and iOS devices. And of course, if you have any questions or comments, please join the forum discussion below!