하단 네비게이션 바에 대해 파던 중에, WillPopScope
클래스에 대해 알게 되었다. 이게 대체 뭐 하는 클래스인지 도저히 감이 안 와서 개인적으로 정리하기 위해 오랜만에 velog에 글을 써 본다.
WillPopScope 클래스 - Flutter 공식 API 문서에 따르면, WillPopScope
클래스는 이런 일을 한다고 한다:
Registers a callback to veto attempts by the user to dismiss the enclosing ModalRoute.
또 모르는 용어가 나왔다. ModalRoute
는 뭘까. 공식 API 문서에서는 이런 클래스라고 한다:
A route that blocks interaction with previous routes.
ModalRoutes cover the entire Navigator.
대충 알아보니 Flutter에서는 Navigator
클래스로 스택을 통해 애플리케이션의 페이지들을 관리하는 것 같다. 앱을 사용하다 보면 메인 화면에서 버튼을 눌러 다음 화면으로 넘어가고, 이러면 여러 페이지들 간에 Depth가 누적이 되니까.
이 때 Navigator
클래스가 Flutter의 페이지 간 기본적인 Route 방식이라면, 이를 대체할 수 있는 다른 수단으로 PopupRoute
, ModalRoute
, PageRoute
등이 존재하는 것이다.
어쨌든 상당히 정확하지 않은 개인적인 결론은, ModalRoute
에서 정해진 Route 관련 내용을 무시하고, 사용자가 직접 뒤로 가기 동작에 대한 Callback 함수를 지정할 수 있게 도와주는 클래스인 것 같다.
간단히 설명해보자면, WillPopScope
는 그냥 기본적인 위젯에 뒤로 가기 동작에 대한 커스터마이징을 제공해준다고 생각하면 될 것 같다. 그래서 기본적으로 표시될 위젯을 child
매개변수로 가지고 있다. 이 child
위젯을 WillPopScope
클래스로 감싸는 느낌.
그리고 뒤로 가기 동작이 감지되면, onWillPop
에서 정의된 async 함수를 Callback한다. 만약 onWillPop
함수가 true
를 반환할 경우, 현재 화면이 Pop된다. 그렇지 않을 경우, 현재 화면은 그대로 유지된다.
다시 말해서 onWillPop
함수가 true
를 반환하지 않을 경우, 뒤로 가기 버튼을 눌렀을 때 텍스트를 표시하는 등 다양한 동작으로 커스터마이징이 가능하다는 뜻.
생성자를 한 번 보자.
WillPopScope({
Key? key,
required Widget child,
required WillPopCallback? onWillPop
})
Widget child
: 기본적으로 표시될 위젯WillPopCallback onWillPop
: 뒤로 가기 동작이 감지될 시 실행될 함수방금 본 동작 방식에 관한 내용을 생각하면서, 공식 문서의 예제 코드를 보자.
import 'package:flutter/material.dart';
// 메인 함수
void main() => runApp(const MyApp());
// 주 위젯
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
// 주 위젯에 탑재될 Stateful 위젯
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
여기까지는 Flutter를 어느 정도 만져봤다면 다 아는 내용이니까 넘어가겠다.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool shouldPop = true;
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return shouldPop;
},
child: Scaffold(
appBar: AppBar(
title: const Text('Flutter WillPopScope demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
child: const Text('Push'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return const MyStatefulWidget();
},
),
);
},
),
OutlinedButton(
child: Text('shouldPop: $shouldPop'),
onPressed: () {
setState(
() {
shouldPop = !shouldPop;
},
);
},
),
const Text('Push to a new screen, then tap on shouldPop '
'button to toggle its value. Press the back '
'button in the appBar to check its behavior '
'for different values of shouldPop'),
],
),
),
),
);
}
}
위는 전체 코드고, 너무 기니까 부분적으로 나누어 보도록 하자.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool shouldPop = true;
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return shouldPop;
},
onWillPop
함수는 변수 shouldPop
을 반환한다. shouldPop
이 bool
자료형이니까 상황에 따라 false
가 될 수도 있다는 말.
OutlinedButton(
child: const Text('Push'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return const MyStatefulWidget();
},
),
);
},
)
이 버튼을 누르면 현재 페이지의 복제본이 그대로 Navigation
스택에 Push 된다. 10번 누르면 스택에 같은 페이지가 10개 쌓인다는 말. 이 상태에서는 뒤로 가기 버튼을 10번 눌러야 앱을 끌 수 있을 것이다.
어쨌든 중요한 건, 버튼을 눌렀을 때 작동하는 onPressed
함수에 Route 관련 동작이 정의되어 있다는 것. 만약 위에서 언급한 onWillPop
이 true를 return하지 않는다면 어떻게 될까?
OutlinedButton(
child: Text('shouldPop: $shouldPop'),
onPressed: () {
setState(
() {
shouldPop = !shouldPop;
},
);
},
),
이 버튼은 onWillPop
함수가 반환하는 bool
값을 반전시키는 버튼이다. 만약 현재 shouldPop
값이 true
일 경우, 이 버튼을 누르면 false
가 된다는 뜻.
만약 버튼을 누르게 되면 onWillPop
함수가 true
가 아닌 값을 반환하므로, 아까 Push 버튼에서 정의한 onPressed
함수 내의 Route 동작은 일어나지 않게 된다.
실행 결과가 궁금한 분들은 WillPopScope 클래스 - Flutter 공식 API 문서에 들러 직접 돌려보도록 하자.
다음에 할 것도 생겼다. ModalRoute가 뭔지 좀 정확히 이해할 수 있었으면 좋겠다.