Flutter

Flutter Day 10-2: InheritedWidget를 이용한 도서 관리 앱

@leem 2025. 1. 20. 17:07
Flutter에서 상태를 공유하고 효율적으로 관리하려면 InheritedWidget을 사용하는 방법이 있습니다.
InheritedWidget은 위젯 트리에서 데이터를 공유하고 자식 위젯들에게 알림을 줄 수 있는 강력한 도구입니다.
InheritedWidget을 활용해 도서 관리 앱을 구현한 예제를 중심으로 개념과 코드를 정리해 보겠습니다.

 InheritedWidget

InheritedWidget은 위젯 트리에서 데이터를 공유하기 위해 사용됩니다. 부모 위젯에서 상태를 관리하고, 자식 위젯들이 그 상태를 참조하거나 변경할 수 있도록 합니다.
이전의 props drilling(프로퍼티 전달) 문제를 해결하고, 트리가 깊어져도 데이터를 효율적으로 관리할 수 있는 구조를 제공합니다.

특징 설명
데이터 공유 부모 위젯이 관리하는 데이터를 트리 아래의 모든 위젯과 공유 가능.
성능 최적화 updateShouldNotify를 통해 데이터 변경 여부를 판단하여 필요할 때만 자식 위젯을 다시 빌드.
BuildContext 사용 context.dependOnInheritedWidgetOfExactType<T>()를 통해 데이터를 쉽게 접근.
Flutter 기본 제공 상태 관리 라이브러리 없이도 기본적으로 사용할 수 있는 방식.

 InheritedWidget을 활용한 도서 관리 앱

(1) InheritedParent 클래스 구현

InheritedWidget을 상속받아 도서 목록 상태와 콜백 메서드를 관리합니다.

import 'package:flutter/cupertino.dart';
import '../common.utils/logger.dart';

class InheritedParent extends InheritedWidget {
  final List<String> state; // 공유 상태 (선택된 도서 목록)
  final void Function(String book) onPressed; // 콜백 메서드

  InheritedParent({
    required this.state,
    required this.onPressed,
    required super.child,
  });

  @override
  bool updateShouldNotify(covariant InheritedParent oldWidget) {
    logger.d('InheritedParent - updateShouldNotify 호출됨');
    if (state.length != oldWidget.state.length) return true;

    for (int i = 0; i < state.length; i++) {
      if (state[i] != oldWidget.state[i]) return true;
    }
    return false; // 상태 변경 없음
  }

 


(2) BookListPage (책 목록 화면)

도서 목록을 표시하고, 선택된 도서는 하이라이트됩니다.

import 'package:flutter/material.dart';
import '../common.utils/logger.dart';
import 'inherited_parent.dart';

class BookListPage extends StatelessWidget {
  final List<String> books = ['호모사피엔스', '다트입문', '홍길동전', '코드리팩토링', '전치사도감'];

  @override
  Widget build(BuildContext context) {
    InheritedParent? inheritedParent =
        context.dependOnInheritedWidgetOfExactType<InheritedParent>();

    List<String> selectedBook = inheritedParent?.state ?? [];

    return inheritedParent == null
        ? Center(child: Text('데이터가 없습니다.'))
        : ListView(
            children: books.map((book) {
              final isSelectedBook = selectedBook.contains(book);
              return ListTile(
                leading: Container(
                  width: 35,
                  height: 35,
                  decoration: BoxDecoration(
                    color: Theme.of(context).secondaryHeaderColor,
                    borderRadius: BorderRadius.circular(8.0),
                    border: Border.all(color: Colors.black),
                  ),
                ),
                title: Text(book, style: TextStyle(fontWeight: FontWeight.bold)),
                trailing: IconButton(
                  onPressed: () {
                    inheritedParent?.onPressed(book); // 콜백 호출
                  },
                  icon: Icon(
                    isSelectedBook ? Icons.remove_circle : Icons.add_circle,
                    color: isSelectedBook ? Colors.red : Colors.green,
                  ),
                ),
              );
            }).toList(),
          );
  }
}

(3) BookCartPage (장바구니 화면)

선택된 도서만 표시합니다.

 
import 'package:flutter/material.dart';
import 'inherited_parent.dart';

class BookCartPage extends StatelessWidget {
  const BookCartPage({super.key});

  @override
  Widget build(BuildContext context) {
    InheritedParent inheritedParent =
        context.dependOnInheritedWidgetOfExactType<InheritedParent>()!;

    return ListView(
      children: inheritedParent.state
          .map((book) => ListTile(title: Text(book)))
          .toList(),
    );
  }
}

(4) HomeScreen (상태 관리 및 화면 전환)

상태를 변경하거나 화면을 전환하는 역할을 합니다.

import 'package:flutter/material.dart';
import '../common.utils/logger.dart';
import 'book_list_page.dart';
import 'book_cart_page.dart';
import 'inherited_parent.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int pageIndex = 0;
  List<String> mySelectedBook = [];

  void _toggleSaveStates(String book) {
    setState(() {
      if (mySelectedBook.contains(book)) {
        mySelectedBook.remove(book);
      } else {
        mySelectedBook.add(book);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: Text('도서 관리 앱'),
          backgroundColor: Theme.of(context).colorScheme.tertiaryContainer,
        ),
        body: InheritedParent(
          state: mySelectedBook,
          onPressed: _toggleSaveStates,
          child: IndexedStack(
            index: pageIndex,
            children: [
              BookListPage(),
              BookCartPage(),
            ],
          ),
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: pageIndex,
          onTap: (index) {
            setState(() {
              pageIndex = index;
            });
          },
          items: [
            BottomNavigationBarItem(
              icon: Icon(Icons.list),
              label: '책 목록',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.shopping_cart),
              label: '장바구니',
            ),
          ],
        ),
      ),
    );
  }
}

  1. InheritedWidget의 사용 방법:
    • InheritedWidget을 상속받아 상태와 메서드를 관리.
    • updateShouldNotify로 상태 변경 여부 판단.
  2. 데이터 흐름 최적화:
    • 중첩된 위젯 트리에서 상태를 필요로 하는 자식 위젯만 다시 빌드.
  3. props drilling 방지:
    • 부모 위젯에서 자식 위젯으로 콜백과 데이터를 계속 전달하는 번거로움을 제거.