Introducción a AWS

Crear una aplicación Android

Crear una aplicación Android sencilla con AWS Amplify

Módulo 5: agregar la capacidad de almacenar imágenes

En este módulo, agregará almacenamiento y la capacidad de asociar una imagen con las notas en su aplicación.

Introducción

Ahora que tenemos una aplicación de notas en funcionamiento, agreguemos la capacidad de asociar una imagen a cada nota. En este módulo, usará la CLI y las bibliotecas de Amplify para crear un servicio de almacenamiento con Amazon S3. Por último, actualizaremos la aplicación Android para permitir la carga, la búsqueda y el procesamiento de imágenes.

Lo que aprenderá

  • Crear un servicio de almacenamiento
  • Actualice su aplicación Android: la lógica para cargar y descargar imágenes
  • Actualice su aplicación Android: la interfaz de usuario

Conceptos clave

Servicio de almacenamiento: el almacenamiento y la consulta de archivos como imágenes y videos es un requisito común para la mayoría de las aplicaciones. Una opción para hacer esto es codificar el archivo con Base64 y enviarlo como cadena para guardarle en la base de datos. Esto viene con desventajas como que el archivo codificado es más grande que el archivo binario original, la operación es costosa en términos informáticos y la codificación y decodificación adecuadas agregan complejidad. Otra opción es crear y optimizar un servicio de almacenamiento específicamente para el almacenamiento de archivos. Los servicios de almacenamiento como Amazon S3 existen para aportar la mayor facilidad, eficiencia y rentabilidad posible.

 Tiempo de realización

10 minutos

 Servicios utilizados

Implementación

  • Crear el servicio de almacenamiento

    Para agregar la funcionalidad de almacenamiento de imágenes, usaremos la categoría de almacenamiento Amplify:

    amplify add storage
    
    ? Please select from one of the below mentioned services: accept the default Content (Images, audio, video, etc.) and press enter
    ? Please provide a friendly name for your resource that will be used to label this category in the project: type image and press enter
    ? Please provide bucket name: accept the default and press enter
    ? Who should have access: accept the default Auth users only and press enter
    ? What kind of access do you want for Authenticated users? select all three options create/update, read, delete using the space and arrows keys, then press enter
    ? Do you want to add a Lambda Trigger for your S3 Bucket? accept the default No and press enter

    Luego de un momento, debería ver lo siguiente:

    Successfully added resource image locally
  • Implementar el servicio de almacenamiento

    Para implementar el servicio de almacenamiento que acabamos de crear, diríjase a su terminal y ejecute el comando:

    amplify push

    Presione Y para confirmar y, luego de un momento, debería ver lo siguiente:

    ✔ Successfully pulled backend environment amplify from the cloud.
  • Agregar bibliotecas de almacenamiento de Amplify al proyecto de Android Studio

    Antes de ir al código, regrese a Android Studio y 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 {
     ...
        // Amplify core dependency
        implementation 'com.amplifyframework:core:1.4.0'
        implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'
        implementation 'com.amplifyframework:aws-api:1.4.0'
        implementation 'com.amplifyframework:aws-storage-s3:1.4.0'
    }
  • Inicializar el complemento de almacenamiento de Amplify en tiempo de ejecución

    De vuelta a Android Studio, en java/example.androidgettingstarted, abra Backend.kit y agregue una línea en la secuencia de inicialización de Amplify en el método initialize(). El bloque de código completo debería verse así:

    try {
        Amplify.addPlugin(AWSCognitoAuthPlugin())
        Amplify.addPlugin(AWSApiPlugin())
        Amplify.addPlugin(AWSS3StoragePlugin())
    
        Amplify.configure(applicationContext)
    
        Log.i(TAG, "Initialized Amplify")
    } catch (e: AmplifyException) {
        Log.e(TAG, "Could not initialize Amplify", e)
    }
  • Agregar métodos de Image CRUD a la clase Backend

    Todavía en Backend.kt. En cualquier lugar de la clase Backend, agregue los siguientes tres métodos para cargar, descargar y eliminar imágenes del almacenamiento:

    fun storeImage(filePath: String, key: String) {
        val file = File(filePath)
        val options = StorageUploadFileOptions.builder()
            .accessLevel(StorageAccessLevel.PRIVATE)
            .build()
    
        Amplify.Storage.uploadFile(
            key,
            file,
            options,
            { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") },
            { result -> Log.i(TAG, "Successfully uploaded: " + result.key) },
            { error -> Log.e(TAG, "Upload failed", error) }
        )
    }
    
    fun deleteImage(key : String) {
    
        val options = StorageRemoveOptions.builder()
            .accessLevel(StorageAccessLevel.PRIVATE)
            .build()
    
        Amplify.Storage.remove(
            key,
            options,
            { result -> Log.i(TAG, "Successfully removed: " + result.key) },
            { error -> Log.e(TAG, "Remove failure", error) }
        )
    }
    
    fun retrieveImage(key: String, completed : (image: Bitmap) -> Unit) {
        val options = StorageDownloadFileOptions.builder()
            .accessLevel(StorageAccessLevel.PRIVATE)
            .build()
    
        val file = File.createTempFile("image", ".image")
    
        Amplify.Storage.downloadFile(
            key,
            file,
            options,
            { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") },
            { result ->
                Log.i(TAG, "Successfully downloaded: ${result.file.name}")
                val imageStream = FileInputStream(file)
                val image = BitmapFactory.decodeStream(imageStream)
                completed(image)
            },
            { error -> Log.e(TAG, "Download Failure", error) }
        )
    }

    Estos tres métodos simplemente realizan llamadas a su contraparte de Amplify. El almacenamiento de Amplify tiene tres niveles de protección de archivos:

    • Público Accesible para todos los usuarios
    • Protegido Legible para todos los usuarios, pero solo el usuario que lo crea puede escribirlo
    • Privado Legible y escribible solo para el usuario que lo crea

    En el caso de esta aplicación, queremos que las imágenes solo estén disponibles para el propietario de la nota, por lo que usamos la propiedad StorageAccessLevel.PRIVATE.

  • Agregar código de UI para capturar una imagen

    El siguiente paso es modificar la interfaz de usuario (UI) para permitir que el usuario seleccione una imagen de la biblioteca del teléfono cuando haga clic en el botón “Add image” (Agregar imagen) en AddNoteACtivity.

    Se necesitan dos cambios: cambiar el diseño de la actividad “Add Note” (Agregar nota) para agregar un botón de “Add image” (Agregar imagen) y una vista de imagen y agregar el código del controlador en la clase de actividad.

    En Android Studio, en res/layout, abra el archivo activity_add_note.xml y agregue este elemento de botón justo encima del botón addNote.

    <!-- after the description EditText -->
    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="280dp"
        android:layout_margin="16dp"
        android:scaleType="centerCrop" />
    
    <!-- after the Space -->
    <Button
        android:id="@+id/captureImage"
        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 image" />

    En Android Studio, en java/example.androidgettingstarted, abra el archivo AddNoteACtivity.kt y agregue este código en el método onCreate().

    // inside onCreate() 
    // Set up the listener for add Image button
    captureImage.setOnClickListener {
        val i = Intent(
            Intent.ACTION_GET_CONTENT,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI
        )
        startActivityForResult(i, SELECT_PHOTO)
    }
    
    // create rounded corners for the image
    image.shapeAppearanceModel = image.shapeAppearanceModel
        .toBuilder()
        .setAllCorners(CornerFamily.ROUNDED, 150.0f)
        .build()

    Agregue la importación correspondiente en Intent, MediaStore y CornerFamily.

    También agregue este valor constante en el objeto complementario:

    // add this to the companion object 
    private const val SELECT_PHOTO = 100

    Por último, agregue el código que recibe y almacena la imagen seleccionada en un archivo temporal.

    Agregue el siguiente código en cualquier lugar de AddNoteACtivityclass:

    //anywhere in the AddNoteActivity class
    
    private var noteImagePath : String? = null
    private var noteImage : Bitmap? = null
    
    override fun onActivityResult(requestCode: Int, resultCode: Int, imageReturnedIntent: Intent?) {
        super.onActivityResult(requestCode, resultCode, imageReturnedIntent)
        Log.d(TAG, "Select photo activity result : $imageReturnedIntent")
        when (requestCode) {
            SELECT_PHOTO -> if (resultCode == RESULT_OK) {
                val selectedImageUri : Uri? = imageReturnedIntent!!.data
    
                // read the stream to fill in the preview
                var imageStream: InputStream? = contentResolver.openInputStream(selectedImageUri!!)
                val selectedImage = BitmapFactory.decodeStream(imageStream)
                val ivPreview: ImageView = findViewById<View>(R.id.image) as ImageView
                ivPreview.setImageBitmap(selectedImage)
    
                // store the image to not recreate the Bitmap every time
                this.noteImage = selectedImage
    
                // read the stream to store to a file
                imageStream = contentResolver.openInputStream(selectedImageUri)
                val tempFile = File.createTempFile("image", ".image")
                copyStreamToFile(imageStream!!, tempFile)
    
                // store the path to create a note
                this.noteImagePath = tempFile.absolutePath
    
                Log.d(TAG, "Selected image : ${tempFile.absolutePath}")
            }
        }
    }
    
    private fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
        inputStream.use { input ->
            val outputStream = FileOutputStream(outputFile)
            outputStream.use { output ->
                val buffer = ByteArray(4 * 1024) // buffer size
                while (true) {
                    val byteCount = input.read(buffer)
                    if (byteCount < 0) break
                    output.write(buffer, 0, byteCount)
                }
                output.flush()
                output.close()
            }
        }
    }

    El código anterior consume la imagen seleccionada como InputStream, dos veces. El primer InputStream crea una imagen de mapa de bits para mostrar en la UI, el segundo InputStream guarda un archivo temporal para enviarlo al backend.

    Este módulo pasa por un archivo temporal porque la API de Amplify consume Fileobjects. Si bien no es el diseño más eficiente, el código es simple.

    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.

  • Almacenar imágenes cuando se crean notas

    Invoquemos los métodos de almacenamiento de Backend cuando se crea una nota. Abra AddNoteActivity.kt y modifique el método addNote.setOnClickListener() para agregar el siguiente código después de crear el objeto de Note.

    // add this in AddNoteACtivity.kt, inside the addNote.setOnClickListener() method and after the Note() object is created.
    if (this.noteImagePath != null) {
        note.imageName = UUID.randomUUID().toString()
        //note.setImage(this.noteImage)
        note.image = this.noteImage
    
        // asynchronously store the image (and assume it will work)
        Backend.storeImage(this.noteImagePath!!, note.imageName!!)
    }
  • Cargar imágenes cuando se cargan notas

    Para cargar imágenes, modificamos el método estático en la clase de datos Note. De esa manera, cada vez que un objeto de NoteData devuelto por la API se convierte en un objeto de Note, la imagen se carga en paralelo. Cuando se carga la imagen, notificamos a UserData de LiveData para informar a los observadores sobre el cambio. Esto activa una actualización de la UI.

    Abra UserData.kt y modifique el objeto complementario de la clase de datos Note de la siguiente manera:

    // 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)
            
            if (noteData.image != null) {
                Backend.retrieveImage(noteData.image!!) {
                    result.image = it
    
                    // force a UI update
                    with(UserData) { notifyObserver() }
                }
            }
            return result
        }
    }
  • Eliminar imágenes cuando se eliminan notas

    El último paso es limpiar después de nosotros mismos, es decir, eliminar imágenes del almacenamiento en la nube cuando un usuario elimina una nota. Si no lo hace para ahorrar espacio de almacenamiento, hágalo por sus facturas de AWS, ya que Amazon S3 cobra por GB de datos almacenados al mes (los primeros 5 GB son gratuitos, no se le cobrará por ejecutar este tutorial).

    Abra SwipeCallback.kt y agregue el siguiente código al final del método onSwipe():

    if (note?.imageName != null) {
        //asynchronously delete the image (and assume it will work)
        Backend.deleteImage(note.imageName!!)
    }
  • Crear y probar

    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 de Note que no eliminó de la sección anterior, si asume que su sesión aún sigue abierta. Use el botón Add Note (Agregar nota) nuevamente para crear una nota. Esta vez, agregue una imagen seleccionada de la tienda de imágenes local.

    Cierre la aplicación y reiníciela para verificar que la imagen se haya cargado correctamente.

    Este es el flujo completo.

    AndroidAppTutorial_Modiule5_Image1
    AndroidAppTutorial_Modiule5_Image2
    AndroidAppTutorial_Modiule5_Image3
    AndroidAppTutorial_Modiule5_Image4
    AndroidAppTutorial_Modiule5_Image5

Conclusión

¡Ha creado una aplicación Android con AWS Amplify! Ha agregado autenticación a su aplicación, lo que permite que los usuarios se registren, inicien sesión y administren su cuenta. La aplicación también cuenta con una API GraphQL escalable configurada con una base de datos Amazon DynamoDB que permite que los usuarios creen y eliminen notas. También ha agregado almacenamiento de archivos mediante Amazon S3, lo que permite que los usuarios carguen imágenes y las vean en su aplicación.

En la última sección, encontrará instrucciones para reutilizar o eliminar el backend que acabamos de crear.

  • Comparta su backend entre diferentes proyectos

    Amplify facilita el intercambio de un solo backend entre diferentes aplicaciones de frontend.

    En una terminal, diríjase hasta el otro directorio de su proyecto y ejecute el siguiente comando:

    mkdir other-project
    cd other-project
    
    amplify pull
    
    ? Do you want to use an AWS profile? accept the default Yes and press enter
    ? Please choose the profile you want to use select the profile you want to use and press enter
    ? Which app are you working on? select the backend you want to share and press enter
    ? Choose your default editor: select you prefered text editor and press enter
    ? Choose the type of app that you're building select the operating system for your new project and press enter
    ? Do you plan on modifying this backend? most of the time, select No and press enter. All backend modifications can be done from the original iOS project.

    Luego de unos segundos, verá el siguiente mensaje:

    Added backend environment config object to your project.
    Run 'amplify pull' to sync upstream changes.

    Puede ver los dos archivos de configuración que se han extraído. Cuando responda “Sí” a la pregunta “¿Planea modificar este backend?”, también verá un directorio de Amplify.

    ➜  other-project git:(master) ✗ ls -al
    total 24
    drwxr-xr-x   5 stormacq  admin   160 Jul 10 10:28 .
    drwxr-xr-x  19 stormacq  admin   608 Jul 10 10:27 ..
    -rw-r--r--   1 stormacq  admin   315 Jul 10 10:28 .gitignore
    -rw-r--r--   1 stormacq  admin  3421 Jul 10 10:28 amplifyconfiguration.json
    -rw-r--r--   1 stormacq  admin  1897 Jul 10 10:28 awsconfiguration.json
  • Eliminar el backend

    Cuando crea un backend para una prueba o un prototipo, o simplemente con fines de aprendizaje, al igual que cuando sigue este tutorial, luego desea eliminar los recursos en la nube que se han creado.

    Aunque el uso de estos recursos en el contexto de este tutorial se incluye en la capa gratuita, es una buena práctica limpiar los recursos que no se utilizan en la nube.

    Para limpiar su proyecto de Amplify, en una terminal, ejecute el siguiente comando:

    amplify delete

    Luego de un momento, verá el siguiente mensaje que confirma que se han eliminado todos los recursos de la nube.

    ✔ Project deleted in the cloud
    Project deleted locally.

Gracias por haber seguido este tutorial hasta el final. Háganos saber sus comentarios con la siguiente herramienta o una solicitud de extracción en nuestro repositorio de Github.

¿Este módulo le resultó útil?

Gracias
Indíquenos lo que le gustó.
Cerrar
Lamentamos haberlo decepcionado
¿Hay información desactualizada, confusa o inexacta? Ayúdenos a mejorar este tutorial con sus comentarios.
Cerrar

¡Felicitaciones!

¡Creó correctamente una aplicación Android en AWS! Como paso siguiente, profundice aún más en las tecnologías específicas de AWS y lleve su aplicación al siguiente nivel.