Introduction
Developing a custom camera plugin for Flutter gives you full control over native camera APIs, advanced features, and low-latency image capture. In this tutorial, you will build a cross-platform camera plugin using MethodChannel, integrating iOS AVFoundation and Android Camera2. We’ll cover project setup, native code, and the Dart interface for a robust camera plugin.
Plugin Project Setup
Start by generating a federated plugin:
flutter create --template=plugin \
--platforms=android,ios \
flutter_camera_plugin
cd flutter_camera_plugin
This scaffold includes lib/, android/src/, and ios/Classes/. In pubspec.yaml, set plugin paths. Create a camera_interface package if you plan to share API definitions across platforms.
Define a MethodChannel in lib/flutter_camera_plugin.dart:
import 'dart:async';
import 'package:flutter/services.dart';
class FlutterCameraPlugin {
static const MethodChannel _channel =
MethodChannel('flutter_camera_plugin');
static Future<void> initialize() async {
await _channel.invokeMethod('initialize');
}
static Future<String?> takePhoto() async {
return await _channel.invokeMethod<String>('takePhoto');
}
}
This code defines two methods: initialize() to start the native camera session and takePhoto() to capture an image, returning a file path.
iOS Implementation (Swift)
Open ios/Classes/SwiftFlutterCameraPlugin.swift and import AVFoundation. Implement the MethodChannel handlers:
import Flutter
import UIKit
import AVFoundation
public class SwiftFlutterCameraPlugin: NSObject, FlutterPlugin {
var session: AVCaptureSession?
var output: AVCapturePhotoOutput?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "flutter_camera_plugin", binaryMessenger: registrar.messenger())
let instance = SwiftFlutterCameraPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialize":
setupSession()
result(nil)
case "takePhoto":
capturePhoto(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
func setupSession() {
session = AVCaptureSession()
guard let session = session,
let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let input = try? AVCaptureDeviceInput(device: device) else { return }
session.addInput(input)
output = AVCapturePhotoOutput()
session.addOutput(output!)
session.startRunning()
}
func capturePhoto(result: @escaping FlutterResult) {
let settings = AVCapturePhotoSettings()
output?.capturePhoto(with: settings, delegate: PhotoCaptureDelegate { data in
let path = NSTemporaryDirectory().appending("photo.jpg")
try? data.write(to: URL(fileURLWithPath: path))
result(path)
})
}
}
Create PhotoCaptureDelegate in the same folder to handle the capture callbacks. This approach writes a JPEG to a temp file and returns its path.
Android Implementation (Kotlin)
In android/src/main/kotlin/com/example/flutter_camera_plugin/FlutterCameraPlugin.kt, use Camera2 APIs. Register the channel and handle calls:
package com.example.flutter_camera_plugin
import android.content.Context
import android.graphics.ImageFormat
import android.media.ImageReader
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileOutputStream
class FlutterCameraPlugin: FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var imageReader: ImageReader
private lateinit var backgroundHandler: Handler
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
channel = MethodChannel(binding.binaryMessenger, "flutter_camera_plugin")
channel.setMethodCallHandler(this)
startBackgroundThread()
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"initialize" -> initializeCamera()
"takePhoto" -> takePhoto(result)
else -> result.notImplemented()
}
}
private fun startBackgroundThread() {
val thread = HandlerThread("CameraBackground").apply { start() }
backgroundHandler = Handler(thread.looper)
}
private fun initializeCamera() {
imageReader = ImageReader.newInstance(1920, 1080, ImageFormat.JPEG, 1)
}
private fun takePhoto(result: MethodChannel.Result) {
imageReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage()
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining()).also { buffer.get(it) }
val file = File(context.cacheDir, "photo.jpg").apply {
FileOutputStream(this).use { it.write(bytes) }
}
image.close()
result.success(file.absolutePath)
}, backgroundHandler)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null
You’ll need to manage camera devices, capture sessions, and request builders. For brevity the snippet shows the core: capturing JPEG bytes and returning a path.
Dart Usage in Your App
Install your plugin locally by referencing its path in the app’s pubspec.yaml. In your Flutter code:
import 'package:flutter/material.dart';
import 'package:flutter_camera_plugin/flutter_camera_plugin.dart';
void main() => runApp(CameraApp());
class CameraApp extends StatefulWidget {
@override
_CameraAppState createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> {
String? _imagePath;
@override
void initState() {
super.initState();
FlutterCameraPlugin.initialize();
}
Future<void> _capture() async {
final path = await FlutterCameraPlugin.takePhoto();
setState(() => _imagePath = path);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Custom Camera Plugin')),
body: Column(
children: [
if (_imagePath != null) Image.file(File(_imagePath!)),
ElevatedButton(onPressed: _capture, child: Text('Take Photo'))
],
),
),
);
}
}
This snippet demonstrates how your camera plugin exposes native functionality through a consistent Dart API. You can extend it with video recording, flash control, and front-camera switching.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Building a native camera plugin for iOS and Android unlocks advanced imaging features and performance optimizations unavailable in high-level packages. By leveraging MethodChannel, AVFoundation, and Camera2, you maintain a single Dart API surface while fully controlling each platform’s hardware.
With this foundation, you can iterate on features like custom filters, real-time image analysis, or ML-powered effects, delivering a rich camera experience tailored to your app’s needs.