All Articles
Tech hub

7 tips for complex mobile app scenarios in NativeScript, part 2

Borislav Kulov
13 Jul 2021
16 min read
Borislav Kulov
13 Jul 2021
16 min read
Nativescript Pt.2 Resolute Website

Welcome back! If you have just finished reading the first part of this blog, welcome now to Part 2.

I have described already what NativeScript is, when to use it, and when not to. I also gave a few tips on how to solve a few complex scenarios related to RadListView items. In this second post, I will go through a few more examples that will make your work with NativeScript easier.

TIP 4: Dynamic tabs in BottomNavigation / Tabs

One common layout design is to have a “BottomNavigation” with a nested “Tabs” control. In NativeScript, that combination works well in case the tabs are defined in the page template. However, sometimes the requirements might include adding or removing tabs dynamically at runtime. This could be challenging because these two components are not fully implemented. In order to accomplish the task, there are different things that need to be done for both platforms. In the example below we have a “BottomNavigation” component with two “TabStrip” items. The first one contains a “Tabs” component to which we will dynamically add tabs. The second “TabStrip” item contains a static about page. To work around the missing (not implemented) features of these components the software developer needs to first declare some parts of the components and then to dynamically create other parts at runtime. More implementation details can be found in the code snippets below:

Template view:

<BottomNavigation id="bottomNavigation" loaded="onBottomNavigationLoaded">

  <TabStrip>

    <TabStripItem>

      <Image src="font://&#xe90d;" class="icons"></Image>

    </TabStripItem>

    <TabStripItem>

      <Image src="font://&#xe90c;" class="icons"></Image>

    </TabStripItem>

  </TabStrip>

  <TabContentItem>

    <Tabs id="tabs" offscreenTabLimit="0" iOSTabBarItemsAlignment="leading" loaded="onTabsLoaded">

      <TabStrip id="tabStrip" class="top-nav">

      </TabStrip>

    </Tabs>

  </TabContentItem>

  <TabContentItem>

    <Frame defaultPage="templates/about-page"></Frame>

  </TabContentItem>

</BottomNavigation>

Template code-behind:

export async function onBottomNavigationLoaded(args: EventData) {

  const bottomNavigation = args.object as BottomNavigation;

  bottomNavigation.bindingContext = {};

  if (isAndroid) {

    const tabsContentItem = bottomNavigation.items[0];

    // check if the tabs are already created

    const originalTabs = tabsContentItem.content as Tabs;

    if (originalTabs.items && originalTabs.items.length > 0) {

        return;

    }

  const tabs = new Tabs();

  tabs.offscreenTabLimit = 0;

  tabs.iOSTabBarItemsAlignment = "leading";

  initTabs(tabsContentItem, tabs);

  }

}

export function onTabsLoaded(args: EventData) {

  if (isAndroid) {

    return;

  }

  const tabs = args.object as Tabs;

  // do not recreate tabs when Tabs component is loaded (on app resume, on navigated from about page, etc.)

  if (tabs.items && tabs.items.length > 0) {

    return;

  }

  initTabs(null, tabs);

}

async function initTabs(tabsContentItem: TabContentItem, tabs: Tabs) {

  const tabDefinitions = await DataRepository.getTabs();

  let tabStrip : TabStrip;

  if (isAndroid) {

    tabStrip = new TabStrip();

    tabStrip.className = "top-nav";

  } else if (isIOS) {

    tabStrip = tabs.getViewById("tabStrip") as TabStrip;

  }

  const tabsItems = [];
  const tabStripItems = [];

tabDefinitions.forEach((tabDefinition) => {

    const tabStripItem = new TabStripItem();

    tabStripItem.title = tabDefinition.title;

    const tabContentItem = new TabContentItem();

   const frame = new Frame();

  let viewModel = new TabViewModel(tabDefinition);

  const loadOptions: LoadOptions = {

    path: "~/templates",

    name: "tab-page",

    attributes: { bindingContext: viewModel },

    exports,

    page: new Page()

  };

  frame.navigate({ create: () => Builder.load(loadOptions) as Page });

  tabContentItem.content = frame;

  tabStripItems.push(tabStripItem);

  tabsItems.push(tabContentItem);

});

tabs.tabStrip = tabStrip;

tabStrip.items = tabStripItems;

tabs.items = tabsItems;

if (isAndroid && tabsContentItem) {

  tabsContentItem.content = tabs;

}

}

TIP 5: Content height and proper vertical scrolling in WebView nested inside ScrollView

“WebView” itself properly handles the vertical scrolling of a large html content piece. But if you need to put some UI elements above or below it on a page and make the whole page’s content scrollable, you need to wrap everything in a “ScrollView”. Two issues here: there are two scrollbars on the page (one from “WebView” and one from “ScrollView”); the height of the scrollable area is not properly calculated and there is a big white space at the end of the “WebView” content. So, what to do?

  • Disable “WebView” scrollbars. To do so you need to play with the underlying native views.

See the code snippet bellow:

if (isIOS) {

    webView.nativeView.scrollView.scrollEnabled = false;

  } else if (isAndroid) {

    // Hide the scrollbars, but don’t disable scrolling

    webView.nativeView.setVerticalScrollBarEnabled(false);

    // Disable scrolling

    let myListener = new android.view.View.OnTouchListener({

      onTouch: function (view, event) {

        return (event.getAction() == android.view.MotionEvent.ACTION_MOVE);

      }

    })

    webView.nativeView.setOnTouchListener(myListener);

  }
  • Manually calculate the height of the “WebView” after its content is loaded:
    • iOS - you could subscribe to “WebView”’s “loadFinished” event and manually set the “WebView” height as in the bellow code snippet:
    • Android - Android doesn’t properly calculate “WebView”’s content height if the component is initialized via the template xml file. So, a possible solution is to add the “WebView” component dynamically after the page is navigated to:
export function onLoadFinished(args) {

  const webView = args.object as WebView;

  if (webView && isIOS) {

    // workaround: in iOS if the WebView is nested into a scroll view it cannot properly determine its height

    // so initialize the height of the WebView some time after the loading is finished in order to get an accurate content height

    setTimeout(() => {

      webView.height = webView.nativeView.scrollView.contentSize.height;

    }, 500);

  }

}
<ScrollView class="parent-container">

  <StackLayout id="webViewParent" class="container">

    <Image class="container__image" src=""></Image>

    <Label class="container__title" text="" textWrap="true"></Label>

    <ios>

      <WebView id="webView" loadFinished="onLoadFinished" loadStarted="onLoadStarted" minHeight="500"></WebView>

    </ios>

  </StackLayout>

</ScrollView>
export function onNavigatedTo(args) {

  const page = <Page>args.object;

  const viewModel = args.context;

  ...

  if (isAndroid) {

    // in Android add the webView dinamically because when it's nested in ScrollView the scroll height cannot be properly calculated

    const webViewAndroid = new WebView();

    webViewAndroid.on('loadFinished', onLoadFinished);

    webViewAndroid.src = viewModel.item.content;

    const parent = page.getViewById("webViewParent");

    parent._addView(webViewAndroid);

  }

}

TIP 6: WebView open links in external browser

The default behavior of “WebView” when a user taps on a link is to navigate to the new link in the same instance of the “WebView”. In some cases, you might want to open the link into an external web browser (the default web browser of your phone). To do so, first you need to configure the “WebView” as shown in the following code snippet:

export function onNavigatedTo(args) {

  const page = <Page>args.object;

  const viewModel = args.context;

  ...

  const webView = <WebView>page.getViewById("webView");

  initWebView(webView);

  webView.src = viewModel.item.content;

}

function initWebView(webView: WebView) {

  if (isAndroid && webView) {

    webView.nativeView.getSettings().setBuiltInZoomControls(false);

    webView.nativeView.client.shouldOverrideUrlLoading =

      (view: WebView, data: any) => {

      let url = '';

      if (typeof data === 'string') {

        url = data;

      } else {

        // Added in API level 24

        url = data.getUrl().toString();

      }

      openUrl(url);

      return true;

    };

  }

}

And then hook to the “WebView”’s “loadStarted” event with the following code:

export const ANDROID_PACKAGE_NAME = "<your Android package name here>";

export const IOS_APP_NAME = "<your iOS app name here>";

export function onLoadStarted(args: LoadEventData) {

  if (args.url && args.url !== "about:blank") {

    if (isAndroid && !args.url.includes(ANDROID_PACKAGE_NAME)) {

      const webView = args.object as WebView;

      webView.goBack();

    } else if (isIOS && !args.url.includes(IOS_APP_NAME)) {

      const webView = args.object as WebView;

      webView.stopLoading();

      webView.reload();

      openUrl(args.url);

    }

  }

}

TIP 7: RadAutoCompleteTextView specifics

“RadAutoCompleteTextView” is a versatile input component which can automatically filter the underlying items collection according to the user’s input. It’s highly customizable and allows the user to specify the suggestion mode (whether to show a drop-down with suggestions or to show the suggestion as appended text), the display mode (single selection as a plain text or multi-selection as tokens), the layout mode (display selected tokens horizontally or vertically) and the completion mode (whether to match the items starting with or containing the user’s input).

There are some specifics that you should keep in mind if you want to use this component:

  • NativeScript bindings support properties of type “ObservableArray”. If a property of type “ObservableArray” has changed (items added or removed) the framework should update the bindings and reflect the changes in the UI. Unfortunately, that doesn’t happen automatically in Android. A possible workaround is to set the “items” property of the component at a later stage after the viewmodel had loaded the data.

Template view:

<au:RadAutoCompleteTextView id="autoComplete" android:loaded="onACLoaded">

</au:RadAutoCompleteTextView>

ViewModel:

public dataLoaded: Promise<void>;

public items: ObservableArray<TokenModel>;

Template code-behind:

const AC_ID = "autoComplete";

export function onACLoaded(args) {

  const viewModel = args.object.bindingContext;

  const ac: RadAutoCompleteTextView = args.object.getViewById(AC_ID);

  if (viewModel && ac) {

    viewModel.dataLoaded.then(() => {

      ac.items = viewModel.items;

    });

  }

}
  • “RadAutoCompleteTextView” emits a few events which could be used by software developers to do additional work. These events are emitted in a certain order that is, unfortunately, different for Android and iOS. The most used events are “didAutoComplete”, “tokenRemoved” and “textChanged”. We hook to the “textChanged” event to do some cleanup in the viewmodel when the user clears the text. When the user selects something from the drop-down two events are emitted in Android (“didAutoComplete” and “textChanged”) but only one in iOS (“didAutoComplete”). “textChanged” event is emitted in Android because when the user selects from the drop-down a token is created, and the input text is cleared. As we’re using this event to do cleanup in the viewmodel we had to implement additional logic to distinguish the cases when this event is emitted after a token is created and after the user manually clears the text.
  • There is a crash on iOS when the drop-down is opened. It was reported some time ago, yet the GitHub issue on it is still open. Apparently, the team maintaining the component doesn’t have enough resources and it’s not clear when it will be fixed. Fortunately, one of the community members has fixed the bug and provided a download location in the GitHub issue.
  • There are some issues with this component for which we still haven’t found workarounds. For example, when you quickly scroll in the drop-down on Android the component will crash. And sometimes on iOS the drop-down is messed up – there are empty list items, or the list items go over each other.

Having those remarks in mind it’s up to you whether to use this component or to write your own implementation.

Final words

Overall, NativeScript is a good choice if you have a JavaScript/web background, and you want to build small to mid-sized projects. But if you need to build an enterprise-grade application meant to last, the cost of overcoming framework limitations like the ones described above may outweigh the benefits. My advice – do your homework, research alternatives and pick the right tool for the right job.

I hope the tips & tricks will help you solve some of the technical challenges that you face, or that you’ll be facing in the future. Good luck.

And remember, if you cannot find what you need and require a consultation, feel free to get in touch with me and my colleagues at Resolute Software! Be sure to sign up for our insights in the form below as we’ll be sharing more tips and tricks from real-life development projects!

NativeScript
Tips & Tricks

stay tuned

Subscribe to our insights

Secured with ReCAPTCHA. Privacy Policy and Terms of Service.