I know there is already a similar question, but i couldn't really solve mine from its answers. So I have a FutureBuilder which won't update when the data is loaded and I cant figure out why. The way I understand it, after the 2 second delay the Widget should be rebuild ("Data Arrived" should be printed). Why is this not happening?
import 'package:flutter/material.dart';
class NewQRPage extends StatefulWidget {
#override
_NewQRPageState createState() => _NewQRPageState();
}
class _NewQRPageState extends State<NewQRPage> {
Future link;
#override
void initState() {
link = getLink();
super.initState();
}
getLink() async{
Future.delayed(Duration(milliseconds: 2000),(){
print("Data Returned");
return "hello";
});
}
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder(
future: link,
builder: (context, snapshot) {
if(snapshot.hasData){
print("Data Arrived");
return Text(snapshot.data);
}
else if(snapshot.hasError){
print("Error");
return Text("Error");
}
else {
print("No Data");
return Text("No Data");
}
},
),
);
}
}
And the Console Output is:
Performing hot restart...
Syncing files to device LYA L29...
Restarted application in 1.044ms.
I/flutter (22206): onStartCalled
I/flutter (22206): No Data
I/flutter (22206): No Data
I/flutter (22206): No Data
I/flutter (22206): Data Returned
You shouldn't return "Hello" in your lamda i.e (){}
Try
Future<String> getLink() async{
await Future.delayed(Duration(milliseconds: 2000));
print("Data Returned");
return "hello";
}
Related
What I am trying to do is that by starting the app, make a request to a server and all information is saved in the database, for that I use a FutureBuilder that does the whole process, once finished it starts the application as normal.
The problem is that the application executes my future synchronization more than twice, causing errors with the insert to database.
the following code is a basic example of what i am trying to do and the result i am getting.
main.dart
void main() => runApp(MyMaterialApp());
class MyMaterialApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Running MyMaterialApp');
return MaterialApp(
home: SplashLoad(),
);
}
}
class SplashLoad extends StatefulWidget {
#override
_SplashLoadState createState() => _SplashLoadState();
}
class _SplashLoadState extends State<SplashLoad> {
final apiSimulation = new ApiSimulation();
#override
Widget build(BuildContext context) {
print('SplashScreen');
return Scaffold(
body: Container(
child: Center(
child: FutureBuilder(
future: apiSimulation.sincronizacion(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
return Text('load finished data:${snapshot.data}');
}
return CircularProgressIndicator();
},
),
),
),
);
}
}
ApiSimulation
class ApiSimulation {
int number = 0;
Future<int> synchronization()async{
print('INIT SYNCHRONIZATION');
await Future.delayed(Duration(seconds: 2));
final data = await _getData();
return data;
}
Future<int> _getData() async{
number++;
print('running getData$number');
await Future.delayed(Duration(seconds: 2));
return number;
}
}
console result
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): Running MyMaterialApp
I/flutter (32404): SplashScreen
I/flutter (32404): INIT SYNCHRONIZATION
I/flutter (32404): running getData1
I/flutter (32404): running getData2
Sometimes reaching 4 in the value of the number
This is what the document says
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateWidget, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
class _SplashLoadState extends State<SplashLoad> {
late final Future simFuture;
#override
void initState() {
super.initState();
simFuture= ApiSimulation().sincronizacion(); //initiate your future here
}
Within your future builder use
FutureBuilder(
future: simFuture,
builder: (ctx,snap){..},
)
i need to call and get the result from an async function before build the widget in statefulWidget in flutter, i tried to do like this, but it didn't work:
#override
void initState() {
super.initState();
loadDocument(details).whenComplete((){
setState(() {});
});
vid = YoutubePlayer.convertUrlToId(details);
}
#override
Widget build(BuildContext context) {
print("from details in build widget");
return Scaffold(
appBar: AppBar(
title: Text(name.toString()),
backgroundColor: Colors.redAccent,
),
body : Center(child: PDFViewer(document: controller.document)))
in this example, first thing call the function (loadDocument), after that call the (build) methode for widget, and then show the result after (whenComplete) has been finished, but what i need is to only call the build function after (whenComplete) finish....
this code for loadDocument
PDFDocument document = PDFDocument();
loadDocument(String url) async {
try {
return document = await PDFDocument.fromURL(
url);
} on Exception catch (e) {
print(e);
}
}
You can use FutureBuilder to build your ui based on the different Future states:
FutureBuilder<*Your future return type*>(
future: *Your future*,
builder: (BuildContext context, AsyncSnapshot<*Your future return type*> snapshot) {
if (snapshot.hasData) {
// Handle future resolved case
} else if (snapshot.hasError) {
// Handle future error case
} else {
// Handle future loading case
}
},
)
You can read more about FutureBuilder here: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
I kept running into the issue where you call setState when the widget is not mounted (especially after a data fetch). My question is:
Can I extend the State class and override setState like so
abstract class MountedState<T extends StatefulWidget> extends State<T> {
#override
void setState(fn) {
if(mounted) super.setState(fn);
}
}
I did this and it worked. I just want to know if it is not ideal or I am not supposed to
Future<void> fetchSubjectsAndClasses() async {
try {
Response res = await TeachersAPI.classesAndSubjects();
classes = res.data;
setState(() {});
} catch (e) {
print(e);
}
}
This is my data fetch that causes the issue. It gets called on initState
This is not ideal. You should be using a FutureBuilder when dealing with async functions that deal with the UI. It's not mandatory, but it takes care of the more annoying parts of updating the UI with Future data.
You should obtain your future in initState and store it in the State of the widget. Then pass that to your FutureBuilder in build:
Future myFuture;
#override
void initState() {
super.initState();
myFuture = futureCall();
}
#override
Widget build() {
return FutureBuilder(
future: myFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
//Show widget that has data
}
else if (snapshot.hasError) {
//Show widget that has error
}
else {
//Show widget while loading
}
}
);
}
I need to store some data inside a ".txt" file into a variable.
To do this I'm using a FutureBuilder.
The idea is to handle the reading action (async) with FutureBuilder and pass the FutureBuilder return to my global variable that will be updated when needed.
Future<String> _read() async {
try {
final file = await _localBio;
String body = await file.readAsString();
// Read the file.
return body;
} catch (e) {
// If encountering an error, return 0.
return "Can't read";
}
}
var bio ="";
String _ReadBio(){
FutureBuilder<String>(
future: _read(),
builder: (context, snapshot){
if (snapshot.hasData) {
bio=snapshot.data.toString(); //this isn't done
return Text("");
}
else {
return Text("Error occured");
}
}
);
return "";
}
Then in the TextField I want to show what is stored inside bio that should be the content of "bio.txt".
child: Text(
_ReadBio()== "" //always true if no error occured
? bio
: "Tell us something about you...",
),
But what is showed is the first bio value, I don't know why.
I strongly suggest you to read about FutureBuilder.
You are missing the point od FutureBuilder because its is a widget to place in a widget tree what you have done is placed on a function that returns a empty string.
Instead you can do it like this,
Future<Widget> _readAndBuildBioWidget(){
return FutureBuilder<String>(
future: _read(),
builder: (context, snapshot){
if (snapshot.hasData) {
bio=snapshot.data.toString(); //this isn't done
return Text(bio ?? "");
}
else {
return Text("Error occured");
}
}
);
return "";
}
#override
void build() {
return Scaffold(
child: _readAndBuildBioWidget(),
);
}
Or you can do as below,
class _HomeScreenState extends State<HomeScreen> {
var bio = "";
Future<String> _read() async {
try {
final file = await _localBio;
String body = await file.readAsString();
return body;
} catch (e) {
return "Can't read";
}
}
#override
void initState() {
_read().then((onValue) => bio = onValue);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(bio ?? "")),
);
}
}
Note above code snippet I haven't tested. I just wanted to give a rough idea.
I am going to combine two stream. But it does not work. What is my mistake ?
My build function is ;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
),
builder: (context, snapshot) {
print("hasData : ${snapshot.hasData}");
if (snapshot.hasData) accountsCombinerResult = snapshot.data;
if (snapshot.hasError) return Text(snapshot.error.toString());
return _buildWidget;
},
);
}
Get All DB Accounts Stream Bloc is
class GetAllDBAccountsBloc {
final _getAllDBAccountsFetcher = PublishSubject<List<AccountDatabaseModel>>();
Observable<List<AccountDatabaseModel>> get getAllDBAccountsStream => _getAllDBAccountsFetcher.stream;
getAllDBAccounts() async {
print("accounts getting");
_getAllDBAccountsFetcher.sink.add(null);
await new Future.delayed(const Duration(seconds: 1));
_getAllDBAccountsFetcher.sink.add(await Repository.getAllDBAccounts());
print("accounts get");
}
dispose() {
_getAllDBAccountsFetcher.close();
}
}
final getAllDBAccountsBloc = GetAllDBAccountsBloc();
Delete DB Account Bloc is
class DeleteDBAccountBloc {
final _deleteDBAccountFetcher = PublishSubject<DeleteAccountResultBlocModel>();
Observable<DeleteAccountResultBlocModel> get deleteDBAccountStream => _deleteDBAccountFetcher.stream;
deleteDBAccount(DeleteAccountRequestBlocModel requestModel) async {
_deleteDBAccountFetcher.sink.add(DeleteAccountResultBlocModel());
await new Future.delayed(const Duration(seconds: 1));
_deleteDBAccountFetcher.sink.add(await Repository.deleteDBAccount(requestModel));
}
dispose() {
_deleteDBAccountFetcher.close();
}
}
final deleteDBAccountBloc = DeleteDBAccountBloc();
Combiner result class is
class AccountsCombinerResult {
final DeleteAccountResultBlocModel deleteAccountResultBlocModel;
final List<AccountDatabaseModel> accountsList;
AccountsCombinerResult({
#required this.accountsList,
#required this.deleteAccountResultBlocModel,
});
}
its mine Run log on android studio..
I/flutter (28323): accounts getting
I/flutter (28323): hasData : false
I/flutter (28323): hasData : false
I/flutter (28323): accounts get
The stream work but i did not get AccountsCombiner Result data.
This build method work but i don't want use it...
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: getAllDBAccountsBloc.getAllDBAccountsStream,
builder: (context, getDbAccountsSnapshot) {
return StreamBuilder(
stream: deleteDBAccountBloc.deleteDBAccountStream,
builder: (context, deleteDbAccountStreamSnapshot) {
if (deleteDbAccountStreamSnapshot.hasData && getDbAccountsSnapshot.hasData) {
print("qweqweq");
accountsCombinerResult = AccountsCombinerResult(
accountsList: getDbAccountsSnapshot.data,
deleteAccountResultBlocModel: deleteDbAccountStreamSnapshot.data,
);
}
if (getDbAccountsSnapshot.hasError) return Text(getDbAccountsSnapshot.error.toString());
if (deleteDbAccountStreamSnapshot.hasError) return Text(deleteDbAccountStreamSnapshot.error.toString());
return _buildWidget;
},
);
},
);
}
You are building a new stream every time the build method is called. You need to keep the stream reference in the state.
StreamController<AccountsCombinerResult> _streamController = StreamController<AccountsCombinerResult>();
#override
void initState() {
super.initState();
_streamController.addStream(Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
));
}
#override
void dispose() {
super.dispose();
_streamController.close();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _streamController.stream,
builder: (context, snapshot) {
print("hasData : ${snapshot.hasData}");
if (snapshot.hasData) accountsCombinerResult = snapshot.data;
if (snapshot.hasError) return Text(snapshot.error.toString());
return _buildWidget;
},
);
}
To make this easier you could use the StreamProvider from the provider package.
https://pub.dev/packages/provider
https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html
It only build the stream once.
#override
Widget build(BuildContext context) {
return StreamProvider<AccountsCombinerResult>(
initialData: null, // not sure if this works, you can try []
create: () => Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
),
catchError: (context, error) => AccountsCombinerResult(
deleteAccountResultBlocModel: null,
accountsList: null,
error: error,
),
child: Builder(
builder: (context) {
final data = Provider.of<AccountsCombinerResult>(context);
// maybe null check
if (data.error != null) return Text(data.error.toString());
accountsCombinerResult =data;
return _buildWidget;
},
),
);
}
class AccountsCombinerResult {
final DeleteAccountResultBlocModel deleteAccountResultBlocModel;
final List<AccountDatabaseModel> accountsList;
final dynamic error;
AccountsCombinerResult({
#required this.accountsList,
#required this.deleteAccountResultBlocModel,
this.error,
});
}
The code is not tested so there may be typos or stuff that I missed, but you should get the general idea.