SnackBarの処理(Scaffold.of(context).showSnackBar)を参考にしてStreamも交えつつ、
ダイヤログを自作する記事になります。
この記事は仕組みについて簡単な解説になります。
上記のサンプルについてGitHubに公開しておりますので下記のURLを踏んでください。
尚、Blocの実装についてある程度経験がある前提で進みます。
1.Preliminary
Flutterのネイティブ機能にダイヤログ関連の処理(showDialog)が備わっており、
特に不自由なくアプリ作成を楽しんでいると思います。(この記事の意義とは)
なんやかんやの個人開発でDialog機能に下記のような物足りなさを感じて自作することになりました。
- 画面を表示したタイミングでダイヤログを表示させたい
(build段階でDialog表示の処理を入れてしまった。。)
- Dialogが二重に表示されて辛い。
- NavigatorされないDialogが欲しい。
2. AppProvider
Dialogの状態をアプリ全体で共有できるようにします。
void main() => runApp(AppProvider(child:MyApp()));
class AppProvider extends StatefulWidget {
final Widget child;
DialogBloc dialogBloc;
AppProvider({
Key key,
@required this.child
}):super(key: key){
dialogBloc = DialogBloc();
}
static _AppProvider of(BuildContext context) {
return context.inheritFromWidgetOfExactType(_AppProvider);
}
// Dialogを表示
static void showDialog(BuildContext context,BaseDialog dialog) {
AppProvider.of(context).dialogBloc.showDialog(dialog: dialog);
}
// Dialogを非表示
static void popDialog(BuildContext context) {
AppProvider.of(context).dialogBloc.popDialog();
}
// 不要なDialogがBloc内にあるのかチェック
static void inspectDialog(BuildContext context) {
AppProvider.of(context).dialogBloc.inspectDialogs();
}
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return AppProviderState();
}
}
class AppProviderState extends State<AppProvider> {...}
class _AppProvider extends InheritedWidget {...}
3. DialogBloc
DialogBlocの主な役割は以下になります。
- AppInjectorから引き渡されたDialogの構成要素をスタック配列として格納し、後入れを表示できるように制御する
- 表示、非表示などDialogの制御要素が更新された際に、StreamController介してsubscribeしているWidgetに通知する。
class DialogBloc {
List<DialogStateController> controllers;
StreamController<List<DialogStateController>> statesStreamController;
Stream get statesStream => statesStreamController.stream;
Sink get statesSink => statesStreamController.sink;
DialogBloc(){
controllers = [];
statesStreamController = StreamController<List<DialogStateController>>();
statesSink.add(controllers);
}
void showDialog({BaseDialog dialog}) async {
//後入れのDialogのみを表示できるように、先入れのDialogを非表示に更新
if(controllers.length > 0) {
controllers.last.state = DialogState.hide;
}
controllers.add(DialogStateController(dialog: dialog));
statesSink.add(controllers);
}
void popDialog() async {
if(controllers.length == 0) return;
//後入れのDialogを削除に更新
controllers.last.state = DialogState.dismiss;
//先入れのDialogを表示に更新
if( controllers.length >= 2) {
controllers[controllers.length - 2].state = DialogState.show;
}
statesSink.add(controllers);
}
void inspectDialogs() async {
//削除対象であれば削除
controllers = controllers.where((dialog)=> dialog.state != DialogState.dismiss).toList();
statesSink.add(controllers);
}
}
Dialogの構成要素を格納
enum DialogType {
center,
bottom,
}
class BaseDialog {
Widget child;
Function outSideTapped;
DialogType type;
BaseDialog({
this.child,
this.outSideTapped,
this.type = DialogType.center
});
}
Dialogの制御要素を格納
enum DialogState {
show,
hide,
dismiss,
}
class DialogStateController {
BaseDialog dialog;
DialogState state = DialogState.show;
DialogStateController({
@required this.dialog,
});
bool get isShow {
return state == DialogState.show;
}
}
4. DialogScreen
DialogScreenが画面の上位レイヤーに表示されるようにします。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Stack(
children: <Widget>[
MyHomePage(),
DialogScreen(
dialogBloc: AppProvider.of(context).dialogBloc,
)
],
),
);
}
}
DialogScreenの仕組みは簡単で、DialogBlocの制御要素をSubscribeし、通知された際に画面を再描画します。
class DialogScreen extends StatefulWidget{
final DialogBloc dialogBloc;
DialogScreen({this.dialogBloc});
@override
State<StatefulWidget> createState() {
return DialogScreenState();
}
}
class DialogScreenState extends State<DialogScreen> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
// DilogBlocの制御情報(DialogStateController)をSubscribe
return StreamBuilder<List<DialogStateController>>(
stream: widget.dialogBloc.statesStream,
builder: (context, snapshot) {
if(!snapshot.hasData || snapshot.data.length == 0) {
return Align(
alignment: Alignment.topLeft,
child: SizedBox(height: 0,width: 0)
);
}
List<Widget> widgets = [];
// Dialogの背景
widgets.add(Positioned(
top: 0,
left: 0,
bottom: 0,
right: 0,
child: GestureDetector(
onTap: snapshot.data.last.dialog.outSideTapped ?? (){
AppProvider.popDialog(context);
},
child:
FadeAnimationWrap(
isShow: widget.dialogBloc.controllers.length > 0,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
child: Container(
color: Color.fromARGB(40, 0, 0, 0),
),
)
),
),
),
),);
// Dialogの構成情報より画面を生成
snapshot.data.forEach(
(controller){
widgets.add(
(){
switch(controller.dialog.type){
case DialogType.bottom:
return modalDialog(controller);
default :
return centerDialog(controller);
}
}()
);
}
);
return
Container(
child: Stack(
children: widgets
),
);
},
);
}
Widget centerDialog(DialogStateController controller){
return Align(
alignment: Alignment.center,
child: FadeAnimationWrap(
isShow: controller.isShow,
child: Container(
child: controller.dialog.child,
padding: EdgeInsets.fromLTRB(24, 0, 24, 0),
),
animateStateListener: (state){
switch(state){
case AnimationStatus.dismissed:
AppProvider.inspectDialog(context);
break;
default: break;
}
}
)
);
}
Widget modalDialog(DialogStateController controller){...}
5. Implement Custom Dialog
こんな感じに実装できます。
class MyHomePage extends StatefulWidget {...}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: 0,
left: 0,
right: 0,
bottom: 0,
child:Scaffold(
backgroundColor: Colors.yellow,
body: Center(
child:Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
onPressed: (){
showScreenDialog();
},
child: Text(
'Show Dialog',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.blueAccent)
),
),
],
)
),
)
)
],
);
}
void showScreenDialog() {
AppProvider.showDialog(
context,
BaseDialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
DialogContentsWarp(
child: Center(
child: Text(
"Hello World \n No Border",
style: Theme.of(context).textTheme.title,
),
)
),
SizedBox(
height: 24,
),
DialogButton(
isBottomCornerRound: true,
isTopCornerRound: true,
txt: "Close",
onPressed: (){
AppProvider.popDialog(context);
},
),
],
)
)
);
}
}
6. Summary
仕組みについての簡単な説明は以上になります。
最初に紹介したSample実装例はGitHubにありますので参考になればと思います。