티스토리 뷰

오늘은 유데미의 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 등의 작업을 할 때는 특별히 주의를 기울여야겠음!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크