virtual_platform 0.0.1 virtual_platform: ^0.0.1 copied to clipboard
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 VirtualPlatformBuilder
s 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(),
));
}