模块 5:添加图像存储功能
在本模块中,您将添加存储以及将图像与您的应用程序中的备注关联的功能。
简介
现在,我们的备注应用程序已在运行,接下来添加将图像与每个备注关联的功能。在本模块中,您将使用 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 项目添加 Amplify 存储库
在转到此代码之前,请先返回 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' }
-
在运行时初始化 Amplify 存储插件
返回 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) }
-
将 Image CRUD 方法添加到后端类
依然在 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 代码以捕获图像
下一步是修改 UI,以允许用户在单击 AddNoteACtivity 上的“Add image”按钮时从手机库中选择图像。
必须执行两个更改:更改“Add Note”活动布局以添加“Add image”按钮和图像视图,以及在活动类中添加处理程序代码。
在 Android Studio 中的“res/layout”下,打开 activity_add_note.xml 文件,然后将以下 Button 元素添加到 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" />
在 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
最后,添加收到的代码并将所选图像存储到临时文件。
将以下代码添加到 AddNoteACtivity 类的任何位置:
//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 会创建一个位图图像以显示在用户界面中,第二次,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!!) }
-
加载备注时加载图像
要加载图像,我们需要修改备注数据类上的静态方法。这样,每当 API 返回的 NoteData 对象转换为 Note 对象时,将同时加载图像。加载图像时,我们会通知 LiveData 的 UserData,以便观察者知晓进行的更改。这将触发用户界面刷新。
打开 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/月收费(前 5Gb 免费,运行本教程不收费)。
打开 SwipeCallback.kt,然后在 onSwipe() 方法末尾添加以下代码:
if (note?.imageName != null) { //asynchronously delete the image (and assume it will work) Backend.deleteImage(note.imageName!!) }
-
构建和测试
要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。
假设您仍在登录中,应用程序将从未从上一部分删除的备注列表开始。再次使用“Add Note”按钮创建备注。这一次,添加从本地图像存储中选择的图片。
退出应用程序并重新启动,以验证图像是否正确加载。
以下是完整流程。
结论
您已经使用 AWS Amplify 构建了一个 Android 应用程序!您为应用程序添加了身份验证功能,允许用户注册、登录和管理账户。应用程序中还添加了已配置 Amazon DynamoDB 数据库的可扩展 GraphQL API,从而允许用户创建和删除笔记。另外,您也使用 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.