I am developing an application in Flutter where I need to implement an image selection function like in instagram.
But there is an issue, my app UI is freezing when trying to get and compress files from user phone gallery.
This is my first experience with flutter isolates, but as far as i know it should work without freezes.
Here is an image for a better understanding of what i want to do.
This is a function that calls getFiles function in isolation.
Here i get paths of user phone gallery files and pass them to another function in order to compress and get files for rendering.
Future fetchImages({ bool fetchMore = false, bool force = false }) async {
if (!fetchMore) {
setState(() => fetched = false);
}
if (force) {
assetsCount = await assetPathEntity!.assetCountAsync;
page = 0;
files.clear();
}
if (assetsCount == 0 || page >= (assetsCount / pageSize)) {
return setState(() => fetched = true);
}
final assetEntities = await assetPathEntity!.getAssetListPaged(page: page++, size: pageSize);
lastCompletedIndex = files.length;
final receivePort = ReceivePort();
final completer = Completer();
getFiles(receivePort.sendPort, assetEntities);
try {
receivePort.listen((filesPaths) {
for (final filePath in filesPaths) {
files.add({
"path": filePath,
"compressedFile": null,
});
}
if (scaledFile == null && files.isNotEmpty) {
scaledFile = File(files.first["path"]);
}
compressAlbumImages();
completer.complete();
setState(() => fetched = true);
}).onError((_) {
compressAlbumImages();
completer.complete();
setState(() => fetched = true);
});
await Future.wait([completer.future]);
} catch (_) { }
finally {
receivePort.close();
}
}
This is getFiles function that runs in isolation
void getFiles(SendPort sendPort, List<AssetEntity> assetEntities) async {
final List<String> filesPaths = [];
for (final assetEntity in assetEntities) {
try {
final file = await assetEntity.file;
if (file != null) {
filesPaths.add(file.path);
}
} catch (_) { }
}
sendPort.send(filesPaths);
}
This is a function that calls compressImages function and adds any value to refresh the list of images
Here i pass the paths and get compressed files for rendering.
void compressAlbumImages() async {
final receivePort = ReceivePort();
final completer = Completer();
compressImages(receivePort.sendPort, files, lastCompletedIndex);
try {
receivePort.listen((compressedFilesWithPath) {
files = compressedFilesWithPath;
fileStreamCt.sink.add(1);
completer.complete();
}).onError((_) {
completer.complete();
});
await Future.wait([completer.future]);
} catch (_) {}
finally {
receivePort.close();
}
return;
}
This is an image compression function that runs in isolation
void compressImages(SendPort sendPort, List<Map<String, dynamic>> files, int startFromIndex) async {
final List<String> filesToBeRemoved = [];
for (int idx = startFromIndex; idx < files.length; idx++) {
final file = files[idx];
try {
final compressedFile = await FlutterNativeImage.compressImage(
file["path"],
quality: 20,
percentage: 20,
targetHeight: 300,
targetWidth: 300,
);
file["compressedFile"] = compressedFile;
} catch (_) {
filesToBeRemoved.add(file["path"]);
}
}
if (filesToBeRemoved.isEmpty) {
return sendPort.send(files);
}
final compressedFilesWithPaths = files
.whereNot((element) => filesToBeRemoved.contains(element["path"]))
.toList();
sendPort.send(compressedFilesWithPaths);
}
And finally i render compressed images
return StreamBuilder(
stream: fileStreamCt.stream,
builder: (ctx, AsyncSnapshot<int> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("LOADING...");
}
if (snapshot.hasError) {
return Text("AN ERROR OCCURRED");
}
return buildAlbumImages();
}
);
Render like this
Image.file(compressedImage);
If in short - When i'm getting user phone gallery files && compress them, my app UI starts freezing.
I don't know why it's freezing.
I tried the same process but with file.readAsBytes() and to render like Image.memory(compressedFileBytes), but it was useless.
I would be very grateful for any help.
Thanks in advance.
In your code you never actually create an isolate. The 'isolate code' in getFile, compressImages etc simply runs on the main isolate and indeed will block the UI.
Per documentation, you create an isolate with Isolate.spawn and pass only the sendPort. The isolate then must send back a receivePort, and the main thread uses that port to send the data you want to isolate to process (like assetEntities), processes it and sends the results back to the main thread. It's a bit complicated, and requires different function signatures than you have here.
Fortunately, a much easier way to accomplish what you want (still using Isolates that won't block the UI) is to use the compute function from the dart:async package:
Change the signature of your getFiles function to Future<List<String>> getFiles(List<AssetEntity> assetEntities) async and do in it what you need to do, returning the list of filesPaths as you do now. Importantly, getFiles must be a top level or a static function, it cannot be a regular class method. Then, where you need the calculation done you use something like var filesPaths = await compute(getFiles, assetEntities). Now, the getFiles function is called in an isolate, and the return value is given back to you on the main isolate. The nice thing is that now this looks a lot like regular await call, no need for sendPorts etc. You can do the same thing for your other heavy calculation methods.
One (big) constraint with isolates is the type of argument you can pass to and from an isolate, see here. Those same constraints apply here, because under the hood the compute function also uses sendPorts etc.
Related
I have the code below that I thought would run whenever the given screen is reached, i.e. whenever the user goes to this screen. This screen creates some temporary files for the user. I don't need them after the user leaves the screen, so I wanted to flush them everytime the user reaches this screen. However, the line with await cleanupTempAudioFiles(); doesn't seem to be doing its job.
#override
void initState() {
super.initState();
initialize();
}
void initialize() async {
uid = auth.currentUser;
filesInProgressFileDirString = systemTempDir.path + '/App/AppAudioFiles/FilesInProgress/';
fileInProgressFileDir = await Directory(fileInProgressFileDirString).create(recursive: true);
myRecorder = await FlutterSoundRecorder().openRecorder();
myPlayer = await FlutterSoundPlayer().openPlayer();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
)..addListener(() {
setState(() {});
});
controller?.reset();
await cleanupTempAudioFiles(); //This code seems to not be running every time the screen is opened
setState(() {
sendableFileExists = 0;
});
}
Future<void> cleanupTempAudioFiles() async {
final dir = Directory(filesInProgressFileDir.path);
final List<FileSystemEntity> files = await dir.list().toList();
files.forEach((file) async {
if (file.path == filesInProgressFileDir.path + currentAppFilename) {
await file.delete();
}
if (file.path == filesInProgressFileDir.path + currentAppFilename + 'High.mp3') {
await file.delete();
}
if (file.path == filesInProgressFileDir.path + currentAppFilename + 'Low.mp3') {
await file.delete();
}
});
}
The initState() function is called when your object is put into the widget tree. This is not the same as every time it is displayed. Navigator.pop() for example will show the screen without re-inserting the widget into the tree. see https://api.flutter.dev/flutter/widgets/State/initState.html
To run every time the user sees a widget you should put the code into the build function or consider #overriding dispose(), didChangeDependencies() or
didUpdateWidget() instead to get the right part of the widget lifecycle.
So yesterday I was developing some sort of offline functionality. Therefore, I added an ApiService that returns Observables.
Currently, I fetch my access_token for jwt-Authentication and then use this token to generate Headers for my API-Request. After a successful request, I save the result to my storage. This works fine. Now here is the problem I want to check for an unsuccessful request (e.g. servers are down, app is offline) and then return my stored result from storage. But I can't get it to work.
Here is my code:
getJobs(): Observable<any> {
this.auth.checkToken()
return from(this.storage.get(ACCESS_TOKEN)).pipe(
switchMap(token => {
let options = this.auth.addToken(token)
return this.http.get(API_URL + "jobs", options)
}),
map(res => {
if (res) {
this.storage.set(JOBS, res)
return res
} else {
return from(this.storage.get(JOBS))
}
}),
catchError(() => {
return from(this.storage.get(JOBS))
})
)
}
Further investigations have shown that after the server or the app is offline neither the map() nor the catchError() functions were executed.
UPDATE:
The solution provided by DJ House is correct. My Code works perfectly in my browser but if I build my app with ionic cordova build android it gets stuck after the this.http.get(...) So it's clearly and issue with cordova
SOLUTION:
Wow! Something magical happened! I've found out that the catchError method gets called BUT after almost 2 Minutes, which is way to slow... So I will implement a timeout.
Thanks
flixoflax
The main issue that you may be facing is you are using the map incorrectly. Map acts upon a normal value (usually, its not an observable) and returns a new value. map() should always return the same type of value. In your map() you are either return the response (which I am assuming is of type Jobs) OR you are return an Observable<Jobs>. This will cause your subscribers to need verbose logic to handle that.
It looks like you are trying to use that map() to set your local storage with the returned jobs from your api. I would recommend using tap() since you aren't trying to change the value you are returning.
function getJobs(): Observable<any> {
this.auth.checkToken()
return from(this.storage.get(ACCESS_TOKEN)).pipe(
switchMap(token => {
let options = this.auth.addToken(token)
return this.http.get(API_URL + "jobs", options)
}),
// change to tap()
tap(res => {
if (res) {
this.storage.set(JOBS, res)
}
}),
catchError(() => {
return from(this.storage.get(JOBS))
})
)
}
If the switchMap throws an error, the tap will be skipped. That will ensure you only set storage if you recieve a value from the API. If you always want to set the storage (even if the API threw an error) then move the tap() to be after the catchError().
Can you please try moving the catchError operator as first operator inside pipe method. This is to ensure that you catch error as soon as you recieve it from observable. Please change it like below:
getJobs(): Observable<any> {
this.auth.checkToken()
return from(this.storage.get(ACCESS_TOKEN)).pipe(
switchMap(token => {
let options = this.auth.addToken(token)
return this.http.get(API_URL + "jobs", options)
}),
catchError(() => {
return from(this.storage.get(JOBS))
})
map(res => {
if (res) {
this.storage.set(JOBS, res)
return res
} else {
return from(this.storage.get(JOBS))
}
}),
)
}
I have data in firebase data that looks like the following:
The code for getting customers data:
getCustomersOnQeueu = async () => {
let customers = this.customersRef.orderByChild("ticket").once('value')
return customers
}
Code for rendering data:
renderCustomers = () => {
let customersViews = []
this.getCustomersOnQeueu().then((customers) => {
let customersTickets = customers.val()
console.log(customersTickets)
let sortedKeys = Object.keys(customersTickets).sort(function(a, b){
return customersTickets[b].ticket - customersTickets[a].ticket
})
console.log(sortedKeys)
for(i=0; i<sortedKeys.length; i++) {
let key = sortedKeys[i]
console.log(customersTickets[key]["customer"])
customersViews.push(<View>
<Text>{customersTickets[key["customer"]}</Text>
</View>)
}
})
return (<View>
<Text>Available Customers: </Text>
{customersViews}
</View>)
}
render() {
return (
<View>
{this.renderCustomers()}
</View>
)
}
Now after data being fetched and sorted I can see the following in console:
I have a problem that this line of code is never executed:
customersViews.push(<View>
<Text>{customersTickets[key["customer"]}</Text>
</View>)
I am guessing that it might be because customersViews array is initialized after rendering is done and not before, how can I wait for data fetching and sorting to finish then render the data?
When you are attempting to get your firebase response you're not actually waiting for it. The code below does not wait for it to be executed.
getCustomersOnQeueu = async () => {
let customers = this.customersRef.orderByChild("ticket").once('value')
return customers
}
To WAIT for it to be executed use AWAIT:
getCustomersOnQeueu = async () => {
let customers = await this.customersRef.orderByChild("ticket").once('value')
return customers
}
setting state via this.setState() always rerenders a component. If you want to rerender a vraible's latest value, put it in the state. Putting customerViews in state and updating it via this.setState() might solve your problem here.
Not sure what you are trying to render. a list of names?
It seems that your line you are talking about is not working because customersTickets.push cant push react element to the array.
You can even try it in your developer console
let array = []
arr.push(test)
and the result is "Uncaught SyntaxError"
I'm developing an Android App (Android 6.0 and above) and I have performance issues with large XML-Files. (about 17 mB)
When the app is started the required XML-document is loaded from the private storage and a List is returned (takes about 2-3 sek.) and filled in an custom adapter -> this works perfectly fine.
But the user can start a synchronization manually inside the app (e.g.: data
was updated on server)
Therefor I've implemented a background download-service so that the UI stays responsive during the download.
The downloaded data is stored inside the private folder again.
problem:
The background download works perfectly and my UI stays responive until I start reading the information from the new XML-File.
I don't get any result back -> even after 3 min there is no return value from the function although I use the same function for reading the XML like I do at the beginning -> GetKontaktliste()
public class MainActivity : AppCompatActivity
{
alert.SetPositiveButton("Synchronisieren", async (senderAlert, args) =>
{ new DownloadTask(this).Execute("");});
}
public class DownloadTask : AsyncTask
{
protected override Java.Lang.Object DoInBackground(params Java.Lang.Object[] #params)
{
App_Tools lAppTools = new App_Tools();
ThreadPool.QueueUserWorkItem(async state =>
{
//Download Function -> received XML-Data is saved in local storage
bool lKontakte = await lAppTools.DownloadKontakte(_context);
bool lVorgaenge = await lAppTools.DownloadVorgaenge(_context);
bool lVorgaengeImport = await lAppTools.ImportVorgaenge(_context);
if (lKontakte == true)
{
//Problem is that i dont get any results back here
List<KontaktItem> lResult = new List<KontaktItem>();
LinqAbfragen lLinq = new LinqAbfragen();
lLinq.GetKontaktliste();
}
});
return true;
}
}
public class LinqAbfragen
{
//Read Contacts
public List<KontaktItem> GetKontaktliste()
{
List<KontaktItem> lResult = new List<KontaktItem>();
//Read from IEnumerable<XElement>
var KontakteAsXElement = ReadXmlAsXElement("Kontakte.xml", "Kontakt");
lResult = (from kontakt in KontakteAsXElement
select new KontaktItem
{
AdressNr = kontakt.Element("AdressNr").Value,
Vorname = kontakt.Element("Vorname1").Value,
Nachname = kontakt.Element("Name1").Value,
xmlData = (string)kontakt.ToString()
}
).ToList();
return lResult;
}
}
I have an odd issue I can't explain the reason for it - maybe someone here can shed some light on it
I have a ticket scanning app in Xamarin Forms currently testing it on android
the interface allows you to:
type an order number and click the check order Button
use the camera scanner to scan which automatically triggers check order
use the barcode scanner to scan which automatically triggers check order
after the check order validation, user has to select the number of tickets from a drop down list and press confrim entry button
what I'm trying to do, is if the seats available on that ticket is just 1 - then automatically trigger confirm entry button functionality
problem that I have is that - some of my logic depends on setting the drop down index in code - for some reason it doesn't update - as seen in the debugger shot here
and this is the second tme I've noticed this today, earlier it was a var I was trying to assign a string and it kept coming up as null - eventually I replaced that code
is this a bug in xamarin ?
code has been simplified:
async void OnCheckOrderButtonClicked(object sender, EventArgs e)
{
await ValidateOrderEntry();
}
private async void scanCameraButton_Clicked(object sender, EventArgs e)
{
messageLabel.Text = string.Empty;
var options = new ZXing.Mobile.MobileBarcodeScanningOptions();
options.PossibleFormats = new List<ZXing.BarcodeFormat>() {
ZXing.BarcodeFormat.QR_CODE,ZXing.BarcodeFormat.EAN_8, ZXing.BarcodeFormat.EAN_13
};
var scanPage = new ZXingScannerPage(options);
scanPage.OnScanResult += (result) =>
{
//stop scan
scanPage.IsScanning = false;
Device.BeginInvokeOnMainThread(async () =>
{
//pop the page and get the result
await Navigation.PopAsync();
orderNoEntry.Text = result.Text;
//automatically trigger update
await ValidateOrderEntry();
});
};
await Navigation.PushAsync(scanPage);
}
private async Task ValidateOrderEntry()
{
//...other code....
checkInPicker.Items.Clear();
if (availablTickets == 1)
{
checkInPickerStack.IsVisible = true;
checkInPicker.SelectedIndex = 0;
messageLabel.Text = "Ticket OK! - " + orderNoEntry.Text;
messageLabel.TextColor = Color.Green;
//select the only element
checkInPicker.SelectedIndex = 0;
await PostDoorEntry();
}
//...other code....
}
private async Task PostDoorEntry()
{
int entryCount = checkInPicker.SelectedIndex + 1;
//... more code...
//...post api code..
}
Maybe I'm overlooking something, but you clear all the items a few lines above the one you are pointing out. That means there are no items in your Picker and thus you can't set the SelectedIndex to anything other than -1, simply because there are no items.