Page Life-Cycle
In Smartface Page is a component with life-cycle. The life-cycle of the page is in the following order:
Creation - Constructor
Pages in Smartface defined as a JavaScript Class. New instances form those classes are created on demand. Router, SwipeView or TabBarController etc. calls the constructor of the class to create a new instance of the page.
This phase of the page life-cycle is achieved once per page instance. Singleton pages are created only once per route.
- Developer Page Code
- UI Editor Generated Page Code
- UI Editor Page Configuration
import Page1Design from "generated/pages/page1";
import { Route, Router } from "@smartface/router";
export default class Page1 extends Page1Design {
constructor(private router?: Router, private route?: Route) {
super({});
}
onShow() {
super.onShow();
}
onLoad() {
super.onLoad();
}
}
//------------------------------------------------------------------------------
//
// This code was auto generated.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
//
//------------------------------------------------------------------------------
import pageContextPatch from "@smartface/contx/lib/smartface/pageContextPatch";
import { Styleable, ViewType } from "generated/core/Styleable";
import Page = require("sf-core/ui/page");
import View = require("sf-core/ui/view");
import { ComponentStyleContext } from "generated/core/ComponentStyleContext";
import System = require("sf-core/device/system");
class $NewPage001 extends Page {
dispatch: (action: { [key: string]: any }) => void;
children: { [key: string]: any } = {};
static $$styleContext: ComponentStyleContext = {
classNames: ".sf-page",
defaultClassNames: " .default_page",
userProps: {},
statusBar: {
classNames: ".sf-statusBar",
defaultClassNames: " .default_statusBar",
userProps: {},
},
headerBar: {
classNames: ".sf-headerBar",
defaultClassNames: " .default_headerBar",
userProps: {},
},
};
constructor(props?: any) {
super(
Object.assign(
{
onLoad: () => {
this.headerBar.title = "newPage001";
},
},
props
)
);
this.children["statusBar"] = this.statusBar || {};
this.children["headerBar"] = this.headerBar || {};
pageContextPatch(this, "newPage001");
}
addChild(
child: View,
name?: string,
classNames?: string,
userProps?: { [key: string]: any },
defaultClassNames?: string
): void {
if (this["layout"]) {
this["layout"].addChild(child);
} else {
this.addChild(child);
}
}
addChildByName(child: View, name: string) {
this.children[name] = child;
this.addChild(child);
}
set testId(value: string) {
if (System.OS === System.OSType.IOS) {
this["layout"].nativeObject.setValueForKey(
value,
"accessibilityIdentifier"
);
} else {
this["layout"].nativeObject.setContentDescription(value);
}
}
}
export default $NewPage001;
{
"components": [
{
"type": "Page",
"id": "8c6b-89ac-e0e5-164f",
"userProps": {},
"className": ".sf-page",
"props": {
"name": "newPage001",
"parent": null,
"children": [
"0059-e8db-d9b7-1169",
"478e-0016-b366-338f"
],
"orientation": "PORTRAIT"
},
"initialized": false
},
{
"type": "StatusBar",
"id": "0059-e8db-d9b7-1169",
"userProps": {},
"className": ".sf-statusBar",
"props": {
"name": "statusBar",
"parent": "8c6b-89ac-e0e5-164f",
"children": [],
"visible": true,
"isRemovable": false
}
},
{
"type": "HeaderBar",
"id": "478e-0016-b366-338f",
"userProps": {
"title": "newPage001"
},
"className": ".sf-headerBar",
"props": {
"name": "headerBar",
"parent": "8c6b-89ac-e0e5-164f",
"children": [],
"title": "newPage001",
"visible": true,
"isRemovable": false
}
}
],
"componentByID": {
"b84d-dbf9-335d-537a": {
"type": "Page",
"id": "8c6b-89ac-e0e5-164f",
"userProps": {},
"className": ".sf-page",
"props": {
"name": "newPage001",
"parent": null,
"children": [
"0059-e8db-d9b7-1169",
"478e-0016-b366-338f"
],
"orientation": "PORTRAIT"
},
"initialized": false
},
"be8e-2a98-a321-18a5": {
"type": "StatusBar",
"id": "0059-e8db-d9b7-1169",
"userProps": {},
"className": ".sf-statusBar",
"props": {
"name": "statusBar",
"parent": "8c6b-89ac-e0e5-164f",
"children": [],
"visible": true,
"isRemovable": false
}
},
"5a0f-0a54-64bf-c261": {
"type": "HeaderBar",
"id": "478e-0016-b366-338f",
"userProps": {
"title": "newPage001"
},
"className": ".sf-headerBar",
"props": {
"name": "headerBar",
"parent": "8c6b-89ac-e0e5-164f",
"children": [],
"title": "newPage001",
"visible": true,
"isRemovable": false
}
}
}
}
The code above is created when a new page is created via UI editor (6.16.3). Constructors of the page are mentioned with the inline comments. Those code blocks are the ones that are going to be called first.
Since Typescript is used, there will also be Javascript compiled code. Those can be found under dist directory.
What is happening in Page Constructor?
- Native UI Object references are being created, but they are not ready to process
- Initial values are being assigned
What to do in Page Constructor
- Create UI objects
- Assign page events
- Create page-wise properties
- Assign events like onPress -> Preferred at onLoad
Page Load
Page load is called later by the native system sometime later than the constructor. The load event is not called more than once during the whole life-cycle of the page. UI operations in this step have less impact on performance rather than the later steps because UI rendering has not started yet.
This phase of the page life-cycle is achieved once per page instance. Singleton pages are created only once per definition.
What is happening during Page Load?
- UI Object references are processed. They are ready to use
- Properties are ready to use
- For the pages that are created by UI editor, initial theming has been applied
What to do in Page load
- Read & change property values
- Assign events to child objects
- Execute one-off code, such as static data loading
- For HeaderBar, create items, set their events
- Setup style of Headerbar
- Make static bindings, such as language texts, RTL-LTR
- Set ScrollView layout height or width (based on direction), if the content is fixed
Page Show
Show event of the page is called every time, just before the page is shown. This event is fired after the page is loaded and can be fired more than one time.
This phase of the page life-cycle may be achieved more than once per page instance. Having a singleton page has no effect on this.
What is happening during show
- Some of the UI object references are created here (HeaderBar & StatusBar)
- Smartface is setting up the navigation history
- A partial rendering of the page starts
What to do in Page Show
- Set (every time) visibility of StatusBar, is at least one page contradicts to others
- Set (every time) visibility of HeaderBar, is at least one page contradicts to others
- Setup dynamic data binding
- If possible and if required, after data binding set ScrollView layout height or width (based on direction) for dynamic content
Post Render
In this phase layout calculations and rendering is completed. Height and width of the components can be read. This becomes handy while the size of the component is not known fully, such as auto-sized components (Image & Text)\
In most of the cases, the developer does not need to use this phase of the page. If it is still required, for iOS & Android it has to be handled differently.
This phase of the page life-cycle may be achieved more than once per page instance. Having a singleton page has no effect on this.
Android
In Android, UI rendering is mostly completed in phase 3 - Show
. In some cases, there can be some components which are not still completed. It is advised to check the values within the onShow
event if they are not fully completed at that step, proceed the with the suggested approach below.
The developer needs to write setTimeout
within the onShow of the page.
onShow() {
super.onShow();
setTimeout(postRender.bind(this), 50);
}
function postRender() {
// do your postRender operation
}
In the example above, after 50
ms, the postRender operation is triggered. This can have some effects:
- The 50 ms might not be sufficient for some devices, if that is the case, do not hesitate to increase the delay
- If the 50 ms is quite long then this can lead to some movement on the UI.
This postRender
function is called every time when the page is shown. In order to avoid this, a custom property can be created with the page.
import NewPage001Design from "generated/pages/newPage001";
import { Route, Router } from "@smartface/router";
export default class NewPage001 extends NewPage001Design {
pageIsShown = false;
constructor(private router?: Router, private route?: Route) {
super({});
}
onShow() {
super.onShow();
!this.pageIsShown && setTimeout(postRender.bind(this), 50);
this.pageIsShown = true;
}
onLoad() {
super.onLoad();
}
}
function postRender() {
// do your postRender operation
}
iOS
On iOS there are 3 native different events to do this, in order:
- viewWillLayoutSubviews - page.nativeObject.onViewLayoutSubviews
- viewDidLayoutSubviews - page.nativeObject.onViewDidLayoutSubviews
- viewDidAppear - page.nativeObject.onViewDidAppear
iOS native life-cylce of those events are explained in iOS UIViewController lifecycle
import NewPage001Design from "generated/pages/newPage001";
import { Route, Router } from "@smartface/router";
export default class NewPage001 extends NewPage001Design {
constructor(private router?: Router, private route?: Route) {
super({});
this.nativeObject.onViewLayoutSubviews = onViewLayoutSubviews.bind(
this,
this.nativeObject.onViewLayoutSubviews &&
this.nativeObject.onViewLayoutSubviews.bind(this)
);
this.nativeObject.onViewDidLayoutSubviews = onViewDidLayoutSubviews.bind(
this,
this.nativeObject.onViewDidLayoutSubviews &&
this.nativeObject.onViewDidLayoutSubviews.bind(this)
);
this.nativeObject.onViewDidAppear = onViewDidAppear.bind(
this,
this.nativeObject.onViewDidAppear &&
this.nativeObject.onViewDidAppear.bind(this)
);
}
onShow() {
super.onShow();
}
onLoad() {
super.onLoad();
}
}
function onViewLayoutSubviews(superOnViewLayoutSubviews) {
superOnViewLayoutSubviews && superOnViewLayoutSubviews();
console.log("Over onViewLayoutSubviews");
}
function onViewDidLayoutSubviews(superOnViewDidLayoutSubviews) {
superOnViewDidLayoutSubviews && superOnViewDidLayoutSubviews();
console.log("Over onViewDidLayoutSubviews");
}
function onViewDidAppear(superOnViewDidAppear) {
superOnViewDidAppear && superOnViewDidAppear();
console.log("Over onViewDidAppear");
}
The onViewDidLayoutSubviews and onViewDidAppear events are most suitable events to use.
- Both of them are going to be fired everytime page is shown
- onViewDidLayoutSubviews will be fired when orientation is changed, or somehow resized; onViewDidAppear will not be fired in those cases
Bring them together
Here are two samples for the page are given to get a postRender event.
- Fires every time page is shown - post Render
- One-off postRender
import NewPage001Design from 'generated/pages/newPage001';
import System from '@smartface/native/device/system';
import { Route, Router } from "@smartface/router";
export default class NewPage001 extends NewPage001Design {
constructor(private router?: Router, private route?: Route) {
super({});
if (System.OS === System.OSType.IOS) {
this.nativeObject.onViewDidLayoutSubviews = onViewDidLayoutSubviews.bind(this,
this.nativeObject.onViewDidLayoutSubviews &&
this.nativeObject.onViewDidLayoutSubviews.bind(this)
);
}
}
onShow() {
super.onShow();
if (System.OS === System.OSType.ANDROID) {
setTimeout(postRender.bind(this), 50);
}
}
onLoad() {
super.onLoad();
}
}
function onViewLayoutSubviews(superOnViewLayoutSubviews) {
superOnViewLayoutSubviews && superOnViewLayoutSubviews();
console.log("Over onViewLayoutSubviews");
}
function onViewDidLayoutSubviews(superOnViewDidLayoutSubviews) {
superOnViewDidLayoutSubviews && superOnViewDidLayoutSubviews();
console.log("Over onViewDidLayoutSubviews");
postRender.call(this);
}
function onViewDidAppear(superOnViewDidAppear) {
superOnViewDidAppear && superOnViewDidAppear();
console.log("Over onViewDidAppear");
}
function postRender() {
// do your postRender operation
import NewPage001Design from "generated/pages/newPage001";
import System from "@smartface/native/device/system";
import { Route, Router } from "@smartface/router";
export default class NewPage001 extends NewPage001Design {
pageIsShown = false;
constructor(private router?: Router, private route?: Route) {
super({});
if (System.OS === System.OSType.IOS) {
this.nativeObject.onViewDidLayoutSubviews = onViewDidLayoutSubviews.bind(
this,
this.nativeObject.onViewDidLayoutSubviews &&
this.nativeObject.onViewDidLayoutSubviews.bind(this)
);
}
}
onShow() {
super.onShow();
if (System.OS === System.OSType.ANDROID) {
!this.pageIsShown && setTimeout(postRender.bind(this), 50);
this.pageIsShown = true;
}
}
onLoad() {
super.onLoad();
}
}
function onViewLayoutSubviews(superOnViewLayoutSubviews) {
superOnViewLayoutSubviews && superOnViewLayoutSubviews();
console.log("Over onViewLayoutSubviews");
}
function onViewDidLayoutSubviews(superOnViewDidLayoutSubviews) {
superOnViewDidLayoutSubviews && superOnViewDidLayoutSubviews();
console.log("Over onViewDidLayoutSubviews");
!this.pageIsShown && postRender.call(this);
this.pageIsShown = true;
}
function onViewDidAppear(superOnViewDidAppear) {
superOnViewDidAppear && superOnViewDidAppear();
console.log("Over onViewDidAppear");
}
function postRender() {
// do your postRender operation
}
Hide
Hide event is called when another page is shown instead of this one.\ This phase of the page may not be achieved at all if the application terminates before. A page which has not shown, will not be hidden.
What to do on Page Hide Event, what not to do
- Do not modify HeaderBar and StatusBar
- No need to store the state of the page, but the data state might be stored if needed
- Do not perform animations or UI operations on the page, this will be a waste of performance
- Might need to keep track of user time on the page for analytics purposes
import NewPage001Design from "generated/pages/newPage001";
import { Route, Router } from "@smartface/router";
export default class NewPage001 extends NewPage001Design {
constructor(private router?: Router, private route?: Route) {
super({});
}
onShow() {
super.onShow();
}
onLoad() {
super.onLoad();
}
onHide() {
super.onHide();
}
}
On iOS, when showing a dialog, onHide of the page will be triggered. On Android, it will not trigger.
Destruction
After a page is created (passed all the steps before), is not currently visible, there are no references to the page instance object, then this JavaScript object is marked for safe to delete. The JavaScript garbage collector is cleaning up that object. As with the garbage collector is clearing the JavaScript counterpart of the object, native side of the object will be destroyed too.
There is no event to capture the destruction of the page.
References to singleton page instances are always kept by Router or Navigator. Therefore they will never be marked to be deleted.
Mozilla documentation about Memory Management is giving best practices for keeping references to JavaScript object. There can be slight differences in the implementation of JavaScript Engines; iOS is using Apple JavaScriptCore, Android is using Google V8.