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();
}
}
},
);
}
}
FirebaseAuthService({auth})
: _auth = auth ?? FirebaseAuth.instance,
_isLoading = false;
bool _isLoading;
set loading(bool val) {
_isLoading = val;
print('loading: $val');
notifyListeners();
}
...
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 :(');
}
}
Widget
static void showErrorSnackBar(BuildContext context, String message) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(
message,
),
backgroundColor: Colors.red,
),
);
}
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()
을 호출하여 상위 항목에 대한 참조를 저장하십시오.
https://stackoverflow.com/questions/49100196/what-does-buildcontext-do-in-flutter
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된 상황
다음과같이 출력
print('RootePage.build. ${context.hashCode}');
root_page
에서 listen true
로 할 필요가 없었다.listen: true
로 하다보니 재빌드가 두번되고있었고결론 :
listen: true
로 빠르게 두번 재빌드 될 때 엉킨 코드를 잘 보자!
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();
}
}
},
);
}
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();
}
}
},
);
}
Widget
20.06.15
추가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()를 사용하는 것입니다.
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
...
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text('message'),
duration: Duration(seconds: 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를 호출할 수 있게 됩니다.
요런 느낌으로 작성해보았네요 :)
솔루션이 좋습니다. 물론 해결은 못했습니다....