Flutter - Provider 없이 클래스 간 상태 관리 하기 (State 직접 업데이트하기)

반응형

 

개요

작성 목적

나는 처음 상태 관리를 provider로 배웠다.
간단한 로직임에도 다른 클래스끼리의 상태 변환을 위해 provider를 불러오고 정의하려니
너무 번거롭기도 하고 익숙한 형태도 아니기 때문에 불편했다.
간단히 클래스 간 상태를 공유하기 위해서,
또한 상태가 변경될 때의 빌드 작동 순서를 명확하게 이해하고자 직접 샘플 프로젝트로 최대한 간단하게 구현해 보았다.


내용

이 포스팅은..
상태 관리 관련 패키지(Provider, GetX, Riverpod, bloc) 등의 패키지를 사용하지 않고
다른 클래스 간 상태를 직접 setState로 변경하는 방법을 다룬다.
 
* 설명으로 다루는 프로젝트는 아래의 깃허브에서 다운로드 가능합니다.


요구사항

UI

FAB(Floating Action Button) 위젯을 배울 때 볼 수 있는 가장 익숙한 형태의 UI이다.
플로팅 액션 버튼을 누르면 숫자가 값이 1씩 카운팅되며 텍스트 위젯에 값을 실시간으로 보여준다.

  • 플로팅 액션 바 : 값 증가를 위함
  • 텍스트 : 상태값의 업데이트 상태를 확인하기 위함


파일 구성 (클래스 구성)

Main파일 외에 각자 다른 클래스 간 상태를 전달할 수 있어야 한다.
상태의 변화에 따라 실시간으로 업데이트가 될 수 있도록 각자 다른 클래스로 구성한다.
이해를 돕기 위해서 편의상 클래스 별로 파일을 만들도록 한다.

  • main.dart
    • 메인 실행 파일
    • MeterialApp>Scaffold로 구성된다.
    • body와 floatingActionButton은 각자 다른 파일로 구성한다.
  • body.dart
    • main.dart 파일에 연결될 Scaffold의 body에 필요한 파일
    • 텍스트 위젯으로 상태 값을 표기해주는 영역을 표시하는 파일이다.
  • fab.dart
    • main.dart 파일에 연결될 Scaffold의 floatingActionButton에 필요한 파일
    • 플로팅 액션 버튼을 구성하는 파일이다. 

설계

사전 참고사항 (힌트)

그렇다면 각자 다른 클래스에 상태를 어떻게 연결해서 어떻게 상태값을 업데이트를 해야할까?
아래의 조건을 살펴보자. 사실 이미 프로그래밍을 해본 사람은 아래의 힌트로 눈치를 챘을 것이다.

  1. 클래스는 생성자로 값을 넘겨주어 클래스 간 값을 전달할 수 있다.
  2. setState를 사용하면 해당 상태값과 관련된 위젯은 다시 리빌드(리렌더링)된다.

작동 절차

1. 메인에서 상태를 정의한다.
2. 상태를 각 클래스끼리 생성자로 초기화하여 공유한다.
3. 플로팅 액션 버튼을 눌러 생성자로 넘겨받은 상태 변경 함수로 상태값 변경을 시도한다.
4. 메인에서 요청 받은 상태를 변경한다.
5. 바디 영역의 생성자로 넘겨받은 상태값과 관련된 위젯을 리빌드한다.
 


코딩

1. 상태값 및 상태 업데이트 함수 정의

main.dart
메인 파일에 상태값과 상태를 업데이트 할 수 있는 함수를 정의한다.
상태 변환을 하는 함수는 setState만 있어서 이해가 쉬울 것이다.

// main.dart
  // 상태 값
  int _currentValue = 0;

  // 상태를 관리하는 함수
  void setValue(int value) {
    setState(() {
      _currentValue = value;
    });
  }

 

2. 클래스 생성자를 통해 상태값 및 상태 전달

body.dart & fab.dart
각 연결된 클래스(파일)에 상태값과 상태 업데이트 함수를 생성자로 넘겨주어 값을 공유할 수 있어야 한다.
각 클래스(플로팅 액션 버튼, 바디 페이지)의 생성자를 통해 값을 넘겨줄 수 있어야 한다.
넘겨지는 값은 상태 관련 매개변수이다.
특이사항으로 위에서 정의한 setState 함수 자체를 넘겨준다.
참고사항에서 언급하였지만 setState와 관련된 위젯은 리빌드 되는 구조이므로,
setState 함수를 클래스 생성자의 매개변수로 넘겨주면 자연스럽게 상태가 공유되게 된다.
포스팅에서는 플로팅 액션 버튼을 누르면 상태가 업데이트 되는 로직이므로,
텍스트가 표현되는 바디 페이지는 상태값만 공유하면 된다.

/* body.dart */
class BodyPage extends StatefulWidget {
  /* 생성자 초기화 */
  final int value; // 상태값

  const BodyPage({
    super.key,
    required this.value,
  });
}


/* fab.dart */
class FloatingActionButtonPage extends StatelessWidget {
  /* 생성자 초기화 */
  final int value; // 상태값
  final Function(int) setValue; // 상태 변환 함수

  const FloatingActionButtonPage({
    super.key,
    required this.value,
    required this.setValue,
  });
}

 

3. 페이지 구성 및 클래스 간 상태 공유

main.dart
앱에 보여질 페이지를 구성하여야 한다.
메인 파일에 MaterialApp 하위에 Scaffold 로 구성하며,
body와 floatingActionButton은 각각 다른 클래스를 연결하도록 한다.

  • 구성 : MyApp > MaterialApp > Scaffold
  • body : body.dart (class BodyPage)
  • floatingActionButton : fab.dart (class FloatingActionButtonPage)
  • 각 클래스에 넘겨줄 파라미터 : 상태 관련 
// main.dart
/* 참고
  // 상태 값
  int _currentValue = 0;

  // 상태를 관리하는 함수
  void setValue(int value) {
    setState(() {
      _currentValue = value;
    });
  }
*/

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        /* 통합 상태 관리를 위해 각각의 페이지에 상태값과 상태변환 함수를 넘겨줌 */
        // 메인 페이지
        body: BodyPage(
          value: _currentValue, // 상태값
        ),
        // 플로팅 액션 바 페이지
        floatingActionButton: FloatingActionButtonPage(
          value: _currentValue, // 상태값
          setValue: setValue, // 상태 업데이트 함수
        ),
      ),
    );
  }

 

4. 플로팅 액션 버튼 기능 정의

fab.dart
메인 파일을 통해 각 페이지끼리 상태를 공유할 수 있도록 연결하였다.
이제 기능을 구현하여야 한다.
플로팅 액션 버튼을 누르면 상태값이 +1이 되도록 해보자.

  • 플로팅 액션 버튼에는 "+1" 이라는 문구를 넣는다.
  • 버튼을 터치 할 경우, 값이 1씩 증가한다.
// fab.dart (StatelessWidget)
/* 참고
  // 생성자 초기화
  final int value; // 상태값
  final Function(int) setValue; // 상태 변환 함수

  const FloatingActionButtonPage({
    super.key,
    required this.value,
    required this.setValue,
  });
*/

@override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      child: const Text("+1"),
      onPressed: () {
        // FAB 버튼 터치 시 값 + 1 카운팅
        // 상태 변환 함수에 값을 넣어 상태값을 업데이트 함
        setValue(value + 1);
      },
    );
  }

 

5. 상태값 확인을 위한 메인 영역(바디 페이지) 정의

body.dart
플로팅 액션 버튼에서 상태값이 1이 증가된 값을 확인하기 위해 분리된 페이지에 텍스트 위젯을 위치하여 업데이트된 상태값을 확인하도록 한다.
특이사항으로는 플로팅 액션 버튼으로 인해 상태값이 setState 되면 상태값과 관련된 위젯이 리빌드 된다.
아래의 코드에서는 _bodyValue 라는 이름의 변수를 생성하여 업데이트된 상태값을 받아 텍스트 위젯으로 보여준다.
텍스트 위젯에 직접적으로 생성자의 값을 넣어주어도 되지만,
상태값이 리빌드 이후에 값을 업데이트하는 절차의 이해를 돕기 위해서 빌드 시 새로운 변수에 상태값을 받도록 처리하였다.

// body.dart (StatefulWidget)
/* 참고
  // 생성자 초기화
  final int value; // 상태값

  const BodyPage({
    super.key,
    required this.value,
  });
*/

  int _bodyValue = 0; // 상태값을 저장할 변수 선언 및 초기화

  @override
  Widget build(BuildContext context) {
    // 생성자로 넘겨받은 상태값 저장, 상태값이 setState되면 리빌드되어 값 업데이트
    _bodyValue = widget.value;

    return Center(
      child: Text(
        _bodyValue.toString(), // 상태값 보여주기
        style: const TextStyle(
          fontSize: 20, // 폰트 크기 20
          fontWeight: FontWeight.bold, // 굵게
        ),
      ),
    );
  }

실행 (확인)

정상적으로 각 클래스마다 상태가 공유되어 상태 관리가 업데이트 되는지 확인해보자.


전체 소스 코드

main.dart

import 'package:class_parameter_value_exchange/body.dart';
import 'package:class_parameter_value_exchange/fab.dart';
import 'package:flutter/material.dart';

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

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

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // 상태 값
  int _currentValue = 0;

  // 상태를 관리하는 함수
  void setValue(int value) {
    setState(() {
      _currentValue = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        /* 통합 상태 관리를 위해 각각의 페이지에 상태값과 상태변환 함수를 넘겨줌 */
        // 메인 페이지
        body: BodyPage(
          value: _currentValue, // 상태값
        ),
        // 플로팅 액션 바 페이지
        floatingActionButton: FloatingActionButtonPage(
          value: _currentValue, // 상태값
          setValue: setValue, // 상태 업데이트 함수
        ),
      ),
    );
  }
}

body.dart

import 'package:flutter/material.dart';

class BodyPage extends StatefulWidget {
  /* 생성자 초기화 */
  final int value; // 상태값

  const BodyPage({
    super.key,
    required this.value,
  });

  @override
  State<BodyPage> createState() => _BodyPageState();
}

class _BodyPageState extends State<BodyPage> {
  int _bodyValue = 0; // 상태값을 저장할 변수 선언 및 초기화

  @override
  Widget build(BuildContext context) {
    // 생성자로 넘겨받은 상태값 저장, 상태값이 setState되면 리빌드되어 값 업데이트
    _bodyValue = widget.value;

    return Center(
      child: Text(
        _bodyValue.toString(), // 상태값 보여주기
        style: const TextStyle(
          fontSize: 20, // 폰트 크기 20
          fontWeight: FontWeight.bold, // 굵게
        ),
      ),
    );
  }
}

fab.dart

import 'package:flutter/material.dart';

class FloatingActionButtonPage extends StatelessWidget {
  /* 생성자 초기화 */
  final int value; // 상태값
  final Function(int) setValue; // 상태 변환 함수

  const FloatingActionButtonPage({
    super.key,
    required this.value,
    required this.setValue,
  });

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      child: const Text("+1"),
      onPressed: () {
        // FAB 버튼 터치 시 값 + 1 카운팅
        setValue(value + 1);
      },
    );
  }
}
반응형