Jak vyvíjet pro chytrou televizi a nerozbrečet se u toho (3/3)

smart TV development

Co vychytat při tvorbě architektury pro televizní aplikaci? S jakým stackem pracovat? V tomto posledním díle série si řekneme o konkrétních postupech, nástrojích a frameworcích. Taky se s vámi podělím o 7 tipů z terénu, které se vám při vývoji pro smart TV můžou hodit.

Už jste získali představu o tvorbě UI pro chytrou televizi. Minule jsme se zase podívali na uživatelskou cestu a důležité featury. Sérii završíme představením architektury pro TV aplikaci, povíme si o builderech a potenciálních problémech. Nakonec vám dám pár tipů z praxe, které by vám vývoj pro smart TV měly ulehčit.

Část 3 – Střípky z vývoje pro chytré televize

Bojový plán

Když vyřešíte ovládání, je čas přistoupit k výběru technologií, ve kterých budete vyvíjet.

Předem jsme věděli, že:

  • Budeme implementovat aplikaci pro televize Samsung (TizenTV) a LG (webOS). Minimální (vývojové) verze SDK obou platforem jsme s klientem ustálili na 3.0.
  • Chceme psát hlavně společný core aplikace, takže co nejméně specifického kódu pro každou z platforem.
  • Chceme, aby aplikace fungovala i na webu, případně se dala zprovoznit jako desktopová aplikace zapouzdřená v Electron containeru.

TizenTV i LG jsou založeny na současných webových technologiích, což pokrývá všechny varianty targetů a zařízení, na kterých by měla aplikace běžet. Zbývalo tedy zvolit vhodný javascript stack a začít s vývojem. Jako pilotní platformu pro vývoj nám klient zvolil TizenTV. My jsme odhadli, že na dodatečný port a úpravy pro webOS budeme potřebovat cca 10–15 % času navíc.
Tento postup se nakonec nicméně projevil jako nevhodný. Časem se totiž ukázalo, že:

  • Televize mají obecně nízký výpočetní výkon (řádově několikrát nižší než mobilní telefony), ale televize od LG (ve stejné cenové hladině) jsou pomalejší než televize od Samsungu.
  • TizenTV běží na a podporuje API Chrome/Chromium verze 47, webOS běží na Chrome 38.
  • webOS zdaleka nepodporuje všechny věci, které očekáváte od současných moderních prohlížečů. Pro srovnání; Chrome 38 vyšel v říjnu 2014, Chrome 47 v prosinci 2015.

Pro prvotní vývoj jsme tedy zvolili kompletnější platformu na rychlejších zařízeních, místo toho, abychom od začátku vyvíjeli na pomalejším zařízení a s méně kompletním API. Ještě lépe řečeno – místo toho, abychom ve druhém vývojovém cyklu upgradovali features pro rychlejší televize, museli jsme downgradovat a optimalizovat features pro pomalejší televize.

Stack

A teď už k samotnému stacku, jehož jádro dopadlo zhruba takhle:

  • React pro views
  • React Router pro navigaci mezi obrazovkami
  • MobX pro data stores
  • Webpack pro development/build tasky
  • Typescript, protože je cool (a podporuje datové typy, Interface, compile time check a další věci, které javascript dělají snesitelnější pro tohle tisíciletí <3)
  • SASS, protože už to roky nejde bez něj
  • Další “honorable mentions”:
    • crypto-js pro práci s šifrováním
    • pica pro hezčí výstupy v rámci <canvas>
    • fetch polyfill, protože máme tohle Promise-based API rádi a nechtěli jsme ho hodit přes palubu jen proto, že ho webOS 3.0 ještě nativně neumí

React jsme zvolili, protože v něm umí většina našich vývojářů a za ty roky jsme si na něj zvykli tak, že už nedokážeme jinak. Pořád si myslím, že dnům kouzelných rychlých mikroaplikací postavených na jQuery ještě úplně neodzvonilo, ale React nám v eManu přijde v tuto chvíli pro single page aplikace jako nejdospělejší, nejpřehlednější a nejlépe zdokumentované řešení.
Optimalizaci performance Reactu bychom mohli věnovat hodně času, ale to není účelem tohoto článku. Takže pouze pár nejdůležitějších postřehů:

  • Na televizi je obrovský rozdíl mezi produkčním a development buildem. Pokud testujete development build a přijde vám, že je aplikace pomalá, nepropadejte panice. V produkci bude pravděpodobně líp.
  • V paměti chcete držet co nejméně vytvořených React komponent, naopak jich chcete co nejvíce vytvářet v runtime. V řeči kódu to znamená, že chcete nahradit všechna React.createElement() volání vlastní wrapper funkcí, která ve vhodný čas rozhodne o tvorbě komponenty. V roce 2018 to asi není třeba příliš zdůrazňovat, ale Babel pluginy transform-react-inline-elements a transform-react-constant-elements jsou dobrým začátkem.
  • Pro kalkulace využijte Web Worker API a nechte React pracovat na hlavním vlákně, aby měl pro sebe co nejvíce prostoru. Vždy si ale na televizi ověřte, jestli to má reálný dopad na performance.
  • Nemusíte se vůbec obtěžovat s hoistingem komponent v rámci reusability. Paměti v televizi je tak málo, že to bude mít spíše negativní dopad na performance.
  • Na webu razíme teorii, že pokud nějakou část skriptu nepotřebujete, neměla by v aplikaci nutně být a je vhodné ji lazy-loadovat později. U televize platí opak – v případě, že budete později potřebovat nějaký skript nebo resource (ne React komponentu!), načtěte si to předem, než uživateli aplikaci ukážete. Uživatel si rád déle počká na spuštění aplikace, protože je hladový a těší se na ni. Nechcete ho už ale otravovat s lagy v rámci jejího běhu.

React v kombinace s MobXem je věc, která nám hodně přirostla k srdci už při vývoji webů. React má kouzelný @observer dekorátor, který zajistí, že kdykoliv se změní některé props, které komponenta observuje, MobX automaticky zařídí nový render cyklus dané komponenty. Pápá componentWillReceiveProps(), pápá shouldComponentUpdate(), bylo nám s vámi hezky, ale je čas jít dál.

V rámci rozhodování o stacku se do debaty vložili i zavilí příznivci starého dobrého Reduxu s argumentem, že akce a reducery jsou přehlednější a člověk vždycky ví, kde se co pokazilo. To je jistě pravda (a sám s tím souhlasím), ale množství boilerplate kódu, které je nutné napsat i v rámci malé datové operace, nám za poslední dva roky přišlo neúnosné. MobX má taky akce, pokud se důsledně anotují, MobX developer tools je snadno zobrazí a debugging je podobně pohodlný jako u Reduxu.

MVP architektura

Shodli jsme se na tom, že React + MobX je opravdu super. My jsme ale chtěli být ještě víc super, takže jsme museli udělat poslední krok navíc a představit dostatečně obecnou architekturu; ta mezi data a Views vmáčkne prostředníka, který vrstvy striktně oddělí. Ideálně ve smyslu jedna obrazovka = jeden kompaktní zapouzdřený celek. Jakkoliv totiž souhlasíme s Reactem jako skvělým pohledem na data, v kombinaci s MobXem nám vadí jeho těsná svázanost s observables.
Základní problémy, které vidíme jsou tyto:

  • React komponenta ví, že existuje nějaký MobX. Nemůžeme ji tedy znovu použít out-of-the box v případě, že se rozhodneme vyměnit MobX za Redux nebo Flux.
  • React komponenta zná identitu konkrétního MobX store. To je velký problém v případě, že budeme chtít komponentu testovat a posílat do ní dummy props (např. v podobě namockovaných JSON objektů). V tom případě musíme nejdřív vytvořit odpovídající MobX store.

Ideálním řešením tedy jsou:

  • Hloupé (ideálně funkcionální, stateless) komponenty, které jen zobrazují data. Původ dat musí být pro komponentu neznámý, musí být pouze schopna je zobrazit. Komponenta nesmí znát @observer, @observable, @computed, @inject, ani nic podobného.
  • MobX stores s vlastními doménami dat v rámci jasně dané sémantiky a s vlastní business logikou, která typicky data doplňuje na vyžádání.
  • Prostředníci, kteří budou sbírat data z různých stores a agregovat je, volat akce na storech a exponovat @observable proměnné pro views, jejichž přesnou identitu nesmějí znát.

Jako bonus jsme chtěli rozdělit props, které může React komponenta přijmout, do dvou skupin:

  • props, které přicházejí z datového zdroje (říkejme jim “props zdola”)
  • props, které přicházejí z nadřazené (high order) komponenty (“props shora”)

…přičemž interface každé z těchto kategorií by měla znát jen objekt/komponenta, která je může nabídnout. Tedy datový zdroj (prostředník, napojený na MobX store) zná props související s čistými daty a nadřazená React komponenta, která renderuje naši child komponentu, je od informací o datech odstíněna. Posílá do ní typicky jen informace o UI, například jakou má mít subkomponenta barvu a podobně.

Problémy s identitami řeší do velké míry samotný Typescript, stačí všude pracovat s Interfaces a ne s reálnými třídami. Prostředníka mezi View (React komponentou) a Modelem (MobX store) se nám podařilo identifikovat jako klasický Presenter a celý pattern tedy jako typické MVP.
Presenter je vanilla javascript třída, která nezná identitu svého View, ale umí obousměrnou komunikaci:

  • směrem k a od MobX stores, od kterých si vyžaduje data
  • směrem k a od View, kterému posílá data a reaguje na jeho akce (stisk tlačítka)

Celé by to v ideálním světě mělo vypadat zhruba takhle:

architektura aplikace pro smart TV

V okamžiku, kdy jsme si ujasnili všechny tyto požadavky, stačilo “jen” napsat factory, která vše slepí hezky dohromady a bude obsahovat a řešit co nejvíce společné logiky pro celé MVP na jednom místě:

import * as React from 'react';
import { observer } from 'mobx-react';
import stores from '../../stores';

function MVPFactory<Props, ParentProps>(View: React.ComponentClass<Props & ParentProps> | React.SFC<Props & ParentProps>, Presenter: new (stores: stores.IAppStores) => Props & mvp.IPresenter) {
 const ObserverView = observer<Props>(View);

 class MVPComponent extends React.Component<ParentProps> {
   private presenter: Props & mvp.IPresenter;

   constructor(props: ParentProps) {
     super(props);
     this.presenter = new Presenter(stores);
   }

   render() {
     const { props: parentProps, presenter: dataProps } = this;
     return <ObserverView {...parentProps} {...dataProps} />;
   }

   componentWillUnmount() {
     this.presenter.destruct();
   }
 }

 return observer(MVPComponent);
}

export default MVPFactory;

 

… a typické volání v modulu, který exportujeme navenek jako “čistou” React komponentu:

import MVPFactory from '../mvp/mobx';
import Presenter from './Login.presenter.mobx';
import View from './Login.component';

export default MVPFactory<login.IProps, login.IParentProps>(View, Presenter);

 

Co se nám ve výsledku podařilo:

  • Factory zná identitu stores a jejich instance posílá presenteru. Je to jediné místo, kde je známa identita datového zdroje.
  • Vnější (hlavní) komponenta zná jen interface props, které je možné poslat shora (od rodiče).
  • Vnitřní komponenta zná všechny props, se kterými může pracovat (props od rodiče + props z datového zdroje).
  • Presenter žije jako privátní proměnná na komponentě a stará se o delivery všech props do komponenty. Identita Presenteru není známa, jen její interface. Víme o ní jen, že jako parametr constructoru přijímá stores.
  • Když je komponenta před zničením, na presenteru voláme destruct(). V našem případě typicky proto, že potřebujeme manuálně uvolňovat strong reference, které vznikají jako efekt autorun() volání v MobXu.

Tento mechanismus nám umožnil rozumně delegovat konkrétní tasky na konkrétní třídy a zpřehlednil práci s views i daty. Navíc se polovina týmu mohla bezbolestně věnovat implementaci Presenterů a Modelů a pouze exponovat interface pro Views, kterým se věnovala druhá polovina týmu.

7 tipů a triků z terénu po dvou měsících vývoje

    1. Stáhněte si TOP 10 aplikací pro danou platformu a pozorujte, co v nich funguje dobře a co v nich nefunguje. Inspirujte se.

 

    1. Zapomeňte na emulátory. Testuje na reálném zařízení. TizenTV i webOS mají remote debuggery, v developer konzoli vždy vidíte, na čem vám aplikace padá. Nemusíte řešit, že emulátory něco neumí (neumí toho spoustu).

 

    1. Testujte s reálnými uživateli, čím méně technicky zdatnými, tím lépe.

 

    1. Pokud animujete, neprovádějte složitější výpočty. WebWorker, který vám nasimuluje druhý thread, občas pomáhá, ale většinou nijak výrazně, protože CPU v TV jsou většinou single/dual core. Experimentujte, testujte reálný dopad na performance.

 

    1. Pokud můžete, vyhněte se práci s <canvas>. Pokud musíte použít <canvas>, nepoužívejte animace. Pokud musíte použít animace, zkuste upřednostnit CSS. Televize nemají dostatečný výkon, a zatímco na desktopu si můžete krásně odladit vykreslování grafu při 60 fps, na televizi to bude spíše 5–10 fps. <canvas> na obou platformách rovněž nepodporuje metodu toBlob() a její javascript polyfill je extrémně drahý. Pokud na canvasu vykonáváte drahé operace s obrázky (kombinace, výřezy), ukládejte si výsledné obrázky na window pomocí URL.createObjectURL(). Používejte pica knihovnu pro lepší výstupy při resize obrázků. Knihovna podporuje různé typy algoritmů (bicubic atp.). Mějte na paměti, že programujete pro obrazovku ve 4k, každý pixel je znát.

 

    1. Pokud budete řešit kryptografii, zkuste přednostně využít WebCrypto API. Má binding přímo na systém a veškeré výpočty provádí v C, ne v javascriptu. webOS 3.0 neimplementuje API v plné šíři a chybí v něm některé algoritmy (potřebovali jsme PBKDF2 a pohořeli jsme). Pokud neuspějete, je tu fallback v podobě skvělé knihovny crypto-js, která je ale řádově pomalejší, protože počítá v javascriptu.

 

  1. Připravte si pevné nervy a hodně čokolády.

A to je v kostce vše o našem prvním televizním dobrodružství. Další průzkum tohohle zapeklitého, špatně zdokumentovaného světa, necháme na vás 🙂 Jestli budete potřebovat pomoct s vývojem vaší chytré aplikace a pobavit se o problematice ještě trochu hlouběji, stavte do eManu na kafe, rádi vám poradíme.

 

Předchozí články:

Petr Čihula
Frontend Developer

RSS