virtual_platform 0.2.0 virtual_platform: ^0.2.0 copied to clipboard
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
andLayoutBuilder
. - Internally, it uses MediaQuery to read the dimensions of the screen. Once a breakpoint is reached, e.g.,
webSmartphone
towebTablet
, 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 toiphone
.
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 smartphonesshowDialog
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
andipadLarge
).
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
anddesktopSystems
only take into account the OSs. The web platform is not considered.mobile
anddesktop
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 VirtualPlatformDispatcher
s and VirtualSubplatformDispatcher
s 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.