PageView と page_indicator で作る説明画面を Stateful widget と Riverpod で作り比べてみた
作り比べてみました
TL;DR
- Stateful widget 版はこちらの記事を参考にしました。ほぼ一緒です。
- Stateful widget 版を作ってから Riverpod 利用に作り替えるのはそんなに詰まるところないと思います
- FlutterでPageViewにPageIndicatorを実装してみた | Qiita
- コード全体はこちら
- JUNKI555 / flutter_practice10 | GitHub
最終的な画面
見た目は↑の参考記事と同じです
最終的な lib 配下
使うパッケージ
- flutter_page_indicator | pub.dev
- よくある現在ページを表現する丸ぽち
- https://pub.dev/packages/flutter_page_indicator
- flutter_riverpod | pub.dev
- pedantic_mono | pub.dev
- mono さん製の強めのlinter
- https://pub.dev/packages/pedantic_mono
プロジェクトの準備
作るプロジェクト名は flutter_practice10
にしてます。
flutter create
して linter 用の analysis_options.yaml を用意します。
flutter create flutter_practice10
cd flutter_practice10
touch analysis_options.yaml
pubspec.yaml と analysis_options.yaml はこんな感じで。
name: flutter_practice10
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.0
flutter_page_indicator: ^0.0.3
flutter_riverpod: ^0.12.3+1
dev_dependencies:
pedantic_mono: any
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
include: package:pedantic_mono/analysis_options.yaml
説明画面 widget を用意する
PageView.children
に渡す widget として
この部分を独自 class に切り出して用意しておきます。
詳しい仕組み(?)は方法はこの記事に書きました
- flutter で widget を独自クラスに切り出す方法 | 北山淳也 | zenn
mkdir -p lib/views/widgets
touch lib/views/widgets/instruction_widget.dart
import 'package:flutter/material.dart';
class Instruction extends Column {
Instruction(
BuildContext context, String title, String image, String instruction)
: super(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
height: 150,
child: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 24,
color: Colors.black54,
),
),
),
),
SizedBox(
width: 300,
height: 200,
child: FlatButton(
color: Colors.black54,
onPressed: () {
Navigator.pushNamed(context, '/test');
},
child: Text(image),
),
),
Padding(
padding: const EdgeInsets.only(
top: 30,
),
child: SizedBox(
width: 300,
child: Text(
instruction,
style: const TextStyle(
fontSize: 14,
color: Colors.black54,
),
),
),
),
],
);
}
特に難しいことはやってないです。
- 縦に並べるので Column を継承
-
mainAxisAlignment: MainAxisAlignment.start,
で上寄せ -
SizedBox
で各要素の大きさを指定しつつ並べる - 一番下の説明文章は
Padding
を使って余白を開けている - 実際のプロダクトに使うときは中央の灰色塗りつぶしを画像とかにするといいんじゃないでしょうか
Stateful widget でつくる
PageViewでのページ切り替えの管理は
PageController がやってくれるのですが、
page_indicator(ページを表す丸ぽち)とボタンの表示切り替えを管理する
isFinalPage
は状態として管理してやる必要があります。
ここの表示切り替えですね。
Stateful widget での実装ではこの isFinalPage
の状態管理に setState()
を使います。
mkdir -p lib/views/screens
touch lib/views/screens/stateful_instruction_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_page_indicator/flutter_page_indicator.dart';
import '../widgets/instruction_widget.dart';
class StatefulInstructionPage extends StatefulWidget {
_StatefulInstructionPage createState() => _StatefulInstructionPage();
}
class _StatefulInstructionPage extends State<StatefulInstructionPage> {
PageController pageController;
bool isFinalPage = false;
void changePageNumber(int page, int pageLength) {
setState(() {
if (page + 1 == pageLength) {
isFinalPage = true;
} else {
isFinalPage = false;
}
});
}
void initState() {
pageController = new PageController();
super.initState();
}
void didUpdateWidget(StatefulInstructionPage oldWidget) {
super.didUpdateWidget(oldWidget);
}
Widget build(BuildContext context) {
final pageList = <Widget>[
Instruction(
context,
'アプリの説明1アプリの説明1',
'1枚目',
'アプリの説明1アプリの説明1アプリの説明1アプリの説明1',
),
Instruction(
context,
'アプリの説明2アプリの説明2',
'2枚目',
'アプリの説明2アプリの説明2アプリの説明2アプリの説明2',
),
Instruction(
context,
'アプリの説明3アプリの説明3',
'3枚目',
'アプリの説明3アプリの説明3アプリの説明3アプリの説明3',
),
Instruction(
context,
'アプリの説明4アプリの説明4',
'4枚目',
'アプリの説明4アプリの説明4アプリの説明4アプリの説明4',
),
];
return Scaffold(
appBar: AppBar(
title: const Text('アプリ説明'),
),
body: Column(
/* Expanded は Column, Row, Flex のいずれかにラップされなければならない */
children: <Widget>[
/* PageViewの切り替え制御を全画面で行うためにExpanded */
Expanded(
/* 全画面のPageView, その上にPageIndicator(と最終ページのボタン)を重ねるためにStack */
child: Stack(
children: <Widget>[
PageView(
scrollDirection: Axis.horizontal,
controller: pageController,
children: pageList,
onPageChanged: (pageCount) {
changePageNumber(pageCount, pageList.length);
},
),
Align(
alignment: const Alignment(0, 0.25),
child: AnimatedOpacity(
opacity: isFinalPage ? 0.0 : 1.0,
duration: const Duration(microseconds: 500),
child: PageIndicator(
layout: PageIndicatorLayout.NONE,
size: 20,
activeSize: 30,
controller: pageController,
space: 20,
count: 4,
color: Colors.black,
activeColor: Colors.red,
),
),
),
Align(
alignment: const Alignment(0, 0.25),
child: AnimatedOpacity(
opacity: isFinalPage ? 1.0 : 0.0,
duration: const Duration(microseconds: 500),
child: SizedBox(
width: 300,
height: 50,
child: RaisedButton(
color: Colors.blue,
onPressed: () {
Navigator.of(context).pop();
},
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
child: const Text(
'はじめる',
style: TextStyle(
fontSize: 20,
color: Colors.white,
),
),
),
),
),
),
],
),
),
],
),
);
}
}
コード内にも少しコメントを入れているので大体わかると思います。
-
changePageNumber()
でisFinalPage
の状態を更新 -
pageList
でPageView.children
を事前に定義 -
Expanded
はColumn
,Row
,Flex
のいずれかにラップされなければならないのでColumn
でラップ -
PageView
の切り替え制御を全画面で行うためにExpanded
を使用 - widget を重ねるために
Stack
- 以下の順で重なってる
-
PageView
が一番下 - その上に 丸ぽち
- その上に 「はじめる」のボタン
- 丸ぽちとボタンは実際は
isFinalPage
とAnimatedOpacity
で表示を切り替えしてるので重なってるというより表示/非表示の制御 - 丸ぽちとボタンは
Alignment
で中央やや下に配置することでPageView
部分の下に表示されるように見せている
- 丸ぽちとボタンは実際は
-
- 以下の順で重なってる
Riverpod でつくる
PageViewでのページ切り替えの管理は
PageController がやってくれるのですが、
page_indicator(ページを表す丸ぽち)とボタンの表示切り替えを管理する
isFinalPage
は状態として管理してやる必要があるのは Stateful widget での実装と同じです。
Riverpod での実装ではこの isFinalPage
の状態管理に
StateProvider
と Consumer
を使います。
touch lib/views/screens/riverpod_instruction_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_page_indicator/flutter_page_indicator.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/instruction_widget.dart';
final isFinalPageProvider = StateProvider<bool>((ref) => false);
class RiverpodInstructionPage extends StatelessWidget {
Widget build(BuildContext context) {
final pageController = new PageController();
final pageList = <Widget>[
Instruction(
context,
'アプリの説明1アプリの説明1',
'1枚目',
'アプリの説明1アプリの説明1アプリの説明1アプリの説明1',
),
Instruction(
context,
'アプリの説明2アプリの説明2',
'2枚目',
'アプリの説明2アプリの説明2アプリの説明2アプリの説明2',
),
Instruction(
context,
'アプリの説明3アプリの説明3',
'3枚目',
'アプリの説明3アプリの説明3アプリの説明3アプリの説明3',
),
Instruction(
context,
'アプリの説明4アプリの説明4',
'4枚目',
'アプリの説明4アプリの説明4アプリの説明4アプリの説明4',
),
];
return Consumer(
builder: (context, watch, child) {
final isFinalPage = watch(isFinalPageProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod版アプリ説明'),
),
body: Column(
/* Expanded は Column, Row, Flex のいずれかにラップされなければならない */
children: <Widget>[
/* PageViewの切り替え制御を全画面で行うためにExpanded */
Expanded(
/* 全画面のPageView, その上にPageIndicator(と最終ページのボタン)を重ねるためにStack */
child: Stack(
children: <Widget>[
PageView(
scrollDirection: Axis.horizontal,
controller: pageController,
children: pageList,
onPageChanged: (pageCount) {
if (pageCount + 1 == pageList.length) {
isFinalPage.state = true;
} else {
isFinalPage.state = false;
}
},
),
Align(
alignment: const Alignment(0, 0.25),
child: AnimatedOpacity(
opacity: isFinalPage.state ? 0.0 : 1.0,
duration: const Duration(microseconds: 500),
child: PageIndicator(
layout: PageIndicatorLayout.NONE,
size: 20,
activeSize: 30,
controller: pageController,
space: 20,
count: 4,
color: Colors.black,
activeColor: Colors.red,
),
),
),
Align(
alignment: const Alignment(0, 0.25),
child: AnimatedOpacity(
opacity: isFinalPage.state ? 1.0 : 0.0,
duration: const Duration(microseconds: 500),
child: SizedBox(
width: 300,
height: 50,
child: RaisedButton(
color: Colors.blue,
onPressed: () {
Navigator.of(context).pop();
},
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
child: const Text(
'はじめる',
style: TextStyle(
fontSize: 20,
color: Colors.white,
),
),
),
),
),
),
],
),
),
],
),
);
},
);
}
}
widget の構成はほぼ Stateful widget の実装と同じです。
- 状態管理を
StateProvider
とConsumer
でやるのでベースはStatelessWidget
-
final isFinalPageProvider = StateProvider<bool>((ref) => false);
で bool値のStateProvider
を用意 -
Consumer
配下のfinal isFinalPage = watch(isFinalPageProvider);
でStateProvider
を監視 -
isFinalPage.state
を set/get することで状態を変更/反映
アプリとして両方呼び出して試せるように仕上げ
PageView と page_indicator で作る説明画面を Stateful widget と Riverpod で作り比べてみる話としてはこれで終わりなのですが、
せっかくなのでアプリとして両方呼び出して試せるようにしてみましょう。
MyApp
┗ 呼び出し元(RootPage)
┣ StatefulInstructionPage(「はじめる」でRootPageに戻る)
┗ RiverpodInstructionPage(「はじめる」でRootPageに戻る)
こんな感じのイメージです。
MyApp の MaterialApp.routes
でルート定義をしておいて、
Navigator.of(context).pushNamed(<呼び出すルート名>);
で遷移、
Navigator.of(context).pop();
で呼び出し元に戻ります。
呼び出し元の RootPage はこんな感じに。
touch lib/views/screens/root_page.dart
import 'package:flutter/material.dart';
class RootPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Root Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RaisedButton(
child: const Text('StatefulWidget Instruction Page'),
onPressed: () {
Navigator.of(context).pushNamed('stateful');
},
),
RaisedButton(
child: const Text('Riverpod Instruction Page'),
onPressed: () {
Navigator.of(context).pushNamed('riverpod');
},
),
],
),
),
);
}
}
最後に main.dart でルーティング定義をしてやって終わりです。
ProviderScope()
でラップするのを忘れないように。
- Riverpod で Bad state: No ProviderScope found エラーに困った時のたったひとつのcoolな答え | 北山淳也 | zenn
import 'package:flutter/material.dart';
import 'package:flutter_practice10/views/screens/root_page.dart';
import 'package:flutter_practice10/views/screens/stateful_instruction_page.dart';
import 'package:flutter_practice10/views/screens/riverpod_instruction_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'PageView Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
routes: {
'/': (_) => RootPage(),
'stateful': (_) => StatefulInstructionPage(),
'riverpod': (_) => RiverpodInstructionPage(),
},
),
);
}
}
以上です。おつかれさまでした。
まとめ
実装にそんなに大差出ないですね。
この程度の単純な状態管理なら StatefulWidget
でやった方がいいわ! と考えるか
プロジェクトとしての状態管理手法が色々あると煩雑になるから
Riverpod で全部状態管理するように作るわ! と考えるかは
実装の方針次第かなと思います。
今回のリポジトリはこちらです。
Discussion