👩🏻‍💻code

root code

class RootPage extends StatelessWidget {
  FirebaseAuthService _auth;

  
  Widget build(BuildContext context) {
    _auth = Provider.of<FirebaseAuthService>(context, listen: true);
    return StreamBuilder(
      stream: _auth.auth.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CandyWidget.showCircularLoadingBar(isLoading: _auth.loading);
        } else {
          if (snapshot.hasData) {
            _auth.firebaseUser = snapshot.data;
            return HomePage();
          } else {
            return LoginPage();
          }
        }
      },
    );
  }
}

provider auth service logic

FirebaseAuthService({auth})
      : _auth = auth ?? FirebaseAuth.instance,
        _isLoading = false;

  bool _isLoading;

  set loading(bool val) {
    _isLoading = val;
    print('loading: $val');
    notifyListeners();
  }
  
  ...

login code

  
  Widget build(BuildContext context) {
    _auth = Provider.of<FirebaseAuthService>(context, listen: true);
    
    return SafeArea(
      child: FocusScope(
        node: _focusNode,
        child: _buildWidget(),
      ),
    );
  }
  
  Widget _buildWidget() {
    return Stack(
      children: <Widget>[
        Scaffold(
          body: Builder(
            builder: (context) {
              return Form(
                child: ListView(
                    children: <Widget>[
                      Text('Login'),
                      _buildEmailInput(),
                      _buildPasswordInput(),
                      _buildSubmitButton(context),
                      _buildSignUpButton(context),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
        CandyWidget.showCircularLoadingBar(isLoading: _auth.loading ?? false),
      ],
    );
  }

Widget _buildSubmitButton(BuildContext context) {
    return RaisedButton(
      onPressed: () async {
        return _submit(context);
      },
      child: Text('submit'),
    );
  }

  Future<void> _submit(BuildContext context) async {
    if (_formKey.currentState.validate()) {
      print('validation ture :)');
      final user = await _auth.signInWithEmailAndPassword(
          email: _emailController.text, password: _passwordController.text);

      if (user == null) {
        print('로그인 맞지않습니다.');
        CandyWidget.showErrorSnackBar(context, _auth.lastErrorCode);
      }
    } else {
      print('validation false :(');
    }
  }

showErrorSnackBar code

static void showErrorSnackBar(BuildContext context, String message) {
    Scaffold.of(context).showSnackBar(
      SnackBar(
        content: Text(
          message,
        ),
        backgroundColor: Colors.red,
      ),
    );
  }




🙅🏻error message

E/flutter ( 7562): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter ( 7562): At this point the state of the widget's element tree is no longer stable.
E/flutter ( 7562): To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

Builder 통해 새 context를 만들어줬는데 왜 사용을 못하니!!!!!!!!!


Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.

Unhandled Exception : deactivated 된 상위(부모,조상) 위젯을 Looking up(바라보는 건) 안전하지 않습니다.

At this point the state of the widget's element tree is no longer stable.

이 시점에서 widget's element tree의 상태는 더 이상 안정적이지 않습니다.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

dispose() 메소드에서 위젯의 상위 항목을 안전하게 참조(조회)하려면 위젯의 didChangeDependencies() 메소드에서 dependOnInheritedWidgetOfExactType()을 호출하여 상위 항목에 대한 참조를 저장하십시오.





+ buildcontext 이해

https://stackoverflow.com/questions/49100196/what-does-buildcontext-do-in-flutter

  • 모든 위젯을 수동으로 데이터를 할당하지 않음.
  • 데이터를 아래쪽(하위 객체)으로 전달하려는 경우 매우 유용
  • 어디서나 엑세스 가능
  • 각 위젯에는 고유한 Buildcontext 존재




🙆🏻solution

사건정리

root_page.dart에서 loading의 변경사항을 읽을려고 loading값을 set해줄 때 마다 notifyListeners(); 호출

set loading(bool val) {
    _isLoading = val;
    notifyListeners();
  }

root build에 작성해둔 listen true로 되어있어서 변경사항이 바뀔때마다 context를 새로 그림.

_auth = Provider.of<FirebaseAuthService>(context, listen: true);

그러다보니 snack bar를 그릴때 보면 root에선 두번 호출이되는데
(로직 시작할때 true, 끝나고 finally에 false)

snackbar를 그릴 context가 await으로 loading true였을때 context를 바라보고있고
따로 snackbar를 그리고 출력할려고 하는 시점에는 이미 loading false로 새 context가 만들어져서

기존 context는 deactivated 하게 된 상황.

그림으로 사건정리

위 내용이 이해가 안됬다면 그림으로..!
((이해가 되신 분들은 넘어가셔도 좋습니다))

root page 구조

root page 구조에서 provider service에서 로딩이 바뀔 경우 listen:true로 해놨기에 (2)처럼 다시 그려짐.

근데 로직 상 loading이 true 됐다가 처리 로직 끝나면 곧바로 false가 되는 두번 호출되는 상황
((그림으로 보면 (2)과 (4) 빠르게 슈슉 2번 호출됨.))

Scaffold.of(context).showSnackBar() 그릴땐 이미 4번의 새로운 context가 만들어지고있고,
async로 2번의 context를 참조해 show!!! 그려줘!!!! 스낵바!!!!! 이러고 있었던 것..

그래서 4번의 context가 active한 상태가 되었고 기존 2번의 context는 deactivated된 상황

+ context가 새로 그린지 쉽게 알 수 있는 방법?

다음과같이 출력

print('RootePage.build. ${context.hashCode}');

해결한 코드

  • root_page에서 listen true로 할 필요가 없었다.
  • 이유 : 내가 똥멍청이라서 connection.waiting을 통해 이미 로딩중인지 검사하고 있음에도 불구하고 _auth.loading 값을 받아오려했다....
  • 필요없는곳에서 listen: true로 하다보니 재빌드가 두번되고있었고
    async로 snackbar를 처리하고있던 죄없는 login showSnackBar는 이전 context를 바라보고...ㅠㅠ

결론 : listen: true로 빠르게 두번 재빌드 될 때 엉킨 코드를 잘 보자!

Before


  Widget build(BuildContext context) {
    _auth = Provider.of<FirebaseAuthService>(context, listen: true);
    return StreamBuilder(
      stream: _auth.auth.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
        				// 바로 이부분 때문에 true로 해놨었는데 👇
          return CandyWidget.showCircularLoadingBar(isLoading: _auth.loading);
        } else {
          if (snapshot.hasData) {
            _auth.firebaseUser = snapshot.data;
            return HomePage();
          } else {
            return LoginPage();
          }
        }
      },
    );
  }

After


  Widget build(BuildContext context) {
    _auth = Provider.of<FirebaseAuthService>(context, listen: false);
    return StreamBuilder(
      stream: _auth.auth.onAuthStateChanged,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
       		// 이미 waiting 일때만 loadingbar를 그리는것이니 true로 고정🤬
          return CandyWidget.showCircularLoadingBar(isLoading: true);
        } else {
          if (snapshot.hasData) {
            _auth.firebaseUser = snapshot.data;
            return HomePage();
          } else {
            return LoginPage();
          }
        }
      },
    );
  }

+ 20.06.15 추가

https://medium.com/@ksheremet/flutter-showing-snackbar-within-the-widget-that-builds-a-scaffold-3a817635aeb2

Global Key

buildcontext로 간혹가다 안되시는 분 globalkey로 구현하시는걸 추천드립니다.

A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the Scaffold.of() called with a context that does not contain a Scaffold.

훌륭한(완벽한) 솔루션은 아니지만, 편리한(적당한) 솔루션은 GlobalKey를 Scaffold에 할당 한 다음, Scaffold를 포함하지 않는 context와 함께 호출 된 Scaffold.of()를 사용하는 것입니다.

Solution Code (Global Key)

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      ...
  • call
_scaffoldKey.currentState.showSnackBar(
  SnackBar(
    content: Text('message'),
    duration: Duration(seconds: 3),
  ));
profile
𝙸 𝚊𝚖 𝚊 𝚌𝚞𝚛𝚒𝚘𝚞𝚜 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 𝚠𝚑𝚘 𝚎𝚗𝚓𝚘𝚢𝚜 𝚍𝚎𝚏𝚒𝚗𝚒𝚗𝚐 𝚊 𝚙𝚛𝚘𝚋𝚕𝚎𝚖. 🇰🇷👩🏻‍💻

7개의 댓글

comment-user-thumbnail
2020년 5월 28일

솔루션이 좋습니다. 물론 해결은 못했습니다....

2개의 답글
comment-user-thumbnail
2020년 11월 3일

Snackbar 를 모든 widget에서 손쉽게 사용하고자 할 때
저는 SnackBarModule class를 정의한 후
get_it package(https://pub.dev/packages/get_it)를 통해 앱이 구동될 때 주입시켜 놓습니다.
SnackBarModule은 아래와 같이 key 와 snackbar를 보여주는 메소드 2개를 가지고 있습니다.
import 'package:flutter/material.dart';

class SnackBarModule {
GlobalKey scaffoldKey = GlobalKey();

void showSnackBar(BuildContext context, String message) async {
final snackBar = SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: Duration(milliseconds: 1500),
backgroundColor: Color.fromRGBO(138, 153, 163, 1),
);
Scaffold.of(context).showSnackBar(snackBar);
}
}

그 후 MaterialApp 하단의 Scaffold의 key에 SnackBarModule 의 scaffoldKey를 전달해줍니다.

이제, 이 후에는 Scaffold 안에 해당하는 모든 widget은 어디에서나 SnackBar를 호출할 수 있게 됩니다.

요런 느낌으로 작성해보았네요 :)

1개의 답글
comment-user-thumbnail
2020년 11월 16일

ㅎ 넘나 어렵..

1개의 답글