Flutter - 멀티 페이지에 뒤로 가기 버튼 기능 구현하기, 커스터마이징하기 (WillPopScope)

반응형

 

1차 수정 (2023-10-13)

WillPopScope 버전 업데이트로 인한 마이그레이션 방법


Preview

이번 포스팅에서는... WillPopScope를 활용하여

  1. 앱 전반적인 모든 페이지에서 기능이 작동할 수 있도록 설정을 하고
  2. 뒤로가기 기능을 재정의(커스터마이징)하여 상황에 맞게 뒤로가기 기능을 활용

하는 것을 목표로 합니다.

 

 

포스팅에서 설명하는 소스 코드는 깃허브에서 확인 및 다운로드 가능합니다.

 

 

  • 뒤로가기 버튼을 누르면 앱 종료 알림이 뜨고, 확인을 누를 경우 앱 종료

 

 

  • 뒤로가기 버튼 한번을 누르면 하단에 알림이 뜨고, 다시 한번 누를 경우 앱 종료


WillPopScope

뒤로가기 버튼을 누를 때 WillPop에서 콜백함수를 호출하며, 반환값은 Future 타입을 반환

  • 즉, 앱에서 뒤로가기 버튼(Back Button)을 누를 경우 작동되는 이벤트를 정의할 수 있도록 해준다.

사용하는 이유

사용하는 이유에 대한 간단한 2가지의 예시를 들어보자.

  1. a, b, c 페이지가 있고, 각각의 페이지에 뒤로가기 버튼을 해야 할 때
    • 뒤로가기 버튼에 대한 기능 구현을 각각의 페이지에서 하면 너무 번거롭고 생산성도 떨어지게 된다.
    • 하물며 페이지가 많으면 많을수록 구현해야하는 작업량도 많아지게 된다.
  2. 특정 페이지에서 정보 입력을 위해 양식을 작성중이거나, 중요한 작업을 진행할 수 있다.
    • 뒤로가기 버튼을 누르게 되면 작업하던 정보를 잃게된다.
    • 뒤로가기 버튼을 무력화하여 정보 손실을 최소화하는 것도 하나의 방법이 될 수 있다.

 

즉, 'WillPopScope' 를 사용하여 위젯을 감싸면 뒤로가기 버튼 이벤트 자체를 가로채서 WillPop에서 뒤로가기 버튼 이벤트를 재정의 할 수 있다.


구문

WillPopScope(child: child, onWillPop: onWillPop)

 

child

  • WillPop을 이용하여 뒤로가기 버튼을 가로챌 위젯 나열
  • 반드시 null이 아닌 위젯으로 정의되어 있어야 한다.
  • 쉽게 말해서, Scaffold 위젯을 감싸게 되면 Scaffold 하위의 위젯들은 모두 뒤로가기 버튼에 대한 이벤트를 WillPop에서 공통적으로 다룰 수 있다.

onWillPop

  • 뒤로가기 버튼에 대한 콜백함수를 정의
  • 뒤로가기 버튼에 대한 기능 구현을 하는 영역을 의미한다.

구현 절차

  1. 위젯을 WillPopScope로 감싼다.
  2. onWillPop에 뒤로가기 버튼을 기능 구현할 콜백함수 코드를 작성한다.
    • 반환값의 true : 현재 페이지에서 사용자의 의도대로 뒤로가기 동작을 수행한다.
    • 반환값의 false : 뒤로가기 버튼의 기본 동작을 막고 현재 화면에 머무른다.
    • 즉, 반환값이 true일 경우 뒤로가기 버튼의 원래의 기능을 수행한다. (앱 종료나 뒤로가기 기능)
  3. child에 감쌀 위젯을 정의한다.
  @override
  Widget build(BuildContext context) {
    // 위젯을 감싸서 WillPop을 통해 뒤로가기 버튼 정의
    return WillPopScope(
      // 뒤로가기 버튼을 누를 경우의 이벤트 정의
      onWillPop: () async {
        // 뒤로가기 기능 구현 코드 작성
        return false;
      },
      // 감쌀 위젯 정의
      child: const Scaffold(
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }

 


예제 (Sample Code)

  • 주석으로 표시되어 있는 코드 작성 부분에 포스팅에 기재되어 있는 함수 호출
    • printMsg() : 메시지 콘솔에 출력
    • onBackPressed(context) : 종료 다이얼로그 출력
    • onBackDoublePressed(context) : 두 번 누를 경우 앱 종료
    • 사용자 정의 함수이므로 상황에 맞게 변경하여서 사용
void main() {
  runApp(
    const MaterialApp(
      home: MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
		/* 코드 작성 부분 */
        return false;
      },
      child: const Scaffold(
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }

뒤로가기 버튼 클릭 시 콘솔에 메시지 출력

// printMsg();
// onWillPop 콜백 함수에서 콘솔에 메시지를 출력하는 함수
  void printMsg() {
    print('WillPopCallback : Hello World!');
  }

 

  • 뒤로가기 버튼 클릭 시


뒤로가기 버튼 클릭 시 종료 알림 다이얼로그 출력

// 앱 종료를 위해 SystemNavigator.pop() 기능을 사용하기 위한 라이브러리 추가
import 'package:flutter/services.dart';


// onBackPressed(context);
// 뒤로가기 버튼 누를 경우 종료 다이얼로그 출력 함수
  Future<void> onBackPressed(BuildContext context) async {
    await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Center(child: Text('앱을 종료하시겠습니까?')),
        actions: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              // 취소 버튼
              ElevatedButton(
                child: const Text('취소'),
                onPressed: () => Navigator.pop(context),
              ),
              // 종료 버튼
              ElevatedButton(
                child: const Text('확인'),
                onPressed: () => SystemNavigator.pop(),
              ),
            ],
          ),
        ],
      ),
    );
  }

 

  • 뒤로가기 버튼 클릭 시
    • 취소 : 앱 화면으로 돌아오기
    • 확인 : 앱 종료하기


뒤로가기 버튼 두 번 클릭 시 앱 종료

이 함수는 두 번 클릭확인을 위해 추가 코드를 작성하여야 합니다.

  1. 시간 확인을 위한 시계열 타입 전역 변수
  2. onWillPop 콜백 함수에서 bool 타입의 반환값을 받아 처리하는 예외처리 코드

또한, 뒤로가기 버튼을 누를 경우 사용자에게 알림을 위한 간단한 UI를 추가해봅시다.

/* 시간 확인을 위한 전역 변수 선언 */
// 뒤로가기 버튼의 누른 시간을 저장하기 위한 변수
  DateTime? currentBackPressTime;


// onBackDoublePressed(context);
// 뒤로가기 버튼 두 번 누를 경우 앱 종료 함수
  Future<bool> onBackDoublePressed(BuildContext context) async {
    // 현재 시간 확인
    DateTime now = DateTime.now();

    // 뒤로가기 두 번 누를 경우의 종료 이벤트 정의
    // currentBackPressTime : 뒤로가기 버튼을 누른 시간을 저장하고 2초 내에 다시 버튼이 눌리는지 확인하기 위한 변수
    // 2초 후 뒤로가기 버튼 눌림 : if를 통한 반환값 : false (종료하지 않음)
    // 2초 내 뒤로가기 버튼 눌림 : if를 통한 반환값 : true (앱 종료)
    if (currentBackPressTime == null ||
        now.difference(currentBackPressTime!) > const Duration(seconds: 2)) {
      currentBackPressTime = now;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Row(
            children: [
              const Icon(
                Icons.notification_important,
                color: Colors.white,
              ),
              const SizedBox(width: 10),
              Expanded(
                child: Text(
                  '뒤로가기 버튼을 한 번 더 누르시면 종료됩니다.',
                  style: Theme.of(context)
                      .textTheme
                      .titleMedium!
                      .copyWith(color: Colors.white),
                ),
              ),
            ],
          ),
        ),
      );
      // 처음 뒤로가기 버튼을 눌렀거나 2초 이후에 누를 경우 : false
      return Future.value(false);
    }
    // 2초 이내에 뒤로가기 버튼을 또 눌럿을 경우 : true
    return Future.value(true);
  }
  
  
/* onWillPop 콜백 함 수 내의 코드에서 두 번 누를 경우의 예외 처리 추가 */
return WillPopScope(
  onWillPop: () async {
    // 뒤로가기 버튼 두 번 누를 경우 앱 종료
    if (await onBackDoublePressed(context)) {
      return true;
    }
    return false;
  },

 

  • 뒤로가기 버튼 한번 클릭 시
    • 사용자에게 뒤로가기 두 번 누를 경우에 대한 알림 추가 (앱의 하단 확인)

 

  • 뒤로가기 버튼을 한번 클릭 후, 2초 내에 다시 클릭 시
    • 앱 종료

전체 소스 코드

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(
    const MaterialApp(
      home: MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // 뒤로가기 버튼의 누른 시간을 저장하기 위한 변수
  DateTime? currentBackPressTime;

  @override
  Widget build(BuildContext context) {
    // 위젯을 감싸서 WillPop을 통해 뒤로가기 버튼 정의
    return WillPopScope(
      // 뒤로가기 버튼을 누를 경우의 이벤트 정의
      onWillPop: () async {
        // 콘솔에 메시지 출력
        //printMsg();

        // 뒤로가기 버튼 누를 경우 앱 종료 확인 다이얼로그 출력
        //onBackPressed(context);

        // 뒤로가기 버튼 두 번 누를 경우 앱 종료
        if (await onBackDoublePressed(context)) {
          return true;
        }

        // 뒤로가기 버튼 기본 기능 무력화
        return false;
      },
      // 감쌀 위젯 정의
      child: const Scaffold(
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }

  // onWillPop 콜백 함수에서 콘솔에 메시지를 출력하는 함수
  void printMsg() {
    print('WillPopCallback : Hello World!');
  }

  // 뒤로가기 버튼 누를 경우 종료 다이얼로그 출력 함수
  Future<void> onBackPressed(BuildContext context) async {
    await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Center(child: Text('앱을 종료하시겠습니까?')),
        actions: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              // 취소 버튼
              ElevatedButton(
                child: const Text('취소'),
                onPressed: () => Navigator.pop(context),
              ),
              // 종료 버튼
              ElevatedButton(
                child: const Text('확인'),
                onPressed: () => SystemNavigator.pop(),
              ),
            ],
          ),
        ],
      ),
    );
  }

  // 뒤로가기 버튼 두 번 누를 경우 앱 종료 함수
  Future<bool> onBackDoublePressed(BuildContext context) async {
    // 현재 시간 확인
    DateTime now = DateTime.now();

    // 뒤로가기 두 번 누를 경우의 종료 이벤트 정의
    // currentBackPressTime : 뒤로가기 버튼을 누른 시간을 저장하고 2초 내에 다시 버튼이 눌리는지 확인하기 위한 변수
    // 2초 후 뒤로가기 버튼 눌림 : if를 통한 반환값 : false (종료하지 않음)
    // 2초 내 뒤로가기 버튼 눌림 : if를 통한 반환값 : true (앱 종료)
    if (currentBackPressTime == null ||
        now.difference(currentBackPressTime!) > const Duration(seconds: 2)) {
      currentBackPressTime = now;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Row(
            children: [
              const Icon(
                Icons.notification_important,
                color: Colors.white,
              ),
              const SizedBox(width: 10),
              Expanded(
                child: Text(
                  '뒤로가기 버튼을 한 번 더 누르시면 종료됩니다.',
                  style: Theme.of(context)
                      .textTheme
                      .titleMedium!
                      .copyWith(color: Colors.white),
                ),
              ),
            ],
          ),
        ),
      );
      // 처음 뒤로가기 버튼을 눌렀거나 2초 이후에 누를 경우 : false
      return Future.value(false);
    }
    // 2초 이내에 뒤로가기 버튼을 또 눌럿을 경우 : true
    return Future.value(true);
  }
}

참고

 

반응형