본문 바로가기
[Flutter]

[Flutter] 내비게이션 / Navigation ( + Future, await )

by Hevton 2021. 9. 5.
반응형

 

내비게이션

새로운 화면으로 전환하거나, 이전 화면으로 돌아가는 것을 내비게이션이라고 한다.

 

 

 

새로운 화면을 띄우는 방법

FirstPage 에서 SecondPage로 전환하려면 Navigator 클래스의 push() 메서드를 사용한다.

// Stateless
return MaterialApp(
...
home: FirstPage()
...


// Stateless
class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: ElevatedButton(
        child: Text('다음 페이지로'),
        onPressed: () {

          Navigator.push( // SecondPage로 화면 이동 코드.
            context,
            MaterialPageRoute(builder: (context) => SecondPage()),
            // MaterialPageRoute는 머테리얼 디자인으로 작성된 페이지 사이에 화면 전환을 할 때 사용된다.
            // MaterialPageRoute는 안드로이드와 iOS 각 플랫폼에 맞는 화면 전환을 지원해준다.
          );

        },
      )
    );
  }
}


//Stateless
class SecondPage extends StatelessWidget {
  const SecondPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('이전 페이지로'),
        onPressed: () {},
      ),
    );
  }
}

lib/main.dart 안에 여러 페이지를 모두 작성할 수도 있고, 별도로 분리하여 페이지 파일을 작성할 수도 있다.

ex)

1. lib/main.dart 

2. lib/main.dart, lib/first_page.dart, lib/second_page.dart
 + 별도로 파일을 분리할 경우엔 import를 통해 다른 파일에 있는 클래스를 사용한다.

실제 프로그램을 작성할 때는 코드를 여러 파일에 분리하는 것이 코드 재활용 등의 면에서 좋다.

 

 

 

이전 화면으로 이동하는 방법

Navigation.push() 메서드로 새로운 화면이 표시되어도, 이전 화면은 메모리에 남게 된다.

이 때 Navigator.pop() 메서드로 현재 화면을 종료하고 이전 화면으로 돌아갈 수 있다.

 

위의 예제코드에서 SecondPage 부분만 수정하면 된다.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('이전 페이지로'),
        onPressed: () {
          Navigator.pop(context); // 현재 화면을 종료하고 이전 화면으로 돌아가기.
        },
      ),
    );
  }
}

 

여기까지 하면, 버튼 클릭시 이동하고, 되돌아가는 예제가 완성된다.

 


 

새로운 화면에 값 전달하는 방법

새로운 화면을 표시하면서, 데이터도 함께 전달하는 방법을 알아본다.

class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: ElevatedButton(
        child: Text('다음 페이지로'),
        onPressed: () {

          final person = Person('홍길동', 20);

          Navigator.push( // SecondPage로 화면 이동 코드.
            context,
            MaterialPageRoute(builder: (context) => SecondPage(person: person)),
            // MaterialPageRoute는 머테리얼 디자인으로 작성된 페이지 사이에 화면 전환을 할 때 사용된다.
            // MaterialPageRoute는 안드로이드와 iOS 각 플랫폼에 맞는 화면 전환을 지원해준다.
          );

        },
      )
    );
  }
}


class SecondPage extends StatelessWidget {

  final Person person;

  SecondPage({required Person this.person});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('사람 : ${person.name}'),
        onPressed: () {
          Navigator.pop(context); // 현재 화면을 종료하고 이전 화면으로 돌아가기.
        },
      ),
    );
  }
}

선택을 의미하는 {} 안에 required를 붙여주면 필수 입력이라고 지정해주는 옵션이 된다.

 

First에서 Second로 버튼을 클릭하여 이동하면, 잘 전달되었음을 알 수 있다.

 

이전 화면으로 데이터 돌려주기

Navigator.push()와 Navigator.pop() 메서드를 조금 수정하면,

SecondPage 클래스에서 FirstPage 클래스로 데이터를 돌려줄 수도 있다.

class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: ElevatedButton(
        child: Text('다음 페이지로'),

        // FirstPage 클래스가 SecondPage 클래스로부터 데이터를 돌려받으려면, 다음과 같이 수정한다.
        onPressed: () async { // async. await 키워드를 사용하는 메서드는 async선언을 해야함.
          final person = Person('홍길동', 20);
          // push() 메서드는 Future 타입의 반환 타입을 갖는다.
          final result = await Navigator.push( // await
            context,
            MaterialPageRoute(builder: (context) => SecondPage(person: person)),
          );

          print(result);
        },
      )
    );
  }
}


class SecondPage extends StatelessWidget {

  final Person person;

  SecondPage({required Person this.person});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('사람 : ${person.name}'),
        onPressed: () {
          Navigator.pop(context, 'ok'); // 현재 화면을 종료하고 이전 화면으로 돌아가기.
        },
      ),
    );
  }
}

이렇게 하면 push() 메서드가 어떤 값을 반환할 때까지 기다린다. (앱 자체가 멈추는 것은 아니다)

나중에 값이 들어오면 그 값이 result에 담기고, 그 뒤에야 print 문이 실행된다.

-> 코드는 거기서 wait되지만 앱 자체는 멈추지 않는다는 것 같다.

 

이렇게 어떤 일이 끝날때까지 기다리면서, 앱이 멈추지는 않는 방식을 비동기 방식이라고 한다.

 

 

쿠퍼티노 디자인 글을 쓰다가도 느꼈지만,

Future<T> x = XX() 를 사용하여 블럭된 뒤 리턴받았을 때, x에는 결과값이 들어가지 않고

x를 이용하여 x.then()을 통해 반환값을 받아왔는데

await를 사용하면 리턴값이 곧바로 변수에 들어가는 것 같다..?!

+ 그리고 Future 리턴형식이어도 위 둘중 아무 처리도 안하면 안기다리는 것 같고.. (지금까지 느낌으로는)

 


 

routes를 활용한 내비게이션

routes를 활용한 내비게이션을 사용하면 좀 더 간결하고 체계적인 방법으로 내비게이션을 구성할 수 있다.

별칭같은걸 미리 체계적으로 명시한 다음에, 간결하게 코드를 작성하여 사용하는 방식이다.

// Stateless

return MaterialApp(
...
home: FirstPage(),

      // routes는 MaterialApp 클래스의 routes 프로퍼티에 다음과 같이 정의할 수 있음.
      routes: {
        // routes 프로퍼티에 키-값 쌍으로 정의.
        // 페이지 구조를 /first/a/b와 같은 형식으로 구조화하면 좋기에 맨 앞에 슬래시 기호를 사용한 형식을 쓴 것. 이러한 방식을 추천한다.
        '/first': (context) => FirstPage(),
        '/second': (context) => SecondPage(),
      }
...


// Stateless
class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: ElevatedButton(
        child: Text('다음 페이지로'),
        onPressed: ()  {
          // 이동 시에는 push() 대신에 pushNamed() 메서드를 사용한다.
          Navigator.pushNamed(context, '/second');
        },
      )
    );
  }
}


// Stateless
class SecondPage extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('Second Page'),
        onPressed: () {
          Navigator.pop(context); // 현재 화면을 종료하고 이전 화면으로 돌아가기.
        },
      ),
    );
  }
}

 


내비게이션 동작 방식의 이해

push() 메서드로 새로운 화면을 띄우고, pop() 메서드로 이전 화면으로 돌아간다는 것을 확인했다.

화면은 stack 구조로 메모리에 쌓이게 된다. 스택에서 모든 화면이 제거되면 앱은 종료된다.

 

 

StatelessWidget 클래스 동작방식

home: FirstPage()



class FirstPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    print('FirstPage Build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('First'),
      ),
      body: ElevatedButton(
        child: Text('다음 페이지로'),
        onPressed: () async {
          final result = await Navigator.pushNamed(context, '/second');

          print(result);
        },

      )
    );
  }
}


class SecondPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    print('SecondPage Build()');
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: ElevatedButton(
        child: Text('Second Page'),
        onPressed: () {
          Navigator.pop(context, 'ok'); // 현재 화면을 종료하고 이전 화면으로 돌아가기.
        },
      ),
    );
  }
}

FirstPage() -> 클릭 -> SecondPage() -> 클릭 순으로 이동하면, 아래와 같은 순서로 로그가 뜬다.

I/flutter ( 8780): FirstPage Build()
I/flutter ( 8780): FirstPage Build()
I/flutter ( 8780): SecondPage Build()
I/flutter ( 8780): ok

FirstPage Build가 기본적으로 두번씩 뜨는데.. 이건 왠지는 모르겠고,,, 어쨌든 로그의 순서 자체가 이렇다는 것에 집중하면 된다.

 

 

StatefulWidget 클래스 동작방식

결과는 동일하다.

책에서는 A -> B로 갈 때 A의 빌드가 재호출된다고 하는데, 이전 버전에서는 이러고 현재 버전은 이렇지 않다.

즉 현재버전기준 StatelessWidget과 그냥 동일하다.

 


 

initState와 dispose

StatefulWidget 클래스에는 build() 메서드 외에도 특정 타이밍에 실행되는 여러 메서드가 있다.

이러한 메서드들을 생명주기 메서드라고 부른다.

 

일단 그 중 두가지만 간단히 보자.

 

initState() :  위젯이 생성될 때 호출된다.

dispose() : 위젯이 완전히 종료될 때(pop) 호출된다.

 

순서 : initState() -> build() -> dispose()

 


참고

책에서는

'StatefulWidget A에서 StatefulWidget B로 push하여 이동할 때',

'다시 B에서 pop하여 A로 이동할 때',

A의 build()가 재호출되는것으로 나와있는데( 205p ~ 207p ) 내가 확인한 결과는 그렇지 않았다.

 

알아본 바로는, 이전 버전의 sdk에서는 책처럼 동작한다고 하나, 현재 버전에서는 내 결과가 맞는 것으로 확인된다.

 

 

또한, 그렇기에 예전(책에서 설명하는)버전에서는 10단계의 화면 전환 페이지라면 10번째 페이지에서 아래 9개의 페이지가 모두 build() 메서드가 호출되기 때문에, 계산이나 네트워크 요청 등의 오래 걸리는 복잡한 처리는 build() 말고 initState()에서 해야만 한다고 하는데( 205p ~208p)

현재버전에서는 10단계의 화면 전환 페이지여도, 10번째 페이지에서 10번째 페이지의 build() 메서드만 호출되기 때문에

복잡한 처리를 build()가 아닌 initState()에서 해야한다는게 확실한 정보인지는 더 자세히 알아봐야 할 것 같다.

 


 

플러터가 나온 지 얼마 안되어 계속 큰 변화를 맞이하고 있어서, 저자님께서 고생을 많이 하실 것 같다..화이팅!

 

서적 : 오준석의 플러터 생존코딩

반응형