次世代のProviderだと何かと話題のRiverpodに手を出しました。すっかり気に入って自作のアプリは全てRiverpodに書き換えてしまいました。
このRiverpodが提供するProviderにはOverrideという機能があります。
この記事ではこのOverrideを使って、手作業でやるには面倒なアプリのスクリーンショットを自動で撮ってみたいと思います。
概要
Flutterアプリのスクリーンショットを自動で撮りたい
Riverpodを使った実装なら好きな状態でスクリーンショットが撮れるかも
Flutterのバージョン
1
2
3
4
5
% flutter --version
Flutter 1.25.0-8.1.pre • channel beta • https://github.com/flutter/flutter.git
Framework • revision 8f89f6505b ( 8 days ago) • 2020-12-15 15:07:52 -0800
Engine • revision 92ae191c17
Tools • Dart 2.12.0 ( build 2.12.0-133.2.beta)
サンプルアプリについて
リポジトリはここ
メインのソースコードは一本にまとめました。
https://github.com/seiichi3141/take_screenshots/blob/master/lib/main.dart
flutter createで作られるサンプルアプリをRiverpod仕様に実装し直して、テーマを変更できるようにしたもの
設定画面を用意する。テーマとダークモードの切替ができるようにする
ホーム画面のAppBarのActionsにIconButtonを追加して設定画面へ遷移できる
Web用にビルドしたものが以下です。ダークモード、テーマ色の変更も確認できると思います。
ライブラリ
riverpodやflutter_driverを追加しています。
1
2
3
4
5
6
7
8
9
10
11
12
dependencies :
flutter :
sdk : flutter
cupertino_icons : ^1.0.0
flutter_riverpod : ^0.12.1
dev_dependencies :
flutter_test :
sdk : flutter
flutter_driver :
sdk : flutter
test : any
Providers
Primary Swatch, Brightness, CounterをStateProviderで管理します。
1
2
3
final primarySwatchProvider = StateProvider (( _ ) => Colors . blue );
final brightnessProvider = StateProvider (( _ ) => Brightness . light );
final counterProvider = StateProvider (( _ ) => 0 );
Consumer
ConsumerでStateProviderを監視(watch)することでStateに変更があるたびにMaterialAppをRebuildしてくれるようになります。MaterialAppのテーマ変更はアニメーションがおまけでついてきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyApp extends StatelessWidget {
@ override
Widget build ( BuildContext context ) {
return Consumer (
builder: ( context , watch , _ ) {
return MaterialApp (
title: 'Screenshot Demo' ,
debugShowCheckedModeBanner: false ,
theme: ThemeData (
primarySwatch: watch ( primarySwatchProvider ). state ,
brightness: watch ( brightnessProvider ). state ,
),
home: MyHomePage (),
);
},
);
}
}
カウンター表示もこの通りConsumerで囲って監視します。
1
2
3
4
5
6
7
8
Consumer (
builder: ( context , watch , _ ) {
return Text (
' ${ watch ( counterProvider ). state } ' ,
style: Theme . of ( context ). textTheme . headline4 ,
);
},
),
Stateの変更
FloatingActionButtonのAddボタンが押された時にcounterProviderのstateを更新します。
1
2
3
4
5
floatingActionButton: FloatingActionButton (
onPressed: () => context . read ( counterProvider ). state ++ ,
tooltip: 'Increment' ,
child: Icon ( Icons . add ),
),
設定画面では例えばSwitchの切り替えでbrightnessProviderのstateを変更します。
1
2
3
4
5
6
7
8
9
10
11
return SwitchListTile (
title: const Text ( 'Dark Mode' ),
value: watch ( brightnessProvider ). state == Brightness . dark ,
onChanged: ( value ) {
if ( value ) {
context . read ( brightnessProvider ). state = Brightness . dark ;
} else {
context . read ( brightnessProvider ). state = Brightness . light ;
}
},
);
今回はこのアプリのスクリーンショットを撮るのを目的とします。
スクリーンショット撮影の流れ
スクリーンショットの撮影は基本的に以下の流れで行います。
スクリーンショット用アプリの起動
テストケース開始
FlutterDriver.requestDataでアプリに変更をリクエスト
スクリーンショット撮影
Flutter Driver用にアプリを実装する
test_driverディレクトリにスクリーンショット撮影用のアプリapp.dartを実装します。
コードはこちら
アプリにはホーム画面と設定画面の二つの画面があります。それを切り替えるために一つProviderを用意します。
1
2
3
4
5
6
enum Screen {
home ,
settings ,
}
final screenProvider = StateProvider (( _ ) => Screen . home );
撮影用アプリのWidgetツリーに流すProviderを提供するProviderContainerを作っておきます。このContainerのproviderをoverrideすることでツリー配下のWidgetに影響を及ぼすことができるようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final providerContainer = ProviderContainer (
overrides: [
screenProvider . overrideWithValue (
StateController ( Screen . home ),
),
primarySwatchProvider . overrideWithValue (
StateController ( Colors . blue ),
),
brightnessProvider . overrideWithValue (
StateController ( Brightness . light ),
),
counterProvider . overrideWithValue (
StateController ( 0 ),
),
],
);
enableFlutterDriverExtension関数を実装します。ここでテストケースからFlutterDriver.requestDataを使って送られてくるデータを処理します。
enableFlutterDriverExtension
以下のコードは抜粋ですが、‘screen::settings’というActionが送られてきたらscreenProviderをScreen.settingsでoverride。‘brightness::dark’というActionが送られてきたらbrightnessProviderをBrightness.darkでoverride、などとスクリーンショットで撮りたい状況に合わせてProviderをoverrideできるようにしておきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
enableFlutterDriverExtension (
handler: ( action ) async {
switch ( action ) {
case 'screen::settings' :
providerContainer . updateOverrides ([
screenProvider . overrideWithValue (
StateController ( Screen . settings ),
),
]);
break ;
case 'brightness::dark' :
providerContainer . updateOverrides ([
brightnessProvider . overrideWithValue (
StateController ( Brightness . dark ),
),
]);
break ;
case 'primarySwatch::yellow' :
providerContainer . updateOverrides ([
primarySwatchProvider . overrideWithValue (
StateController ( Colors . yellow ),
),
]);
break ;
case 'counter::large' :
providerContainer . updateOverrides ([
counterProvider . overrideWithValue (
StateController ( 99999999 ),
),
]);
break ;
default :
break ;
}
return '' ;
},
);
アプリの開始。UncontrolledProviderScopeでproviderContainerをWidgetツリーに流し込みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
runApp (
UncontrolledProviderScope (
container: providerContainer ,
child: Consumer (
builder: ( context , watch , _ ) {
Widget screen ;
switch ( watch ( screenProvider ). state ) {
case Screen . settings:
screen = SettingsScreen ();
break ;
default :
screen = MyHomePage ();
break ;
}
return MaterialApp (
title: 'Screenshot Demo' ,
debugShowCheckedModeBanner: false ,
theme: ThemeData (
primarySwatch: watch ( primarySwatchProvider ). state ,
brightness: watch ( brightnessProvider ). state ,
),
home: screen ,
);
},
),
),
);
これでアプリ側は準備完了です。
スクリーンショット撮影ユーティリティ
コードはこちら
FlutterDriverにはスクリーンショット取得をするscreenshot関数が用意されています。アニメーション待ち、ディレクトリ作成、ファイル保存なども行いたいのでユーティリティーとして以下のような関数を用意します。
1
2
3
4
5
6
7
8
9
10
11
12
Future < void > takeScreenshot ( FlutterDriver driver , String path ) async {
print ( 'will take screenshot $ path ' );
await driver . waitUntilNoTransientCallbacks ();
final pixels = await driver . screenshot ();
final dir = Directory ( '.screenshots' );
if ( ! dir . existsSync ()) {
dir . createSync ();
}
final file = File ( join ( dir . path , path ));
await file . writeAsBytes ( pixels );
print ( 'wrote $ file ' );
}
テストケースを実装する
コードはこちら
最後にテストケースを実装します。FlutterDriverと接続して、requestDataで撮影したい状況に画面を更新した後にスクリーンショット撮影ユーティリティーでファイル化します。
以下は初期画面を撮影するケースです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void main () {
group ( 'screenshots' , () {
FlutterDriver driver ;
setUpAll (() async {
driver = await FlutterDriver . connect ();
});
tearDownAll (() async {
if ( driver != null ) {
await driver . close ();
}
});
test ( 'home light' , () async {
await driver . requestData ( 'brightness::light' );
await takeScreenshot ( driver , 'home_light.png' );
});
});
}
他にも例えばダークモードで特定のカウンタの状況を写したい場合、設定画面を写したい場合など柔軟に撮影が可能です。
カウンターが99999999の状態を写したい場合など、実際にタップ操作で起こすのが困難なケースでもRiverpodでstateを管理していればすぐに実現可能です。
1
2
3
4
5
6
7
8
9
10
11
test ( 'home dark large counter' , () async {
await driver . requestData ( 'brightness::dark' );
await driver . requestData ( 'counter::large' );
await takeScreenshot ( driver , 'home_dark_large_counter.png' );
});
test ( 'settings dark' , () async {
await driver . requestData ( 'brightness::dark' );
await driver . requestData ( 'screen::settings' );
await takeScreenshot ( driver , 'settings_dark.png' );
});
Flutter Driverを実行
シミュレータを起動し、以下のコマンドでDriverを実行します。
1
% flutter driver --target= test_driver/app.dart
以下は撮影をしている様子です。スクリーンが変わっていきます。
VIDEO
指定したディレクトリには撮影した画像が残ります。
以上です。
まとめ
Widgetに与えるデータによって見た目が変わる要素をRiverpodで管理できていればスクリーンショットも撮りやすい。
UIをフレキシブルに変えられるFlutterだからこそ、リリースするアプリのスクリーンショットはいつでも簡単に撮影できるようにしておくとリリース時の手間も大きく減らせると思います。