7 tips for complex mobile app scenarios in NativeScript (part 1)
What is NativeScript
NativeScript {N} is an open-source framework for building cross-platform, native mobile apps for iOS and Android platforms using a single codebase. The idea is simple – you write your views in specific XML-based format and your code in JavaScript (or by using any programming language that transpiles to JavaScript, such as TypeScript) and NativeScript compiler then transforms that into native mobile applications. NativeScript is supported in frameworks like Angular, VueJS, React, Svelte, Capacitor and Ionic. The apps built with NativeScript result in fully native apps, which use the same APIs as if they were developed in Xcode or Android Studio. In addition to the UI components provided by the framework, software developers can use community developed plugins and third-party libraries from NPM, CocoaPods, Maven and Gradle.
NativeScript was originally conceived and developed by Telerik/Progress in 2014 and at the end of 2019 was taken over by Progress partner, nStudio and other community members.
Why NativeScript
NativeScript embraces the idea of Single Page Applications (SPA). The app has a main page that contains one or more frames, and you can implement navigation by loading different pages into these frames. This concept should be familiar to the developers who are already using modern JavaScript-based frameworks. Software developers can use Angular, TypeScript or modern JavaScript to get truly native UI and performance while reusing skills and code from their web projects. NativeScript comes with a useful set of UI components, third-party plugins and libraries, and provides 100% access to the native APIs via JavaScript.
Why not NativeScript
The idea behind NativeScript is great – use your web skills to develop truly native mobile apps for iOS and Android. When the framework was being actively developed by Telerik/Progress, the manpower investment in the project was quite substantial, with people working on the core framework, the UI components, native bridges, plugins, documentation and customer support. With the growing popularity of the open source framework among the community, at the end of 2019 Progress decided to transfer the ownership of the framework to its community and step aside. Sadly, this means the project is now maintained by a few active community members with limited capacity to keep up with framework updates, support the latest mobile operating systems and help the community. Some third-party community-maintained plugins have also not been touched for months and risk becoming unusable after a few framework updates.
Some background
А few months ago, my team started working on a mobile app that spreads health information globally. Initially, the content was grouped in four tabs containing lists of various articles. After the first version was released, people started using it actively and our client decided to upgrade it by adding new requirements. The app needed to provide information in several different languages, information related to a specific region, information about some global pandemics such as Covid-19 and Ebola, so it can facilitate users in customizing their data feed in order to get only the information relevant to their interests.
Even though NativeScript provides a set of UI components that you can use into your applications, some of them are lacking features on one side and on the other, some features are incomplete. Having mentioned the need of implementing the additional requirements, it’s probably not far from your mind that they represented a new challenge we had to overcome. However, if there’s a challenge, there should be also ways on how to handle it.
Some of the obstacles that we faced during this project and the solutions we have used are listed below. Part of them go against the best practices in software development and I don’t claim these are the only or the best possible ways to go with but that’s what worked well for the essence of the project and the goals of the task, so the good results were a fact.
A few useful tips & tricks to make your {N} life easier
TIP 1: RadListView expandable item template (iOS)
Imagine that we want to implement a filtering panel on the top of the page. As the mobile screens are smaller than the desktop ones, the filtering panel needs to be collapsible/expandable. On the other hand, we want this filtering panel to be scrollable along with the other list items in order to fit more of them on the screen.
There are multiple approaches to this challenge:
- We could use a “ScrollView” as a parent and put the filtering panel and a “Repeater” element inside. We could also use “ListView” instead of the “Repeater” but “ListView” and “ScrollView” don’t work well together and by combining them, we will lose some cool features of the “ListView” such as “pull to refresh” and “load more items”.
- We could use “RadListView” header to implement the filtering panel which will preserve features as “pull to refresh” and “load more items”, but “RadListView” header and footer are not scrollable.
- Different combinations of layout root components and repeater / list.
We approached the matter in another way. “RadListView” and “ListView” both support configurable item templates, so we’ve defined two item templates – one for the first item which is the filtering panel, and one for the rest of the list view items:
<lv:RadListView items="" itemTemplateSelector="selectItemTemplate">
<lv:RadListView.itemTemplates>
<template key="filtering">
...
</template>
<template key="regular">
...
</template>
</lv:RadListView.itemTemplates>
</lv:RadListView>
export function selectItemTemplate(item, index, items) {
return index === 0 ? "filtering" : "regular";
}
The template with key “filtering” is the more interesting one and here we had to play with its visibility in order to make its content expandable/collapsible
<template key="filtering">
<StackLayout padding="10">
<Label text="Click to expand/collapse" tap="onToggleFilteringPanel"/>
<StackLayout id="filteringPanel" marginTop="5" visibility="">
<!-- collapsible content here -->
...
</StackLayout>
</StackLayout>
</template>
Аll good till now, yet the above practice works fine for Android, but not for iOS. By design “RadListView” for iOS doesn’t reflect the “visibility” property changes at run-time. Therefore, we need to manually trigger a refresh when there are changes in “RadListView” item templates and delay the refresh to allow the framework to update the bindings first.
const FILTERING_PANEL_ID = "filteringPanel";
export function onToggleFilteringPanel(args) {
const filteringPanelStackLayout = args.object.parent.getViewById(FILTERING_PANEL_ID) as StackLayout;
const viewModel = filteringPanelStackLayout?.bindingContext;
if (filteringPanelStackLayout && viewModel) {
viewModel.isCollapsed = !viewModel.isCollapsed;
if (isIOS) {
// iOS list item templates are not automatically layouted by design
// so when we change the visibility of some items in the filtering panel we need to manually refresh the list
const list: RadListView = filteringPanelStackLayout.parent.parent as RadListView;
setTimeout(() => {
list.refresh();
});
}
}
}
TIP 2: RadListView displays message when the source items array is empty
A good user experience is to show a message to the user when there are no items in the list. The list components for desktop development have this feature integrated, but that’s not the case with “RadListView”. There are basically two commonly used approaches to implement this feature:
- One is to put the “RadListView” and a “Label” component containing the message inside “GridLayout” or “StackLayout” and play with their visibility attribute.
- And the second approach is to use “RadListView” footer template to show the message. The trick here is to hide the message when there are items in the list view and show it when the list view is empty. NativeScript is not performing in the best way when changing the “visibility” attribute at runtime in iOS, so we use “Label” “opacity” attribute to show/hide the message.
The code snippet bellow shows it in practice.
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:lv="nativescript-ui-listview">
<lv:RadListView items="" itemTemplateSelector="selectItemTemplate" loadOnDemandMode="Auto" loadMoreDataRequested="onLoadMoreItemsRequested">
<lv:RadListView.itemTemplates>
<template key="highlight">
...
</template>
<template key="regular">
...
</template>
</lv:RadListView.itemTemplates>
<lv:RadListView.footerItemTemplate>
<Label text="No items" textWrap="true" android:visibility=""
ios:opacity=""></Label>
</lv:RadListView.footerItemTemplate>
</lv:RadListView>
</Page>
TIP 3: RadListView lazy loading more items on demand
“RadListView” comes with support for lazy loading of its source items collection. The “load-on-demand” feature is particularly useful in cases when data needs to be loaded in chunks (pages) to optimize bandwidth usage and improve the UX. If you have a long list of items, or you simply don’t need all items at the beginning, you could load only a chunk of them and then load another chunk when the user scrolls to the bottom of the page. What happens is that the component emits an event (“loadMoreDataRequested”) which you could handle and provide the next chunk of items. In theory, if there are no more items to load you should set a special flag in the event handler’s arguments object to indicate that the event shouldn’t be triggered anymore. In practice, there is a difference how this is internally implemented (or not implemented yet) in Android and in iOS. We will see the difference in the following code snippet:
export function onLoadMoreItemsRequested(args: LoadOnDemandListViewEventData) {
const listView: RadListView = args.object;
const viewModel: PublicationsViewModel<IDataItem> = listView.bindingContext;
viewModel.loadMoreData().then((result: boolean) => {
if (isAndroid) {
// this is how it should work in both platforms
args.returnValue = result;
listView.notifyAppendItemsOnDemandFinished(0, !result);
} else if (isIOS) {
// this should always be set to 'false' or the event won't be emitted again
args.returnValue = false;
}
if (!result) {
listView.notifyLoadOnDemandFinished(true);
}
});
}
The trick here is to set the “returnValue” property of the “args” parameter to “false” in iOS or the event won’t be triggered again.
Stay tuned for more NativeScript tricks
This is the first of two blog posts in which you can explore how a few complex scenarios were solved with the help of NativeScript. If you find this information helpful, please, subscribe to our Blog in order to receive a notification for the second part in which I will have a closer look at problems like dynamic tabs in BottomNavigation, Content height and proper vertical scrolling in WebView nested inside ScrollView and more.