virtual_platform 0.3.1 virtual_platform: ^0.3.1 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, together with declarative virtual platform utils.
NB: If you are using also physical_platform
, you can opt to only use omni_platform
instead. omni_platform
integrates virtual_platform
and physical_platform
, without adding any extra functionality. This way you only need to declare one package in your pubspec.yaml
.
Warning: use this package only when targeting a single design language
This package assumes that you use only one design language (e.g. only MaterialApp is defined in your widget tree), however, some parts of the code might require better-fitting widgets (e.g., the date picker and the bottom sheets should look like proper iOS ones if the virtual platform is iOS).
On the other hand, if your intent is to use multiple design languages (e.g. MaterialApp should be used in Android devices, CupertinoApp on iOS devices, etc.), then you should use another package: design_language
. A virtual platform can still be used in combination with design_language
if the package design_language
does not provide all necessary utils.
Motivation #
Standard Flutter does not offer a straightforward approach for rendering platform-specific widgets on a different platform. For example, let's say that the widget tree is mainly composed of Material widgets, however the date picker and the bottom sheets should look like proper iOS ones. One would usually need an Android smartphone and an iPhone to test this.
However, what if you don't have an iOS device? 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 development station? 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 the scenarios above: by introducing a virtual platform, which can be changed at runtime, we can force all the virtual-platform-specific widgets to be rebuilt when needed.
This package — virtual_platform
— does not come with any prebuilt, ready-to-use widgets: it is up to the developer to determine the exact widget, per virtual platform.
Getting started #
You need to add virtual_platform
to your dependencies.
dependencies:
virtual_platform: ^latest # replace latest with version number
Next, you have to import package:virtual_platform/virtual_platform.dart
.
Usage #
The arguments of all instruments are functions, i.e., VirtualPlatformDispatcher
and matchVirtualPlatform
are of
type Widget Function(BuildContext context, Widget? child)
and T Function()
, respectively. If the goal is to only select a widget or a value — a function
expression is what needs to be passed, e.g., (_, __) => Text('text')
or () => 'value'
, respectively.
Virtual platforms and virtual platform groups are summarized at the end of this README.
VirtualPlatform and its notifier #
You need to instantiate VirtualPlatform.notifier
before you use VirtualPlatformDispatcher
or anything that relies on the virtual platform. A good place would be inside your main
function.
void main() {
VirtualPlatform.notifier = VirtualPlatformNotifier(VirtualPlatform.fromPhysicalPlatform);
// the virtual platform is reset upon every restart
runApp(VirtualPlatformDispatcher(
other: (_, __) => const MyApp(), // this will get selected by all devices but iOS devices
ios: (_, __) => const MyIosApp(), // this will get selected by iOS devices
));
}
If the goal is to force a specific virtual platform on startup for all platforms:
void main() {
we VirtualPlatform.notifier = VirtualPlatformNotifier(VirtualPlatforms.ios);
// the virtual platform is reset upon every restart
runApp(VirtualPlatformDispatcher(
other: (_, __) => const MyApp(),
ios: (_, __) => const MyIosApp(), // this will get selected by all devices
));
}
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:
void main() async {
final sharedPreferences = await SharedPreferences.getInstance();
final previous = sharedPreferences.getString('virtual_platform');
final previousVirtualPlatform = VirtualPlatform.fromString(previous);
VirtualPlatform.notifier = VirtualPlatformNotifier(previousVirtualPlatform);
runApp(VirtualPlatformDispatcher(
other: (_, __) => const MyApp(),
ios: (_, __) => const MyIosApp(),
));
}
How to change the virtual platform
You can change the platform by accessing VirtualPlatform.notifier
's setter chosenPlatform
, e.g.:
ElevatedButton(
onPressed: () => VirtualPlatform.notifier.chosenPlatform =
VirtualPlatforms.linux,
child: const Text('set linux'),
),
ElevatedButton(
onPressed: () => VirtualPlatform.notifier.chosenPlatform =
VirtualPlatforms.ios,
child: const Text('set ios'),
),
...
How to reset the virtual platform:
ElevatedButton(
onPressed: () => VirtualPlatform.notifier.chosenPlatform =
VirtualPlatform.fromPhysicalPlatform,
child: const Text('reset virtual platform'),
),
VirtualPlatforms
VirtualPlatforms
is a class containing all possible VirtualPlatform
values as static fields. The constructors of VirtualPlatforms
and VirtualPlatform
are both private.
N.B.: The default and current virtual platform are accessed through VirtualPlatform
(and not through VirtualPlatforms
):
VirtualPlatform.fromPhysicalPlatform
VirtualPlatform.current
(once the notifier is set)
VirtualPlatformDispatcher #
VirtualPlatformDispatcher
is a widget builder dispatcher that should be used if there's a dependency only on the virtual platform.
VirtualPlatformDispatcher in action
The following snippets should be self-explanatory. If they are not, consult the platform table at the end of this README
.
Each defined platform requires a nullable TransitionBuilder
, with exception to the other
chain, whose TransitionBuilder
is non-nullable.
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('web'),
);
However, not the same might be said about the following two widgets:
// [1] At the top of the widget tree:
VirtualPlatformDispatcher(
other: (_, __) => MaterialApp(...), // NB: do not use the context, since it doesn't exist yet.
ios: (_, __) => CupertinoApp(...), // NB: do not use the context, since it doesn't exist yet.
macos: (_, __) => MacosApp(...), // NB: do not use the context, since it doesn't exist yet.
);
// [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 VirtualPlatforms.ios
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(...),
);
Also, if support for an additional design language is added or dropped, the static analysis will not show you any errors or warnings if you have been using this package for design-language dispatching.
For these reasons, one should prefer the package design_language
over virtual_platform
when doing cross-design-language development
Context and child provision
One might need the context in some cases. The first parameter of all builders is a BuildContext context
. You can also specify a Widget? child
widget, so that you do not have to repeat it in all builders.
Example:
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: const Text('example')),
body: VirtualPlatformDispatcher(
child: const CommonSubtree(),
mobileSystems: (context, child) => SpecialWidget(
child,
onPressed(() {do something with context}),
),
other: (context, child) => OtherSpecialWidget(
child,
onPressed(() {do something with context}),
),
);
),
);
matchVirtualPlatform #
matchVirtualPlatform
is a declarative pattern to invoke the right function for
the matching physical platform.
You can use this in case you need to invoke some virtual-platform-specific functions, e.g.:
showCupertinoModalPopup
for iOSshowModalBottomSheet
for all other cases
Same example, in code:
// inside some callback
matchVirtualPlatform(
other: () => showModalBottomSheet(...),
ios: () => showCupertinoModalPopup(...),
);
Avoid using this function for building widgets as VirtualPlatformDispatcher
should be used for this use-case.
Platforms and platform groups #
Platform | appleSystems | mobileSystems | desktopSystems |
---|---|---|---|
android | ✓ | ||
ios | ✓ | ✓ | |
linux | ✓ | ||
macos | ✓ | ✓ | |
windows | ✓ | ||
web | |||
fuchsia |
Priority order: from left to right.