One Codebase, Every Screen: How Scapia Built a Multi-Platform Flutter App

The final and most crucial step in achieving a truly single codebase was isolating every single platform-specific decision. While our PlatformWidget solved the high-level layout, we needed a robust system to prevent "platform-aware logic" from polluting individual UI components.

The Problem: When kIsWeb Pollutes the Code

The moment a component has to ask, “Am I on the web?” it becomes tightly coupled to platform constraints and loses its portability.

Press enter or click to view image in full size

Our goal was to eliminate every single **kIsWeb** check from our presentation layer. We achieved this by implementing a powerful 3-Level Presentation System based on classic design patterns: Abstract Factory, Strategy, and Dependency Inversion.

The Solution: Abstraction is Liberation

We inverted the dependency: instead of the component asking the platform what to do, we inject the platform’s directives into the component.

Level 1: The Abstraction Layer (Interfaces)

This layer defines the contracts. Our high-level application logic only ever deals with abstract interfaces, ensuring it is platform-agnostic.

  • **PlatformComponentFactory**: Defines contracts for platform-specific widgets (e.g., createDocumentUploadButton()).
  • **NavigationStrategy**: Defines contracts for platform-specific behaviour (e.g., MapsAfterUpload()).
  • **StyleProvider**: Defines contracts for simple style differences (e.g., defaultAlignment()).

abstract class PlatformComponentFactory {

Widget createDocumentUploadButton({

required String docCode,

required VoidCallback onPressed, }); Widget createTravellerListWrap({

required List<Widget> children,

}); Widget createPickupDatesWidget({

required List<DateTime> dates,

}); Widget createFormNavigationButton({

required String text,

required VoidCallback onTap, });

}

Level 2: The Implementation Layer (Mobile & Web)

This layer provides the concrete implementations of the interfaces for each platform.

  • MobilePlatformComponentFactory returns widgets designed for mobile.
  • WebPlatformComponentFactory returns widgets designed for the web.

For instance, the web component factory might return a Wrap widget with WrapAlignment.center, while the mobile factory returns the same Wrap widget with WrapAlignment.start. The consuming widget doesn’t know or care which one it receives.

class MobilePlatformComponentFactory implements PlatformComponentFactory { Widget createDocumentUploadButton({

required String docCode,

required VoidCallback onPressed, }) {

return DocumentUploadButton(

docCode: docCode, onPressed: onPressed, ); } Widget createTravellerListWrap({

required List<Widget> children,

}) {

return Wrap(

alignment: WrapAlignment.start, children: children, ); } }

class WebPlatformComponentFactory implements PlatformComponentFactory {

Widget createDocumentUploadButton({

required String docCode,

required VoidCallback onPressed, }) {

return DocumentUploadButton(

docCode: docCode, onPressed: onPressed, ); } Widget createTravellerListWrap({

required List<Widget> children,

}) {

return Wrap(

alignment: WrapAlignment.center, children: children, ); }

}

Level 3: Dependency Injection (The Service Locator)

We use a simple Service Locator pattern to manage and provide the correct platform implementations at runtime. We leverage the detection capabilities we built in Section 1:

void initializePlatformServices() {

final platformDetector = PlatformDetector();

final platform = platformDetector.detectPlatform();

if (platform == PlatformType.web) {

PlatformServiceLocator.instance.initialize( componentFactory: WebPlatformComponentFactory(), navigationStrategy: WebNavigationStrategy(), styleProvider: WebStyleProvider(), );

} else {

PlatformServiceLocator.instance.initialize( componentFactory: MobilePlatformComponentFactory(), navigationStrategy: MobileNavigationStrategy(), styleProvider: MobileStyleProvider(), ); }

}

This is the only place in the entire presentation layer where a platform check is allowed. After this initialization, every component in the app becomes platform-agnostic.

Why this 3-Level Presentation System?

A single, high-level component can now be used on any platform. The injected StyleStrategy handles details like dynamic text variations (e.g., ‘Previous Question’ on mobile vs. ‘Previous’ on web), adhering to the Dependency Inversion Principle (DIP).

This architecture delivered massive benefits:

  • Massive Reusability: Components are now truly atomic and can be dropped anywhere.
  • Simplified Testing: We can unit test any component by simply mocking the platform services.
  • Future-Proofing: Adding support for a new platform only requires creating one new set of factory and strategy implementations, leaving all existing UI components untouched (Open/Closed Principle).

By connecting our intelligent PlatformWidget system to this 3-Level Presentation System, we achieved the two core pillars needed to scale our single codebase across any device.

trang chủ - Wiki
Copyright © 2011-2025 iteam. Current version is 2.147.1. UTC+08:00, 2025-11-08 06:28
浙ICP备14020137号-1 $bản đồ khách truy cập$