AWS の開始方法

Android アプリケーションをビルドする

AWS Amplify を使用してシンプルな Android アプリケーションを作成する

モジュール 5: 画像保存機能を追加する

このモジュールでは、ストレージと、画像をアプリのメモに関連付ける機能を追加します。

はじめに

メモアプリが機能するようになったので、各メモに画像を関連付ける機能を追加しましょう。このモジュールでは、Amplify CLI とライブラリを使用して、Amazon S3 を利用するストレージサービスを作成します。最後に、Android アプリケーションを更新して、画像のアップロード、フェッチ、レンダリングを有効にします。

学習内容

  • ストレージサービスを作成する
  • Android アプリケーションを更新する - 画像をアップロードおよびダウンロードするロジック
  • Android アプリケーションを更新する - ユーザーインターフェイス

主要な概念

ストレージサービス - 画像や動画などのファイルの保存とクエリは、ほとんどのアプリケーションの一般的な要件です。これを行う 1 つのオプションは、ファイルを 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.
  • Amplify ストレージライブラリを Android Studio Project に追加する

    コードに進む前に、Android Studio に戻って、以下の依存関係をモジュールの build.gradle および以前に追加したさまざまな `amplifyframework` implementation に追加して、[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)
    }
  • 画像 CRUD メソッドをバックエンドクラスに追加する

    Backend.kt にいるものとします。任意のバックエンドクラスで、3 つのメソッドを追加して、Storage から画像をアップロード、ダウンロード、削除します。

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

    3 つのメソッドは単に、Amplify カウンターパートを呼び出します。Amplify ストレージには、3 段階のファイル保護レベルがあります。

    • パブリック すべてのユーザーに読み取り権限がある
    • 保護 すべてのユーザーに読み取り権限があるが、書き込み権限は作成したユーザーに限定される
    • プライベート 読み込み権限も書き込み権限も、作成したユーザーに限定される

    このアプリケーションでは、画像の利用者をメモ所有者だけに限定したいため、StorageAccessLevel.PRIVATE プロパティを使用しています。

  • UI コードを追加して、画像をキャプチャします

    次のステップとして、UI を修正して、ユーザーが AddNoteACtivity の [画像を追加] ボタンをクリックすると、電話ライブラリから画像を選択できるようにします。

    2 つの変更が必要です。[Add Note (メモを追加)] のアクティビティレイアウトを変更して、[Add image (画像を追加)] のボタンおよびイメージビューを追加し、ハンドラーコードをアクティビティクラスに追加します。

    Android Studio の [res/layout] の直下で、activity_add_note.xml ファイルを開き、[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

    最後に、受信するコードを追加し、選択された画像を一時ファイルに保存します。

    下記のコードを 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 として 2 回使用します。最初の InputStream によって Bitmap 画像が作成されて UI に表示され、2 回目の 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 オブジェクトがメモオブジェクトに変換されるたびに、画像が同時にロードされます。画像がロードされると、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
        }
    }
  • メモが削除されたら、画像を削除します

    最後のステップのクリーンアップとして、ユーザーがメモを削除したら画像をクラウドストレージから削除します。ストレージスペースの有効利用するために削除する必要がないとしても、月間 GB 単位の Amazon S3 利用料金で AWS の請求額を抑えるために削除します (最初の 5GB が無料のため、このチュートリアルの実行では請求は発生しません)。

    SwipeCallback.kt を開き、下記のコードを onSwipe() メソッドの末尾に追加します。

    if (note?.imageName != null) {
        //asynchronously delete the image (and assume it will work)
        Backend.deleteImage(note.imageName!!)
    }
  • ビルドとテスト

    すべてが想定どおりに動作することを検証するには、プロジェクトをビルドして実行します。ツールバーの [Run (実行)] アイコン ▶️ をクリックするか、または「^ R」と入力します。操作エラーがないようにします。

    ユーザーはサインインしたままだと思われますが、アプリケーションは全セクションで削除しなかったメモのリストどおりに動作を開始します。もう一度 [Add Note (メモを追加)] ボタンを使用して、メモを作成します。今回は、ローカルの画像ストアから選択した写真を追加します。

    アプリケーションを終了し、再起動して画像が正しくロードされたか検証します。

    完全なフローは下記のとおりです。

    AndroidAppTutorial_Modiule5_Image1
    AndroidAppTutorial_Modiule5_Image2
    AndroidAppTutorial_Modiule5_Image3
    AndroidAppTutorial_Modiule5_Image4
    AndroidAppTutorial_Modiule5_Image5

まとめ

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.

    プルされた 2 つの構成ファイルが確認できます。「このバックエンドを変更する予定はありますか」という質問に「はい」と答えると、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 テクノロジーをさらに詳しく見て、アプリケーションを次のレベルに引き上げます。