Using flutter HookWidget and didChangeAppLifecycleState - flutter

How can I monitor the life cycle states of the app from a particular page using HookWidget the way you can with a Stateful widget?
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
...
}
if (state == AppLifecycleState.resumed) {
...
}
if (state == AppLifecycleState.detached) {
...
}
}

First make a class:
class MyObserver implements WidgetsBindingObserver {
}
Then create it and register it with:
Widget build(BuildContext) {
useEffect(() {
final observer = MyObserver();
WidgetsBinding.instance.addObserver(observer);
return () => WidgetsBinding.instance.removeObserver(observer);
}, const []);
...
}

Flutter hooks is shipped with an inbuilt didchangeapplifecycle
access it as follows
final appLifecycleState = useAppLifecycleState();
useEffect(() {
print("current app state");
print(appLifecycleState);
if (appLifecycleState == AppLifecycleState.paused || appLifecycleState == AppLifecycleState.inactive) {
//...
} else if (appLifecycleState == AppLifecycleState.resumed) {
//...
}
return null;
}, [appLifecycleState]);

In the docs here search for "ways to create a hook". You'll see there are 2 ways of creating a hook, using a function or using a class. You are going for the "using a class" one. Then use initHook override as your initState and dispose works the same. Thats how I implemented it on my end.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
useWidgetLifecycleObserver(BuildContext context) {
return use(const _WidgetObserver());
}
class _WidgetObserver extends Hook<void> {
const _WidgetObserver();
#override
HookState<void, Hook<void>> createState() {
return _WidgetObserverState();
}
}
class _WidgetObserverState extends HookState<void, _WidgetObserver> with WidgetsBindingObserver {
#override
void build(BuildContext context) {}
#override
void initHook() {
super.initHook();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
print("app state now is $state");
super.didChangeAppLifecycleState(state);
}
}
Then
class Root extends HookWidget {
#override
Widget build(BuildContext context) {
useWidgetLifecycleObserver(context);

I've just had to deal with the same problem. And here is my solution using custom hooks:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
AppLifecycleState useAppLifecycleState() {
return use(const _LifeCycleState());
}
class _LifeCycleState extends Hook<AppLifecycleState> {
const _LifeCycleState();
#override
__LifeCycleState createState() => __LifeCycleState();
}
class __LifeCycleState extends HookState<AppLifecycleState, _LifeCycleState>
with WidgetsBindingObserver {
AppLifecycleState _theState;
#override
void initHook() {
super.initHook();
WidgetsBinding.instance.addObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
setState(() {
_theState = state;
});
}
#override
AppLifecycleState build(BuildContext context) {
return _theState;
}
#override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
}
And in the HookWidget that you want to access the app lifecycle state use the useEffect :
final appLifecycleState = useAppLifecycleState();
useEffect(() {
print("current app state");
print(appLifecycleState);
if (appLifecycleState == AppLifecycleState.paused ||
appLifecycleState == AppLifecycleState.inactive) {
//...
} else if (appLifecycleState == AppLifecycleState.resumed) {
//...
}
return null;
}, [appLifecycleState]);

Related

how to get user status (is online) on flutter web

I am working on a flutter web project and want to get the status of the user (online?). I use WidgetsBindingObserver, but didChangeAppLifecycleState doesn't work on the web.
I tried this code.
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>with WidgetsBindingObserver {
#override
void didChangeAppLifecycleState(AppLifecycleState state){
if(state == AppLifecycleState.resumed){
//is online
}else{
//isn't
}
}
#override
void initState() {
// TODO: implement initState
if (kIsWeb) {
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
} else {
WidgetsBinding.instance.addObserver(this);
}
super.initState();
}
#override
void dispose() {
if (kIsWeb) {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
} else {
WidgetsBinding.instance.removeObserver(this);
}
super.dispose();
}
void onFocus(Event e) {
didChangeAppLifecycleState(AppLifecycleState.resumed);
}
void onBlur(Event e) {
didChangeAppLifecycleState(AppLifecycleState.paused);
}
#override
Widget build(BuildContext context) {
return Scaffold(body: Container(child:Text('hello') ));
}
}
When you focus and blur the web page, it works. The user is still online if you close the tab or browser. To no avail, I tried setting it to outline in the dispose method.

"Multiple widgets used the same GlobalKey" error in flutter

I'm getting an error like the one in the picture. I'm confused because I'm not setting up GlobalKey on every page. I just made a GlobalKey on main.dart for this:
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
StreamController<bool> _showLockScreenStream = StreamController();
StreamSubscription _showLockScreenSubs;
GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_showLockScreenSubs = _showLockScreenStream.stream.listen((bool show){
if (mounted && show) {
_showLockScreenDialog();
}
});
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_showLockScreenSubs?.cancel();
super.dispose();
}
// Listen for when the app enter in background or foreground state.
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// user returned to our app, we push an event to the stream
_showLockScreenStream.add(true);
} else if (state == AppLifecycleState.inactive) {
// app is inactive
} else if (state == AppLifecycleState.paused) {
// user is about quit our app temporally
} else if (state == AppLifecycleState.suspending) {
// app suspended (not used in iOS)
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey,
...
);
}
void _showLockScreenDialog() {
_navigatorKey.currentState.
.pushReplacement(new MaterialPageRoute(builder: (BuildContext context) {
return PassCodeScreen();
}));
}
}
I've tried to remove the GlobalKey _navigatorKey but the error still appears.
The error appears when switching pages. Is there anyone who can help me?
There are many kinds of Keys. But the GlobalKey allows access to the state of a widget (if it's a StatefulWigdet).
Then, if you use the same GlobalKey for many of them, there is a conflict with their States.
In addition, they must be of the same type due to its specification:
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
// ...
void _register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
assert(element.widget != null);
final Element oldElement = _registry[this]!;
assert(oldElement.widget != null);
assert(element.widget.runtimeType != oldElement.widget.runtimeType);
_debugIllFatedElements.add(oldElement);
}
return true;
}());
_registry[this] = element;
}
// ...
}
This fragment of code shows that in debug mode, there is an assertion for ensuring that there isn't any other GlobalState of the same type previously registered.

ChangeNotifier mounted equivalent?

I am extract some logic from Stateful Widget to Provider with ChangeNotifier: class Model extends ChangeNotifier {...}
In my Stateful Widget I have:
if (mounted) {
setState(() {});
}
How I can check if Widget is mounted in Model?
For example how I can call:
if (mounted) {
notifyListeners();
}
A simple way is pass 'State' of your Stateful Widget as a parameter to your 'Model'.
Like this:
class Model extends ChangeNotifier {
Model(this.yourState);
YourState yourState;
bool get _isMounted => yourState.mounted;
}
class YourState extends State<YourStatefulWidget> {
Model model;
#override
void initState() {
super.initState();
model = Model(this);
}
#override
Widget build(BuildContext context) {
// your code..
}
}
I think you don't need to check the State is mounted or not. You just need to check the Model has been already disposed. You can override dispose() method in ChangeNotifier:
class Model extends ChangeNotifier {
bool _isDisposed = false;
void run() async {
await Future.delayed(Duration(seconds: 10));
if (!_isDisposed) {
notifyListeners();
}
}
#override
void dispose() {
super.dispose();
_isDisposed = true;
}
}
And don't forget dispose Model when the State is disposed:
class YourState extends State {
Model model;
#override
void initState() {
super.initState();
model = Model();
}
#override
void dispose() {
model?.dispose();
super.dispose();
}
/// Your build code...
}
Or you can use ChangeNotifierProvider in package Provider, it will help you to dispose Model automatically.
class YourState extends State {
Model model;
#override
void initState() {
super.initState();
model = Model();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Model>(
builder: (build) => model,
child: Container(
child: Consumer<Model>(
builder: (context, model, widget) => Text("$model"),
),
),
);
}
}
as long as you wrap your widget with the provider model state
and as it is known once your widget is disposed
the provider model that is wrapping it already get disposed by default
so all you have to do is to define a variable isDisposed and modify the notifyListeners
as below
MyState with ChangeNotifier{
// to indicate whether the state provider is disposed or not
bool _isDisposed = false;
// use the notifyListeners as below
customNotifyListeners(){
if(!_isDisposed){
notifyListeners()
}
}
#override
void dispose() {
super.dispose();
_isDisposed = true;
}
}
Just use a custom ChangeNotifier class.
import 'package:flutter/cupertino.dart';
class CustomChangeNotifier extends ChangeNotifier {
bool isDisposed = false;
#override
void notifyListeners() {
if (!isDisposed) {
super.notifyListeners();
}
}
#override
void dispose() {
isDisposed = true;
super.dispose();
}
}
you can just override notifyListeners like this
class Model extends ChangeNotifier {
#override
void notifyListeners() {
WidgetsBinding.instance.addPostFrameCallback((t) {
print("skip notify after ${t.inMilliseconds}ms");
super.notifyListeners();
});
}
}
no need additional variables / constructor modification

inheritFromWidgetOfExactType(InheritedProvider<ConnectivityStatus>) or inheritFromElement() was called before

In this simple class i want to make base state class to manage some actions such as accessing to internet connection:
abstract class BaseState<T extends StatefulWidget> extends State {
bool isOnline;
ConnectivityStatus _connectivityStatus;
#override
void initState() {
super.initState();
_connectivityStatus = Provider.of<ConnectivityStatus>(context);
isOnline = _connectivityStatus == ConnectivityStatus.Connected;
if (!isOnline) {
showSimpleNotification(Text("disconnected"), background: Colors.green);
} else {
showSimpleNotification(Text("connected"), background: Colors.red);
}
}
}
when i try to use this class like with:
class _FragmentLoginState extends BaseState<FragmentLogin> with SingleTickerProviderStateMixin {
PageController _pageController;
Color left = Colors.black;
Color right = Colors.white;
#override
void initState() {
super.initState();
_pageController = PageController(initialPage: 1);
}
#override
Widget build(BuildContext context) {
}
}
the problem is you don't have a valid Context yet.
you can try theses two solution
defer using context:
#override
void initState() {
super.initState();
_initConnectivity();
}
Future _initConnectivity() async {
await Future.delayad(Duration.zero);
_connectivityStatus = Provider.of<ConnectivityStatus>(context);
isOnline = _connectivityStatus == ConnectivityStatus.Connected;
if (!isOnline) {
showSimpleNotification(Text("disconnected"), background: Colors.green);
} else {
showSimpleNotification(Text("connected"), background: Colors.red);
}
}
move your logic in the build function ( in BaseState )
_initConnectivity() {
_connectivityStatus = Provider.of<ConnectivityStatus>(context);
isOnline = _connectivityStatus == ConnectivityStatus.Connected;
if (!isOnline) {
showSimpleNotification(Text("disconnected"), background: Colors.green);
} else {
showSimpleNotification(Text("connected"), background: Colors.red);
}
}
#override
Widget build(BuildContext context) {
_initConnectivity();
}
in the second method you can also create a flag like isFirstBuild to make sure it runs only one time.
also these may help :
initialize data once in initState and call the setState when data is ready causes exception
Flutter get context in initState method
Use a frame callback which delays execution until the next frame (i.e. after initState has complete)
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_connectivityStatus = Provider.of<ConnectivityStatus>(context);
isOnline = _connectivityStatus == ConnectivityStatus.Connected;
if (!isOnline) {
showSimpleNotification(Text("disconnected"), background: Colors.green);
} else {
showSimpleNotification(Text("connected"), background: Colors.red);
}
});
}

Where I can get information to make the WidgetsBindingObserver works again?

After upgrading to Flutter 0.7.3 channel beta (Dart 2.1.0-dev.1.0.flutter-ccb16f7282) the WidgetsBindingObserver is not working.
It worked before and after login if AppLifecycleState paused, inactive or suspended it returns to main page. But seems it doesnt work with new update. My question is where I can get information to see how to make the WidgetsBindingObserver works again.
// Statefull HomePage
class PersonalLoginPage extends StatefulWidget {
const PersonalLoginPage({ Key key }) : super(key: key);
#override
_PersonalLoginPageState createState() => new _PersonalLoginPageState();
}
class _PersonalLoginPageState extends State<PersonalLoginPage> with WidgetsBindingObserver {
AppLifecycleState _appLifecycleState;
bool _appStatePause;
// TODO: initState function
#override
void initState() {
print("initState Starting Now .......................");
super.initState();
authenticateUser();
WidgetsBinding.instance.addObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
Timer _timer;
setState(() {
_appLifecycleState = state;
if (_appLifecycleState == AppLifecycleState.paused ||
_appLifecycleState == AppLifecycleState.inactive ||
_appLifecycleState == AppLifecycleState.suspending) {
_appStatePause = true;
print("New Timer Starting Now .......................");
_timer = Timer.periodic(Duration(seconds: 60), _callback);
} else {
_appStatePause = false;
}
});
}
void _callback(_timer) {
if (_appStatePause == true) {
print("Timer Finished without cancel...................");
setState(() {
Navigator.push(
context,
SlideRightRoute(widget: MyApp()),
);
});
} else {
_timer.cancel();
print("Timer cancel now................................");
}
}
// TODO: authenticateUser function
Future authenticateUser() async {
……
……
……
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// TODO: main build Widget
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Center(
child: new Text(“Hello World”)
)
);
}