Never give up

Flutter - Easy Overlay (feat. toast) 본문

Flutter

Flutter - Easy Overlay (feat. toast)

대기만성 개발자 2021. 8. 13. 22:47
반응형

Overlay Entry는 특정 상황에서 정말 유용한 기능입니다

 

Flutter는 기본적으로 toast message가 없기도 하고

 

snackbar만으로 표현하기에는 조금 아쉬운 상황이 있다보니

 

Overlay Entry를 이용해서 만들면 유용할 수 있습니다

(실제로 Overlay Entry를 이용해서 개발한 toast패키지도 본적이 있습니다)

 

아니면 특정 위젯을 눌렀을 때 선택할 수 있는

 

옵션 선택형 위젯을 만들어준다던가

 

검색창을 만들때 밑에 히스토리를 보여준다던가 추천검색어를 보여준다던가 말이죠

 

근데 사용하다보면 OverlayEntry, OverlayState 셋팅

 

그리고 상황에 따라 위치잡기가 까다로울 수도 있습니다

 

그래서 필자의 삽질과 고민 그리고 구글링을 통해

 

정말 쉽게 구현할 수 있는 방법을 찾아서 공유드리고자 합니다

 

main

class EasyOverlay extends StatelessWidget {
  EasyOverlay({Key? key, required this.title}) : super(key: key);

  final String title;

  final OverlayExample _example = OverlayExample();

  @override
  Widget build(BuildContext context) {
    _example.context = context;
    return Scaffold(
        appBar: AppBar(title: Text(title)),
        body: Center(
            child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
              ElevatedButton(
                  onPressed: _example.showAlert, child: Text('Show alert')),
              ElevatedButton(onPressed: _example.toast, child: Text('Toast'))
            ])));
  }
}

Overlay example

class OverlayExample {
  late BuildContext context;

  void showAlert() async {
    OverlayEntry _overlay = OverlayEntry(builder: (_) => const Alert());

    Navigator.of(context).overlay!.insert(_overlay);

    await Future.delayed(const Duration(seconds: 1));
    _overlay.remove();
  }

  void toast() async {
    OverlayEntry _overlay = OverlayEntry(builder: (_) => const Toast());

    Navigator.of(context).overlay!.insert(_overlay);

    await Future.delayed(const Duration(seconds: 2));
    _overlay.remove();
  }
}

Alert

class Alert extends StatefulWidget {
  const Alert({Key? key}) : super(key: key);

  @override
  _AlertState createState() => _AlertState();
}

class _AlertState extends State<Alert> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _animation;

  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 1));
    _animation = Tween<Offset>(begin: Offset(0.0, -1.0), end: Offset.zero)
        .animate(
            CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn));

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Align(
        alignment: Alignment.topCenter,
        child: Padding(
          padding: const EdgeInsets.only(top: 80.0),
          child: SlideTransition(
            position: _animation,
            child: Material(
              color: Colors.transparent,
              child: Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                    color: Colors.red, borderRadius: BorderRadius.circular(30)),
                child: Text(
                  'Alert',
                  style: TextStyle(
                      color: Colors.white, fontWeight: FontWeight.bold),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Toast

class Toast extends StatefulWidget {
  const Toast({Key? key}) : super(key: key);

  @override
  _ToastState createState() => _ToastState();
}

class _ToastState extends State<Toast> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));

    _animation = Tween(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(parent: _controller, curve: Curves.decelerate));

    _controller.forward().whenComplete(() {
      _controller.reverse();
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Align(
        alignment: Alignment.bottomCenter,
        child: Padding(
          padding: const EdgeInsets.only(bottom: 70),
          child: FadeTransition(
            opacity: _animation,
            child: Material(
              child: Container(
                  padding:
                      const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(20),
                      color: Colors.grey),
                  child: Text('Toast', style: TextStyle(color: Colors.white))),
            ),
          ),
        ),
      ),
    );
  }
}

 

 

< 쉽고 간단한 알림!? >

Navigator.of(Navigator state class)에 overlay 속성이 있습니다

 

이 속성을 이용하면 Overlay State를 불러올 수 있고

 

insert를 통해 overlay를 띄워줍니다

 

그리고 객체화 된 overlay의 remove를 이용해서

 

원하는 시점에 overlay를 없앨 수 있습니다

(상황에 따라 onTap등 callback을 이용해서 닫기도 가능하다는 이야기 입니다)

 

여기서 주의할점은 내부를 구현할 때

 

material이 없으면 원하는 모양대로 구현이 안되고, material을 최상단에 위치시키면

 

overlay가 전체 화면을 잡아먹어서(?) 다른부분 탭이 불가능 합니다

 

그래서 필자가 구현한것처럼 material이 필요한 child가 있는곳 바로 윗 부분에

 

붙여주는게 가장 깔끔하게 구현하기 좋은거 같습니다

 

그리고 더 더 더 중요한 포인트가 있습니다

 

overlay를 remove하지 않으면 다른화면으로 이동했을때도 따라오는 현상이 있을것이고

 

bool값을 통해 overlay가 있는지 없는지 체크해주지 않으면 골치아픈 상황이 생깁니다

 

예를들어 똑같은 overlay가 여러개 겹쳐지는 경우죠

 

이 경우 예기치 못한 동작이나 앱이 죽어버리는 상황이 생길 수도 있습니다

 

웬만하면 overlay는 한번 콜에 한번만 띄울 수 있는 로직을 만드는게 중요합니다

 

  bool isActive = false;
  
  void showAlert() async {
    if(!isActive) {
      isActive = true;
      OverlayEntry _overlay = OverlayEntry(builder: (_) => const Alert());

      Navigator
          .of(context)
          .overlay!
          .insert(_overlay);

      await Future.delayed(const Duration(seconds: 1));
      _overlay.remove();
      isActive = false;
    }
  }

 

이런식으로 말이죠

 

만약 특정 상황에 화면에 계속 띄워놔야된다 라고 하면

 

Stateful 같은 경우 dispose 쪽에서 remove하는 로직을 만들거나

 

Stateless 같은 경우 Navigator pop 되는 부분에 remove를 하는 부분을 만들어주면 됩니다

반응형
Comments