Skip to content

Flutter - Introduction to Package

Packages are the foundation of code sharing and organization in Dart and Flutter. They allow developers to bundle reusable libraries, modules, or plugins that can be easily incorporated into multiple projects.

A Dart package is essentially a directory containing Dart code and a crucial pubspec.yaml file that defines its metadata (name, version, dependencies, etc.). It’s like a Dart application but typically doesn’t have a main() entry point unless it’s designed to be an executable.

Standard package structure conventions:

  • lib/: Contains the public code of the package. Code inside lib/ can be imported by other packages/applications.
  • lib/src/: Contains private implementation details. Code inside lib/src/ should generally not be imported directly by consumers; instead, it should be exported from a file directly inside lib/.
  • lib/<package_name>.dart: Often serves as the main entry point file for the package, exporting the primary public APIs.
  • pubspec.yaml: The metadata file defining the package.
  • README.md: Documentation explaining what the package does and how to use it.
  • CHANGELOG.md: Records changes between package versions.
  • LICENSE: Specifies the software license.
  • example/: (Optional but recommended) Contains a small example application demonstrating how to use the package.

To use code from a package named my_package:

// Import the main library file
import 'package:my_package/my_package.dart';
// Or import a specific file within lib/
import 'package:my_package/utils/helpers.dart';

Packages can be broadly categorized based on their dependencies and purpose:

Contain only pure Dart code, without Flutter dependencies. They can be used in any Dart project (Flutter apps, command-line tools, server-side applications). Examples: http, intl, collection.

Depend on the Flutter framework (sdk: flutter in pubspec.yaml) and contain Dart code specifically for Flutter applications, often providing custom widgets or Flutter-specific utilities. They do not contain platform-specific (Android/iOS) native code. Examples: provider, cached_network_image, fl_chart.

A special type of Flutter package that includes both Dart code and platform-specific native code (Kotlin/Java for Android, Swift/Objective-C for iOS). Plugins are used to access platform features not available through Dart alone (e.g., camera, GPS, device sensors, native SDKs). They use Platform Channels internally. Examples: url_launcher, camera, shared_preferences, google_maps_flutter.

Integrating third-party packages into your Flutter project is straightforward:

  1. Find Packages: Discover packages on the official repository: pub.dev.
  2. Add Dependency: Open your project’s pubspec.yaml file and add the package under the dependencies: section, specifying the package name and desired version constraint (e.g., ^1.2.3 allows versions >= 1.2.3 and < 2.0.0).
dependencies:
flutter:
sdk: flutter
# Add packages here
http: ^1.1.0
provider: ^6.1.1
intl: ^0.18.1
  1. Install Package: Run flutter pub get in your terminal from the project root, or use the ‘Pub get’ action provided by your IDE (Android Studio/VS Code often prompt automatically after saving pubspec.yaml). This downloads and links the package.
  2. Import and Use: Import the necessary Dart files from the package in your code and start using its classes and functions.
import 'package:http/http.dart' as http; // Use 'as' for namespace
import 'package:provider/provider.dart';
void main() {
// Use functions/classes from packages
http.get(...);
ChangeNotifierProvider(...);
}

Let’s create the my_browser plugin from the platform channel examples as a reusable Flutter Plugin.

  1. Create Plugin Project: Use the IDE or command line:
    • IDE (Android Studio/VS Code): File > New > New Flutter Project... > Select Flutter Plugin. Fill in the name (my_browser), organization, and desired platforms.
    • Command Line: flutter create --template=plugin --platforms=android,ios -a kotlin -i swift my_browser (Flags specify platforms and preferred native languages).
  2. Implement Dart API (lib/my_browser.dart): Define the public Dart interface for the plugin.
import 'dart:async';
import 'package:flutter/services.dart';
class MyBrowser {
// Use the plugin's name for the channel
static const MethodChannel _channel = MethodChannel('my_browser');
// Optional: Example platform version method often included in template
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
// Define the method to open the browser
Future<bool> openBrowser(String urlString) async {
try {
final bool? result = await _channel.invokeMethod('openBrowser', {
'url': urlString
});
// Return the success status from native code, default to false on error/null
return result ?? false;
} on PlatformException catch (e) {
print('Failed to open browser via plugin: ${e.message}');
return false;
} catch (e) {
print('An unexpected error occurred in openBrowser: $e');
return false;
}
}
}
  1. Implement Android Native Code (android/src/main/kotlin/.../MyBrowserPlugin.kt): Implement the platform channel handler.
package com.example.my_browser // Use your plugin's package name
import androidx.annotation.NonNull
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
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
/** MyBrowserPlugin */
class MyBrowserPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
/// The MethodChannel that will hold the communication between Flutter and native Android
private lateinit var channel : MethodChannel
private var activity: Activity? = null // Hold activity reference
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
// Use binaryMessenger from binding
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "my_browser")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
// MethodCallHandler implementation
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else if (call.method == "openBrowser") {
val url = call.argument<String>("url")
if (url != null && activity != null) {
val success = openBrowserInActivity(url)
result.success(success)
} else if (activity == null) {
result.error("NO_ACTIVITY", "Cannot open browser without an activity.", null)
} else {
result.error("INVALID_ARGUMENT", "URL argument is missing or invalid.", null)
}
} else {
result.notImplemented()
}
}
private fun openBrowserInActivity(url: String): Boolean {
return try {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
activity?.startActivity(intent)
true // Indicate success
} catch (e: Exception) {
Log.e("MyBrowserPlugin", "Error opening browser", e)
false // Indicate failure
}
}
// ActivityAware implementation (to get context/activity)
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
}
}
  1. Implement iOS Native Code (ios/Classes/SwiftMyBrowserPlugin.swift):
import Flutter
import UIKit
public class SwiftMyBrowserPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
// Use registrar.messenger()
let channel = FlutterMethodChannel(name: "my_browser", binaryMessenger: registrar.messenger())
let instance = SwiftMyBrowserPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "getPlatformVersion" {
result("iOS " + UIDevice.current.systemVersion)
} else if call.method == "openBrowser" {
if let args = call.arguments as? [String: Any],
let urlString = args["url"] as? String {
openBrowser(urlString: urlString, result: result)
} else {
result(FlutterError(code: "INVALID_ARGUMENT",
message: "URL argument is missing or not a string",
details: nil))
}
} else {
result(FlutterMethodNotImplemented)
}
}
private func openBrowser(urlString: String, result: @escaping FlutterResult) {
guard let url = URL(string: urlString) else {
result(FlutterError(code: "INVALID_URL",
message: "Could not parse URL: \(urlString)",
details: nil))
return
}
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:]) { (success) in
result(success)
}
} else {
result(FlutterError(code: "CANNOT_OPEN_URL",
message: "Cannot open URL: \(urlString)",
details: nil))
}
}
}
  1. Test the Plugin: The generated plugin project includes an example/ directory. Modify example/lib/main.dart to import and use your plugin.
// In example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:my_browser/my_browser.dart'; // Import the plugin
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _myBrowserPlugin = MyBrowser(); // Create instance
bool _browserOpened = false;
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion = await MyBrowser.platformVersion ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
Future<void> _callOpenBrowser() async {
bool success = await _myBrowserPlugin.openBrowser('https://flutter.dev');
if (mounted) { // Check if widget is still in the tree
setState(() { _browserOpened = success; });
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? 'Browser open attempt successful.' : 'Browser open attempt failed.')),
);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin Example App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Running on: $_platformVersion\n'),
ElevatedButton(
onPressed: _callOpenBrowser,
child: const Text('Open Browser (flutter.dev)'),
),
if (_browserOpened) const Text('Browser opened! (Check device)'),
],
)
),
),
);
}
}
  1. Run the example app (cd example, flutter run) on both Android and iOS simulators/devices to verify functionality.
  2. Publish (Optional): Once tested, you can publish your plugin to pub.dev for others to use.

Developing plugins allows extending Flutter’s capabilities by bridging Dart with the power of native platform APIs.