포스트

chatgpt로 simple flutter code 만들기(17) - ListView 에 loadingIndicator 넣기 +alpha

ListView를 맨 아래까지 스크롤하면 원형 로딩 인디케이터가 돌아가면서 화면이 refresh되는 것을 본 적이 있습니다.

이번에는 그 기능이 필요하여 저의 앱에도 구현했습니다.

저는 listView 위젯에 순서를 재설정 할 수 있는 ReorderableListView이지만, listView에 사용할 때와 똑같이 인디케이터를 구현합니다.

isRefreshing 변수를 provider를 사용해서 읽어옵니다. isRefreshing은 isRefreshingProvider를 watch해서 읽습니다.

isRefreshingProvider에는 또 다시 내부적으로 두 개 변수를 가지는 IsRefreshingState라는 커스텀 클래스 타입의 데이터를 취급합니다. 두 가지 변수가 필요했던 이유는, 실제로 refresh를 켜고 끄는 스위치 역할 변수와, loadingIndicator를 켜고 끄는 스위치 역할 변수를 분리할 필요가 있어보였기 때문입니다.

실제 refresh는 매우 빠르게 끝날 수 있어서 loadingIndicator가 거의 보이지 않고 사라질 수 있기 때문에,

loadingIndicator를 refresh의 종료시점으로부터 1초 더 보이도록 하는 데에 이 두 변수를 사용했습니다.

1
2
3
4
final isRefreshingProvider = StateProvider((ref) {
  return IsRefreshingState();
});

1
2
3
4
5
6
7
8
9
10
11
12
13
class IsRefreshingState {
  final bool isRefreshing;
  final bool isRefreshingIndicatorVisible;

  IsRefreshingState({this.isRefreshing = false, this.isRefreshingIndicatorVisible = false});

  IsRefreshingState copyWith({bool? isRefreshing, bool? isRefreshingIndicatorVisible}) {
    return IsRefreshingState(
      isRefreshing: isRefreshing ?? this.isRefreshing,
      isRefreshingIndicatorVisible: isRefreshingIndicatorVisible ?? this.isRefreshingIndicatorVisible,
    );
  }
}

기능적으로 지금 설명드리려는 loadingIndicator와는 상관이 없지만,

부가적으로 제 코드를 이해하기 위해 나머지 변수를 설명드리겠습니다.

selectedItem은 어떤 리스트 그룹을 조작하고 있는지 provider로 읽어와서 담는 변수입니다. 그 안에는 리스트를 이루는 각 요소인 selectedItem.sites가 있습니다.

isEditMode는 리스트의 요소의 정보(siteInfo)나 정렬순서(index)를 조작하는 중인지 확인하고 저장하거나 읽어오기 위한 스위치입니다. 마찬가지로 provider로 읽어옵니다.

저의 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class MyHomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final selectedItem = ref.watch(drawerItemProvider);
    final isEditMode = ref.watch(editModeProvider);
    
    IsRefreshingState isRefreshing = ref.watch(isRefreshingProvider);

    return Scaffold(
      // ... other properties ...
      body: selectedItem == null
          ? Center(child: Text('Select a category'))
          : NotificationListener(
              onNotification: (ScrollNotification scrollInfo) {
                if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !isRefreshing) {
                  // Call refresh only if not already refreshing
                  _refreshDrawerState(ref);
                }
                return true;
              },
              child: ReorderableListView.builder(
                itemCount: selectedItem.sites.length + (isRefreshing.isRefreshingIndicatorVisible ? 1 : 0),
                 // +1 for loading indicator
                itemBuilder: (context, index) {
                  if (index == selectedItem.sites.length) {
                    // Footer with loading indicator only if refreshing
                    return LoadingIndicatorFooter(key: ValueKey('footer'));
                  }
                  var siteInfo = selectedItem.sites[index];
                  return EditableSiteItem(
                    key: ValueKey(siteInfo),
                    siteInfo: siteInfo,
                    index: index,
                    isEditMode: isEditMode,
                  );
                },
                onReorder: (int oldIndex, int newIndex) {
                  // Your reorder logic
                },
              ),
            ),
    );
  }

  Future _refreshDrawerState(WidgetRef ref, DrawerItem drawerItem) async {
    // Set refreshing state to true

    // Add your refresh logic here
    // ...

    // Once refresh is complete, set refreshing state to false
  }
}

ListView에서 요소의 개수는 isRefreshingIndicatorVisible가 true일 경우만 한 개(+1)가 늘어나도록 하고, false일 경우 다시 제자리(+0)로 돌아오도록 했습니다.

itemCount: selectedItem.sites.length + (isRefreshing.isRefreshingIndicatorVisible ? 1 : 0),

refresh할 때의 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  Future _refreshDrawerState(WidgetRef ref, DrawerItem drawerItem) async {
    // Implement your logic to refresh the drawer's state

    // Set refreshing state to true
    ref
        .read(isRefreshingProvider.notifier)
        .state = IsRefreshingState(
        isRefreshing: true,
        isRefreshingIndicatorVisible: true
    );

    // Delay the state update to after the build phase
    ref
        .read(drawerItemProvider.notifier)
        .state = drawerItem;

    printDebug("Refreshed drawer state");

    // Once refresh is complete, set isRefreshing state to false
    ref
        .read(isRefreshingProvider.notifier)
        .state = IsRefreshingState(
        isRefreshing: false,
        isRefreshingIndicatorVisible: true,
    );

    // Wait for 1 second before hiding the loading indicator
    await Future.delayed(const Duration(seconds: 1)); // Adjust delay as needed

    // Once refresh is complete, set isRefreshingIndicatorVisible state to false
    ref
        .read(isRefreshingProvider.notifier)
        .state = IsRefreshingState(
      isRefreshing: false,
      isRefreshingIndicatorVisible: false,
    );
  }

로딩 인디케이터는 다음과 같이 만들었습니다. 색상은 파란색입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoadingIndicatorFooter extends StatelessWidget {
  const LoadingIndicatorFooter({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 20.0),
      alignment: Alignment.center,
      child: CircularProgressIndicator(
        color: Colors.lightBlueAccent,
      ),
    );
  }
}

아래로 스크롤하면 원형으로 파란색 인디케이터가 1초 이상 돌아갑니다.

[영상]

아래 영상은 제가 추가한 다른 기능을 사용해서 리스트가 업데이트해야 할 요소를 인위적으로 만들어 테스트했습니다.

그 다른 기능은 바로 웹뷰와, 웹뷰 내에 있는 즐겨찾기추가 버튼입니다.

웹뷰에서 우측하단 '즐겨찾기' 별 모양 버튼을 누르면 빠르게 최근 선택되었던 즐겨찾기 그룹에 저장되도록 하는 기능을 같이 시연하였습니다.

새로운 site의 추가로 인해 리스트뷰에서는 요소의 변경이 일어났고,

provider를 다시 읽어 업데이트 해줘야 하는 부분이 있습니다.

이를 위해 아래로 스크롤을 해서 추가된 요소를 리스트에 반영하게 됩니다.

반영은 순식간에 일어나고, 남은 1초 동안 인디케이터가 더 회전하게 되는 모습을 보여줍니다.

[영상]

이 앱이 이렇게 보완하고 추가할 점이 많을 지 몰랐습니다. 그리고 그것들을 제가 업데이트를 하게 될 줄은 더 몰랐습니다. 자잘한 기능들이 내 앱에 없어서 앱이 쓸모없고 부족해보이는 게 싫었던 것 같습니다. 감사하게도 chatgpt가 있어서 기능들을 찾아서 구현해나가는 데에 크게 어렵지는 않았습니다. 사실 chatgpt가 없었으면 기능 하나조차도 노력과 에너지를 들이는 수지타산이 안맞아서 할 엄두를 못 냈을 것 같습니다. 안되는 거 붙잡다가 속이 타들어가기 전에 기능 구현을 완성할 수 있다는 점이 진짜 삶을 윤택하게 해주는 것 같습니다. 필요한 기능을 말로 잘 설명해주기만 하면 chatgpt라는 선생님이 옆에서 과외해주고 보조해준다는 점이 너무 편리하고 고맙습니다. ㅎㅎ 기왕 다같이 chatgpt쓰는거, 저도 실컷 써줘야 겠습니다.

sticker

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.