Introducción a AWS

Crear una aplicación Android

Crear una aplicación Android sencilla con AWS Amplify

Módulo 4: agregar una API de GraphQL y base de datos

En este módulo usará las CLI y bibliotecas de Amplify para configurar y agregar la API de GraphQL a su aplicación.

Introducción

Ahora que hemos creado y configurado la aplicación con autenticación de usuario, agreguemos una API y operaciones de crear, leer, actualizar, eliminar (CRUD) en una base de datos.

En este módulo, agregará una API a la aplicación mediante la CLI y las bibliotecas de Amplify. La API que creará es una API GraphQL que aprovecha AWS AppSync (un servicio de GraphQL administrado) que está respaldado por Amazon DynamoDB (una base de datos de NoSQL). Para obtener una introducción a GraphQL, visite esta página.

La aplicación que creará es para tomar notas que permite a los usuarios crear, eliminar y enumerar notas. Este ejemplo otorga una buena idea de cómo crear muchos tipos de aplicaciones CRUD+L (crear, leer, actualizar, eliminar y enumerar) populares.

Lo que aprenderá

  • Crear e implementar una API de GraphQL
  • Escribir un código front-end para interactuar con la API

Conceptos clave

API: proporciona una interfaz de programación que permite la comunicación y las interacciones entre diversos intermediarios de software.

GraphQL: una implementación de API del lado del servidor e idioma de consulta basada en una representación tipada de su aplicación. Esta representación de API se declara utilizando un esquema basado en el sistema de tipo GraphQL. (Para obtener más información GraphQL, visite esta página).

 Tiempo de realización

20 minutos

 Servicios utilizados

Implementación

  • Para crear la API GraphQL y su base de datos de respaldo, abra una Terminal y ejecute este comando desde el directorio de su proyecto.

    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

    El editor de texto predeterminado que elige cuando inicializa el proyecto (amplify init) se abre con un esquema de datos creado previamente.

    Elimine el esquema y reemplácelo con nuestro esquema GraphQL de aplicación.

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

    El modelo de datos se compone de una clase NoteData y cuatro propiedades: el ID y el nombre son obligatorios y la descripción y la imagen son cadenas opcionales.

    El transformador @model indica que queremos crear una base de datos para almacenar los datos.

    El transformador @auth agrega reglas de autenticación para permitir el acceso a estos datos. Para este proyecto, queremos que solo el propietario de NoteData tenga acceso a ellos.

    Una vez hecho esto, no olvide guardar, luego regrese a su terminal para decirle a la CLI de Amplify que ha terminado.

    ? Press enter to continue, press enter.

    Luego de unos segundos, debería ver un mensaje de éxito:

    GraphQL schema compiled successfully.
  • En función de la definición del modelo de datos GraphQL que acabamos de crear, Amplify genera código del lado del cliente (es decir, código Swift) para representar los datos en nuestra aplicación.

    Para generar el código, en su terminal, ejecute el siguiente comando:

    amplify codegen models

    Esto crea archivos Java en el directorio java/com/amplifyframework.datastore.generated.model, como puede ver con:

    ➜  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

    Los archivos se importan automáticamente a su proyecto.

  • Para implementar la API de backend y la base de datos que acabamos de crear, diríjase a su terminal y ejecute el 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

    Luego de unos minutos, debería ver un mensaje de éxito:

    ✔ All resources are updated in the cloud
    
    GraphQL endpoint: https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql
  • Antes de ir al código, regrese a Android Studio, agregue la siguiente dependencia a build.gradle de su módulo junto con las otras implementaciones amplifyframework que agregó antes y haga clic en Sync Now (Sincronizar ahora) cuando se le solicite.

    dependencies {
        implementation 'com.amplifyframework:aws-api:1.4.0'
        implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'
    }
  • Abra Backend.kt y agregue una línea en la secuencia de inicialización de Amplify en el método initialize(). El bloque de prueba/captura completo debería verse así:

    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)
    }
  • Nuestro proyecto ya tiene un modelo de datos para representar una nota. En este tutorial continuaremos usando ese modelo y proporcionaremos una manera fácil de convertir NoteData en Note. Abra UserData.kt y agregue dos componentes: una propiedad dinámica que devuelva un objeto NoteData de un UserData.Note y lo contrario: un método estático que acepte una API NoteData y devuelva una Userdata.Note.

    Dentro de la clase de datos Note, agregue lo siguiente:

    // 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
        }
    }     

    Asegúrese de importar la clase NoteData del código generado.

  • Agreguemos tres métodos para llamar a la API: un método para consultar el Note, un método para crear un nuevo Note y un método para eliminar el Note. Tenga en cuenta que estos métodos funcionan en el modelo de datos de la aplicación (Note) a fin de facilitar la interacción desde la interfaz de usuario. Estos métodos convierten de forma transparente a Nota en objetos de NoteData de GraphQL.

    Abra el archivo Backend.kt y agregue el siguiente fragmento al final de la clase 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) }
        )
    }

    Asegúrese de importar las clases ModelQuery, ModelMutation y NoteData del código generado.

    Por último, debemos llamar a la API a fin de consultar la lista de Note para el usuario registrado actualmente cuando se inicie la aplicación.

    En Backend.ktfile, actualice el método updateUserData(withSignInStatus: Boolean) para que se vea así:

    // 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()
        }
    }

    Ahora, lo único que falta es crear una parte de la interfaz de usuario para crear un nuevo Note y eliminar un Note de la lista.

  • Ahora que las partes del backend y del modelo de datos están en su lugar, el último paso de esta sección es permitir a los usuarios crear nuevos Note y eliminarlos.

    a. En Android Studio, en res/layout, cree un diseño nuevo: haga clic con el botón derecho en el diseño y seleccione New (Nuevo), luego Layout Resource File (Archivo de recurso de diseño). Nómbrelo activity_add_note y acepte todos los demás valores predeterminados. Haga clic en OK (Aceptar).

    Abra el archivo activity_add_note que acaba de crear y reemplace el código generado con lo siguiente:

    <?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>

    Este es un diseño muy simple que permite ingresar el título y la descripción de Note.

    b. Agregue una clase AddNoteActivity.

    En java/com.example.androidgettingstarted, cree un nuevo archivo kotlin AddActivityNote.kt, ábralo y agregue este código: 

    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"
        }
    }    

    Por último, en manifiestos, abra AndroidManifest.xml y agregue este elemento de actividad en cualquier lugar dentro del nodo de la aplicación.

    <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. Agregue un FloatingActionButton de “Add Note” (Agregar nota) en la actividad principal. En res/layout, abra activity_main.xml y agregue este sobre el botón de acción flotante existente.

    <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"/>

    Agregue un ícono de “Add Note” (Agregar nota) en res/drawable. Haga clic con el botón derecho en drawable (dibujable), seleccione New (Nuevo), luego Vector Asset (Activo de vector). Escriba ic_baseline_add como nombre y elija el ícono de agregar desde imágenes prediseñadas. Haga clic en Next (Siguiente) y en Finish (Finalizar).

    d. Agregue códigos para manejar el botón “Add Note” (Agregar nota).

    Las últimas dos cosas que faltan para tener un botón de “Add” (Agregar) completamente funcional es hacer que el botón aparezca o desaparezca según el valor isSignedIn y, por supuesto, agregar un código para que maneje los clics en el botón.

    Abra mainActivity.kt y agregue esto al final del método onCreate():

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

    A continuación, todavía en el método onCreate(), reemplace UserData.isSignedIn.observe con lo siguiente:

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

    Para verificar que todo funcione como se espera, cree el proyecto. Haga clic en el menú Build (Crear) y seleccione Make Project (Crear proyecto) o, en Mac, presione ⌘F9. No debería haber ningún error.

    Cuando ejecute la aplicación, verá que el botón “Add Note” (Agregar nota) aparece cuando el usuario inicia sesión y desaparece cuando el usuario cierra la sesión. Ahora puede agregar una nota.

  • Se puede agregar el comportamiento de deslizar para eliminar si se suma un controlador de toques a la lista de Note. El controlador de toques se encarga de dibujar el fondo rojo y el ícono de eliminar y de llamar al método Backend.delete() cuando se lanza el toque.

    a. Cree una nueva clase SimpleTouchCallback. En java/com, haga clic derecho en example.androidgettingstarted, seleccione New (Nuevo) y luego Kotlin File (Archivo Kotlin), escriba SwipeCallback como nombre.

    Pegue el siguiente código en ese archivo nuevo:

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

    Las líneas de código importantes están en el método onSwiped(). Se lo llama a este método cuando finaliza el gesto de deslizar. Recopilamos la posición en la lista del elemento deslizado y eliminamos la nota correspondiente de la estructura de UserData (esto actualiza la UI) y del backend en la nube.

    b. Ahora que tenemos una clase, vamos a agregar un ícono de “Delete” (Eliminar) en res/drawable. Haga clic con el botón derecho en drawable (dibujable), seleccione New (Nuevo), luego Vector Asset (Activo de vector). Escriba ic_baseline_delete_sweep como nombre y elija el ícono “delete sweep” (eliminar deslizamiento) en imágenes prediseñadas. Haga clic en Next (Siguiente) y en Finish (Finalizar).

    c. Pegue el siguiente código en ese nuevo archivo: Agregue el controlador de movimientos deslizar para eliminar en RecyclerView.

    En java/com/example.androidgettingstarted, abra MainActivity.kt y agregue estas dos líneas de código en setupRecyclerView:

    // add a touch gesture handler to manager the swipe to delete gesture
    val itemTouchHelper = ItemTouchHelper(SwipeCallback(this))
    itemTouchHelper.attachToRecyclerView(recyclerView)
  • Para verificar que todo funcione como se espera, cree y ejecute el proyecto. Haga clic en el icono Run (Ejecutar) ▶️️ en la barra de herramientas o escriba ^ R. No debería haber ningún error.

    La aplicación comienza en la lista vacía, si asume que su sesión aún sigue abierta. Ahora tiene un botón de “Add Note” (Agregar nota) para agregar una nota. Seleccione el cartel de Add Note (Agregar nota), escriba un título y una descripción, presione el botón Add Note(Agregar nota) y la nota debería aparecer en la lista.

    Puede eliminar una nota si desliza una fila hacia la izquierda.

    Este es el flujo completo.

Conclusión

¡Ha creado una aplicación Android! Mediante AWS Amplify, agregó una API de GraphQL y configuró una funcionalidad para crear, leer y eliminar en su aplicación.

En el siguiente módulo, agregaremos una interfaz de usuario y comportamientos para administrar imágenes.

¿Este módulo le resultó útil?

Agregar almacenamiento