How to ensure code is run only once in a widget? - flutter

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;
}
}
}

Related

LateInitializationError with Future

I hope you could help me!
Error saying 'tables' has not been initiliazed. But when I set tables = [] instead of
widget.data.then((result) {tables = result.tables;})
it works. I think the problem comes from my app state data which is a Future.
My simplified code:
class NavBar extends StatefulWidget {
final Future<Metadata> data;
const NavBar({Key? key, required this.data}) : super(key: key);
#override
State<NavBar> createState() => _NavBarState();
}
class _NavBarState extends State<NavBar> {
late List<MyTable> tables;
#override
void initState() {
widget.data.then((result) {
tables = result.tables;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: buildPages(page.p)
)
);
}
Widget buildPages(index){
switch (index) {
case 0:
return ShowTablesNew(tables: tables);
case 1:
return const Details();
case 2:
return const ShowTables();
default:
return const ShowTables();
}
}
}
Future doesn't contain any data. It's an asynchronous computation that will provide data "later". The initialization error happens because the variable 'tables' is marked as late init but is accessed before the future is completed, when in fact it's not initialized yet.
Check this codelab for async programming with dart.
For your code you can use async/await in the initState method doing something like this
String user = '';
#override
void initState() {
asyncInitState();
super.initState();
}
void asyncInitState() async {
final result = await fetchUser();
setState(() {
user = result;
});
}
but since you're using a list of custom objects the most straightforward way is probably to use a FutureBuilder widget

Getting "Missing concrete implementation of State.build" Error in Flutter

I am receiving the error "Missing concrete implementation of State.build" when attempting to run this code for Angela Yu's FLutter course:
import 'package:flutter/material.dart';
import 'package:clima/services/location.dart';
import 'package:http/http.dart';
class LoadingScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _LoadingScreenState();
}
}
class _LoadingScreenState extends State<LoadingScreen> {
#override
void initState() {
super.initState();
getLocation();
}
}
void getLocation() async {
Location location = Location();
await location.getCurrentLocation();
print(location.latitude);
print(location.longitude);
}
void getData() async {
Response response = await get(
'https://samples.openweathermap.org/data/2.5/weather?lat=35&lon=139&appid=439d4b804bc8187953eb36d2a8c26a02');
print(response);
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
I have tried the responses related to this question:
missing concrete implementation of state.build
...but have not had any success. Any insight as to what I'm doing wrong would be very much appreciated.
enter code hereYou have a extra } after
#override
void initState() {
super.initState();
getLocation();
}
Delete it
And another missed above
#override
Widget build(BuildContext context) {
return Scaffold();
}
Check your { }
Check your {}. This error mainly comes due to the missing of {} or using extra {}.

How acess provider outside a Widget?

I have an application that I making some tests with RxDart using observables and subjects. So I make this code:
class CompanyList extends StatefulWidget {
const CompanyList({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => new CompanyListState();
}
class CompanyListState extends State<CompanyList> {
CompanyController companyController = new CompanyController();
List<dynamic> companyList = [];
#override
void initState() {
super.initState();
getActiveCompanys();
companyController.loadMoreData$.listen((value) {
print(value);
});
}
#override
void dispose() {
super.dispose();
}
getActiveCompanys() async {
companyController.getActiveCompanys().then((value) {
for (var i = 0; i < 10; i++) {
setState(() {
companyList.add(value[i]);
});
}
}).catchError((_) {
print('ENTROU NO ERRO');
});
}
getCompanyData(company) {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.leftToRight,
child: CompanyScreen(
company: company,
),
),
);
}
#override
Widget build(BuildContext context) {
final double screenHeight = MediaQuery.of(context).size.height;
final double screenWidth = MediaQuery.of(context).size.width;
... the code goes on!
I want to get a provider data like this CompanyController companyController = new Provider.of<CompanyController>(context, listen: false); Where I put CompanyController companyController = new CompanyController();
But I don't have a context and I need this instance to make the RxDart subject and observables to work together.
You need a reference to the context in order to use the Provider, the State object has a reference as a field of its class, but this reference shouldn't be used before initState() is called, so just use the reference you need inside initState() and create everything you need there:
initState() {
super.initState();
companyController = new Provider.of<CompanyController>(context, listen: false);
// use companyController here.
...
}
Also, remember to dispose all objects that need to be disposed at dispose().

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

initialize data once in initState and call the setState when data is ready causes exception

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();
}
}
}