티스토리 뷰

Android 14부터 WillPopScope가 사라지게 된다고 함.

 

왜냐하면 Android 14부터 Predictive Back 기능이 생겼는데, 이 기능은 사용자가 뒤로 가기 제스처를 수행할 때 현재 화면 뒤를 살짝 보게 하여, 제스처를 계속할지 취소할지를 결정할 수 있게 함.

 

 

그러나 이는 사용자가 뒤로 가기 제스처를 받은 후 이를 취소할 수 있게 했던 Flutter의 기존 네비게이션 API와 호환되지 않음. Predictive Back에서는 제스처가 시작되면 즉시 애니메이션이 시작되어 제스처가 완료되기 전에 발생 -> 그래서 Flutter 앱은 그 시점에서 이를 허용할지 결정할 기회가 없고, 이러한 결정은 미리 되어 있어야 함.

 

 

이러한 이유로, Flutter 앱 개발자가 뒤로 가기 제스처를 받은 시점에서 네비게이션을 취소할 수 있는 모든 API가 더 이상 사용되지 않게 되었으며, 대신 항상 뒤로 가기 네비게이션이 가능한지를 나타내는 boolean 상태를 유지하는 API로 대체됨.

 

 

뒤로 가기 네비게이션이 가능(true)할 때 -> Predictive Back 애니메이션 발생

뒤로 가기 네비게이션 불가능(false)할 때 -> 네비게이션이 중단됨

 

 

그래서 기존의 WillPopScope의 경우에는 해당 액션(뒤로가기 버튼 누르기)이 발생했을 때, onWillPop이라는 메서드에서 어떻게 작동할지 정의했다면, PopScope는 해당 액션이 발생하기 전에 뒤로가기 네비게이션을 가능하게 할지를 결정하는 bool 값을 미리 canPop 속성에 정의해두고, 실제 해당 액션이 발생했을 때 발생될 코드는 onPopInvoked에 정의해주면 된다는 것 같음!

 

 

onPopInvoked메서드는 Navigation.pop이 호출되었음을 알려주는 콜백 타입의 함수. Navigation.pop의 성공여부를 didPop이라는 인자로 알려줌. 즉, Navigation.pop이 성공했으면 true, 실패했으면 false.

onPopInvoked메서드는 canPop의 값과 상관없이 뒤로가기 동작이 발생하면 무조건 호출되는 함수임.

 


 

 

 

canPop == false

    => 뒤로 가기 동작이 허용되지 않음

    => onPopInvoked의 didPop으로 false 값이 들어옴

 

 

canPop == true

    => 뒤로 가기 동작이 허용됨

    => onPopInvoked didPop으로 true값이 들어옴. 그러나 뒤로 가기가 실패했을 때는 false값이 들어올 수도 있음

 

 

 


 

 

보통 우리가 PopScope를 쓰는 이유는 뒤로 가는 동작을 막기 위함이니, canPop을 false로 코드를 작성해보겠음

canPop을 true 설정했을 때는 화면은 접어두겠음

더보기

canPop == true일 때는 그냥 뒤로 가기가 가능함

그러나 Navigator.pop이 실패할 가능성도 있으면 그에 대한 처리 코드는 넣어두었음.

//두 번째 화면 코드 (Sub Screen)
class PopScopeSubScreen extends StatefulWidget {
  const PopScopeSubScreen({super.key});

  @override
  State<PopScopeSubScreen> createState() => _PopScopeSubScreenState();
}

class _PopScopeSubScreenState extends State<PopScopeSubScreen> {
  Future<bool?> _showBackDialog() {
    return showDialog<bool>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('현재 창을 닫으시겠습니까?'),
          content: const Text(
            '입력 중이던 내용은 저장되지 않아요.',
          ),
          actions: <Widget>[
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('취소'),
              onPressed: () {
                Navigator.pop(context, false);
              },
            ),
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('나가기'),
              onPressed: () {
                Navigator.pop(context, true);
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    //⭐️⭐️⭐️
    return PopScope(
      //canPop 값만 true false로 바꿔가며 테스트
      canPop: true,
      onPopInvoked: (didPop) async {
        if (didPop) {
          return;
        }
        final bool shouldPop = await _showBackDialog() ?? false;
        if (context.mounted && shouldPop) {
          Navigator.of(context).pop();
        }
      },
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Sub Screen'),
        ),
        body: const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('canPop == true'),
            ],
          ),
        ),
      ),
    );
  }
}

 

 

첫 번째 화면 -> 두 번째 화면(Sub Screen)으로 간 뒤, 두 번째 화면에서 PopScope를 적용해보았음!

//두 번째 화면 코드 (Sub Screen)
class PopScopeSubScreen extends StatefulWidget {
  const PopScopeSubScreen({super.key});

  @override
  State<PopScopeSubScreen> createState() => _PopScopeSubScreenState();
}

class _PopScopeSubScreenState extends State<PopScopeSubScreen> {
  Future<bool?> _showBackDialog() {
    return showDialog<bool>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('현재 창을 닫으시겠습니까?'),
          content: const Text(
            '입력 중이던 내용은 저장되지 않아요.',
          ),
          actions: <Widget>[
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('취소'),
              onPressed: () {
                Navigator.pop(context, false);
              },
            ),
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('나가기'),
              onPressed: () {
                Navigator.pop(context, true);
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    //⭐️⭐️⭐️
    return PopScope(
      canPop: false,
      onPopInvoked: (didPop) async {
        if (didPop) {
          return;
        }
        final bool shouldPop = await _showBackDialog() ?? false;
        if (context.mounted && shouldPop) {
          Navigator.of(context).pop();
        }
      },
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Sub Screen'),
        ),
        body: const Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('canPop == true'),
            ],
          ),
        ),
      ),
    );
  }
}

 

 

 

 

이렇게 작동함!

 


 

그런데 앱을 만들다 보니, 네비게이션 스택에 화면이 하나밖에 없을 때 백버튼을 누르는 일도 발생!

그러면 스택에 그 화면 밖에 없어서 pop되고 나면 검은 화면이 나오게 되는데, 이것도 막아야 하는 일이 발생!

(※ 예시를 위해 만든 일시적으로 만든 화면이라 버튼명 및 레이블과 맞지 않음)

 

 

그런 경우에는 아래처럼 해볼 수 있겠음!

일단 두 번째면 화면으로 넘어갈 때 모든 화면들을 다 pop한 후, PopScopeSubScreen2(연결할 화면)라는 화면만 스택에 남도록 함

OutlinedButton(
  onPressed: () {
    Navigator.pushNamedAndRemoveUntil(
      context, Routes.popScopeSub2, (route) => false);
    },
  child: const Text('2. 네비게이션 마지막 스택일 때'),
)

 

그리고 두번째 화면에서는 canPop의 인자에 Navigator.of(context).canPop()이라는 인자를 넣어줌. Navigator.of(context).canPop()는 말 그대로 현재 네비게이터가 pop이 가능한지 bool 값으로 알려줌.

 

 

그리고 만약 pop이 불가능하다면, 현재 화면을 다른 화면으로 대체하는 Navigator.pushNamedReplacement()를 통해 다시 홈화면으로 오도록 했음.

(코드는 PopScope 위젯 부분만 담음)

PopScope(
  canPop: Navigator.of(context).canPop(),
  onPopInvoked: (didPop) async {
    if (didPop) {
      return;
    }
    final bool shouldPop = await _showBackDialog() ?? false;
      if (context.mounted && shouldPop) {
        Navigator.pushReplacementNamed(context, Routes.main);
      }
    }
  child: ...
}

 

다시 홈 화면으로 와서 동영상의 첫 화면 같지만, 첫 화면으로 온 거 맞음!

 


 

그런데 아직 해결 못한 게 있음.

iOS 부분임. PopScope를 iOS에 적용해보니, 문제가 발생.

 

 

canPop == false일 때, onPopInvoked 메서드가 호출되지 않기 때문에 어떠한 처리를 할 수가 없음.

chatGPT에게 물어보니, iOS의 경우 뒤로 가기 제스쳐가 CupertinoRouteTransitionMixin에 의해 처리되기 때문에 시스템 수준의 제스처로 인식되지 않아 'onPopInvoked' 메서드가 호출되지 않을 수도 있다고 함 

 

 

해결책으로는 CupertinoPageRoute를 사용하거나, WillPopScope를 사용하라는데...WillPopScope가 deprecated돼서 쓰는 건데,,,

이 부분은 다시 해결책을 찾아봐야겠음.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크