Nozioni di base su AWS

Creazione di un'applicazione Android

Creazione di un'applicazione Android semplice utilizzando AWS Amplify

Modulo 4: Aggiunta di un'API GraphQL e di un database

In questo modulo verrà utilizzata l'interfaccia a riga di comando (CLI) di Amplify per configurare e aggiungere un'API GraphQL alla tua app.

Introduzione

Ora che abbiamo creato e configurato l'applicazione con l'autenticazione utente, aggiungiamo un'API e le operazioni CRUD (Crea, Leggi, Aggiorna, Elimina) in un database.

In questo modulo aggiungerai un'API alla nostra app usando l'interfaccia a riga di comando (CLI) e le librerie di Amplify. L'API che creerai è un'API GraphQL che sfrutta AWS AppSync (un servizio GraphQL gestito) che è gestito da Amazon DynamoDB (un database NoSQL). Per un'introduzione a GraphQL, visita questa pagina.

L'applicazione che creerai sarà un'app di note che consentirà agli utenti di creare, eliminare ed elencare note. Questo esempio ti darà un'idea su come creare molti tipi famosi di applicazioni CRUD+L (crea, leggi, aggiorna, elimina ed elenca).

Avrai modo di approfondire i seguenti aspetti

  • Creazione e distribuzione di un'API GraphQL
  • Scrittura di un codice front-end per interagire con l'API

Concetti chiave

API: fornisce un'interfaccia di programmazione che consente la comunicazione e le interazioni tra più intermediari software.

GraphQL: un linguaggio di query e un'implementazione API lato server basati su una rappresentazione tipizzata dell'applicazione. Questa rappresentazione API viene dichiarata usando uno schema basato sul tipo di sistema GraphQL. (Per ulteriori informazioni su GraphQL, visita questa pagina.)

 Tempo richiesto per il completamento

20 minuti

 Servizi utilizzati

Implementazione

  • Per creare l'API GraphQL e il relativo database di supporto, apri un terminale ed esegui questo comando dalla directory del progetto:

    amplify add api
    
    ? Please select from one of the below mentioned services: select GraphQL and press enter
    ? Provide API name: select the default, press enter
    ? Choose the default authorization type for the API: use the arrow key to select Amazon Cognito User Pool and press enter
    ? Do you want to configure advanced settings for the GraphQL API: select the default No, I am done and press enter
    ? Do you have an annotated GraphQL schema? keep the default N and press enter
    ? What best describes your project: choose any model, we are going to replace it with our own anyway. Press enter
    ? Do you want to edit the schema now? type Y and press enter

    L'editor di testo predefinito che hai selezionato quando hai inizializzato il progetto (Amplify init) apre uno schema di dati precompilato.

    Elimina lo schema e sostituiscilo con lo schema GraphQL della nostra applicazione:

    type NoteData
    @model
    @auth (rules: [ { allow: owner } ]) {
        id: ID!
        name: String!
        description: String
        image: String
    }

    Il modello di dati è composto da una classe NoteData e da quattro proprietà: ID e nome sono obbligatorie, descrizione e immagine sono stringhe facoltative.

    Il Transformer @model indica che vogliamo creare un database per archiviarvi i dati.

    Il Transformer @auth aggiunge le regole di autenticazione per consentire l'accesso a questi dati. Per questo progetto, vogliamo che l'accesso sia consentito solo al proprietario dei NoteData.

    Una volta completato, non dimenticarti di salvare, quindi torna al tuo terminale per dire ad Amplify CLI che hai finito.

    ? Press enter to continue, press enter.

    Dopo alcuni secondi, dovresti visualizzare un messaggio di corretta esecuzione:

    GraphQL schema compiled successfully.
  • Sulla base della definizione del modello di dati GraphQL appena creato, Amplify genera un codice lato client (ossia codice Swift) per rappresentare i dati nella nostra applicazione.

    Per generare il codice, esegui il comando seguente nel tuo terminale:

    amplify codegen models

    Come puoi vedere, il comando crea file Java nella directory java/com/amplifyframework.datastore.generated.model:

    ➜  Android Getting Started git:(master) ✗ ls -al app/src/main/java/com/amplifyframework/datastore/generated/model 
    total 24
    drwxr-xr-x  4 stormacq  admin   128 Oct  7 15:27 .
    drwxr-xr-x  3 stormacq  admin    96 Oct  7 15:27 ..
    -rw-r--r--  1 stormacq  admin  1412 Oct  7 15:27 AmplifyModelProvider.java
    -rw-r--r--  1 stormacq  admin  7153 Oct  7 15:27 NoteData.java

    I file vengono importati automaticamente nel tuo progetto.

  • Per distribuire il database e l'API di back-end appena creati, apri il terminale ed esegui il comando:

    amplify push
    # press Y when asked to continue
    
    ? Are you sure you want to continue? accept the default Y and press enter
    ? Do you want to generate code for your newly created GraphQL API type N and press enter

    Dopo alcuni minuti, dovresti visualizzare un messaggio di corretta esecuzione:

    ✔ All resources are updated in the cloud
    
    GraphQL endpoint: https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql
  • Prima di passare al codice, torna in Android Studio, aggiungi la seguente dipendenza al file build.gradle del modulo, insieme alle altre implementazioni 'amplifyframework' aggiunte in precedenza e fai clic su Sincronizza ora quando richiesto:

    dependencies {
        implementation 'com.amplifyframework:aws-api:1.4.0'
        implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'
    }
  • Apri Backend.swift e aggiungi una riga nella sequenza di inizializzazione di Amplify nel metodo initialize(). Il blocco try/catch completo dovrebbe avere il seguente aspetto:

    try {
        Amplify.addPlugin(AWSCognitoAuthPlugin())
        Amplify.addPlugin(AWSApiPlugin())
        Amplify.configure(applicationContext)
    
        Log.i(TAG, "Initialized Amplify")
    } catch (e: AmplifyException) {
        Log.e(TAG, "Could not initialize Amplify", e)
    }
  • Il nostro progetto contiene già un modello di dati per rappresentare una nota. In questo tutorial continueremo a utilizzare quel modello e a fornire un modo facile per convertire un NoteData in una nota. Apri UserData.kt e aggiungi due componenti: una proprietà dinamica che restituisce un oggetto NoteData da un elemento UserData.Note e l'opposto, un metodo statico che accetta un'API NoteData e restituisce un elemento Userdata.Note.

    All'interno della classe di dati Note, aggiungi quanto segue:

    // return an API NoteData from this Note object
    val data : NoteData
        get() = NoteData.builder()
                .name(this.name)
                .description(this.description)
                .image(this.imageName)
                .id(this.id)
                .build()
    
    // static function to create a Note from a NoteData API object
    companion object {
        fun from(noteData : NoteData) : Note {
            val result = Note(noteData.id, noteData.name, noteData.description, noteData.image)
            // some additional code will come here later
            return result
        }
    }     

    Assicurati di importare la classe NoteData dal codice generato.

  • Aggiungiamo tre metodi per chiamare la nostra API: un metodo per eseguire una query della nota, un metodo per creare una nuova nota e un metodo per eliminare una nota. Tieni presente che questi metodi funzionano nel modello di dati dell'applicazione (Note) per semplificare l'interazione dall'interfaccia utente. Questi metodi convertono la nota in oggetti NoteData di GraphQL in modo trasparente.

    Apri il file Backend.swift e aggiungi il seguente frammento alla fine della classe Backend:

    fun queryNotes() {
        Log.i(TAG, "Querying notes")
    
        Amplify.API.query(
            ModelQuery.list(NoteData::class.java),
            { response ->
                Log.i(TAG, "Queried")
                for (noteData in response.data) {
                    Log.i(TAG, noteData.name)
                    // TODO should add all the notes at once instead of one by one (each add triggers a UI refresh)
                    UserData.addNote(UserData.Note.from(noteData))
                }
            },
            { error -> Log.e(TAG, "Query failure", error) }
        )
    }
    
    fun createNote(note : UserData.Note) {
        Log.i(TAG, "Creating notes")
    
        Amplify.API.mutate(
            ModelMutation.create(note.data),
            { response ->
                Log.i(TAG, "Created")
                if (response.hasErrors()) {
                    Log.e(TAG, response.errors.first().message)
                } else {
                    Log.i(TAG, "Created Note with id: " + response.data.id)
                }
            },
            { error -> Log.e(TAG, "Create failed", error) }
        )
    }
    
    fun deleteNote(note : UserData.Note?) {
    
        if (note == null) return
    
        Log.i(TAG, "Deleting note $note")
    
        Amplify.API.mutate(
            ModelMutation.delete(note.data),
            { response ->
                Log.i(TAG, "Deleted")
                if (response.hasErrors()) {
                    Log.e(TAG, response.errors.first().message)
                } else {
                    Log.i(TAG, "Deleted Note $response")
                }
            },
            { error -> Log.e(TAG, "Delete failed", error) }
        )
    }

    Assicurati di importare la classe ModelQuery, ModelMutation e NoteData dal codice generato.

    Infine, dobbiamo chiamare l'API per eseguire una query dell'elenco di note per l'utente attualmente connesso quando l'applicazione viene avviata.

    Nel file Backend.swift, aggiorna il metodo updateUserData(withSignInStatus:Boolean) perché abbia il seguente aspetto:

    // change our internal state and query list of notes 
    private fun updateUserData(withSignedInStatus : Boolean) {
        UserData.setSignedIn(withSignedInStatus)
    
        val notes = UserData.notes().value
        val isEmpty = notes?.isEmpty() ?: false
    
        // query notes when signed in and we do not have Notes yet
        if (withSignedInStatus && isEmpty ) {
            this.queryNotes()
        } else {
            UserData.resetNotes()
        }
    }

    Ora, non ci rimane che creare un pezzo dell'interfaccia utente per creare una nuova nota e per eliminare una nota dall'elenco.

  • Ora che i pezzi del back-end e del modello di dati sono sistemati, l'ultima fase in questa sezione è consentire agli utenti di creare una nuova nota e di eliminarli.

    a. In Android Studio, in res/layout, crea un nuovo layout: fai clic con il tasto destro del mouse su layout e seleziona New (Nuovo), quindi Layout Resource File (File risorse di layout). Denomina il file activity_add_note e accetta tutti gli altri valori predefiniti. Fai clic su OK.

    Apri il file activity_add_note appena creato e sostituisci il codice generato incollando quanto segue:

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:fillViewport="true">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="8dp">
    
            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Create a New Note"
                android:textSize="10pt" />
    
            <EditText
                android:id="@+id/name"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:hint="name"
                android:inputType="text"
                android:lines="5" />
    
            <EditText
                android:id="@+id/description"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:hint="description"
                android:inputType="textMultiLine"
                android:lines="3" />
    
            <Space
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1" />
    
            <Button
                android:id="@+id/addNote"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:backgroundTint="#009688"
                android:text="Add Note" />
    
            <Button
                android:id="@+id/cancel"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:backgroundTint="#FFC107"
                android:text="Cancel" />
    
        </LinearLayout>
    </ScrollView>

    Si tratta di un layout molto semplice che consente di inserire un titolo e una descrizione della nota.

    b. Aggiungi una classe AddNoteActivity.

    In java/com.example.androidgettingstarted, crea un nuovo file kotlin AddActivityNote.kt, aprilo e aggiungi questo codice: 

    package com.example.androidgettingstarted
    
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import kotlinx.android.synthetic.main.activity_add_note.*
    import java.util.*
    
    class AddNoteActivity : AppCompatActivity()  {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_add_note)
    
            cancel.setOnClickListener {
                this.finish()
            }
    
            addNote.setOnClickListener {
    
                // create a note object
                val note = UserData.Note(
                    UUID.randomUUID().toString(),
                    name?.text.toString(),
                    description?.text.toString()
                )
    
                // store it in the backend
                Backend.createNote(note)
    
                // add it to UserData, this will trigger a UI refresh
                UserData.addNote(note)
    
                // close activity
                this.finish()
            }
        }
    
        companion object {
            private const val TAG = "AddNoteActivity"
        }
    }    

    Infine, in manifests, apri il file AndroidManifest.xml e aggiungi questo elemento di attività in qualsiasi punto del nodo dell'applicazione.

    <activity
        android:name=".AddNoteActivity"
        android:label="Add Note"
        android:theme="@style/Theme.GettingStartedAndroid.NoActionBar">
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.example.androidgettingstarted.MainActivity" />
    </activity>

    c. Aggiungi un FloatingActionButton "Add Note (Aggiungi nota)" nell'attività principale. In res/layout, apri il file activity_main.xml e aggiungi il Floating Action Button in alto a quello esistente.

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabAdd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:visibility="invisible"
        android:src="@drawable/ic_baseline_post_add"
        app:fabCustomSize="60dp"
        app:fabSize="auto"/>

    Aggiungi un'icona "Add Note (Aggiungi nota)" in res/drawable. Fai clic con il tasto destro del mouse su drawable, seleziona New (Nuovo), quindi Vector Asset (Asset vettore). Inserisci ic_baseline_add come nome e scegli l'icona Aggiungi dalla Clip Art. Fai clic su Next (Avanti), quindi su Finish (Fine).

    d. Aggiungi il codice per gestire il pulsante "Add Note (Aggiungi nota)".

    Le ultime due cose da fare per avere un "pulsante Aggiungi" completamente funzionante sono far apparire o scomparire il pulsante secondo il valore isSignedIn, e, ovviamente, aggiungere il codice per gestire i tocchi sul pulsante.

    Apri mainActivity.kt e aggiungi quanto segue alla fine del metodo onCreate():

    // register a click listener
    fabAdd.setOnClickListener {
        startActivity(Intent(this, AddNoteActivity::class.java))
    }

    Quindi, sempre nel metodo onCreate(), sostituisci UserData.isSignedIn.observe con questo codice:

    UserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp ->
        // update UI
        Log.i(TAG, "isSignedIn changed : $isSignedUp")
    
        //animation inspired by https://www.11zon.com/zon/android/multiple-floating-action-button-android.php
        if (isSignedUp) {
            fabAuth.setImageResource(R.drawable.ic_baseline_lock_open)
            Log.d(TAG, "Showing fabADD")
            fabAdd.show()
            fabAdd.animate().translationY(0.0F - 1.1F * fabAuth.customSize)
        } else {
            fabAuth.setImageResource(R.drawable.ic_baseline_lock)
            Log.d(TAG, "Hiding fabADD")
            fabAdd.hide()
            fabAdd.animate().translationY(0.0F)
        }
    })    

    Per verificare che tutto funzioni come previsto, crea il progetto. Fai clic sul menu Build (Crea) e seleziona Make Project (Crea progetto) oppure, nei Mac, digita ⌘F9. Non dovrebbero essere restituiti errori.

    Quando esegui l'applicazione, vedrai che il pulsante "Add Note (Aggiungi nota)" appare quando un utente esegue l'accesso e scompare quando un utente si scollega. Puoi ora aggiungere una nota.

  • Il comportamento Scorri per eliminare può essere aggiunto aggiungendo un gestore di tocchi all'elenco di note. Il gestore di tocchi ha la funzione di disegnare lo sfondo rosso, l'icona di eliminazione e di chiamare il metodo Backend.delete() quando il tocco viene rilasciato.

    a. Crea una nuova classe SimpleTouchCallback. In java/com, fai clic con il tasto destro del mouse su example.androidgettingstarted, seleziona New (Nuovo), quindi Kotlin File (File Kotlin) e inserisci SwipeCallback come nome.

    Incolla il codice in basso nel nuovo file:

    package com.example.androidgettingstarted
    
    import android.graphics.Canvas
    import android.graphics.Color
    import android.graphics.drawable.ColorDrawable
    import android.graphics.drawable.Drawable
    import android.util.Log
    import android.widget.Toast
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.ContextCompat
    import androidx.recyclerview.widget.ItemTouchHelper
    import androidx.recyclerview.widget.RecyclerView
    
    
    // https://stackoverflow.com/questions/33985719/android-swipe-to-delete-recyclerview
    class SwipeCallback(private val activity: AppCompatActivity): ItemTouchHelper.SimpleCallback(
        0,
        ItemTouchHelper.LEFT
    ) {
    
        private val TAG: String = "SimpleItemTouchCallback"
        private val icon: Drawable? = ContextCompat.getDrawable(
            activity,
            R.drawable.ic_baseline_delete_sweep
        )
        private val background: ColorDrawable = ColorDrawable(Color.RED)
    
        override fun onChildDraw(
            c: Canvas,
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            dX: Float,
            dY: Float,
            actionState: Int,
            isCurrentlyActive: Boolean
        ) {
            super.onChildDraw(
                c,
                recyclerView,
                viewHolder,
                dX,
                dY,
                actionState,
                isCurrentlyActive
            )
            val itemView = viewHolder.itemView
            val backgroundCornerOffset = 20
            val iconMargin = (itemView.height - icon!!.intrinsicHeight) / 2
            val iconTop = itemView.top + (itemView.height - icon.intrinsicHeight) / 2
            val iconBottom = iconTop + icon.intrinsicHeight
            val iconRight: Int = itemView.right - iconMargin
            if (dX < 0) {
                val iconLeft: Int = itemView.right - iconMargin - icon.intrinsicWidth
                icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
                background.setBounds(
                    itemView.right + dX.toInt() - backgroundCornerOffset,
                    itemView.top, itemView.right, itemView.bottom
                )
                background.draw(c)
                icon.draw(c)
            } else {
                background.setBounds(0, 0, 0, 0)
                background.draw(c)
            }
        }
    
        override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean {
            Toast.makeText(activity, "Moved", Toast.LENGTH_SHORT).show()
            return false
        }
    
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {
    
            Toast.makeText(activity, "deleted", Toast.LENGTH_SHORT).show()
    
            //Remove swiped item from list and notify the RecyclerView
            Log.d(TAG, "Going to remove ${viewHolder.adapterPosition}")
    
            // get the position of the swiped item in the list
            val position = viewHolder.adapterPosition
    
            // remove to note from the userdata will refresh the UI
            val note = UserData.deleteNote(position)
    
            // async remove from backend
            Backend.deleteNote(note)
        }
    }

    Le righe di codice importanti si trovano nel metodo onSwiped(). Quando metodo viene chiamato quando termina il gesto di scorrimento. Inseriamo la posizione nell'elenco relativo all'elemento di scorrimento e rimuoviamo la nota corrispondente dalla struttura UserData (questa operazione aggiorna l'interfaccia utente) e dal back-end del cloud.

    b. Ora che abbiamo una classe, aggiungiamo un'icona "Delete (Elimina)" in res/drawable. Fai clic con il tasto destro del mouse su drawable, seleziona New (Nuovo), quindi Vector Asset (Asset vettore). Inserisci ic_baseline_delete_sweep come nome e scegli l'icona "delete sweep" dalla Clip Art. Fai clic su Next (Avanti), quindi su Finish (Fine).

    c. Incolla il codice in basso nel nuovo file. Aggiungi il comportamento di scorrimento per eliminare il gestore di gesti in RecyclerView.

    In java/com/example.androidgettingstarted, apri MainActivity.kt e aggiungi queste due righe di codice in setupRecyclerView:

    // add a touch gesture handler to manager the swipe to delete gesture
    val itemTouchHelper = ItemTouchHelper(SwipeCallback(this))
    itemTouchHelper.attachToRecyclerView(recyclerView)
  • Per verificare che tutto funzioni come previsto, crea ed esegui il progetto. Fai clic sull'icona Run (Esegui) ▶️ nella barra degli strumenti o digita ^ R. Non dovrebbero essere restituiti errori.

    Supponendo che non ti sia ancora disconnesso, l'applicazione inizia nell'elenco vuoto. Ora è presente un pulsante Add note (Aggiungi nota) per aggiungere una nota. Tocca il segno Add Note (Aggiungi nota), inserisci un titolo, una descrizione, tocca il pulsante Add Note (Aggiungi nota) e la nota apparirà nell'elenco.

    Puoi eliminare la nota facendo scorrere la riga verso sinistra.

    Questo è il flusso completo.

Conclusione

Hai creato un'app Android. Usando AWS Amplify hai aggiunto un'API GraphQL e hai configurato le funzionalità di creazione, lettura ed eliminazione nella tua app.

Nel prossimo modulo, aggiungeremo l'interfaccia utente e il comportamento per gestire le immagini.

Questo modulo è stato utile?

Aggiungi storage