Les 4: Native

Sebastiaan HenauOngeveer 9 minuten

Les 4: 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

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

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. Dit detail view kan geopend worden door op een afbeelding te drukken. Vervolgens kan de gebruiker door de verschillende foto's swipen. Om dit te implementeren kan je de <ion-slides>open in new window component (momenteel nog) gebruiken. Deze component is echter deprecated in de huidige versie van Ionic en zal in Ionic 7 verwijderd worden. Vanaf Ionic 7 wordt de bibliotheek Swiper.jsopen in new window gebruikt. Je gebruikt natuurlijk geen deprecated componenten in een nieuw project, implementeer deze opgave dus met Swiper. Je kan dit pakket installeren via onderstaand commando.

pnpm add swiper@^8.4.7

Gevaar

De laatste versie van Swiper bevat niet langer Angular componenten, maar is overgestapt op Web Componentsopen in new window. Dit zijn componenten die in elk framework gebruikt kunnen worden. De documentatie voor deze laatste release is echter nog heel beperkt, daarbovenop is er nog niet echt voldoende ondersteuning voor web components in WebStorm. Daarom wordt aangeraden om Swiper 8 te gebruiken voor deze oefening. Je vindt de documentatie op https://v8.swiperjs.com/open in new window.

In het vervolg van deze opgave gaan we er dan ook van uit dat je versie 8 gebruikt.

De documentatie over hoe de CSS-files geïmporteerd moeten worden is contradictorisch en niet heel duidelijk, hieronder vind je de nodige SCSS-regels. Ga voor verdere info over hoe de swiper component gebruikt kan worden, kijken in de documentatieopen in new window.

@import 'swiper/swiper-bundle.min.css';

.swiper {
  height: 100%;
}

Naast de slides, bevat de nieuwe pagina (SwipePage) ook een <ion-footer>open in new window 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.

Figuur 1: Slide View: PWA (links) & Native (rechts)

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 APIopen in new window 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.

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>open in new window component.

Figuur 2: Foto verwijderen

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, dit kan door volgende code toe te voegen aan je TypeScript file. Vervolgens zijn alle properties en methodes die in de Swiper APIopen in new window beschikbaar via swiperRef (lijn 8).

Let op, de variabele swiper is niet beschikbaar in de constructor.

export class SwipePage implements OnInit {
    @ViewChild(SwiperComponent) swiper?: SwiperComponent;
    
    someMethod(): void {
        // Do something with the Swiper
        // swiperRef offers access to all methods en properties.
        // https://swiperjs.com/swiper-api#methods-and-properties
        this.swiper.swiperRef...
    }
}

 





 


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.

Gebruik setInterval en setTimeout via window anders krijg je type errors omdat Webstorm denk dat de setInterval of setTimeout functies van Node gebruikt wordt, in de plaats van de functies uit de browser

const timeoutId = window.setTimeout(() => {
}, timeOutInMs);
const intervalId = window.setInterval(() => {
}, intervalInMs);

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.

Figuur 3: Fullscreen view

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 op die manier ook geopend worden, in de plaats van met de fullscreen knop. Om het dubbelklikken te implementeren kan je gebruikmaken van de voorbeelden op de gesturesopen in new window pagina van het Ionic Framework.

Koppel het dubbelklik event aan de swiperRef 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. 
  }
}

 





 
Figuur 4: Fullscreen openen/sluiten

Oefening 1.5: Oriëntatie aanpassen

Een Capacitor applicatie past zich automatisch aan als de smartphone geroteerd wordt, maar daarvoor moet deze optie natuurlijk aan staan in het OS. Gebruik de screen orientationopen in new window 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).

Figuur 5: Oriëntatie wijzigen

Oefening 1.6: First run melding

Het switchen tussen fullscreen modus en de normale modus is eventueel niet voor elke gebruiker duidelijk. Gebruik de preferencesopen in new window 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 geraken.

Figuur 6: Alert voor de eerste keer dat fullscreen geopend wordt

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

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>open in new window 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.

Figuur 7: Tijdstip in de deadline

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 een toast getoond. 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-inopen in new window.

Figuur 8: Settings

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.

Momenteel wordt het id van een taak bepaald op basis van een instantievariabele die elke keer dat er een nieuwe taak gemaakt wordt met 1 verhoogt wordt. We kunnen dit natuurlijk ook voortzetten met persistente data, maar in dat geval moet de instantievariabele id ook persistent gemaakt worden of moeten we een SQL database gebruiken als back-end. Een properdere oplossing bestaat uit het gebruik van de uuidopen in new window library. Je kan deze library installeren via onderstaand commando.

pnpm add uuid

Vervolgens kan je als volgt een universally unique id genereren.

import {v4 as uuid} from 'uuid';

const id = uuid();

Pas de bestaande code aan zodat de taken een string als id krijgen. Doe hetzelfde voor de labels. Zorg er verder voor dat de taken en labels persistent zijn, i.e. dat de taken opnieuw ingelezen worden na het herstarten van de app en dat de taken bij elke wijziging weggeschreven worden.

Je gebruikt de capacitor-data-storage-sqliteopen in new window plug-in om persistentie te implementeren. Volg de installatie instructies voor de Android en Web platformen.

Info

Deze plug-in bied 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 sqliteopen in new window plugin gebruiken.

De documentatie voor deze plug-in is niet heel overzichtelijk of eenvoudig te interpreteren. Je kan zelf op zoek gaan op het GitHubopen in new window 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. Je kan de API documentatieopen in new window raadplegen om informatie te vinden over de set en get methodes die op CapacitorDataStorageSqlite beschikbaar zijn nadat de #initStore methode uitgevoerd is.

import {Injectable} from '@angular/core';
import {CapacitorDataStorageSqlite, capOpenStorageOptions} from 'capacitor-data-storage-sqlite';

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

    readonly #options: capOpenStorageOptions = {
        database: 'todo',
        table: 'keyValueStorageSqlite',
    };

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

    /**
     * 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;
        } 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. Doe dit door middel van de Local Notifications plug-inopen in new window.

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.

Figuur 9: Windows 11 notification

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

Figuur 10: Android notification

De 1 minuten komt uit de waarde die de gebruiker ingesteld heeft in het settings scherm.

Hint

Om de notifications in te plannen kan de addMinutesopen in new window methode uit date-fnsopen in new window van pas komen. Om de geselecteerde datum om te vormen naar een JavaScript Date object kan de parseISOopen in new window methode uit date-fnsopen in new window gebruikt worden. Je kan deze library installeren via onderstaand commando.

pnpm add date-fns

Oefening 2.6: Icon & Splash

Download icon.png en zet het vervolgens op de correcte plaats in je project. Genereer het splashscreen en icoon. Zorg ervoor dat zowel het splashscreen als het icoon de achtergrondkleur #e2e2e2 krijgen.

Figuur 11: Splash & Icon

Waarschuwing

Omwille van de bug die in de lestekst beschreven is, zal het splashscreen momenteel een kleiner icoon bevatten.

Oefening 2.7: Notification met een icoon

Ga in de documentatie van de Local Notifications plug-inopen in new window op zoek naar een manier om een icoontje toe te voegen aan een notification. Gebruik hiervoor hetzelfde icoon als voor de app.

Figuur 12: Notification met aangepast icoon

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.

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 Appopen in new window plug-in.

Figuur 13: Close toast
Laatst geüpdate:
Bijdragers: Sebastiaan Henau