はじめに
現在進めているプロジェクトでは画面サイズに合わせてWidgetやフォントサイズを調整できるよう
flutter_screenutilを導入しています。
Widget Test実装時はGolden Testで実装しているのですがその際に詰まったことを備忘録がてら書きます。
前提
flutter_screenutilとは?
Widgetやフォントサイズを動的に変更できる便利なパッケージです。
詳細な説明は省きますが公式を見て初期設定さえすれば簡単に実装することができます。
Golden Testについて
Golden Testの詳細は省きますが今回の対応ではgoolden_toolkitを利用しています。
Flutterではパッケージを導入せずともGolden Testの実装は可能ですが、よりテストを実装しやすくするために上記のパッケージを導入しています。
今回のソースコードはGithubにおいてあるので詳細気になる方は見てください。
目次
簡単なサンプルの確認
Flutterのチュートリアルでも利用されているカウンターアプリにGolden Testを実装してみます。
flutter_test_configを以下のように実装します。flutter_test_configの詳細については以下を読めばわかりやすいと思います。
flutter_test library
テストコード実装の際にWidget Testの対象となるWidgetを指定すると思うのですがその時にpageWrapperでラップするとプロジェクト独自のtheme等の設定を適用できるので以下の様な構成にしています。
TestDeviceではgolden_tookitで異なるデバイスのUIを確認する際のデバイス一覧を定義しています。
Device.iphone11
の様な書き方をすると簡単にDeviceを定義することもできます。
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
return GoldenToolkit.runWithConfiguration(
() async {
await loadAppFonts();
await testMain();
},
config: GoldenToolkitConfiguration(
// Currently, goldens are not generated/validated in CI for this repo. We have settled on the goldens for this package
// being captured/validated by developers running on MacOSX. We may revisit this in the future if there is a reason to invest
// in more sophistication
skipGoldenAssertion: () => !Platform.isMacOS,
enableRealShadows: true,
),
);
}
Widget pageWrapper(Widget widget) =>
MaterialApp(
// ThemeやfontoFamiliyの指定はココで
home: widget,
debugShowCheckedModeBanner: false,
);
class TestDevice {
static const all = [
Device(
size: Size(375, 667),
name: 'iPhoneSE',
devicePixelRatio: 3,
),
Device(
size: Size(375, 812),
name: 'iPhone13Mini',
devicePixelRatio: 3,
),
Device(
size: Size(428, 926),
name: 'iPhone13ProMax',
devicePixelRatio: 3,
),
];
}
肝心のテストコードは以下です。
void main() {
testGoldens('MyHomePage', (tester) async {
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(devices: TestDevice.all)
..addScenario(
widget: materialWrapper(const MyHomePage(title:'screenutil_golden_demo')),
name: 'default',
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'MyHomePage');
});
}
実際に発生したエラー
flutter test --update-goldens
すると各デバイスサイズのスクリーンショットが生成されるイメージだったのですが以下のエラーが発生しました。
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following LateError was thrown building MyHomePage(dirty, state: _MyHomePageState#9eb02):
LateInitializationError: Field '_minTextAdapt@249084504' has not been initialized.
The relevant error-causing widget was:
MyHomePage
When the exception was thrown, this was the stack:
#0 ScreenUtil._minTextAdapt (package:flutter_screenutil/src/screen_util.dart)
#1 ScreenUtil.scaleText (package:flutter_screenutil/src/screen_util.dart:177:7)
#2 ScreenUtil.setSp (package:flutter_screenutil/src/screen_util.dart:204:44)
#3 SizeExtension.sp (package:flutter_screenutil/src/size_extension.dart:18:33)
#4 _MyHomePageState.build (package:screenutil_golden_demo/main.dart:53:65)
#5 StatefulElement.build (package:flutter/src/widgets/framework.dart:5080:27)
#6 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4968:15)
#7 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5133:11)
#8 Element.rebuild (package:flutter/src/widgets/framework.dart:4690:5)
#9 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4950:5)
#10 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5124:11)
#11 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4944:5)
... Normal element mounting (214 frames)
スタックトレースを確認するとscreenutilに起因するエラーっぽい。。
screenutilパッケージを利用する際の初期設定がテストコードでは反映されていないことが原因っぽいことはわかるような。。
以下のようにpageWrapperを書き換えて見ましたが今度はスクリーンショットの解像度?がおかしくなる。。
Widget pageWrapper(Widget widget) =>
ScreenUtilInit(
designSize: const Size(375,812),
builder: (context, child) =>
MaterialApp(
home: widget,
debugShowCheckedModeBanner: false,
)
);
解消方法
screenutilのリポジトリでそれっぽいissueがたてられていました。
MediaQueryでラップする必要があるっぽい。。?
とりあえず上記で指定されたやり方でpageWrapperを実装してみます。
class _Wrapper extends StatelessWidget {
const _Wrapper(this.child);
final Widget child;
@override
Widget build(BuildContext context) {
ScreenUtil.init(context, designSize: const Size(1400, 926),);
return child;
}
}
Widget pageWrapper(Widget widget) =>
MediaQuery(
data: const MediaQueryData(),
child: MaterialApp(
home: Scaffold(body: _Wrapper(widget)),
debugShowCheckedModeBanner: false,
),
);
最後に
なぜテストコード側だけMediaQueryでラップする必要があるかは分からないですが公式のexampleを見た感じだと同様にテストコードを実装していたのでこのやり方で問題はなさそうです。
パッケージの実装を確認しましたが、レスポンシブ対応するためにMediaQueryを利用していました。
なので、テストコード側もMediaQueryでラップして画面サイズ等の情報を定義する必要があるといった理屈なんですかね。
もっとクリティカルな根拠を見つけたら追記します。