[Flutter] watch, read 대신 listen 함수 사용하기

겨레·2024년 7월 23일

📍 Listen

무언가 값이 변경됐는데 그 변경된 값에 의해 어떤 함수를 실행하고 싶을 때 listen을 실행하게 된다.

Provider를 쓰면서 listen을 쓸 수 있는데 좀 특이하게 쓴다!

지금까지 써봤던 listen들은 initState( ) 내에서 보통 사용했는데,
Riverpod의 Provider로 listen 할 땐, build 함수 안에서 바로 쓸 수 있다!


지금까지 watch 또는 read를 통해 실행봤는데, 대신 listen 함수를 사용해보자!



① screen/listen_provider_screen.dart 파일 생성

①-1. StatefulWidget을 ConsumerStatefulWidget으로 바꿔준다.
①-2. State가 있는 곳들을 ConsumerState로 바꿔준다


📍 Provider를 사용할 수 있는 위젯으로 바꿀 때

  • StatelessWidget
    ① StatelessWidget → ConsumerWidget으로 바꿔준다.
    ② build 함수에 WidgetRef ref 파라미터를 추가한다.

  • StatefulWidget
    ① StatefulWidget → ConsumerStatefulWidget으로 바꿔준다.
    ② State가 있는 곳들을 ConsumerState로 바꿔준다.

📍 StatefulWidget

StatefulWidget 같은 경우엔 Consumer로 전환하더라도 build 함수에 두 번째 파라미터를 받지 않고 바로 ref를 사용할 수 있음!



  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen> {
  
  Widget build(BuildContext context) {
    return const DefalutLayout(
      title: 'ListenProviderScreen',
      body: Column(children: []),
    );
  }
}

이렇게 수정해주면 StatefulWidget도 StatefulWidget의 모든 기능을 유지한채
Provider를 사용할 수 있는 위젯들을 만들어낼 수 있게 된다.



② 탭을 다시 한 번 만들어보자!

②-1. ListenProviderScreen에 TabBarView 코드 작성

  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen>
    with TickerProviderStateMixin {
  late final TabController controller;

  
  void initState() {
    super.initState();

    // 길이는 10개
    // vsync눈 this를 해줘야 함! => 대신 with TickerProviderStateMixin도 넣어줘야 함!
    controller = TabController(length: 10, vsync: this);
  }

  
  Widget build(BuildContext context) {
    return DefalutLayout(
      title: 'ListenProviderScreen',
      // 인덱스별로 인덱스 숫자만 보이게 생성할거임
      body: TabBarView(
        controller: controller,
        children: List.generate(
          10,
          (index) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                index.toString(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

②-2. HomeScreen에 ListenProviderScreen 버튼 추가

그럼 이렇게 홈화면에 버튼이 추가된 것을 볼 수 있다.

그리고 화면에도 숫자가 잘 나오고, 옆으로 스와이프를 하면 숫자도 잘 변경되는 걸 확인할 수 있다.




②-3. 스크롤로 인해 움직이지 못하게 physics를 주기

이렇게 해 주면 좌우로 넘겨도 스와이프되지 않음!
그럼 이걸 어떻게 움직일 수 있게 할 수 있을까? 👉 버튼을 만들어서 할거임!

  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen>
    with TickerProviderStateMixin {
  late final TabController controller;

  
  void initState() {
    super.initState();

    // 길이는 10개
    // vsync눈 this를 해줘야 함! => 대신 with TickerProviderStateMixin도 넣어줘야 함!
    controller = TabController(length: 10, vsync: this);
  }

  
  Widget build(BuildContext context) {
    return DefalutLayout(
      title: 'ListenProviderScreen',
      // 인덱스별로 인덱스 숫자만 보이게 생성할거임
      body: TabBarView(
        physics: const NeverScrollableScrollPhysics(),
        controller: controller,
        children: List.generate(
          10,
          (index) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                index.toString(),
              ),
              ElevatedButton(
                onPressed: () {},
                child: const Text('NEXT'),
              ),
              ElevatedButton(
                onPressed: () {},
                child: const Text('BACK'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

버튼만 만들어졌지 아직 각 버튼에 대한 스와이프 동작은 이루어지지 않고 있음!
이 동작이 되도록 만들어보겠음!


③ provider 생성하기
riverpod/listen_provider.dart 파일 생성
난 그냥 StateProvider로 만들거임!



④ ref.listen 해주기

ListenProviderScreen으로 돌아가서 ref.listen을 해준다.
(자동완성 왜 안되는거야...)



📍 ref.listen

ref.listen(provider, (previous, next) {  });
  • provider 👉 listen할 provider
  • previous 👉 기존 state (기존 상태)
  • next 👉 변경될 다음 상태


    📌 ref.listen 옆에 < > 제너릭을 작성해주면 previous, next에서 어떤 타입이 반환되는지 지정할 수도 있음!
ref.listen<int>(listenProvider, (previous, next)  {  });

④-1. listen에 그냥 print 해줘보기
④-2. ref.read 해주기
아래 ElevatedButton에 ref.read 해주기.

⭐ onPresseds

onPresseds는 무조건 read 해서 실행해줘야함!
onPresseds에서는 무언가 행동을 한 다음 provider를 가져올 때 무조건 read 함수를 씀!


  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/listen_provider.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen>
    with TickerProviderStateMixin {
  late final TabController controller;

  
  void initState() {
    super.initState();

    // 길이는 10개
    // vsync눈 this를 해줘야 함! => 대신 with TickerProviderStateMixin도 넣어줘야 함!
    controller = TabController(length: 10, vsync: this);
  }

  
  Widget build(BuildContext context) {
    // ref.listen 자동완성 왜 안됨?
    ref.listen(listenProvider, (previous, next) {
      print(previous);
      print(next);
    });

    return DefalutLayout(
      title: 'ListenProviderScreen',
      // 인덱스별로 인덱스 숫자만 보이게 생성할거임
      body: TabBarView(
        physics: const NeverScrollableScrollPhysics(),
        controller: controller,
        children: List.generate(
          10,
          (index) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                index.toString(),
              ),
              ElevatedButton(
                onPressed: () {
                  // 만약 state가 10이면 (최대 페이지까지 갔으면) 10을 반환,
                  // 아니면 state + 1을 반환
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 10 ? 10 : state + 1);
                },
                child: const Text('NEXT'),
              ),
              ElevatedButton(
                onPressed: () {
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 0 ? 0 : state - 1);
                },
                child: const Text('BACK'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}



이렇게 한 다음 NEXT 버튼을 누르면...
아래와 같이 출력된다.

❓ 0, 1이 나오는 이유는...
👉 ListenProviderScreen 코드 중에서 ref.listen 부분에서 print(previous); , print(next); 해둠.
이때 previous 원래 0이었기 때문에 0이 나오고, NEXT를 누르면 1이 나오게 되는 것!

NEXT를 또 누르면 1, 2가 나옴.
왜냐면 1로 바뀐 다음이기 때문에 previous는 1이고, +1을 했기 때문에 2가 나온 것!


BACK 버튼을 누르면?

3에서 2로, 2에서 1로 줄어드는 것을 볼 수 있다.

그렇다면... 이걸 사용해서 뭘 할 수 있을까?!!!


  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/listen_provider.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen>
    with TickerProviderStateMixin {
  late final TabController controller;

  
  void initState() {
    super.initState();

    // 길이는 10개
    // vsync눈 this를 해줘야 함! => 대신 with TickerProviderStateMixin도 넣어줘야 함!
    controller = TabController(length: 10, vsync: this);
  }

  
  Widget build(BuildContext context) {
    // ref.listen 자동완성 왜 안됨?
    ref.listen<int>(listenProvider, (previous, next) {
      // previous랑 next가 다르다면 controller에서 animateTo를 실행해 next 값으로 이동
      if (previous != next) {
        controller.animateTo(
          next,
        );
      }

      //print(previous);
      //print(next);
    });

    return DefalutLayout(
      title: 'ListenProviderScreen',
      // 인덱스별로 인덱스 숫자만 보이게 생성할거임
      body: TabBarView(
        physics: const NeverScrollableScrollPhysics(),
        controller: controller,
        children: List.generate(
          10,
          (index) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                index.toString(),
              ),
              ElevatedButton(
                onPressed: () {
                  // 만약 state가 10이면 (최대 페이지까지 갔으면) 10을 반환,
                  // 아니면 state + 1을 반환
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 10 ? 10 : state + 1);
                },
                child: const Text('NEXT'),
              ),
              ElevatedButton(
                onPressed: () {
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 0 ? 0 : state - 1);
                },
                child: const Text('BACK'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}




저장하고 앱 재실행해서 ListenProviderScreen 버튼을 눌러
ListenProviderScreen 화면으로 이동됐을 때
NEXT, BACK 버튼을 누르면 좌우로 탭이 이동되는 걸 볼 수 있다.

탭이 왜 이동될까?!!

ref.listen을 하면 이 listenProvider 값이 변경될 때마다

 (previous, next) {
      
      if (previous != next) {
        controller.animateTo(
          next,
        );
      }

이 함수가 실행되는데, 이 함수는 controller를 이용해서
현재 listenProvider 안에 있는 숫자값 0을 animateTo로 해서 인덱스로 변경해 주고 있음.

그렇기 때문에 NEXT를 누르면 listenProvider 안에 있는 숫자가
1에서 2가 되기 때문에 2라는 값을 controller에 넣게 되고,
다음 페이지로 이동되는 것!!

❓ 그럼... Listen의 경우엔 dispose를 따로 할 필요가 없는걸까?
그렇다! dispose를 따로 할 필요가 없다.

Riverpod에서 제공해주는 Listen의 경우엔 그냥 build 함수 안에서
중복으로 절대로 listen이 안되게 설계되어 있음.

그래서 아무 신경 쓰지 않고 initState에서 listen 해놓은 것처럼 그냥 listen해 주면 된다.


그런데 뒤로갔다가 다시 ListenProviderScreen으로 돌아오면 숫자가 0으로 변경된다.
그리고... NEXT 버튼을 누르면 1이 아니라 갑자기 3으로 뛴다.

왜 그럴까?

일단 TabBarView의 기본 값(인덱스)가 0이라서 0부터 시작하는 것!

_ListenProviderScreenState가 만약에 새로 생성된다면
initState를 무조건 부르게 되어 있는데,
initState 안에서 controller가 초기화되면서 initialIndex 안에
기존 listenProvider 안에 있는 값을 초기 index로 설정하라고 코드가 작성되어 있기 때문에
원래 index(마지막으로 누른 버튼의 숫자)로 돌아가는 것이다!

📍 initState( ) 안에서 ref 사용하는 방법

그냥 initialIndex 하고 ref를 써 주면 된다.
그리고 ⭐initState( )에서는 어떤 경우에서도 watch를 하면 안 된다!


  void initState() {
    super.initState();
    controller = TabController(
      length: 10,
      vsync: this,
      initialIndex: ref.read(listenProvider),
    );
  }

  • listen_provider_screen.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_study/layout/defalut_layout.dart';
import 'package:riverpod_study/riverpod/listen_provider.dart';

class ListenProviderScreen extends ConsumerStatefulWidget {
  const ListenProviderScreen({super.key});

  
  ConsumerState<ListenProviderScreen> createState() =>
      _ListenProviderScreenState();
}

class _ListenProviderScreenState extends ConsumerState<ListenProviderScreen>
    with TickerProviderStateMixin {
  late final TabController controller;

  
  void initState() {
    super.initState();

    // 길이는 10개
    // vsync눈 this를 해줘야 함! => 대신 with TickerProviderStateMixin도 넣어줘야 함!
    controller = TabController(
      length: 10,
      vsync: this,
      initialIndex: ref.read(listenProvider),
    );
  }

  
  Widget build(BuildContext context) {
    // ref.listen 자동완성 왜 안됨?
    ref.listen<int>(listenProvider, (previous, next) {
      // previous랑 next가 다르다면 controller에서 animateTo를 실행해 next 값으로 이동
      if (previous != next) {
        controller.animateTo(
          next,
        );
      }

      //print(previous);
      //print(next);
    });

    return DefalutLayout(
      title: 'ListenProviderScreen',
      // 인덱스별로 인덱스 숫자만 보이게 생성할거임
      body: TabBarView(
        physics: const NeverScrollableScrollPhysics(),
        controller: controller,
        children: List.generate(
          10,
          (index) => Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                index.toString(),
              ),
              ElevatedButton(
                onPressed: () {
                  // 만약 state가 10이면 (최대 페이지까지 갔으면) 10을 반환,
                  // 아니면 state + 1을 반환
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 10 ? 10 : state + 1);
                },
                child: const Text('NEXT'),
              ),
              ElevatedButton(
                onPressed: () {
                  ref
                      .read(listenProvider.notifier)
                      .update((state) => state == 0 ? 0 : state - 1);
                },
                child: const Text('BACK'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}



📑 이해를 위한 정리...

  • HomeScreen을 Tab으로 만들기
    TabBarView를 만들고 controller를 넣어주면 됨.
    그리고 이렇게 만든 controller로 화면 전환을 할 수 있음.

  • provider를 선언할 때, StateProvider로 0이라는 값(상태)을 넣음.

  • ListenProviderScreen로 돌아가보면...

    NEXT, BACK 버튼을 누를 때마다 ListenProvider 안에 있는 숫자 0이
    탭 index에 해당될테니 그 index를 변경해줘야 한다.
    (그 로직은 onPressde 안에 ref.read로 작성함!)

  • 그런데 문제는 뭐냐면...
    NEXT, BACK 버튼을 누름으로써 발생하는 state의 변경이
    controller와 연동이 안 되어 있다는 것!

뭐... onPressde 함수 안에 controller 코드를 작성해도 되겠지만,
그렇게되면 매번 상태를 바꾸면서 controller도 연동시켜야 하는 번거로움이 발생!

그리고 내가 원하는 것도 이게 아님!!!

  • 내가 원하는 건 이 상태가 변경이 될 때마다 controller가 자동으로 Tab을 변경해주는 거임!

    그래서... listen을 통해 provider를 리스팅하면서 값이 변경될 때마다
(previous, next) {
      // previous랑 next가 다르다면 controller에서 animateTo를 실행해 next 값으로 이동
      if (previous != next) {
        controller.animateTo(
          next,
        );
      }
 
    });
  • 이렇게 함수를 실행하면 변경된 이 previous, next 값에 의해서 controller를 실행할 수 있음!

    그래서 값이 변경될 때마다 controller에다가 animateTo를 실행해서
    원하는 탭의 인덱스로 이동할 수 있게 됨!
profile
호떡 신문지에서 개발자로 환생

0개의 댓글