Async work is freezing UI in Jetpack Compose for Web - android-jetpack-compose

When I try to load data from disk asynchronously with Coroutines in Jetpack Compose Web, process freezes UI thread:
var content by remember { mutableStateOf("") }
LaunchedEffect(true) {
launch {
// content.txt is located at src/commonMain/resources/
content = loadBytesFromPath("content.txt").decodeToString()
}
}
Text(content)
Is there a way to do it really asynchronously?
P.S. there is a sample I'm working with.

Related

Sending commands to the web app in electron renderer process since contextIsolation changes

I understand that contextIsolation changes are introduced for security purposes, and i read about contextBridge which exposes custom API to the webapp running in the renderer process so web app can control electron app in which is runing.
electron preload script
const contextBridge = require("electron").contextBridge;
contextBridge.exposeInMainWorld("electronApi", {
'doSomething' : function () {
// some code to execute
}
});
web app
window.electronApi.doSomething();
This is perfectly clear and i understand why is this done this way.
However, i do not understand how can communication work the other way, so how can electron execute web app commands? Let's take following example, web app has window.someWebAppMethod defined and electron should execute it
web app
window.someWebAppMethod = function () {
// do somehing web app related
}
electron preload script
window.someWebAppMethod()
^^ this does not work because of contextIsolation which was whole point of contextIsolation, but i still need to have a certain way of triggering web app commands from electron. Most obvious reason is let's say i have electron main menu with command labeled "Open Quick Jump" which should tell the web app loaded in rendered process to execute method which will show the "Quick Jump" function of the web app.
Maybe i'm missing something painfully obvious, but i'd still appreciate any help i can get.
Thanks
I found a way of doing this. Not sure it's obvious or if it is secure enough, but here it is:
electron preload script
const contextBridge = require("electron").contextBridge;
let doSomethingInWebApp = null;
contextBridge.exposeInMainWorld("electronApi", {
'exposeDoSomethingInWebApp' : function (callback) {
doSomethingInWebApp = callback;
}
});
web app
if (window.electronApi && window.electronApi.exposeDoSomethingInWebApp) {
window.electronApi.exposeDoSomethingInWebApp(function () {
// execute whatever you need to execute in webapp
});
}
electron preload script
if (doSomethingInWebApp) {
// execute previously defined custom behavior in web app
doSomethingInWebApp();
}
So it's quite simple and it works.

ReactJs PWA not updating on iOS

I'm building a ReactJs PWA but I'm having trouble detecting updates on iOS.
On Android everything is working great so I'm wondering if all of this is related to iOS support for PWAs or if my implementation of the service worker is not good.
Here's what I've done so far:
Build process and hosting
My app is built using webpack and hosted on AWS. Most of the files (js/css) are built with some hash in their name, generated from their content. For those which aren't (app manifest, index.html, sw.js), I made sure that AWS serves them with some Cache-Control headers preventing any cache. Everything is served over https.
Service Worker
I kept this one as simple as possible : I didn't add any cache rules except precache for my app-shell:
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
Service-worker registration
Registration of the service worker occurs in the main ReactJs App component, in the componentDidMount() lifecycle hook:
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW is already up and running
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
} else {
// first service worker registration, do nothing
}
}
};
};
});
}
}
Service worker lifecycle management
According to the Google documentation about service workers, a new version of the service worker should be detected when navigating to an in-scope page. But as a single-page application, there is no hard navigation happening once the app has been loaded.
The workaround I found for this is to hook into react-router and listen for route changes, then manually ask the registered service worker to update itself :
const history = createBrowserHistory(); // from 'history' node package
history.listen(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (!reg) {
return null;
}
reg.update();
});
}
});
Actual behavior
Throwing a bunch of alert() everywhere in the code showed above, this is what I observe :
When opening the pwa for the first time after adding it to the homescreen, the service worker is registered as expected, on Android and iOS
While keeping the app opened, I deploy a new version on AWS. Navigating in the app triggers the manual update thanks to my history listener. The new version is found, installed in the background. Then my snackbar is displayed and I can trigger the switch to the new SW.
Now I close the app and deploy a new version on AWS. When opening the app again :
On Android the update is found immediately as Android reloads the page
iOS does not, so I need to navigate within the app for my history listener to trigger the search for an update. When doing so, the update is found
After this, for both OS, my snackbar is displayed and I can trigger the switch to the new SW
Now I close the app and turn off the phones. After deploying a new version, I start them again and open the app :
On Android, just like before, the page is reloaded which detects the update, then the snackbar is displayed, etc..
On iOS, I navigate within the app and my listener triggers the search for an update. But this time, the new version is never found and my onupdatefound event handler is never triggered
Reading this post on Medium from Maximiliano Firtman, it seems that iOS 12.2 has brought a new lifecycle for PWAs. According to him, when the app stays idle for a long time or during a reboot of the device, the app state is killed, as well as the page.
I'm wondering if this could be the root cause of my problem here, but I was not able to find anyone having the same trouble so far.
So after a lot of digging and investigation, I finally found out what was my problem.
From what I was able to observe, I think there is a little difference in the way Android and iOS handle PWAs lifecycle, as well as service workers.
On Android, when starting the app after a reboot, it looks like starting the app and searching an update of the service worker (thanks to the hard navigation occuring when reloading the page) are 2 tasks done in parallel. By doing that, the app have enough time to subscribe to the already existing service worker and define a onupdatefound() handler before the new version of the service worker is found.
On the other hand with iOS, it seems that when you start the app after a reboot of the device (or after not using it for a long period, see Medium article linked in the main topic), iOS triggers the search for an update before starting your app. And if an update is found, it will be installed and and enter its 'waiting' status before the app is actually started. This is probably what happens when the splashscreen is displayed...
So in the end, when your app finally starts and you subscribe to the already existing service worker to define your onupdatefound() handler, the update has already been installed and is waiting to take control of the clients.
So here is my final code to register the service worker :
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if (reg.waiting) {
// a new version is already waiting to take control
this.newWorker = reg.waiting;
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
}
// handler for updates occuring while the app is running, either actively or in the background
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW already has control over the app
/*
same code omitted
*/
} else {
// very first service worker registration, do nothing
}
}
};
};
});
}
}
Note :
I also got rid of my listener on history that I used to trigger the search for an update on every route change, as it seemed overkill.
Now I rely on the Page Visibility API to trigger this search every time the app gets the focus :
// this function is called in the service worker registration promise, providing the ServiceWorkerRegistration instance
const registerPwaOpeningHandler = (reg) => {
let hidden;
let visibilityChange;
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
window.document.addEventListener(visibilityChange, () => {
if (!document[hidden]) {
// manually force detection of a potential update when the pwa is opened
reg.update();
}
});
return reg;
};
As noted by Speckles (thanks for saving me the headache), iOS installs the new SW before launching the app. So the SW doesn't get a chance to catch the 'installing' state.
Work-around: check if the registration is in the waiting state then handle it.
I've made an (untested) example of handling this. - a mod to the default CRA SW.

Prevent concurrent access to the same data in Dart

I'm trying to create a file cache in Dart (Flutter), where a file only gets downloaded once and then cached for future requests. (Yes, I know there are existing packages for this, but my needs are more specific.)
Problem is, if I have two widgets on the same page trying to display the same image, they're both making the same request at the same time, downloading the file twice.
I tried turning the cache into a singleton, handing out a single instance of itself, but that seems to have no effect:
class FileCache {
final _fileList = List<File>();
static FileCache _instance;
factory FileCache() {
if (_instance == null) {
_instance = FileCache._internal();
}
return _instance;
}
FileCache._internal();
bool add(File file) {
if (_fileList.contains(file)) {
return false;
}
_fileList.add(file);
return true;
}
void remove(File file) {
_fileList.remove(file);
}
}
I did see another package that does synchronization (here), but looking at the Dart code I have no idea how it is enforcing the synchronous access.
How, in Dart, can you force a specific class or member variable to be accessed serially for this purpose?
The Flutter UI runs in a single isolate. Memory isn't shared across isolates (hence the name), so you don't need to worry about parallel operations (as you would with multiple threads on a multi-core system). However, you do need to worry about concurrent operations that can be interleaved when execution yields from await.
This means that you don't need special atomic primitives. You could set a flag when downloading a file to avoid downloading it again.
You don't use Futures anywhere, so there are no places for your code (as shown) to be interrupted. However, you also don't show the code where you're actually downloading files, and presumably you have asynchrony there. You could do something like:
final pendingDownloads = <String, Future<void>>{};
Future<void> downloadFile(String url) {
if (pendingDownloads.containsKey(url)) {
return pendingDownloads[url];
}
Future<void> downloadFileInternal() async {
final request = await HttpClient().getUrl(...);
...
}
pendingDownloads[url] = downloadFileInternal();
return pendingDownloads[url];
}

.play() for audio tags lag on iOS (and possibly other mobile devices)

I am attempting to rebuild a game that works on itch to be compatible on most major devices and browsers. I have a problem where on iOS (and possibly other mobile devices) a call to play audio tags from click and touch events has quite a significant delay.
I have read about several potential causes, including the 300ms delay, preventDefault for the second click event, stopPropagation for potential parent clicks, different audio formats causing lag in decompression, etc. Nothing seems to work.
Initially my intent was to keep everything in vanilla js without outside libraries to force myself to really learn what's going on under the hood. However, I have been having some success with some outside libraries for other problems, so I tried fastclick.js for this problem. That didn't work for me either. So, if someone knows how to address this issue without a library that would be great, but after looking at the fastclick code, that may be beyond my level of comprehension.
A current build can be found at www.teachersteve.net/assett_loading_with_ian/assett_loading_with_ian.html
Some explanation of my thought process:
I removed anything that is actually game related to try and isolate the problem. I put all the assetts directly in the html to simplify the loading process and wait to start the js after the DOM loaded
document.addEventListener("DOMContentLoaded", doSomething);
I am currently only calling one audio tag to play as I read somewhere that calling multiple tags can overload the decompression process and cause a delay. That seems to not be the issue.
I have 3 different file formats so far for compatibility attempts... I did read that LEI16 (a wav format) might be best because it eliminates compression, although I haven't tried it yet.
This is the rest of the doSomething() function
function doSomething() {
document.body.style.opacity = 1;
document.addEventListener("click", playAudio);
document.addEventListener('click', preventInputDefault);
document.addEventListener('ontouchend', preventInputDefault);
console.log("assetts loaded");
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
function playAudio() {
// backgroundMusic.play();
letterAudio.play();
// correctAnswerAudio.play();
// letterAudio.play();
// correctAnswerAudio.play();
}
function preventInputDefault(evt) {
evt.preventDefault();
console.log("hello preventInputDefault");
evt.stopPropagation();
console.log("hello stopPropagation");
}
}
Thanks!

Understanding why I need to dispatch back to main thread

I just wanted to clear up something that feels a bit unclear for me. Consider the following code that executes a closure asynchronously:
func fetchImage(completion: UIImage? -> ()) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
// fetch the image data over the internet
// ... assume I got the data
let image = UIImage(data: data)
dispatch_async(dispatch_get_main_queue()) {
completion(image)
}
}
}
To my understanding, the reason we need to dispatch back to the main thread is because it would otherwise take longer to call the completion closure to give back the image.
However, I feel that perspective is a bit cheesy. For example, I'd also like to create a isLoading property that would be used to prevent multiple network calls from happening at the same time:
func fetchImage(completion: UIImage? -> ()) {
// if isLoading is true, then don't continue getting the image because I want only 1 network operation to be running at 1 time.
if isLoading {
completion(nil)
return
}
isLoading = true
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
let image = UIImage(data: data)
// image creation is complete. Set isLoading to false to allow new fetches
self.isLoading = false
dispatch_async(dispatch_get_main_queue()) {
completion(image)
}
}
}
For this above snippet, my question is - Should I place self.isLoading = false in the dispatch block to the main queue? Or is it insignificant?
All advice appreciated!
It isn't that "it would otherwise take longer", it is that all updates to the UI must be performed on the main queue to prevent corruption that may occur from concurrent updates to the autolayout environment or other UI datastructures that aren't thread-safe.
In prior versions of iOS a common side effect of not updating the UI on the main thread was a delay in that upgrade appearing, however as of iOS 9 you will get an exception.
In terms of your question, it is best that your code behaves consistently. I.e. Either always dispatch the completion handler on the main queue or never do so. This will allow the programmer who is writing the completion block to know whether they need to dispatch UI updates or not.
It is probably best to set isLoading to false as soon as the load has finished, so outside the dispatch_async is best.
Given that your function is retrieving a UIImage there is a good chance that the caller will be updating the UI, so it is probably 'nice' to dispatch the completion handler on the main thread.
To fetch the image from internet in background you simply need to do an async request, you don't need to do it in the background queue as you are doing now.
On the main thread you basically need to do all that things about UI manipulation, because it always run on main thread. This is the important part.
So, the request completion block (the one you'll use to fetch the image) is executed in background (since it is async) and here, inside the block, you need to get the main thread to set the image for the UIImageView for instance.
Other properties than the ones directly related to UI element doesn't needs to be on the main thread as far as I know and I have never had a problem this way.

Resources