You may have found yourself searching for a flutter plugin on the largest library-sharing platform, pub.dev. Yet, despite your persistence in scrolling through the list, nothing seems to match your search... An idea suddenly crosses your mind...
What if it were YOU who would create that elusive plugin that only your mind is capable of imagining?
In addition to creating a plugin that will be useful for your wonderful applications, get ready to help others in the future 😇
At first glance, creating a Flutter plugin can seem intimidating.
In this article, we will break down this prejudice and show you that creating one is simple 👍.
This tutorial has been written with the aim of teaching you how to easily create a Flutter plugin that will allow you to retrieve battery information 🔋 on iOS (Swift) and Android (Kotlin) only.
Why create a Plugin / Package?
What we are going to create is a library that will allow us to encapsulate a maximum amount of logic and/or communicate directly with the device on the native side.
The main advantage of using a library is to make code reusable, allowing anyone to easily integrate our library into their own project, and thanks to the wonderful world of open source, contribute to its development and maintenance.
To get started, we first need to distinguish between a Plugin and a Package. Both may seem similar, but there are differences...
What the difference between a flutter Package and a plugin?
Let's start with the Package.
A Package contains ONLY Dart code and nothing more.
It doesn't allow for any native calls.
Here are some examples of packages: bloc, mockito, http, lint, and so on.
What is a Plugin?
A Plugin, on the other hand, contains both Dart code and native code. The native code enables communication with the APIs of the device running the application.
This requires a minimum level of knowledge in the following languages:
Platform | Languages |
---|---|
iOS | Objective-C ou Swift |
macOS | Objective-C ou Swift |
Android | Java ou Kotlin |
Windows | C++ |
Linux | C++ |
Web | HTML/CSS/JS |
It's worth noting that you can choose the development language for iOS, Android, and macOS.
For example, if you are more comfortable with Objective-C, it's entirely possible to create a plugin entirely using that language for the iOS and macOS parts. Here are some examples of plugins: image_picker, flutter_native_splash, camerawesome, cached_network_image, etc.
Choosing Between Package or Plugin?
If your library doesn't communicate with the native side, then go with a Package.
If you plan to communicate with the native part of the device, then a Plugin is the best choice.
As a reminder, this tutorial will only cover the creation of a Plugin, as creating a package is relatively similar, with the native part omitted.
Let's Get Started 🤗!
Create a Flutter plugin step by step
- Create the project.
- Implement Dart-side methods.
- Implement tests.
- Implement iOS.
- Implement Android.
- Use the plugin in the example.
Creating the Project 😎
Execute the following command to create a new plugin project for iOS and Android:
flutter create --template=plugin --platforms=android,ios mybatteryplugin
Note ℹ️: By default, Swift is used for iOS and Kotlin for Android. If you want to use another language, you can add the arguments -i objc to use Objective-C and -a java to use Java.
Open the newly created project in your favorite code editor (we will use VSCode here).
Several files are available, and here's a brief explanation of the purpose of the files you will need during development:
- android: Native files for Android (Kotlin code).
- example: A Flutter sample project that is useful for testing your plugin.
- ios: Native files for iOS (Swift code).
- lib: Dart code of the plugin.
- test: Various files related to testing the plugin.
- CHANGELOG.md: A list of all versions that will be released with associated changes. It's a markdown file.
- LICENCE: The license for your plugin. You can use choosealicense.com to find the one that suits you.
- pubspec.yaml: It contains information about your plugin and the various associated native classes.
- README.md: A useful markdown file for presenting your project, which will be displayed on pub.dev's first page.
Implementing Dart-side methods of your plugin
- Open the file
lib/mybatteryplugin_platform_interface.dart
, let's add the definition of your method at the bottom of the markdown file.
abstract class MybatterypluginPlatform extends PlatformInterface {
// [...]
Future<num?> getBatteryLevel() {
throw UnimplementedError('getBatteryLevel() has not been implemented.');
}
}
We will now define the battery retrieval method. To do so, open the file lib/mybatteryplugin.dart. This method will be visible once the plugin is imported into the project.
class Mybatteryplugin {
// [...]
/// récupération du niveau de la batterie
Future<num?> getBatteryLevel() {
return MybatterypluginPlatform.instance.getBatteryLevel();
}
}
Note ℹ️: It is highly recommended to add a comment with three "///" slashes to specify the role of your method.
And finally, we will edit the file lib/mybatteryplugin_method_channel.dart and insert the final implementation that will use the method channel to communicate with the native side.
class MethodChannelMybatteryplugin extends MybatterypluginPlatform {
// [...]
Future<num?> getBatteryLevel() {
return methodChannel.invokeMethod<num?>('getBatteryLevel');
}
}
We're done with the Dart side, wasn't that easy? 😎
Implementing tests for your plugin 🧪
It's always a good idea to test your plugin to make sure it works as expected. A good application is an application that is tested! And so is a good plugin!
To do this, we will use the test folder.
Attention 🚨: By default, the template already includes an example based on the method
getPlatformVersion()
, which will be entirely removed from the project in this tutorial.
Open the file test/mybatteryplugin_test.dart
(it should be highlighted in red) and start by adding, at the top of the file, the mock related to our new method, and then implement the test in the main() method:
class MockMybatterypluginPlatform
with MockPlatformInterfaceMixin
implements MybatterypluginPlatform {
// [...]
// Here, we're overriding the getBatteryLevel() method to return a fixed value of 21,
// which will be tested in the following test.
Future<num?> getBatteryLevel() => Future.value(21);
}
void main() {
// [...]
// We're creating a new test to verify if the previously overridden value is returned correctly.
test('getBatteryLevel', () async {
Mybatteryplugin mybatterypluginPlugin = Mybatteryplugin();
MockMybatterypluginPlatform fakePlatform = MockMybatterypluginPlatform();
MybatterypluginPlatform.instance = fakePlatform;
expect(await mybatterypluginPlugin.getBatteryLevel(), 21);
});
}
Now, open the file test/mybatteryplugin_method_channel_test.dart and modify it to replace the mock and the associated new test:
void main() {
// [...]
setUp(() {
// We will add a new mock related to the Method Channel object
// This will return the value of 21.
channel.setMockMethodCallHandler((MethodCall methodCall) async {
if (method.call == 'getBatteryLevel') {
return 21;
}
});
});
// [...]
test('getBatteryLevel', () async {
expect(await platform.getBatteryLevel(), 21);
});
}
Everything is okay 🤗, you should be able to run the command flutter test successfully!
Implementing iOS side of your plugin 🍏
Now, we need to run the installation of dependencies for the Xcode project using CocoaPods:
cd example/ios/
pod install --repo-update
Note ℹ️: If you don't have CocoaPods installed, you can follow the instructions on the official website: https://cocoapods.org/
From this point on, we will be developing in Swift from Xcode.
However, it's also possible to continue using VSCode or other IDEs.
Now, open the file example/ios/Runner.xcworkspace with Xcode.
open Runner.xcworkspace
Attention 🚨: Make sure to open the .xcworkspace file and not the .xcodeproj file!
Note ℹ️: By opening the example project, you will be able to run your plugin on the sample project and debug the native code from Xcode, which is quite convenient!
In the sidebar, expand "Pods," do the same for "Development Pods," and so on for "mybatteryplugin."
Expand ".." and repeat the process by expanding the first item each time until you reveal 3 files.
Open the file SwiftMybatterypluginPlugin.swift
We will now add a switch statement inside the handle() method to detect a call to our method:
// [...]
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch (call.method) {
case "getBatteryLevel":
// code natif
default:
result(FlutterError(code: "HANDLE_ERROR", message: "method not implemented", details: nil))
break
}
}
Modify the code that will allow us to retrieve the battery level:
// [...]
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch (call.method) {
case "getBatteryLevel":
// il est nécessaire d'activer le suivi de la batterie avant
UIDevice.current.isBatteryMonitoringEnabled = true
// we return the battery level in percent
result(UIDevice.current.batteryLevel * 100)
default:
result(FlutterError(code: "HANDLE_ERROR", message: "method not implemented", details: nil))
break
}
}
The iOS side of our flutter plugin is now complete! 🍏
Implementing Android side of your flutter plugin 🤖
Now, we will implement the Android side of our plugin.
In this section, we will use Android Studio, but it is entirely possible to continue using VSCode.
Open the project example/android/ using Android Studio.
Note ℹ️: By opening the example project, you will be able to run your plugin on the sample project and debug the native code from Android Studio, which is essential!
In the sidebar, expand mybatteryplugin, then java, and finally com.example.mybatteryplugin.
Open the file MybatterypluginPlugin.kt
We will now replace the contents of the onMethodCall() method with a switch statement to detect a specific call to our method:
// [...]
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getBatteryLevel" -> {
// native code
}
else -> {
result.notImplemented()
}
}
}
Let's add the battery manager that will allow us to retrieve the battery level later, and don't forget to import the dependencies.
class MybatterypluginPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var batteryManager: BatteryManager // add this line
// [...]
}
We will now instantiate the battery manager using the context from the plugin in the onAttachedToEngine() method.
// [...]
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "mybatteryplugin")
channel.setMethodCallHandler(this)
// Initialisation du battery manager
batteryManager = flutterPluginBinding.applicationContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
}
// [...]
All that's left is to add the piece of code that will retrieve the battery level from the manager:
// [...]
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getBatteryLevel" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
result.success(batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY));
} else {
result.error("WRONG_VERSION", "android version not supported", "");
}
}
else -> {
result.notImplemented()
}
}
}
// [...]
The Android side of our flutter plugin is now complete! 🤖
Using the plugin in the example 📱
Now that our plugin is complete, we will use it in the example project to test it.
The long-awaited moment is approaching! We can finally test our plugin 🤩
To do this, open the example project located in the example folder with VSCode.
Edit the file lib/main.dart
and import your plugin (usually it's already imported):
import 'package:mybatteryplugin/mybatteryplugin.dart';
In the default stateful widget created, we will instantiate and use the plugin:
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// plugin instanciation
final _mybatterypluginPlugin = Mybatteryplugin();
// this is where we will store the battery level
num? _batteryLevel;
void initState() {
super.initState();
// execute the method to retrieve the battery level
_mybatterypluginPlugin.getBatteryLevel().then((batteryLevel) {
setState(() {
_batteryLevel = batteryLevel;
});
});
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
// we display the battery level
child: _batteryLevel != null
? Text('Niveau de la batterie: $_batteryLevel')
: const CircularProgressIndicator(),
),
),
);
}
}
It's now complete! 🤩
Bonus Scoring your plugin on pub.dev
A plugin/package is deployed on pub.dev. Google highlights libraries that are well maintained with good code quality.
To ensure your new plugin is well-referenced, it's important to pay attention to the score it's assigned. This score is based on a total of 140 "pub points," with the goal, of course, being to obtain as many points as possible.
This score is calculated once the package is published and analyzed by a Google robot.
To optimize our chances, we will use the pana tool, which calculates the score before publication, a great help, isn't it? 👍
Execute the following command to install pana.
dart pub global activate pana
Go to the root of your project, and then run the analysis with pana.
dart pub global run pana .
It's up to you to fix any issues (if there are any) and get the maximum points!
Note ℹ️: pana can modify your code, so be sure to create a git stash before running it.
Publish your plugin on pub.dev
CAs seen earlier, pub.dev is the platform that will allow you to easily distribute your plugin to thousands of developers for free.
Log in with your Google account on pub.dev.
Execute the following command to test your publication before publishing it:
flutter pub publish --dry-run
Now that everything is ready, we can publish our plugin on pub.dev:
flutter pub publish
Conclusion
You have just created your first plugin for iOS and Android, allowing you to interact with the device's native features.
Now it's your turn to come up with the best plugin idea and let your imagination run wild 😁