[Flutter]상태관리와 Stateful Widget의 UI 갱신 이슈 해결기록

한상욱·2024년 12월 31일
0

Flutter

목록 보기
28/29
post-thumbnail

들어가며

Flutter의 앱의 규모가 커질수록 상태관리는 아주 효과적인 솔루션입니다. 대표적으로 Provider, BLoC, RiverPods 등등이 있습니다.

보통 이러한 경우에는 Stateless Widget으로도 UI를 만들어낼 수 있지만, 애니메이션 또는 커스텀 위젯에 경우 Stateful Widget을 이용하여 UI를 제작합니다.

Stateful Widget는 자체적인 상태를 갖고 lifecycle을 통해 관리할 수 있습니다. 이를 알아보고 상황에 따라 적절한 솔루션을 찾는 방법에 대해서 알아봅시다.

Stateful Widget

Stateful Widget은 자체적인 상태를 갖습니다. Stateful Widget만으로도 훌륭하게 상태를 관리하며 UI를 재빌드할 수 있습니다. 보다 자세한 설명은 아래 링크에서 확인할 수 있습니다.
[Flutter]StatelessWidget & StatefulWidget

상태관리 라이브러리와 함께 사용하는 Stateful Widget

하지만 잘못된 방법을 통해 접근하게 되면 Provider와 같은 상태관리 라이브러리를 사용해도 UI가 의도한 대로 빌드되지 않는 경우가 있습니다.

제가 직접 제작한 Stateful Widget은 children을 전달받아 한 배열당 4개의 원소를 갖는 3차원 배열로 변환한 뒤 UI로 랜더링 합니다.

  List<List<List<dynamic>>> convertTo3D(List<Ingredient> children) {
    final oneDArray = children.map((i) => _buildItem(i)).toList();
    int groupSize = widget.rowCount * 4;

    // 3차원 배열을 저장할 리스트
    List<List<List<Widget>>> threeDArray = [];

    // 배열을 8개씩 나누어 처리
    for (int i = 0; i < oneDArray.length; i += groupSize) {
      // 8개씩 자른 서브 리스트를 만듦
      List<Widget> sublist = oneDArray.sublist(i,
          i + groupSize > oneDArray.length ? oneDArray.length : i + groupSize);

      // 마지막 그룹이 8개 미만이면 Empty Item으로 채움
      if (sublist.length < groupSize) {
        sublist = sublist +
            List.generate(
                groupSize - sublist.length, (index) => _generateEmtpy());
      }

      // 3차원 배열 규칙에 맞게 추가
      threeDArray.add(List.generate(widget.rowCount,
          (index) => sublist.sublist(index * 4, (index + 1) * 4)));
    }

    return threeDArray;
  }

이를 Stateful Widget이 생성되는 시점인 initState에서 호출하여 앱이 한번 빌드되면 children을 3차원 배열로 변환하게 됩니다.

  
  void initState() {
    _items = convertTo3D(widget.children);
    _totalPage = widget.children.length ~/ (4 * widget.rowCount) + 1;
    _pageController = PageController();
    _tabController = TabController(length: _totalPage, vsync: this);
    super.initState();
  }

그리고 이 Stateful Widget은 Consumer를 통해서 viewModel인 Provider의 값이 변경되는 경우마다 재빌드합니다.

  Widget _freezer() {
    return Padding(
      key: const Key("freezer"),
      padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0),
      child: Consumer<IngredientViewModelImpl>(
          builder: (context, provider, child) {
        return RefreginatorContainer(
            label: "냉동 보관", children: provider.myFreezedIngredients);
      }),
    );
  }

만약 여기서 children이 사용자의 입력을 통해서 변하게 된다면 어떻게 될까요? 이론적으로는 Consumer를 통해서 변화를 감지하게 되고, 재빌드가 되어 변경된 개수에 따른 UI가 나타나야 될 것 같지만 실제로는 그렇지 않습니다.

LifeCycle

여기서 LifeCycle이라는 개념이 등장합니다. 사실, 눈치가 빠른 사람은 이미 원인을 알아냈을 것입니다.

우리는 children을 3차원으로 변환하는 과정을 initState 내부에서 진행했습니다. initState는 위젯이 처음 생성되는 순간에 딱 한번만 실행하게 됩니다.

Provider에서 상태가 변경됨을 감지하게 되면 Consumer는 하위 위젯을 재빌드하게 되는데요. 이때는 하위 위젯트리의 build 메소드만을 재실행합니다. initState는 build보다 먼저 실행되므로 build가 실행된다고 해서 3차원 배열로 바뀐 _items는 그대로 원래의 원소를 유지하는 것이죠.

여담으로, 그렇기에 공식문서에서 Provider를 StatefulWidget에서 접근할 때, build 메소드내부에서 사용하라고 권장하는 것입니다.

initState, dispose는 각각 위젯의 최초 생성과 위젯의 삭제에서 한번씩 실행합니다. 그렇다면 의존성이 변경된 경우에는 어떠한 메소드를 사용해야될까요? 정답은 didUpdateWidget 메소드입니다.

  
  void didUpdateWidget(covariant RefreginatorContainer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.children.length != widget.children.length) {
      _tabController.dispose();
      _items = convertTo3D(widget.children);
      _totalPage = widget.children.length ~/ (4 * widget.rowCount) + 1;
      _tabController = TabController(length: _totalPage, vsync: this);
    }
  }

didUpdateWidget 메소드는 oldWidget을 통해 변경 이전의 위젯의 상태에 접근이 가능합니다. 이를 통해서 상태가 변경되었을 때, 본래의 children의 변경이 생긴다면 다시 3차원 배열로 변환해 주어야 하는 것이죠. 저는 children의 크기가 TabController와 다른 변수에도 영향을 끼쳤기에 함께 갱신하는 코드가 작성되어 있습니다. 이를 통해서 상태가 변경되면 Stateful Widget은 didUpdateWidget 메소드를 통해서 다시 3차원 배열로 변환하는 작업을 수행하여 의도했던 UI를 빌드하게 됩니다.

변경 전변경 후
변경 전 UI 갱신 제대로 X변경 후 서버와의 통신 후 원할하게 갱신

마무리하며

Stateful Widget의 LifeCycle을 통해서 효과적으로 UI를 빌드해봅시다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글

관련 채용 정보