SetState call blocks access to field updated by stream - dart

I am making a barcode scanning functionality. The camera feed is setting _currentImage.
// CameraView.dart
CameraImage _currentImage;
CameraImage takePhoto() => _currentImage;
void imageStream(CameraImage image) async {
print("_currentImage is set with " + image.toString()); // keeps saying it sets 'currentImage'
this._currentImage = image;
}
Later on I call takePhoto from button pressed handler and call 'setState' empty...
void pressedShowBarcodeOnScreenButton() async {
var image = this.cameraView.takePhoto();
if (image == null) {
print(// occurs after having called empty 'setState' every time button is pressed
"image was null, maybe it is not possible to take images this fast!");
return;
}
var barcode =
await VisionService.getInstance() // is not producing any errors
.analyzeBarcode(ImageUtils.toAbstractImage(image));
if (Utils.isNullOrEmpty(barcode)) {
return;
}
print("got barcode: " + barcode); // prints all barcodes ok, but stops doing so when calling 'setState'
setState(() {
// Makes it so that 'currentImage' is null at all times, despite continuously set by imageStream given from log
//this.scannedBarcodes = [...this.scannedBarcodes, barcode]; // my intention eventual to update a list
});
}
I suspect it might be due to some underlaying dart behavour, or there is some sort of bug? Why does the setState call prevent me from accessing the _currentImage - or why is blocked from being set by the imageStream()?

The CameraView which contained the CameraController that had the stream, was part of the page to display the barcodes.
So SetState(){} triggered a reconstruction of the CameraView, and the controller was not able to be reconstructed. Probably the old stream was producing the log and was misleading.
It helped to extract the CameraController in this scenario, and have it accessed as singleton, inside CameraViewController that I made:
//...
static CameraController _cameraController;
static Future<CameraController> getCameraControllerInstance() async {
if (_cameraController == null) {
var cameras = await availableCameras();
if (cameras?.length == 0) {
print("the device did not have a camera");
return null;
}
_cameraController = CameraController(cameras[0], ResolutionPreset.max);
await _cameraController.initialize();
_cameraController.startImageStream(updateCurrentImage);
}
return _cameraController;
}
And I use it in a FutureBuilder like so, in the CameraView:
FutureBuilder futureBuilder() => FutureBuilder<CameraController>(
future: CameraViewController.getCameraControllerInstance(), // <--!!!
builder:
(BuildContext context, AsyncSnapshot<CameraController> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return LoadingPage();
}
return content(snapshot.data);
});
I am tempted to have one instance of the CameraView around at all times (?), since it looks glitchy when updating the list - but as I see it would be anti pattern as the Widget's are supposed to rerender (immutable) themselves:
The content of the page that has the barcode list:
Widget content() {
return Container(
child: (Column(children: [
cameraView, // camera view part of page and recontructed on 'scannedProducts' state change
header(),
scanButton(),
Column(
children: this.scannedProducts(), /// list
)
])));
}

Related

Riverpod with hooks beaks when widget is disposedI

I have Flutter mobile app that is using Riverpod with hooks.
I have the following function that I would like to be called when the widget is disposed:
useEffect(
() {
final firestoreRepo =
ref.read(firebaseFirestoreRepositoryProvider);
return () async {
try {
// I get exception at this line.
// I need this future to be called when the
// widget is disposed.
// Calling this future earlier is not userful
// for my business logic.
final relationship =
await ref.read(relationshipWithProvider(pid).future);
if (relationship?.unseen ?? false) {
await firestoreRepo?.updateRelatinoship(pid: pid);
}
} catch (e, st) {
// print error
}
};
},
[],
);
I keep getting this error at the line shown in the comment above.
I/flutter ( 5967): Looking up a deactivated widget's ancestor is unsafe.
I/flutter ( 5967): At this point the state of the widget's element tree is no longer stable.
How can I sold this problem
We can initially get our relationship and then await and use it:
useEffect(
() {
final firestoreRepo = ref.read(firebaseFirestoreRepositoryProvider);
final relationship = ref.read(relationshipWithProvider(pid).future);
return () async {
try {
if (await relationship?.unseen ?? false) {
await firestoreRepo?.updateRelatinoship(pid: pid);
}
} catch (e, st) {
// print error
}
};
},
[],
);
As far as I can tell, this won't contradict the logic of the business process, because one way or another, we'll have to make the relationshipWithProvider(pid) request early (when we initialize the widget) or late (when we delete the widget).

Flutter : Capture the Future response from a http call as a normal List

I am trying to make my previously static app, dynamic by making calls to the backend server.
This is what my service looks like
String _url = "http://localhost:19013/template/listAll";
Future<List<TemplateInfoModel>> fetchTemplates() async {
final response =
await http.get(_url);
print(response.statusCode);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
Iterable l = json.decode(response.body);
List<TemplateInfoModel> templates = l.map((i) => TemplateInfoModel.fromJson(i)).toList();
return templates;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
And my model looks like this :
class TemplateInfoModel {
final String templateId;
final String messageTag;
final String message;
final String messageType;
TemplateInfoModel({this.templateId, this.messageTag, this.message,
this.messageType});
factory TemplateInfoModel.fromJson(Map<String, dynamic> json) {
return TemplateInfoModel ( templateId: json['templateId'], messageTag : json['messsageTag'], message : json ['message'] , messageType : json['messageType']);
}
}
I have a utils method within which I am capturing the http request/response data; which would then use that to create a DropDown Widget ( or display it within a text)
My earlier dummy data was a list; I am wondering how best I can convert this Future> to List
class SMSTemplatingEngine {
var _SMSTemplate; //TODO this becomes a Future<List<TemplateInfoModels>>
// TemplateInfoService _templateInfoService;
SMSTemplatingEngine(){
_SMSTemplate=fetchTemplates();
}
// var _SMSTemplate = {
// 'Reminder':
// 'Reminder : We’re excited to launch a new feature on our platform that will revolutionize your Facebook marketing and triple your ROI. Visit url.com to learn more',
// 'New Message':
// 'New Message: We’re excited to launch a new feature on our platform that will revolutionize your Facebook marketing and triple your ROI. Visit url.com to learn more',
// 'Connecting Again':
// 'Connecting Again : We’re excited to launch a new feature on our platform that will revolutionize your Facebook marketing and triple your ROI. Visit url.com to learn more',
// };
List<String> getKeys(){
List<String> smsKeys = new List();
for ( var key in _SMSTemplate.keys)
smsKeys.add(key);
return smsKeys;
}
String getValuePerKey(String key){
return _SMSTemplate['${key}'];
}
}
P.S. I have looked at some posts but I was completely bowled over since I am a Flutter Newbie.
Is there an easier way for this to happen.
Thanks,
The widget which would display the content from the http call
var templateMessagesDropDown = new DropdownButton<String>(
onChanged: (String newValue) {
setState(() {
templateMsgKey = newValue;
print("Selcted : ${templateMsgKey.toString()} ");
});
},
// value: _defaultTemplateValue,
style: textStyle,
//elevation: 1,
hint: Text("Please choose a template"),
isExpanded: true,
//
items: smsEngine.getKeys().map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
I am wondering how best I can convert this Future to List
Future<List<TemplateInfoModel>> fetchTemplates() should return a List that you're expecting. You may want to consider either using FutureBuilder or StreamBuilder to update the UI elements on your screen. Or if you're not keen on using either of those, you can just call the Future and update the List on your current screen.
List<TemplateInfoModel> _listTemplateInfoModel = [];
...
fetchTemplates().then((value){
setState((){
_listTemplateInfoModel = value;
});
});

How do I show a Progress Indicator when loading data in StreamBuilder?

Consider I have this
StreamBuilder(
stream: myBloc.productList,
builder: (context, AsyncSnapshot<List<Product>> snapshot) {
if (snapshot.hasData && snapshot != null) {
if (snapshot.data.length > 0) {
return buildProductList(snapshot);
}
else if (snapshot.data.length==0){
return Center(child: Text('No Data'));
}
} else if (snapshot.hasError) {
return ErrorScreen(errMessage: snapshot.error.toString());
}
return CircularProgressIndicator();
},
),
At first progress indicator will work fine but when data is not found and once 'No Data' gets displayed then the progress indicator never appears again.
How to show progress indicator while loading data only. And show no data when no data and show data when there is data?
This is how the bloc part
final _fetcher = BehaviorSubject<List<Product>>();
Observable<List<Product>> get productList => _fetcher.stream;
Just fetch data from RESTAPI then put it in the List
List<Product> product = await _repository.fetchProduct().catchError((err) => _fetcher.addError(err));
_fetcher.sink.add(product);
First of all, snapshot.hasData and snapshot.data != null are literally the exact same (hasData calls data != null internally). I actually misread your code there, but snapshot will never be null. Thus you can drop it anyway.
The problem here is that you have a misunderstanding of how Stream's work. The stream will not push an update if you are currently adding a product. How would it know when to do that anyway? It will only update if you call add on it and in that case the data will not be null. Hence, there is no progress indicator.
You can easily fix that by adding null when loading:
_fetcher.sink.add(null); // to show progress indicator
List<Product> product = await _repository.fetchProduct().catchError((err) => _fetcher.addError(err));
_fetcher.sink.add(product); // to show data
snapshot.connectionState can tell if your data is still loading...
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
The accepted answer was quite feasible before Dart moved on to null safety but now, it might be a bit uncomfortable if you have to change your stream type to T? just to be able to pass null in these cases. So, I prefer another approach now. I simply create a special "error":
class StreamBuilderProgress extends Error {}
and pass this to the list when the time comes:
list.addError(StreamBuilderProgress());
and make sure that I return the progress indicator whenever snapshot.hasError && snapshot.error is StreamBuilderProgress.
StreamBuilder<List<Room>>(
stream: roomsSource.stream,
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.waiting){
print("loading data");
}else if(!snapshot.hasData || snapshot.data!.isEmpty){
print("no data");
}else if(snapshot.hasData && snapshot.data!.isNotEmpty){
print("has data");
}else {
print("error");
}
}
)

splash screen and one time intro in flutter

I want my splash screen to always appear in my application and it does which is great, but I have a walk through after the splash screen and I want it to be a one time walk through, So i want to add an integer to the shared preferences with a value of 0 and everytime I open the splash screen the value is incremented by one, so when "number" equals 1 or greater at the second run the splash screen skips the walkthrough and goes to home , here is the code that I want to edit now :
void initState() {
// TODO: implement initState
super.initState();
Timer(Duration(seconds: 5), () => MyNavigator.goToIntro(context));
}
And I want it to be like :
void initState() {
// TODO: implement initState
super.initState();int number=0;//this is in the shared prefs
Timer(Duration(seconds: 5), () => if(number==0){MyNavigator.goToIntro(context));
}else{MyNavigator.goToHome(context));
number++;}
}
The below code prints perfectly as we expect(during first launch only, "First launch"). You can use your navigation logic instead of print.
#override
void initState() {
super.initState();
setValue();
}
void setValue() async {
final prefs = await SharedPreferences.getInstance();
int launchCount = prefs.getInt('counter') ?? 0;
prefs.setInt('counter', launchCount + 1);
if (launchCount == 0) {
print("first launch"); //setState to refresh or move to some other page
} else {
print("Not first launch");
}
}
We need to have the number value to be saved across multiple app launches. We can use shared_preference plugin to achieve this.
secondly, getData that saved in our device.
Future<bool> getSaveData() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
bool isIntroScreenOpenedBefore =
sharedPreferences.getBool("isIntroScreenOpened") ?? false;
print(sharedPreferences.containsKey("isIntroScreenOpened")); // check your key either it is save or not?
if (isIntroScreenOpenedBefore == true) {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return LoginBoard();
}));
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WalKThroughScreen();
}));
}
return isIntroScreenOpenedBefore;
}
at first, let's save the data as boolean
Future<void> saveData() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
bool isIntroScreenOpened = true;
sharedPreferences.setBool("isIntroScreenOpened", isIntroScreenOpened); // saved data to your device.
}
Answer by #Dinesh Balasubramanian is works really fine.
But I have 4 initial screen that need to show once. I have done that using same logic in each screen. and then my app was showing 5th screen second time like fast forwarding all the previous screen and stopping on 5th screen.
To resolve this I am getting all the set Preferences at main.dart to open directly 5th screen. but when I do that I am having this problem,
"E/flutter (32606): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)]
Unhandled Exception: Navigator operation requested with a context that
does not include a Navigator.
E/flutter (32606): The context used to
push or pop routes from the Navigator must be that of a widget that is
a descendant of a Navigator widget."
Here is code to switch from main.dart:
int firstLogin, firstMobile, firstOtp, firstInfo;
void setValue() async {
final prefs = await SharedPreferences.getInstance();
firstLogin = prefs.getInt('counterLogin') ?? 0;
firstMobile = prefs.getInt('counterMobile') ?? 0;
firstOtp = prefs.getInt('counterOtp') ?? 0;
firstInfo = prefs.getInt('counterInfo') ?? 0;
prefs.setInt('counterLogin', firstLogin + 1);
prefs.setInt('counterMobile', firstMobile + 1);
prefs.setInt('counterOtp', firstOtp + 1);
prefs.setInt('counterInfo', firstInfo + 1);
if ((firstLogin == 0) && (firstMobile == 0) && (firstOtp == 0) && (firstInfo == 0)) {
setState(() {
print("first launch");
Navigator.of(context).pushNamed(LoginScreen.routeName);
});
} else {
setState(() {
print("not first launch");
Navigator.of(context).pushNamed(LandingSection.routeName);
});
}
}
And calling the setValue() in initState()
I am looking forward for solution.

Loading a file using rootBundle

I need to load a string from a file. The following code always returns null:
static String l( String name ) {
String contents;
rootBundle
.loadString( 'i10n/de.yaml' )
.then( (String r) { contents = 'found'; print( 'then()' ); })
.catchError( (e) { contents = '#Error#'; print( 'catchError()' ); })
.whenComplete(() { contents = 'dd'; print( 'whenComplete()' ); })
;
print( 'after' );
if ( null == contents ) {
return '#null#';
}
String doc = loadYaml( contents );
return doc;
}
I have added this to the flutter: section in pupspec.yaml section:
assets:
- i10n/de.yaml
- i10n/en.yaml
The file i10n/de.yaml exists.
I'm aware, that rootBundle.loadString() is async. Therefore I appended the then() call - assuming that
(String r) { contents = 'found'; }
only gets executed if the Future returned by rootBundle.loadString() is able to return a value.
Actually, the method always returns '#null#'. Therefore, I added the print() statements, which output this:
I/flutter (22382): after
I/flutter (22382): then()
I/flutter (22382): whenComplete()
OK, obviously the future of loadString() executes later than the final print() statement.
Q: But how do I force the future to execute, so that I may retrieve its value?
In other words: How do I wrap some async stuff in certain code to retrieve its value immediately?
PS: First day of flutter/dart. Probably a trivial question...
the .then() is getting executed, but after the rest of the body. As you mention loadString() returns a Future, so completes in the future. To wait for a Future to complete use await. (Note that when you mark the function as async, the function must now return a Future itself - as it has to wait for loadString to complete in the future, so it itself has to complete in the future...) When you call l('something') you will have to await the result.
Future<String> l(String name) async {
try {
String contents = await rootBundle.loadString('i10n/de.yaml');
return contents == null ? '#null#' : loadYaml(contents);
} catch (e) {
return 'oops $e';
}
}
It's no big deal that lots of your utility functions become async as a result of having to wait for things (there's a lot of waiting - for files to read, http requests to complete, etc). You'll eventually end up at the top with something like (call this from initState)
refresh(String s) {
l(s).then((r) {
setState(() {
i18nStuff = r;
});
});
}
to set the state of your Widget when the i18nStuff is ready, and a Widget that has this in its build to switch between a dummy ui for the few milliseconds until it's ready, and then the real UI.
Widget build() {
if (i18nStuff == null) {
return new Container();
}
return new Column(
// build the real UI here
);
}

Resources