Write and test platform-specific widgets in Flutter
Tags: flutter, test, testing
Published: 2020-04-27 08:08:13

---
title: Write and test platform-specific widgets in Flutter
published: true
description: How to compute which widget to display on Android, iOS, etc.
tags: flutter, test, testing
cover_image: https://res.cloudinary.com/arthurdenner/image/upload/v1594144444/posts/platform_specific_widgets.png
---

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](https://api.flutter.dev/flutter/foundation/TargetPlatform-class.html) and can be overwritten on tests with the [debugDefaultTargetPlatformOverride](https://api.flutter.dev/flutter/foundation/debugDefaultTargetPlatformOverride.html) property.

## Writing the widget

A very simple implementation.

```dart
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

```dart
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:

```dart
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:

```dart
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](https://dev.to/arthurdenner/test-behavior-of-widgets-in-flutter-3h4j).

## 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.