AWS 入門

建置 Android 應用程式

使用 AWS Amplify 建立簡單的 Android 應用程式

第五單元︰新增儲存影像的功能

在本單元中,您將新增儲存體至應用程式,並讓影像能夠與應用程式中的筆記關聯。

簡介

現在,記事應用程式開始工作了,讓我們新增將影像與每個筆記關聯的功能。在本單元中,您將使用 Amplify CLI 和程式庫來建立利用 Amazon S3 的儲存服務。最後,您將更新 Android 應用程式以實現影像上傳擷取和呈現。

您將學到的內容

  • 建立儲存服務
  • 更新您的 Android 應用程式 - 上傳和下載影像的邏輯
  • 更新您的 Android 應用程式 - 使用者界面

主要概念

儲存服務 - 儲存和查詢影像和影片之類的檔案是大多數應用程式的常見要求。一種選擇是對檔案進行 Base64 編碼,並作為字串傳送以儲存在資料庫中。這樣做的缺點是,編碼檔案比原始二進位檔案大,操作在運算上成本高昂,並且由於需要正確編碼和解碼,所以複雜性有所增加。另一種選擇是專門為檔案儲存建立和優化儲存服務。諸如 Amazon S3 之類的儲存服務能夠讓這個過程盡可能簡單、高效能且廉價。

 完成時間

10 分鐘

 使用的服務

實作

  • 要新增影像儲存功能,我們將使用 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

    一段時間後,您應看到:

    Successfully added resource image locally
  • 若要部署我們剛建立的儲存服務,請前往您的終端機並執行命令:

    amplify push

    Y 進行確認,一段時間後,您應看到:

    ✔ Successfully pulled backend environment amplify from the cloud.
  • 在前往程式碼之前,請返回 Android Studio,然後將以下相依項以及之前新增的其他 `amplifyframework` 實作,新增至單元的 `build.gradle` 中,並在提示時按一下 Sync Now (立即同步):

    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'
    }
  • 返回 Android Studio,在 java/example.androidgettingstarted 下,開啟 Backend.kit,並在 initialize() 方法的 Amplify 初始化序列中新增一行。完整的程式碼區塊應如下所示:

    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)
    }
  • 仍在 Backend.kt 中。在後端類別的任何位置,新增以下三種方法可從儲存體上傳、下載和刪除影像:

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

    這三種方法只需叫用其 Amplify 對應物即可。Amplify 儲存體具有三個檔案保護級別:

    • 公有所有使用者均可存取
    • 受保護所有使用者可讀取,但僅建立使用者可寫入
    • 私有僅建立使用者可讀取和寫入

    對於此應用程式,我們想讓影像適用於附註擁有者,我們將使用 StorageAccessLevel.PRIVATE 屬性。

  • 下一步是修改 UI,以允許使用者在 AddnoteACtivity 上的 "Add image" (新增影像) 按鈕時,從電話庫中選取影像。

    需要進行兩項變更:變更 "Add Note" (新增附註) 活動版面配置,以新增 "Add Image" (新增影像) 按鈕和影像檢視,並在活動類別中新增處理程式的程式碼。

    在 Android Studio 的 res/layout 下,開啟 activity_add_note.xml 檔案,並在 Add Note (新增附註) 按鈕上方新增此按鈕元素:

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

    在 Android Studio 的 java/example.androidgettingstarted 下,開啟 AddnoteACtivity.kt 檔案,然後在 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()

    在 Intent、MediaStore 和 CornerFamily 上新增所需的匯入。

    此外還會將此常量值新增至伴隨物件中:

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

    最後,新增用於接收所選影像並將其存放至臨時檔案的程式碼。

    將以下程式碼新增至 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()
            }
        }
    }

    上面的程式碼兩次將所選影像用作 InputStream。第一個 InputStream 建立可在 UI 中顯示的點陣圖影像,第二個 InputStream 儲存臨時檔案以傳送至後端。

    此單元將處理臨時檔案,因為 Amplify API 使用了 Fileobjects。雖然不是最有效的設計,但程式碼很簡單。

    若要驗證是否一切如預期,請建置專案。按一下 Build (建置) 功能表,然後選取 Make Project (製作專案),或者在 Mac 上,鍵入 ⌘F9。應不會發生錯誤。

  • 建立附註時,我們從後端叫用儲存方法。開啟 AddnoteActivity.kt 並修改 addnote.setOnClickListener() 方法,以在建立附註物件之後新增以下程式碼。

    // 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!!)
    }
  • 若要載入影像,我們將修改附註資料類別上的 static from 方法。這樣一來,每次將 API 返回的 noteData 物件轉換為附註物件時,都會平行載入影像。載入影像後,我們會通知 LiveData 的 UserData 來讓觀察者了解變更。這會觸發 UI 重新整理。

    開啟 UserData.kt 並修改附註資料類別的伴隨物件,如下所示:

    // 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
        }
    }
  • 最後一步是自行清除,即當使用者刪除附註時從雲端儲存體中刪除影像。如果您不需要這樣做來節省儲存空間,請考慮您的 AWS 帳單而進行此操作,因為 Amazon S3 根據每月每 GB 收取資料存放費用 (前 5 GB 免費,執行本教學無需付費)。

    開啟 SwipeCallback.kt,並在 onSwipe() 方法末尾新增以下程式碼:

    if (note?.imageName != null) {
        //asynchronously delete the image (and assume it will work)
        Backend.deleteImage(note.imageName!!)
    }
  • 若要驗證是否一切如預期,請建置並執行專案。按一下工具列中的 Run (執行) 圖示 ▶️,或鍵入 ^ R。應不會發生錯誤。

    假設您仍處於登入狀態,該應用程式會從上一部分未刪除的註冊清單中開始。再次使用 Add Note (新增附註) 按鈕來建立附註。這次,新增從本機影像存放區中選取的圖片。

    退出該應用程式,然後重新啟動以驗證影像是否正確載入。

    這就是完整的流程。

結論

您已使用 AWS Amplify 建置 Android 應用程式! 您已將身份驗證新增到應用程式中,從而允許使用者註冊、登入和管理其帳戶。該應用程式還具有可擴展的 GraphQL API 以及 Amazon DynamoDB 資料庫,允許使用者建立和刪除筆記。您還使用 Amazon S3 新增了檔案儲存體,允許使用者上傳影像並在應用程式中進行檢視。

在最後一部分,您會找到有關重新使用或刪除剛建立後端的指導。

  • Amplify 可讓您在多個前端應用程式間輕鬆共用單一後端。

    在終端機中,導覽至您的其他專案目錄並執行以下命令:

    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.

    幾秒鐘後,您會看到以下訊息:

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

    您可以看到已提取的兩個組態檔案。若您對「您打算修改此後端嗎?」問題的回答為「是」,則還會看到一個 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
  • 針對測試或原型,或者僅出於學習目的建立後端時,就像按照本教學進行操作一樣,您需要刪除已建立的雲端資源。

    雖然免費方案涵蓋在本教學內容中使用此資源,但最好是清除雲端中未使用的資源。

    若要清除您的 Amplify 專案,請在終端機中執行以下命令:

    amplify delete

    稍後,您會看到以下訊息,確認已刪除所有雲端資源。

    ✔ Project deleted in the cloud
    Project deleted locally.

感謝您完成本教學全部課程。請使用以下工具或在我們的 Github 儲存儲提取請求,告知我們您的意見回饋。

這個單元對您是否有幫助?

恭喜您!

您已成功在 AWS 上建置 Android 應用程式! 下一步,您將深入了解特定的 AWS 技術,並將您的應用程式提升到一個新的水平。