Arthur Denner
Posted on April 27, 2020
With Flutter, it's possible to display a CupertinoButton on iOS and a RaisedButton on Android, giving an even more native looking to our apps.
Display a widget on a specific platform is a reasonable requirement and it can apply to other platforms that Flutter supports too - Fuchsia, Linux, macOS and Windows.
In this post, let's see the approach I use to achieve this requirement using a CustomButton
widget as example.
Figuring out the platform
Although the Platform
class from dart:io
provide this information, it's not possible to mock it and our tests always run on Android so we can't test the widget on other platforms.
Alternatively, we can use Theme.of(context).platform
, which returns a TargetPlatform and can be overwritten on tests with the debugDefaultTargetPlatformOverride property.
Writing the widget
A very simple implementation.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
const CustomButton({
Key key,
this.onPressed,
});
final VoidCallback onPressed;
Widget buildCupertinoWidget(BuildContext context) {
return CupertinoButton(
onPressed: onPressed,
child: Text('Click me'),
);
}
Widget buildMaterialWidget(BuildContext context) {
return RaisedButton(
onPressed: onPressed,
child: Text('Click me'),
);
}
Widget build(BuildContext context) {
final _platform = Theme.of(context).platform;
return _platform == TargetPlatform.iOS
? buildCupertinoWidget(context)
: buildMaterialWidget(context);
}
}
Testing the widget
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'path/to/CustomButton.dart';
void main() {
Widget buildApp({VoidCallback onPressed}) {
return MaterialApp(
home: CustomButton(
onPressed: onPressed,
),
);
}
group('CustomButton >', () {
group('output >', () {
testWidgets(
'displays CupertinoButton on iOS',
(tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(buildApp());
expect(find.byType(CupertinoButton), findsOneWidget);
debugDefaultTargetPlatformOverride = null; // <-- this is required
},
);
testWidgets(
'displays RaisedButton on other platforms',
(tester) async {
await tester.pumpWidget(buildApp());
expect(find.byType(RaisedButton), findsOneWidget);
},
);
});
});
}
Abstracting the logic
One improvement to this approach is to create an abstract class that computes which method to call and extend your widget from it:
import 'package:flutter/material.dart';
abstract class PlatformWidget extends StatelessWidget {
const PlatformWidget({Key key}) : super(key: key);
Widget buildCupertinoWidget(BuildContext context);
Widget buildMaterialWidget(BuildContext context);
@override
Widget build(BuildContext context) {
final _platform = Theme.of(context).platform;
return _platform == TargetPlatform.iOS
? buildCupertinoWidget(context)
: buildMaterialWidget(context);
}
}
and then:
class CustomButton extends PlatformWidget { ... }
// Remember to remove the `build` method
This way, if you add another method to a different platform, all widgets will be requested to override it.
Bonus - Test behavior to improve your tests
Better than test what the widget is displaying to each platform, we should assert the behavior is the same on all platforms. See how to do that in this post.
Notes
- We must reset
debugDefaultTargetPlatformOverride
tonull
by the end of every test case, otherwise Flutter will throw an error.
If you're using a different solution or have any suggestions to improve this example, feel free to share it in the comments.
I hope you enjoyed this post and follow me on any platform for more.
Posted on April 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.