Les 4: Native

Sebastiaan HenauOngeveer 30 minuten

Les 4: Native

Tot nu toe hebben we webapplicaties ontwikkeld, geen mobiele applicatie. Deze les bekijken we hoe we native features op een mobiel toestel kunnen aanspreken door middel van plug-ins. Verder bekijken we hoe de webapplicaties die we tot nu geschreven hebben, via Capacitor, gecompileerd kunnen worden voor Android.

Plug-ins

Er zijn een aantal verschillende bronnen voor plug-ins. Deze bronnen hebben elk hun voor- en nadelen. Hieronder worden de verschillende opties besproken, en wordt er beargumenteerd waarom bepaalde plug-ins beter zijn dan andere.

In deze context is een plug-in een, in Swift of Java geschreven, programma dat aangesproken kan worden via een TypeScript API. De plug-in wordt dus aangesproken vanuit onze hybrid applicaties en spreekt op zijn beurt het besturingssysteem (iOS of Android) aan.

Cordova Plug-ins

Cordova, de "voorloper" van Capacitor bestaat al sinds 2009. De hoeveelheid plug-in die geschreven zijn voor dit platform is dan ook aanzienlijk. De meeste van deze plug-ins zijn compatibel met Capacitor, er zijn echter ook plug-ins die te lang niet meer geüpdatet zijn en niet meer werken met moderne Android/iOS versies. Capacitor houd een lijst bij van plug-ins waarvan bekend is dat deze niet werken met Capacitor, deze (niet exhaustieve) lijst is beschikbaar op de officiële Capacitor siteopen in new window.

Aan Cordova plug-ins zijn een reeks nadelen verbonden. Dit soort plug-ins zijn ontwikkeld door de open-source community, dus worden een groot deel van deze plug-ins niet meer actief ontwikkeld. Dit heeft dan weer tot gevolg dan er een hele hoop plug-ins zijn die deprecated features gebruiken. Of, in sommige gevallen, zelfs features die niet langer toegestaan zijn op Android of iOS. Daarnaast is er voor deze plug-ins zelden documentatie beschikbaar die rechtstreeks toepasbaar is voor Capacitor applicaties.

Daarnaast was Cordova was een framework voor mobiele applicatie, niet voor PWA's. Er zijn dus plug-ins beschikbaar die geen (goede) ondersteuning bieden voor webapplicaties. Capacitor plug-ins bieden altijd ondersteuning voor PWA's als dit haalbaar is met browsertechnologieën, zaken zoals FaceId of vingerafdrukken kunnen niet zomaar vanuit een browser.

Tenslotte bieden de meeste plug-ins voor Cordova enkel een JavaScript API en geen TypeScript API. Op dit laatste probleem heeft Ionic wel een oplossing voorzien, voor een groot deel Cordova plug-ins zijn er TypeScript wrappers beschikbaar gemaakt door Ionic. Een lijst van deze plug-ins is terug te vinden op de Awesome Cordova Plugins websiteopen in new window.

Gevaar

Ondanks de TypeScript wrappers, werken deze plug-ins niet altijd correct en is de documentatie niet altijd even duidelijk. Daarbovenop bied Ionic sinds kort geen informatie meer over deze plug-in op hun eigen website, maar wordt je doorverwezen naar een door de community onderhouden site.

Vermijd deze plug-ins tenzij je geen andere keuze hebt.

Capacitor plug-ins

Capacitor biedt een aantal gratis plug-ins aan voor de meest gebruikte (en minst complexe) onderdelen van een mobiele applicatie. Deze plug-ins worden actief ontwikkeld en zullen dus geen deprecated features gebruiken. De plug-ins bieden ondersteuning voor Android, iOS en PWA, dit via een promise-based TypeScript API. Officiële Capacitor plug-ins zijn in alle situaties beter dan Cordova alternatieven. Hetzelfde geld voor de meeste community plug-ins.

Capacitor levert kwaliteitsvolle plug-ins, maar doet dit natuurlijk niet uit de goedheid van hun hart. De interessantere plug-ins zoals Authentication, Encrypted Storage, Biometrics, ... worden niet gratis aangeboden, maar kosten duizenden Euro's. De lijst van Capacitor Core (gratis) plug-ins is te vinden op de Capacitor websiteopen in new window. Sommige van deze enterprise plug-ins kunnen vervangen worden met alternatieven. Voor Authentication kan bijvoorbeeld Capacitor Firebaseopen in new window gebruikt worden, voor Biometrics is Capacitor Native Biometricopen in new window beschikbaar.

Community plug-ins

Net zoals voor Cordova zijn er ook voor Capacitor plug-ins beschikbaar die ontwikkeld zijn door de open-source community. Capacitor is relatief nieuw, en de meeste plug-ins dus ook. Dit betekent dat deze plug-ins up-to-date zijn en werken met de recentste versies van Android en iOS.

Er zijn verschillende gecureerde lijsten van Capacitor plug-ins beschikbaar, de meest uitgebreide lijsten zijn te vinden op in de Awesome Capacitoropen in new window en Capacitor Communityopen in new window GitHub organizations. Daarnaast is ook Capawesomeopen in new window een goede bron voor krachtige en goed functionerende Capacitor plug-ins. Tenslotte kan je altijd terugvallen op een zoekopdracht op NPMopen in new window, op het moment van schrijven zijn er ongeveer 2400 packages die voldoen aan het 'capacitor' zoekcriteria, een hoop meer dan dat er in de gecureerde lijsten te vinden zijn.

PWA Elements

Capacitor plug-ins ondersteunen PWA's. Maar in sommige gevallen is hiervoor een extra bibliotheek nodig, namelijk PWA Elementsopen in new window. Deze bibliotheek wordt door Capacitor aangeboden en is dus steeds up-to-date en compatibel met de Capacitor Core plug-ins.

Capacitor is, in eerste instantie, bedoeld voor mobiele operating systems. Sommige plug-ins, zoals Cameraopen in new window hebben geen standaard UI in een browser, op Android of iOS toestellen wordt natuurlijk gebruik gemaakt van de camera app die standaard aanwezig is op het besturingssysteem. PWA elements bevat dus de UI-componenten voor die situaties waar er geen standaard UI-component aanwezig is in de browser. Een plug-in zoals Clipboardopen in new window heeft dus geen nood aan PWA elements omdat deze plug-in niets visueels toont.

Zonder PWA elements produceert de getPhoto methode van de camera plug-in onderstaand scherm.

Figuur 1: Camera plug-in zonder PWA Elements

Bovenstaand scherm is niets anders dan een FilePicker, een optie die in elke browser aanwezig is. We kunnen hier wel foto's uploaden, maar een nieuwe foto maken is onmogelijk. Na de installatie van PWA elements produceert de getPhoto methode volgend scherm (de webcam is gesimuleerd, als er een echte webcam aanwezig was zou de camera feed getoond worden).

Figuur 2: Camera plug-in met PWA Elements

Promises

Capacitor plug-ins zijn promise based. Een promise is een belofte dat een methode ergens in de toekomst een resultaat zal teruggeven. Het belangrijkste deel van voorgaande zin is "ergens in de toekomst", de methode voert geen onmiddellijke operatie uit. Het is mogelijk dat het een minuut (of langer) duurt voor het resultaat beschikbaar is.

Promises zijn asynchroon, omdat de operaties even kunnen duren is het geen goed idee om te wachten tot de operatie afgewerkt is. Dit zou betekenen dat de gebruiker, bijvoorbeeld, een halve minuut niets kan doen. Een zeer slechte user experience dus. Omwille van de asynchrone aard van een promise is het mogelijk voor de gebruikers om andere acties uit te voeren terwijl er gewacht wordt op het resultaat. Dit betekend dus dat tussen twee statements in een asynchrone functie, eventueel andere code uitgevoerd wordt. De flow van het programma is dus niet meer puur iteratief.

Een promise is een belofte dat er iets teruggegeven zal worden in de toekomst, wat dat iets juist kan zijn wordt bepaald door het type van de promise. Een promise heeft steeds de structuur Promise<T> waar T alle mogelijke datatypes kan zijn, een string, boolean, number, void, een custom type, ...

Een promise heeft een methode then die uitgevoerd wordt als de promise succesvol resolved is, i.e. als er zich geen errors hebben voorgedaan tijdens het uitvoeren van de asynchrone methode. Naast de then methode is er ook een catch methode aanwezig om te reageren op error. Onderstaande code zal de tekst "Do something with the result" uitprinten.

class Example {

  async someAsynchronousOperation(): Promise<string> {
    // Do some stuff
    return 'Result';
  }

  callSomeAsynchronousOperation(): void {
    this.someAsynchronousOperation()
            .then((result: string) => console.log("Do something with the result."))
            .catch((err: Error) => console.error("An error occurred."));
  }
}

const example = new Example();
example.callSomeAsynchronousOperation();

Als we de someAsynchronousOperation() methode aanpassen naar onderstaander code zal de tekst "An error occurred" uitgeprint worden.

class Example {
  async someAsynchronousOperation(): Promise<string> {
    // Do some stuff
    throw new Error("An error in the asynchronous function");
    return 'Result';
  }
}

Zoals in bovenstaande methodes te zien is, geeft een asynchrone methode steeds een Promise terug, eventueel een Promise<void> als de methode geen return waarde heeft.

De async en await keywords zijn een wrapper rond promises De callSomeAsynchronousOperation methode kan herschreven worden als een asynchrone methode op volgende manier. Merk op dat we nu een try-catch blok nodig hebben. De promise kan eventueel een error teruggeven en deze moet natuurlijk opgevangen worden.

class Example {
  async callSomeAsynchronousOperation(): Promise<void> {
    try {
      const result = await this.someAsynchronousOperation();
      console.log('Do something with the result.');
    } catch (err: any) {
      console.log("An error in the asynchronous function");
    }
  }
}

Promises kunnen aan elkaar gekoppeld worden, elke then methode geeft ook een promise terug

const example = new Example();
example.callSomeAsynchronousOperation()
        .then(r => {
          console.log('In Promise 2');
          return;
        })
        .then(r => {
          console.log('In Promise 3');
          return;
        })

Om het gebruik van native features te illustreren, bouwen we een eenvoudige gallery app. Gebruikers kunnen foto's nemen via een camera, of uploaden vanop het smartphone of computer. Tijdens het bouwen van deze applicatie maken we kennis met Capacitor en enkele veel voorkomende plug-ins.

We maken een nieuw project aan op dezelfde manier als in les 2. Vervolgens voegen we een nieuwe service PhotoService toe. Tenslotte injecteren we deze service alvast in de constructor van de HomePage

export class HomePage {
  constructor(public photoService: PhotoService) {
  }
}

Plug-ins installeren

Capacitor plug-ins moeten één per één geïnstalleerd worden. In de vorige versie van Capacitor was dit niet het geval, als je iets zou opzoeken, controleer dan grondig dat het gevonden resultaat geldig is voor Capacitor 4.

Om de applicatie uit te bouwen hebben we volgende plug-ins nodig:

Al deze plug-ins kunnen geïnstalleerd worden via onderstaand pnpm commando (voor de leesbaarheid gesplits over 2 lijnen, kan eventueel ook met 1 commando of met 4 afzonderlijke commando's).

pnpm add @capacitor/camera @capacitor/filesystem
pnpm add @capacitor/preferences @ionic/pwa-elements

PWA Elements configureren

PWA elements is zeer eenvoudig te configureren, in src/main.ts moet één lijn code en één import statement toegevoegd worden.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { defineCustomElements } from '@ionic/pwa-elements/loader';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

defineCustomElements(window);





 








 

PhotoService

Deze sectie beschrijft de verschillende methodes van de PhotoService, de bijhorende UI code wordt in de volgende sectie HomePage besproken.

De PhotoService heeft een aantal instantie variabelen nodig. De eerste variabele is photos, een lijst van alle foto's in de app. Als datatype gebruiken we de Capacitor Photoopen in new window interface. De documentatie toont een reeks attributen die voldoende zijn voor onze doeleinden. Het is dus niet nodig om zelf een type te definiëren. De attributen die opgelijst zijn in de documentatie zijn allemaal optioneel, met uitzondering van het format attribuut dat verplicht een waarde moet hebben en het saved attribuut dat aangeeft of de afbeelding in de galerij van het toestel bewaard is. We voegen ook alvast een methode toe om alle foto's op te vragen.

De tweede property is een string variabele key die de naam bevat waaronder we een lijst van filesystem URI's zullen bewaren. Deze key wordt doorgegeven aan de Preferences plug-in, vervolgens kan de Preferences plug-in de waarde gekoppeld aan deze key uitlezen of wegschrijven.

De derde variabelen bevat de lijst van filesystem URI's, i.e. de locaties op het filesystem waar elke foto bewaard is.

Het laatste veld bevat de permissions die de app gekregen heeft. Als datatype gebruiken we PermissionStatusopen in new window uit de Camera plug-in. Deze plug-in kan gebruik maken van 2 permissions, camera en photos. De eerste geeft aan of een foto gemaakt mag worden via de camera. De tweede permission geeft aan of een foto geladen mag worden uit foto's die de gebruiker eerder gemaakt heeft, al dan niet via een andere app. Daarnaast kunnen we, als deze permission de status 'granted' heeft, foto's gemaakt via de camera bewaren in de galerij van het systeem (Android of iOS).

Permissions zijn, voor de camera plug-in niet beschikbaar in een web-context. De browser doet daar de afhandeling automatisch, probeer je een foto te nemen, dan zal de browser vragen naar toestemming. De permissions voor een PWA app kunnen niet aangevraagd worden via Capacitor. Daarom initialiseren we de permissions op {camera: 'granted', photos: 'granted'}, als de app uitgevoerd wordt als een native Android of iOS-applicatie zullen we deze permissions overschrijven met de effectieve permission op dat toestel.

Info

Voor andere plug-ins, zoals bijvoorbeeld Local Notificationsopen in new window is het wel nodig om manueel de rechten aan te vragen. Controleer dit dus voor elke plug-in die je gebruikt.

import {Injectable} from '@angular/core';
import {Camera, PermissionStatus, Photo} from '@capacitor/camera';

export class PhotoService {
    
  readonly #photos: Photo[] = [];
  readonly #key = 'photos';

  #photoURIs: string[] = [];
  #permissionGranted: PermissionStatus = {camera: 'granted', photos: 'granted'};
  
  constructor() {
  }
  
  getPhotos(): Photo[] {
    return this.#photos;
  }
}









 








De variabelen photos en key krijgen de readonly modifier, zo wordt het onmogelijk om deze variabelen te overschrijven. Variabelen met deze modifier moeten geïnitialiseerd zijn voordat de constructor afgewerkt is. readonly heeft een gelijkaardige werking als het const keyword, maar dit laatste kan niet gebruikt worden voor instantie variabelen.

De laatste twee variabelen photoURIs en permissionGranted kunnen geen readonly modifier krijgen omdat deze niet niet geïnitialiseerd kunnen worden in de constructor. Beide variabelen krijgen hun waarde via asynchrone methodes, dit betekent dat het onmogelijk is om te garanderen dat deze asynchrone methodes uitgevoerd zijn voordat de constructor afgewerkt is.

Filesystem URIs ophalen

We weten al dat het veld key gebruikt wordt om de filesystem URI's weg te schrijven en in te lezen. Hiervoor gebruiken we de Preferences plug-in. Let op, deze plug-in is ideaal voor kleine ongestructureerde data, maar voor grotere of complexere data is een cloud database of embedded SQL databaseopen in new window beter.

Deze plug-in gebruikt, net zoals alle andere officiële Capacitor plug-ins, de meest optimale opslaglocatie voor het platform waarop de applicatie draait. Voor webapplicaties (PWA's) wordt gebruik gemaakt van localStorageopen in new window. Native iOS applicaties maken gebruikt van de UserDefaultsopen in new window. Voor Android apps worden de SharedPreferencesopen in new window gebruikt.

import {Preferences} from '@capacitor/preferences';

export class PhotoService {
    // Niet relevante code weggelaten.
}





Waarschuwing

Maak geen gebruik van Ionic Storageopen in new window. Deze plug-in maakt geen gebruikt van de UserDefaults of SharedPreferences maar van localStorage en indexedDB. LocalStorage wordt op verschillende toestellen periodiek geleegd. De SharedPreferences en UserDefaults die Capacitor Storage gebruikt zijn veiliger en zullen pas geleegd worden als de app verwijderd wordt.

We hebben twee methodes nodig, één om de URI's in te lezen, en één om de URI's weg te schrijven. Voor we de URI's kunnen wegschrijven moet de array geserialiseerd worden, i.e. geconverteerd naar een string, dit kan via de functie JSON.stringify(). In de andere richting moet een ingelezen lijst van URI's natuurlijk ook terug geconverteerd worden naar een JavaScript object, hiervoor kan JSON.parse() gebruikt worden.

export class PhotoService {

    readonly #key = 'photos';
    #photoURIs: string[] = [];

    async #retrievePhotoURIs(): Promise<void> {
        const uris = await Preferences.get({key: this.#key});
        this.#photoURIs = uris ? JSON.parse(uris.value) : [];
    }

    async #persistPhotoURIs(): Promise<void> {
        await Preferences.set({
            key: this.#key,
            value: JSON.stringify(this.#photoURIs)
        });
    }
}







 









De code op lijn 8 lijkt op het eerste zich wat eigenaardig. Deze werkt omdat Preferences.get() de waarde null teruggeeft als er niets bewaard is onder de naam key. Vervolgens krijgt JSON.parse ook null als argument, aangezien null niet verder gedeserialiseerd kan worden geeft JSON.parse ook null terug. Voor meer info verwijzen we door naar de sectie over de nullish coalescing operator in les 1.

Permissions

Net zoals voor de URI's zijn er ook voor de permissions twee methodes nodig. De eerste methode leest de permissions in, de tweede vraagt de gebruiker om toestemming. Zoals eerder gezegd kunnen permission niet opgevraagd of aangevraagd worden op PWA's, daarom wordt gebruik gemaakt van een try-catch blok.

export class PhotoService {

  #permissionGranted: PermissionStatus = {camera: 'prompt', photos: 'prompt'};

  async #requestPermissions(): Promise<void> {
    try {
      this.#permissionGranted = await Camera.requestPermissions({permissions: ['photos', 'camera']});
    } catch (error) {
      console.error(`Permissions aren't available on this device: ${Capacitor.getPlatform()} platform.`);
    }
  }

  async #retrievePermissions(): Promise<void> {
    try {
      this.#permissionGranted = await Camera.checkPermissions();
    } catch (error) {
      console.error(`Permissions aren't available on this device: ${Capacitor.getPlatform()} platform.`);
    }
  }
}

Foto's nemen

Hoe een foto genomen wordt is afhankelijk van het platform. Op een native platform kunnen we een foto rechtstreeks laten bewaren op het filesystem, de URI van het filesystem kan vervolgens gebruikt worden om de foto te tonen in de applicatie. Dit is echter niet mogelijk op een PWA want zo'n app draait volledig in een browser en heeft dus geen filesystem. Via de Capacitor FileSystem plug-in kan IndexedDB gebruikt worden als FileSystem voor PWA's, maar de Camara plug-in kan een foto niet rechtstreeks bewaren in dit pseudo-filesystem. Voor een PWA wordt de foto genomen en teruggegeven als base64open in new window string, vervolgens zullen we deze bewaren op het pseudo-filesystem.

Helper methodes

Voor we deze functionaliteit kunnen implementeren zijn er een aantal helper methodes nodig. Ten eerste moeten we bepalen welke permissions we hebben, deze zijn al bewaard in een instantievariabele, maar om de code overzichtelijk te houden voegen we volgende twee helper methodes toe.

export class PhotoService {

  #permissionGranted: PermissionStatus = {camera: 'prompt', photos: 'prompt'};

  #haveCameraPermission(): boolean {
    return this.#permissionGranted.camera === 'granted';
  }

  #havePhotosPermission(): boolean {
    return this.#permissionGranted.photos === 'granted';
  }
}

Deze twee methodes kunnen gebruikt worden om te bepalen waar de foto's vandaan kunnen komen. Als we camera permission hebben kunnen de foto's genomen worden via de camera, als we photos permission hebben kunnen de foto's vanop het toestel opgeladen worden. Als we beide permissions hebben, laten we de gebruiker kiezen.

export class PhotoService {

    #determinePhotoSource(): CameraSource {
        if (this.#havePhotosPermission() && this.#haveCameraPermission()) {
            return CameraSource.Prompt;
        } else {
            return this.#havePhotosPermission() ?
                CameraSource.Photos : CameraSource.Camera;
        }
    }
}

Foto's op native toestellen

Gebruik makend van bovenstaande methodes kunnen we vervolgens de takePhotoNative methode implementeren. Het interessantste in onderstaande code is het resultType, hier gebruiken we CameraResultType.Uri, wat ervoor zorgt dat we een filesystem URI als resultaat krijgen. Het resultaat heeft volgende attributen:

  • path: De filesystem URI waar de afbeelding bewaard wordt.
  • webpath: Een filesystem URI (file://...) kan niet rechtstreeks gebruikt worden in een hybrid-webview applicatie. Een webview heeft namelijk geen rechtstreekse toegang tot het filesystem. Daarom voegt capacitor ook dit attribuut toe dat wel rechtstreeks in een webview gebruikt kan worden. Dit attribuut heeft de vorm (http://localhost/capacitor_file//...).
  • format: De extensie van de genomen foto (jpg, png, ...).
export class PhotoService {

    readonly #photos: Photo[] = [];
    #photoURIs: string[] = [];

    async #takePhotoNative(): Promise<void> {
        const image = await Camera.getPhoto({
            quality: 90,
            resultType: CameraResultType.Uri,
            saveToGallery: this.#havePhotosPermission(),
            source: this.#determinePhotoSource()
        });
        
        if (image?.path) {
          this.#photoURIs.push(image.path);
        }

        this.#photos.push(image);
    }
}








 











Foto's in web applicaties

Om foto's te nemen in een webapplicatie wordt gebruik gemaakt van base64 encoding. Dit is een manier om binaire data te bewaren als string. Onderstaande string is een base64 representatie van het logo op de home page van deze site.

Een base64 string kan als src voor een img element gebruikt worden nadat deze geconverteerd wordt naar een data URLopen in new window. Dit is een URL van de vorm data:[<mediatype>];base64,<data>. Hierin wordt mediatype een MIME-type van de vorm image/[extension], waar extension bijvoorbeeld jpg of png is.

Onderstaande code vraagt de afbeelding als base64 string op lijn 4, op lijn 12 wordt deze base64 string omgevormd naar een data URL. Merk op dat we dit keer altijd de bron CameraSource.Camera gebruiken, dit is voldoende omdat de PWA lay-out voor deze optie een knop bevat waarmee een bestand opgeladen kan worden.

export class PhotoService {

    readonly #photos: Photo[] = [];
    #photoURIs: string[];

    async #takePhotoPWA(): Promise<void> {
        const image = await Camera.getPhoto({
            quality: 90,
            resultType: CameraResultType.Base64,
            source: CameraSource.Camera
        });

        const uri = await this.#saveImageToFileSystem(image);
        this.#photoURIs.push(uri);
        image.path = uri;


        image.dataUrl = `data:image/${image.format};base64,${image.base64String}`;
        this.#photos.push(image);
    }
}








 



 




 



Ook voor foto's gemaakt op in een PWA moeten we de fileURI bewaren, anders kunnen we de foto's niet opnieuw tonen na een reload. Op een PWA moeten we de foto echter zelf wegschrijven naar het bestandssysteem (lijn 13). Hiervoor gebruiken we onderstaande nieuwe methode.

export class PhotoService {

    async #saveImageToFileSystem(photo: Photo): Promise<string> {
        if (!photo.base64String) {
            throw new Error(`Can't write the photo to the filesystem because there is no base64 data.`)
        }   
    
        const fileName = `${new Date().getTime()}.${photo.format}`;
        const savedFile = await Filesystem.writeFile({
            path: fileName,
            data: photo.base64String,
            directory: Directory.Data
        });
        return savedFile.uri;
    }
}






 
 








We kunnen de base64 string rechtstreeks meegeven aan de writeFile methode (lijn 7). Deze wordt automatisch geconverteerd naar binaire data, zodat de afbeelding geopend kan worden met elke image viewer applicatie. We moeten aangeven waar de afbeelding bewaart wordt, dit gebeurt op lijn 8. Er zijn een aantal mogelijke opties, maar Directory.Dataopen in new window is de enige map die ondersteund is op nieuwere toestellen en die niet automatisch geleegd kan worden door Android of iOS als er een gebrek aan opslagruimte is.

Tenslotte moet enkel nog beslist worden welke van de twee methodes gebruikt moet worden (#takePhotoNative of #takePhotoPWA), hiervoor schrijven we een nieuwe methode #takePhoto. Om te beslissen welke van de twee gebruikt moet worden, is het nodig om te weten op welk platform de applicatie draait. Capacitor bied een aantal handige utility methode waarmee dit bepaald kan worden:

Om crashes te vermijden controleren we eerst uitdrukkelijk of we de nodige permissions hebben en indien dit niet het geval is, vragen we deze eerst aan. Vervolgens roepen we op basis van het platform de juiste methode op en tenslotte bewaren we de filesystem URI voor de nieuwe foto via de Capacitor Preferences plug-in.

export class PhotoService {

    async takePhoto(): Promise<void> {
        if (!this.#haveCameraPermission() || !this.#havePhotosPermission()) {
            await this.#requestPermissions();
        }

        if (Capacitor.isNativePlatform()) {
            await this.#takePhotoNative();
        } else {
            await this.#takePhotoPWA();
        }
        await this.#persistPhotoURIs();
    }
}







 







Foto's inladen

Nu de foto's gemaakt kunnen worden op alle platformen, moeten we de optie voorzien om deze in te laden als de applicatie start, ook de permissions en URI's moeten nog geladen worden als de applicatie start.

We voorzien opnieuw twee verschillende methodes, één voor PWA's en één voor native applicaties.

export class PhotoService {

    async #loadPhotos(): Promise<void> {
        if (Capacitor.isNativePlatform()) {
            await this.#loadPhotosNative();
        } else {
            await this.#loadPhotosPWA();
        }
    }
}

Zoals eerder gezegd verwacht het Photo datatype dat format niet undefined is. Via een nieuwe methode kunnen we de extensie eenvoudig uit de file URI halen.

export class PhotoService {

    #getPhotoFormat(uri: string): string {
        const splitUri = uri.split('.');
        return splitUri[splitUri.length - 1];
    }
}

De methode voor native applicaties is relatief eenvoudig te implementeren, we kunnen de afbeeldingen tenslotte rechtstreeks vanop het bestandssysteem tonen. Het enige waarmee we rekening moeten houden is dat de foto's wel rechtstreeks ingelezen kunnen worden, maar dat we de URI moeten converteren naar een HTTP URI omdat de app in een webview draait. Dit kan via de methode convertFileSrcopen in new window die Capacitor voorziet.

export class PhotoService {

    async #loadPhotosNative(): Promise<void> {
        for (const uri of this.#photoURIs) {
            this.#photos.push({
                path: uri,
                format: this.#getPhotoFormat(uri),
                webPath: Capacitor.convertFileSrc(uri),
                saved: this.#havePhotosPermission()
            });
        }
    }
}

De methode voor PWA's is net iets complexer, hier moeten we de afbeeldingen opnieuw als base64 inlezen en gebruik maken van een dataURL om de afbeelding te tonen. Via de readFile methode van de Filesystem plug-in kunnen we een afbeelding inlezen als base64 string (lijnen 6-8), vervolgens kunnen we deze sting omvormen naar een dataURL (lijn 12).

export class PhotoService {

    async #loadPhotosPWA(): Promise<void> {
        for (const uri of this.#photoURIs) {

            const data = await Filesystem.readFile({
                path: uri
            });

            const format = this.#getPhotoFormat(uri);
            this.#photos.push({
                dataUrl: `data:image/${format};base64,${data.data}`,
                format,
                path: uri,
                saved: false
            });
        }
    }
}





 
 
 



 







Tenslotte rest enkel nog het oproepen van al deze methodes.

export class PhotoService {

    constructor() {
        this.#loadData();
    }

    async #loadData(): Promise<void> {
        await this.#retrievePhotoURIs();
        await this.#retrievePermissions();
        await this.#loadPhotos();
    }
}

HomePage

De code voor de HomePage is relatief eenvoudig, we gebruiken het Ionic grid systeem om drie afbeeldingen naast elkaar weer te geven en roepen de takePhoto methode van de PhotoService op via een FAB.

Om de foto's te tonen gebruiken we de dataURL als deze niet undefined is (PWA), anders gebruiken we het webPath (native).

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Gallery
    </ion-title>
  </ion-toolbar>
</ion-header>


<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Gallery</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-grid>
    <ion-row>
      <ion-col size="4" *ngFor="let photo of photoService.getPhotos()">
        <ion-img [src]="photo.dataUrl || photo.webPath"></ion-img>
      </ion-col>
    </ion-row>
  </ion-grid>


  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button (click)="photoService.takePhoto()">
      <ion-icon name="camera"></ion-icon>
    </ion-fab-button>
  </ion-fab>

</ion-content>


















 
 
 





 





Applicatie testen als native Android app

Zoals tijdens de inleiding besproken, worden er voor onze applicaties een (productie) build gemaakt, een website. Deze website wordt geladen in een browser die in een native Android of iOS app uitgevoerd wordt.

Om deze native app aan te maken is de Capacitor CLI vereist Je kan deze installeren via het onderstaande commando (normaalgezien is dit al gebeurt tijdens het aanmaken van een Ionic project met ionic start).

pnpm add --save-dev @capacitor/cli

Capacitor project configureren

Capacitor heeft een configuratie file nodig waarin aangegeven wordt wat de naam van de app is, wat het versienummer is, welke plug-ins toegevoegd moeten worden voor iOS of Android, ... Een gedetailleerde beschrijving van de opties in deze configuratiefile is beschikbaar op de officiële Capacitor siteopen in new window.

Een project dat gegenereerd is via ionic start bevat al een Capacitor configuratiefile. Hieronder hebben we de nodige opties al aangepast naar een gepaste waarde. De betekenis hiervan wordt onder de code besproken.

import {CapacitorConfig} from '@capacitor/cli';

const config: CapacitorConfig = {
    appId: 'be.thomasmore.graduaten.gallery',
    appName: ' Gallery',
    webDir: 'www',
    bundledWebRuntime: false
};

export default config;

appId

Het package id van je app wordt gebruikt om te bepalen of een app al geïnstalleerd is, of de app toegang heeft tot de online services die we in Firebase gebruiken (zie les 6), en voor een hele reeks andere doeleinden. Het package id moet in reverse domain name notationopen in new window formaat genoteerd worden. Als je een app bouwt met als doel deze te publiceren in de plaats van om te leren, is het natuurlijk belangrijk dat je hiervoor een domain naam gebruikt die je effectief bezit. Domeinnamen zijn uniek en twee apps zullen dus nooit als "dezelfde app" beschouwd worden. Laat je het default (com.example.app) staan, dan bestaat de kans dat je app niet geaccepteerd wordt in de App/Play Store omdat iemand anders dit appId al gebruikt heeft.

Info

Gebruik tijdens deze cursus, een appId van de vorm com.achternaam.voornaam.appnaam. De apps die als voorbeeld aangeboden worden krijgen steeds een id van de vorm be.thomasmore.graduaten.appnaam.

appName

De appName parameter bevat de naam van de applicatie, de naam die voor gebruikers te zijn is in de Play of App Store en op hun toestel in de app drawer/app library. De naam moet dus leesbaar en duidelijk zijn voor elke toekomstige gebruiker. Voor dit voorbeeld passen we de appName aan naar Gallery.

webDir

Deze optie geef de naam weer van de map waarin de gecompileerde versie van de website te staan komt. Je past deze optie niet aan.

bundledWebRuntime

Deze optie laat je op false staan. Via bundledWebRuntime kan je Capacitor gebruiken om de bundle (collectie van statische assets) te generen, dit is enkel nodig als je niet met een framework zoals React, Angular of Vue werkt.

Android & iOS project aanmaken

Een project aanmaken voor Android of iOS is relatief eenvoudig. We moeten slechts één commando uitvoeren om een native project aan te maken. Let op, doe dit na je de config file aangemaakt hebt, zo ben je zeker dat de juiste package name en id gebruikt worden.

Hieronder maken we enkel een Android project aan, een iOS project aanmaken verloopt volledig analoog, maar om zo'n project te openen en te compileren is een toestel met macOS nodig. We werken dus enkel met Android projecten voor het vervolg van de cursus omdat deze op alle platformen gecompileerd en getest kunnen worden.

Om een Android project aan te maken moeten we eerst de bijhorende library installeren, deze library bevat de Android code die de webserver aanmaakt en de app inlaad in de webview. Gebruik onderstaande commando's om een nieuw de library te installeren en vervolgens een nieuw Android project aan te maken.

pnpm add @capacitor/android
pnpm exec cap add android
Info voor iOS/macOS gebruikers

De applicatie builden als iOS-variant doe je op je eigen risico, we gaan verder in de les altijd Android applicaties builden. Als je tegen fouten aanloopt raden we altijd aan om de applicatie als Android variant te builden en te kijken of de fouten hiermee opgelost zijn.

Gebruik onderstaande commando om het iOS platform toe te voegen

pnpm add @capacitor/ios

En het volgende commando om het iOS project aan te maken

pnpm exec cap add ios

De map die door dit laatste commando aangemaakt word, zullen we (bijna) niet rechtstreeks aanpassen. Alle wijzigingen worden door Capacitor, Android Studio of Cordova Res gemaakt.

Figuur 3: Android project

Rechten

Elke plug-in heeft bepaalde rechten nodig om correct te werken, raadpleeg steeds de documentatie van een bepaalde plug-in om de juiste rechten te vinden. Voor de Cameraopen in new window plug-in zijn volgende rechten nodig.

We moeten deze rechten uitdrukkelijk aanvragen in het Android project, enkel het recht om internet te gebruiken wordt standaard toegevoegd. De rechten worden gedefinieerd in het bestand AndroidManifest.xml (dat binnen het gegenereerde Android project staat).

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="be.thomasmore.graduaten.gallery">

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">

        <!-- Niet relevante code weggelaten. -->
    </application>

    <!-- Permissions -->

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>


















 
 

De Filesystemopen in new window plug-in heeft dezelfde rechten nodig, maar enkel als Directory.Documents gebruikt wordt. Aangezien wij Directory.Data gebruikt hebben, zijn er dus geen specifieke rechten nodig voor de Filesystem plug-in. De Preferencesopen in new window plug-in heeft helemaal geen rechten nodig.

Webapplicatie compileren

Voordat de app gecompileerd kan worden voor Android moeten we een build maken van de webapplicatie. Tot nu toe hebben de app steeds via een development server getest, we hebben nog geen build gemaakt die op een webserver geplaatst kan worden.

In onderstaande voorbeelden maken we een production build, het genereren van zo'n build duurt langer, maar het resultaat zal vlotter werken omdat bepaalde controles, zoals alle change-detection twee keer uitvoeren, niet langer aanwezig zijn. Je kan natuurlijk de --prod vlag weglaten als je nog bugs hebt in de code van je webapplicatie en deze wil proberen op te lossen op Android.

ionic build --prod

Bovenstaand commando heeft een map www gegenereerd, deze map moet gekopieerd worden naar het Android project (pnpm exec cap copy). Daarnaast moeten de geïnstalleerde plug-ins ook toegevoegd worden aan het Android project (pnpm exec cap update). Deze twee commando's kunnen gecombineerd worden in één commando.

pnpm exec cap sync android

Tenslotte moet het Android project geopend worden in Android Studio, hiervoor kan je het volgende commando gebruiken.

pnpm exec cap open android

Als dit commando fouten geeft, is Android Studio niet correct geïnstalleerd en/of zijn de environment variables niet correct geconfigureerd. De correcte configuratie is te vinden in de sectie over de development environment.

Het is natuurlijk niet ideaal om deze 3 commando telkens opnieuw in te moeten geven, we kunnen, in package.json, een nieuw script definiëren dat deze 3 commando's na elkaar uitvoert.

"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android"
}







 

Dit nieuwe script kan dan via onderstaand commando uitgevoerd worden.

pnpm run android

Android Developer Setting

Onderstaande beschrijving gaat ervan uit dat je een fysiek Android toestel hebt, is dit niet het geval, dan kan je een Android Virtual Device (AVD) opzetten (en hoef je deze configuratie niet uit te voeren). Dit is iets trager (tenzij je krachtige hardware hebt), maar werkt wel. Een AVD heeft enkele beperkingen, de camera is bijvoorbeeld gesimuleerd en toont steeds dezelfde foto. Alle functionaliteiten kunnen echter wel gebruikt/getest worden. De installatie van zo'n AVD is relatief eenvoudig, voor meer details verwijzen we je door naar de documentatie van Android Studioopen in new window. Sinds eind september 2022 is het ook (officieel) mogelijk om het WSA (Windows Subsystem for Android) te gebruiken. Deze feature is enkel beschikbaar vanaf Windows 11 22H2, maar is wel sneller dan een klassieke emulator. We verwijzen de geïnteresseerde lezer door naar de installatiehandleidingopen in new window.

Tips

Gebruik steeds een virtual device met dezelfde Android versie als de target versie van het Capacitor project. Je kan in variables.gradle controleren wat de targetSdkVersion is. Als je een andere versie gebruikt werken de AVD's niet altijd correct. Dit wil niet zeggen dat een fysiek toestel met een oudere versie niet werkt. Emulators zijn zelden perfect.

Volg deze stappen om te testen op een fysiek toestel

Om de applicatie uit te voeren op een fysiek toestel moet de ontwikkelingsmodus geactiveerd worden. Hoe je dit doet, varieert heel sterk van toestel tot toestel. Je moet het build nummer zoeken in de instellingen en hier 7 keer op drukken. Hieronder volgen de officiële richtlijnen van Googleopen in new window, maar deze zijn niet geldig op elk toestel. Bepaalde fabrikanten bouwen hun eigen shell bovenop Android en tonen het build nummer op een andere locatie.

  • Android 9 (API level 28) and higher: Settings > About Phone > Build Number
  • Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): Settings > System > About Phone > Build Number
  • Android 7.1 (API level 25) and lower: Settings > About Phone > Build Number
Figuur 4: Mogelijke locaties van het build number

Vervolgens zijn de ontwikkelaarsopties zichtbaar, nu moet de optie "USB Debugging" aangezet worden. Het is opnieuw mogelijk dat deze opties op een andere locatie geplaatst is door je hardwarefabrikant.

  • Android 9 (API level 28) and higher: Settings > System > Advanced > Developer Options > USB debugging
  • Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): Settings > System > Developer Options > USB debugging
  • Android 7.1 (API level 25) and lower: Settings > Developer Options > USB debugging

Verbind je smartphone vervolgens met een USB-kabel en stel de connectie in op "data overdracht". Je smartphone zou nu moeten vragen of de RSA sleutel van je computer toegestaan mag worden (enkel als Android Studio open staat). Het is vanzelfsprekend dat je hier voor "OK" kiest.

Figuur 5: RSA sleutel goedkeuren

App uitvoeren op een (virtueel) Android toestel

Als alles correct geconfigureerd is, detecteert Android studio nu automatisch je smartphone (als deze aangesloten is via een USB-kabel) of je virtueel toestel. Rechtsboven zie je een lijst met de verschillende toestellen die beschikbaar zijn. Zowel de virtuele als fysieke toestellen staan in deze lijst. Kies een toestel en klik op run.

Figuur 6: App uitvoeren

Wordt je toestel niet automatisch gedetecteerd, dan kan je proberen om de Gradle (build tool) configuratie opnieuw op te bouwen. Als dit ook niet werkt, is er ergens iets mis gegaan tijden de configuratie van je development environment of heb je plug-ins en Capacitor libraries gebruikt waarvan de versienummers niet compatibel zijn met elkaar.

Figuur 7: Synchroniseren met Gradle

Splash screen

Het is aan te raden om een splash screen toe te voegen aan je applicatie. Dit is een scherm dat getoond wordt tijdens het laden van de applicatie. Zodra de browser (webview) waarin onze app draait gestart is, wordt het splashscreen verborgen. Wil je dit default gedrag aanpassen, bijvoorbeeld omdat je de pagina pas wil tonen als alle data ingeladen is, dan kan je hiervoor de Capacitor Splash Screenopen in new window plug-in gebruiken. Onderstaande gif toont hoe een splash screen werkt.

Figuur 8: Splash Screen voorbeeld

Om een splash screen te generen gebruiken we de tool capacitor-assetsopen in new window, deze tool kan geïnstalleerd worden via onderstaand commando.

pnpm add -g @capacitor/assets

Icon

Bijna elke recent Android toestel maakt gebruikt van Adaptive Icons. Dit is een feature, die sinds Android 8.0 aanwezig is, waarbij een icoon een bepaalde vorm krijgt afhankelijk van de shell die de fabrikant rond Android gebouwd heeft, of de launcher die de gebruiker verkiest. Op de ene smartphone zal de achtergrond rond zijn, op de andere vierkant, en op een derde nog iets anders. Hieronder zie je enkele voorbeelden van dezelfde app, de vorm van de achtergrond wordt niet door de programmeur bepaald, maar door het systeem.

Figuur 9: Adaptive Icons

Om zo'n adaptive icon te generen moet het icoon opnieuw redelijk wat witruimte hebben, ongeveer 25% langs alle kanten. Daarnaast moet de naam icon.png zijn en moet de resolutie minimaal 1240X1240 px bedragen en een transparante achtergrond hebben. Het png bestand komt in de map /resources te staan. We gebruiken voor deze applicatie het volgende icon (download hier).

Figuur 10: Icon voor de Gallery app

Via dit icon bestand genereren we deze les iconen en splash screens voor een Android (of iOS) applicatie. In les 7 gebruiken we ditzelfde bestand om iconen te generen voor een PWA. Het commando dat we zullen gebruiken bevat parameters waarmee de achtergrondkleur voor het icoon en splashscreen gespecifieerd kan worden. In het commando worden de kleuren gebruikt die overeenkomen met de standaards kleuren binnen Ionic. Als je deze aanpast, past dan zeker ook dit commando aan (of op zijn minst voor het splashscreen).

capacitor-assets generate --iconBackgroundColor '#eeeeee' --iconBackgroundColorDark '#222222' --splashBackgroundColor '#eeeeee' --splashBackgroundColorDark '#111111' --logoSplashScale 0.5  --android

Om bovenstaand commando niet telkens opnieuw in te typen, kan je het opnieuw toevoegen aan *package.json.

"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android",
    "android-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --android",
    "ios-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --ios",
    "gen-resources": "pnpm run ios-resources && pnpm run android-resources"
}








 



Nu kunnen het icon en splash screen gegenereerd worden via onderstaand commando.

pnpm run android-resources

Voorbeeldcode & samenvatting

Volledig uitgewerkte lesvoorbeelden met commentaaropen in new window

Samenvatting

Appendix

Deze sectie beschrijft enkele technieken die nuttig kunnen zijn tijdens het ontwikkelen en debuggen van een Ionic applicatie. Deze sectie bevat geen nieuwe leerstof, enkel zaken die het ontwikkelen aangenamer maken.

Capacitor run

Het run commando kan gebruikt worden om je applicatie automatisch uit te voeren op een specifiek toestel, zo moet je niet steeds naar Android Studio wisselen en daar op "run" drukken. Dit kan enkel als Android Studio geopend is. Voeg onderstaand script toe aan package.json.

"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android",
    "android-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --android",
    "ios-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --ios",
    "gen-resources": "pnpm run ios-resources && pnpm run android-resources",
    "android-run": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap run android"
}











 

Als je vervolgens het commando pnpm android-run uitvoert, krijg je een menu te zien waar de verschillende verbonden toestellen opgelijst zijn.

? Which device would you like to target?    ? Which device would you like to target?
> Xiaomi Redmi Note 9 Pro (f3ceaf2d)
  5.1  WVGA API 25 (emulator) (5.1_WVGA_API_25)
  Pixel 3 XL API 29 (emulator) (Pixel_3_XL_API_29)
  Pixel 3 API 29 (emulator) (Pixel_3_API_29)

Als je hier vervolgens een toestel kiest, wordt de app automatisch gecompileerd en uitgevoerd op het gespecifieerde toestel. Eens je dit commando één keer hebt uitgevoerd, kan je het aanpassen zodat dit menu niet langer nodig is. Elke van de toestellen bevat tussen ronde haken het id van het toestel, voor het geselecteerde toestel (in bovenstaande voorbeeld) is dit f3ceaf3d. Je kan dit id toevoegen aan het commando in package.json om de app steeds op hetzelfde toestel te openen.

"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android",
    "android-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --android",
    "ios-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --ios",
    "gen-resources": "pnpm run ios-resources && pnpm run android-resources",
    "android-run": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap run android",
    "android-run-default": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap run android --target f3ceaf2d"
}












 

Live Reloading

Waarschuwing

Op het netwerk van Thomas More is het niet mogelijk om live reloading te gebruiken. De development server zal wel starten maar is niet bereikbaar vanop je smartphone.

Telkens, na elke wijziging, je code compileren en je app uitvoeren op Android is niet ideaal. Tijdens het development process kan je ervoor kiezen om een lokaal een development server te draaien en deze website te tonen in je native Android app. Zo is elke wijziging in je code meteen zichtbaar op je smartphone, net alsof je ionic serve uitvoert. Dit is enkel mogelijk als je smartphone en computer met hetzelfde netwerk verbonden zijn.

Om van deze feature gebruik te kunnen maken is het nodig om native-runopen in new window (globaal) te installeren.

pnpm add -g native-run

Vervolgens voeg je opnieuw een script toe aan package.json.

"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android",
    "android-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5  --android",
    "ios-resources": "capacitor-assets generate --iconBackgroundColor #eeeeee --iconBackgroundColorDark #222222 --splashBackgroundColor #eeeeee --splashBackgroundColorDark #111111 --logoSplashScale 0.5 --ios",
    "gen-resources": "pnpm run ios-resources && pnpm run android-resources",
    "android-run": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap run android",
    "android-run-default": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap run android --target f3ceaf2d",
    "android-live": "ionic cap run android -l --external",
}













 

Als je vervolgens pnpm run android-live uitvoert krijg je een lijst van de verbonden Android toestellen te zien, kies hier het toestel waarop je de app wil uitvoeren.

? Which device would you like to target?    ? Which device would you like to target?
> Xiaomi Redmi Note 9 Pro (f3ceaf2d)
  5.1  WVGA API 25 (emulator) (5.1_WVGA_API_25)
  Pixel 3 XL API 29 (emulator) (Pixel_3_XL_API_29)
  Pixel 3 API 29 (emulator) (Pixel_3_API_29)

Vervolgens wordt er gevraagd op welk ip-adres de development server beschikbaar moet zijn, kies hier voor je ethernet of Wi-Fi adaptor. Let op, je computer en smartphone moeten op hetzelfde netwerk zitten.

? Please select which IP to use: (Use arrow keys)
> 192.168.1.237 (Ethernet)
  172.23.96.1 (vEthernet (WSL))

Tenslotte start de app op je Android toestel. De app op je toestel wordt verbonden met de development server op je computer. Dit betekent natuurlijk ook dat je deze server in je browser kunt openen, de uitvoer op de command line bevat het juiste IP-adres.

[ng] √ Compiled successfully.

[INFO] Development server running!

       Local: http://localhost:8100
       External: http://192.168.209.1:8100, http://192.168.30.1:8100, http://10.147.6.16:8100

       Use Ctrl+C to quit this process




 



Chrome inspect

Ionic applicaties worden geladen in een webview, de developer tools van een browser vormen dan ook een cruciaal onderdeel van het ontwikkelingsproces. Ook als je de applicatie bent aan het testen op een fysiek toestel kan je gebruik maken van de developer tools. Dit gaat echter enkel voor Android apps, en via de Google Chrome browser.

Als een app draait op je mobiel toestel, en dit toestel via USB verbonden is, kan je via Google Chrome de webview debuggen. Hiervoor navigeer je naar 'chrome://inspect' via de navigatiebalk van je browser. Vervolgens worden de beschikbare toestellen geladen (dit kan even duren).

Figuur 11: Google Chrome device inspect

Als je vervolgens op "inspect" klikt, wordt de webview geladen en kan je de dev tools gebruiken.

Figuur 12: Google Chrome Dev Tools
Laatst geüpdate:
Bijdragers: Sebastiaan Henau