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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Platform-Specific Code With Flutter Method Channel: Getting Started
30 mins
- Getting Started
- Understanding the Method Channel
- Setting up Method Channel on iOS With Swift
- Understanding PhotoKit
- Requesting Permissions on iOS
- Fetching all Photos From iOS
- Reading Image Data From iOS
- Setting up the Method Channel on Flutter
- Building a Custom Image Provider in Flutter
- Rendering Images From the Host Device
- Rendering Selected Images
- Setting up Method Channel on Android with Kotlin
- Understanding Android’s Media API
- Requesting User Permissions on Android
- Fetching all Images From Android
- Reading Image Bytes on Android
- Consuming Method Calls From Host Platforms in Flutter
- Where to Go From Here?
Rendering Images From the Host Device
Having implemented a custom ImageProvider
, the next step is using it to load the images.
Kick off this section by opening images_grid.dart in the widgets directory. The build tree currently consists of a horizontal scroll widget containing a column of icons. You’ll add a SliverGrid
to this horizontal scroll widget. But first, you need to determine the logical pixel of each of the images.
Add this declaration just above the return
statement in build()
:
final imageSize = MediaQuery.of(context).size.width * 0.6;
This means the width
and height
of each of the images will be 60% of the device width.
Next, import AdaptiveImage
into images_grid.dart, like so:
import '../common/adaptive_image.dart';
Below the ImagesGridWidget
class, declare another class named _ImageWidget
, like so:
class _ImageWidget extends StatelessWidget {
final String id;
final VoidCallback onTap;
final double size;
const _ImageWidget({
Key? key,
required this.onTap,
required this.id,
required this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
}
}
This class will contain the build instructions for individual image widgets. Add the following code inside its build()
:
return Stack(
children: [
Positioned.fill(
child: Image(
key: ValueKey(id),
fit: BoxFit.cover,
image: AdaptiveImage(
id: id,
width: size,
height: size,
),
),
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(onTap: onTap),
),
)
],
);
The build instructions above will display the image using the AdaptiveImage you wrote earlier.
Next, replace the TODO
text in the build()
of ImagesGridWidget
with this:
SliverGrid.count(
crossAxisCount: 2,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
children: images.map((e) {
return _ImageWidget(
onTap: () => onImageTap(e),
id: e,
size: imageSize,
);
}).toList(),
),
The instructions above should be familiar; you’re basically mapping the images
to _ImageWidget
.
Now, run the app, tap the gallery icon and you should see something similar to this:
Rendering Selected Images
The final stage for this section allows the user to add and remove images in the input area.
Open images_list.dart in the widgets directory. Then, add this import statement:
import '../common/adaptive_image.dart';
You’ll pass a ListView
as the child of the SizedBox
in build()
later, but first, declare the class for the single image widget below ImagesListWidget
, like so:
class _ImageWidget extends StatelessWidget {
final String id;
final VoidCallback onRemoved;
final double size;
const _ImageWidget({
Key? key,
required this.onRemoved,
required this.id,
required this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final isIos = Theme.of(context).platform == TargetPlatform.iOS;
final imageSize = size - 15;
return Padding(
padding: const EdgeInsets.only(left: 5, right: 10),
child: SizedBox(
height: size,
width: size,
child: Stack(
clipBehavior: Clip.none,
children: [
],
),
),
);
}
}
The Stack
widget will have two widgets: the image widget and the cancel icon on top of it. So, pass the following widgets as the children to the Stack
widget in build()
of _ImageWidget
:
Positioned(
top: 15,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(
width: imageSize,
height: imageSize,
fit: BoxFit.cover,
image: AdaptiveImage(
id: id,
width: imageSize,
height: imageSize,
),
),
),
),
Positioned(
top: -10,
right: -10,
child: IconButton(
onPressed: onRemoved,
icon: Icon(
isIos ? CupertinoIcons.multiply_circle_fill : Icons.cancel,
),
),
)
You’re using negative offsets for the top
and right
arguments of the cancel icon to ensure it’s positioned at the edge of the item widget.
Next, supply the child
argument to the SizedBox
in the build()
of ImagesListWidget
:
child: ListView.builder(
controller: controller,
scrollDirection: Axis.horizontal,
itemCount: images.length,
padding: const EdgeInsets.symmetric(horizontal: 10),
itemBuilder: (c, i) {
final id = images.elementAt(i);
return _ImageWidget(
key: ValueKey(id),
onRemoved: () => onRemoved(id),
id: id,
size: itemSize,
);
},
),
Subsequently, open home.dart and update both onImageRemoved()
and onImageTap()
to:
void onImageRemoved(String id) {
setState(() => selectedImages.remove(id));
}
void onImageTap(String id) {
setState(() => selectedImages.add(id));
WidgetsBinding.instance?.addPostFrameCallback((_) {
final pos = selectedImagesController.position.maxScrollExtent;
selectedImagesController.jumpTo(pos);
});
}
One of these functions is called when the state of selectedImages
needs to mutate. After updating selectedImages
, onImageTap()
scrolls the ListView
to the end to ensure the just-added image is visible.
Now, run on iOS, and you should have a similar experience:
Setting up Method Channel on Android with Kotlin
In this section, you’ll replicate the Method Channel setup flow you already did on iOS.
Start by opening the android directory in the starter project with Android Studio. Then, open MainActivity.kt, and add the following fields:
class MainActivity : FlutterFragmentActivity() {
private var methodResult: MethodChannel.Result? = null
private var queryLimit: Int = 0
}
Also, add these import statements to the import section:
import android.Manifest
import android.content.ContentResolver
import android.content.ContentUris
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Next, override configureFlutterEngine()
, set up the Method Channel and listen for method calls. Write this function below the fields you just declared:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor.binaryMessenger
MethodChannel(messenger, "com.raywenderlich.photos_keyboard")
.setMethodCallHandler { call, result ->
when (call.method) {
"getPhotos" -> {
methodResult = result
queryLimit = call.arguments()
getPhotos()
}
"fetchImage" -> fetchImage(call.arguments(), result)
else -> result.notImplemented()
}
}
}
Finally, add these dummy functions below the override above:
private fun getPhotos() {
TODO("Not yet implemented")
}
private fun fetchImage(args: Map<String, Any>, result: MethodChannel.Result) {
TODO("Not yet implemented")
}
Understanding Android’s Media API
Access to local media on Android is policed by the MediaStore API. Android stores data pertaining to each kind of media — image, video, audio, etc. — in an SQLite database, and you make SQL-like queries to retrieve the data. So, throughout this section, you’ll work with the MediaStore
API to query the images on the device and read the image data.
Requesting User Permissions on Android
Android has two major sets of permissions: runtime permissions and install-time permissions. The latter set, such as internet access, is implicitly granted to your app at install time. The former controls access to more sensitive data, and the user needs to grant it explicitly. Reading the images from an Android device is an example of such permission.
Open AndroidManifest.xml in the manifests directory, and add this declaration above the internet permission declaration:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
With this, you’re declaring that your app will read the external storage sometime in its lifecycle.
Back in MainActivity
, declare an activity result launcher field below the previous functions:
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
getPhotos()
} else {
methodResult?.error("0", "Permission denied", "")
}
}
The lambda function is called after the permission dialog closes, and granted
is the result of the permission.
Next, use this permissionLauncher
to request permission. So, declare a function above this field:
private fun hasStoragePermission(): Boolean {
// 1
val permission = Manifest.permission.READ_EXTERNAL_STORAGE
// 2
val state = ContextCompat.checkSelfPermission(this, permission)
if (state == PackageManager.PERMISSION_GRANTED) return true
// 3
permissionLauncher.launch(permission)
return false
}
In this code:
- This is the identifier for the permission you want to request.
- This checks if your app already has the permission and returns
true
if so. - If your app doesn’t have the permission, it requests permission.