Erste Schritte mit AWS

Erstellen einer Android-Anwendung

Erstellen einer einfachen Android-Anwendung mit AWS Amplify

Modul 5: Hinzufügen der Funktion, Bilder zu speichern

In diesem Modul fügen Sie Speicherplatz hinzu und haben die Möglichkeit, ein Bild mit den Notizen in Ihrer App zu verknüpfen.

Einführung

Nun, da die Notizen-App funktioniert, wollen wir die Möglichkeit hinzufügen, jeder Notiz ein Bild zuzuordnen. In diesem Modul werden Sie die Amplify CLI und Bibliotheken verwenden, um einen Speicher-Service zu erstellen, der Amazon S3 nutzt. Schließlich werden Sie die Android-Anwendung aktualisieren, um das Hochladen, Abrufen und Rendern von Bildern zu ermöglichen.

Lerninhalte

  • Erstellen eines Speicher-Services
  • Aktualisieren Ihrer Android-App: die Logik zum Hoch- und Herunterladen von Bildern
  • Aktualisieren Ihrer Android-App: die Benutzeroberfläche

Wichtige Konzepte

Speicher-Service – Das Speichern und Abfragen von Dateien wie Bildern und Videos ist eine allgemeine Anforderung für die meisten Anwendungen. Eine Möglichkeit, dies zu tun, besteht darin, die Datei Base64 zu kodieren und als String zu senden, um sie in der Datenbank zu speichern. Dies bringt Nachteile mit sich, wie z. B. dass die kodierte Datei größer ist als die ursprüngliche Binärdatei, dass die Operation rechenintensiv ist und dass die Kodierung und Dekodierung komplizierter wird. Eine weitere Option ist ein speziell für die Dateiablage entwickelter und optimierter Service. Speicherdienste wie Amazon S3 existieren, um dies so einfach, performant und kostengünstig wie möglich zu machen.

 Veranschlagte Zeit

10 Minuten

 Verwendete Services

Implementierung

  • Um Bildspeicherfunktionen hinzuzufügen, verwenden wir die Amplify-Speicherkategorie:

    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

    Nach einer Weile sollten Sie Folgendes sehen:

    Successfully added resource image locally
  • Um den soeben von uns bereitgestellten Speicher-Service bereitzustellen, gehen Sie zu Ihrem Terminal und führen Sie den Befehl aus:

    amplify push

    Drücken Sie Y zum Bestätigen und nach einer Weile sollten Sie Folgendes sehen:

    ✔ Successfully pulled backend environment amplify from the cloud.
  • Bevor Sie zum Code gehen, gehen Sie zurück zu Android Studio, fügen Sie die folgende Abhängigkeit zur `build.gradle` Ihres Moduls zusammen mit anderen `Amplifyframework`-Implementierungen hinzu, die Sie zuvor hinzugefügt haben, und klicken Sie auf Jetzt synchronisieren, wenn Sie dazu aufgefordert werden:

    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'
    }
  • Zurück zu Android Studio, unter java/example.androidgettingstarted, öffnen Sie Backend.kit und fügen Sie eine Zeile in der Initialisierungssequenz von Amplify Backend in der initialize()-Methode hinzu. Der vollständige Codeblock sollte wie folgt aussehen:

    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)
    }
  • Noch in Backend.kt. Fügen Sie irgendwo in der Backend-Klasse die folgenden drei Methoden zum Hochladen, Herunterladen und Löschen von Bildern aus dem Speicher hinzu:

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

    Diese drei Methoden nennen einfach ihr Amplify-Gegenstück. Amplify Storage verfügt über drei Dateischutzstufen:

    • Öffentlich Zugänglich für alle Benutzer
    • Geschützt Lesbar für alle Benutzer, aber nur schreibbar für den erstellenden Benutzer
    • Privat Nur für den Ersteller lesbar und beschreibbar

    Für diese Anwendung möchten wir, dass die Bilder nur dem Besitzer der Notiz zur Verfügung stehen, wir verwenden die Eigenschaft StorageAccessLevel.PRIVATE.

  • Der nächste Schritt besteht darin, die Benutzeroberfläche so zu ändern, dass der Benutzer ein Bild aus der Telefonbibliothek auswählen kann, wenn er auf die Schaltfläche "Bild hinzufügen" in der AddNoteACtivity klickt.

    Zwei Änderungen sind erforderlich: Ändern Sie das Layout der Aktivität "Notiz hinzufügen", um eine Schaltfläche "Bild hinzufügen" und eine Bildansicht hinzuzufügen, und fügen Sie Handler-Code in der Aktivitätsklasse hinzu.

    Öffnen Sie in Android Studio unter res/layout die Datei activity_add_note.xml und fügen Sie dieses Schaltflächenelement direkt über der Schaltfläche addNote hinzu:

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

    Öffnen Sie in Android Studio unter java/example.androidgettingstarted die Datei AddNoteACtivity.kt und fügen Sie diesen Code in der Methode onCreate() hinzu:

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

    Fügen Sie den erforderlichen Import auf Intent, MediaStore und CornerFamily hinzu.

    Fügen Sie diesen konstanten Wert auch im Begleitobjekt hinzu:

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

    Schließlich fügen Sie den Code, der das ausgewählte Bild empfängt und speichert, in eine temporäre Datei ein.

    Fügen Sie den untenstehenden Code an beliebiger Stelle in der AddNoteACtivity-Klasse hinzu:

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

    Der obige Code konsumiert das ausgewählte Bild zweimal als InputStream. Der erste InputStream erstellt ein Bitmap-Bild zur Anzeige in der Benutzeroberfläche, der zweite InputStream speichert eine temporäre Datei zum Senden an das Backend.

    In diesem Modul wird eine temporäre Datei durchlaufen, da die Amplify-API Fileobjects konsumiert. Obwohl es nicht das effizienteste Design ist, ist der Code einfach.

    Um zu überprüfen, ob alles wie erwartet funktioniert, erstellen Sie das Projekt. Klicken Sie auf das Menü Build und wählen Sie Projekt machen oder, auf Macs, geben Sie ⌘F9 ein. Es sollte kein Fehler vorliegen.

  • Rufen wir die Speichermethoden aus dem Backend auf, wenn eine Notiz erstellt wird. Öffnen Sie AddNoteActivity.kt und ändern Sie die Methode addNote.setOnClickListener(), um den unten stehenden Code nach der Erstellung des Notizobjekts hinzuzufügen.

    // 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!!)
    }
  • Um Bilder zu laden, modifizieren wir die Static from-Methode in der Datenklasse Notiz. Auf diese Weise wird jedes Mal, wenn ein von der API zurückgegebenes NoteData-Objekt in ein Note-Objekt konvertiert wird, das Bild parallel geladen. Wenn das Bild geladen wird, benachrichtigen wir die UserData der LiveData, um Beobachter über die Änderung zu informieren. Dies löst eine Aktualisierung der Benutzeroberfläche aus.

    Öffnen Sie UserData.kt und modifizieren Sie das Begleitobjekt der Notizdatenklasse wie folgt:

    // 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
        }
    }
  • Der letzte Schritt besteht darin, hinter uns aufzuräumen, d. h. Bilder aus dem Cloud-Speicher zu löschen, wenn ein Benutzer eine Notiz löscht. Wenn Sie es nicht tun, um Speicherplatz zu sparen, tun Sie es für Ihre AWS-Rechnungen, da Amazon S3 pro GB/Monat gespeicherter Daten berechnet (die ersten 5 GB sind kostenlos, für die Durchführung dieses Tutorials werden Ihnen keine Gebühren berechnet).

    Öffnen Sie SwipeCallback.kt und fügen Sie den unten stehenden Code am Ende der Methode onSwipe() hinzu:

    if (note?.imageName != null) {
        //asynchronously delete the image (and assume it will work)
        Backend.deleteImage(note.imageName!!)
    }
  • Um zu überprüfen, ob alles wie erwartet funktioniert, bauen und betreiben Sie das Projekt. Klicken Sie auf das Symbol Ausführen ▶️ in der Symbolleiste oder geben Sie ^ R ein. Es sollte kein Fehler vorliegen.

    Angenommen, Sie sind noch angemeldet, dann startet die App auf der Liste der Notizen, die Sie nicht aus dem vorherigen Abschnitt gelöscht haben. Verwenden Sie erneut die Schaltfläche Notiz hinzufügen, um eine Notiz zu erstellen. Fügen Sie diesmal ein aus dem lokalen Bildspeicher ausgewähltes Bild hinzu.

    Beenden Sie die Anwendung und starten Sie sie neu, um zu überprüfen, ob das Bild korrekt geladen ist.

    Hier ist der vollständige Ablauf.

Fazit

Sie haben mit AWS Amplify eine Android-Anwendung erstellt! Sie haben Ihrer Anwendung eine Authentifizierung hinzugefügt, mit der Benutzer sich anmelden, einloggen und ihr Konto verwalten können. Die Anwendung verfügt außerdem über eine skalierbare GraphQL-API, die mit einer Amazon DynamoDB-Datenbank konfiguriert ist und es Benutzern ermöglicht, Notizen zu erstellen und zu löschen. Sie haben auch einen Dateispeicher mit Amazon S3 hinzugefügt, der es Benutzern ermöglicht, Bilder hochzuladen und in ihrer Anwendung anzusehen.

Im letzten Abschnitt finden Sie Anweisungen zur Wiederverwendung oder zum Löschen des soeben erstellten Backends.

  • Amplify macht es einfach, ein einziges Backend für mehrere Frontend-Anwendungen gemeinsam zu nutzen.

    Navigieren Sie in einem Terminal zu Ihrem anderen Projektverzeichnis und führen Sie den folgenden Befehl aus:

    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.

    Nach einigen Sekunden sehen Sie die folgende Meldung:

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

    Sie können die beiden Konfigurationsdateien sehen, die herausgezogen wurden. Wenn Sie auf die Frage "Planen Sie eine Änderung dieses Backends?" mit "Ja" antworten, sehen Sie auch ein Amplify Backend.

    ➜  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
  • Wenn Sie ein Backend für einen Test oder einen Prototyp oder einfach nur zu Lernzwecken erstellen, möchten Sie, genau wie beim Befolgen dieses Tutorials, die erstellten Cloud-Ressourcen löschen.

    Obwohl die Nutzung dieser Ressourcen im Rahmen dieses Tutorials in das kostenlose Kontingent fällt, ist es eine bewährte Methode, ungenutzte Ressourcen in der Cloud zu bereinigen.

    Um Ihr Amplify-Projekt zu bereinigen, führen Sie in einem Terminal den folgenden Befehl aus:

    amplify delete

    Nach einer Weile sehen Sie die untenstehende Meldung, die bestätigt, dass alle Cloud-Ressourcen gelöscht wurden.

    ✔ Project deleted in the cloud
    Project deleted locally.

Vielen Dank, dass Sie dieses Tutorial bis zum Ende verfolgt haben. Bitte teilen Sie uns Ihr Feedback mit, indem Sie das untenstehende Tool oder eine Pull-Anfrage zu unserem Github-Repository verwenden.

War das Modul hilfreich?

Herzlichen Glückwunsch!

Sie haben eine Android-Anwendung auf AWS erstellt! Als großen nächsten Schritt sollten Sie tiefer in spezifische AWS-Technologien eintauchen und Ihre Anwendung auf die nächste Stufe bringen.