Frontend/Flutter

[flutter] 상태관리가 되는 앱 만들어 보기 01

JINJINC 2025. 1. 20. 11:25
728x90
반응형

우리가 배울 여러 상태 관리 방법을 이용해 데이터 일관성을 유지해 봅시다.
StatefulWidget
InheritedWidget
Provider(라이브러리)
Riverpod(라이브러리)

1단계 statelessWidget statefulWidget 위젯 만들어보기

StatelessWidget

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyStatelessWidget(),
    ));

//statelessWidget은
//상태의 변화없이 항상 동일한 UI를 그린다.
//이런 위젯은 데이터나 변수의 값이 변하지 않을때 사용합니다.

//포인트!! => 사실 상태는 변수에 할당된 값을 말한다.
//stateless 위젯을 할때 그래서 상수값으로 많이 할당함 => final
class MyStatelessWidget extends StatelessWidget {
  MyStatelessWidget({super.key});

  final msg = '안녕하세요 저는 상태가 없는 (고정된) 메세지 잆니다.';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(msg),
    );
  }
}

StatefulWidget

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    home: MyStatefulWidget(),
  ));
}

//포인트! ->  사실 상태는 변수에 할당된 값이다.
//'상태가 있다' 라는 말은 값이 변화할 수 있는 가능성을 이야기한다.
//외부에서 보이는 부분
class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({super.key});

  //변수 --- 고정 ---> 상수
  final msg = '저는 stateful 위젯입니다.';

  //여기가 호출되고 => 아래의 private class를 보이게 된다.
  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  //변수에 할당된 값이 상수가 아니기 때문에 언제든지 변경이 가능하다.
  //즉 , 여기를 상태 변수라고 부른다.
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Scaffold(
      body: Center(
        child: InkWell(
            onTap: () {
              setState(() {
                count += 1;
              });
            },
            child: Text('${widget.msg} onTap Count : ${count}')),
      ),
    ));
  }
}
  1. StatefulWidget

StatefulWidget은 상태를 변경할 수 있고, 상태가 변경된 경우 setState()를 호출하여 자식 위젯들을 갱신할 수 있습니다.

2 단계 도서관리 앱으로 상태변화 관리해보기

도서 관리 앱 - 위젯 트리 구조 (StatefulWidget을 이용한 상태 관리)

homePage (StatefulWidget)
|  ├── 공유 상태: 대출 목록에 추가된 도서 목록 (공유 상태)
|
├── Library (Store 역할)
|     ├── Book 1 (도서 목록)
|     ├── Book 2
|     ├── Book 3
|     └── Book 4
|
└── BorrowList (Cart 역할)
      ├── Book 2 (대출 목록에 추가된 책)
      └── Book 4

기본 구성하기

main 화면

import 'package:flutter/material.dart';

import 'home_screen.dart';

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      //flutter에서 material3 적용
      useMaterial3: true,
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), // 색상을 알아서 조합해줌
    ),
    home: HomeScreen(),
  ));
}

home_screen.dart

import 'package:flutter/material.dart';

import '../common.utils/logger.dart';
import 'book_cart_page.dart';
import 'book_list_page.dart';

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

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

class _HomeScreenState extends State<HomeScreen> {
  //로컬상태
  int pageIndex = 0;

  // 공유상태 => 여러 위젯에서 관리할 수 있음 => 카트에 담긴 책 정보( 책 리스트 화면과, 장바구니 화면에서 공유하는 데이터)
  // 상품 ->  책 (String) 타입으로 관리하자
  //객체 배열로 관리할 수 있다.
  List<String> mySelectedBook = [];

  //상태를 변경하는 메서드 만들기
  void _toggleSaveStates(String book) {
    //다시 화면을 그려라 => 요청함수
    setState(() {
      if (mySelectedBook.contains(book)) {
        mySelectedBook.remove(book);
      } else {
        mySelectedBook.add(book);
      }
    });
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    setState(() {
      pageIndex = 0;
    });
  }

  void _changePageIndex(int index) {
    setState(() {
      pageIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    logger.d('HomeScreen build 메서드 호출됨 ');
    return SafeArea(
        child: Scaffold(
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: Icon(
            Icons.arrow_back_ios,
            color: Colors.white,
          ),
        ),
        title: Text(
          '지니의 서재',
          style: TextStyle(color: Colors.white),
        ),
        actions: [
          Icon(
            Icons.shopping_cart,
            color: Colors.white,
          )
        ],
        backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant,
      ),
      body: IndexedStack(
        index: pageIndex,
        children: [
          BookListPage(),
          BookCartPage(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: pageIndex,
        onTap: (index) => _changePageIndex(index),
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.list), label: 'book-list'),
          BottomNavigationBarItem(
              icon: Icon(Icons.shopping_cart), label: 'book-cart'),
        ],
      ),
    ));
  }
}

book_list_page.dart book_cart_page

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('북 리스트 페이지'),
    );
  }
}
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Book cart page'),
    );
  }
}

Call back 사용해보기

home_screen

=> home screen에서 선언된 함수와 list 를 하위에 전달하기

import 'package:flutter/material.dart';

import '../common.utils/logger.dart';
import 'book_cart_page.dart';
import 'book_list_page.dart';

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

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

class _HomeScreenState extends State<HomeScreen> {
  //로컬상태
  int pageIndex = 0;

  // 공유상태 => 여러 위젯에서 관리할 수 있음 => 카트에 담긴 책 정보( 책 리스트 화면과, 장바구니 화면에서 공유하는 데이터)
  // 상품 ->  책 (String) 타입으로 관리하자
  //객체 배열로 관리할 수 있다.
  List<String> mySelectedBook = [];

  //상태를 변경하는 메서드 만들기
  void _toggleSaveStates(String book) {
    //다시 화면을 그려라 => 요청함수
    setState(() {
      if (mySelectedBook.contains(book)) {
        mySelectedBook.remove(book);
      } else {
        mySelectedBook.add(book);
      }
    });
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    setState(() {
      pageIndex = 0;
    });
  }

  void _changePageIndex(int index) {
    setState(() {
      pageIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    logger.d('HomeScreen build 메서드 호출됨 ');
    return SafeArea(
        child: Scaffold(
      appBar: AppBar(
        leading: IconButton(
          onPressed: () {},
          icon: Icon(
            Icons.arrow_back_ios,
            color: Colors.white,
          ),
        ),
        title: Text(
          '지니의 서재',
          style: TextStyle(color: Colors.white),
        ),
        actions: [
          Icon(
            Icons.shopping_cart,
            color: Colors.white,
          )
        ],
        backgroundColor: Theme.of(context).colorScheme.onSurfaceVariant,
      ),
      body: IndexedStack(
        index: pageIndex,
        children: [
          BookListPage(
            mySelectedBooked: mySelectedBook,
            onToggleSaved: _toggleSaveStates,
          ),
          BookCartPage(
            mySelectedBooked: mySelectedBook,
            onToggleSaved: _toggleSaveStates,
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: pageIndex,
        onTap: (index) => _changePageIndex(index),
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.list), label: 'book-list'),
          BottomNavigationBarItem(
              icon: Icon(Icons.shopping_cart), label: 'book-cart'),
        ],
      ),
    ));
  }
}

book_list_page

전달받은 내용 사용하여 코드 구현하기

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

import '../common.utils/logger.dart';

class BookListPage extends StatelessWidget {
  final Function(String) onToggleSaved;
  final List<String> mySelectedBooked;
  BookListPage(
      {required this.onToggleSaved, required this.mySelectedBooked, super.key});

  //임시데이터 - 교보문고에 볼 수 있는 책 목록 리스트
  final List<String> books = [
    '호모사피엔스',
    'dart입문',
    '홍길동전',
    'code Refactoring',
    '전치사도감',
  ];

  @override
  Widget build(BuildContext context) {
    logger.d('데이터 확인 : ${mySelectedBooked.toString()}');
    return SafeArea(
      child: Scaffold(
        body: ListView(
          children: books.map(
            (book) {
              //함수의 body에는 식을 작성할 수 있다

              final isSelecedBook = mySelectedBooked.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: Theme.of(context).primaryColor),
                  ),
                ),
                title: Text(
                  book,
                  style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
                ),
                trailing: IconButton(
                  onPressed: () {
                    onToggleSaved(book);
                  },
                  icon: isSelecedBook
                      ? Icon(Icons.remove_circle)
                      : Icon(Icons.add_circle),
                  color: isSelecedBook ? Colors.red : Colors.green,
                ),
              );
            },
          ).toList(),
        ),
      ),
    );
  }
}

book_cart_page

import 'package:flutter/material.dart';

class BookCartPage extends StatelessWidget {
  final Function(String) onToggleSaved;
  final List<String> mySelectedBooked;
  const BookCartPage(
      {required this.onToggleSaved, required this.mySelectedBooked, super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
        children: mySelectedBooked.map((cart) {
      return ListTile(
        leading: Container(
          width: 35,
          height: 35,
          decoration: BoxDecoration(
            color: Theme.of(context).secondaryHeaderColor,
            borderRadius: BorderRadius.circular(8.0),
            border: Border.all(color: Theme.of(context).primaryColor),
          ),
        ),
        title: Text(
          cart,
          style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
        ),
        trailing: IconButton(
            onPressed: () {
              onToggleSaved(cart);
            },
            icon: Icon(
              Icons.remove_circle,
              color: Theme.of(context).primaryColor,
            )),
      );
    }).toList());
  }
}
728x90
반응형