전자액자 만들기(위젯 생명주기와 타이머)

잠만보·2024년 8월 11일
post-thumbnail

사전지식

위젯 생명주기

위젯이 화면에 그려지는 순간부터 삭제되는 순간까지의 주기는 어떻게 될까?

상태없는 위젯 (StatelessWidget) 의 생명주기

매우 간단하다

  1. 먼저 StatelessWidget 이 빌드되면 생성자가 실행된다.

  2. 필수로 오버라이드 해야 하는 build() 함수가 실행된다.

  3. build() 함수에 반환한 위젯이 화면에 렌더링 된다.

플러터에서 모든 위젯은 Widget 클래스상속하고 Widget 클래스불변(한번 생성하고나면 속성을 변경할 수 없음)특성을 가지고 있다.

만약에 생성자에 새로운 매개변수가 입력되는 경우 처럼 위젯의 속성을 변경해야 한다면
build() 함수에서 변경된 매개변수를 기반으로 build() 함수재실행 해줘야 한다.

하지만 Stateless Widget불변이기 때문에 한번 생성된 인스턴스의 build() 함수는 재실행 되지 않는다.

따라서 인스턴스를 새로 생성한 후 기존의 인스턴스를 대체해서 변경된 사항을 화면에 렌더링 한다.

상태 있는 위젯 (Stateful Widget)의 생명주기

Stateful WidgetWidget 클래스State 클래스 2개로 구성되어있고 생명주기가 복잡하다.

상태 변경이 없는 생명주기

위젯이 화면에 나타나며 생성되고 화면에서 사라지며 삭제되는 과정을 의미한다.
중간에 위젯의 상태가 변경되지 않는다.

  1. StatefulWidget 생성자 실행

  2. createState() 함수 실행
    {필수로 오버라이드 해야 하는 함수로, Stateful Widget 과 연동되는 State를 생성한다.

  3. State가 생성되면 initState() 가 실행된다.
    {initState()State생성되는 순간에만 단 한번 실행되고 절대로 다시 실행되지 않는다.

  4. didChangeDependencies() 가 실행된다.
    {initState() 와는 다르게 BuildContext가 제공되고 State가 의존하는 값이 바뀌면 재실행된다.}

  5. State의 상태가 dirty 로 설정된다.
    {dirty 상태는 build 가 재실행되야 하는 상태이다.}

  6. build() 함수가 재실행되고 UI가 반영된다.

  7. build() 실행이 완료되면 상태가 clean 상태로 변경된다. 화면에 변화가 없으면 이 상태를 유지한다.
    위젯을 update 하거나, 새로운 State를 지정하면 5번의 단계로 다시 돌아간다

  8. 위젯이 위젯 트리에서 사라지면 deactivate() 가 실행된다. deactivate()State가 일시적 또는 영구적으로 삭제될 때 실행된다.

  9. dispose() 가 실행된다. 위젯이 영구적으로 삭제될 때 실행된다.

Stateful Widget 생성자의 매개변수가 변경됬을 때 생명주기

아까 위에서 말했던것 처럼 생성자 매개변수가 변경되어서 위젯이 업데이트 됬을 때의 상황이다.

  1. 생성자 매개변수가 변경되었으므로 다시 생성자를 실행한다.

  2. didUpdateWidget() 함수가 실행된다.

  3. Statedirty 상태로 변경된다.

  4. dirty 상태로 변경되었으므로 build()가 다시 실행된다.

  5. State의 상태가 clean 으로 변경된다.

State 자체적으로 build() 를 재실행할 때 생명주기(즉 새로운 상태로 바뀌었을 때)

Stateless Widget은 생성될 때 build() 함수가 한번만 실행되고 절대로 다시 실행되지 않는다.

반면 Stateful WidgetStateful Widget 클래스State 클래스로 구성되어 있는데
State 클래스setState() 함수를 실행해서 build() 함수를 자체적으로 재실행할 수 있다.

===============================================================

  • 날씨 정보를 받아와서 화면에 렌더링하는 Stateful Widget 을 생각해보자.

  • 날씨 정보라는 상태(State)가 있지 않겠는가?

  • 날씨는 매일 바뀌므로 날씨정보가 바뀔때 마다 setState()함수가 실행되서 날씨 State를 바꿀것이고 그럼 State의 상태는 dirty가 될 것이다.

  • 따라서 다시 build()를 하여 화면에 바뀐 날씨의 정보를 렌더링 해야 하고 build()가 끝나면 Stateclean 상태가 된다.

Timer

타이머는 특정 시간이 지난 후에 일회성 또는 지속적으로 함수를 실행한다.

이번 프로젝트에서는 Timer.periodic()을 사용해서 주기적으로 콜백 함수를 실행하겠다.

Timer.periodic() 은 매개변수 2개를 입력받는다.

Timer.periodic(
 Duration(seconds: 3),	// 주기
  (Timer timer) {},		// 콜백 함수
 );
  • 주기의 Duration에 days, hours, minutes, seconds, miliseconds, microseconds 매개변수를 이용해서 다양한 주기를 설정할 수 있다.

  • 콜백함수 : 주기가 지날 때 마다 실행할 콜백 함수이다. 매개변수에 현재 실행중인 Timer 객체가 제공된다.

사전준비

이미지 추가

unsplash 사이트에서 무료 라이센스 이미지 5장을 준비했다.

pubspec.yaml 파일 설정

추가한 이미지 asset을 등록해준다.

flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  assets:
    - asset/img/

프로젝트 초기화

앱의 기본 홈 화면으로 사용할 home_screen.dart 파일 생성

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('Home Screen'),
    );
  }
}

main.dart파일에서 HomeScreen을 홈 위젯으로 등록해줌

import 'package:flutter/material.dart';
import 'package:image_carousel/screen/home_screen.dart';

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

구현하기

페이지뷰 구현하기

PageView는 여러개의 위젯을 단독 페이지로 생성하고 가로 또는 세로 swipe 로 페이지를 넘길 수 있게 하는 위젯이다.

home_screen.dart 에 PageView를 추가해보자

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: PageView(
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset('asset/img/image_$number.jpg'),
          )
          .toList(),
    ));
  }
}

배열에 1, 2, 3, 4, 5 넣어두고 map으로 하나씩 숫자를 뽑아서 해당 숫자(number)에 맞는 image_${number}의 이미지를 렌더링 하는 방식이다.

위에 보면 핸드폰 화면 비율마다 위아래 여백이 남는데 이미지 핏을 조정해서 항상 전체화면을 차지하도록 바꿔보자

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: PageView(
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover, // 이미지 핏 조정해서 항상 전체화면을 채우도록
            ),
          )
          .toList(),
    ));
  }
}


아까랑 비교하면 위에 빈 여백이 사라진걸 볼 수 있다

BoxFit 속성은 여러가지가 있는데 필요할 때 마다 찾아보자
위에처럼 cover 를 하면 부모 위젯 전체를 덮는 선에서 최소한의 크기로 조정한다.

상태바 색상 변경하기

지금은 흰색인데 검은색으로 바꿀수도 있다.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // services 패키지 import

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); // dark로 지정해서 검은색으로 바뀜,
    
    return Scaffold(
        body: PageView(
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover,
            ),
          )
          .toList(),
    ));
  }
}


검은색으로 바뀌었다

SystemChrome 클래스는 시스템 UI의 그래픽 설정을 변경하는 기능을 제공한다.

이것도 필요할 때 마다 찾아서 보자.

타이머 추가하기

손으로 일일이 스와이프 해서 사진을 바꾸는건 귀찮다.

이제 일정 기간마다 자동으로 페이지 사진이 변경되는 기능을 추가해보자.

Timer를 추가하려면 HomeScreen 위젯을 상태가 있는 위젯으로 바꿔줘야 한다.

근데 그냥 Stateless 위젯에다가 build 부분에 타이머 넣으면 되는거 아닌가?
아니다. build()에 타이머를 등록하면 위젯이 새로 생성(렌더링) 될 때 마다 매번 새로운 타이머가 생긴다. 따라서 메모리 누수가 발생할 수 있다.

앞에서 배운 상태 있는 위젯의 생명주기를 보면 initState()를 사용해서 State가 생성될 때 딱 한번만 타이머를 생성할 수 있다.

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

// 1. Stateful Widget 정의
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

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

// 2. _HomeScreenState 정의
class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);

    return Scaffold(
        body: PageView(
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover,
            ),
          )
          .toList(),
    ));
  }
}
  1. Stateful WidgetStatefulWidget 클래스를 상속해서 정의할 수 있다.
    생명주기에서 배웠던 것 처럼 Stateful WidgetcreateState() 함수를 정의해야 하며 이 함수는 State를 반환해준다.

  2. _HomeScreenState 클래스는 먼저 생성한 StatefulWidget 클래스를 매개변수로 받는 State 클래스를 상속한다. build() 함수는 State 에서 정의한다.

이제 initState()함수에 타이머를 추가해보자

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async'; // 1. async 패키지 불러오기


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

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


class _HomeScreenState extends State<HomeScreen> {
  @override
  // 2. initState 함수 등록
  void initState() {
    super.initState(); // 3. 부모 initState 실행, 모든 initState 함수는 부모의 initState 함수를 실행해줘야 한다.

    Timer.periodic(Duration(seconds: 3), (timer) { // 4. 3초마다 실행되는 Timer.periodic 등록
      print('실행!');
    });
  }

  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);

    return Scaffold(
        body: PageView(
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover,
            ),
          )
          .toList(),
    ));
  }
}


3초마다 실행! 글자가 콘솔에 출력된다.

PageView 조작하기

이전에 웹/앱 만들기에서 웹뷰를 조작할 때, WebViewController 를 사용한 것 처럼
PageViewPageController 를 사용해서 조작할 수 있다.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';


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

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


class _HomeScreenState extends State<HomeScreen> {
  // 1. PageController 생성
  final PageController pageController = PageController();

  @override
  void initState() {
    super.initState();

    Timer.periodic(Duration(seconds: 3), (timer) {
      print('실행!');
    });
  }

  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);

    return Scaffold(
        body: PageView(
          controller: pageController, // 2. PageController 등록
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover,
            ),
          )
          .toList(),
    ));
  }
}

타이머 콜백함수 변경해서 자동 페이지 스와이프 하게 하기

위에서 PageView 컨트롤러를 등록했으니 이 컨트롤러로 페이지 뷰를 조작할 수 있다.

타이머의 콜백함수를 변경해서 주기적으로 스와이프 하게 해보자

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';

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

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

class _HomeScreenState extends State<HomeScreen> {
  final PageController pageController = PageController();

  @override
  void initState() {
    super.initState();

    Timer.periodic(Duration(seconds: 3), (timer) {
		// 1. 현재 페이지 불러오기                  
      int? nextPage = pageController.page?.toInt();
    // 2. 페이지 값이 없을 때 예외처리
      if (nextPage == null) {
        return;
      }
    // 3. 페이지 끝에 도달하면 0으로 초기화
      if (nextPage == 4) {
        nextPage = 0;
      } else { // 페이지가 남아있으면 페이지수 + 1
        nextPage++;
      }
      pageController.animateToPage( // 4. 페이지 변경
        nextPage,
        duration: Duration(milliseconds: 500), // 0.5 초 동안에 이미지 변경
        curve: Curves.ease,
      );
    });
  }

  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);

    return Scaffold(
        body: PageView(
      controller: pageController,
      children: [1, 2, 3, 4, 5]
          .map(
            (number) => Image.asset(
              'asset/img/image_$number.jpg',
              fit: BoxFit.cover,
            ),
          )
          .toList(),
    ));
  }
}

1번주석에서 pageController.page getter를 사용해서 페이지뷰의 현재 페이지를 가져올 수 있다.
페이지가 변경중인 경우 소숫점으로 값이 반환이 될 수 있으므로 toInt() 를 사용해서 정수로 변환해뒀다.

int? nextPage = pageController.page?.toInt(); 여기서 ??. 기호가 뭘까
바로 null-safety와 관련된 문법이다.

? (Null-aware Operator): 이 연산자는 변수나 객체가 null일 수 있음을 나타낸다. 즉, ?를 사용하여 null이 될 수 있는 값에 대해 안전하게 작업할 수 있다.

?. (Null-aware Access Operator): 이 연산자는 객체가 null일 때, 그 객체의 속성이나 메서드를 안전하게 접근할 수 있게 해준다.
객체가 null이라면, 메서드 호출이나 속성 접근을 시도하지 않고 null을 반환한다.

최종 결과

profile
아프지 말자 - (잘못된 정보, 수정 사항 있으면 언제든지 알려주시면 감사하겠습니다!)

0개의 댓글