Flutter를 이용하여 개발한 지 시간이 꽤 지났지만 아직 BuildContext에 대해 정확히 알지 못한 채 진행해 왔던 것 같다. BuildContext는 Stateless와 Stateful 위젯 모두에서 사용되며, Dialog를 띄울 때와 화면 높이, 너비를 알아낼 때도 사용되는 등 자주 만나게 되는 만큼 정리해 보고 넘어가려 한다.
BuildContext
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Demo')),
body: Builder(
... 이하 중략
override 메소드인 build는 구현한 UI 위젯들을 화면에 출력될 수 있도록 리턴한다. BuildContext 타입은 현재 위젯의 위젯트리상에서 위치에 관한 정보를 담고 있다.
대략적으로 BiuldContext에 대한 기능을 요약하자면 아래와 같다.
- 플러터 프레임워크에서 사용되는 개념으로, 위젯 트리에서 현재 위젯의 위치와 상태에 대한 정보를 제공
- 현재 위젯의 렌더링 및 업데이트와 관련된 작업을 수행하기 위해 사용
- 위젯 트리의 각 노드에 대해 생성되고, 해당 노드의 위젯에 대한 정보를 담고 있음
- 위젯은 부모 위젯으로부터 BuildContext를 전달받고, 필요한 경우 하위 위젯에게도 BuildContext를 전달하여 정보를 공유
- 위젯 트리의 계층 구조를 따라 단방향인 상위에서 하위로 전달, 따라서 정확한 BuildContext를 사용하는 것이 중요
- 위젯을 찾거나 필요한 상태 및 동작에 접근
대표적으로 높이와 너비를 구할 때 사용할 수 있는데 context는 return 하는 위젯의 부모 위젯에 대한 위치 정보를 가지고 있다. 그렇기 때문에 위에서 언급한 높이와 너비를 알아낼 때 사용할 수 있다.
- double width = MediaQuery.of(context).size.width
- double height = MediaQuery.of(context).size.height
이러한 동작은 가장 가까운 위젯을 탐색하는 특성을 이용한 것인데 이 부분을 조금 더 자세히 알아보자.
가장 가까운 위젯 탐색
BuildContext는 현재 위젯과 가장 가까운 위젯을 찾을 때도 사용할 수 있다. 보통 of를 이용해 찾는데 바로 위에서 설명한 높이와 너비를 찾는 경우에도 사용되어 있다는 것을 생각하면 조금 더 이해하기 쉬울 것이다. 자주 사용하는 Navigator.of(context).pop()과 같은 메소드도 같은 원리이다. 한 페이지 안에서 설명하기 위해 간단하게 SnackBar를 사용하여 예제를 구현해 보았다.
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print("Root BuildContext: ${context.hashCode}");
return Scaffold(
body: Center(
child: Text("Text BuildContext: ${context.hashCode}"), // Root BuildContext와 동일
),
floatingActionButton: FloatingActionButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.blue,
content: Text("SnackBar Builder BuildContext: ${context.hashCode}"), // Root BuildContext와 동일
),
),
tooltip: 'snack_bar',
child: const Icon(Icons.access_alarm),
)
);
}
}
참고로 Scaffold.of(context).showSnackBar(snackBar)는 Flutter 3.0 이후 버전에선 Depreciated 되어 ScaffoldMessenger.of(context).showSnackBar(snackBar)로 사용하는 것이 좋다.
build가 호출되고 난 후 만들어지는 context는 Scaffold의 부모 위젯의 BuildContext이다. build가 실행되는 시점은 Scaffold가 생성이 되기 이전이다. 따라서 build 이전, 특히 Stateful 사용 시 initState에서는 context 등을 활용할 수 없다는 점을 주의하여야 한다.
ScaffoldMessenger.of 메소드는 인자로 제공해 준 context의 조상 중 가장 가까운 Scaffold를 반환해 주는 역할을 수행한다. 가장 주의 깊게 보아야 할 점은 현재 SnackBar가 위치한 곳의 Scaffold가 아니라 부모에 존재하는 Scaffold를 찾는다는 것이다. 지금 상태에선 아래와 같이 모두 동일한 BuildContext를 공유하고 있다.
Root BuildContext: 861044180
Text BuildContext: 861044180
FloatingActionButton Builder BuildContext: 861044180
SnackBar Builder BuildContext: 861044180
사용하는데 큰 문제는 없다. 하지만 이렇게 되면 자식에 해당하는 위젯들은 부모 BuildContext의 정보에 따라 위의 예시였던 높이, 너비를 구하는 것 이외에도 여러 부분에서 독립적이지 못할 수 있다. 이를 해결하기 위해선 두 가지의 방법이 존재한다. Builder를 이용하는 방법과 새로운 Widget class를 만들어 주는 방법이 있다.
새로운 Widget class를 구현하는 것은 프로젝트 관리 측면에서 재사용성이 뛰어나고 가독성이 좋아지므로 구현하고자 하는 위젯의 UI가 복잡하다면 새로운 위젯을 만드는 것도 좋다. 새로운 Widget을 만드는 경우는 너무 익숙하니 생략하고 Builder를 활용하는 방법을 알아보자.
Builder
Builder 클래스를 사용하면 간편하게 구현이 가능하고, 내부 위젯들을 새로운 위젯으로 강제적으로 만들고 그 부모의 context로 접근가능하게 만들어준다.
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print("Root BuildContext: ${context.hashCode}");
return Scaffold(
body: Center(
child: Text("Text BuildContext: ${context.hashCode}"), // Root BuildContext와 동일
),
floatingActionButton: Builder(builder: (context) {
print("FloatingActionButton Builder BuildContext: ${context.hashCode}");
return FloatingActionButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.blue,
content: Builder(builder: (context) {
return Text("SnackBar Builder BuildContext: ${context.hashCode}");
}),
),
),
tooltip: 'snack_bar',
child: const Icon(Icons.access_alarm),
);
})
);
}
}
위와 같이 Builder로 각 자식 위젯들을 감싸게 되면 BuildContext는 Scaffold와 그 아래의 각 Builder 마다 생성되게 된다.
Root BuildContext: 958791018
Text BuildContext: 958791018
FloatingActionButton Builder BuildContext: 720876259
SnackBar Builder BuildContext: 65486211
결과적으로 해당 화면의 위젯트리와 BuildContext의 상태는 아래 그림과 같다. context id는 새로 빌드할 때마다 변경된다.
아래는 Flutter 전체 소스 코드이다.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print("Root BuildContext: ${context.hashCode}");
return Scaffold(
body: Center(
child: Text("Text BuildContext: ${context.hashCode}"), // Root BuildContext와 동일
),
floatingActionButton: Builder(builder: (context) {
print("FloatingActionButton Builder BuildContext: ${context.hashCode}");
return FloatingActionButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.blue,
content: Builder(builder: (context) {
return Text("SnackBar Builder BuildContext: ${context.hashCode}");
}),
),
),
tooltip: 'snack_bar',
child: const Icon(Icons.access_alarm),
);
})
);
}
}
관련 포스트
참고 자료
https://api.flutter.dev/flutter/widgets/BuildContext-class.html
https://velog.io/@rokwon_k/Flutter-Widget%EC%9D%98-%ED%83%80%EC%9E%85%EA%B3%BC-BuildContext
https://velog.io/@okko8522/%ED%94%8C%EB%9F%AC%ED%84%B0%EC%9D%98-BuildContext
https://velog.io/@flxh4894/flutter-context%EC%9D%98-%EC%9D%98%EB%AF%B8
소스 코드
https://github.com/sehoon787/blog/blob/main/Languages/Flutter(Dart)/build_context_test.dart