Flutter - 상태 관리 + MVVM

Gun·2023년 10월 25일
0

Flutter

목록 보기
25/25
post-thumbnail

state-mgmt

1. 상태 관리에 대한 이해

플러터에서 상태 관리는 굉장히 중요한 개념입니다

1. 상태 (State) 란?

상태는 앱의 정보나 데이터를 나타내는 것으로, 어떤 시점에서 앱이 "어떻게 보이는지"와 "어떻게 동작하는지"를 결정합니다. 예를 들면, 체크박스의 체크 여부, 텍스트 입력 필드의 내용, 리스트의 아이템 등이 상태에 해당합니다.

2. 위젯 (Widget) 이란?

플러터에서 모든 UI 요소는 위젯입니다. 위젯은 불변(Immutable)합니다. 즉, 한 번 생성되면 변경할 수 없습니다. 그렇기 때문에 UI를 업데이트하기 위해서는 새로운 위젯을 생성해야 합니다.

3. 상태 없는 위젯 (StatelessWidget)

상태를 갖지 않는 위젯으로, 한 번 생성되면 변경되지 않습니다. build 메서드를 통해 UI를 정의합니다.

4. 상태 있는 위젯 (StatefulWidget)

변경 가능한 상태를 갖는 위젯입니다. State 객체를 통해 상태를 관리하며, 상태가 변경될 때마다 setState 메서드를 호출하여 UI를 업데이트합니다.

5. 상태 관리 기법

플러터에서는 다양한 상태 관리 기법과 도구가 있습니다:

  • Local State: 작은 위젯에서 상태를 관리할 때 유용합니다.
  • Lifting State Up: 상태를 상위 위젯으로 이동하여 여러 위젯이 상태를 공유할 수 있게 합니다.
  • Provider, Riverpod: 상태 객체를 효과적으로 제공하고 관리하는 도구입니다.
  • Redux, BLoC, MobX: 큰 앱에서 복잡한 상태 관리에 사용되는 패턴 및 라이브러리입니다.

💡 작업 순서
   1. 폴더 구조 잡기

1단계 목표 화면

ProductList


import 'package:class_my_part/models/product.dart';
import 'package:flutter/material.dart';

class ProductList extends StatelessWidget {
  ProductList({super.key});

  // 샘플 데이터 ==> class_model 로 옮길 예정
  List<Product> productList =
      List.generate(10, (index) => Product('p_${index}', '상품 ${index}', 1000));

  
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: productList.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: Text('${productList[index].productId}'),
            title: Text('${productList[index].productName}'),
            trailing:
                IconButton(onPressed: () {
                  // 로직 추가 예정
                }, icon: Icon(Icons.shopping_cart)),
          );
        });
  }
}

Product


class Product {

  String productId;
  String productName;
  double price;

  Product(this.productId, this.productName, this.price);
}

main


import 'package:class_my_part/view/product_list.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SafeArea(child: Scaffold(
        body: IndexedStack(
          children: [
            ProductList()
          ],
        ),
      )),
    );
  }
}

2단계 목표 화면

💡 작업 순서 
   1. MyCart 뷰 만들기 
   2. main.dart 파일 수정 
   3. Appbar 생성 

1. MyCart 뷰 만들기


import 'package:flutter/material.dart';

import '../models/product.dart';

class MyCart extends StatelessWidget {
  MyCart({super.key});

  // 샘플 데이터 ---> view_model 옮길 예정
  List<Product> cartList =
      List.generate(2, (index) => Product('p_${index}', '상품 ${index}', 1000));

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(
              Icons.shopping_cart,
              size: 30,
              color: Colors.orangeAccent,
            ),
            Text(
              '${cartList.length} 개',
              style: const TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.bold,
              ),
            )
          ],
        )
      ],
    );
  }
}

2. main.dart 파일 수정

3. Appbar 생성

main


import 'package:clss_my_cart/view/my_cart.dart';
import 'package:clss_my_cart/view/product_list.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

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

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _index = 0;

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SafeArea(
        child: Scaffold(
          appBar: buildAppBar(),
          body: IndexedStack(
            index: _index,
            children: [
              ProductList(),
              MyCart(),
            ],
          ),
          bottomNavigationBar: BottomNavigationBar(
            items: [
              BottomNavigationBarItem(icon: Icon(Icons.list), label: '상품'),
              BottomNavigationBarItem(
                  icon: Icon(Icons.shopping_cart), label: '장바구니'),
            ],
            currentIndex: _index,
            onTap: (newIndex) {
              setState(() {
                _index = newIndex;
              });
            },
          ),
        ),
      ),
    );
  }
}

AppBar buildAppBar() {
  return AppBar(
    title: const Text('teco 쇼핑'),
    actions: [
      Center(
        child: Stack(
          children: [
            Icon(Icons.shopping_cart),
            Positioned(
              top: 0,
              right: 0,
              child: CircleAvatar(
                radius: 8.0,
                backgroundColor: Colors.redAccent,
                child: Text('2'),
              ),
            )
          ],
        ),
      ),
      SizedBox(width: 16),
    ],
  );
}
💡 작업 순서 
   1. view_model 만들기 

ProductListViewModel


import 'package:class_my_part/models/product.dart';

class ProductListViewModel {

  // 샘플 데이터 ==> class_model 로 옮길 예정 (통신)
  List<Product> _productList =
  List.generate(10, (index) => Product('p_${index}', '상품 ${index}', 1000));


  List<Product> get products => _productList;
}

MyCartViewModel


import 'package:class_my_part/models/product.dart';

class MyCartViewModel {

  // 데이터 상태 값
  List<Product> _items = [];
  List<Product> get items => _items;

  // 아이템 등록 기능
  void addProduct(Product product) {
    _items.add(product);
  }

  // 아이템 제거 기능
  void removeProduct(Product product) {
    _items.remove(product);
  }
}

MyCart 코드 수정


import 'package:flutter/material.dart';

import '../models/product.dart';
import '../view_models/my_cart_view_model.dart';

class MyCart extends StatelessWidget {
  final MyCartViewModel myCartVm;
  MyCart({required this.myCartVm, super.key});

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(
              Icons.shopping_cart,
              size: 30,
              color: Colors.orangeAccent,
            ),
            Text(
              '${myCartVm.items.length} 개',
              style: const TextStyle(
                fontSize: 30,
                fontWeight: FontWeight.bold,
              ),
            )
          ],
        )
      ],
    );
  }
}

최종 코드

main


import 'package:class_my_part/view/my_cart.dart';
import 'package:class_my_part/view/product_list.dart';
import 'package:class_my_part/view_models/my_cart_view_model.dart';
import 'package:class_my_part/view_models/product_list_view_model.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final ProductListViewModel productVm = ProductListViewModel();
  final MyCartViewModel cartVm = MyCartViewModel();
  int _index = 0;

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SafeArea(
          child: Scaffold(
            appBar: buildAppbar(cartVm),
        body: IndexedStack(
          index: _index,
          children: [
            ProductList(productVm: productVm,myCartVm: cartVm),
            MyCart(myCartVm: cartVm),
          ],
        ),
        bottomNavigationBar: BottomNavigationBar(items: [
          BottomNavigationBarItem(icon: Icon(Icons.list), label: '상품'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '장바구니'),
        ],
          currentIndex: _index,
          onTap: (newIndex){
            setState(() {
              _index = newIndex;
            });
          },
        ),
      )),
    );
  }
}

AppBar buildAppbar(MyCartViewModel myCartVm) {
  return AppBar(
    title: const Text('tenco 쇼핑'),
    actions: [
      Center(
        child: Stack(
          children: [
            const Icon(Icons.shopping_cart),
            Positioned(
              top: 0,
              right: 0,
              child: CircleAvatar(
                radius: 8.0,
                backgroundColor: Colors.redAccent,
                child: Text('${myCartVm.items.length}'),
              ),
            )
          ],
        ),
      ),
      const SizedBox(width: 15,)
    ],
  );
}

ProductList


import 'package:class_my_part/view_models/my_cart_view_model.dart';
import 'package:class_my_part/view_models/product_list_view_model.dart';
import 'package:flutter/material.dart';

class ProductList extends StatefulWidget {
  final ProductListViewModel productVm;
  final MyCartViewModel myCartVm;

  ProductList({required this.myCartVm,required this.productVm,super.key});
  
  State<ProductList> createState() => _ProductListState();
}

// StatefulWidget 으로 변경 상위 클래스, 하위 클래스가 존재
// 하위 클래스에서 --> 상위 클래스에 접근 하기 위해 widget을 참조 변수로 제공합니다.
// 즉 widget은 StatefulWidget 클래스의 인스턴스를 참조 하며, 이를 통해 부모 위젯으로 부터
// 데이터를 전달 받거나 부모 위젯에 메서드를 호출 할 수 있습니다.
class _ProductListState extends State<ProductList> {
 // DI 외부에서 생성자를 통해서 데이터를 주입
  
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: widget.productVm.products.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: Text('${widget.productVm.products[index].productId}'),
            title: Text('${widget.productVm.products[index].productName}'),
            trailing:
                IconButton(onPressed: () {
                  // 로직 추가 예정
                  setState(() {
                    widget.myCartVm.addProduct(widget.productVm.products[index]);
                  });
                }, icon: Icon(Icons.shopping_cart)),
          );
        });
  }
}

Mycart


import 'package:class_my_part/view_models/my_cart_view_model.dart';
import 'package:flutter/material.dart';

class MyCart extends StatelessWidget {
  final MyCartViewModel myCartVm;
  MyCart({required this.myCartVm,super.key});

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.shopping_cart,
              size: 30,
              color: Colors.blueAccent,
            ),
            Text(
              '${myCartVm.items.length} 개',
              style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
            )
          ],
        )
      ],
    );
  }
}

2개의 댓글

comment-user-thumbnail
2024년 2월 28일

안녕하세요! 인상 깊게 봤습니다. 특히 앱 구현하려는 gif 에 흥미가 갔습니다 ㅎㅎ
이런 gif 제작 방법에 대해 알 수 있을까요??

답글 달기
comment-user-thumbnail
2024년 2월 28일

안녕하세요! 인상 깊게 봤습니다. 특히 앱 구현하려는 gif 에 흥미가 갔습니다 ㅎㅎ
이런 gif 제작 방법에 대해 알 수 있을까요??

답글 달기