tabris-js icon indicating copy to clipboard operation
tabris-js copied to clipboard

Widgets created in async functions do not appear

Open elshadsm opened this issue 3 years ago • 1 comments

Problem description

When we append a new view to the contentView within async function, it is not rendered. However, it appears when we move the app to the background and then come back to the foreground. We can reproduce the issue with the following snippet, which new screen (with red background) should be opened after 4 seconds when we click the button on the first yellow screen.

Expected behavior

The rendering of the view should not be affected by any async functions.

Environment

  • Tabris.js version: 3.7.2
  • Device: iPad 10.2, Samsung A70
  • OS: iOS 14.6, Android 11

Code snippet

import {Button, Color, Composite, LayoutData, contentView} from 'tabris';

class Screen extends Composite {

  constructor(background) {
    super({
      layoutData: LayoutData.stretch,
      background
    });
    this.append(
      new Button({
        left: 16,
        right: 16,
        centerY: 0,
        text: 'Click'
      }).onSelect(async () => {
        await new Promise(resolve => setTimeout(resolve, 4000)); // without this line it works fine
        new Screen(Color.red).appendTo(contentView);
      })
    );
  }

}

new Screen(Color.yellow).appendTo(contentView);

elshadsm avatar Aug 04 '21 08:08 elshadsm

It works fine when rewriting it to use only Promise, and it also works fine when dropping it in to a freshly generated tabris app using the default Jsx/JavaScript template. That's because it's re-written by tsc to use the Promise that tabris provides:

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const tabris_1 = require("tabris");
class Screen extends tabris_1.Composite {
    constructor(background) {
        super({
            layoutData: tabris_1.LayoutData.stretch,
            background
        });
        this.append(new tabris_1.Button({
            left: 16,
            right: 16,
            centerY: 0,
            text: 'Click'
        }).onSelect(() => __awaiter(this, void 0, void 0, function* () {
            yield new Promise(resolve => setTimeout(resolve, 4000)); // without this line it works fine
            new Screen(tabris_1.Color.red).appendTo(tabris_1.contentView);
        })));
    }
}
new Screen(tabris_1.Color.yellow).appendTo(tabris_1.contentView);

So it is really only the native async/await that do not work, and I don't believe we can make it work easily. They are using (I assume) a native Promise implementation provided by the JS VM (V8/corejs) that (naturally) does not know that tabris.flush() needs to be called when a promise is awaited. So the best we can do is document this, unless @mpost and @karolszafranski know a way to modify the built-in Promise/async support.

tbuschto avatar Aug 04 '21 12:08 tbuschto