앱 상에서 페이지 간 전환을 담당하는 Navigator 클래스이다. 지금 하고 있는 프로젝트에서 스택으로 페이지 전환을 정리하는 걸 구현해보려고 하는데, 일단 그 전에 Flutter의 전반적인 화면 전환 방식을 확실하게 이해하고 갈 필요가 있어 보였다. 이 게시글을 작성하는 이유.
Navigator 클래스 - Fltter 공식 API 문서를 읽어 보자.
가장 중요한 용어 정리. Flutter는 (일반적인 표현으로는 스크린, 페이지 등으로 불리는) 모바일 앱 상에서 내용을 표현하는 풀스크린 요소를 Route라고 정의하며, Route는 Navigator로 관리하게 된다. 따라서 아래부터는 페이지 대신 Route라는 용어를 사용하도록 하겠다.
Navigator는 여러 Route를 Stack으로 쌓아 관리하며, 동시에 Stack을 관리하는 아래 두 가지 방법을 제공한다:
...라고는 하는데 결국은 둘 다 쓰게 되어 있는 것 같다. 아래 예제를 참고해보자.
공식 문서에는 정확히는 'Using named navigator routes' 라고 언급하고 있다. 근데 이런 어려운 표현보다는 그냥 URL로 Route에 접근한다고 이해하는 게 편할 것이다.
Named routes 구조는 URL 또는 디렉토리 경로와 유사한 형태의 문자열을 활용하여 Route를 전환한다. (예를 들어 '/main/user/profile'라던지)
아래에 간단한 Named routes 구조를 직접 구현해 보았다. 이 앱은 메인 화면에서 1번을 누르면 첫 번째 페이지로, 2번을 누르면 두 번째 페이지로... n번을 누르면 n 번째 페이지로 전환되는 그런 앱이다. (라고 하기엔 3번까지밖에 구현을 안 했지만.)
어쨌든 이상의 작동 방식을 생각하면서 예제를 보자.
import 'package:flutter/material.dart';
void main() =>
runApp(MaterialApp(home: MyApp(), routes: <String, WidgetBuilder>{
'/first': (BuildContext context) => MyPage(string: "첫 번째 페이지"),
'/second': (BuildContext context) => MyPage(string: "두 번째 페이지"),
'/third': (BuildContext context) => MyPage(string: "세 번째 페이지"),
}));
Named routes 구조는 MaterialApp을 선언할 때, routes 매개 변수에 <String, WidgetBuilder>
형태의 Map 객체를 같이 포함해줌으로써 사용할 수 있다. Map은 다들 알겠지만 Python의 딕셔너리에 해당되는 자료형이고. 그럼 Map에 들어가는 String과 WidgetBuilder가 무엇을 의미하는지 알아보자.
String이 우리가 말하는 URL이다. WidgetBuilder에는 Flutter의 모든 위젯을 빌드하는 Widget build(BuildContext context)
함수를 넣어주면 된다. 그리고 소스 코드에서 String에 해당하는 Route를 표시해야 할 때, WidgetBuilder에 지정된 함수가 실행되면서 Route를 렌더링하게 된다.
에를 들어 "/user": UserPage()
항목이 Map 객체에 있을 때, 프로그램에서 "/user"
Route를 호출하면 UserPage()
위젯에서 구현한 Widget build(BuildContext context)
함수가 실행되면서 새로운 Userpage Widget을 만들고, 이걸 Navigator에 Push하여 화면에 표시해주는 그런 원리가 되시겠다.
자, 그러면 다음으로는 저 소스에 적힌 MyPage(string: "첫 번째 페이지")
의 MyPage
클래스를 구현해보자.
class MyPage extends StatelessWidget {
String string = "기본값";
MyPage({required this.string});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(string)),
body: Center(
child:
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(string, style: const TextStyle(fontSize: 30)),
SizedBox(height: 10),
TextButton(
child: Text("이전 페이지로", style: const TextStyle(fontSize: 20)),
onPressed: () => Navigator.pop(context),
)
])));
}
}
이 클래스에는 명명된 생성자(Named constructor)를 적용했다. string
변수가 MyPage
의 중앙에 표시될 예정이기 때문에, string
변수의 내용을 Widget을 선언할 때 반드시 같이 적어주도록 required
를 붙였다. (명명된 생성자까지 다루면 글이 너무 broad해지므로 여기서는 생략하겠다.)
MyPage
클래스 속 Column
에 지정된 세 가지 Widget을 자세히 보자. 먼저 Text
는 클래스 선언 시 지정해 준 Route에 표시될 내용이다. SizedBox
는 장식용이니까 넘어가고, 마지막으로 TextButton
이다.
MyPage
Route가 열리는 상황을 생각해보자. 메인 Route에서 숫자 버튼을 눌러야 MyPage
가 열릴 것이다. 그러면 스택의 관점에서 봤을 때, 메인 Route의 카운터가 0이면 MyPage
의 카운터는 1일 것이다. 이 상황에서 Navigator.pop
을 호출하면 Navigator
는 카운터 1에 있는 MyPage
를 Pop하면서 우리는 다시 메인 Route를 보게 되는 거다.
이 기능을 TextButton
으로 구현했다. 그래서 TextButton
의 onPressed
매개변수에 Navigator.pop(context)
가 들어가 있는 거고. 이제 여러분은 저 버튼을 누르면 메인 Route로(조금 더 정확히는 한 단계 이전 Route로) 돌아갈 수 있다.
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("안녕")),
body: Center(
child:
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
TextButton(
child: Text("1", style: const TextStyle(fontSize: 30)),
onPressed: () => Navigator.pushNamed(context, "/first"),
),
TextButton(
child: Text("2", style: const TextStyle(fontSize: 30)),
onPressed: () => Navigator.pushNamed(context, "/second"),
),
TextButton(
child: Text("3", style: const TextStyle(fontSize: 30)),
onPressed: () => Navigator.pushNamed(context, "/third"),
)
])));
}
}
Scaffold
는 최대한 간단하게 구현해봤다. Center
랑 TextStyle
은 살짝 더 이쁘게 만들기 위해 넣은 곁가지니까 생략하고, 주요 내용에 해당하는 TextButton
만 보자.
각 TextButton
에는 1부터 3까지 숫자가 있고, 해당 숫자를 누를 경우 첫 번째, 두 번째, 세 번째 페이지로 이동하게 된다.
그럼 첫 번째 TextButton
의 onPressed
매개변수를 보자. Navigator.pushNamed(context, "/first")
라고 되어 있다. 가장 처음에 MaterialApp
의 routes
매개변수에서 지정한 Route다. 따라서 여러분이 이 버튼을 누르면 "/first"
Route에 지정된 MyPage(string: "첫 번째 페이지")
Widget을 빌드하는 함수가 실행되고, 해당 페이지가 여러분에게 보여지게 된다.
Route가 전환될 때, Stack에서 Pop이 되는 식으로 전환된다는 건 앞에서 언급했다. 이 때 사용자는 Navigation.pop()
메소드의 매개 변수에 원하는 값을 지정하여, 화면 전환 시 해당 값이 같이 반환될 수 있도록 코드를 짤 수 있다고 한다.
공식 문서의 예제를 보자.
bool value = await Navigator.push(context, MaterialPageRoute<bool>(
builder: (BuildContext context) {
return Center(
child: GestureDetector(
child: Text('OK'),
onTap: () { Navigator.pop(context, true); }
),
);
}
));
일단 눈여겨 볼 것은, 값을 받아오는 과정을 비동기로 진행한다는 점이다. 그래서 Navigator.push()
앞에 await
키워드가 붙어 있는 것을 볼 수 있다. 이 코드가 하는 일은 아래와 같다:
GestureDetector
로 Wrap된 "OK" 텍스트 버튼이 담긴 Route를 Push한다.true
값을 value
변수에 같이 반환한다.여기서 잠깐, 어떻게 Route를 Pop 하는 동시에 값을 넘겨줄 수 있는지, Navigator.push()
의 공식 문서 설명을 참고하며 확인해보자:
void pop<T extends Object?>(
BuildContext context,
[T? result]
)
보면 사용자가 반환하고자 하는 값을 받아 넘기는 result 매개 변수가 선언되어 있다. 이 코드에서 알 수 있는 것은 아래와 같다:
<T>
는 <Object?>
를 확장한 제네릭임result
는 선택적 매개 변수임Object?
를 확장하였기에 nullable한 모든 자료형(int?
, String?
등)을 다 담을 수 있음 바로 위에서 언급된 Object?
, Object
와 Nullable, Non-nullable 자료형에 대한 더 자세한 내용은 Null 안전성에 대해 이해하기 | Dart 게시글을 참고해보자. 게시글 내용이 상당히 복잡하고 개인적으로는 Null 안전성이 Dart 쓰면서 (지금까지는) 가장 애 먹는 부분 중 하나다. Dart와 Flutter를 만져 볼 생각이 있다면 꼭 이 게시글을 주의 깊게 읽어보자. 도움이 될 것이라고 생각한다.
그리고 두 가지 공부해야 할 것이 새로 생겼다:
Flutter는 비동기 Context에서는 무조건 Future
자료형을 반환하는 것으로 알고 있는데, 어떻게 Future<bool>
이 아닌 일반적인 bool
자료형에 await
키워드가 붙은 함수를 통해 값을 저장할 수 있는 걸까?
다음에는 Null 안전성에 이어 계속 날 짜증나게 만드는 Dart의 비동기 처리를 좀 공부해야겠다.
<T>
이렇게 생긴 친구, C++처럼 Template이라고 부르는 줄 알았는데 Dart에서는 또 Generic이라고 부르나 보다.
Dart의 Generic 체계에 대해 알아볼 필요가 있겠다. 솔직히 C++이랑 생겨먹은 게 비슷해서 본격적으로 파 본 적은 없음.