DEV Community

Edo Lubis for Tentang Anak Tech Team

Posted on • Updated on

Creating your own packages and plugins in flutter

When building applications with Flutter, Developers often rely on packages/plugins from pub.dev for their projects, whether developed by Google's team or by others. Utilizing these packages/plugins is highly beneficial as it can faster the development process, saving developers time by avoiding the need to create everything from scratch.

but consider this scenario…

Imagine you have developed a fantastic widget that enhances the functionality of your application. Wouldn't it be better to use that code in other of your projects without rewriting it?

or Imagine, where the package/plugin you heavily rely on becomes discontinued or unsupported. You're left in a dilemma, uncertain about how to proceed with your project.

In these scenario, the solution is create your own Flutter packages.

Creating Packages

Let's illustrate this with an example. Suppose we want to create a package to show a beautiful modal and we call the package name "beautiful_modal".

1. Create a new Flutter project using Android Studio and select the project type as a package.
Create a new Flutter project

2. After the project is successfully created, the folder structure will look like this.
folder structure

3. Code your package according to your needs. For example, for the "beautiful_modal" package, the code will look like this:

library beautiful_modal;

import 'package:flutter/material.dart';

void showBeautifulDialog(BuildContext context, String title, String subtitle) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return BeautifulModal(
        subTitle: subtitle,
        title: title,
      );
    },
  );
}

class BeautifulModal extends StatelessWidget {
  final String title;
  final String subTitle;

  const BeautifulModal({Key? key, required this.title, required this.subTitle})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.0),
      ),
      elevation: 0,
      backgroundColor: Colors.transparent,
      child: contentBox(context),
    );
  }

  Widget contentBox(BuildContext context) {
    return Stack(
      children: <Widget>[
        Container(
          padding:
              const EdgeInsets.only(left: 20, top: 50, right: 20, bottom: 20),
          margin: const EdgeInsets.only(top: 20),
          decoration: BoxDecoration(
            shape: BoxShape.rectangle,
            color: Colors.white,
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              const SizedBox(
                height: 100,
                width: 100,
                child: FlutterLogo(),
              ),
              const SizedBox(height: 8),
              Text(
                title,
                style:
                    const TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 8),
              Text(
                subTitle,
                style: const TextStyle(fontSize: 14),
              ),
              const SizedBox(height: 8),
              Align(
                alignment: Alignment.bottomRight,
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                  child: const Text(
                    'Close',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Run and test your package.
Test your package

Creating Plugins

You might wonder what a plugin is and how it differs from a package. Simply put, a plugin is a type of package designed to act as a bridge between Flutter's Dart code and platform-specific APIs on native operating systems like iOS and Android. So, all plugins are packages, but not all packages are plugins.

For example, let's say we want to show a native modal from iOS or Android.
Native Modal

The modal above is not created using Flutter code; instead, it's the default modal provided by the operating system (OS).

1. Create a new Flutter project using Android Studio and select the project type as a plugin.
Create a new Flutter project

2. After the project is successfully created, the folder structure will look like this.
folder structure

Dart Code

1. Open the beautiful_native_modal_plaform_interface.dart file and replace the getPlatformVersion function with showNativeModal.

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'beautiful_native_modal_method_channel.dart';

abstract class BeautifulNativeModalPlatform extends PlatformInterface {
  /// Constructs a BeautifulNativeModalPlatform.
  BeautifulNativeModalPlatform() : super(token: _token);

  static final Object _token = Object();

  static BeautifulNativeModalPlatform _instance = MethodChannelBeautifulNativeModal();

  /// The default instance of [BeautifulNativeModalPlatform] to use.
  ///
  /// Defaults to [MethodChannelBeautifulNativeModal].
  static BeautifulNativeModalPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [BeautifulNativeModalPlatform] when
  /// they register themselves.
  static set instance(BeautifulNativeModalPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<void> showNativeModal(String message) {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Open the beautiful_native_modal_method_channel.dart file and implement the showNativeModal function.

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'beautiful_native_modal_platform_interface.dart';

/// An implementation of [BeautifulNativeModalPlatform] that uses method channels.
class MethodChannelBeautifulNativeModal extends BeautifulNativeModalPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('beautiful_native_modal');

  @override
  Future<void> showNativeModal(String message) async {
    await methodChannel.invokeMethod<String>('showNativeModal', {"message": message});
  }
}
Enter fullscreen mode Exit fullscreen mode

The string showNativeModal in the methodChannel.invokeMethod<String> function represents the name of the method to be executed on the Android or iOS side. Additionally, {"message": message} serves as the parameter being sent along with this method call.

3. Add the showNativeModal function to the beautiful_native_modal.dart file.

import 'beautiful_native_modal_platform_interface.dart';

class BeautifulNativeModal {
  Future<void> showNativeModal(String message) {
    return BeautifulNativeModalPlatform.instance.showNativeModal(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Plugins in Android

1. Open the BeautifulNativeModalPlugin.kt file in the android folder and add the function to call the dialog from Android.

private fun showNativeModal(message: String?) {
        val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
        builder.setTitle("Modal")
        builder.setMessage(message)
        builder.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
        val dialog: AlertDialog = builder.create()
        dialog.show()
    }
Enter fullscreen mode Exit fullscreen mode

2. Call the showNativeModal function from the onMethodCall method.

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "showNativeModal") {
            val message = call.argument<String?>("message")
            showNativeModal(message)
            result.success(null)
        } else {
            result.notImplemented()
        }
    }
Enter fullscreen mode Exit fullscreen mode

3. Then, the full code will look like this:

package com.example.beautiful_native_modal

import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/** BeautifulNativeModalPlugin */
class BeautifulNativeModalPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
    /// The MethodChannel that will the communication between Flutter and native Android
    ///
    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
    /// when the Flutter Engine is detached from the Activity
    private lateinit var channel: MethodChannel
    private var activity: Activity? = null

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "beautiful_native_modal")
        channel.setMethodCallHandler(this)
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivityForConfigChanges() {
        activity = null
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
        activity = binding.activity
    }

    override fun onDetachedFromActivity() {
        activity = null
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "showNativeModal") {
            val message = call.argument<String?>("message")
            showNativeModal(message)
            result.success(null)
        } else {
            result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }

    private fun showNativeModal(message: String?) {
        val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
        builder.setTitle("Modal")
        builder.setMessage(message)
        builder.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
        val dialog: AlertDialog = builder.create()
        dialog.show()
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Run and test your code
Android Dialog

Plugins in iOS

1. Open the BeautifulNativeModalPlugin.swift file in the ios folder and add the function to display the modal in native iOS.

private func showNativeModal(message: String) {
        let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
        let defaultAction = UIAlertAction(title: "OK", style: .default) { _ in }
        alert.addAction(defaultAction)

        guard let controller = controller else {
            return
        }

        controller.present(alert, animated: true)
      }
Enter fullscreen mode Exit fullscreen mode

2. Call the showNativeModal function from the handle method.

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
   switch call.method {
          case "showNativeModal":
          if let args = call.arguments as? [String: Any],
             let message = args["message"] as? String {
              showNativeModal(message: message)
              result(nil)
          } else {
              result(FlutterError(code: "INVALID_ARGUMENT",
                                   message: "Missing or invalid argument",
                                   details: nil))
          }
          default:
              result(FlutterMethodNotImplemented)
          }
}
Enter fullscreen mode Exit fullscreen mode

3. Then the full code will look like this

import Flutter
import UIKit

public class SwiftBeautifulNativeModalPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "beautiful_native_modal", binaryMessenger: registrar.messenger())
    let instance = SwiftBeautifulNativeModalPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
     switch call.method {
            case "showNativeModal":
            if let args = call.arguments as? [String: Any],
               let message = args["message"] as? String {
                showNativeModal(message: message)
                result(nil)
            } else {
                result(FlutterError(code: "INVALID_ARGUMENT",
                                     message: "Missing or invalid argument",
                                     details: nil))
            }
            default:
                result(FlutterMethodNotImplemented)
            }
  }

   private func showNativeModal(message: String) {
        let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
        let defaultAction = UIAlertAction(title: "OK", style: .default) { _ in }
        alert.addAction(defaultAction)

        guard let controller = controller else {
            return
        }

        controller.present(alert, animated: true)
      }

   private var controller: UIViewController? {
     return UIApplication.shared.keyWindow?.rootViewController
   }
}
Enter fullscreen mode Exit fullscreen mode

4. Run and test your plugin.
Ios Dialog

Publish package/plugin to pub.dev

If you want to share your packages/plugins with the Flutter community, publishing it on pub.dev allows other developers to easily discover and use your package.

1. Create a pub.dev Account
To publish packages on pub.dev, you need to create an account. Visit the pub.dev website and sign in with your Google account.

2. Prepare Your Pubspec
Ensure your pubspec.yaml file contains accurate metadata for your package, including the name, version, description, author, homepage, and dependencies. You can also include tags to help users discover your package.

3. Publishing Your Package
Once your package is ready, Run the command flutter pub publish --dry-run then run flutter pub publish.

For more details on publishing, see the publishing docs on dart.dev.

Conclusion

In conclusion, Flutter packages and plugins are powerful tools. Developers can save time and effort by reusing code across projects. This eliminates the need to rewrite the same functionality from scratch. By utilizing packages from pub.dev or creating custom packages and plugins, developers can enhance productivity, reuse code, and access native features seamlessly across different platforms.

that's it, thankyou

Reference:
Developing packages & plugins
Writing custom platform-specific code

Top comments (0)