본문 바로가기
Flutter

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

by JINJINC 2025. 1. 20.
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
반응형

댓글