Flutter Navigator 사용법
플러터에서는 뭘로 화면 전환하지?
Navigator를 사용하여 화면전환이 가능하다.
화면 전환 코드에 대해 한 번 확인해보자.
사전 준비
별도의 사전 준비는 필요 없다.
다만, 코드 변경이 조금 필요하다.
void main() => runApp(
MaterialApp(
home: MyApp(),
)
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
}
MaterialApp이 뭐지?
플러터에서 Material Design을 적용하기 위한 최상위 위젯이다.
앱의 전반적인 구조와 동작을 설정하며 여러 기능들을 담당하는데,
특히 여기를 주목
"ㆍ라우팅 관리 (routes, onGenerateRoute, onUnknownRoute): 앱 내에서 화면 간의 네비게이션을 관리"
이 말은, 화면 전환은 MaterialApp 위젯이 필수이며,
MaterialApp으로 앱 내 모든 위젯을 포함해야 한다.
때문에, 아래처럼 선언해야 Navigator를 쓸 수 있다.
void main() => runApp(
MaterialApp(
home: MyApp(),
)
);
그럼 저렇게 안하면 어떤 일이 발생할까?
Navigator operation requested with a context that does not include a Navigator.
Android Studio 사용 중이면 이런 문구가 빨간색으로 표시되고,
화면 전환이 일절 발생하지 않는다.
전체 코드
main.dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('첫 번째 화면'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
},
child: const Text('두 번째 화면으로 이동'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('두 번째 화면'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('이전 화면으로 돌아가기'),
),
),
);
}
}
실행 결과
좀 더 효율적으로 쓰려면?
당연 문제 없다.
오히려 라우터 분리를 권장한다.
매 번 Navigator.push가 호출된다고 생각해봐라.
그 때마다 고통스럽다.
코드 변경에 분노하고,
유지보수 하기도 힘들어서 분노한다.
먼저 파일을 세 가지로 나눈다.
lib/
└ main.dart
└ utils/
└ route_manager.dart
└ features/
└ my_app.dart
└ count/
└ count_screen.dart
└ count_provider.dart
└ count_provider.g.dart
└ dialog/
└ dialog_screen.dart
main.dart
import 'package:flutter/material.dart';
import './features/my_app.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(ProviderScope(
child: MaterialApp(
home: MyApp(),
debugShowCheckedModeBanner: false,
),
));
}
상태관리 도구로 Riverpod를 사용하기로 했다.
Riverpod로 상태관리를 하려면 전역을 ProviderScope로 감싸야 한다.
그래야 사용 가능하다.
왜 RiverPod인가?
플러터 상태관리는 C언어의 동적 메모리 할당과 같다.
C언어에서 특정 메모리 공간을 할당할 때,
원하는 시점에 할당 하고, 사용 후, 반납하는 코드가 있다.
똑같이, 플러터도 상태를 할당했으면,
안 쓸 땐 해제해야 한다.
그럼 해제하는 코드 dispose 쓰면 되는 거 아님?
상태가 한 두개면 상관없다.
하지만 앱이 커지면서 상태를 많이 쓰면?
이 때 부터 하나 둘씩 놓치게 돼, 메모리 누수가 발생한다.
Riverpod는 자동 해제를 지원한다.
때문에, 나는 Riverpod를 쓴다.
실수없이 잘 관리하면 되잖아??
뭐, 그렇게 말하면 할 말 없다.
그땐 자유롭게 쓰면 되니까.
my_app.dart
// lib/my_app.dart
import 'package:flutter/material.dart';
import './count/count_screen.dart';
import './dialog_example/dialog_example.dart';
import '../utils/route_manager.dart';
class MyApp extends StatelessWidget{
MyApp({
super.key
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: buildColumn(context),
),
);
}
// 위젯을 빌드에 필요한 context를
Widget buildColumn(BuildContext context){
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
buildElevatedButton(context, CountScreen(number: 5), "count example"),
buildElevatedButton(context, DialogExample(), "dialog example"),
],
);
}
Widget buildElevatedButton(BuildContext context, Widget target, String buttonName){
return Container(
margin: EdgeInsets.all(15),
child: ElevatedButton(
onPressed: () {
RouteManager.moveTo(context, () => target);
},
child: Text("${buttonName}")
),
);
}
}
my_app에는 Navigator를 직접 쓰지 않는다.
route_manager.dart
// lib/utils/route_manager.dart
import 'package:flutter/material.dart';
class RouteManager {
static void moveTo(
BuildContext context,
Widget Function() targetBuilder, {
bool fullscreenDialog = false,
}) {
// future: 조건 확인, 로그, 애니메이션 등 가능
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => targetBuilder(),
fullscreenDialog: fullscreenDialog
)
);
}
}
static으로 두고, 언제든 사용할 수 있는 형태로 정의한다.
저렇게 정의하면 뭐가 좋냐?
1. 위젯마다 파라미터 유연한 처리 가능
2. 해당 함수만 호출하면 화면전환이 자유로움
count_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'count_provider.dart'; // 위에서 만든 상태관리 코드
class CountScreen extends ConsumerWidget {
final number;
CountScreen({
super.key,
this.number,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Scaffold(
body: Center(
child: Text('Count $number : $count', style: TextStyle(fontSize: 40)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(countProvider.notifier).increment();
},
child: Icon(Icons.add),
),
);
}
}
count_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'count_provider.g.dart'; // 이거 빌드하면 생성됨
@riverpod
class Count extends _$Count {
@override
int build() {
return 0; // 초기값 0
}
void increment() {
state++;
}
void decrement() {
state--;
}
}
count_provider.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'count_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$countHash() => r'9d2b44a8c99f10f03e7255c3bfd4c417ce9a6000';
/// See also [Count].
@ProviderFor(Count)
final countProvider = AutoDisposeNotifierProvider<Count, int>.internal(
Count.new,
name: r'countProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$countHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Count = AutoDisposeNotifier<int>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
위 코드는 build runner라는 명령어로 상태 관리도구를 자동생성한다.
이번 예제에서만 원활한 상태관리를 위해
자동 생성 도구를 썼지만,
실제로 Dart 지원 범위의 한계와
아직 적용 안 되는 상태관리 도구가 있어서 그런지
이후 작성할 땐 자동생성 도구를 쓰지 않을 예정이다.
dialog_screen.dart
import 'package:flutter/material.dart';
class DialogScreen extends StatelessWidget{
DialogScreen({super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: (){
print("눌렀습니다");
},
child: Text("dialog 예제")),
);
}
}
실행결과
잘 실행된다.
Count에서는 생성자로 number를 받았지만
Navigator로 자동 전달 돼,
화면전환 도구의 구현체를 전혀 신경 쓸 필요 없다.
dialog 버튼도 누름면 눌렀습니다 라는 문구를 잘 표시한다.
결론
오늘은 Flutter의 화면전환에 대해 간단히 알아봤다.
이번 포스팅의 예제 외 다른 방식도 존재한다.
만약, 이 포스팅의 화면 전환 방식이 마음에 안든다면
다른 방법을 검색하여 찾아보면 되겠다.
플러터를 시작한지 얼마 안 돼, 아직 알아봐야 할 게 많다.
아마 Navigator 만으로는 안되는 무언가가 있지 않을까 생각한다.