Write and test platform-specific widgets in Flutter

arthurdenner

Arthur Denner

Posted on April 27, 2020

Write and test platform-specific widgets in Flutter

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
        },
      );
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

and then:

class CustomButton extends PlatformWidget { ... }
// Remember to remove the `build` method
Enter fullscreen mode Exit fullscreen mode

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 to null 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.

💖 💪 🙅 🚩
arthurdenner
Arthur Denner

Posted on April 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related