3. Native
3. Native
Tijdens deze oefeningen bouw je verder aan de Gallery en To-Do apps, je breidt de gallery app uit met een extra view waarmee tussen de foto's geswipet kan worden en je voegt persistentie en notificaties toe aan de To-Do app. Tijdens deze oefeningenreeks oefen je op
- De concepten uit de vorige lessen
- Het gebruikt van Capacitor plug-ins
- Het compileren van een webproject als Android app
- Het lezen van documentatie
Oefening 1: Gallery
De gallery app wordt uitgebreid met een detail view waar je tussen de verschillende foto's kan swipen en foto's kunt verwijderen. Verder wordt een fullscreen modus toegevoegd en wordt het mogelijk gemaakt om de app in landscape of portrait modus te zetten.
Voor de screenshots is een placeholder afbeelding gebruikt. Je moet de afbeelding zelf dus niet reproduceren in je app.
Oefening 1.1: Detail (swipe) view
Maak een nieuwe pagina voor het detail view (SwipePage). Deze view kan geopend worden door op een afbeelding te drukken, vervolgens kan de gebruiker door de verschillende foto's swipen. Om dit te implementeren gebruik je Swiper.js. De installatie van deze bibliotheek is relatief complex en de documentatie is verre van perfect, daarom worden hieronder de nodige stappen beschreven. Indien je een extra uitdaging wil, kan je proberen om de configuratie uit te voeren aan de hand van de documentatie voor Swiper Element (WebComponent).
Installatie Swiper
De eerste stap is natuurlijk het installeren van de bibliotheek.
Swiper is gebouwd met Web Components, een manier om framework agnostische componenten te bouwen. Via deze techniek kan je dus een component bouwen in Vue en deze in een Angular project gebruiken (of omgekeerd). Web components gebruiken zelfgedefinieerd tags, om deze correct te renderen, moeten deze via een JavaScript functie geregistreerd worden in de browser. Omdat dit slechts één keer mag gebeuren, plaatsen we deze code in main.ts.
import {enableProdMode} from '@angular/core'
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'
// import function to register Swiper custom elements
import { register } from 'swiper/element/bundle'
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).then()
// register Swiper custom elements
register()Zoals eerder gezegd, gebruiken Web Components een custom tag, de Angular compiler herkent deze tags standaard niet. Om dit probleem op te lossen moeten we Angular vertellen dat er zulke tags gebruikt zullen worden in een bepaalde pagina. Dit kan door middel van enkele wijzigingen aan de module file van de pagina waarin we de component gaan gebruiken.
Eens deze stappen doorlopen zijn, kan de component als volgt gebruikt worden in een template.
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core'
import {CommonModule} from '@angular/common'
import {FormsModule} from '@angular/forms'
import {IonicModule} from '@ionic/angular'
import {SwipePageRoutingModule} from './swipe-routing.module'
import {SwipePage} from './swipe.page'
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
SwipePageRoutingModule
],
declarations: [SwipePage],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SwipePageModule {}<swiper-container>
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<swiper-slide>Slide 3</swiper-slide>
...
</swiper-container>Swipe page lay-out
Naast de slides, bevat de nieuwe pagina (SwipePage) ook een <ion-footer> component, deze bevat op zijn beurt drie knoppen. De eerste 2 knoppen zijn links uitgelijnd en bevatten de Ionic iconen phone-portrait en phone-landscape, omdat de oriëntatie enkel op een fysiek toestel aangepast kan worden, worden deze knoppen niet getoond als de app als PWA uitgevoerd wordt. De laatste knop is links uitgelijnd en bevat het icoon trash. Momenteel moeten de knoppen nog niets doen.
In de header wordt een knop voorzien waarmee we naar full-screen modus kunnen overschakelen. Deze knop bevat het expand icoon. Ook deze knop moet momenteel nog niets doen.
Om de afbeelding te centreren kan je volgende CSS toevoegen aan de SwipePage.
swiper-container {
height: 100%;
}
swiper-slide {
display: flex;
align-content: center;
}
Je kan de pagina bekijken door op eender welke foto te klikken in de galerij. Je wordt dan rechtstreeks naar de geselecteerde foto gebracht, ook als de foto ergens midden in de galerij zit. Je kan dus zowel naar links als naar rechts swipen als je niet de eerste of laatste foto gekozen hebt. Ga in de Swiper API op zoek naar een parameter waarmee de eerst geselecteerde slide ingesteld kan worden. Alle parameters op de voorgaande link kunnen rechtstreeks gekoppeld worden aan de swiper component via component properties.
Waarschuwing
Ondanks wat er in de documentatie staat moet je de properties niet in kabab-case noteren maar kan je gewoon camelCase gebruiken.
WebStorm kan de properties van Web Component niet identificeren, je zal dus foutmeldingen zien als je een property probeert mee te geven aan *<swiper-container>, maar je code zal wel werken.
Oefening 1.2: Verwijderen
De foto's moeten verwijderd kunnen worden, maar de gebruiker moet de kans krijgen om deze actie te annuleren.
Op het moment dat de gebruiker op verwijderen drukt moet de foto onmiddellijk uit de view verdwijnen, maar pas 2 seconden later mag de foto effectief uit het filesysteem verwijderd worden. Als de gebruiker binnen de 2 seconden op ongedaan maken klikt, wordt de foto terug in de galerij geplaatst, op dezelfde locatie.
De boodschap om de foto terug te tonen is gebouwd met een <ion-toast> component.
Om dit te programmeren moet je weten welke foto er momenteel in de view staat, i.e. welke slide zichtbaar is. Hiervoor heb je toegang nodig tot de Swiper component zodat je de methodes en properties uit de API kan gebruiken. Je krijgt toegang door volgende code toe te voegen, vervolgens zijn alle properties en methodes beschikbaar vanaf dat de component gerenderd is.
Let op, de variabele swiper is niet beschikbaar in de constructor of in ngOnInit.
<swiper-container #swiperRef>
<swiper-slide>Slide 1</swiper-slide>
<swiper-slide>Slide 2</swiper-slide>
<swiper-slide>Slide 3</swiper-slide>
...
</swiper-container>import Swiper from 'swiper'
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'
export class SwipePage implements OnInit {
@ViewChild('swiperRef') swiperRef?: ElementRef
swiper?: Swiper
someMethod(): void {
// https://swiperjs.com/swiper-api#methods-and-properties
this.swiper?.someApiMethodOrProperty
}
ionViewDidEnter(): void {
// Methode wordt uitgevoerd nadat de view geladen is,
// zie les 6 voor meer details.
this.swiper = this.swiperRef?.nativeElement.swiper
}
}Hint
De setInterval en setTimeout functies geven een integer terug waarmee het interval of de timeout callback geannuleerd kunnen worden. De clearInterval en clearTimeout functies nemen deze integer als argument.
Als je ondersteuning voor None geactiveerd hebt in WebStorm krijg je foutmeldingen als je setInterval of setTimeout gebruikt. Node bevat namelijk soortgelijke functies, maar die geven iets anders terug. Je kan dit probleem oplossen door window.setTimeout en window.setInterval te gebruiken.
Oefening 1.3: FullScreen
Als de gebruiker op de fullscreen knop druk, worden de header en footer verborgen. Op die manier kunnen de foto's groter bekeken worden. Normaal gezien zorgt de CSS die je in oefening 1.1 toegevoegd heb er automatisch voor dat de foto's zich automatisch aanpassen aan de grotere viewport (indien dit mogelijk is zonder het aspect ratio aan te passen).

Oefening 1.4: Fullscreen afsluiten
De gebruiker kan de fullscreen modus afsluiten door 2 keer snel achter elkaar op de afbeelding te drukken. De fullscreen modus kan ook op deze manier geopend worden, in de plaats van met de fullscreen knop. Om het dubbelklikken te implementeren kan je gebruikmaken van de voorbeelden op de gestures pagina van het Ionic Framework.
Koppel het dubbelklik event aan this.swiper?.wrapperEl die je reeds in de voorgaande oefening gebruikt hebt. Let op, deze swiperRef is niet beschikbaar in de constructor of ngOnInit methode, je kan gebruik maken van de ionViewDidEnter lifecycle methode, in de laatste les worden deze en andere lifecycle methodes in detail besproken. Je zal het dubbelklik event dus pas in ionViewDidEnter kunnen koppelen.
export class SwipePage implements OnInit {
@ViewChild(SwiperComponent) swiper?: SwiperComponent;
ionViewDidEnter(): void {
// this.swiper.swiperRef can at the earliest be used in this method,
// the element is not available in ngOnInit or the constructor.
}
}Oefening 1.5: Oriëntatie aanpassen
Gebruik de screen orientation plug-in om de landscape en portrait knoppen in de footer te implementeren. Logischerwijs moet de landscape knop de app in landscape modus zetten en moet de portrait knop de app in portrait modus zetten. Aangezien deze functionaliteit niet beschikbaar is in een PWA worden de knoppen op dit platform verborgen.
De knoppen tonen ook steeds de huidige modus, de knop die overeenkomt met de huidige modus gebruikt de filled variant van het icoon. De andere knop gebruikt de outline variant (-outline toevoegen achteraan de naam van het icoon).
Oefening 1.6: First run melding
Het switchen tussen fullscreen modus en de normale modus is mogelijks niet voor elke gebruiker duidelijk. Gebruik de preferences plug-in om bij te houden of de gebruiker de fullscreen modus al eens geactiveerd heeft of niet. Als het de eerste keer is dat de gebruiker de fullscreen modus activeert, toon je een melding over hoe je terug uit de fullscreen modus kan raken.
Oefening 2: To-Do
Tijdens deze oefening breid je de To-Do app uit met local notifications. Als de deadline van een taak nadert en de taak nog niet afgewerkt is, krijgt de gebruiker een melding te zien. Daarnaast voorzie je persistentie in de To-Do applicatie.
Deze oefening vertrekt van de oplossing van de oefeningen van les 2.
Oefening 2.1: Task view aanpassen
Momenteel kan je enkel een datum selecteren als deadline, dit is natuurlijk verre van ideaal. Ga in de documentatie van de <ion-datetime> component op zoek naar een gepaste manier om een time-picker toe te voegen. Je hoeft in de TypeScript code niets aan te passen, enkel de template moet aangepast worden.

Oefening 2.3: Settings pagina
Maak een nieuwe pagina aan voor instellingen en voeg deze toe aan het menu.
Via deze pagina kan de gebruiker aangeven of er al dan niet herinneringen gestuurd moeten worden als de deadline nadert (standaard false). Als de gebruiker hiervoor kiest, kan geselecteerd worden wanneer de melding moet verschijnen (standaard 30 minuten voor de deadline). Het is vanzelfsprekend dat deze instellingen bewaard blijven nadat de app afgesloten en herstart wordt.
De gebruiker moet natuurlijk toestemming geven om notifications te ontvangen, als dit geweigerd wordt, kan de optie ook niet geactiveerd worden. Als de gebruiker de optie probeert te activeren en de toestemming weigert, of de toestemming eerder geweigerd heeft, dan wordt er foutmelding getoond in een toast. De toast verdwijnt automatisch na 2 seconden, of zodra de gebruiker op de knop "Close" drukt. Om de toestemming aan te vragen en om te controleren welke toestemming er gegeven is, kan je gebruik maken van de Local Notifications plug-in.
Oefening 2.4: Persistente data
Momenteel gaan alle wijzigingen verloren nadat de applicatie opnieuw gestart wordt. Voor we dit probleem kunnen oplossen, moeten ook de id's van de taken aangepast worden.
Je gebruikt de capacitor-data-storage-sqlite plug-in om persistentie te implementeren. Volg de installatie-instructies voor de Android en Web platformen.
Info
Deze plug-in biedt eenvoudige key-value storage via een embedded SQLite database. Let op, dit is geen volwaardige SQL-database, relaties, tabellen, foreign keys, primary keys, ... worden niet ondersteund. Wil je dit toch implementeren dan kan je de complexere sqlite plugin gebruiken.
De documentatie voor deze plug-in is niet heel overzichtelijk of eenvoudig te interpreteren. Je kan zelf op zoek gaan op het GitHub repository of je kan de service die hieronder toegevoegd is gebruiken als basis.
Startcode
Onderstaande service kan gebruikt worden als basis om de persistentie te implementeren. In de API documentatie vind je informatie over de set en get methodes die op CapacitorDataStorageSqlite beschikbaar zijn nadat de #initStore methode uitgevoerd is. Gebruik deze methodes om onderstaande service uit te breiden met methodes om data in weg te schrijven en op te halen.
import {Injectable} from '@angular/core'
import {CapacitorDataStorageSqlite, capOpenStorageOptions} from 'capacitor-data-storage-sqlite'
@Injectable({
providedIn: 'root'
})
export class StorageService {
readonly #options: capOpenStorageOptions & {database: string} = {
database: 'todo',
table: 'keyValueStorageSqlite',
};
constructor() {
this.#initStore().then()
}
/**
* Open a connection to the database.
*
* @private
*/
async #initStore(): Promise<void> {
try {
await CapacitorDataStorageSqlite.openStore(this.#options)
} catch (err) {
console.log('Error initialising capacitor-data-storage-sqlite.')
console.log(err)
}
}
/**
* Check if a store is opened
*
* @returns True if the store is opened, false otherwise.
*/
async #isStoreOpen(): Promise<boolean> {
try {
const {result} = await CapacitorDataStorageSqlite.isStoreOpen({database: this.#options.database})
return result as boolean
} catch {
return false
}
}
/**
* Helper method to call before storing or retrieving something.
* If the store hasn't been opened yet, this method ensures that it is openend before this method resolves.
*
* @private
*/
async #ensureOpenStore(): Promise<void> {
const isOpen = await this.#isStoreOpen()
if (!isOpen) {
await this.#initStore()
}
}
}Oefening 2.5: Notifications
Schrijf een methode die voor elke onafgewerkte taak (met een deadline in de toekomst) een herinnering inplant.
Deze herinnering moet op het door de gebruiker gekozen aantal minuten voor de deadline getoond worden. Gebruik de Local Notifications plug-in om de meldingen te tonen.
Hou er rekening mee dat je eerst alle bestaande notificaties moet verwijderen voordat je nieuwe inplant, anders kan het zijn dat je voor eenzelfde taak 20 notifications stuurt. Plan de notification in op na elke actie die een invloed kan hebben op de status van een taak of op de deadline.
Op Windows 11 ziet de melding er als volgt uit.

Een notificatie ziet er als volgt uit op een Android toestel.

De 1 minuten komt uit de waarde die de gebruiker ingesteld heeft in de settings pagina.
Hint
Om de notifications in te plannen kan de addMinutes methode uit date-fns van pas komen. Om de, in de UI, geselecteerde datum om te vormen naar een JavaScript Date object kan de parseISO methode uit date-fns gebruikt worden. Je kan deze library installeren via onderstaand commando.
Oefening 2.6: Icon & Splash
Download icon.png en zet het vervolgens op de correcte plaats in je project. Genereer het splashscreen en icoon en zorg ervoor dat zowel het splashscreen als het icoon de achtergrondkleur #e2e2e2 krijgen.

Oefening 2.7: Notification met een icoon
Ga in de documentatie van de Local Notifications plug-in op zoek naar een manier om een icoontje toe te voegen aan een notification. Gebruik hiervoor hetzelfde icoon als voor de app.

Oefening 2.8: App sluiten met de back button
Android toestellen hebben een back-knop waarmee naar de vorige pagina in een app genavigeerd kan worden, en waarmee de app gesloten kan worden. De eerste functionaliteit wordt door Ionic afgehandeld, de tweede werkt niet standaard.
Ga op zoek in de Capacitor documentatie en zorg er voor dat de app afgesloten kan worden via de back button. Na één keer te drukken op de back button, wordt een toast getoond met de boodschap 'Press the back button again to close the app.'. Als de gebruiker dit doet binnen 2 seconden wordt de app gesloten, anders verdwijnt de toast en moet de gebruiker opnieuw 2 keer drukken om de app te sluiten.
Op dit te implementeren kan je gebruik maken van de Capacitor App plug-in.
