virtual_platform 0.2.0 copy "virtual_platform: ^0.2.0" to clipboard
virtual_platform: ^0.2.0 copied to clipboard

discontinued

A package that simplifies the cross-platform development process by providing a virtual platform than can be changed at runtime.

Welcome to virtual_platform, a package that simplifies the cross-platform development process by providing a virtual platform than can be changed at runtime, and declarative virtual platform instruments (including a subplatform responsive layout dispatcher).

In some cases, the actual physical platform may be needed instead of the virtual one, and in other cases the virtual/physical platform may not be necessary, but only the responsive layout dispatcher; this package meets these needs by also providing physical platform utils and a virtual/physical platform-independent responsive layout dispatcher.

Motivation #

Standard Flutter does not currently offer a straightforward approach for testing platform-specific widgets. For example, let's say the widget tree on iOS should mainly consists of Cupertino widgets and on Android of Material widgets, while on macOS it should render widgets from macos_ui and on Windows those from fluent_ui. One would need 4 different machines to correctly test the UI.

However, what if you don't have an iOS device or a Mac? Or what if constantly switching between physical devices ends up being very time consuming and/or annoying? Wouldn't you prefer to to test all these different layouts directly from your main PC? Since all platforms can render widgets from any design language — e.g., Cupertino widgets can be rendered on any platform — why not take full advantage of that?

This package was made to cover all those scenarios: by introducing a virtual platform, which can be changed at runtime, we can force all the platform-specific widgets to be rebuilt when needed.

This package also comes with a widget builder dispatcher for responsive layouts (where you can specify iphone, android, androidTablet, and so on). The motivation behind it is that an app's UI should adapt based on the screen size. Modern mobile devices support a feature called split view, while browsers and desktop apps can be resized, therefore the widgets should also be rebuilt when a breakpoint is reached.

This package does not come with a set of widget targeting different UIs. It is up to the developer to determine the exact widget, per virtual platform. Building your own bricks will make your project much easier to maintain in the long term.

Getting started #

You need to add the package virtual_platform to your dependencies.

dependencies:
  virtual_platform: ^latest # replace latest with version number

Glimpse into platforms and subplatforms #

This package uses platforms, platform groups, subplatforms and subplatform groups. They are explained at the end of this README.

Virtual platform library #

As the name of the package might suggest, this is the main library of this package.

Usage #

You get to import package:virtual_platform/virtual_platform.dart.

Note that the package is virtual_platform, while the library is virtual_platform defined inside virtual_platform.dart.

virtualPlatformNotifier #

You need to instantiate virtualPlatformNotifier before you use VirtualPlatformDispatcher or anything that relies on the virtual platform. A good place would be inside your main function.

void main() {
  virtualPlatformNotifier = VirtualPlatformNotifier.physicalPlatform(); // the virtual platform is reset upon every restart
  runApp(VirtualPlatformDispatcher(
    other: () => const MyApp(),
    ios: () => const MyIosApp(),
  ));
}

The previous virtual platform can also be loaded from persistance storage.


// in a previous session:
sharedPreferences.setString('virtual_platform', VirtualPlatform.current.toString());

// when the app starts:
Future<void> main() async {
  final sharedPreferences = await SharedPreferences.getInstance();
  final previous = sharedPreferences.getString('virtual_platform');
  final previousVirtualPlatform = VirtualPlatform.fromString(previous);
  virtualPlatformNotifier = VirtualPlatformNotifier(previousVirtualPlatform);
  runApp(VirtualPlatformDispatcher(
    other: () => const MyApp(),
    ios: () => const MyIosApp(),
  ));
}

How to change the virtual platform #

You can change the platform by accessing virtualPlatformNotifier's setter chosenPlatform, e.g.: `

ElevatedButton(
    onPressed: () => virtualPlatformNotifier.chosenPlatform =
        linuxVirtualPlatform,
    child: const Text('set linux'),
),

Or you can use setToPhysicalPlatform.

ElevatedButton(
    onPressed: () => virtualPlatformNotifier.setToPhysicalPlatform(),
    child: const Text('set physical platform'),
),

Let's now look at the specific widgets and functions this library offers.

VirtualPlatformDispatcher #

VirtualPlatformDispatcher is a widget builder that should be used when there's a dependency only on the virtual platform and not on the screen size (e.g. a "share photo" button that uses a special icon on MacOS and iOS (that fits with the Apple ecosystem), and that uses a different icon on all other devices).

VirtualPlatformDispatcher in action #

The following snippets should be self-explanatory. If they are not, consult the platforms and subplatforms tables at the end of this README.

Each defined platform requires a context-free builder. If the goal is to only select a value, a function expression is what needs to be passed, e.g., () => 'value'.

VirtualPlatformDispatcher(
  desktopSystems: () => Text('desktop'),
  other: () => Text('other'),
);
// 'other' on web, all mobile platforms and fuchsia
// 'desktop' will be displayed on macOS, Windows and Linux
VirtualPlatformDispatcher(
  desktopSystems: () => Text('desktop'),
  linux: () => Text('linux'),
  other: () => Text('other'),
);
// 'other' on web, all mobile platforms and fuchsia
// 'desktop' will be displayed only on macOS and Windows
// 'linux' will be displayed on Linux
VirtualPlatformDispatcher(
  appleSystems: () => Text('apple'),
  other: () => Text('other'),
);
// 'apple' will be displayed on macOS and iOS
VirtualPlatformDispatcher(
  appleSystems: () => Text('apple'),
  ios: () => Text('ios'),
  other: () => Text('other'),
);
// 'apple' will be displayed only on macOS

Other chain and safety measures #

The following code is pretty safe:

VirtualPlatformDispatcher(
  other: () => Text('other'),
  mobileSystems: () => Text('mobile'),
  appleSystems: () => Text('apple'),
  ios: () => Text('ios'),
  desktopSystems: () => Text('desktop'),
  linux: () => Text('linux'),
  web: () => Text('linux'),
);

However, not the same might be said about the following two widgets:

// [1] At the top of the widget tree:
VirtualPlatformDispatcher(
  other: () => MaterialApp(...),
  ios: () => CupertinoApp(...),
  macos: () => MacosApp(...),
);

// [2] More down in the widget tree:
VirtualPlatformDispatcher(
  other: () => ListTile(...), // will crash if virtual platform is iOS
  macos: () => MacosListTile(...),
);

Specifying [other] does not automatically bring safety, i.e., a crash might occur even if the [other] chain is specified. For example, let us assume the virtual platform is [iosVirtualPlatform] and the widget tree consists of Cupertino widgets ([1] in the code snippet above). If the matched chain is [other] and this chain builds a widget using the Material design ([2] in the code snippet above), this widget should be wrapped in a [Material] before getting added to the widget tree. If not, it might result in a crash. The following snippet is a possible fix:

// Safe variant of [2]:
VirtualPlatformDispatcher(
  other: () => ListTile(...),
  ios: () => Material(child: ListTile(...)),
  macos: () => MacosListTile(...),
);

VirtualSubplatformDispatcher #

In some cases the widget might have a dependency not only on the virtual platform, but also on the screen dimension of the device, i.e. window resizing or split view should change the layout (e.g. the page scaffold should display a BottomNavigationBar on smartphones, however it should switch to a NavigationRail on tablets). In those cases, VirtualSubplatformDispatcher should be used.

How to use this widget:

  • It cannot be used before one of MaterialApp(.router), CupertinoApp(.router), ... is added to the widget tree.
  • It does not necessarily have to be used only at the scaffold level: it can also be used down the widget tree; however, in some cases you might prefer to look into a combination of VirtualPlatformDispatcher and LayoutBuilder.
  • Internally, it uses MediaQuery to read the dimensions of the screen. Once a breakpoint is reached, e.g., webSmartphone to webTablet, a different widget might be built (if an influential chain is specified).
  • The subplatforms are only based on the dimension of the screen, i.e., there are no C calls retrieving the platform. Example: on an actual iPad, the dispatcher will likely select ipad. However, if the screen is resized (split view), the selected chain might change to iphone.

Usage examples:

VirtualSubplatformDispatcher(
  ios: () => Text('ios'),
  ipad: () => Text('ipad'),
  androidSmartphone: () => Text('androidSmartphone'),
  android: () => Text('android'),
  other: () => Text('other'),
);
// iPhones will display 'ios', while iPads 'ipad' (unless the app is resized to look like an iPhone app, in that case 'ios' will be displayed).
// Similarly, Android smartphones will display 'androidSmartphone', while Android tablets will display 'android'.
// All other platforms will display 'other'.

What will differ if ios is removed?

VirtualSubplatformDispatcher(
  other: () => Text('other'), // now required
  ipad: () => Text('ipad'),
  androidSmartphone: () => Text('androidSmartphone'),
  android: () => Text('android'),
);
// iPhones will display 'other', while iPads 'ipad' (unless the app is resized to look like an iPhone app, in that case 'other' will be displayed).
// Similarly, Android smartphones will display 'androidSmartphone', while Android tablets will display 'android'.

matchVirtualPlatform #

matchVirtualPlatform is a declarative pattern to invoke the right function for the matching physical platform.

Usage examples:

// inside some callback
matchVirtualPlatform(
  other: () => showModalBottomSheet(...),
  ios: () => showCupertinoModalPopup(...),
);

Avoid using this function for building widgets as VirtualPlatformDispatcher should be used for that use-case.

You can use this in case you need to invoke some platform-specific functions, e.g.:

  • showModalBottomSheet
  • showCupertinoModalPopup

In some advanced cases, however, an exception could be made for business logic that is specific to the virtual platform.

matchVirtualSubplatform #

You can use this in case you need to invoke some virtual subplatform-specific functions, e.g.:

  • showModalBottomSheet on smartphones
  • showDialog on tablets and screens with bigger dimensions

Physical platform library #

The virtual platform is very powerful. However, there are times where some widgets are not supported by all physical platforms, therefore the virtual platform approach would not work in all those cases. This package offers a secondary library for the physical platform that has a similar API to the one targeting the virtual platform.

Usage #

You get to import package:virtual_platform/physical_platform.dart.

Note that the package is (still) virtual_platform, while the library is physical_platform defined inside physical_platform.dart.

The physical_layout library has a similar API to virtual_platform. Its approach is more declarative than using Platform.isAndroid or Platform.isIOS (what Flutter offers out of the box). It should be used in parts of code where there is a dependency on the actual platform.

PhysicalPlatformDispatcher #

PhysicalPlatformDispatcher works similarly to VirtualPlatformDispatcher, however it relies on the physical platform.

It is used when a widget is not supported only by all platforms. For example the flutter_webview widget:

PhysicalPlatformDispatcher(
  other: () => Center(child: Text('platform not supported')),
  android: () => MyWebView(),
  ios: () => MyWebView(),
);

PhysicalSubplatformDispatcher #

It is very similar to PhysicalPlatformDispatcher, however also the screen dimensions are important.

Example:

PhysicalPlatformDispatcher(
  other: () => Center(child: Text('platform not supported or not enough space')), // androidSmartphone and iphone fall here
  androidTablet: () => MyWebView(),
  iosTablet: () => MyWebView(),
);

matchPhysicalPlatform #

matchPhysicalPlatform is a declarative pattern to invoke the right function for the matching physical platform. If the goal is only to select a value, a function expression is what needs to be passed, e.g., () => 'value'.

Usage examples:

final dbPath = matchPhysicalPlatform(
  other: () => 'default/path/to/db',
  android: () => 'path/to/db/on/android',
  macos: () => 'path/to/db/on/android',
);

dbPath will have value 'default/path/to/db' on all platforms (including web), except on Android and macOS.

This function will probably be the instrument you are going to use the most from the physical_platform library.

matchPhysicalSubplatform #

matchPhysicalSubplatform is a declarative pattern to invoke the right function for the matching physical subplatform.

I honestly can't come up with an instance where this function should be preferred over matchVirtualSubplatform.

ResponsiveDispatcher and Breakpoints #

Usage #

You get to import package:virtual_platform/responsive_layout.dart.

Note that the package is (still) virtual_platform, while the library is responsive_layout defined inside responsive_layout.dart.

ResponsiveDispatcher #

If one does not need to distinguish between virtual or physical platforms but needs a responsive layout, they can use ResponsiveDispatcher.

Example:

ResponsiveDispatcher(
  other: () => const Text('other'),
  smartphone: () => const Text('screen is too small'),
),

Breakpoints #

The developers don't have to specify any breakpoints, since they are already taken care of by this package. Some examples:

  • ipadSmall targets all different generations of iPad mini.
  • ipadLarge targets all other iPads (e.g. Air, Pro, ...).
  • ipad targets all iPad models (i.e., ipadSmall and ipadLarge).

It might be possible that those breakpoints are incorrect on some devices; in those cases please open an issue ;)

To test it you can override breakpoints in your main:

void main() {
  virtualPlatformNotifier = VirtualPlatformNotifier.physicalPlatform(); // the virtual platform is reset upon every restart
  breakPoints = BreakPoints(500, 800, 1300);
  runApp(VirtualPlatformDispatcher(
    other: () => const MyApp(),
    ios: () => const MyIosApp(),
  ));
}

Or you can also implement AbstractBreakPoints yourself if there should be a mismatch among platforms:

class AdvancedBreakPoints extends AbstractBreakPoints {
  final double smartphoneMaxWidth;
  final double tabletSmallMaxWidth;
  final double tabletLargeMaxWidth;

  MyBreakPoints(
    this.smartphoneMaxWidth,
    this.tabletSmallMaxWidth,
    this.tabletLargeMaxWidth,
  );

  //! In case a platform has way more logical pixels than the other ones,
  //! add the chain of that platform to the [matchPhysicalPlatform] below.

  @override
  bool isSmartphone(double width) => matchPhysicalPlatform(
        other: () => lessThan(width, smartphoneMaxWidth),
        ios: () => lessThan(width, 550), //! maybe pixels differ on iOS?
      );

  @override
  bool isTabletSmall(double width) =>
      matchPhysicalPlatform(other: () => lessThan(width, tabletSmallMaxWidth));

  @override
  bool isTabletLarge(double width) =>
      matchPhysicalPlatform(other: () => lessThan(width, tabletLargeMaxWidth));
}

And then:

void main() {
  virtualPlatformNotifier = VirtualPlatformNotifier.physicalPlatform(); // the virtual platform is reset upon every restart
  breakPoints = AdvancedBreakPoints(500, 800, 1300);
  runApp(VirtualPlatformDispatcher(
    other: () => const MyApp(),
    ios: () => const MyIosApp(),
  ));
}

Platforms and subplatforms #

Platforms and platform groups #


Platform appleSystems mobileSystems desktopSystems
android
ios
linux
macos
windows
web
fuchsia

Priority order: from left to right.

Subplatforms and subplatform groups #

Subplatforms are screen-size inferrable platforms, such as ipadSmall. They are only relevant for the widgets:

  • VirtualSubplatformDispatcher
  • PhysicalSubplatformDispatcher

androidTablet, ipad and webTablet are subgroups having precedence over the platforms.

apple, tabletSmall, ... are subgroups that will be checked before resorting to the other function/builder.

The entries marked with parenthesis in the table below can be disabled by setting the correct bool parameter (listed under the table) to false.

Subplatform androidTablet(🤖)
ipad(🍎)
webTablet(🌐)
linuxTablet(🐧)
macosTablet(🍏)
windowsTablet(🪟)
Platform appleSystems smallTablet(🔹)
tabletLarge(🔷) ³
tablet mobile desktop
androidSmartphone android
androidTabletSmall 🤖 android 🔹
androidTabletLarge 🤖 android 🔷
androidDesktop (🤖)¹ android (🔷)¹ (✓)¹ (✓)¹
iphone ios
ipadSmall 🍎 ios 🔹
ipadLarge 🍎 ios 🔷
iosDesktop (🍎)² ios (🔷)² (✓)² (✓)²
webSmartphone web
webTabletSmall 🌐 web 🔹
webTabletLarge 🌐 web 🔷
webDesktop web
linuxSmartphone linux (✓)³
linuxTabletSmall 🐧 linux (🔹)³ (✓)³ (✓)³
linuxTabletLarge 🐧 linux (🔷)³ (✓)³ (✓)³
linuxDesktop linux
macosSmartphone macos (✓)⁴
macosTabletSmall 🍏 macos (🔹)⁴ (✓)⁴ (✓)⁴
macosTabletLarge 🍏 macos (🔷)⁴ (✓)⁴ (✓)⁴
macosDesktop macos
windowsSmartphone windows (✓)⁵
windowsTabletSmall 🪟 windows (🔹)⁵ (✓)⁵ (✓)⁵
windowsTabletLarge 🪟 windows (🔷)⁵ (✓)⁵ (✓)⁵
windowsDesktop windows

Priority order: from left to right.

Enabled based on bool parameters (which default to true):

  • (1) treatAndroidDesktopAsTabletLarge
  • (2) treatIosDesktopAsTabletLarge
  • (3) treatSmallLinuxAsMobile
  • (4) treatSmallMacosAsMobile
  • (5) treatSmallWindowsAsMobile

NB: appleSystems works the same across platforms and subplatforms. However, there are some differences among mobileSystems/desktopSystems platform groups and mobile/desktop subplatform groups:

  • mobileSystems and desktopSystems only take into account the OSs. The web platform is not considered.
  • mobile and desktop take into account the screen size.

3-rd party packages #

You should not create 3-rd party packages with a dependency on virtual_platform, because your VirtualPlatformDispatchers and VirtualSubplatformDispatchers might create widgets that aren't compatible with the end user's widget subtree. For example, you might specify a Material Widget for iOS, however the end user might have a Cupertino-based widget tree, potentially causing a crash or a UI mismatch.

0
likes
0
pub points
4%
popularity

Publisher

verified publishermanuelplavsic.ch

A package that simplifies the cross-platform development process by providing a virtual platform than can be changed at runtime.

Repository (GitLab)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on virtual_platform