티스토리 뷰
오늘은 유데미의 provider 강의 중 들은 부분 중 addPostFrameCallback을 사용한 에러 해결 부분을 정리해보려고 한다!
(흔히 사용하는 콜백 메서드이지만, 이렇게 케이스를 정리해서 알려주시니 딱 정리가 되지 뭐야 ʅ(´・ᴗ・` )ʃ)
https://www.udemy.com/course/flutter-provider-essential-korean/
0. 사전 준비
너무 길어서 접어두겠음!
오늘의 실습을 위해서는 총 3개의 페이지와 하나의 클래스가 필요함
- 하나는 맨날 쓰는 ChangeNotifier를 mixin한 Couner class
import 'package:flutter/material.dart';
class Counter with ChangeNotifier {
int number = 0;
void increment() {
number++;
notifyListeners();
}
}
- 그리고 각 페이지로 연결해줄 메인 페이지
MaterialApp을 ChangeProvider로 Wrap해주고 위에서 만든 Counter타입의 객체로 create 해주었음!
이 말은 이제부터 context.read<Counter>()이나 context.watch<Counter>()를 할 수 있다는 얘기가 되겠음
import 'package:addphotocallback_error/pages/counter_page.dart';
import 'package:addphotocallback_error/pages/dialog_page.dart';
import 'package:addphotocallback_error/pages/navigator_page.dart';
import 'package:addphotocallback_error/provider/counter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.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 ChangeNotifierProvider<Counter>(
create: (context) => Counter(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'addPostFrameCallback'),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: ListView(
children: [
ListTile(
title: const Text(
'Counter page',
style: TextStyle(fontSize: 20),
),
subtitle: const Text(
'initState에서 provider 접근',
style: TextStyle(color: Colors.grey),
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CounterPage(),
),
);
},
),
ListTile(
title: const Text(
'Dialog page',
style: TextStyle(fontSize: 20),
),
subtitle: const Text(
'initState에서 showDialog\n특정 조건에서 showDialog',
style: TextStyle(color: Colors.grey),
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DialogPage(),
),
);
},
),
ListTile(
title: const Text(
'Navigator page',
style: TextStyle(fontSize: 20),
),
subtitle: const Text(
'특정 조건에서 Navigator push',
style: TextStyle(color: Colors.grey),
),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NavigatorPage(),
),
);
},
)
],
),
),
);
}
}
- 그리고 메인페이지의 리스트뷰에서 각 연결될 각각의 페이지
counter_page.dart
import 'package:addphotocallback_error/provider/counter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int myCounter = 0;
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
final number = context.watch<Counter>().number;
return Scaffold(
appBar: AppBar(
title: const Text(
'Counter Page',
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'provider : $number',
style: TextStyle(fontSize: 30),
),
Text(
'내꺼 : $myCounter',
style: TextStyle(fontSize: 30),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<Counter>().increment();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
dialog_page.dart
import 'package:addphotocallback_error/provider/counter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class DialogPage extends StatefulWidget {
const DialogPage({super.key});
@override
State<DialogPage> createState() => _DialogPageState();
}
class _DialogPageState extends State<DialogPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final number = context.watch<Counter>().number;
return Scaffold(
appBar: AppBar(
title: const Text(
'Dialog Page',
),
),
body: Center(
child: Text(
'$number',
style: TextStyle(fontSize: 30),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<Counter>().increment();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
navigator_page.dart
import 'package:addphotocallback_error/provider/counter.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class NavigatorPage extends StatefulWidget {
const NavigatorPage({super.key});
@override
State<NavigatorPage> createState() => _NavigatorPageState();
}
class _NavigatorPageState extends State<NavigatorPage> {
@override
Widget build(BuildContext context) {
final number = context.watch<Counter>().number;
return Scaffold(
appBar: AppBar(
title: const Text(
'Navigator Page',
),
),
body: Center(
child: Text(
'$number',
style: TextStyle(fontSize: 30),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<Counter>().increment();
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class DestinationScreen extends StatelessWidget {
const DestinationScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('navigator push'),
),
);
}
}
1. initState에서 provider의 객체에 접근한다면? (conter_page.dart)
counter_page.dart 파일에서 CounterPage 클래스의 initState에서 아래처럼 프로바이더 객체에 접근해보겠음!
@override
void initState() {
// TODO: implement initState
context.read<Counter>().increment();
myCounter = context.read<Counter>().number + 10;
super.initState();
}
그러면 아래처럼 에러가 발생!
setState() 또는 markNeedsBuild()가 빌드 중에 호출되었다고 함! Flutter에서 빌드 중에는 위젯 트리를 재구성하고 있기 때문에, 이 시점에 위젯을 다시 빌드하도록 요청하면 혼란을 초래할 수 있다고 함!
한 마디로, 현재 위젯트리를 그리고 있는데 context.read를 함으로써 다시 빌드해달라고 요청한 상황! 그래서 에러가 발생했음!
해결법은? 바로 addPostFrameCallback!
이 메서드는 프레임이 다 그려진 후 callback되는 메서드이기 때문에 화면이 다 그려진 시점으로 실행을 미룰 수 있음
@override
void initState() {
// TODO: implement initState
WidgetsBinding.instance.addPostFrameCallback(
(_) {
context.read<Counter>().increment();
myCounter = context.read<Counter>().number + 10;
},
);
super.initState();
}
이렇게 하면 화면이 정상적으로 작동함!
또 다른 해결책으로는 Future.delayed를 주는 방법도 있음! 이렇게 해도 정상 작동함!
Future.delayed는 비동기적으로 동작하고, initState()가 호출된 후 프레임이 완료된 후에 상태를 변경하기 때문에 가능하다고 함!
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 0), () {
context.read<Counter>().increment();
myCounter = context.read<Counter>().counter + 10;
});
}
2. initState에서 바로 dialog 띄우기 (dialogPage.dart)
이번에는 initState에서 바로 dialog를 띄우고 싶음!
@override
void initState() {
// TODO: implement initState
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('저는 얼럿입니다.'),
content: Text('제가 initState에서 나왔나요?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('확인'))
],
),
);
super.initState();
}
에러 발생!!!
찾아보니 initState 중에 context에 접근해서 그렇다고 함.
그러면 이 문제 또한 위젯 트리가 빌드 된 뒤로 showDialog를 미루면 되겠음!
@override
void initState() {
// TODO: implement initState
WidgetsBinding.instance.addPostFrameCallback(
(_) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('저는 얼럿입니다.'),
content: Text('제가 initState에서 나왔나요?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('확인'))
],
),
);
},
);
super.initState();
}
정상적으로 initState에서 dialog가 뜸!
2-2. 특정 조건에서 dialog 띄우기! (dialogPage.dart)
Counter 프로바이더의 number 값이 3이 됐을 때 dialog가 짠하고 나오게 하고 싶음!
그래서 아래와 같이 코드를 작성했더니?
@override
Widget build(BuildContext context) {
final int number = context.watch<Counter>().number;
if (number == 3) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Count is 3'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context), child: Text('확인'))
],
);
},
);
}
...
}
에러가 발생! 현재 빌드 중인 위젯은 Overlay 위젯이며, 빌드가 필요한 상태로 표시될 수 없습니다. Flutter 앱에서 setState() 또는 markNeedsBuild() 메서드를 잘못된 시기에 호출했을 때 발생하며 현재 빌드 중인 위젯은 Overlay 위젯이라고 함
Dialog 위젯은 Overlay 위젯인데, 그 아래 있는 Dialog Screen이 빌드되는 와중에 띄우려고 하니, 에러가 발생한 듯!
이 에러 또한 addPostFrameCallback 메서드로 해결이 가능
@override
Widget build(BuildContext context) {
final int number = context.watch<Counter>().number;
if (number == 3) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Count is 3'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('확인'))
],
);
},
);
},
);
}
...
}
원하는 조건이 됐을 때 정상적으로 Dialog가 show됨
3. 마지막으로 특정 조건에서 navigator.push()를 하고 싶을 때는? (navigator_page.dart)
이번에는 Counter 객체의 number 값이 3이 되었을 때 특정 페이지로 넘기고 싶음!
그래서 아래처럼 작성했더니?
@override
Widget build(BuildContext context) {
final number = context.watch<Counter>().number;
if (number == 3) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DestinationScreen(),
));
}
...
}
또 에러가 발생.
number가 3이 되었을 때 Dialog를 띄웠던 2-2와 유사한 에러로, 에러가 발생한 시점에 빌드되고 있던 위젯이 NavigatorPage라는 것이 다름. 이 에러 또한 addPostFrameCallback을 이용해 해결이 가능
@override
Widget build(BuildContext context) {
final number = context.watch<Counter>().number;
if (number == 3) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DestinationScreen(),
));
},
);
}
}
문제없이 작동
!
이렇게 빌드 중에 context에 접근하거나, setState() 중에 dialog를 띄운다거나 navigator.push 등의 작업을 할 때는 특별히 주의를 기울여야겠음!
'Flutter' 카테고리의 다른 글
ProxyProvider 간단 사용 예제 (0) | 2024.07.16 |
---|---|
try-catch 정리해보기 (0) | 2024.07.11 |
Form 필드로 유효성 체크하기 (1) | 2024.07.02 |
Kodeco Flutter 면접 질문_Junior Written Questions2 (0) | 2024.06.25 |
ChangeNotifierProvider 간단 사용기 (0) | 2024.06.21 |
- Total
- Today
- Yesterday