Flutter로 안드로이드 앱을 개발할 때에는 '뒤로 가기 버튼'의 동작을 핸들링하는 것이 중요합니다.
안드로이드에서는 위 이미지와 같은 '뒤로 가기 버튼'을 통해 현재 화면을 종료하고 이전 화면으로 돌아가는 기능을 제공합니다. 문제는 앱을 실행 중일 때에 뒤로 가기 버튼을 누르면 앱이 그대로 종료될 수 있다는 것입니다.
만약 앱 내에서 무언가 열심히 진행하다가 손이 미끄러져서(?) 뒤로 가기 버튼을 누르게 된다면??? 그 순간 앱이 곧바로 꺼지면서 휘발성 상태 값들이 사라지게 됩니다. 그렇기 때문에 안드로이드 앱을 개발할 때에는 사용자가 실수로 앱을 종료하지 않도록 뒤로 가기 버튼을 제어할 필요가 있습니다.
Flutter에서는 WillPopScope 위젯을 사용하여 이러한 동작을 제어할 수 있습니다. 저는 이를 보다 쉽게, 그리고 제 입맛대로 제어하기 위해 BackButtonHandler라는 커스텀 위젯을 만들어 사용하고 있습니다.
이번 글에서는 제가 직접 구현하여 사용하고 있는 BackButtonHandler에 대해 설명해 보겠습니다.
BackButtonHandler의 역할
BackButtonHandler는 Flutter에서 뒤로 가기 버튼의 동작을 제어하는 위젯입니다. 이 위젯은 사용자가 뒤로 가기 버튼을 두 번 눌러야만 앱이 종료되도록 하여 실수로 앱이 종료되는 것을 방지합니다. 뿐만 아니라 필요시 커스텀 동작까지 수행할 수 있도록 구현하였습니다.
BackButtonHandler의 구현
아래는 BackButtonHandler의 전체 코드입니다.
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class BackButtonHandler extends StatefulWidget {
final Widget child;
final GlobalKey<ScaffoldState>? scaffoldKey;
final Future<bool> Function()? onBackButtonPressed;
final double toastBottomOffset;
const BackButtonHandler({
super.key,
required this.child,
this.scaffoldKey,
this.onBackButtonPressed,
this.toastBottomOffset = 0,
});
@override
State<BackButtonHandler> createState() => _BackButtonHandlerState();
}
class _BackButtonHandlerState extends State<BackButtonHandler> {
DateTime? currentBackPressTime;
Future<bool> onWillPop() async {
if (widget.onBackButtonPressed != null) {
return widget.onBackButtonPressed!();
}
if (widget.scaffoldKey?.currentState != null &&
widget.scaffoldKey!.currentState!.isDrawerOpen) {
widget.scaffoldKey!.currentState!.closeDrawer();
return false;
}
DateTime now = DateTime.now();
if (currentBackPressTime == null ||
now.difference(currentBackPressTime!) > const Duration(seconds: 2)) {
currentBackPressTime = now;
FToast fToast = FToast();
fToast.init(context);
fToast.showToast(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
margin: EdgeInsets.only(bottom: widget.toastBottomOffset),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.black54,
),
child: const Text(
"'뒤로' 버튼을 한 번 더 누르면 종료됩니다.",
style: TextStyle(color: Colors.white),
),
),
gravity: ToastGravity.BOTTOM,
);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
currentBackPressTime = null;
});
}
});
return false;
}
return true;
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: onWillPop,
child: widget.child,
);
}
}
사용자가 뒤로 가기 버튼을 누를 경우, 상태에 따라 특정 동작을 수행하며 최종적으로 WillPopScope를 사용해 뒤로 가기 버튼을 제어합니다.
위 전체 코드를 단계별로 쪼개어 세부 구현을 설명해 보겠습니다.
세부 구현
1. 클래스 정의 및 생성자
class BackButtonHandler extends StatefulWidget {
final Widget child;
final GlobalKey<ScaffoldState>? scaffoldKey;
final Future<bool> Function()? onBackButtonPressed;
final double toastBottomOffset;
const BackButtonHandler({
super.key,
required this.child,
this.scaffoldKey,
this.onBackButtonPressed,
this.toastBottomOffset = 0,
});
@override
State<BackButtonHandler> createState() => _BackButtonHandlerState();
}
🥚 클래스 정의: BackButtonHandler는 StatefulWidget을 상속받아 상태를 관리합니다.
🥚 생성자: child, scaffoldKey, onBackButtonPressed, toastBottomOffset을 매개변수로 받습니다.
🍳 child는 이 위젯이 감싸는 하위 위젯을 의미합니다. BackButtonHandler는 child 위젯을 감싸고 해당 child 위젯의 뒤로 가기 버튼 동작을 제어합니다.
🍳 scaffoldKey는 Scaffold의 상태를 제어하기 위한 키입니다. 이 키를 통해 드로어(서랍형 UI)가 열려 있는지 확인하고, 필요할 경우 드로어를 닫을 수 있습니다. 이때 드로어가 있는 화면일 때 scaffoldKey를 받습니다. 드로어가 열려 있는 경우에는 앱을 종료하지 않고 드로어만 닫도록 동작합니다.
🍳 onBackButtonPressed는 뒤로 가기 버튼이 눌렸을 때 실행되는 커스텀 콜백 함수입니다. 예를 들어, 사용자가 앱을 종료하려고 할 때 사용자에게 확인 대화상자를 표시하고 사용자의 선택에 따라 앱의 동작을 결정하도록 커스텀할 수 있습니다.
🍳 toastBottomOffset는 토스트 메시지가 화면 하단에서부터 얼마나 떨어져서 표시될지를 결정하는 오프셋 값입니다. 원래라면 정해진 위치에만 토스트 메시지가 나타나지만, 해당 위치에 다른 요소들이 있어서 시각적으로 겹치거나 가릴 수 있기 때문에 오프셋 값을 받아 토스트 메시지의 위치를 조정하도록 구현했습니다.
2. 상태 클래스 및 변수 선언
class _BackButtonHandlerState extends State<BackButtonHandler> {
DateTime? currentBackPressTime;
🥚 상태 클래스: _BackButtonHandlerState는 BackButtonHandler의 상태를 관리합니다.
🥚 상태 변수 선언: currentBackPressTime은 사용자가 마지막으로 뒤로가기 버튼을 누른 시간을 기록합니다.
🍳 사용자가 뒤로가기 버튼을 처음 누르면 현재 시간을 currentBackPressTime에 저장하고, 두 번째로 뒤로 가기 버튼을 눌렀을 때 현재 시간과 저장된 시간의 차이를 계산합니다.
이 차이가 2초 이내라면 앱을 종료하고, 그렇지 않다면 다시 currentBackPressTime을 업데이트하여 다음 뒤로가기 버튼 입력을 기다립니다. 이를 통해 사용자가 실수로 앱을 종료하는 것을 방지할 수 있습니다.
3. onWillPop 메서드
Future<bool> onWillPop() async {
if (widget.onBackButtonPressed != null) {
return widget.onBackButtonPressed!();
}
if (widget.scaffoldKey?.currentState != null &&
widget.scaffoldKey!.currentState!.isDrawerOpen) {
widget.scaffoldKey!.currentState!.closeDrawer();
return false;
}
DateTime now = DateTime.now();
if (currentBackPressTime == null ||
now.difference(currentBackPressTime!) > const Duration(seconds: 2)) {
currentBackPressTime = now;
FToast fToast = FToast();
fToast.init(context);
fToast.showToast(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
margin: EdgeInsets.only(bottom: widget.toastBottomOffset),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.black54,
),
child: const Text(
"'뒤로' 버튼을 한 번 더 누르면 종료됩니다.",
style: TextStyle(color: Colors.white),
),
),
gravity: ToastGravity.BOTTOM,
);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
currentBackPressTime = null;
});
}
});
return false;
}
return true;
}
🥚 커스텀 콜백 실행: onBackButtonPressed가 정의되어 있으면 이를 실행합니다.
🥚 드로어 제어: 드로어가 열려 있으면 닫고 false를 반환하여 화면이 종료되지 않도록 합니다.
🥚 토스트 메시지: 사용자가 뒤로 가기 버튼을 두 번 눌러야 앱이 종료된다는 메시지를 표시합니다.
🥚 시간 차이 계산: 두 번의 버튼 클릭 사이의 시간이 2초 이상이면 currentBackPressTime을 갱신하고 토스트 메시지를 표시합니다.
4. build 메서드
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: onWillPop,
child: widget.child,
);
}
🥚 WillPopScope 사용: WillPopScope 위젯을 사용하여 뒤로가기 버튼의 동작을 onWillPop 메서드로 제어합니다.
🥚 child 위젯: BackButtonHandler가 감싸는 하위 위젯을 렌더링 합니다.
사용하는 방법
BackButtonHandler는 아래 예시처럼 사용합니다.
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) {
final scaffoldKey = GlobalKey<ScaffoldState>();
return BackButtonHandler(
scaffoldKey: scaffoldKey,
child: HomeScreen(scaffoldKey: scaffoldKey),
);
},
),
GoRoute(
path: '/notice',
builder: (context, state) => BackButtonHandler(
child: const NoticeScreen(),
),
),
...
HomeScreen의 경우 드로어(서랍 UI)를 포함하고 있기 때문에 scaffoldKey를 함께 주입합니다. 드로어를 포함하지 않은 NoticeScreen은 BackButtonHandler의 child로만 주입합니다.
BackButtonHandler를 적용한 결과, 아래 이미지처럼 토스트 메시지가 나타납니다. 이로써 사용자의 뒤로 가기 실수를 방지하고 앱을 안전하게 종료할 수 있도록 도와줍니다.
마무리
제가 구현한 BackButtonHandler는 Flutter 앱 개발 과정에서 안드로이드 기기의 뒤로 가기 버튼을 핸들링할 수 있는 위젯입니다. 사용자가 실수로 앱을 종료하는 것을 방지하고, 상황에 따라 다양한 커스텀 기능을 적용할 수 있습니다.
만약, 안드로이드 기기의 뒤로 가기 버튼 때문에 애먹으신 분이 계시다면 이 글에 나와있는 코드를 응용하여 자신만의 BackButtonHandler를 구현해 보시기 바랍니다.
'나는 이렇게 학습한다 > App' 카테고리의 다른 글
안드로이드 13+ 버전에서 이미지 권한 처리하기 (0) | 2025.02.02 |
---|---|
Flutter 앱에서 iOS 포그라운드(실행 중) 알림 동작 설정하기 (0) | 2024.11.27 |
Flutter TextField에서 한글 입력 시 자음과 모음이 분리되는 문제 해결하기 (1) | 2024.11.20 |
Flutter FloatingActionButton 터치 시 버튼 모양 불일치 해결하기 (0) | 2024.11.19 |
Flutter 앱 아이콘과 이름, 스플래시 화면 설정하기 (0) | 2024.11.18 |