Learn Dart - Multiplatform & Shared Libraries
Series/Learn Dart/Episode 17
Episode 17 of 23

Learn Dart - Multiplatform & Shared Libraries

This episode covers multiplatform strategy and shared libraries in Dart: sharing code between mobile, web, server, and CLI, modular architecture with reusable packages, cross-platform testing, and integration with Flutter plugins.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

One of Dart's biggest promises comes true when the same code runs on mobile, web, server, and the command line. Episode 17 covers multiplatform strategy and shared libraries: how to separate logic from the platform, organize modular projects, test across platforms, and connect with Flutter plugins.

The key to all of this is discipline: shared code must be pure and free of platform dependencies. You'll learn to separate the layers properly so that a single logic package serves every client.

Sharing Code Between Mobile, Web, Server, and CLI

A Pure Core Layer

The heart of a multiplatform strategy is separating business logic (pure Dart) from platform details (UI, files, native networking):

Multiplatform project structure
monorepo/
├── packages/domain_core/    # murni Dart, dipakai semua platform
├── apps/flutter_app/        # mobile dan web via Flutter
├── apps/server/             # backend dengan Shelf
└── apps/cli/                # command-line tool

packages/domain_core/ is a pure Dart package without Flutter or io-specific dependencies. Every application adds this package as a dependency and uses exactly the same logic on every platform.

Keeping Code Portable

To make a package shareable, follow these rules: avoid dart:io and dart:html in the core layer — both are only available on some platforms. For platform-dependent operations, define abstractions (interfaces) in core and provide concrete implementations per platform.

Modular Architecture and Reusable Packages

Monorepo with Workspace

Dart supports multi-package projects in a single monorepo. A common structure uses packages/ to hold reusable libraries:

Dependencies between packages
name: apps_flutter
dependencies:
  domain_core:
    path: ../../packages/domain_core

domain_core is added with path: so changes to the package are visible immediately without publishing. Each package has its own pubspec.yaml and versioning cycle.

Defining Package Boundaries

A good package has a single responsibility: domain, data access, or presentation. Clear boundaries let teams work on different parts without stepping on each other, and make testing easier since dependencies can be mocked.

Start with just two packages — one for the domain and one for the app — then split further as the team and features grow. Enforcing package boundaries too early only adds coordination cost without real benefit.

Cross-Platform Testing and Code Reuse Patterns

Test the Core Once, for Everyone

Since every platform uses the same logic, test the core package once and the result applies to all clients:

Running the core package tests
cd packages/domain_core
dart test

dart test in the core package validates the logic used by Flutter, the server, and the CLI at the same time. That's a huge saving compared to writing separate tests per platform.

Effective Reuse Patterns

  • Interfaces as contracts: define repositories and services in core, implement them on each platform.
  • Dependency injection: pass in the concrete implementation when the app boots.
  • DTOs: share data models through core so serialization stays consistent across platforms.

Combining these three produces a layer that can be tested in isolation: the repository uses interfaces from core, and the application injects an HTTP- or database-based implementation per platform. This flow lets you swap data sources without touching business logic at all.

Integration with Flutter Plugins

Calling Native Capabilities

Platform-specific needs are handled through Flutter plugins. A plugin combines Dart code with native Android, iOS, and other implementations:

Using a Flutter plugin
import 'package:shared_preferences/shared_preferences.dart';
 
Future<void> simpanPref() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('tema', 'gelap');
}

SharedPreferences.getInstance() is an example of a popular plugin that provides a Dart API over per-platform native implementations.

Writing Your Own Plugin

To create a plugin: flutter create --template=plugin. A plugin produces a Dart API in the lib/ folder, an Android implementation in android/, an iOS one in ios/, and so on. Platform channels connect the two. If the platform logic becomes complex, consider FFI (episode 16) as a more portable alternative.

Because plugins touch many platforms, make sure every change is tested across all targets. Start from the built-in template, then add platform functionality one at a time — this keeps the plugin small and maintainable.

Don't put business logic inside a plugin. Keep it in the core package and let the plugin be nothing more than a thin bridge to native capabilities.

Conclusion

Key takeaways:

  • Keep pure business logic in a core package free of platform dependencies.
  • Avoid dart:io and dart:html in layers shared across platforms.
  • A monorepo with packages/ and path dependencies makes modular development easy.
  • Test the core package once; the results apply to every client.
  • Platform-specific capabilities are handled through abstractions and Flutter plugins.
  • A plugin combines a Dart API with per-platform native implementations.

In the next episode 18, we'll cover ecosystem extensions and the community — popular packages like dio, freezed, and Riverpod, working with pub.dev and publishing packages, contributing to Dart open source, and learning resources and community channels.

Learn Dart - Multiplatform & Shared Libraries | Learn Dart