Как мы отбираем хлеб у мобильных разработчиков

Сергей Мелашич
Agilie

Обеспечиваем полный цикл разработки
от дизайна до тестирования

Отделы Front-end, Back-end, iOS и Android разработки

Более 9 лет на рынке,
офисы в Днепре и Запорожье

Немного о тенденциях

Javascript разработчики могли бы захватить
мир, если бы заказчики правки по проектам
не прислали

(золотой фонд цитат Джейсона Стетхема)

Наше преимущество?

  JavaScript

Кросс-платформенные решения

Mobile OS functionality

Plugins

Native Libs

WebView JS Bridge

JavaScript

Html, CSS

Browser

Легкий и быстрый старт

Поддержка множества платформ

Доступ к основным встроенным функциям

Работа в автономном режиме

Огромное сообщество

Никаких ограничений по UI

 

Нет доступа к расширенным встроенным функциям

Сложности обработки данных

Mobile OS functionality

API Meta

Native Libs

JS to Native Bridge

JavaScript

UI Layout & Styles

JavaScript Engine

Повышенная производительность

 

 

 

 

 

 

 

Работа в UI потоке

Есть ограничения по UI

Общие абстракции над платформами
UI компоненты компилируются в нативные

Метаданные
Возможность прямой работы с нативным кодом

Довольно большое сообщество

Итак, почему NativeScript?

Негативный опыт с Cordova в прошлом

ReactJS не входит в наш технологический стек

Интеграция с Angular "из коробки"

Marketplace и готовые решения

Большое сообщество в Slack

$55,838M

$39,128M

$88M

Разработка компании Telerik

Ваше первое приложение

<ActionBar text="Dashboard"></ActionBar>

<GridLayout rows="160, 80, *">

    <app-expenses-chart row="0">
    </app-expenses-chart>

    <Label [text]="balance.income" 
            row="1">
    </Label>

    <Button text="Sign In"
            row="2">
    </Button>

</GridLayout>
$ tns create MyFirstApp
<DatePicker loaded="onDatePickerLoaded" 
            day="20" 
            month="04" 
            year="1980">
</DatePicker>

Визуальные
компоненты

import { 
  confirm, 
  ConfirmOptions
} from "tns-core-modules/ui/dialogs";


const options: ConfirmOptions = {
    title: 'Confirm',
    message: 'Are you sure?',
    okButtonText: 'Ok',
    cancelButtonText: 'Cancel',
}

confirm(options).then(handleResponse);

Визуальные
компоненты

Нативные функции

import { Image } from "tns-core-modules/ui/image";
import * as camera from "nativescript-camera";

camera.takePicture()
  .then((imageAsset) => {
        const image = new Image();
        image.src = imageAsset;
    })
  .catch(handleError);
import * as geolocation from "nativescript-geolocation";
import { Accuracy } from "tns-core-modules/ui/enums";

geolocation.getCurrentLocation({
            desiredAccuracy: Accuracy.high,
            maximumAge: 5000,
            timeout: 10000
        })
        .then(loc => {
          handleLocation(loc)
        })
        .catch(handleError);

const clipboard = require("nativescript-clipboard");

clipboard.setText(someContent)
  .then(() => {
      console.log("Success");
  });

clipboard.getText()
  .then((content) => {
      console.log("Clipboard content: " + content);
  });

Производительность

Разметка и разметка

StackLayout &

GridLayout

AbsoluteLayout

Позиционировать можно только top и left

Нельзя позиционировать в относительных величинах

Как определять размеры?

Ни onNavigatedTo,
ни ngAfterViewInit не работают

Можно использовать setTimeout, но вы ведь не будете, правда?

Только loaded можно верить

<ScrollView>
  <GridLayout rows="auto * auto auto" 
              class="dark">

    <StackLayout row="0">
      <Image [src]="wallet.logo"></Image>
      <Label [text]="cashAmount(wallet) | currency">
      </Label>
      ...

    </StackLayout>
    ...

    <StackLayout row="1">
      ...
      <Button text="Submit"
              (loaded)="handleLoaded()"> 
      </Button>
    </StackLayout>

  </GridLayout>
</ScrollView>
<ul class="dropdown-menu">

  <li *ngFor="let option of options">
    <a href="#">
      <span [class]="option.type">
      </span> 
      
      {{ option.label }}
 
    </a>
  </li>

</ul>
<StackLayout class="dropdown-menu">

  <StackLayout 
    *ngFor="let option of options"
    orientation="horizontal">

    <Label [class]="option.type"></Label>
    <Label [text]="option.label"></Label>


  </StackLayout>

</StackLayout>

Dropdown - тудушник
в мире верстки

Layout должен быть максимально "плоским"

<AbsoluteLayout class="dropdown-menu">

   <ng-template ngFor let-option 
                [ngForOf]="options" 
                let-i="index">

       <Label [class]="option.type" 
              [marginTop]="i * 35">
       </Label>

       <Label [text]="option.label" 
              [marginTop]="i * 35">
       </Label>

   </ng-template>

</AbsoluteLayout>
<StackLayout class="dropdown-menu">

  <StackLayout 
    *ngFor="let option of options"
    orientation="horizontal">

    <Label [class]="option.type">
    </Label>
    

    <Label [text]="option.label">
    </Label>


  </StackLayout>

</StackLayout>
<DockLayout class="page" stretchLastChild="false">
  <StackLayout dock="top" class="card">
    <GridLayout columns="*, auto" rows="auto" class="card-h">
      <StackLayout col="0" row="0" verticalAlignment="center">
        <StackLayout orientation="horizontal">
          <Label text="Terminator" class="h3 title m-r-2">
          </Label>
          <Label text="(1984)" class="h3 caption"></Label>
        </StackLayout>
        <Label text="Action" class="h5 caption"></Label>
      </StackLayout>
      <Image col="1" row="0" 
             src="~/images/logo.png" class="img-circle">
      </Image>
    </GridLayout>
    <StackLayout class="hr-light"></StackLayout>
    <StackLayout class="card-body">
      <Label text="Lorem ipsum..."
             textWrap="true" class="body caption"></Label>
    </StackLayout>
  </StackLayout>
  <Button dock="bottom" text="Button"></Button>
</DockLayout>
<GridLayout columns="auto, *, auto" 
            rows="auto, auto, auto, auto" 
            dock="top" class="card">
    <Label col="0" row="0" 
           text="Terminator" 
           class="h3 title m-r-2 m-l-12 m-t-10">
    </Label>
    <Label col="1" row="0" text="(1984)" 
           class="h3 caption m-t-10">
    </Label>
    <Label col="0" row="1" text="Action" 
           class="h5 caption m-l-12 p-b-10">
    </Label>
    <Image col="2" row="0" rowSpan="2" 
           src="~/images/logo.png" 
           class="img-circle m-r-12" 
           verticalAlignment="center">
    </Image>
    <Label col="0" colSpan="3" row="2" 
           class="hr-light">
    </Label>
    <Label col="0" colSpan="3" row="3" 
           text="Lorem ipsum..." textWrap="true" 
           class="body caption m-x-12 p-y-10">
    </Label>
</GridLayout>

Нет, серьезно - МАКСИМАЛЬНО "плоским"

Используйте атрибуты вместо компонентов

<GridLayout>
   <StackLayout>
       <app-element></app-element>
   </StackLayout>
</GridLayout>
<GridLayout>
   <StackLayout class="wrapper-class" appElement>
   </StackLayout>
</GridLayout>
@Component({
  selector: 'app-element, [appElement]',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppElement {
  ...
}

Списки - еще одна головная боль

Используйте ngFor только, если все элементы списка умещаются на экране

И все равно не забывайте про trackBy

Для больших списков используйте ListView и RadListView

Не забывайте посматривать на
https://docs.nativescript.org/ui/professional-ui-components​ 

Переиспользование кода

Angular Schematics

$ ng add @nativescript/schematics

SharedProject

Web App

Mobile App

ng serve

npm run ios

npm run android

Переиспользуем сервисы

app.module.ts  

app.module.tns.ts  

HttpClientModule

NativeScriptHttpClientModule

HttpClient

Web App

Mobile App

export type StorageInterface = {
    set: (key: string, val: string) => void;
    get: (key: string) => string;
    delete: (key: string) => void;
    clear: () => void;
};
import {Injectable} from '@angular/core';
import {StorageInterface} from '~/app/interfaces';



@Injectable({
    providedIn: 'root'
})
export class StorageService implements StorageInterface {

    clear(): void {
        sessionStorage.clear();
    }

    delete(key: string): void {
        sessionStorage.removeItem(key);
    }

    get(key: string): string {
        return sessionStorage.getItem(key);
    }

    set(key: string, val: string): void {
        sessionStorage.setItem(key, val);
    }
}
import {Injectable} from '@angular/core';
import {StorageInterface} from '~/app/interfaces';

const cache = require('nativescript-cache');

@Injectable({
    providedIn: 'root'
})
export class StorageService implements StorageInterface {

    clear(): void {
        cache.clear();
    }

    delete(key: string): void {
        cache.delete(key);
    }

    get(key: string): string {
        return cache.get(key);
    }

    set(key: string, val: string): void {
        cache.set(key, val);
    }
}

Пишем сервисы

storage.service.ts  

storage.service.tns.ts  

Переиспользуем компоненты

Web App

Mobile App

name.component.html

name.component.tns.html

name.component.scss

name.component.tns.scss

name.component.class.ts

name.component.ts

name.component.tns.ts

Переиспользуем компоненты (Router)

import {InjectionToken} from '@angular/core';

export const UNIVERSAL_ROUTER = new InjectionToken<string>('Custom router');
import {Router} 
  from '@angular/router';
...

@NgModule({
  ...
  providers: [
    ...
    {
      provide: UNIVERSAL_ROUTER, 
      useExisting: Router
    },
  ]
})
export class AppModule {
  ...  
}
import {RouterExtensions} 
  from 'nativescript-angular/router';
...

@NgModule({
  ...  
  providers: [
    ...
    {
      provide: UNIVERSAL_ROUTER, 
      useClass: RouterExtensions
    }
  ]
})
export class AppModule { 
  ...
}

app.module.ts  

app.module.tns.ts  

Метаданные

Метаданные - соответствие между JavaScript и
Objective-C / Java кодом

const Toast = android.widget.Toast;

Toast
  .makeText(
    app.android.context, 
    message, 
    Toast.LENGTH_SHORT)
  .show();
import android.widget.Toast;

Toast
  .makeText(
    getApplicationContext(), 
    message, 
    Toast.LENGTH_SHORT)
  .show();
import {topmost} from 'tns-core-modules/ui/frame';

declare const CSToastManager: any;

CSToastManager.setDefaultDuration(2.0);
CSToastManager.setDefaultPosition('CSToastPositionBottom');

topmost().ios.controller.view.makeToast(message);
[CSToastManager setDefaultDuration:2.0];
[CSToastManager 
  setDefaultPosition:@"CSToastPositionBottom"];

[self.view makeToast:message];

Java

Objective C

JavaScript

JavaScript

Плагины

plugin.common.ts

plugin.ios.ts

plugin.android.ts

export abstract class Common {
  abstract property;
  abstract method();
}
export class PluginClass extends Common {

  property;

  method() {
    // iOS specific code
  };

}
export class PluginClass extends Common {

  property;

  method() {
    // Android specific code
  };

}

Вместо заключения

Мы не используем NativeScript, если в приложении задействованы низкоуровневые или специализированные API

Мы не используем NativeScript, если элементы дизайна слишком далеки от стандартных и сложнокастомизируемы

Использование NativeScript оправдано даже для одной платформы, если у вас есть специалисты по бизнес-логике приложения

Использование NativeScript оправдано, если дизайнеры согласовывают элементы дизайна с разработчиками

NativeScript хорошо зарекомендовал себя, обеспечивая поддержку начиная с Android 5 и iOS 9

Благодарю за внимание

Пожалуйста, разбудите спящих соседей

SergeyMell

Seroga.Mell

Sergey Melashych

sergey.mell@agilie.com

Made with Slides.com