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
Related
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]);
I do have a lot of code that looks like
this:
bool _somethingFromApiLoaded = false;
Something _somethingFromApi;
loadSomething() async {
final something = await ServiceProvider.of(context).apiService.getSomething();
setState(() => _somethingFromApi = something);
}
#override
Widget build(BuildContext context) {
if (!_somethingFromApiLoaded) {
loadSomething();
_somethingFromApiLoaded = true;
}
}
Note how I produce a lot of boilerplate code to ensure loadSomething is only called once.
I wonder if there isn't a lifecycle method to do so that I somehow misinterpret. I can't use initState because it does not have context.
I would try to a use a StatefulWidget and use initState() method.
That is the lifecycle you are referring to.
You should try to use a Future inside the initState()
#override
void initState() {
super.initState(); // make sure this is called in the beggining
// your code here runs only once
Future.delayed(Duration.zero,() {
_somethingFromApi = await ServiceProvider.of(context).apiService.getSomething();
});
}
As User gegobyte said, Context is available in the initState.
But apparently can't be used for everything.
You can use context in initState() by passing it to the widget:
class HomeScreen extends StatefulWidget {
final BuildContext context;
HomeScreen(this.context);
#override
State<StatefulWidget> createState() => new _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _somethingFromApiLoaded = false;
Something _somethingFromApi;
loadSomething() async {
final something = await ServiceProvider.of(widget.context).apiService.getSomething();
setState(() => _somethingFromApi = something);
}
#override
void initState() {
super.initState();
if (!_somethingFromApiLoaded) {
loadSomething();
_somethingFromApiLoaded = true;
}
}
}
Sorry if this is a novice question. I have the following repo file:
class TwitterRepo {
final TwitterRepoCallback _callback;
TwitterRepo(this._callback){
// do stuff
}
}
abstract class TwitterRepoCallback{
void onEvent();
}
In my UI file I have the following:
class TweetList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _TweetListState();
}
}
class _TweetListState extends State<TweetList> implements TwitterRepoCallback {
final TwitterRepo _twitterRepo = TwitterRepo(this);
// other stuff like initState, build and onEvent
}
There is an error on
final TwitterRepo _twitterRepo = TwitterRepo(this);
where I use "this", Invalid reference to 'this' expression.
I'm at a loss on how to pass in my callback to receive events.
Try this.
class ParentPageState extends State<ParentPage> implement Callback{
...
#override
void callback(){
...
}
#override
void callback1(String str){
....
}
#override
Widget build(BuildContext context){
return Scaffold(
body : Container(
child : ChildPage(callback : this.callback, callback1 : this.callback1)
)
);
}
}
And ChildPage
import .....
//Main Point
typedef Callback = void Function();
typedef Callback1 = void Function(String str);
class ChildPage extends StatelessWidget{
final Callback _callback;
final Callback1 _callback1;
ChildPage({Callback callback, Callback1 callback1}): _callback : callback, _callback1 : callback1;
.....
#override
Widget build(BuildContext context){
return Container(
child : InkWell(
onPressed : (){
this._callback();
this._callback1("test");
},
child : ....
)
);
}
This is may have issue. The main point is "typedef"
I probably wouldn't use callbacks for this type of need. Instead I'd use some kind of InheritedWidget like system to grab data and propagate it down the widget tree. I know you just started, but a great tool is the Provider package. To do what you're trying to do here it'd look something like this:
class TwitterRepo extends ChangeNotifier{
//construct Repo
TwitterRepo(){
_setupNetworkListener();
}
List<Data> data = [];
//set up the way to listen to and get data here then add it to your list,
//finally notify your listeners of the data changes
_setupNetworkListener()async{
var _data = await gettingInfo();
data.addAll(_data);
notifyListeners();
}
}
class TwitterRepoUI extends StatefulWidget {
#override
_TwitterRepoUIState createState() => _TwitterRepoUIState();
}
class _TwitterRepoUIState extends State<TwitterRepoUI> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<TwitterRepo>(
builder: (context)=> TwitterRepo(),
child: Consumer<TwitterRepo>(
builder: (context, model, child){
return ListView.builder(
itemCount: model.data.length,
itemBuilder: (context, index){
return Center(
child: Text(index.toString()),
);
});
},
),
);
}
}
If you want to use the callback to notify the UI to render some new data, you may want to use Future or Stream. Anyway, the question is how to implement a callback/listener so here I give you some examples:
You can't declare a variable using this, you could initialize the variable on the constructor
_TweetListState() {
_twitterRepo = TwitterRepo(this);
}
or inside initState()
#override
void initState() {
super.initState();
_twitterRepo = TwitterRepo(this);
}
A better way to do this would be:
final TwitterRepo _twitterRepo = TwitterRepo();
#override
void initState() {
super.initState();
_twitterRepo.listen(this);
}
And the listen method implemented on TwitterRepo would set the callback
You could also use VoidCallback instead of TwitterRepoCallback:
#override
void initState() {
super.initState();
_twitterRepo.listen(() {
// Do stuff
});
}
Or a callback function using Function(String a, int b) instead of TwitterRepoCallback
#override
void initState() {
super.initState();
_twitterRepo.listen((a, b) {
// Do stuff
});
}
I have widget with data that changes regularly and I'm using a Timer.periodic to rebuild the widget. This starts out working smoothly but becomes choppy pretty quickly is there a better way to do this?
class _MainScreenState extends State<MainScreen> {
static const Duration duration = Duration(milliseconds: 16);
update(){
system.updatePos(duration.inMilliseconds/1000);
setState(() {});
}
#override
Widget build(BuildContext context) {
Timer.periodic(duration, (timer){
update();
});
return PositionField(
layoutSize: widget.square,
children: system.map
);
}
}
You are making a big mistake:
The build method must never have any side effects, because it is called again whenever setState is called (or when some higher up widget changes, or when the user rotates the screen...).
Instead, you want to create your Timer in initState, and cancel it on dispose:
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> {
Timer _timer;
int _foo = 0;
// this is only called once when the widget is attached
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) => _update());
}
// stop the timer when the widget is detached and destroyed
#override
void dispose() {
_timer.cancel();
super.dispose();
}
void _update() {
setState(() {
_foo++;
});
}
#override
Widget build(BuildContext context) {
return Text('Foo: ${_foo}');
}
}
Since flutter calls the build method many times in different condition, to avoid getting the data many times, I initialize the data in initState.
I want to re-build the widget when the data is ready.
Here is my code :
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
Data data;
bool dataReady = false;
#override
void initState() {
super.initState();
getData(context).then((Data data) async {
setState(() {
dataReady= true;
});
});
}
#override
Widget build(BuildContext context) {
if (dataReady) {
return createMainContent(context);
} else {
return new Container();
}
}
}
However, it results in following exception :
inheritFromWidgetOfExactType(_InheritedProvider) or inheritFromElement() was called before _TestState.initState() completed.
May I know am I doing something wrong here?
When I add the following line to implementation of getData(context)
await Future.delayed(new Duration(milliseconds: 300));
the exception does not happen.
For everyone coming here at a later point
It is best to use the #override void didChangeDependencies () method of the State class.
From the docs
This method is also called immediately after initState. It is safe to call BuildContext.inheritFromWidgetOfExactType from this method.
But make sure to check if you have already performed your initialization
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (bloc == null) { // or else you end up creating multiple instances in this case.
bloc = BlocProvider<MyBloc>.of(context);
}
}
Edit: Better answer below.
Apparently, you cannot access getData(context) during initState (more concrete: before it completed).
The reason, so I believe, is that getData tries to look up an InheritedWidget ancestor up in the tree, but the tree is just now being built (your widget is created during the parent widget's build).
The obvious solution would be to delay getData's lookup to a later point in time. There are several ways to achieve that:
Delay the lookup to a later time. scheduleMicrotask should work fine.
Look it up during the first build call. You could have an isInitialized field set to false and in you build, something like:
if (!isInitialized) {
isInitialized = true;
// TODO: do the getData(...) stuff
}
an alternative is to put it inside PostFrameCallback which is between initState and Build.
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) => getData());
super.initState();
}
getData() async {
}
I moved my code to my build method from initState and it worked
class _TestState extends State<Test> {
Data data;
bool dataReady = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
getData(context).then((Data data) async {
setState(() {
dataReady= true;
});
});
if (dataReady) {
return createMainContent(context);
} else {
return new Container();
}
}
}