AWS の開始方法
Android アプリケーションをビルドする
AWS Amplify を使用してシンプルな Android アプリケーションを作成する
モジュール 4: GraphQL API とデータベースを追加する
このモジュールでは、Amplify CLI とライブラリを使用して、アプリに GraphQL API を設定して追加します。
はじめに
ユーザー認証搭載のアプリケーションを作成、設定できました。API および CRUD オペレーション (作成、読み取り、更新、削除) をデータベースに追加してみます。
このモジュールでは、Amplify CLI とライブラリを使用してアプリケーションに API を追加します。作成する API は GraphQL API で、Amazon DynamoDB (NoSQL データベース) によってサポートされる AWS AppSync (マネージド GraphQL サービス) を利用します。GraphQL の概要については、このページにアクセスしてください。
作成するアプリケーションは、ユーザーがメモを作成、削除、一覧表示できるようにするメモアプリです。この例から、人気のあるさまざまな CRUD + L (作成、読み取り、更新、削除、一覧表示) アプリケーションをビルドする方法がよくわかります。
学習内容
- GraphQL API を作成してデプロイする
- API と対話するためのフロントエンドコードを記述する
主要な概念
API – 複数のソフトウェア仲介者間の通信および相互作用を可能にするプログラミングインターフェイスを提供します。
GraphQL – アプリケーションの型付き表現に基づくクエリ言語とサーバー側 API の実装。この API 表現は、GraphQL 型システムに基づくスキーマを使用して宣言されます。(GraphQL の詳細については、このページにアクセスしてください)。
所要時間
20 分
使用するサービス
実装
-
GraphQL API サービスおよびデータベースを作成する
GraphQL API および GraphQL API がサポートするデータベースを作成するには、ターミナルを開き、プロジェクトディレクトリからこのコマンドを実行します。
amplify add api ? Please select from one of the below mentioned services: select GraphQL and press enter ? Provide API name: select the default, press enter ? Choose the default authorization type for the API: use the arrow key to select Amazon Cognito User Pool and press enter ? Do you want to configure advanced settings for the GraphQL API: select the default No, I am done and press enter ? Do you have an annotated GraphQL schema? keep the default N and press enter ? What best describes your project: choose any model, we are going to replace it with our own anyway. Press enter ? Do you want to edit the schema now? type Y and press enter
プロジェクト (amplify init) を初期化した際に選択したデフォルトのテキストエディタによって、事前ビルド済みのデータスキーマが開かれます。
そのスキーマを削除して、アプリケーションの GraphQL スキーマと置換します。
type NoteData @model @auth (rules: [ { allow: owner } ]) { id: ID! name: String! description: String image: String }
データモデルは 1 つの NoteData クラスおよび 4 つのプロパティ (文字列の ID と名前は必須。説明と画像は任意) で構成されます。
データベースを作成して、データを保存する目的であることを @model トランスフォーマーが示しています。
@auth トランスフォーマーによって、このデータへのアクセスを許可する認証ルールが追加されます。このプロジェクトでは、NoteData の所有者だけがアクセスできるようにします。
完了したら必ず保存し、ターミナルに戻って、完了したことを Amplify CLI に伝達します。
? Press enter to continue, press enter.
数秒経過したら、成功メッセージが表示されます。
GraphQL schema compiled successfully.
-
クライアントサイドコードを生成する
作成した GraphQL データモデル定義に基づき、Amplify によってクライアントサイドコード (すなわち Swift コード) が生成され、アプリケーション内のデータが記述されます。
スクリプトを実行するには、ターミナルで次のコマンドを実行します。
amplify codegen models
この結果、ご覧のとおり、java/com/amplifyframework.datastore.generated.model ディレクトリに Java ファイルが作成されます。
➜ Android Getting Started git:(master) ✗ ls -al app/src/main/java/com/amplifyframework/datastore/generated/model total 24 drwxr-xr-x 4 stormacq admin 128 Oct 7 15:27 . drwxr-xr-x 3 stormacq admin 96 Oct 7 15:27 .. -rw-r--r-- 1 stormacq admin 1412 Oct 7 15:27 AmplifyModelProvider.java -rw-r--r-- 1 stormacq admin 7153 Oct 7 15:27 NoteData.java
このファイルが自動的にプロジェクトにインポートされます。
-
API サービスおよびデータベースをデプロイする
作成したバックエンド API およびデータベースをデプロイするには、ターミナルでコマンドを実行します。
amplify push # press Y when asked to continue ? Are you sure you want to continue? accept the default Y and press enter ? Do you want to generate code for your newly created GraphQL API type N and press enter
数分後、成功メッセージが表示されます。
✔ All resources are updated in the cloud GraphQL endpoint: https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql
-
API クライアントライブラリを Android Studio プロジェクトを追加する
コードに進む前に、Android Studio に戻って、下記の依存関係をモジュールの build.gradle および以前に追加したさまざまな amplifyframework 運用に追加して、[Sync Now] が表示されたらクリックします。
dependencies { implementation 'com.amplifyframework:aws-api:1.4.0' implementation 'com.amplifyframework:aws-auth-cognito:1.4.0' }
-
実行時に Amplify ライブラリを初期化する
Backend.kt を開き、initialize() メソッドの Amplify 初期化シーケンスに 1 行を追加します。try/catch ブロックが完成すると、下記のようになります。
try { Amplify.addPlugin(AWSCognitoAuthPlugin()) Amplify.addPlugin(AWSApiPlugin()) Amplify.configure(applicationContext) Log.i(TAG, "Initialized Amplify") } catch (e: AmplifyException) { Log.e(TAG, "Could not initialize Amplify", e) }
-
GraphQL データモデルとアプリケーションモデル間のブリッジングを追加する
プロジェクトには、メモを表すデータモデルがすでにあります。そのため、引き続きこのモデルを使うことにして、NoteData をメモに簡単に変換します。UserData.kt を開き、2 つのコンポーネント、つまり UserData.Note から NoteData オブジェクトを返す動的プロパティと、その反対に API NoteData を受け取り Userdata.Note を返す静的メソッドを追加します。
データクラスの Note 内に、下記を追加します。
// return an API NoteData from this Note object val data : NoteData get() = NoteData.builder() .name(this.name) .description(this.description) .image(this.imageName) .id(this.id) .build() // 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) // some additional code will come here later return result } }
必ず生成されたコードから NoteData クラスをインポートするようにします。
-
API CRUD メソッドをバックエンドクラスに追加する
API を呼び出す 3 つのメソッド、つまり Note をクエリするメソッド、新しい Note を作成するメソッド、Note を削除するメソッドを追加します。ユーザーインターフェイスから 3 つのメソッドと簡単にインタラクションできるように、アプリケーションデータモデル (メモ) 上で動作するようになっています。これらのメソッドは、透過的にメモを GraphQL の NoteData オブジェクトに変換します。
Backend.kt ファイルを開き、次のスニペットをバックエンドクラスの最後に追加します。
fun queryNotes() { Log.i(TAG, "Querying notes") Amplify.API.query( ModelQuery.list(NoteData::class.java), { response -> Log.i(TAG, "Queried") for (noteData in response.data) { Log.i(TAG, noteData.name) // TODO should add all the notes at once instead of one by one (each add triggers a UI refresh) UserData.addNote(UserData.Note.from(noteData)) } }, { error -> Log.e(TAG, "Query failure", error) } ) } fun createNote(note : UserData.Note) { Log.i(TAG, "Creating notes") Amplify.API.mutate( ModelMutation.create(note.data), { response -> Log.i(TAG, "Created") if (response.hasErrors()) { Log.e(TAG, response.errors.first().message) } else { Log.i(TAG, "Created Note with id: " + response.data.id) } }, { error -> Log.e(TAG, "Create failed", error) } ) } fun deleteNote(note : UserData.Note?) { if (note == null) return Log.i(TAG, "Deleting note $note") Amplify.API.mutate( ModelMutation.delete(note.data), { response -> Log.i(TAG, "Deleted") if (response.hasErrors()) { Log.e(TAG, response.errors.first().message) } else { Log.i(TAG, "Deleted Note $response") } }, { error -> Log.e(TAG, "Delete failed", error) } ) }
必ず生成されたコードから ModelQuery、ModelMutation、NoteData クラスをインポートするようにします。
最後に、このアプリケーションの起動時に、この API を呼び出して、サインインしているユーザーのメモのリストをクエリする必要があります。
Backend.ktfile で、updateUserData(withSignInStatus: Boolean) を下記のように更新します。
// change our internal state and query list of notes private fun updateUserData(withSignedInStatus : Boolean) { UserData.setSignedIn(withSignedInStatus) val notes = UserData.notes().value val isEmpty = notes?.isEmpty() ?: false // query notes when signed in and we do not have Notes yet if (withSignedInStatus && isEmpty ) { this.queryNotes() } else { UserData.resetNotes() } }
後は新しい Note の作成とリストから Note の削除を行うためのユーザーインターフェイス部分を作るだけです。
-
編集ボタンを追加して、メモを追加する
バックエンドおよびデータモデル部分が所定の位置に収まりました。このセクションの最後のステップとしてはユーザーが新しい Note を作成、削除できるようにします。
a.Android Studio で、res/layout 下に新しいレイアウトを作成します。[レイアウト] を右クリックして [New (新規)] を選択し、次に [Layout Resource File (レイアウトリソースファイル)] を選択します。名前を「activity_add_note」にしてその他はすべてデフォルトの値にします。[OK] をクリックします。
今作成した「activity_add_note」ファイルを開き、以下のコードを貼り付けて、生成されたコードを置換します。
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:fillViewport="true"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="8dp"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Create a New Note" android:textSize="10pt" /> <EditText android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:hint="name" android:inputType="text" android:lines="5" /> <EditText android:id="@+id/description" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:hint="description" android:inputType="textMultiLine" android:lines="3" /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <Button android:id="@+id/addNote" 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 Note" /> <Button android:id="@+id/cancel" style="?android:attr/buttonStyleSmall" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:backgroundTint="#FFC107" android:text="Cancel" /> </LinearLayout> </ScrollView>
メモの見出しと説明が入力できる非常にシンプルなレイアウトです。
b.AddNoteActivity クラスを追加します。
java/com.example.androidgettingstarted に新しい kotlin ファイル AddActivityNote.kt を作成してそれを開き、次のコードを追加します。
package com.example.androidgettingstarted import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_add_note.* import java.util.* class AddNoteActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_add_note) cancel.setOnClickListener { this.finish() } addNote.setOnClickListener { // create a note object val note = UserData.Note( UUID.randomUUID().toString(), name?.text.toString(), description?.text.toString() ) // store it in the backend Backend.createNote(note) // add it to UserData, this will trigger a UI refresh UserData.addNote(note) // close activity this.finish() } } companion object { private const val TAG = "AddNoteActivity" } }
最後に、manifests で AndroidManifest.xml を開き、アプリケーションノード内の任意の場所にこのアクティビティ要素を追加します。
<activity android:name=".AddNoteActivity" android:label="Add Note" android:theme="@style/Theme.GettingStartedAndroid.NoActionBar"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.example.androidgettingstarted.MainActivity" /> </activity>
c.メインアクティビティで「メモを追加」の FloatingActionButton を追加します。res/layout で、activity_main.xml を開いて既存の Floating Action Button の上にこれを追加します。
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fabAdd" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" android:visibility="invisible" android:src="@drawable/ic_baseline_post_add" app:fabCustomSize="60dp" app:fabSize="auto"/>
res/drawable に「メモを追加」アイコンを追加します。[drawable] を右クリックし、[New (新規)] を選択し、次に [Vector Asset (ベクターアセット)] を選択します。名前として「 ic_baseline_add」と入力し、Clip Art から追加アイコンを選択します。[Next (次へ)]、[Finish (終了)] の順にクリックします。
d.「メモを追加」ボタンをハンドリングするためのコードを追加します。
完全な機能を持つ「[追加] ボタン」にするための最後の 2 つの部分は、isSignedIn 値に従ってボタンを表示、非表示させることと、言うまでもありませんがボタンへのタップをハンドリングするコードを追加することです。
mainActivity.kt を開き onCreate() メソッドの末尾に次のコードを追加します。
// register a click listener fabAdd.setOnClickListener { startActivity(Intent(this, AddNoteActivity::class.java)) }
その後、同じ onCreate() メソッド内で、UserData.isSignedIn.observe をこのコードで置換します。
UserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp -> // update UI Log.i(TAG, "isSignedIn changed : $isSignedUp") //animation inspired by https://www.11zon.com/zon/android/multiple-floating-action-button-android.php if (isSignedUp) { fabAuth.setImageResource(R.drawable.ic_baseline_lock_open) Log.d(TAG, "Showing fabADD") fabAdd.show() fabAdd.animate().translationY(0.0F - 1.1F * fabAuth.customSize) } else { fabAuth.setImageResource(R.drawable.ic_baseline_lock) Log.d(TAG, "Hiding fabADD") fabAdd.hide() fabAdd.animate().translationY(0.0F) } })
すべてが想定どおりに動作することを検証するには、プロジェクトをビルドします。[Build (構築)] メニューをクリックして、[Make Project (プロジェクトの作成)] を選択します。Mac の場合は、⌘F9 を押します。操作エラーがないようにします。
アプリケーションを実行すると、「メモを追加」ボタンはユーザーがサインインすると表示され、ユーザーがサインアウトすると消えます。これで、メモを追加できるようになりました。
-
「スワイプして削除」の動作を追加する
「スワイプして削除」の動作はメモのリストにタッチハンドラーを追加することで実装できます。タッチハンドラーは赤の背景、削除アイコンを描画する部分を管理しており、タッチが発生すると Backend.delete() メソッドを呼び出します。
a.新しいクラスの SimpleTouchCallback を作成します。java/com の直下で、[example.androidgettingstarted] を右クリックし、[New (新規)]、[Kotlin File (Kotlin ファイル)] の順に選択して、名前として「SwipeCallback」と入力します。
新しいファイルに下記のコードをペーストします。
package com.example.androidgettingstarted import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView // https://stackoverflow.com/questions/33985719/android-swipe-to-delete-recyclerview class SwipeCallback(private val activity: AppCompatActivity): ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.LEFT ) { private val TAG: String = "SimpleItemTouchCallback" private val icon: Drawable? = ContextCompat.getDrawable( activity, R.drawable.ic_baseline_delete_sweep ) private val background: ColorDrawable = ColorDrawable(Color.RED) override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { super.onChildDraw( c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive ) val itemView = viewHolder.itemView val backgroundCornerOffset = 20 val iconMargin = (itemView.height - icon!!.intrinsicHeight) / 2 val iconTop = itemView.top + (itemView.height - icon.intrinsicHeight) / 2 val iconBottom = iconTop + icon.intrinsicHeight val iconRight: Int = itemView.right - iconMargin if (dX < 0) { val iconLeft: Int = itemView.right - iconMargin - icon.intrinsicWidth icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) background.setBounds( itemView.right + dX.toInt() - backgroundCornerOffset, itemView.top, itemView.right, itemView.bottom ) background.draw(c) icon.draw(c) } else { background.setBounds(0, 0, 0, 0) background.draw(c) } } override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { Toast.makeText(activity, "Moved", Toast.LENGTH_SHORT).show() return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) { Toast.makeText(activity, "deleted", Toast.LENGTH_SHORT).show() //Remove swiped item from list and notify the RecyclerView Log.d(TAG, "Going to remove ${viewHolder.adapterPosition}") // get the position of the swiped item in the list val position = viewHolder.adapterPosition // remove to note from the userdata will refresh the UI val note = UserData.deleteNote(position) // async remove from backend Backend.deleteNote(note) } }
コードの重要な行は、onSwiped() メソッド内にあります。このメソッドはスワイプのジェスチャが終わると呼び出されます。スワイプされたアイテムのリストで配置を収集し、UserData の構造 (これが UI を更新) とクラウドのバックエンドから対応するメモを排除します。
b.クラスができたので res/drawable に [Delete (削除)] アイコンを追加しましょう。[drawable] を右クリックし、[New (新規)] を選択し、次に [Vector Asset (ベクターアセット)] を選択します。名前として「ic_baseline_delete_sweep」と入力し、Clip Art から「delete sweep」アイコンを選択します。[Next (次へ)]、[Finish (終了)] の順にクリックします。
c.この新しいファイルに下記のコードをペーストします。「スワイプして削除」のジェスチャハンドラーを RecyclerView に追加します。
java/com/example.androidgettingstarted の直下で、MainActivity.kt を開いて、2 行のコードを setupRecyclerView に追加します。
// add a touch gesture handler to manager the swipe to delete gesture val itemTouchHelper = ItemTouchHelper(SwipeCallback(this)) itemTouchHelper.attachToRecyclerView(recyclerView)
-
ビルドとテスト
すべてが想定どおりに動作することを検証するには、プロジェクトをビルドして実行します。ツールバーの [Run (実行)] アイコン ▶️ をクリックするか、または「^ R」と入力します。操作エラーがないようにします。
まだサインインしていると想定して、アプリケーションは空のリストから起動します。現在はメモを追加するための [Add Note (メモを追加)] ボタンがあります。[Add Note (メモを追加)] シンボルをタップし、タイトルと説明を入力し、[Add Note (メモを追加)] ボタンをタップすると、メモがリストに表示されます。
メモは行を左にスワイプすることで削除できます。
完全なフローは下記のとおりです。