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

discontinued

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

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

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 iPhone or a macOS? 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 library 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 library also comes with a widget builder for responsive layouts (where you can specify iphone, android, androidTablet, and so on). The motivation behind it is that modern mobile devices support a feature called split view, while browsers and desktop apps can be resized.

Getting started #

You need to add virtual_platform to your dependencies.

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

To use it, import package:virtual_platform/virtual_platform.dart.

Usage #

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

You need to instantiate virtualPlatformNotifier before you use VirtualPlatformBuilder or anything that relies on the virtual platform. I would do it in your main function.

void main() {
  virtualPlatformNotifier = VirtualPlatformNotifier.physicalPlatform(); // the virtual platform is reset upon every restart
  runApp(VirtualPlatformBuilder(
    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.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(VirtualPlatformBuilder(
    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.

VirtualPlatformBuilder #

VirtualPlatformBuilder is a widget builder that should be used when there's a dependency only on the platform (e.g. how a "share photo" button should look like).

VirtualPlatformBuilder 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.

VirtualPlatformBuilder(
  other: () => Text('other'),
  desktop: () => Text('desktop'),
  linux: () => Text('linux'),
);
// 'other' on web, all mobile platforms and fuchsia
// 'desktop' will be displayed only on macOS and Windows
// 'linux' will be displayed on Linux
VirtualPlatformBuilder(
  linux: () => Text('linux'),
  desktop: () => Text('desktop'),
);
// since there is no `other` chain, this might lead to a crash, but it is not all that bad considering that an `other` chain is not totally safe either (this is explained later in this README)
VirtualPlatformBuilder(
  other: () => Text('other'),
  apple: () => Text('apple'),
);
// 'apple' will be displayed on macOS and iOS
VirtualPlatformBuilder(
  other: () => Text('other'),
  apple: () => Text('apple'),
  ios: () => Text('ios'),
);
// 'apple' will be displayed only on macOS

otherRequired constructor

Let's say crashes keep occurring because we are not being careful, e.g.:

VirtualPlatformBuilder(
  mobile: () => Text('mobile'),
  web: () => Text('web'),
);

In this case you can enforce the other chain by switching constructor to otherRequired.

VirtualPlatformBuilder.otherRequired(
  mobile: () => Text('mobile'),
  web: () => Text('web'),
  other: () => Text('other'), // this param is required now
);

N.B.: it is safer to use the otherRequired constructor. However, when multiple VirtualPlatformBuilders are nested or when all possible platforms are specified (see example), the other may never be called, this is why in the default constructor is not there.

VirtualPlatformBuilder(
  // other: () => Text('other'), -- would never be invoked
  mobile: () => Text('mobile'),
  apple: () => Text('apple'),
  ios: () => Text('ios'),
  desktop: () => Text('desktop'),
  linux: () => Text('linux'),
);

Why isn't the otherRequired the default constructor?

Note that enforcing [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. If the matched chain is [other] and this chain builds a widget using the Material design, this widget should be wrapped in a [Material] before getting added to the widget tree. If not, it might result in a crash.

If there are valid reasons to change this, please open an issue.

ResponsiveBuilder #

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, ResponsiveBuilder should be used.

The widget can be used anywhere in the code, i.e., not necessarily only at the scaffold level. 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 defined so).

Usage examples:

ResponsiveBuilder(
  ios: () => Text('ios'),
  ipad: () => Text('ipad'),
  androidSmartphone: () => Text('androidSmartphone'),
  android: () => Text('android'),
);
// 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'.

The otherRequired constructor also exists for this widget:

ResponsiveBuilder.otherRequired(
  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'.

ResponsiveBuilder is not made only for scaffolding a page based on platform and subplatform. It can also be used to modify a button.

In some cases you might prefer to look into a combination of LayoutBuilder and VirtualPlatformBuilder.

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.

When constructing widgets, use the virtual platform, i.e., VirtualPlatformBuilder or ResponsiveBuilder should be preferred.

matchVirtualPlatform #

matchVirtualPlatform 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 folderName = matchVirtualPlatform(
  other: () => 'other',
  android: () => 'android',
  macos: () => 'macos',
);

Avoid using this function for building widgets as VirtualPlatformBuilder 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.

Platforms and subplatforms #

Platforms and platform groups #


Platform apple mobile desktop
android
ios
linux
macos
windows
web
fuchsia

Priority order: from left to right.

Subplatforms, their platforms and platform groups #

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

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.

Subplatform
androidTablet
ipad
webTablet
Platform apple
tabletSmall
tabletLarge
tablet mobile desktop
androidSmartphone android
androidTabletSmall
android
androidTabletLarge
android
iphone ios
ipadSmall
ios
ipadLarge
ios
webSmartphone web
webTabletSmall
web
webTabletLarge
web
webDesktop web
linux
macos
windows
fuchsia

Priority order: from left to right.

Breakpoints #

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

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

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(VirtualPlatformBuilder(
    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(VirtualPlatformBuilder(
    other: () => const MyApp(),
    ios: () => const MyIosApp(),
  ));
}
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