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 及其后备数据库,请打开一个终端,然后从项目目录中执行此命令

    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) 将使用预构建数据 schema 打开。

    删除此 schema,并使用我们的应用程序 GraphQL schema 替换

    type NoteData
    @model
    @auth (rules: [ { allow: owner } ]) {
        id: ID!
        name: String!
        description: String
        image: String
    }

    数据模型由一个类 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 和数据库,请转至您的终端,然后执行命令

    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
  • 在转到此代码之前,请先返回 Android Studio,将以下依赖关系连同您之前添加的其他“amplifyframework”实现一起添加到您的模块的 build.gradle,然后在看到提示时单击 Sync Now

    dependencies {
        implementation 'com.amplifyframework:aws-api:1.4.0'
        implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'
    }
  • 打开 Backend.kt,然后在 initialize() 方法的 Amplify 初始化序列中添加一行。完整的尝试/捕获代码块应如下所示:

    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)
    }
  • 我们的项目已经有一个数据模型来表示备注。在此教程中,我们将继续使用该模型,并提供一种将 NoteData 转换为备注的简单方法。打开 UserData.kt 并添加两个组件:一个动态属性,从 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 类。

  • 我们添加 3 种方法来调用 API:一种查询 Note 的方法,一种创建新 Note 的方法,以及一种删除 Note 的方法。请注意,这些方法适用于应用程序数据模型 (Note),以便通过用户界面轻松交互。这些方法可以透明地将 Note 转换为 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,以查询当前登录用户的 Note 列表。

    在 Backend.kt 文件中,更新 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 下,创建一个新布局:右键单击 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>

    这是一个非常简单的布局,可以只输入 Note 标题和描述。

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

    最后,在清单下打开 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.在“Main Activity”中添加一个“Add Note”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 中添加一个“Add Note”图标。右键单击“drawable”,选择 New,然后选择 Vector Asset。输入 ic_baseline_add 作为名称,并从“Clip Art”中选择添加图标。单击 Next,然后单击 Finish

    d.添加代码以处理“Add Note”按钮。

    要拥有功能完全的“Add Button”,还需要完成的最后两项工作是让按钮根据 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。应该不会出现错误。

    运行应用程序时,您将看到“Add Note”按钮会在用户登录时显示,在用户注销时消失。您现在可以添加备注了。

  • 可以通过在 Note 列表中添加触摸处理程序来添加“滑动删除”行为。触摸处理程序负责绘制红色背景、删除图标,并在释放触摸时调用 Backend.delete() 方法。

    a.创建一个新类 SimpleTouchCallback。在 java/com 下,右键单击 example.androidgettingstarted,依次选择 NewKotlin File,然后输入 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 结构(这将更新用户界面)和云后端中删除。

    b.现在,我们已经有了类,让我们在 res/drawable 中添加“删除”图标。右键单击“drawable”,选择 New,然后选择 Vector Asset。输入 ic_baseline_delete_sweep 作为名称,并从“Clip Art”中选择“删除滑动”图标。单击 Next,然后单击 Finish

    c.将以下代码粘贴到新文件中:为 RecyclerView 添加“滑动删除”手势处理程序。

    在 java/com/example.androidgettingstarted 下,打开 MainActivity.kt,并将下面两行代码添加到 setupRecyclerView 中:

    // add a touch gesture handler to manager the swipe to delete gesture
    val itemTouchHelper = ItemTouchHelper(SwipeCallback(this))
    itemTouchHelper.attachToRecyclerView(recyclerView)
  • 要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。

    假设您仍处于登录状态,应用程序会在一个空列表上启动。它现在具有一个用于添加 Note 的“Add Note”按钮。单击“Add Note”符号、输入标题和描述、单击“Add Note”按钮,随后备注应显示在列表中。

    您可以通过向左滑动一行来删除 Note。

    以下是完整流程。

结论

现在您已创建了一个 Android 应用程序,并使用 AWS Amplify 添加了 GraphQL API,以及在应用程序中配置了创建、读取和删除功能。

在下一个模块中,我们将添加 UI 和行为来管理图片。