3. Native
3. 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.
Begrip: Plugin
In de context van hybrid mobile app development, is een plug-in een in Swift of Java/Kotlin 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.
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 de alternatieven die hieronder besproken worden.
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 website. Sommige van deze enterprise plug-ins kunnen vervangen worden met alternatieven. Voor Authentication kan bijvoorbeeld Capacitor Firebase gebruikt worden, voor Biometrics is Capacitor Native Biometric beschikbaar.
Community plug-ins
Er zijn honderden community plug-ins beschikbaar voor Capacitor, deze plug-ins zijn open-source en voornamelijk ontwikkeld door developers die Capacitor gebruiken. De meeste plug-ins zijn up-to-date en werken vlot, maar er zijn natuurlijk ook uitzonderingen die niet meer actief ontwikkeld worden en mogelijks niet meer compatibel zijn met de laatste Android/iOS-versies.
Er is een officieel gecureerde lijst van plug-ins beschikbaar voor Capacitor. De plug-ins in deze lijst zijn door de ontwikkelaars van Capacitor gecontroleerd en zouden kwalitatief in orde moeten zijn. Deze lijst is echter niet exhaustief, verschillende nuttige plug-ins zoals Capacitor Firebase, worden hier niet vermeld. Het is dus regelmatig nodig om via Google (of een andere zoekmachine) op zoek te gaan naar een gepaste plug-in. Een zoekopdracht op NPM toont meer dan 2800 resultaten voor Capacitor, veel meer dus dan in de gecureerde lijst.
Cordova Plug-ins
Zo veel mogelijk te vermijden!
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 bijgewerkt zijn en niet meer werken met moderne Android/iOS-versies. Capacitor houdt 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 site.
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.
Cordova was een framework voor mobiele applicatie, niet voor PWA's. Er zijn dus plug-ins beschikbaar die geen (goede) ondersteuning bieden voor een progressive web app. Capacitor plug-ins bieden altijd ondersteuning voor PWA's als dit haalbaar is met browsertechnologieën, zaken zoals FaceId of vingerafdrukken kunnen niet zomaar gebruikt worden vanuit een browser.
Tenslotte bieden de meeste plug-ins voor Cordova enkel een JavaScript API en geen TypeScript API. Ionic heeft hiervoor 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 Ionic website. Ondanks de TypeScript wrappers, werken deze plug-ins niet altijd correct.
Gallery App
Info
In de verdere tekst van deze les gaan we ervan uit dat de lezer bekend is met promises. Indien dit niet het geval is verwijzen we door naar de appendix van les 4 uit de cursus frontend frameworks.
Er is één startbestand voorzien, we gebruiken dit bestand om foutmeldingen te tonen aan de gebruiker. Download het bestand en plaats het in /src/services.
error.service.ts
Om het gebruik van native features te illustreren, bouwen we een eenvoudige gallery app. Gebruikers kunnen foto's nemen via de camera, of uploaden vanuit het bestandssysteem op het toestel of de computer. Tijdens het bouwen van deze applicatie maken we kennis met Capacitor en enkele veel voorkomende plug-ins.
Gallery project aanmaken
We maken een nieuw project aan op dezelfde manier als in les 1. Vervolgens voegen we een nieuwe PhotoService toe. Tenslotte injecteren we deze service alvast in de constructor van de HomePage
export class HomePage {
photoService = inject(PhotoService)
constructor() {}
}Plug-ins installeren
Capacitor plug-ins moeten één per één geïnstalleerd worden. In Capacitor 3 was dit niet het geval, als je iets zou opzoeken, controleer dan grondig dat het gevonden resultaat geldig is voor Capacitor 4 of 5.
Om de applicatie uit te bouwen hebben we volgende plug-ins nodig:
- Camera: Deze plug-in wordt gebruikt om de camera aan te spreken en foto's te kunnen nemen.
- FileSystem: Deze plug-in wordt gebruikt om foto's weg te schrijven naar het bestandssysteem en deze terug uit te lezen.
- Preferences: Deze plug-in wordt gebruikt om de filesystem URI's van de foto's te bewaren.
- PWA Elements: PWA elements wordt, zoals hierboven al aangehaald, gebruikt om een UI te voorzien voor de Camera plug-in, als deze in een web-context uitgevoerd wordt.
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).
PWA Elements
Capacitor plug-ins ondersteunen PWA's. Maar in sommige gevallen is hiervoor een extra bibliotheek nodig, PWA Elements. 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 Camera 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 aanwezig is in de browser. Een plug-in zoals Clipboard 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.

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).

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 HomePage sectie besproken.
De PhotoService heeft een aantal instantie variabelen nodig. We bespreken deze hieronder.
De eerste variabele is photos, een lijst van alle foto's in de app. Als datatype gebruiken we de Photo interface uit de camera plug-in. De documentatie toont een reeks attributen die voldoende zijn voor onze doeleinden, het is dus niet nodig om zelf een interface 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 storageKey die de naam bevat waaronder we een lijst van filesystem URI's zullen bewaren. Deze naam wordt als key gebruikt voor de Preference plug-in.
De derde variabele 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 PermissionStatus uit de Camera plug-in. Deze plug-in kan camera en photos rechten krijgen. De camera rechten bepalen of een foto gemaakt mag worden via de camera, photos bepaald of een foto geladen mag worden uit de galerij. Als photos de status 'granted' heeft, kunnen we foto's ook bewaren in de galerij van het systeem (Android of iOS).
Permissions zijn niet altijd beschikbaar in een web-context. Het is bijvoorbeeld wel mogelijk om het recht om local notifications te tonen aan te vragen, maar de rechten om de camera te gebruiken kunnen pas aangevraagd worden op het moment dat je de camera probeert te gebruiken. 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.
import {Injectable} from '@angular/core'
import {Photo, PermissionStatus} from '@capacitor/camera'
@Injectable({
providedIn: 'root'
})
export class PhotoService {
readonly photos: Photo[] = []
readonly #storageKey = 'photos'
#photoURIs: string[] = []
#permissionGranted: PermissionStatus = {camera: 'granted', photos: 'granted'}
constructor() {
}
}De variabelen photos en storageKey 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 dat deze asynchrone methodes uitgevoerd zijn voordat de synchrone constructor afgewerkt is.
Filesystem URIs ophalen
We weten al dat het veld storageKey gebruikt wordt om de filesystem URI's weg te schrijven en in te lezen. Hiervoor gebruiken we de Preferences plug-in.
Waarschuwing
Let op, deze plug-in is ideaal voor kleine ongestructureerde data, maar voor grotere of complexere data is een cloud database of embedded SQL database beter.
Als je de preferences plug-in in je examenproject gebruikt om complexe data te bewaren, dan zal je hier niet alle punten voor krijgen.
Deze plug-in gebruikt de meest optimale opslaglocatie voor het platform waarop de applicatie draait. Voor webapplicaties (PWA's) wordt gebruik gemaakt van localStorage. Native iOS applicaties maken gebruikt van de UserDefaults. Voor Android apps worden de SharedPreferences gebruikt.
Waarschuwing
Gebruik localStorage nooit rechtstreeks in je mobiele applicatie, maar gebruik steeds de preferences plug-in. De localStorage kan automatisch geleegd worden om ruimte vrij te maken op het mobiele toestel, de preferences plug-in bewaard zodanig dat deze nooit verwijderd wordt tot de code daarvoor opgeroepen wordt of de app verwijderd wordt.
Maak ook geen gebruik van Ionic Storage, zonder extra configuratie is dit niets anders dan een wrapper rond localStorage.
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.
Natuurlijk roepen we de retrievePhotoURIs methode op als de service geïnitialiseerd wordt.
import {Preferences} from '@capacitor/preferences'
export class PhotoService {
readonly photos: Photo[] = []
readonly #storageKey = 'photos'
// Niet relevante code weggelaten.
constructor() {
this.#loadData().then(() => console.log('Data loaded'))
}
async #loadData() {
await this.#retrievePhotoURIs()
}
async #retrievePhotoURIs(): Promise<void> {
const {value} = await Preferences.get({key: this.#storageKey})
this.#photoURIs = value ? JSON.parse(value) : []
}
async #persistPhotoURIs(): Promise<void> {
await Preferences.set({
key: this.#storageKey,
value: JSON.stringify(this.#photoURIs),
})
}
}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.
Merk op dat we gebruik maken van de Capacitor core API's om het platform waarop de app draait uit te lezen.
import {Capacitor} from '@capacitor/core'
export class PhotoService {
#permissionGranted: PermissionStatus = {camera: 'granted', photos: 'granted'}
// Niet relevante code weggelaten.
constructor() {
this.#loadData().then(() => console.log('Data loaded'))
}
async #loadData() {
await this.#retrievePhotoURIs()
await this.#retrievePermissions()
}
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
Om een foto te nemen moeten we enkele stappen doorlopen, we beginnen met een foto te nemen via de Camera plug-in. Vervolgens krijgen we een base64 string terug.
Begrip: Base64
Elk binair bestand kan geconverteerd worden naar een base64 string. Dit is bijvoorbeeld gebeurd voor elk bestand dat je ooit via HTTP gedownload hebt, HTTP is een text-only protocol en dus moet elke afbeelding, video, ... eerst omgevormd worden naar een string voor het verstuurd kan worden.
Onderstaande string is een base64 representatie van het logo op de home page van deze site.
Een base64 string op zich geeft niet voldoende informatie, de metadata ontbreekt. We weten dus niet of een base64 string geïnterpreteerd moet worden als een afbeelding, video, pdf, exe, ...
Deze metadata kan toegevoegd worden door de base64 string om te vormen naar een data URL. Dit is een URL van de vorm data:[<mediatype>];base64,<data>. Hierin wordt mediatype een MIME-type van de vorm [data-type]/[extension], bijvoorbeeld image/png, video/mp4 of text/css.
Indien de data url verwijst naar een afbeelding of een video, dan kan je de url doorgeven aan het src attribuut van respectievelijk een img en video tag.
Nadat de foto genomen is moet deze weggeschreven worden naar het bestandssysteem, op een browser is er natuurlijk geen bestandssysteem ter beschikking. Een website kan enkel iets wegschrijven naar het bestandssysteem als de gebruiker een bestand download. Toch biedt de filesystem plug-in een oplossing, in de plaats van een echt bestandssysteem te gebruiken, wordt dit gesimuleerd met indexedDB. Voor ons, als gebruiker van de plug-in, is er geen verschil tussen het wegschrijven op een Android/iOS systeem en op een browser.
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 {
// Niet relevante code weggelaten.
#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. Op een PWA heeft enkel CameraSource.Camera zin, de PWA-UI die door pwa-elements toegevoegd is geeft de gebruiker namelijk altijd de keuze.
import {Photo, PermissionStatus, Camera, CameraSource} from '@capacitor/camera'
export class PhotoService {
// Niet relevante code weggelaten.
#determinePhotoSource(): CameraSource {
if (!Capacitor.isNativePlatform()) {
return CameraSource.Camera
}
if (this.#havePhotosPermission() && this.#haveCameraPermission()) {
return CameraSource.Prompt
} else {
return this.#havePhotosPermission() ?
CameraSource.Photos : CameraSource.Camera
}
}
}Foto nemen
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 de eerder geschreven methode determinePhotoSource gebruiken als bron en dat we eerst controleren of we de nodige rechten wel hebben. Als we deze rechten nog niet hebben, vragen we die eerste aan. Als de gebruiker de rechten dan nog niet geeft, tonen we een foutmelding.
export class PhotoService {
readonly photos: Photo[] = []
#photoURIs: string[]
#errorService = inject(ErrorService)
// Niet relevante code weggelaten.
async takePhoto(): Promise<void> {
if (!this.#haveCameraPermission() || !this.#havePhotosPermission()) {
await this.#requestPermissions()
}
if (!this.#haveCameraPermission() || !this.#havePhotosPermission()) {
this.#errorService.enqueueErrorMessage(`Can't take a photo because the right to do so has not been granted.`)
return
}
const image = await Camera.getPhoto({
quality: 90,
resultType: CameraResultType.Base64,
source: this.#determinePhotoSource(),
})
// Word verder in de les besproken.
const uri = await this.#saveImageToFileSystem(image)
this.#photoURIs.push(uri)
image.path = uri
this.#persistPhotoURIs().then()
image.dataUrl = `data:image/${image.format};base64,${image.base64String}`
this.photos.push(image)
}
}Natuurlijk moeten we de afbeelding ook weggeschreven naar het filesystem, hiervoor gebruiken we de filesystem plug-in. We kunnen de base64 string rechtstreeks meegeven aan de writeFile methode (lijn 42). Deze wordt automatisch geconverteerd naar binaire data, zodat de afbeelding geopend kan worden met elke image viewer applicatie. We moeten aangeven waar de afbeelding bewaard wordt, dit gebeurt op lijn 30. Er zijn een aantal mogelijke opties, maar Directory.Data 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 bewaren we de URI die we terugkrijgen van de filesystem plug-in via de preferences plug-in.
export class PhotoService {
// Niet relevante code weggelaten.
async #saveImageToFileSystem(photo: Photo): Promise<string> {
if (!photo.base64String) {
throw new Error('No photo data available')
}
const fileName = `${new Date().getTime()}.${photo.format}`
const savedFile = await Filesystem.writeFile({
path: fileName,
data: photo.base64String,
directory: Directory.Data,
})
return savedFile.uri
}
}Foto's inladen
Nu de foto's gemaakt kunnen worden, moeten we de optie voorzien om deze in te laden als de applicatie start. Hiervoor moeten we eerste de URI's van de foto's uitlezen en deze vervolgens gebruiken om de inhoud van de foto in te lezen in base64. Tenslotte kunnen we de base64 data gebruiken om een Photo object te instantiëren. Zoals eerder gezegd verwacht het Photo datatype dat format niet undefined is, we gebruiken een nieuwe methode die de extensie uit een URI haalt om de format property op te vullen tijdens het instantiëren.
Natuurlijk moet de nieuwe methode dan ook opgeroepen worden als de service geïnstantieerd wordt.
export class PhotoService {
// Niet relevante code weggelaten.
async #loadData() {
await this.#retrievePhotoURIs()
await this.#retrievePermissions()
await this.#loadPhotos()
}
async #loadPhotos(): 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,
})
}
}
#getPhotoFormat(uri: string): string {
const splitUri = uri.split('.')
return splitUri[splitUri.length - 1]
}
}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.
<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.photos">
<ion-img [src]="photo.dataUrl"></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 van het vak 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 (normaal gezien is dit al gebeurt tijdens het aanmaken van een Ionic project met ionic start).
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 is beschikbaar in de Capacitor documentatie.
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',
server: {
androidScheme: 'https',
},
}
export default configappId
appName
De appName parameter bevat de naam van de applicatie, de naam die voor gebruikers te zien 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 inlaadt in de webview. Gebruik onderstaande commando's om een nieuw de library te installeren en vervolgens een nieuw Android project aan te maken.
De map die door dit laatste commando aangemaakt wordt, zullen we (bijna) niet rechtstreeks aanpassen. Alle wijzigingen worden door Capacitor, Android Studio of Capacitor Assets gemaakt.

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. De vereisten rechten moeten uitdrukkelijk vermeld worden in AndroidManifest.xml, doe je dit niet, dan werkt de requestPermission methode van de plug-in niet.
Voor de Camera plug-in zijn onderstaande rechten nodig.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
</manifest>De Filesystem 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 Preferences 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 --prodBovenstaand 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.
Tenslotte moet het Android project geopend worden in Android Studio, hiervoor kan je het volgende commando gebruiken.
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. In package.json kunnen nieuw scripts gedefinieerd worden, het onderstaande script voor de drie commando's uit in één keer.
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint",
"android": "ionic build --prod && pnpm exec cap sync android && pnpm exec cap open android"
}Dit nieuwe script kan dan via onderstaand commando uitgevoerd worden.
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 Studio. 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 installatiehandleiding.
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 Google, maar deze zijn niet uitgeschreven voor elk toestel. Bepaalde fabrikanten bouwen hun eigen shell bovenop Android en tonen het build nummer op een andere locatie of laten de developer settings op een andere manier activer.
| Device | Setting |
|---|---|
| Google Pixel | Settings > About phone > Build number |
| Samsung Galaxy S8 en nieuwe | Settings > About phone > Software information > Build number |
| LG G6 en nieuwer | Settings > About > Software information > More > Build number of Settings > System > About phone > Software information > More > Build number |
| OnePlus 5T en nieuwer | Settings > About phone > 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) en nieuwer: Settings > System > Advanced > Developer Options > USB debugging
- Android 8.0.0 (API level 26) en Android 8.1.0 (API level 26): Settings > System > Developer Options > USB debugging
- Android 7.1 (API level 25) en ouder: 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.

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.

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.

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 Screen plug-in gebruiken. Onderstaande gif toont hoe een splash screen werkt.
Om een splash screen te generen gebruiken we de tool capacitor-assets, deze tool kan geïnstalleerd worden via onderstaand commando.
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.

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).

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 --androidInfo
Als het bovenstaande commando vasthangt moet je Android Studio sluiten en het commando opnieuw uitvoeren.
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.
Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
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.
Voeg onderstaand script toe aan package.json. Dit commando werkt enkel als Android Studio geopend is.
"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-run (globaal) te installeren.
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 processChrome 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).

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