Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/Flutter] Navigation 동작 방식 (initState & dispose) 본문

Programming/Flutter

[Programming/Flutter] Navigation 동작 방식 (initState & dispose)

scii 2020. 10. 9. 20:44

push() 메소드로 새로운 화면이 실행되고 pop() 메소드로 이전 화면으로 돌아간다는 것을 확인했다. 실행되는 화면은 스택(Stack) 구조로 메모리에 쌓이게 된다. 스택은 나중에 들어간 것이 먼저 나오는 구조이다. 스택에서 모든 화면이 제거되면 앱이 종료된다.

StatelessWidget & StatefulWidget 클래스의 동작 방법 차이점

StatelessWidget 클래스 동작

build() 메소드가 언제 호출되는지 확인해보자. 각 화면의 build 메소드의 return 앞에 어떤 화면인지 확인할 수 있도록 print() 로그를 작성하자.

화면이 표시되면서 build() 메소드가 호출된다. pop() 메소드로 뒤로 돌아갈 때는 두 번째 화면에서 받은 Text 객체가 출력되었다.


StatefulWidget 클래스 동작

상태를 가지는 StatefulWidget 클래스 경우, 조금 다르게 동작한다. 

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: new FirstPage(),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  Widget build(BuildContext context) {
    print('FirstPage build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('첫번째 페이지'),
      ),
      body: new RaisedButton(
        child: Text('다음 화면'),
        onPressed: () {
          setState(() {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondPage()),
            );
          });
        },
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  Widget build(BuildContext context) {
    print('SecondPage build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('두번째 페이지'),
      ),
      body: RaisedButton(
        child: Text('이전 화면'),
        onPressed: () {
          setState(() {
            Navigator.pop(context);
          });
        },
      ),
    );
  }
}

push 메소드로 SecondStatefulPage 클래스를 표시한 직후에 FirstStatefulPage 클래스의 build() 메소드와 SecondStatefulPage 클래스의 build() 메소드가 호출된다. pop() 메소드를 실행하여 이전 화면으로 돌아갈 때도 First 화면이 다시 그려진다.

이렇게 StatefulWidget 클래스의 내비게이션 동작은 기존 메모리에 남아 있던 화면도 모두 새로 그리는 동작을 한다. 그렇기 때문에 StatefulWidget 클래스의 build() 메소드에서는 앱 성능에 지장을 줄만한 코드(예를 들어 네트워크에 접속하여 데이터를 다운로드하거나 복잡한 계산을 하는 등)는 작성하면 안된다.


initState & dispose

네트워크 접속처럼 오래 걸리면서 자주 호출되면 안 되는 처리는 어디에 해야 할까? StatefulWidget 클래스에는 build() 메소드 외에도 특정 타이밍에 실행되는 여러 메소드가 있다. 이러한 메소드들을 생명주기(lifecycle) 메소드라고 부른다.

  • initState() 메소드는 위젯이 생성될 때 호출된다.
  • dispose() 메소드는 위젯이 완전히 종료될 때 (pop될 때) 호출된다.

다음과 같이 initState(), dispose() 메소드를 재정의(override)하고 print() 함수로 로그를 출력해보자.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: new FirstPage(),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  @override
  void initState() {
    super.initState();
    print('FirstPage initState()');
  }

  @override
  void dispose() {
    super.dispose();
    print('FirstPage dispose()');
  }

  @override
  Widget build(BuildContext context) {
    print('FirstPage build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('첫번째 페이지'),
      ),
      body: new RaisedButton(
        child: Text('다음 화면'),
        onPressed: () {
          setState(() {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondPage()),
            );
          });
        },
      ),
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  void initState() {
    super.initState();
    print('SecondPage initState()');
  }

  @override
  void dispose() {
    super.dispose();
    print('SecondPage dispose()');
  }

  @override
  Widget build(BuildContext context) {
    print('SecondPage build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('두번째 페이지'),
      ),
      body: RaisedButton(
        child: Text('이전 화면'),
        onPressed: () {
          setState(() {
            Navigator.pop(context);
          });
        },
      ),
    );
  }
}

이 로그대로라면 방금 설명한 대로 build() 메소드에서 복잡한 처리나 네트워크 요청 등을 하면 안 된다는 것을 알 수 있다.

예를 들어 페이지가 10단계로 화면 전환되는 앱을 만들었다면, 10번째 페이지를 표시할 때 그 아래의 9개 페이지도 모두 build() 메소드가 호출될 수 있다. 그렇기 때문에 계산이나 네트워크 요청 등의 로직은 build() 메소드가 아닌 initState() 메소드에서 수행해야 한다.

그렇다고 하더라도 StatefulWidget 클래스의 build() 메소드가 자주 호출되기 때문에 성능이 안 좋지 않을까 걱정할 수 있다. 하지만 구글에서는 이러한 방식에 아무런 문제가 없다고 한다.


정리

  • Navigator 클래스에는 내비게이션 기능을 제공하는 메소드들이 준비되어 있다.
  • 새로운 화면을 표시할 때는 push() 메소드를 사용한다.
  • 현재 화면을 종료하고 이전 화면을 표시하려면 pop() 메소드를 사용한다.
  • pop() 메소드의 두 번째 인수에 이전 화면으로 전달할 값을 지정할 수 있다.
  • push() 메소드는 반환 타입이 Future이며 비동기로 동작하고 작업이 끝날 때까지 기다릴 수 있다.
  • Future 값을 받으려면 async - await 패턴을 사용한다.
  • 비동기 코드가 실행 중일 때는 앱이 멈추지 않는다.
  • StatefulWidget 클래스는 상황에 따라 build() 메소드가 자주 호출될 수 있다.
  • initState() 메소드는 StatefulWidget 클래스가 생성될 때 호출된다.
  • dispose() 메소드는 StatefulWidget 클래스가 종료될 때 호출된다.
Comments