NativeScript Cheatsheet¶
NativeScript - Native Mobile Apps with JavaScript
NativeScript is an open-source framework for building truly native mobile applications using JavaScript, TypeScript, Angular, Vue.js, or React. It provides direct access to native APIs and UI components without WebViews.
Table of Contents¶
- Installation
- Getting Started
- Project Structure
- CLI Commands
- Core Concepts
- UI Components
- Layouts
- Navigation
- Data Binding
- Styling
- Platform APIs
- Plugins
- Angular Integration
- Vue.js Integration
- React Integration
- Testing
- Performance
- Deployment
- Best Practices
Installation¶
Prerequisites¶
# Install Node.js (version 14 or later)
# Download from nodejs.org
# Verify Node.js installation
node --version
npm --version
# Install Android Studio (for Android development)
# Download from developer.android.com
# Install Xcode (for iOS development, macOS only)
# Download from Mac App Store
# Set environment variables
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools
NativeScript CLI Installation¶
# Install NativeScript CLI globally
npm install -g @nativescript/cli
# Verify installation
ns --version
tns --version # Alternative command
# Check environment setup
ns doctor
ns doctor android
ns doctor ios
# Install platform tools
ns install
Development Environment Setup¶
# For Android development
# 1. Install Android Studio
# 2. Install Android SDK
# 3. Set ANDROID_HOME environment variable
# 4. Add platform-tools to PATH
# For iOS development (macOS only)
# 1. Install Xcode
# 2. Install Xcode Command Line Tools
xcode-select --install
# 3. Install CocoaPods
sudo gem install cocoapods
# Verify setup
ns doctor
Getting Started¶
Create New Project¶
# Create new NativeScript project
ns create MyApp
# Create with specific template
ns create MyApp --template @nativescript/template-hello-world
ns create MyApp --template @nativescript/template-tab-navigation
ns create MyApp --template @nativescript/template-drawer-navigation
# Create with framework
ns create MyApp --ng # Angular
ns create MyApp --vue # Vue.js
ns create MyApp --react # React
# Create with TypeScript
ns create MyApp --ts
# Navigate to project directory
cd MyApp
# Run the app
ns run android
ns run ios
Project Templates¶
# JavaScript templates
ns create MyApp --template @nativescript/template-hello-world
ns create MyApp --template @nativescript/template-tab-navigation
ns create MyApp --template @nativescript/template-drawer-navigation
ns create MyApp --template @nativescript/template-master-detail
# TypeScript templates
ns create MyApp --template @nativescript/template-hello-world-ts
ns create MyApp --template @nativescript/template-tab-navigation-ts
# Angular templates
ns create MyApp --template @nativescript/template-hello-world-ng
ns create MyApp --template @nativescript/template-tab-navigation-ng
ns create MyApp --template @nativescript/template-drawer-navigation-ng
# Vue.js templates
ns create MyApp --template @nativescript/template-hello-world-vue
ns create MyApp --template @nativescript/template-tab-navigation-vue
# React templates
ns create MyApp --template @nativescript/template-hello-world-react
Project Structure¶
Basic Structure¶
MyApp/
├── app/ # Application source code
│ ├── app.js # Application entry point
│ ├── app.css # Global styles
│ ├── main-page.js # Main page logic
│ ├── main-page.xml # Main page markup
│ └── main-view-model.js # Main page view model
├── platforms/ # Platform-specific code (generated)
│ ├── android/
│ └── ios/
├── node_modules/ # Dependencies
├── package.json # Project configuration
├── nsconfig.json # NativeScript configuration
├── webpack.config.js # Webpack configuration
└── App_Resources/ # Platform resources
├── Android/
│ ├── src/main/res/
│ └── app.gradle
└── iOS/
├── Info.plist
└── build.xcconfig
app.js (Entry Point)¶
import { Application } from '@nativescript/core';
// Start the application
Application.run({ moduleName: 'app-root' });
// Alternative with navigation
Application.run({ moduleName: 'main-page' });
main-page.xml (UI Markup)¶
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo" class="page">
<Page.actionBar>
<ActionBar title="My App" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<GridLayout class="page">
<Label text="Hello, NativeScript!" class="h1 text-center" />
<Button text="Tap me!" tap="{{ onTap }}" class="btn btn-primary" />
</GridLayout>
</Page>
main-page.js (Page Logic)¶
import { fromObject } from '@nativescript/core';
export function onNavigatingTo(args) {
const page = args.object;
const viewModel = fromObject({
message: 'Hello, NativeScript!',
onTap: function() {
this.set('message', 'Button tapped!');
}
});
page.bindingContext = viewModel;
}
CLI Commands¶
Project Management¶
# Create new project
ns create <app-name> [options]
# Add platform
ns platform add android
ns platform add ios
# Remove platform
ns platform remove android
ns platform remove ios
# List platforms
ns platform list
# Clean project
ns platform clean android
ns platform clean ios
# Update platform
ns platform update android
ns platform update ios
Development Commands¶
# Run on device/emulator
ns run android
ns run ios
# Run with specific options
ns run android --device
ns run ios --device
# Run on specific device
ns run android --device <device-id>
ns run ios --device <device-id>
# Debug mode
ns debug android
ns debug ios
# Preview in browser (limited functionality)
ns preview
# Serve for hot reload
ns run android --hmr
ns run ios --hmr
Build Commands¶
# Build for platform
ns build android
ns build ios
# Build for release
ns build android --release
ns build ios --release --for-device
# Build with specific configuration
ns build android --env.production
ns build ios --env.production
# Clean build
ns clean
# Prepare platform
ns prepare android
ns prepare ios
Plugin Management¶
# Install plugin
ns plugin add <plugin-name>
# Remove plugin
ns plugin remove <plugin-name>
# List installed plugins
ns plugin list
# Update plugin
ns plugin update <plugin-name>
# Search for plugins
npm search nativescript-plugin
Core Concepts¶
Observable and Data Binding¶
import { Observable, fromObject } from '@nativescript/core';
// Create observable object
const viewModel = new Observable();
viewModel.set('name', 'John Doe');
viewModel.set('age', 30);
// Listen to property changes
viewModel.on(Observable.propertyChangeEvent, (args) => {
console.log(`Property ${args.propertyName} changed to ${args.value}`);
});
// Create from object
const user = fromObject({
firstName: 'John',
lastName: 'Doe',
fullName: function() {
return `${this.firstName} ${this.lastName}`;
},
updateName: function(first, last) {
this.set('firstName', first);
this.set('lastName', last);
}
});
// Observable Array
import { ObservableArray } from '@nativescript/core';
const items = new ObservableArray([
{ name: 'Item 1', value: 1 },
{ name: 'Item 2', value: 2 },
{ name: 'Item 3', value: 3 }
]);
// Listen to array changes
items.on(ObservableArray.changeEvent, (args) => {
console.log('Array changed:', args);
});
// Add items
items.push({ name: 'Item 4', value: 4 });
// Remove items
items.splice(0, 1);
Page Navigation¶
import { Frame, topmost } from '@nativescript/core';
// Navigate to page
export function navigateToDetails() {
const frame = Frame.topmost();
frame.navigate({
moduleName: 'details-page',
context: { id: 123, name: 'Sample Item' }
});
}
// Navigate with animation
export function navigateWithAnimation() {
topmost().navigate({
moduleName: 'details-page',
animated: true,
transition: {
name: 'slide',
duration: 300,
curve: 'ease'
}
});
}
// Navigate back
export function goBack() {
topmost().goBack();
}
// Clear history and navigate
export function navigateAndClearHistory() {
topmost().navigate({
moduleName: 'home-page',
clearHistory: true
});
}
// Modal navigation
export function showModal() {
const frame = topmost();
frame.showModal('modal-page', {
context: { data: 'modal data' },
fullscreen: false,
animated: true
});
}
// Close modal
export function closeModal() {
const frame = topmost();
frame.closeModal();
}
Application Lifecycle¶
import { Application, on as applicationOn, launchEvent, suspendEvent, resumeEvent, exitEvent } from '@nativescript/core';
// Application launch
applicationOn(launchEvent, (args) => {
console.log('Application launched');
if (args.android) {
// Android-specific code
console.log('Launched Android application with the following intent: ' + args.android.getIntent());
} else if (args.ios) {
// iOS-specific code
console.log('Launched iOS application with options: ' + args.ios);
}
});
// Application suspend
applicationOn(suspendEvent, (args) => {
console.log('Application suspended');
// Save application state
});
// Application resume
applicationOn(resumeEvent, (args) => {
console.log('Application resumed');
// Restore application state
});
// Application exit
applicationOn(exitEvent, (args) => {
console.log('Application exit');
// Cleanup resources
});
// Low memory warning
applicationOn('lowMemory', (args) => {
console.log('Low memory warning');
// Free up memory
});
// Orientation change
applicationOn('orientationChanged', (args) => {
console.log('Orientation changed to:', args.newValue);
});
UI Components¶
Basic Components¶
<!-- Label -->
<Label text="Hello World" class="label" />
<Label text="{{ message }}" textWrap="true" />
<!-- Button -->
<Button text="Click Me" tap="{{ onTap }}" class="btn btn-primary" />
<Button text="Disabled" isEnabled="false" />
<!-- TextField -->
<TextField text="{{ username }}" hint="Enter username" />
<TextField secure="true" hint="Password" />
<!-- TextView -->
<TextView text="{{ description }}" editable="false" />
<TextView hint="Enter description" />
<!-- Image -->
<Image src="~/images/logo.png" stretch="aspectFit" />
<Image src="{{ imageUrl }}" loadMode="async" />
<!-- ActivityIndicator -->
<ActivityIndicator busy="{{ isLoading }}" />
<!-- Progress -->
<Progress value="{{ progressValue }}" maxValue="100" />
<!-- Slider -->
<Slider value="{{ sliderValue }}" minValue="0" maxValue="100" />
<!-- Switch -->
<Switch checked="{{ isEnabled }}" />
<!-- DatePicker -->
<DatePicker date="{{ selectedDate }}" />
<!-- TimePicker -->
<TimePicker time="{{ selectedTime }}" />
<!-- SearchBar -->
<SearchBar text="{{ searchQuery }}" hint="Search..." submit="{{ onSearch }}" />
List Components¶
<!-- ListView -->
<ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<ListView.itemTemplate>
<Label text="{{ name }}" class="list-item" />
</ListView.itemTemplate>
</ListView>
<!-- ListView with complex template -->
<ListView items="{{ users }}" separatorColor="transparent">
<ListView.itemTemplate>
<GridLayout columns="auto, *" rows="auto, auto" class="list-item">
<Image src="{{ avatar }}" row="0" col="0" rowSpan="2" class="avatar" />
<Label text="{{ name }}" row="0" col="1" class="name" />
<Label text="{{ email }}" row="1" col="1" class="email" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
<!-- Repeater -->
<Repeater items="{{ items }}">
<Repeater.itemTemplate>
<StackLayout class="item">
<Label text="{{ title }}" class="title" />
<Label text="{{ description }}" class="description" />
</StackLayout>
</Repeater.itemTemplate>
</Repeater>
Advanced Components¶
<!-- WebView -->
<WebView src="{{ url }}" />
<WebView src="~/html/index.html" />
<!-- HtmlView -->
<HtmlView html="{{ htmlContent }}" />
<!-- Placeholder -->
<Placeholder creatingView="{{ onCreatingView }}" />
<!-- ContentView -->
<ContentView content="{{ dynamicContent }}" />
<!-- Frame -->
<Frame defaultPage="main-page" />
<!-- TabView -->
<TabView selectedIndex="{{ selectedTab }}">
<TabViewItem title="Tab 1">
<Label text="Content of Tab 1" />
</TabViewItem>
<TabViewItem title="Tab 2">
<Label text="Content of Tab 2" />
</TabViewItem>
</TabView>
<!-- SegmentedBar -->
<SegmentedBar items="{{ segments }}" selectedIndex="{{ selectedSegment }}" />
<!-- ActionBar -->
<ActionBar title="My App" class="action-bar">
<ActionItem text="Settings" tap="{{ openSettings }}" ios.position="right" android.position="actionBar" />
<ActionItem icon="res://ic_menu" tap="{{ openMenu }}" ios.position="left" android.position="actionBar" />
</ActionBar>
Layouts¶
StackLayout¶
<!-- Vertical stack (default) -->
<StackLayout>
<Label text="First" />
<Label text="Second" />
<Label text="Third" />
</StackLayout>
<!-- Horizontal stack -->
<StackLayout orientation="horizontal">
<Button text="Button 1" />
<Button text="Button 2" />
<Button text="Button 3" />
</StackLayout>
<!-- With spacing and padding -->
<StackLayout spacing="10" padding="20">
<Label text="Item 1" />
<Label text="Item 2" />
</StackLayout>
GridLayout¶
<!-- Basic grid -->
<GridLayout columns="*, *, *" rows="auto, auto">
<Label text="(0,0)" row="0" col="0" />
<Label text="(0,1)" row="0" col="1" />
<Label text="(0,2)" row="0" col="2" />
<Label text="(1,0)" row="1" col="0" />
<Label text="(1,1)" row="1" col="1" />
<Label text="(1,2)" row="1" col="2" />
</GridLayout>
<!-- Spanning cells -->
<GridLayout columns="*, *" rows="auto, auto, auto">
<Label text="Header" row="0" col="0" colSpan="2" class="header" />
<Label text="Left" row="1" col="0" />
<Label text="Right" row="1" col="1" />
<Label text="Footer" row="2" col="0" colSpan="2" class="footer" />
</GridLayout>
<!-- Complex layout -->
<GridLayout columns="100, *, 50" rows="auto, *, auto">
<Label text="Fixed 100" row="0" col="0" />
<Label text="Flexible" row="0" col="1" />
<Label text="Fixed 50" row="0" col="2" />
<ScrollView row="1" col="0" colSpan="3">
<StackLayout>
<!-- Content -->
</StackLayout>
</ScrollView>
<Button text="Footer" row="2" col="0" colSpan="3" />
</GridLayout>
FlexboxLayout¶
<!-- Basic flexbox -->
<FlexboxLayout flexDirection="row" justifyContent="space-between">
<Label text="Start" />
<Label text="Center" />
<Label text="End" />
</FlexboxLayout>
<!-- Vertical flexbox -->
<FlexboxLayout flexDirection="column" alignItems="center">
<Label text="Centered" />
<Button text="Button" />
</FlexboxLayout>
<!-- Flex wrap -->
<FlexboxLayout flexDirection="row" flexWrap="wrap">
<Label text="Item 1" flexGrow="1" />
<Label text="Item 2" flexGrow="1" />
<Label text="Item 3" flexGrow="1" />
<Label text="Item 4" flexGrow="1" />
</FlexboxLayout>
<!-- Complex flexbox -->
<FlexboxLayout flexDirection="column" height="100%">
<Label text="Header" class="header" />
<FlexboxLayout flexDirection="row" flexGrow="1">
<Label text="Sidebar" class="sidebar" />
<ScrollView flexGrow="1">
<StackLayout>
<!-- Main content -->
</StackLayout>
</ScrollView>
</FlexboxLayout>
<Label text="Footer" class="footer" />
</FlexboxLayout>
AbsoluteLayout¶
<!-- Absolute positioning -->
<AbsoluteLayout>
<Label text="Top Left" left="0" top="0" />
<Label text="Top Right" right="0" top="0" />
<Label text="Bottom Left" left="0" bottom="0" />
<Label text="Bottom Right" right="0" bottom="0" />
<Label text="Center" left="50%" top="50%" />
</AbsoluteLayout>
<!-- Overlapping elements -->
<AbsoluteLayout>
<Image src="~/images/background.jpg" width="100%" height="100%" />
<Label text="Overlay Text" left="20" top="50" class="overlay-text" />
<Button text="Action" right="20" bottom="50" />
</AbsoluteLayout>
DockLayout¶
<!-- Basic dock layout -->
<DockLayout>
<Label text="Top" dock="top" class="dock-top" />
<Label text="Bottom" dock="bottom" class="dock-bottom" />
<Label text="Left" dock="left" class="dock-left" />
<Label text="Right" dock="right" class="dock-right" />
<Label text="Fill" class="dock-fill" />
</DockLayout>
<!-- App layout with dock -->
<DockLayout>
<ActionBar dock="top" title="My App" />
<GridLayout dock="bottom" columns="*, *, *" class="tab-bar">
<Button text="Home" col="0" />
<Button text="Search" col="1" />
<Button text="Profile" col="2" />
</GridLayout>
<ScrollView>
<StackLayout>
<!-- Main content -->
</StackLayout>
</ScrollView>
</DockLayout>
WrapLayout¶
<!-- Horizontal wrap -->
<WrapLayout orientation="horizontal" itemWidth="100" itemHeight="100">
<Label text="1" class="item" />
<Label text="2" class="item" />
<Label text="3" class="item" />
<Label text="4" class="item" />
<Label text="5" class="item" />
</WrapLayout>
<!-- Vertical wrap -->
<WrapLayout orientation="vertical" itemWidth="150" itemHeight="50">
<Button text="Button 1" />
<Button text="Button 2" />
<Button text="Button 3" />
<Button text="Button 4" />
</WrapLayout>
Navigation¶
Frame Navigation¶
import { Frame, topmost } from '@nativescript/core';
// Basic navigation
export function navigateToPage() {
topmost().navigate('details-page');
}
// Navigation with context
export function navigateWithData() {
topmost().navigate({
moduleName: 'details-page',
context: {
id: 123,
title: 'Sample Item',
data: { key: 'value' }
}
});
}
// Navigation with transition
export function navigateWithTransition() {
topmost().navigate({
moduleName: 'details-page',
animated: true,
transition: {
name: 'slideLeft',
duration: 300,
curve: 'easeIn'
}
});
}
// Back navigation
export function goBack() {
if (topmost().canGoBack()) {
topmost().goBack();
}
}
// Clear history
export function navigateAndClearHistory() {
topmost().navigate({
moduleName: 'home-page',
clearHistory: true
});
}
Modal Navigation¶
// Show modal
export function showModal() {
const frame = topmost();
frame.showModal({
moduleName: 'modal-page',
context: { message: 'Hello from modal' },
fullscreen: false,
animated: true,
stretched: false
}).then((result) => {
console.log('Modal closed with result:', result);
});
}
// Close modal with result
export function closeModal() {
const frame = topmost();
frame.closeModal('Modal result data');
}
// Modal page setup
// modal-page.js
export function onShownModally(args) {
const page = args.object;
const context = args.context;
page.bindingContext = fromObject({
message: context.message,
closeModal: function() {
page.closeModal('Data from modal');
}
});
}
Tab Navigation¶
<!-- TabView navigation -->
<TabView selectedIndex="{{ selectedTab }}" selectedIndexChanged="{{ onTabChanged }}">
<TabViewItem title="Home" iconSource="res://ic_home">
<Frame defaultPage="home-page" />
</TabViewItem>
<TabViewItem title="Search" iconSource="res://ic_search">
<Frame defaultPage="search-page" />
</TabViewItem>
<TabViewItem title="Profile" iconSource="res://ic_profile">
<Frame defaultPage="profile-page" />
</TabViewItem>
</TabView>
Drawer Navigation¶
<!-- RadSideDrawer (requires plugin) -->
<nsDrawer:RadSideDrawer xmlns:nsDrawer="@nativescript/ui-sidedrawer">
<nsDrawer:RadSideDrawer.drawerContent>
<StackLayout class="drawer-content">
<Label text="Menu" class="drawer-header" />
<Button text="Home" tap="{{ navigateToHome }}" />
<Button text="Settings" tap="{{ navigateToSettings }}" />
<Button text="About" tap="{{ navigateToAbout }}" />
</StackLayout>
</nsDrawer:RadSideDrawer.drawerContent>
<nsDrawer:RadSideDrawer.mainContent>
<Frame defaultPage="main-page" />
</nsDrawer:RadSideDrawer.mainContent>
</nsDrawer:RadSideDrawer>
Data Binding¶
One-Way Binding¶
<!-- Text binding -->
<Label text="{{ message }}" />
<Label text="{{ 'Hello ' + name }}" />
<!-- Property binding -->
<Button isEnabled="{{ canSubmit }}" />
<Image src="{{ imageUrl }}" />
<ActivityIndicator busy="{{ isLoading }}" />
<!-- Conditional binding -->
<Label text="{{ isLoggedIn ? 'Welcome' : 'Please login' }}" />
<Button visibility="{{ hasData ? 'visible' : 'collapsed' }}" />
Two-Way Binding¶
<!-- TextField two-way binding -->
<TextField text="{{ username, mode=twoWay }}" />
<Slider value="{{ volume, mode=twoWay }}" />
<Switch checked="{{ isEnabled, mode=twoWay }}" />
<DatePicker date="{{ selectedDate, mode=twoWay }}" />
Event Binding¶
<!-- Event handlers -->
<Button text="Click Me" tap="{{ onButtonTap }}" />
<TextField text="{{ username }}" textChange="{{ onTextChange }}" />
<ListView items="{{ items }}" itemTap="{{ onItemTap }}" />
<SearchBar text="{{ query }}" submit="{{ onSearch }}" clear="{{ onClear }}" />
List Binding¶
// View model with observable array
import { Observable, ObservableArray } from '@nativescript/core';
export function createViewModel() {
const viewModel = new Observable();
const items = new ObservableArray([
{ id: 1, name: 'Item 1', description: 'First item' },
{ id: 2, name: 'Item 2', description: 'Second item' },
{ id: 3, name: 'Item 3', description: 'Third item' }
]);
viewModel.set('items', items);
viewModel.set('addItem', function() {
const newItem = {
id: items.length + 1,
name: `Item ${items.length + 1}`,
description: `Item number ${items.length + 1}`
};
items.push(newItem);
});
viewModel.set('removeItem', function(index) {
items.splice(index, 1);
});
viewModel.set('onItemTap', function(args) {
const item = items.getItem(args.index);
console.log('Tapped item:', item.name);
});
return viewModel;
}
<!-- List with binding -->
<StackLayout>
<Button text="Add Item" tap="{{ addItem }}" />
<ListView items="{{ items }}" itemTap="{{ onItemTap }}">
<ListView.itemTemplate>
<GridLayout columns="*, auto" class="list-item">
<StackLayout col="0">
<Label text="{{ name }}" class="item-name" />
<Label text="{{ description }}" class="item-description" />
</StackLayout>
<Button text="Delete" col="1" tap="{{ $parents['Page'].removeItem, $index }}" />
</GridLayout>
</ListView.itemTemplate>
</ListView>
</StackLayout>
Styling¶
CSS Styling¶
/* app.css - Global styles */
/* Import NativeScript theme */
@import '@nativescript/theme/css/core.css';
@import '@nativescript/theme/css/default.css';
/* Global styles */
.page {
background-color: #f0f0f0;
}
.action-bar {
background-color: #3f51b5;
color: white;
}
/* Typography */
.h1 {
font-size: 24;
font-weight: bold;
color: #333;
}
.h2 {
font-size: 20;
font-weight: bold;
color: #666;
}
.body {
font-size: 16;
color: #333;
}
/* Buttons */
.btn {
font-size: 16;
padding: 12;
margin: 8;
border-radius: 4;
}
.btn-primary {
background-color: #3f51b5;
color: white;
}
.btn-secondary {
background-color: #f0f0f0;
color: #333;
border-width: 1;
border-color: #ccc;
}
/* Layout helpers */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.m-10 {
margin: 10;
}
.p-10 {
padding: 10;
}
/* List styles */
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: #e0e0e0;
}
.list-item:active {
background-color: #f5f5f5;
}
/* Form styles */
.form-field {
margin: 8;
padding: 12;
border-width: 1;
border-color: #ccc;
border-radius: 4;
}
.form-field:focus {
border-color: #3f51b5;
}
/* Platform-specific styles */
.android .action-bar {
height: 56;
}
.ios .action-bar {
height: 44;
}
/* Responsive styles */
@media (orientation: landscape) {
.container {
flex-direction: row;
}
}
SCSS Support¶
// app.scss
@import '@nativescript/theme/scss/variables/blue';
@import '@nativescript/theme/scss/core';
@import '@nativescript/theme/scss/default';
// Variables
$primary-color: #3f51b5;
$secondary-color: #f0f0f0;
$text-color: #333;
$border-color: #e0e0e0;
// Mixins
@mixin button-style($bg-color, $text-color) {
background-color: $bg-color;
color: $text-color;
padding: 12;
border-radius: 4;
font-size: 16;
}
@mixin card-style {
background-color: white;
border-radius: 8;
elevation: 2;
margin: 8;
padding: 16;
}
// Styles
.page {
background-color: $secondary-color;
}
.btn-primary {
@include button-style($primary-color, white);
}
.btn-secondary {
@include button-style($secondary-color, $text-color);
border-width: 1;
border-color: $border-color;
}
.card {
@include card-style;
}
// Nested styles
.list-container {
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: $border-color;
.item-title {
font-size: 18;
font-weight: bold;
color: $text-color;
}
.item-subtitle {
font-size: 14;
color: lighten($text-color, 20%);
}
}
}
Platform-Specific Styling¶
/* Platform-specific CSS */
.android .action-bar {
background-color: #3f51b5;
color: white;
height: 56;
}
.ios .action-bar {
background-color: #007aff;
color: white;
height: 44;
}
/* Device-specific styles */
.phone .container {
padding: 16;
}
.tablet .container {
padding: 32;
max-width: 600;
horizontal-align: center;
}
/* Orientation-specific styles */
.portrait .layout {
flex-direction: column;
}
.landscape .layout {
flex-direction: row;
}
Dynamic Styling¶
// Dynamic styling in code
import { Color } from '@nativescript/core';
export function applyDynamicStyles(view, theme) {
if (theme === 'dark') {
view.backgroundColor = new Color('#333');
view.color = new Color('#fff');
} else {
view.backgroundColor = new Color('#fff');
view.color = new Color('#333');
}
}
// CSS class manipulation
export function toggleTheme(view) {
if (view.className.includes('dark-theme')) {
view.className = view.className.replace('dark-theme', 'light-theme');
} else {
view.className = view.className.replace('light-theme', 'dark-theme');
}
}
// Style animation
import { Animation } from '@nativescript/core';
export function animateButton(button) {
const animation = new Animation([{
target: button,
scale: { x: 1.2, y: 1.2 },
duration: 200
}, {
target: button,
scale: { x: 1, y: 1 },
duration: 200
}]);
animation.play();
}
Platform APIs¶
Device Information¶
import { Device, Screen, isAndroid, isIOS } from '@nativescript/core';
// Device information
console.log('Device Type:', Device.deviceType);
console.log('OS:', Device.os);
console.log('OS Version:', Device.osVersion);
console.log('Model:', Device.model);
console.log('Manufacturer:', Device.manufacturer);
console.log('UUID:', Device.uuid);
console.log('Language:', Device.language);
console.log('Region:', Device.region);
// Screen information
console.log('Screen Width:', Screen.mainScreen.widthDIPs);
console.log('Screen Height:', Screen.mainScreen.heightDIPs);
console.log('Screen Scale:', Screen.mainScreen.scale);
// Platform detection
if (isAndroid) {
console.log('Running on Android');
// Android-specific code
} else if (isIOS) {
console.log('Running on iOS');
// iOS-specific code
}
// Platform-specific API access
if (isAndroid) {
const context = Application.android.context;
const packageManager = context.getPackageManager();
// Use Android APIs
} else if (isIOS) {
const currentDevice = UIDevice.currentDevice;
// Use iOS APIs
}
File System¶
import { File, Folder, knownFolders, path } from '@nativescript/core';
// Known folders
const documentsFolder = knownFolders.documents();
const tempFolder = knownFolders.temp();
const appFolder = knownFolders.currentApp();
// Create file
const file = documentsFolder.getFile('data.txt');
// Write to file
file.writeText('Hello, NativeScript!')
.then(() => {
console.log('File written successfully');
})
.catch((error) => {
console.error('Error writing file:', error);
});
// Read from file
file.readText()
.then((content) => {
console.log('File content:', content);
})
.catch((error) => {
console.error('Error reading file:', error);
});
// Create folder
const folder = documentsFolder.getFolder('myFolder');
// List folder contents
folder.getEntities()
.then((entities) => {
entities.forEach((entity) => {
console.log('Entity:', entity.name, entity.path);
});
});
// File operations
const sourceFile = documentsFolder.getFile('source.txt');
const targetFile = documentsFolder.getFile('target.txt');
// Copy file
sourceFile.copy(targetFile.path)
.then(() => {
console.log('File copied');
});
// Move file
sourceFile.move(targetFile.path)
.then(() => {
console.log('File moved');
});
// Delete file
file.remove()
.then(() => {
console.log('File deleted');
});
// Check if file exists
if (file.exists) {
console.log('File exists');
}
// Get file info
console.log('File size:', file.size);
console.log('Last modified:', file.lastModified);
HTTP Requests¶
import { Http } from '@nativescript/core';
// GET request
Http.getJSON('https://api.example.com/users')
.then((result) => {
console.log('Users:', result);
})
.catch((error) => {
console.error('Error:', error);
});
// POST request
Http.request({
url: 'https://api.example.com/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
content: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then((response) => {
console.log('Response:', response.content.toJSON());
})
.catch((error) => {
console.error('Error:', error);
});
// File upload
Http.request({
url: 'https://api.example.com/upload',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data'
},
content: {
file: file,
description: 'Uploaded file'
}
})
.then((response) => {
console.log('Upload successful');
})
.catch((error) => {
console.error('Upload failed:', error);
});
// Download file
Http.getFile('https://example.com/file.pdf', documentsFolder.path + '/downloaded.pdf')
.then((file) => {
console.log('File downloaded:', file.path);
})
.catch((error) => {
console.error('Download failed:', error);
});
Local Storage¶
import { ApplicationSettings } from '@nativescript/core';
// Store data
ApplicationSettings.setString('username', 'john_doe');
ApplicationSettings.setNumber('userId', 123);
ApplicationSettings.setBoolean('isLoggedIn', true);
// Retrieve data
const username = ApplicationSettings.getString('username', 'default_user');
const userId = ApplicationSettings.getNumber('userId', 0);
const isLoggedIn = ApplicationSettings.getBoolean('isLoggedIn', false);
// Remove data
ApplicationSettings.remove('username');
// Check if key exists
if (ApplicationSettings.hasKey('username')) {
console.log('Username exists');
}
// Clear all data
ApplicationSettings.clear();
// Store complex objects
const user = {
id: 123,
name: 'John Doe',
email: 'john@example.com'
};
ApplicationSettings.setString('user', JSON.stringify(user));
// Retrieve complex objects
const storedUser = JSON.parse(ApplicationSettings.getString('user', '{}'));
Plugins¶
Core Plugins¶
# Camera plugin
ns plugin add @nativescript/camera
# Geolocation plugin
ns plugin add @nativescript/geolocation
# Local notifications
ns plugin add @nativescript/local-notifications
# Social share
ns plugin add @nativescript/social-share
# Email plugin
ns plugin add @nativescript/email
# Phone plugin
ns plugin add @nativescript/phone
# Fingerprint authentication
ns plugin add @nativescript/fingerprint-auth
# Secure storage
ns plugin add @nativescript/secure-storage
# Background HTTP
ns plugin add @nativescript/background-http
Using Camera Plugin¶
import { Camera, requestPermissions } from '@nativescript/camera';
export function takePicture() {
requestPermissions()
.then(() => {
const options = {
width: 300,
height: 300,
keepAspectRatio: true,
saveToGallery: false
};
return Camera.takePicture(options);
})
.then((imageAsset) => {
console.log('Picture taken:', imageAsset);
// Use the image
const image = new Image();
image.src = imageAsset;
})
.catch((error) => {
console.error('Camera error:', error);
});
}
export function selectFromGallery() {
const options = {
width: 300,
height: 300,
keepAspectRatio: true
};
Camera.requestPermissions()
.then(() => {
return Camera.selectFromGallery(options);
})
.then((imageAsset) => {
console.log('Image selected:', imageAsset);
})
.catch((error) => {
console.error('Gallery error:', error);
});
}
Using Geolocation Plugin¶
import { Geolocation, CoreTypes } from '@nativescript/geolocation';
export function getCurrentLocation() {
const options = {
desiredAccuracy: CoreTypes.Accuracy.high,
maximumAge: 5000,
timeout: 10000
};
Geolocation.getCurrentLocation(options)
.then((location) => {
console.log('Location:', location);
console.log('Latitude:', location.latitude);
console.log('Longitude:', location.longitude);
console.log('Altitude:', location.altitude);
console.log('Speed:', location.speed);
})
.catch((error) => {
console.error('Location error:', error);
});
}
export function watchLocation() {
const options = {
desiredAccuracy: CoreTypes.Accuracy.high,
updateDistance: 10,
minimumUpdateTime: 1000
};
const watchId = Geolocation.watchLocation(
(location) => {
console.log('Location update:', location);
},
(error) => {
console.error('Location error:', error);
},
options
);
// Stop watching
setTimeout(() => {
Geolocation.clearWatch(watchId);
}, 30000);
}
Custom Plugin Development¶
// Create custom plugin
// my-plugin/index.js
export class MyPlugin {
static getMessage() {
return 'Hello from custom plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// Plugin logic here
setTimeout(() => {
resolve(`Action completed with: ${data}`);
}, 1000);
});
}
}
// my-plugin/index.android.js
export class MyPlugin {
static getMessage() {
// Android-specific implementation
return 'Hello from Android plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// Android-specific logic
const context = Application.android.context;
// Use Android APIs
resolve(`Android action completed with: ${data}`);
});
}
}
// my-plugin/index.ios.js
export class MyPlugin {
static getMessage() {
// iOS-specific implementation
return 'Hello from iOS plugin!';
}
static performAction(data) {
return new Promise((resolve, reject) => {
// iOS-specific logic
// Use iOS APIs
resolve(`iOS action completed with: ${data}`);
});
}
}
// Usage
import { MyPlugin } from './my-plugin';
console.log(MyPlugin.getMessage());
MyPlugin.performAction('test data')
.then((result) => {
console.log('Plugin result:', result);
})
.catch((error) => {
console.error('Plugin error:', error);
});
Angular Integration¶
Setup Angular Project¶
# Create Angular project
ns create MyAngularApp --ng
# Add Angular dependencies
cd MyAngularApp
npm install @angular/core @angular/common @angular/forms @angular/router
# Generate components
ng generate component home
ng generate component details
ng generate service data
App Module¶
// app.module.ts
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptModule } from '@nativescript/angular';
import { NativeScriptRouterModule } from '@nativescript/angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { DetailsComponent } from './details/details.component';
@NgModule({
bootstrap: [AppComponent],
imports: [
NativeScriptModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
DetailsComponent
],
providers: [],
schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }
Routing¶
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { NativeScriptRouterModule } from '@nativescript/angular';
import { HomeComponent } from './home/home.component';
import { DetailsComponent } from './details/details.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'details/:id', component: DetailsComponent }
];
@NgModule({
imports: [NativeScriptRouterModule.forRoot(routes)],
exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }
Component¶
// home.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { DataService } from '../data.service';
@Component({
selector: 'ns-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
items: any[] = [];
isLoading = false;
constructor(
private router: Router,
private dataService: DataService
) { }
ngOnInit(): void {
this.loadData();
}
loadData(): void {
this.isLoading = true;
this.dataService.getItems()
.subscribe(
(data) => {
this.items = data;
this.isLoading = false;
},
(error) => {
console.error('Error loading data:', error);
this.isLoading = false;
}
);
}
onItemTap(item: any): void {
this.router.navigate(['/details', item.id]);
}
onRefresh(): void {
this.loadData();
}
}
<!-- home.component.html -->
<ActionBar title="Home" class="action-bar">
<ActionItem text="Refresh" (tap)="onRefresh()" ios.position="right"></ActionItem>
</ActionBar>
<GridLayout rows="*">
<ActivityIndicator [busy]="isLoading" *ngIf="isLoading"></ActivityIndicator>
<ListView [items]="items" (itemTap)="onItemTap($event.object.bindingContext)" *ngIf="!isLoading">
<ng-template let-item="item">
<GridLayout columns="auto, *" rows="auto, auto" class="list-item">
<Label [text]="item.title" row="0" col="1" class="item-title"></Label>
<Label [text]="item.description" row="1" col="1" class="item-description"></Label>
</GridLayout>
</ng-template>
</ListView>
</GridLayout>
Service¶
// data.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Http } from '@nativescript/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com';
constructor() { }
getItems(): Observable<any[]> {
return new Observable((observer) => {
Http.getJSON(`${this.apiUrl}/items`)
.then((data: any[]) => {
observer.next(data);
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
getItem(id: number): Observable<any> {
return new Observable((observer) => {
Http.getJSON(`${this.apiUrl}/items/${id}`)
.then((data: any) => {
observer.next(data);
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
createItem(item: any): Observable<any> {
return new Observable((observer) => {
Http.request({
url: `${this.apiUrl}/items`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
content: JSON.stringify(item)
})
.then((response) => {
observer.next(response.content.toJSON());
observer.complete();
})
.catch((error) => {
observer.error(error);
});
});
}
}
Vue.js Integration¶
Setup Vue Project¶
# Create Vue project
ns create MyVueApp --vue
# Add Vue dependencies
cd MyVueApp
npm install vue@next @vue/runtime-core
Main App¶
// app.js
import { createApp } from 'nativescript-vue';
import Home from './components/Home.vue';
const app = createApp(Home);
app.start();
Vue Component¶
<!-- components/Home.vue -->
<template>
<Frame>
<Page>
<ActionBar title="Home" />
<GridLayout rows="auto, *">
<StackLayout row="0" class="form">
<TextField v-model="newItem" hint="Enter new item" />
<Button text="Add Item" @tap="addItem" class="btn btn-primary" />
</StackLayout>
<ListView row="1" :items="items" @itemTap="onItemTap">
<template #default="{ item, index }">
<GridLayout columns="*, auto" class="list-item">
<Label col="0" :text="item.name" class="item-name" />
<Button col="1" text="Delete" @tap="deleteItem(index)" class="btn btn-secondary" />
</GridLayout>
</template>
</ListView>
</GridLayout>
</Page>
</Frame>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const newItem = ref('');
const items = reactive([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
const addItem = () => {
if (newItem.value.trim()) {
items.push({
id: Date.now(),
name: newItem.value
});
newItem.value = '';
}
};
const deleteItem = (index) => {
items.splice(index, 1);
};
const onItemTap = (event) => {
const item = event.item;
console.log('Tapped item:', item.name);
};
return {
newItem,
items,
addItem,
deleteItem,
onItemTap
};
}
};
</script>
<style scoped>
.form {
padding: 20;
}
.list-item {
padding: 16;
border-bottom-width: 1;
border-bottom-color: #e0e0e0;
}
.item-name {
font-size: 16;
vertical-align: center;
}
.btn {
margin: 8;
}
</style>
Vue Router¶
// router.js
import { createRouter } from 'nativescript-vue';
import Home from './components/Home.vue';
import Details from './components/Details.vue';
const router = createRouter({
routes: [
{ path: '/', component: Home },
{ path: '/details/:id', component: Details }
]
});
export default router;
React Integration¶
Setup React Project¶
# Create React project
ns create MyReactApp --react
# Add React dependencies
cd MyReactApp
npm install react react-nativescript
App Component¶
// app.jsx
import React from 'react';
import { StackNavigator } from 'react-nativescript-navigation';
import { Home } from './components/Home';
import { Details } from './components/Details';
const AppContainer = StackNavigator(
{
Home: {
screen: Home
},
Details: {
screen: Details
}
},
{
initialRouteName: 'Home'
}
);
export default AppContainer;
React Component¶
// components/Home.jsx
import React, { useState, useEffect } from 'react';
export function Home({ navigation }) {
const [items, setItems] = useState([]);
const [newItem, setNewItem] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setIsLoading(true);
try {
// Simulate API call
const data = [
{ id: 1, name: 'Item 1', description: 'First item' },
{ id: 2, name: 'Item 2', description: 'Second item' },
{ id: 3, name: 'Item 3', description: 'Third item' }
];
setItems(data);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setIsLoading(false);
}
};
const addItem = () => {
if (newItem.trim()) {
const item = {
id: Date.now(),
name: newItem,
description: `Description for ${newItem}`
};
setItems([...items, item]);
setNewItem('');
}
};
const deleteItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
const navigateToDetails = (item) => {
navigation.navigate('Details', { item });
};
return (
<page>
<actionBar title="Home" />
<gridLayout rows="auto, *">
<stackLayout row={0} className="form">
<textField
text={newItem}
onTextChange={(args) => setNewItem(args.value)}
hint="Enter new item"
/>
<button
text="Add Item"
onTap={addItem}
className="btn btn-primary"
/>
</stackLayout>
{isLoading ? (
<activityIndicator row={1} busy={true} />
) : (
<listView
row={1}
items={items}
onItemTap={(args) => navigateToDetails(args.item)}
cellFactory={(item) => (
<gridLayout columns="*, auto" className="list-item">
<stackLayout col={0}>
<label text={item.name} className="item-name" />
<label text={item.description} className="item-description" />
</stackLayout>
<button
col={1}
text="Delete"
onTap={() => deleteItem(item.id)}
className="btn btn-secondary"
/>
</gridLayout>
)}
/>
)}
</gridLayout>
</page>
);
}
React Hooks¶
// hooks/useApi.js
import { useState, useEffect } from 'react';
import { Http } from '@nativescript/core';
export function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const result = await Http.getJSON(url);
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in component
export function DataComponent() {
const { data, loading, error } = useApi('https://api.example.com/data');
if (loading) return <activityIndicator busy={true} />;
if (error) return <label text={`Error: ${error.message}`} />;
return (
<listView
items={data}
cellFactory={(item) => (
<label text={item.name} />
)}
/>
);
}
Testing¶
Unit Testing¶
# Install testing dependencies
npm install --save-dev @nativescript/unit-test-runner
npm install --save-dev jasmine
npm install --save-dev karma
npm install --save-dev karma-jasmine
npm install --save-dev karma-nativescript-launcher
# Run tests
ns test android
ns test ios
Test Configuration¶
// karma.conf.js
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'app/**/*.spec.js'
],
exclude: [],
preprocessors: {},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['NativeScript'],
singleRun: true,
concurrency: Infinity
});
};
Unit Test Example¶
// tests/example.spec.js
describe('Calculator', function() {
let calculator;
beforeEach(function() {
calculator = new Calculator();
});
it('should add two numbers correctly', function() {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('should subtract two numbers correctly', function() {
const result = calculator.subtract(5, 3);
expect(result).toBe(2);
});
it('should multiply two numbers correctly', function() {
const result = calculator.multiply(4, 3);
expect(result).toBe(12);
});
it('should divide two numbers correctly', function() {
const result = calculator.divide(10, 2);
expect(result).toBe(5);
});
it('should throw error when dividing by zero', function() {
expect(function() {
calculator.divide(10, 0);
}).toThrow();
});
});
// Calculator class
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
E2E Testing¶
# Install Appium
npm install --save-dev appium
npm install --save-dev wd
# Install test framework
npm install --save-dev mocha
npm install --save-dev chai
// e2e/test.spec.js
const wd = require('wd');
const { expect } = require('chai');
describe('App E2E Tests', function() {
let driver;
before(async function() {
this.timeout(60000);
driver = wd.promiseChainRemote('localhost', 4723);
const desiredCaps = {
platformName: 'Android',
platformVersion: '10',
deviceName: 'emulator-5554',
app: './platforms/android/app/build/outputs/apk/debug/app-debug.apk',
automationName: 'UiAutomator2'
};
await driver.init(desiredCaps);
});
after(async function() {
if (driver) {
await driver.quit();
}
});
it('should display home screen', async function() {
const title = await driver.elementByAccessibilityId('home-title');
const titleText = await title.text();
expect(titleText).to.equal('Home');
});
it('should navigate to details screen', async function() {
const button = await driver.elementByAccessibilityId('details-button');
await button.tap();
const detailsTitle = await driver.elementByAccessibilityId('details-title');
const titleText = await detailsTitle.text();
expect(titleText).to.equal('Details');
});
it('should add new item', async function() {
const textField = await driver.elementByAccessibilityId('new-item-field');
await textField.sendKeys('Test Item');
const addButton = await driver.elementByAccessibilityId('add-button');
await addButton.tap();
const listItem = await driver.elementByAccessibilityId('list-item-0');
const itemText = await listItem.text();
expect(itemText).to.contain('Test Item');
});
});
Performance¶
Memory Management¶
// Proper cleanup
export function onNavigatingFrom(args) {
const page = args.object;
// Remove event listeners
if (page.bindingContext && page.bindingContext.cleanup) {
page.bindingContext.cleanup();
}
// Clear timers
if (page._timer) {
clearInterval(page._timer);
page._timer = null;
}
// Clear references
page.bindingContext = null;
}
// Observable cleanup
class ViewModel extends Observable {
constructor() {
super();
this._subscriptions = [];
}
addSubscription(subscription) {
this._subscriptions.push(subscription);
}
cleanup() {
this._subscriptions.forEach(sub => {
if (sub && sub.unsubscribe) {
sub.unsubscribe();
}
});
this._subscriptions = [];
}
}
Image Optimization¶
// Lazy loading images
export function onImageLoading(args) {
const image = args.object;
const listView = image.parent;
// Only load images for visible items
if (listView && listView.isItemAtIndexVisible) {
const index = listView.getItemAtIndex(args.index);
if (listView.isItemAtIndexVisible(index)) {
image.src = args.item.imageUrl;
}
}
}
// Image caching
class ImageCache {
constructor() {
this.cache = new Map();
this.maxSize = 50; // Maximum number of cached images
}
get(url) {
return this.cache.get(url);
}
set(url, imageSource) {
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(url, imageSource);
}
clear() {
this.cache.clear();
}
}
const imageCache = new ImageCache();
// Optimized image loading
export function loadOptimizedImage(imageView, url) {
// Check cache first
const cachedImage = imageCache.get(url);
if (cachedImage) {
imageView.imageSource = cachedImage;
return;
}
// Load and cache image
ImageSource.fromUrl(url)
.then((imageSource) => {
imageCache.set(url, imageSource);
imageView.imageSource = imageSource;
})
.catch((error) => {
console.error('Error loading image:', error);
});
}
List Performance¶
// Virtual scrolling for large lists
export function createVirtualList(container, items, itemHeight, visibleItems) {
let startIndex = 0;
let endIndex = Math.min(visibleItems, items.length);
const updateList = () => {
container.removeChildren();
for (let i = startIndex; i < endIndex; i++) {
const item = items[i];
const itemView = createItemView(item, i);
container.addChild(itemView);
}
};
const onScroll = (scrollY) => {
const newStartIndex = Math.floor(scrollY / itemHeight);
const newEndIndex = Math.min(newStartIndex + visibleItems, items.length);
if (newStartIndex !== startIndex || newEndIndex !== endIndex) {
startIndex = newStartIndex;
endIndex = newEndIndex;
updateList();
}
};
updateList();
return { updateList, onScroll };
}
// Efficient list item recycling
export function createRecyclingListView(items, itemTemplate) {
const recycledViews = [];
const activeViews = new Map();
const getRecycledView = () => {
return recycledViews.pop() || null;
};
const recycleView = (view) => {
recycledViews.push(view);
};
const createItemView = (item, index) => {
let view = getRecycledView();
if (!view) {
view = itemTemplate();
}
// Bind data to view
view.bindingContext = item;
activeViews.set(index, view);
return view;
};
const removeItemView = (index) => {
const view = activeViews.get(index);
if (view) {
activeViews.delete(index);
recycleView(view);
}
};
return { createItemView, removeItemView };
}
Deployment¶
Android Deployment¶
# Build debug APK
ns build android
# Build release APK
ns build android --release --key-store-path myapp.keystore --key-store-password password --key-store-alias myapp --key-store-alias-password password
# Generate keystore
keytool -genkey -v -keystore myapp.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000
# Build AAB (Android App Bundle)
ns build android --release --aab --key-store-path myapp.keystore --key-store-password password --key-store-alias myapp --key-store-alias-password password
# Upload to Google Play Console
# Use the generated AAB file in platforms/android/app/build/outputs/bundle/release/
iOS Deployment¶
# Build for device
ns build ios --for-device --release
# Build with provisioning profile
ns build ios --for-device --release --provision "MyApp Distribution Profile"
# Create IPA
ns build ios --for-device --release --provision "MyApp Distribution Profile" --team-id TEAM_ID
# Upload to App Store
# Use Xcode or Application Loader to upload the IPA
Continuous Integration¶
# .github/workflows/build.yml
name: Build and Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Install NativeScript CLI
run: npm install -g @nativescript/cli
- name: Add platforms
run: |
ns platform add android
ns platform add ios
- name: Build Android
run: ns build android --release
- name: Build iOS
run: ns build ios --for-device --release
- name: Run tests
run: ns test android --justlaunch
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: builds
path: |
platforms/android/app/build/outputs/apk/release/
platforms/ios/build/Release-iphoneos/
Best Practices¶
Project Structure¶
app/
├── core/ # Core functionality
│ ├── services/ # Business logic services
│ ├── models/ # Data models
│ ├── utils/ # Utility functions
│ └── constants/ # App constants
├── shared/ # Shared components
│ ├── components/ # Reusable UI components
│ ├── directives/ # Custom directives
│ └── pipes/ # Custom pipes
├── features/ # Feature modules
│ ├── home/ # Home feature
│ ├── profile/ # Profile feature
│ └── settings/ # Settings feature
├── assets/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── sounds/
└── styles/ # Global styles
├── _variables.scss
├── _mixins.scss
└── app.scss
Code Organization¶
// Use consistent naming conventions
// PascalCase for classes and constructors
class UserService {
constructor() {
this.users = [];
}
}
// camelCase for variables and functions
const userName = 'john_doe';
const getUserById = (id) => {
return users.find(user => user.id === id);
};
// UPPER_CASE for constants
const API_BASE_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;
// Use meaningful names
// Bad
const d = new Date();
const u = users.filter(x => x.a);
// Good
const currentDate = new Date();
const activeUsers = users.filter(user => user.isActive);
// Use consistent error handling
class ApiService {
async fetchData(url) {
try {
const response = await Http.getJSON(url);
return { success: true, data: response };
} catch (error) {
console.error('API Error:', error);
return { success: false, error: error.message };
}
}
}
// Use proper async/await patterns
// Bad
function loadUserData(userId) {
getUserById(userId)
.then(user => {
return getProfileData(user.id);
})
.then(profile => {
return getPreferences(profile.id);
})
.then(preferences => {
console.log('User data loaded');
})
.catch(error => {
console.error('Error:', error);
});
}
// Good
async function loadUserData(userId) {
try {
const user = await getUserById(userId);
const profile = await getProfileData(user.id);
const preferences = await getPreferences(profile.id);
console.log('User data loaded');
return { user, profile, preferences };
} catch (error) {
console.error('Error loading user data:', error);
throw error;
}
}
Performance Best Practices¶
// Optimize data binding
// Use one-time binding for static data
// XML: text="{{ message, mode=oneTime }}"
// Minimize property changes
class OptimizedViewModel extends Observable {
constructor() {
super();
this._batchUpdates = false;
this._pendingUpdates = {};
}
startBatch() {
this._batchUpdates = true;
}
endBatch() {
this._batchUpdates = false;
Object.keys(this._pendingUpdates).forEach(key => {
super.set(key, this._pendingUpdates[key]);
});
this._pendingUpdates = {};
}
set(name, value) {
if (this._batchUpdates) {
this._pendingUpdates[name] = value;
} else {
super.set(name, value);
}
}
}
// Use efficient list operations
// Bad
items.forEach((item, index) => {
if (item.shouldRemove) {
items.splice(index, 1);
}
});
// Good
const itemsToKeep = items.filter(item => !item.shouldRemove);
items.splice(0, items.length, ...itemsToKeep);
// Optimize image loading
const optimizeImageLoading = (imageView, url, placeholder) => {
// Show placeholder immediately
imageView.src = placeholder;
// Load actual image asynchronously
ImageSource.fromUrl(url)
.then(imageSource => {
imageView.imageSource = imageSource;
})
.catch(error => {
console.error('Image loading failed:', error);
// Keep placeholder or show error image
});
};
// Use proper cleanup
class ComponentManager {
constructor() {
this.components = new Map();
this.timers = new Set();
this.subscriptions = new Set();
}
addTimer(timer) {
this.timers.add(timer);
}
addSubscription(subscription) {
this.subscriptions.add(subscription);
}
cleanup() {
// Clear timers
this.timers.forEach(timer => clearTimeout(timer));
this.timers.clear();
// Unsubscribe from observables
this.subscriptions.forEach(sub => {
if (sub && sub.unsubscribe) {
sub.unsubscribe();
}
});
this.subscriptions.clear();
// Clear component references
this.components.clear();
}
}
Summary¶
NativeScript provides a powerful platform for building truly native mobile applications using web technologies:
Key Advantages: - True Native Performance: Direct access to native APIs without WebViews - Code Sharing: Share business logic across platforms while maintaining native UI - Framework Flexibility: Support for Angular, Vue.js, React, and vanilla JavaScript - Native UI: Platform-specific UI components that look and feel native - Plugin Ecosystem: Rich ecosystem of plugins for native functionality
Best Use Cases: - Apps requiring native performance and UI - Cross-platform apps with complex business logic - Teams with web development expertise - Apps needing extensive native API access - Enterprise applications with custom requirements
Considerations: - Steeper learning curve than hybrid frameworks - Larger app size compared to web-based solutions - Platform-specific testing required - Need to understand native platform concepts
NativeScript is ideal for developers who want to leverage web technologies while building truly native mobile applications with excellent performance and platform integration.