{"value":"### **背景:**\n亚马逊云科技提供了100余种产品免费套餐。其中,计算资源Amazon EC2首年12个月免费,750小时/月;存储资源 Amazon S3 首年12个月免费,5GB标准存储容量。\n[https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el](https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el)\n大家好,我是坚果,由于最近一直疫情居家,所以想到外边转转,但是确实出不去,那么作为程序员的我们肯定也不能闲着,于是想做一个很简单的旅行应用,具有基础的增删改查的功能。以及身份验证!这个时候考虑到 Amazon 具有高响应,高可用等特点,这个时候你只要 Amazon 结合一定的 Android 基础,就可以很方便快捷的拥有自己的应用程序。而且由于 Amazon 具有全球优势的特点,以及 Amazon Amplify 是一组位于云端的工具和无服务器服务都让他拥有了一定的优势,这样一来技术选型接确定了,那么说了这么多,如何结合 Amazon 很快的创建属于自己的 Android 应用程序呢?只需要简单的五个步骤就可以。接下来开始正文\n开始之前看一下具体你要具备哪些条件:\n### **先决条件**\n- [Android Studio 4.x](https://developer.android.com/studio) 或更高版本\n- 一个至少具有[这些权限](https://github.com/sebsto/amplify-ios-getting-started/blob/master/amplify-policy.json)的 [Amazon](https://aws.amazon.com/cn/getting-started/hands-on/build-android-app-amplify/?trk=6c2fcc87-4e1c-4db1-bedd-836152ec5ac8&sc_channel=el) 账户\n- [Node.js](https://nodejs.org/en/) 10 或更高版本\n- Amazon 命令行界面 [Amazon CLI 2.0.x](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) 或更高版本。\n 检查 noidejs 版本,node -v,发现版本是16.13.0,符合条件 \nandroid studio 版本查看方式\n点击 help-about,如图所示:版本符合要求\n\n![image.png](https://dev-media.amazoncloud.cn/a47f4ae90660444fba425493fd56337b_image.png)\n\n![image.png](https://dev-media.amazoncloud.cn/09dbecd7ea1d4e598f17798315f155ee_image.png)\n\n接下来构建您的首个 Android 应用程序。\n### **概览**\n在本文中,您将使用 Amazon Amplify 创建一个简单的 Android 应用程序,Amazon Amplify 是一组位于云端的工具和无服务器服务。在第一个单元中,您将构建一个简单的 Android 应用程序。在其余单元中,您将使用 Amplify 命令行接口 (Amplify CLI) 初始化一个本地应用程序、添加用户身份验证、添加一个 GraphQL API 和一个数据库以存储您的数据,并更新您的应用程序以存储图像。\n本文将引导您完成创建上面讨论的简单 Android 应用程序。\n本文分为五个简短单元。您必须按顺序完成每个单元才能进入下一单元。\n\n![image.png](https://dev-media.amazoncloud.cn/ee205bcc78724130a798c70e10f1fee6_image.png)\n\n使用 Amazon Amplify 创建简单的 Android 应用qq\n 1.构建 Android 应用程序\n 在 Android 模拟器中创建 Android 应用程序并测试\n 2.初始化本地应用程序\n 使用 Amazon Amplify 初始化本地应用程序\n 3.添加身份验证\n 添加身份验证到您的应用程序中\n 4.添加API和数据库\n 创建 GraphQL API\n 5.添加存储\n 添加存储到您的应用程序中\n\n![image.png](https://dev-media.amazoncloud.cn/2484d943c9c146f89de1a07cc587cb95_image.png)\n\n您将使用终端和 Google 的 [Android Studio](https://developer.android.com/studio) IDE 来构建此 Android 应用程序。\n### **1.创建和部署 Android 应用程序**\n在这,您将创建 Android 应用程序并使用 Amazon Amplify 的 Web 托管服务将其部署到云\n\n![image.png](https://dev-media.amazoncloud.cn/8c442d8140d44705a0ff7f51cb67e8f4_image.png)\n\n#### **1.1简介**\nAmazon Amplify 提供基于 Git 的工作流,用于创建、管理、集成和部署 Web 和移动应用程序的无服务器后端。Amplify CLI 提供了一个简单的基于文本的用户界面,用于预置和管理后端服务,如用于您的应用程序的用户身份验证或 REST 或 GraphQL API。使用 Amplify 库可以轻松地将这些后端服务与应用程序中的几行代码集成。\n#### **1.2实施**\n##### **1.2.1.创建 Android 项目**\n启动 Android Studio,然后选择 New Project\n\n![image.png](https://dev-media.amazoncloud.cn/3092c1b798444ae28d5ac860d2966403_image.png) \n\n在 **Phone and Tablet** 下,选择 **Basic Activity**,然后单击 **Next**:\n\n![image.png](https://dev-media.amazoncloud.cn/3794940a560d4ad5a96868f197cbdf6b_image.png)\n\n为您的项目键入名称,例如 **Android Amazon Started**。确保语言为 **Kotlin**,开发工具包最低为 **API 26: Android 8.0 (oreo)**,然后单击 **Finish**:\n\n![image.png](https://dev-media.amazoncloud.cn/2bca974217564e55a0d401a7308204de_image.png)\n\n现在,项目的框架已经存在,我们需要完成 4 个步骤来让我们的基本应用程序运行:\n1. 删除不需要的类和文件,并添加插件\n2. 创建类以保留数据结构\n3. 创建视图以在列表中保留单个备注\n4. 修改 MainActivity 以显示备注列表\n\n##### **1.2.2.删除不需要的类和文件,并添加插件**\n在“res/layout”和 java/com.example.androidgettingstarted 下,删除四个**片段**文件(选择这 4 个文件,右键单击,然后从上下文菜单中选择 **Delete** ):\n\n![image.png](https://dev-media.amazoncloud.cn/e26a3abb225445d3a37394de4ddb0604_image.png)\n\n在 **Gradle Scripts**下,打开 **build.gradle (Module:app)**,并添加 Kotlin 扩展插件。\nKotlin \n```\nplugins {\n id 'com.android.application'\n id 'kotlin-android'\n id 'kotlin-android-extensions' // <== add this line\n}\n```\n![image.png](https://dev-media.amazoncloud.cn/86c68dd2c20d46ec95ef825b6b45bd4e_image.png)\n\n##### **1.2.3.创建类以保留数据结构**\nUserData 类可保留用户状态:一个 isSignedIn 标记和一个备注值列表。\n要创建新类,请右键单击 java/com.example/androidgettingstarted,然后选择 **New -> Kotlin file/class**。键入 **UserData** 作为名称。\n\n![image.png](https://dev-media.amazoncloud.cn/5c748cf53d864bfdadb004699c99e3de_image.png)\n\n将以下代码粘贴到新创建的文件 (UserData.kt) 中 \nKotlin\n```\npackage com.example.androidgettingstarted\n\nimport android.graphics.Bitmap\nimport android.util.Log\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\n\n// a singleton to hold user data (this is a ViewModel pattern, without inheriting from ViewModel)\nobject UserData {\n\n private const val TAG = \"UserData\"\n\n //\n // observable properties\n //\n\n // signed in status\n private val _isSignedIn = MutableLiveData<Boolean>(false)\n var isSignedIn: LiveData<Boolean> = _isSignedIn\n\n fun setSignedIn(newValue : Boolean) {\n // use postvalue() to make the assignation on the main (UI) thread\n _isSignedIn.postValue(newValue)\n }\n\n // the notes\n private val _notes = MutableLiveData<MutableList<Note>>(mutableListOf())\n\n // please check https://stackoverflow.com/questions/47941537/notify-observer-when-item-is-added-to-list-of-livedata\n private fun <T> MutableLiveData<T>.notifyObserver() {\n this.postValue(this.value)\n }\n fun notifyObserver() {\n this._notes.notifyObserver()\n }\n\n fun notes() : LiveData<MutableList<Note>> = _notes\n fun addNote(n : Note) {\n val notes = _notes.value\n if (notes != null) {\n notes.add(n)\n _notes.notifyObserver()\n } else {\n Log.e(TAG, \"addNote : note collection is null !!\")\n }\n }\n fun deleteNote(at: Int) : Note? {\n val note = _notes.value?.removeAt(at)\n _notes.notifyObserver()\n return note\n }\n\n fun resetNotes() {\n this._notes.value?.clear() //used when signing out\n _notes.notifyObserver()\n }\n\n\n // a note data class\n data class Note(val id: String, val name: String, val description: String, var imageName: String? = null) {\n override fun toString(): String = name\n\n // bitmap image\n var image : Bitmap? = null\n\n }\n}\n```\n**我们刚刚添加了哪些内容?**\n- UserData 类负责保留用户数据,即一个 isSignedIn 标记用于跟踪当前的身份验证状态和备注对象列表。\n- 这两个属性是根据 LiveData 发布/订阅框架来实现的。它允许图形用户界面 (GUI) 订阅更改并做出相应反应。\n- 我们还添加了一个备注数据类,仅用于保留单个备注的数据。针对 ImageName 和 Image 使用了两个不同的属性。我们将在后续单元中介绍 Image。\n- 为 UserData 对象实施了单态设计模式,因为此模式允许只使用 UserData 即可从应用程序的任何位置引用此对象。\n##### **1.2.4.为列表中的但各单元格添加GUI**\n滚动列表中的单个单元格称为 RecyclerView,因为当用户上下滚动屏幕,屏幕上再也看不到视图时,可以回收视图。\n与常规视图一样,我们也创建了布局 XML 文件和 Kotlin 类。单个单元格类似以下内容:\n\n![image.png](https://dev-media.amazoncloud.cn/4e951e9df2354c448190f35b1c682eb4_image.png)\n\n要创建布局文件,请右键单击“res/layout”,然后选择 **New -> Layout Resource File**。键入 content_note 作为名称,并保留所有其他值,因为我们将直接编辑 XML 文件\n\n![image.png](https://dev-media.amazoncloud.cn/8a746fdec5bc41328a04d19c26a4d081_image.png)\n\n打开新创建的文件的 **Code** 视图。\n\n![image.png](https://dev-media.amazoncloud.cn/f7f0f58455cb4df2b74832687c562ede_image.png)\n\n在新创建的文件 (content_note.xml) 的“Code”视图中,通过粘贴以下代码来替代所生成的代码:\nKotlin\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"wrap_content\"\n android:orientation=\"horizontal\"\n android:paddingVertical=\"16dp\">\n\n <ImageView\n android:id=\"@+id/image\"\n android:layout_width=\"100dp\"\n android:layout_height=\"match_parent\"\n android:padding=\"8dp\"\n android:scaleType=\"centerCrop\"\n android:src=\"@drawable/ic_launcher_background\" />\n\n <LinearLayout\n android:layout_width=\"match_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_gravity=\"center\"\n android:layout_marginLeft=\"5dp\"\n android:orientation=\"vertical\">\n\n <TextView\n android:id=\"@+id/name\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"wrap_content\"\n android:text=\"Title\"\n android:textSize=\"20sp\"\n android:textStyle=\"bold\" />\n\n <TextView\n android:id=\"@+id/description\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"wrap_content\"\n android:text=\"Title\"\n android:textSize=\"15sp\" />\n\n </LinearLayout>\n</LinearLayout>\n```\n最后,创建 Kotlin 类:右键单击 java/com.example/androidgettingstarted,然后选择 **New -> Kotlin file/class**。键入 **NoteRecyclerViewAdapter** 作为名称。\n将以下代码粘贴到新创建的文件 (NoteRecyclerViewAdapter.kt) 中\nKotlin \n```\npackage com.example.androidgettingstarted\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\n\n// this is a single cell (row) in the list of Notes\nclass NoteRecyclerViewAdapter(\n private val values: MutableList<UserData.Note>?) :\n RecyclerView.Adapter<NoteRecyclerViewAdapter.ViewHolder>() {\n\n override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n val view = LayoutInflater.from(parent.context)\n .inflate(R.layout.content_note, parent, false)\n return ViewHolder(view)\n }\n\n override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n\n val item = values?.get(position)\n holder.nameView.text = item?.name\n holder.descriptionView.text = item?.description\n\n if (item?.image != null) {\n holder.imageView.setImageBitmap(item.image)\n }\n }\n\n override fun getItemCount() = values?.size ?: 0\n\n inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {\n val imageView: ImageView = view.findViewById(R.id.image)\n val nameView: TextView = view.findViewById(R.id.name)\n val descriptionView: TextView = view.findViewById(R.id.description)\n }\n}\n```\n**我们刚刚添加了哪些内容?**\n上述代码包含\n- 一个布局 xml 文件,该文件说明表示备注的单元格上的组件布局。它显示图像、备注标题和备注说明。\n- 一个支持 Kotlin 类。它在创建时接收备注数据对象,并将单个值分配给其相对应的视图(图像、标题和说明)\n##### **1.2.5.更新主要活动**\n现在,我们已经有了数据类(UserData 和备注)和单个备注的视图 (NoteRecyclerViewAdapter),让我们在主要活动上创建备注列表。\n从 Android Studio 左侧的文件列表,打开 res/layout/content_main.xml,并将代码替换为以下内容:\nXML\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n xmlns:tools=\"http://schemas.android.com/tools\"\n android:id=\"@+id/frameLayout\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n >\n\n <androidx.recyclerview.widget.RecyclerView\n android:id=\"@+id/item_list\"\n android:name=\"com.example.myapplication.ItemListFragment\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:layout_marginTop=\"60dp\"\n\n android:paddingHorizontal=\"8dp\"\n android:paddingVertical=\"8dp\"\n app:layoutManager=\"LinearLayoutManager\"\n tools:context=\".MainActivity\"\n tools:listitem=\"@layout/content_note\" />\n\n</FrameLayout>\n```\n从 Android Studio 左侧的文件列表,打开 java/com.example/androidgettingstarted/MainActivity.kt,并将代码替换为以下内容:\nXML\n```\npackage com.example.androidgettingstarted\n\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.Observer\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\nimport kotlinx.android.synthetic.main.activity_main.*\nimport kotlinx.android.synthetic.main.content_main.*\n\nclass MainActivity : AppCompatActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_main)\n setSupportActionBar(toolbar)\n\n // prepare our List view and RecyclerView (cells)\n setupRecyclerView(item_list)\n }\n\n // recycler view is the list of cells\n private fun setupRecyclerView(recyclerView: RecyclerView) {\n\n // update individual cell when the Note data are modified\n UserData.notes().observe(this, Observer<MutableList<UserData.Note>> { notes ->\n Log.d(TAG, \"Note observer received ${notes.size} notes\")\n\n // let's create a RecyclerViewAdapter that manages the individual cells\n recyclerView.adapter = NoteRecyclerViewAdapter(notes)\n })\n }\n\n companion object {\n private const val TAG = \"MainActivity\"\n }\n}\n```\n**我们刚刚添加了哪些内容?**\n- 主要布局是一个 RecyclerView,用于管理我们之前创建的单个单元格列表\n- 主要活动类可观察备注列表的变化,并创建一个 NoteRecyclerViewAdapter 以创建单个单元格。\n##### **1.2.6.验证生成依赖项**\n- 在 Gradle Scripts 下,打开 build.gradle (Module:app),并验证所生成的依赖关系是否正确。需要选中 \nlibraries versions。\nKotlin\n```\ngradle\ndependencies {\n implementation \"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version\"\n implementation 'androidx.core:core-ktx:1.3.2'\n implementation 'androidx.appcompat:appcompat:1.2.0'\n implementation 'com.google.android.material:material:1.2.1'\n implementation 'androidx.constraintlayout:constraintlayout:2.0.2'\n implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'\n implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'\n testImplementation 'junit:junit:4.+'\n androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n}\n```\n##### **1.2.7.构建和测试**\n- 现在,请在模拟器中构建并启动应用程序。单击工具栏中的运行图标 ▶️ 或按 ^ R。\n\n![image.png](https://dev-media.amazoncloud.cn/87a6ad453d4d460aa2c3778bcc5062ea_image.png)\n\n片刻之后,应用程序会在 Android 模拟器中启动,初始屏幕为空。\n\n![image.png](https://dev-media.amazoncloud.cn/f09db6d5333a424ea387659236ef98f6_image.png)\n\n在此阶段,没有要在运行时呈现的数据。到此您已成功创建 Android 应用程序。接下来开始使用 Amplify 进行构建!\n### **2.初始化 Amplify**\n在此单元中,您将安装并配置 Amplify CLI。\n\n![image.png](https://dev-media.amazoncloud.cn/854fccc737b24fd09e6ba661c03406c3_image.png)\n\n#### **2.1简介**\n现在我们已创建一个 Android 应用程序,我们想要继续开发并添加新功能。\n要开始在应用程序中使用 Amazon Amplify,必须安装 Amplify 命令行,初始化 Amplify 项目目录,将项目配置为使用 Amplify 库,并在运行时初始化 Amplify 库。\n#### **2.2实施**\n##### **2.2.1安装 Amplify CLI**\nAmazon Amplify CLI 取决于 Node.js,没有安装的化,请安装。\n要安装 Amazon Amplify CLI,请打开一个终端,然后**输入以下命令**:\nXML\n```\n## Install Amplify CLI\nnpm install -g @aws-amplify/cli\n```\n![image.png](https://dev-media.amazoncloud.cn/9ff3927ea71f4080805c63e66a2e93f2_image.png)\n\n![image.png](https://dev-media.amazoncloud.cn/3fa058c3818e415bb10ff863ac0c1e2e_image.png)\n查看版本:\n\n![image.png](https://dev-media.amazoncloud.cn/d3e0b4ed2bb246d4ba0f0ff51ace1029_image.png)\nXML\n```\n## Verify installation and version\namplify --version\n\n 7.6.26\n```\n##### **2.2.2初始化 Amplify 后端**\n要创建后端基本结构,首先需要初始化 Amplify 项目目录并创建云后端。\n打开项目所在目录,输入cmd,即可打开终端\n\n![image.png](https://dev-media.amazoncloud.cn/518a4a0c0f5542a3a56ad685079f8650_image.png)\n\n![image.png](https://dev-media.amazoncloud.cn/f221c0299308436380a7de2015ee1124_image.png)\n验证您是否在正确的目录中,它应该如下所示:\nXML\n```\nF:\\workspace\\AndroidASW>tree\n卷 开发环境 的文件夹 PATH 列表\n卷序列号为 D4D7-D5B3\nF:.\n├─.gradle\n│ ├─7.0.2\n│ │ ├─dependencies-accessors\n│ │ ├─fileChanges\n│ │ ├─fileHashes\n│ │ └─vcsMetadata-1\n│ ├─buildOutputCleanup\n│ ├─checksums\n│ └─vcs-1\n├─.idea\n│ ├─libraries\n│ └─modules\n│ └─app\n├─app\n│ ├─libs\n│ └─src\n│ ├─androidTest\n│ │ └─java\n│ │ └─com\n│ │ └─example\n│ │ └─androidasw\n│ ├─main\n│ │ ├─java\n│ │ │ └─com\n│ │ │ └─example\n│ │ │ └─androidasw\n│ │ └─res\n│ │ ├─drawable\n│ │ ├─drawable-v24\n│ │ ├─layout\n│ │ ├─menu\n│ │ ├─mipmap-anydpi-v26\n│ │ ├─mipmap-hdpi\n│ │ ├─mipmap-mdpi\n│ │ ├─mipmap-xhdpi\n│ │ ├─mipmap-xxhdpi\n│ │ ├─mipmap-xxxhdpi\n│ │ ├─navigation\n│ │ ├─values\n│ │ ├─values-land\n│ │ ├─values-night\n│ │ ├─values-w1240dp\n│ │ └─values-w600dp\n│ └─test\n│ └─java\n│ └─com\n│ └─example\n│ └─androidasw\n└─gradle\n └─wrapper\n\nF:\\workspace\\AndroidASW>\n```\n接下来,在亚马逊服务管理后台创建对应的用户,并添加权限类型,具体如下图所示:\n##### **2.2.3控制台相关操作**\n**[第一步注册](https://portal.aws.amazon.com/billing/signup?type=resubscribe#/paymentinformation)**\n\n![image.png](https://dev-media.amazoncloud.cn/be189597410f469dbc440aab8f2310f8_image.png)\n\n[https://aws.amazon.com/cn/free/?trk=95502bdb-28e0-4dc1-895c-2e975a171d36&sc_channel=ba&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=all&awsf.Free%20Tier%20Categories=all](https://aws.amazon.com/cn/free/?trk=95502bdb-28e0-4dc1-895c-2e975a171d36&sc_channel=ba&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=all&awsf.Free%20Tier%20Categories=all)\n**登录控制台**\n\n![image.png](https://dev-media.amazoncloud.cn/b4664aea104b4304ba4be59142e994ce_image.png)\n\n添加权限\n\n![image.png](https://dev-media.amazoncloud.cn/e278f8491a444689b895bf10ee4aa172_image.png)\n\n点击“下一步”,选择“直接附加现有策略”,一直“下一步\",后会提示创建用户成功。\n\n**添加用户**\n\n![image.png](https://dev-media.amazoncloud.cn/a81fb1f130e54e32b6f48104a9ceffd7_image.png)\n\n到此,我们也就创建完成。接下来继续下面的步骤。\n初始化 Amplify 项目结构和配置文件。**运行以下命令**:\namplify init\nJavaScript\n```\n\n? Enter a name for your project (androidgettingstarted): accept the default, press enter\n? Enter a name for the environment (dev): accept the default, press enter\n? Choose your default editor: use the arrow key to select your favorite text editor an press enter\n? Choose the type of app that you're building: android is already selected, press enter\n? Where is your Res directory: accept the default, press enter\n? Do you want to use an AWS profile?, Y, press enter\n? Please choose the profile you want to use: use the arrow keys to select your profile and press enter.\n```\n如果没有配置文件,可使用 Amazon CLI 键入命令 aws configure --profile <name> 创建一个。\nAmplify 在云中初始化您的项目,可能需要几分钟。几分钟后,您应该会看到如下消息:\n\n![image.png](https://dev-media.amazoncloud.cn/be6f4d491d5d4ab59aac77454cb772e4_image.png)\n\n##### **2.2.4将 Amplify 库添加到项目中**\nAmplify for Android 是作为 Apache Maven 软件包分发的。在本部分中,您会将软件包和其他必需的指令添加到构建配置中。\n返回 Android Studio,展开 Gradle Scripts 并打开 build.gradle (Project: Android_Getting_Started)。在 buildscript 和 allprojects 数据块的 repositories 数据块内添加行 mavenCentral()。\nJavaScript\n```\nbuildscript {\n ext.kotlin_version = \"1.4.10\"\n repositories {\n google()\n jcenter()\n\n // Add this line into `repositories` in `buildscript`\n mavenCentral()\n }\n dependencies {\n classpath \"com.android.tools.build:gradle:4.0.1\"\n classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n\n // NOTE: Do not place your application dependencies here; they belong\n // in the individual module build.gradle files\n }\n}\n\nallprojects {\n repositories {\n google()\n jcenter()\n\n // Add this line into `repositories` in `buildscript`\n mavenCentral()\n }\n}\n```\n在 **Gradle Scripts** 下,打开 **build.gradle (Module:app)**,并在 implementations 数据块中添加 Amplify 框架核心依赖项。\nJavaScript\n```\ndependencies {\n implementation fileTree(dir: \"libs\", include: [\"*.jar\"])\n implementation \"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version\"\n implementation 'androidx.core:core-ktx:1.3.2'\n implementation 'androidx.appcompat:appcompat:1.2.0'\n implementation 'com.google.android.material:material:1.2.1'\n implementation 'androidx.constraintlayout:constraintlayout:2.0.1'\n implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'\n implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'\n testImplementation 'junit:junit:4.13'\n androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n}\n```\n如果您使用 Java 或目标 Android SDK 21 或更早版本进行开发,请[查看文档](https://docs.amplify.aws/lib/project-setup/create-application/q/platform/android#n2-install-amplify-libraries)了解其他配置更改。\n现在,运行 **Gradle Sync**。\n\n![image.png](https://dev-media.amazoncloud.cn/99e6a70cb30044e5ba1901d3a1b16e16_image.png)\n\n过一会儿您将看到\nBUILD SUCCESSFUL in 1s\n\n![image.png](https://dev-media.amazoncloud.cn/5e582ca7acd04bac88a06fd591bf1062_image.png)\n\n##### **2.2.5在运行时初始化 Amplify**\n我们来创建一个后端类对代码进行分组,以便与后端交互。我使用[单例设计模式](https://en.wikipedia.org/wiki/Singleton_pattern),使其通过应用程序轻松可用,并确保 Amplify 库仅初始化一次。\n类初始化程序负责初始化 Amplify 库。\n在 java/com.example.androidgettingstarted 下创建一个新的 Kotlin 文件 Backend.kt,打开它并添加以下代码:\nJavaScript\n```\npackage com.example.androidgettingstarted\n\nimport android.content.Context\nimport android.util.Log\nimport com.amplifyframework.AmplifyException\nimport com.amplifyframework.core.Amplify\n\nobject Backend {\n\n private const val TAG = \"Backend\"\n\n fun initialize(applicationContext: Context) : Backend {\n try {\n Amplify.configure(applicationContext)\n Log.i(TAG, \"Initialized Amplify\")\n } catch (e: AmplifyException) {\n Log.e(TAG, \"Could not initialize Amplify\", e)\n }\n return this\n }\n}\n```\n应用程序启动时,初始化单例 Backend 对象。\n在 java/com.example.androidgettingstarted 下创建一个新的 Kotlin 文件 Application.kt,打开它并添加以下代码:\nJavaScript\n```\npackage com.example.androidgettingstarted\n\nimport android.app.Application\n\nclass AndroidGettingStartedApplication : Application() {\n\n override fun onCreate() {\n super.onCreate()\n\n // initialize Amplify when application is starting\n Backend.initialize(applicationContext)\n }\n}\n```\n在“manifests”下,打开 AndroidManifest.xml,并将应用程序类的名称添加到 <application> 元素。\nJavaScript\n```\n<!-- add the android:name attribute to the application node -->\n <application\n android:name=\"AndroidGettingStartedApplication\"\n android:allowBackup=\"true\"\n android:icon=\"@mipmap/ic_launcher\"\n android:label=\"@string/app_name\"\n android:roundIcon=\"@mipmap/ic_launcher_round\"\n android:supportsRtl=\"true\"\n android:theme=\"@style/Theme.GettingStartedAndroid\">\n...\n```\n打开此文件后,请添加一些在本教程的后续步骤中应用程序需要的权限:\nXML\n```\n<!-- add these nodes between manifest and application -->\n <uses-permission android:name=\"android.permission.INTERNET\"/>\n <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n```\n##### **2.2.6验证您的设置**\n要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。\n要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的 运行 图标 ▶️ 或按 ^ R。\n\n![image.png](https://dev-media.amazoncloud.cn/e3a0c6bfc2d74c0791e675b22265fcab_image.png)\n\n应该不会出现错误。\nBUILD SUCCESSFUL in 6s\n23 actionable tasks: 8 executed, 15 up-to-date\n\n### **3.添加身份验证**\n在此单元中,您将使用 Amplify CLI 和库配置和添加身份验证到您的应用程序中。\n\n![image.png](https://dev-media.amazoncloud.cn/c89263a43dc040729742921e6169b259_image.png)\n\n#### **3.1简介**\n您将添加的下一个功能是用户身份验证。在此单元中,您将了解如何使用 Amplify CLI 和库对用户进行身份验证,以利用托管用户身份提供商 [Amazon Cognito](https://aws.amazon.com/cn/cognito/)。\n您还将了解如何使用 Cognito 托管用户界面展示整个用户身份验证流,从而使用户只需几行代码即可注册、登录和重置密码。\n使用“托管用户界面”意味着应用程序利用 Cognito 网页登录和注册用户界面流。应用程序的用户将重定向到 Cognito 托管的网页,并在登录后重定向回应用程序。当然,Cognito 和 Amplify 也支持本机 UI。\n#### **3.2实施**\n##### **3.2.1创建身份验证服务**\n要创建身份验证服务,请打开一个终端,然后执行以下命令:\namplify add auth\n当您看到此消息时,即表示配置成功(资源的确切名称将有所不同):\nSuccessfully added resource androidgettingstartedfc5a4717 locally\n##### **3.2.2部署身份验证服务**\n现在,已在本地配置身份验证服务,我们可以将它部署到云:在终端中,请在项目目录中执行以下命令:\namplify push\n\n#press Y when asked to continue\n片刻之后,您应看到以下消息:\nJavaScript\n```\n✔ All resources are updated in the cloud\n\nHosted UI Endpoint: https://androidgettingstarted-dev.auth.eu-central-1.amazoncognito.com/\nTest Your Hosted UI Endpoint: https://androidgettingstarted-dev.auth.eu-central-1.amazoncognito.com/login?respons\n```\n##### **3.2.3向项目添加 Amplify 身份验证库**\n在转到代码之前,请先返回 Android Studio,将以下依赖项连同您之前添加的其他 \n```amplifyframework``` 实现一起,添加到您的单元的 \n```build.gradle```,然后在看到提示时,单击 **Sync Now**:\nJavaScript\n```\ndependencies {\n ...\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n}\n```\n##### **3.2.4在运行时配置Amplify身份验证库**\n返回 Android Studio,打开 Backend.kt 文件。在后端类中,向我们在上一部分(在 initialize() 方法中)添加的 Amplify 初始化代码**添加一行**。\n完整代码块应如下所示:\t\nJavaScript\n```\n// inside Backend class\nfun initialize(applicationContext: Context) : Backend {\n try {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.configure(applicationContext)\n\n Log.i(TAG, \"Initialized Amplify\")\n } catch (e: AmplifyException) {\n Log.e(TAG, \"Could not initialize Amplify\", e)\n }\n return this\n}\n```\n请不要忘记添加导入语句,Android Studio 会自动为您完成此操作(在 Mac 上,在代码编辑器检测到的每个错误上,同时按 Alt 和 Enter 键)。\n像在上一步骤中那样,为每个缺少的类定义添加所需的导入语句(在红色单词上同时按 Alt 和 Enter 键)。\n要验证一切是否都按预期运行,请构建项目。单击 **Build** 菜单,并选择 **Make Project**,或者,在 Mac 上按 **⌘F9**。应该不会出现错误。\n##### **3.2.5在运行时触发身份验证**\n其余代码更改会跟踪用户的状态(他们是否已登录?)并在用户单击锁定图标时触发“SignIn/SignUp”用户界面。\n###### **a.添加 signIn 和 signOut 方法**\n在后端类中的任意位置,添加以下四种方法:\nJava\n```\nprivate fun updateUserData(withSignedInStatus : Boolean) {\n UserData.setSignedIn(withSignedInStatus)\n}\n\nfun signOut() {\n Log.i(TAG, \"Initiate Signout Sequence\")\n\n Amplify.Auth.signOut(\n { Log.i(TAG, \"Signed out!\") },\n { error -> Log.e(TAG, error.toString()) }\n )\n}\n\nfun signIn(callingActivity: Activity) {\n Log.i(TAG, \"Initiate Signin Sequence\")\n\n Amplify.Auth.signInWithWebUI(\n callingActivity,\n { result: AuthSignInResult -> Log.i(TAG, result.toString()) },\n { error: AuthException -> Log.e(TAG, error.toString()) }\n )\n}\n```\n然后为每个缺少的类定义添加所需的导入语句(在红色单词上按 Alt 和 Enter 键)。当您可以在多个类之间进行选择时,请务必从 Amplify 包中选择一个,如下面的屏幕截图所示。\n\n![image.png](https://dev-media.amazoncloud.cn/7b53ca6149004caea5522c1627342ef7_image.png)\n\n请注意,我们没有在这些方法中更新 UserData.isSignedIn 标记,该操作将在下一节中完成。\n###### **b.添加身份验证中心侦听器**\n为了跟踪身份验证状态的变化,我们添加了代码以订阅由 Amplify 发送的身份验证事件。我们在 Backend.initialize() 方法中初始化该中心。\n在收到身份验证事件时,我们将调用 updateUserData() 方法。此方法可使 UserData 对象保持同步。UserData.isSignedIn 属性是 LiveData<Boolean>,这意味着当值更改时,订阅此属性的观察者将收到通知。我们使用此机制来自动刷新用户界面。\n我们还添加了代码以在应用程序启动时检查以前的身份验证状态。当应用程序启动时,它会检查 Cognito 会话是否已存在,并相应地更新 UserData。\n在 Backend.initialize() 中,在 try/catch 块之后和 return 语句之前**添加以下代码**。\nJava\n```\n// in Backend.initialize() function, after the try/catch block but before the return statement \n\nLog.i(TAG, \"registering hub event\")\n\n// listen to auth event\nAmplify.Hub.subscribe(HubChannel.AUTH) { hubEvent: HubEvent<*> ->\n\n when (hubEvent.name) {\n InitializationStatus.SUCCEEDED.toString() -> {\n Log.i(TAG, \"Amplify successfully initialized\")\n }\n InitializationStatus.FAILED.toString() -> {\n Log.i(TAG, \"Amplify initialization failed\")\n }\n else -> {\n when (AuthChannelEventName.valueOf(hubEvent.name)) {\n AuthChannelEventName.SIGNED_IN -> {\n updateUserData(true)\n Log.i(TAG, \"HUB : SIGNED_IN\")\n }\n AuthChannelEventName.SIGNED_OUT -> {\n updateUserData(false)\n Log.i(TAG, \"HUB : SIGNED_OUT\")\n }\n else -> Log.i(TAG, \"\"\"HUB EVENT:${hubEvent.name}\"\"\")\n }\n }\n }\n}\n\nLog.i(TAG, \"retrieving session status\")\n\n// is user already authenticated (from a previous execution) ?\nAmplify.Auth.fetchAuthSession(\n { result ->\n Log.i(TAG, result.toString())\n val cognitoAuthSession = result as AWSCognitoAuthSession\n // update UI\n this.updateUserData(cognitoAuthSession.isSignedIn)\n when (cognitoAuthSession.identityId.type) {\n AuthSessionResult.Type.SUCCESS -> Log.i(TAG, \"IdentityId: \" + cognitoAuthSession.identityId.value)\n AuthSessionResult.Type.FAILURE -> Log.i(TAG, \"IdentityId not present because: \" + cognitoAuthSession.identityId.error.toString())\n }\n },\n { error -> Log.i(TAG, error.toString()) }\n)\n```\n要验证一切是否都按预期运行,请构建项目。单击 **Build**菜单,并选择 **Make Project**,或者,在 Mac 上按 **⌘F9**。应该不会出现错误。\n###### **c.更新用户界面代码**\n代码中的最后一个更改与用户界面相关,我们将 [FloatingActionButton](https://developer.android.com/reference/com/google/android/material/floatingactionbutton/FloatingActionButton) 添加到主要活动中。\n在“res/layout”下,打开 activity_main.xml,并将现有的 FloatingActionButton **替换**为以下内容:\nJava\n```\n<com.google.android.material.floatingactionbutton.FloatingActionButton\n android:id=\"@+id/fabAuth\"\n android:layout_width=\"wrap_content\"\n android:layout_height=\"wrap_content\"\n android:layout_alignParentRight=\"true\"\n android:layout_gravity=\"bottom|end\"\n android:layout_margin=\"@dimen/fab_margin\"\n android:src=\"@drawable/ic_baseline_lock\"\n app:fabCustomSize=\"60dp\"\n app:fabSize=\"auto\"\n />\n```\n在“res/drawable”下添加一个锁状图标。右键单击“drawable”,选择 **New**,然后选择 **Vector Asset**。从 Clilp Art 中选择锁状图标,然后输入 **ic_baseline_lock**(不含 _24)作为名称,并从 Clip Art 中选择闭合的锁状图标。单击 **Next**,然后单击 **Finish**。\n对打开的锁状图标重复相同的操作。\n\n![image.png](https://dev-media.amazoncloud.cn/e1b2deee21d64c8f8b4594009c28c550_image.png)\n\n执行完以上操作后,您的“drawable”目录下应具有以下文件:\n\n![image.png](https://dev-media.amazoncloud.cn/0573d92a6f824f9c934c437f4a3d2284_image.png)\n\n现在,在代码中链接新创建的按钮。在 java/com.example.androidgettingstarted/ 下,打开 MainActivity.kt 并添加以下代码。\nJava\n```\n// anywhere in the MainActivity class\nprivate fun setupAuthButton(userData: UserData) {\n\n // register a click listener\n fabAuth.setOnClickListener { view ->\n\n val authButton = view as FloatingActionButton\n\n if (userData.isSignedIn.value!!) {\n authButton.setImageResource(R.drawable.ic_baseline_lock_open)\n Backend.signOut()\n } else {\n authButton.setImageResource(R.drawable.ic_baseline_lock_open)\n Backend.signIn(this)\n }\n }\n}\n```\n还是在 MainActivity 中,在 onCreate() 方法的末尾**添加以下代码**:\nJava\n```\nsetupAuthButton(UserData)\n\nUserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp ->\n // update UI\n Log.i(TAG, \"isSignedIn changed : $isSignedUp\")\n\n if (isSignedUp) {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock_open)\n } else {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock)\n }\n})\n```\n以上代码会针对 Userdata.isSignedIn 值注册观察者。当 isSignedIn 值更改时调用闭包。现在,我们只是更改锁状图标:当用户通过身份验证时为打开的锁,当用户没有会话时为闭合的锁。\n要验证一切是否都按预期运行,请构建项目。单击 **Build** 菜单,并选择 **Make Project**,或者,在 Mac 上按 **⌘F9**。应该不会出现错误。\n###### **d.更新 AndroidManifest.xml 和 MainActivity**\n最后,我们必须确保在 Cognito 托管用户界面提供的 Web 身份验证序列结束时启动我们的应用程序。我们在清单文件中添加一个新的活动。当接收到 gettingstarted URI 方案时,将调用该活动。\n在 Android Studio 中的“manifests”下,打开 AndroidManifest.xml,并在应用程序元素中添加以下活动。\nJava\n```\n <activity\nandroid:name=\"com.amazonaws.mobileconnectors.cognitoauth.activities.CustomTabsRedirectActivity\">\n <intent-filter>\n <action android:name=\"android.intent.action.VIEW\" />\n\n <category android:name=\"android.intent.category.DEFAULT\" />\n <category android:name=\"android.intent.category.BROWSABLE\" />\n\n <data android:scheme=\"gettingstarted\" />\n </intent-filter>\n </activity>\n```\n在 java/com.example.androidgettingstarted/ 下,打开 MainActivity.kt 并在类中的任意位置添加以下代码。\nJava\n```\n// MainActivity.kt\n// receive the web redirect after authentication\noverride fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n super.onActivityResult(requestCode, resultCode, data)\n Backend.handleWebUISignInResponse(requestCode, resultCode, data)\n}\n在 java/com.example.androidgettingstarted/ 下,打开 Backend.kt 并在类中的任意位置添加以下代码。\n// Backend.kt\n// pass the data from web redirect to Amplify libs \nfun handleWebUISignInResponse(requestCode: Int, resultCode: Int, data: Intent?) {\n Log.d(TAG, \"received requestCode : $requestCode and resultCode : $resultCode\")\n if (requestCode == AWSCognitoAuthPlugin.WEB_UI_SIGN_IN_ACTIVITY_CODE) {\n Amplify.Auth.handleWebUISignInResponse(data)\n }\n}\n```\n###### **e.构建和测试**\n要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。应用程序将启动,且屏幕右下角会显示一个闭合的锁状浮动按钮。\n以下是完整的注册流程。\n\n![image.png](https://dev-media.amazoncloud.cn/fbce7916dc2f489893479b94cb8994a5_image.png)\n![image.png](https://dev-media.amazoncloud.cn/8ff2e17421eb4e9b8c072b04455dc579_image.png)\n![image.png](https://dev-media.amazoncloud.cn/d51b7e3254954a2ab6ae88a4000dc5b5_image.png)\n![image.png](https://dev-media.amazoncloud.cn/a96f27872fab47f7b4b20a98ae638b7c_image.png)\n![image.png](https://dev-media.amazoncloud.cn/dd8be465bd5d4385b2289426d23a550d_image.png)\n\n### **4.添加 GraphQL API 和数据库**\n在此单元中,您将使用 Amplify CLI 和库配置和添加 GraphQL API 到您的应用程序中。\n\n![image.png](https://dev-media.amazoncloud.cn/c82a789162a14adc9ec4c608735bf542_image.png)\n\n#### **4.1简介**\n现在,我们已经创建并配置了带用户身份验证功能的应用程序。接下来,我们要在数据库中添加 API 以及“创建”、“读取”、“更新”、“删除”(CRUD) 操作。\n在此单元中,您将使用 Amplify CLI 和库将 API 添加到您的应用程序中。您将创建的 API 是 [GraphQL](https://graphql.org/) API,它利用 [Amazon DynamoDB](https://aws.amazon.com/cn/dynamodb/)(NoSQL 数据库)支持的 [Amazon AppSync](https://aws.amazon.com/cn/appsync/)(托管 GraphQL 服务)。有关 GraphQL 的介绍,请[访问此页面](https://graphql.org/learn/)。\n您将构建的应用程序是备注记录应用程序,用户可使用它创建、删除和列出备注。本示例将让您了解如何构建很多常见类型的 CRUD+L(创建、读取、更新、删除和列出)应用程序。\n#### **4.2实施**\n##### **4.2.1创建 GraphQL API 服务和数据库**\n要创建 GraphQL API 及其后备数据库,请打开一个终端,然后从项目目录中**执行此命令**:\namplify add api\n初始化项目时选择的默认文本编辑器 (amplify init) 将使用预构建数据 schema 打开。\n**删除**此 schema,并使用我们的应用程序 GraphQL schema **替换**:\nJava\n```\ntype NoteData\n@model\n@auth (rules: [ { allow: owner } ]) {\n id: ID!\n name: String!\n description: String\n image: String\n}\n```\n数据模型由一个类 NoteData 和 4 个属性组成:ID 和名称是必填项,描述和图像是选填字符串。\n@model 转换器指示我们需要创建数据库来存储数据。\n@auth 转换器添加了身份验证规则,以允许访问此数据。对于此项目,我们希望仅 NoteData 的拥有者有访问它们的权限。\n完成后,请不要忘记**保存**,然后返回您的终端以告知 Amplify CLI 您已完成。\n? Press enter to continue, press enter.\n几秒钟后,您应该会看到一条成功消息:\nGraphQL schema compiled successfully.\n##### **4.2.2生成客户端代码**\n根据我们刚刚创建的 GraphQL 数据模型定义,Amplify 会生成客户端代码(即 Swift 代码)以代表我们应用程序中的数据。\n要生成代码,请在终端**执行以下命令**:\namplify codegen models\n这将在 java/com/amplifyframework.datastore.generated.model 目录中创建 Java 文件,如以下情况所示:\nJava\n```\n➜ Android Getting Started git:(master) ✗ ls -al app/src/main/java/com/amplifyframework/datastore/generated/model \ntotal 24\ndrwxr-xr-x 4 stormacq admin 128 Oct 7 15:27 .\ndrwxr-xr-x 3 stormacq admin 96 Oct 7 15:27 ..\n-rw-r--r-- 1 stormacq admin 1412 Oct 7 15:27 AmplifyModelProvider.java\n-rw-r--r-- 1 stormacq admin 7153 Oct 7 15:27 NoteData.java\n```\n这些文件会自动导入到您的项目中。\n##### **4.2.2部署 API 服务和数据库**\n要部署我们刚刚创建的后端 API 和数据库,请转至您的终端,然后**执行命令**:\nJava\n```\namplify push\n# press Y when asked to continue\n\n? Are you sure you want to continue? accept the default Y and press enter\n? Do you want to generate code for your newly created GraphQL API type N and press enter\n```\n几分钟后,您应该会看到一条成功消息:\n✔ All resources are updated in the cloud\n\nGraphQL endpoint: [https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql]( https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql)\n##### **4.2.4将API客户端库添加到 Android Studio 项目**\n在转到此代码之前,请先返回 Android Studio,将以下依赖关系连同您之前添加的其他“amplifyframework”实现一起添加到您的单元的 build.gradle,然后在看到提示时单击 **Sync Now**。\nJava\n```\ndependencies {\n implementation 'com.amplifyframework:aws-api:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n}\n```\n##### **4.2.5在运行时初始化 Amplify 库**\n打开 Backend.kt,然后在 initialize() 方法的 Amplify 初始化序列中添加一行。完整的尝试/捕获代码块应如下所示:\nJava\n```\ntry {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.addPlugin(AWSApiPlugin())\n Amplify.configure(applicationContext)\n\n Log.i(TAG, \"Initialized Amplify\")\n} catch (e: AmplifyException) {\n Log.e(TAG, \"Could not initialize Amplify\", e)\n}\n```\n##### **4.2.6在 GraphQL 数据模型和应用程序模型之间添加桥接**\n我们的项目已经有一个数据模型来表示备注。在此教程中,我们将继续使用该模型,并提供一种将 NoteData 转换为备注的简单方法。打开 UserData.kt 并添加两个组件:一个动态属性,从 UserData.Note 返回 NoteData 对象;一个相反静态方法,接收 API NoteData 并返回 Userdata.Note。\n在数据类 Note 中,添加以下内容:\nJava\n```\n// return an API NoteData from this Note object\nval data : NoteData\n get() = NoteData.builder()\n .name(this.name)\n .description(this.description)\n .image(this.imageName)\n .id(this.id)\n .build()\n\n// static function to create a Note from a NoteData API object\ncompanion object {\n fun from(noteData : NoteData) : Note {\n val result = Note(noteData.id, noteData.name, noteData.description, noteData.image)\n // some additional code will come here later\n return result\n }\n} \n```\n确保通过生成的代码导入 NoteData 类。\n##### **4.2.7将 API CRUD 方法添加到后端类**\n我们添加 3 种方法来调用 API:一种查询 Note 的方法,一种创建新 Note 的方法,以及一种删除 Note 的方法。请注意,这些方法适用于应用程序数据模型 (Note),以便通过用户界面轻松交互。这些方法可以透明地将 Note 转换为 GraphQL 的 NoteData 对象。\n**打开** Backend.kt 文件,然后在后端类末尾**添加**以下代码段:\nJava\n```\nfun queryNotes() {\n Log.i(TAG, \"Querying notes\")\n\n Amplify.API.query(\n ModelQuery.list(NoteData::class.java),\n { response ->\n Log.i(TAG, \"Queried\")\n for (noteData in response.data) {\n Log.i(TAG, noteData.name)\n // TODO should add all the notes at once instead of one by one (each add triggers a UI refresh)\n UserData.addNote(UserData.Note.from(noteData))\n }\n },\n { error -> Log.e(TAG, \"Query failure\", error) }\n )\n}\n\nfun createNote(note : UserData.Note) {\n Log.i(TAG, \"Creating notes\")\n\n Amplify.API.mutate(\n ModelMutation.create(note.data),\n { response ->\n Log.i(TAG, \"Created\")\n if (response.hasErrors()) {\n Log.e(TAG, response.errors.first().message)\n } else {\n Log.i(TAG, \"Created Note with id: \" + response.data.id)\n }\n },\n { error -> Log.e(TAG, \"Create failed\", error) }\n )\n}\n\nfun deleteNote(note : UserData.Note?) {\n\n if (note == null) return\n\n Log.i(TAG, \"Deleting note $note\")\n\n Amplify.API.mutate(\n ModelMutation.delete(note.data),\n { response ->\n Log.i(TAG, \"Deleted\")\n if (response.hasErrors()) {\n Log.e(TAG, response.errors.first().message)\n } else {\n Log.i(TAG, \"Deleted Note $response\")\n }\n },\n { error -> Log.e(TAG, \"Delete failed\", error) }\n )\n}\n确保通过生成的代码导入 ModelQuery、ModelMutation 和 NoteData 类。\n最后,我们必须在应用程序启动时调用此 API,以查询当前登录用户的 Note 列表。\n在 Backend.kt 文件中,更新 updateUserData(withSignInStatus: Boolean) 方法,使其看起来类似以下内容:\n// change our internal state and query list of notes \nprivate fun updateUserData(withSignedInStatus : Boolean) {\n UserData.setSignedIn(withSignedInStatus)\n\n val notes = UserData.notes().value\n val isEmpty = notes?.isEmpty() ?: false\n\n // query notes when signed in and we do not have Notes yet\n if (withSignedInStatus && isEmpty ) {\n this.queryNotes()\n } else {\n UserData.resetNotes()\n }\n}\n```\n现在,只需创建一个用户界面即可创建新 Note 和从列表中删除 Note。\n##### **4.2.8添加“Edit\"按钮以添加备注**\n现在,后端和数据模型已到位,本节的最后一步是让用户创建新的 Note 然后将其删除。\n###### **a.在 Android Studio 中的 res/layout 下,创建一个新布局:**\n右键单击 **layout**,选择“New”,然后选择 **Layout Resource File**。将此文件命名为 activity_add_note 并接受所有其他默认值。单击 **OK**。\n\n![image.png](https://dev-media.amazoncloud.cn/94ea6d84d209458eb7b08114cdd4dc2c_image.png)\n\n打开刚刚创建的文件 activity_add_note,然后通过粘贴以下内容**替换**所生成的代码:\nJava\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"match_parent\"\n android:fitsSystemWindows=\"true\"\n android:fillViewport=\"true\">\n\n <LinearLayout\n android:layout_width=\"match_parent\"\n android:layout_height=\"wrap_content\"\n android:orientation=\"vertical\"\n android:padding=\"8dp\">\n\n <TextView\n android:id=\"@+id/title\"\n android:layout_width=\"wrap_content\"\n android:layout_height=\"wrap_content\"\n android:layout_marginTop=\"8dp\"\n android:text=\"Create a New Note\"\n android:textSize=\"10pt\" />\n\n <EditText\n android:id=\"@+id/name\"\n android:layout_width=\"fill_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_marginTop=\"8dp\"\n android:hint=\"name\"\n android:inputType=\"text\"\n android:lines=\"5\" />\n\n <EditText\n android:id=\"@+id/description\"\n android:layout_width=\"fill_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_marginBottom=\"8dp\"\n android:hint=\"description\"\n android:inputType=\"textMultiLine\"\n android:lines=\"3\" />\n\n <Space\n android:layout_width=\"match_parent\"\n android:layout_height=\"0dp\"\n android:layout_weight=\"1\" />\n\n <Button\n android:id=\"@+id/addNote\"\n style=\"?android:attr/buttonStyleSmall\"\n android:layout_width=\"fill_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_gravity=\"center_horizontal\"\n android:backgroundTint=\"#009688\"\n android:text=\"Add Note\" />\n\n <Button\n android:id=\"@+id/cancel\"\n style=\"?android:attr/buttonStyleSmall\"\n android:layout_width=\"fill_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_gravity=\"center_horizontal\"\n android:backgroundTint=\"#FFC107\"\n android:text=\"Cancel\" />\n\n </LinearLayout>\n</ScrollView>\n```\n这是一个非常简单的布局,可以只输入 Note 标题和描述。\n###### **b.添加 AddNoteActivity 类。**\n在 java/com.example.androidgettingstarted 下,创建一个新的 Kotlin 文件 AddActivityNote.kt,然后打开此文件并添加以下代码: \nJava\n```\npackage com.example.androidgettingstarted\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport kotlinx.android.synthetic.main.activity_add_note.*\nimport java.util.*\n\nclass AddNoteActivity : AppCompatActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_add_note)\n\n cancel.setOnClickListener {\n this.finish()\n }\n\n addNote.setOnClickListener {\n\n // create a note object\n val note = UserData.Note(\n UUID.randomUUID().toString(),\n name?.text.toString(),\n description?.text.toString()\n )\n\n // store it in the backend\n Backend.createNote(note)\n\n // add it to UserData, this will trigger a UI refresh\n UserData.addNote(note)\n\n // close activity\n this.finish()\n }\n }\n\n companion object {\n private const val TAG = \"AddNoteActivity\"\n }\n} \n```\n最后,在清单下打开 AndroidManifest.xml,并在应用程序节点的任何位置添加以下活动元素。\nJava\n```\n<activity\n android:name=\".AddNoteActivity\"\n android:label=\"Add Note\"\n android:theme=\"@style/Theme.GettingStartedAndroid.NoActionBar\">\n <meta-data\n android:name=\"android.support.PARENT_ACTIVITY\"\n android:value=\"com.example.androidgettingstarted.MainActivity\" />\n</activity>\n```\n###### **c.在“Main Activity”中添加一个“[Add Note”FloatingActionButton](https://developer.android.com/reference/com/google/android/material/floatingactionbutton/FloatingActionButton)。** \n在 res/layout 下,打开 activity_main.xml 并将以下内容添加到现有“Floating Action Button”上方。\nJava\n```\n<com.google.android.material.floatingactionbutton.FloatingActionButton\n android:id=\"@+id/fabAdd\"\n android:layout_width=\"wrap_content\"\n android:layout_height=\"wrap_content\"\n android:layout_alignParentRight=\"true\"\n android:layout_gravity=\"bottom|end\"\n android:layout_margin=\"@dimen/fab_margin\"\n android:visibility=\"invisible\"\n android:src=\"@drawable/ic_baseline_post_add\"\n app:fabCustomSize=\"60dp\"\n app:fabSize=\"auto\"/>\n```\n在 res/drawable 中添加一个“Add Note”图标。右键单击“drawable”,选择 **New**,然后选择 **Vector Asset**。输入 **ic_baseline_add** 作为名称,并从“Clip Art”中选择添加图标。单击 **Next**,然后单击 **Finish**。\n\n![image.png](https://dev-media.amazoncloud.cn/50c96c12d9c749eaa89a2dedcb2adb36_image.png)\n\n###### **d.添加代码以处理“Add Note”按钮。**\n要拥有功能完全的“Add Button”,还需要完成的最后两项工作是让按钮根据 isSignedIn 值显示或消失,显然,需要添加代码来处理按钮点击操作。\n打开 mainActivity.kt,并将以下内容添加到 onCreate() 方法的末尾:\nJava\n```\n// register a click listener\nfabAdd.setOnClickListener {\n startActivity(Intent(this, AddNoteActivity::class.java))\n}\n然后,仍然在 onCreate() 方法中,将 UserData.isSignedIn.observe 替换为以下内容:\nUserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp ->\n // update UI\n Log.i(TAG, \"isSignedIn changed : $isSignedUp\")\n\n //animation inspired by https://www.11zon.com/zon/android/multiple-floating-action-button-android.php\n if (isSignedUp) {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock_open)\n Log.d(TAG, \"Showing fabADD\")\n fabAdd.show()\n fabAdd.animate().translationY(0.0F - 1.1F * fabAuth.customSize)\n } else {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock)\n Log.d(TAG, \"Hiding fabADD\")\n fabAdd.hide()\n fabAdd.animate().translationY(0.0F)\n }\n}) \n```\n要验证一切是否都按预期运行,请构建项目。单击 **Build** 菜单,并选择 **Make Project**,或者,在 Mac 上按 **⌘F9**。应该不会出现错误。\n运行应用程序时,您将看到“Add Note”按钮会在用户登录时显示,在用户注销时消失。您现在可以添加备注了。\n##### **4.2.9添加“滑动删除”行为**\n可以通过在 Note 列表中添加触摸处理程序来添加“滑动删除”行为。触摸处理程序负责绘制红色背景、删除图标,并在释放触摸时调用 Backend.delete() 方法。\n###### **a.创建一个新类 SimpleTouchCallback。**\n在 java/com 下,右键单击 example.androidgettingstarted,依次选择 New、Kotlin File,然后输入 SwipeCallback 作为名称。\n\n![image.png](https://dev-media.amazoncloud.cn/5d8a6018e08143b790f21ceb94e3cb8e_image.png)\n\n将以下代码粘贴到新文件中:\nJava\n```\npackage com.example.androidgettingstarted\n\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.drawable.ColorDrawable\nimport android.graphics.drawable.Drawable\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\n\n\n// https://stackoverflow.com/questions/33985719/android-swipe-to-delete-recyclerview\nclass SwipeCallback(private val activity: AppCompatActivity): ItemTouchHelper.SimpleCallback(\n 0,\n ItemTouchHelper.LEFT\n) {\n\n private val TAG: String = \"SimpleItemTouchCallback\"\n private val icon: Drawable? = ContextCompat.getDrawable(\n activity,\n R.drawable.ic_baseline_delete_sweep\n )\n private val background: ColorDrawable = ColorDrawable(Color.RED)\n\n override fun onChildDraw(\n c: Canvas,\n recyclerView: RecyclerView,\n viewHolder: RecyclerView.ViewHolder,\n dX: Float,\n dY: Float,\n actionState: Int,\n isCurrentlyActive: Boolean\n ) {\n super.onChildDraw(\n c,\n recyclerView,\n viewHolder,\n dX,\n dY,\n actionState,\n isCurrentlyActive\n )\n val itemView = viewHolder.itemView\n val backgroundCornerOffset = 20\n val iconMargin = (itemView.height - icon!!.intrinsicHeight) / 2\n val iconTop = itemView.top + (itemView.height - icon.intrinsicHeight) / 2\n val iconBottom = iconTop + icon.intrinsicHeight\n val iconRight: Int = itemView.right - iconMargin\n if (dX < 0) {\n val iconLeft: Int = itemView.right - iconMargin - icon.intrinsicWidth\n icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)\n background.setBounds(\n itemView.right + dX.toInt() - backgroundCornerOffset,\n itemView.top, itemView.right, itemView.bottom\n )\n background.draw(c)\n icon.draw(c)\n } else {\n background.setBounds(0, 0, 0, 0)\n background.draw(c)\n }\n }\n\n override fun onMove(\n recyclerView: RecyclerView,\n viewHolder: RecyclerView.ViewHolder,\n target: RecyclerView.ViewHolder\n ): Boolean {\n Toast.makeText(activity, \"Moved\", Toast.LENGTH_SHORT).show()\n return false\n }\n\n override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {\n\n Toast.makeText(activity, \"deleted\", Toast.LENGTH_SHORT).show()\n\n //Remove swiped item from list and notify the RecyclerView\n Log.d(TAG, \"Going to remove ${viewHolder.adapterPosition}\")\n\n // get the position of the swiped item in the list\n val position = viewHolder.adapterPosition\n\n // remove to note from the userdata will refresh the UI\n val note = UserData.deleteNote(position)\n\n // async remove from backend\n Backend.deleteNote(note)\n }\n}\n```\n重要的代码行位于 onSwiped() 方法中。当滑动手势完成时,将调用此方法。我们将收集列表中滑动项的位置,并会将相应备注从 UserData 结构(这将更新用户界面)和云后端中删除。\n###### **b.现在,我们已经有了类,让我们在 res/drawable 中添加“删除”图标。**\n右键单击“drawable”,选择 **New**,然后选择 **Vector Asset**。输入 **ic_baseline_delete_sweep** 作为名称,并从“Clip Art”中选择“删除滑动”图标。单击 **Next**,然后单击 **Finish**。\n\n![image.png](https://dev-media.amazoncloud.cn/4faf844970ba4d76ab20066f2972963a_image.png)\n\n###### **c.将以下代码粘贴到新文件中:为 RecyclerView 添加“滑动删除”手势处理程序。**\n在 java/com/example.androidgettingstarted 下,打开 MainActivity.kt,并将下面两行代码添加到 setupRecyclerView 中:\nJava\n```\n// add a touch gesture handler to manager the swipe to delete gesture\nval itemTouchHelper = ItemTouchHelper(SwipeCallback(this))\nitemTouchHelper.attachToRecyclerView(recyclerView)\n```\n##### **4.2.10构建和测试**\n要验证一切是否都按预期运行,请构建并**运行**项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。\n假设您仍处于登录状态,应用程序会在一个空列表上启动。它现在具有一个用于添加 Note 的“Add Note”按钮。**单击“Add Note”符号、输入标题和描述、单击“Add Note”按钮**,随后备注应显示在列表中。\n您可以通过向左滑动一行来删除 Note。\n\n### **5.添加图像存储功能**\n在本单元中,您将添加存储以及将图像与您的应用程序中的备注关联的功能。\n\n![image.png](https://dev-media.amazoncloud.cn/0064920b16b2417b83366906550bc701_image.png)\n\n#### **5.1简介**\n现在,我们的备注应用程序已在运行,接下来添加将图像与每个备注关联的功能。在本单元中,您将使用 Amplify CLI 和库来创建利用 [Amazon S3](https://aws.amazon.com/cn/s3/) 的存储服务。最后,您将更新 Android 应用程序以启用图像上传、获取和渲染。\n\n#### **5.2实施**\n##### **5.2.1创建存储服务**\n要添加图像存储功能,我们将使用 Amplify 存储类别:\namplify add storage\n一段时间后,您将看到:\nJava\n```\nSuccessfully added resource image locally\n```\n##### **5.2.2部署存储服务**\n要部署我们刚刚创建的存储服务,请转至您的终端,然后执行以下命令:\namplify push\n按 **Y** 确认,一段时间后,您将看到:\nJava\n```\n✔ Successfully pulled backend environment amplify from the cloud.\n```\n##### **5.2.3向 Android Studio 项目添加 Amplify 存储库**\n在转到此代码之前,请先返回 Android Studio,将以下依赖关系连同您之前添加的其他 \n```amplifyframework``` 实现一起添加到您的单元的 \n```build.gradle```,然后在看到提示时单击 **Sync Now**:\nJava\n```\ndependencies {\n ...\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n implementation 'com.amplifyframework:aws-api:1.4.0'\n implementation 'com.amplifyframework:aws-storage-s3:1.4.0'\n}\n```\n##### **5.2.4在运行时初始化 Amplify 存储插件**\n返回 Android Studio,在 java/example.androidgettingstarted 下,打开 Backend.kit,并在 initialize() 方法中的 Amplify 初始化序列中添加一行。完整代码块应如下所示:\nJava\n```\ntry {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.addPlugin(AWSApiPlugin())\n Amplify.addPlugin(AWSS3StoragePlugin())\n\n Amplify.configure(applicationContext)\n\n Log.i(TAG, \"Initialized Amplify\")\n} catch (e: AmplifyException) {\n Log.e(TAG, \"Could not initialize Amplify\", e)\n}\n```\n##### **5.2.5将 Image CRUD 方法添加到后端类**\n依然在 Backend.kt 中。在后端类的任何位置,添加以下三个方法,用于从存储中上传、下载和删除图像:\nJava\n```\nfun storeImage(filePath: String, key: String) {\n val file = File(filePath)\n val options = StorageUploadFileOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n Amplify.Storage.uploadFile(\n key,\n file,\n options,\n { progress -> Log.i(TAG, \"Fraction completed: ${progress.fractionCompleted}\") },\n { result -> Log.i(TAG, \"Successfully uploaded: \" + result.key) },\n { error -> Log.e(TAG, \"Upload failed\", error) }\n )\n}\n\nfun deleteImage(key : String) {\n\n val options = StorageRemoveOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n Amplify.Storage.remove(\n key,\n options,\n { result -> Log.i(TAG, \"Successfully removed: \" + result.key) },\n { error -> Log.e(TAG, \"Remove failure\", error) }\n )\n}\n\nfun retrieveImage(key: String, completed : (image: Bitmap) -> Unit) {\n val options = StorageDownloadFileOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n val file = File.createTempFile(\"image\", \".image\")\n\n Amplify.Storage.downloadFile(\n key,\n file,\n options,\n { progress -> Log.i(TAG, \"Fraction completed: ${progress.fractionCompleted}\") },\n { result ->\n Log.i(TAG, \"Successfully downloaded: ${result.file.name}\")\n val imageStream = FileInputStream(file)\n val image = BitmapFactory.decodeStream(imageStream)\n completed(image)\n },\n { error -> Log.e(TAG, \"Download Failure\", error) }\n )\n}\n```\n这三种方法仅调用其 Amplify 对应项。Amplify 存储有三个文件保护级别:\n- **公有**:所有用户均可访问\n- **受保护**:所有用户均可读取,但只有创建用户可写入\n- **私有**:只有创建用户可读可写\n对于此应用程序,我们希望仅备注拥有者可使用图像,我们将使用 StorageAccessLevel.PRIVATE 属性。\n##### **5.2.6添加 UI 代码以捕获图像**\n下一步是修改 UI,以允许用户在单击 AddNoteACtivity 上的“Add image”按钮时从手机库中选择图像。\n必须执行两个更改:更改“Add Note”活动布局以添加“Add image”按钮和图像视图,以及在活动类中添加处理程序代码。\n在 Android Studio 中的“res/layout”下,打开 activity_add_note.xml 文件,然后**将以下 Button 元素添加到** addNote 按钮的正上方:\nJava\n```\n<!-- after the description EditText -->\n<com.google.android.material.imageview.ShapeableImageView\n android:id=\"@+id/image\"\n android:layout_width=\"match_parent\"\n android:layout_height=\"280dp\"\n android:layout_margin=\"16dp\"\n android:scaleType=\"centerCrop\" />\n\n<!-- after the Space -->\n<Button\n android:id=\"@+id/captureImage\"\n style=\"?android:attr/buttonStyleSmall\"\n android:layout_width=\"fill_parent\"\n android:layout_height=\"wrap_content\"\n android:layout_gravity=\"center_horizontal\"\n android:backgroundTint=\"#009688\"\n android:text=\"Add image\" />\n```\n在 Android Studio 中的 java/example.androidgettingstarted 下,打开 AddNoteACtivity.kt 文件,然后在 onCreate() 方法中**添加以下代码**:\nJava\n```\n// inside onCreate() \n// Set up the listener for add Image button\ncaptureImage.setOnClickListener {\n val i = Intent(\n Intent.ACTION_GET_CONTENT,\n MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n )\n startActivityForResult(i, SELECT_PHOTO)\n}\n\n// create rounded corners for the image\nimage.shapeAppearanceModel = image.shapeAppearanceModel\n .toBuilder()\n .setAllCorners(CornerFamily.ROUNDED, 150.0f)\n .build()\n```\n在 Intent、MediaStore 和 CornerFamily 上添加所需导入。\n同时在伴生对象中添加以下常量值:\n// add this to the companion object \nprivate const val SELECT_PHOTO = 100\n最后,添加收到的代码并将所选图像存储到临时文件。\n将以下代码添加到 AddNoteACtivity 类的任何位置:\nJava\n```\n//anywhere in the AddNoteActivity class\n\nprivate var noteImagePath : String? = null\nprivate var noteImage : Bitmap? = null\n\noverride fun onActivityResult(requestCode: Int, resultCode: Int, imageReturnedIntent: Intent?) {\n super.onActivityResult(requestCode, resultCode, imageReturnedIntent)\n Log.d(TAG, \"Select photo activity result : $imageReturnedIntent\")\n when (requestCode) {\n SELECT_PHOTO -> if (resultCode == RESULT_OK) {\n val selectedImageUri : Uri? = imageReturnedIntent!!.data\n\n // read the stream to fill in the preview\n var imageStream: InputStream? = contentResolver.openInputStream(selectedImageUri!!)\n val selectedImage = BitmapFactory.decodeStream(imageStream)\n val ivPreview: ImageView = findViewById<View>(R.id.image) as ImageView\n ivPreview.setImageBitmap(selectedImage)\n\n // store the image to not recreate the Bitmap every time\n this.noteImage = selectedImage\n\n // read the stream to store to a file\n imageStream = contentResolver.openInputStream(selectedImageUri)\n val tempFile = File.createTempFile(\"image\", \".image\")\n copyStreamToFile(imageStream!!, tempFile)\n\n // store the path to create a note\n this.noteImagePath = tempFile.absolutePath\n\n Log.d(TAG, \"Selected image : ${tempFile.absolutePath}\")\n }\n }\n}\n\nprivate fun copyStreamToFile(inputStream: InputStream, outputFile: File) {\n inputStream.use { input ->\n val outputStream = FileOutputStream(outputFile)\n outputStream.use { output ->\n val buffer = ByteArray(4 * 1024) // buffer size\n while (true) {\n val byteCount = input.read(buffer)\n if (byteCount < 0) break\n output.write(buffer, 0, byteCount)\n }\n output.flush()\n output.close()\n }\n }\n}\n```\n以上代码将所选图像作为 InputStream 使用两次。第一次,InputStream 会创建一个位图图像以显示在用户界面中,第二次,InputStream 会保存一个临时文件以发送到后端。\n本单元将浏览一个临时文件,因为 Amplify API 使用 Fileobjects。尽管不是最高效的设计,但此代码十分简单。\n要验证一切是否都按预期运行,请构建项目。单击 **Build **菜单,并选择 **Make Project**,或者,在 Mac 上按 **⌘F9**。应该不会出现错误。\n###### **5.2.7创建备注时存储图像**\n创建备注时,我们从后端调用存储方法。打开 AddNoteActivity.kt 并修改 addNote.setOnClickListener() 方法,以便在创建备注对象后添加以下代码。\nJava\n```\n// add this in AddNoteACtivity.kt, inside the addNote.setOnClickListener() method and after the Note() object is created.\nif (this.noteImagePath != null) {\n note.imageName = UUID.randomUUID().toString()\n //note.setImage(this.noteImage)\n note.image = this.noteImage\n\n // asynchronously store the image (and assume it will work)\n Backend.storeImage(this.noteImagePath!!, note.imageName!!)\n}\n```\n###### **5.2.8加载备注时加载图像**\n要加载图像,我们需要修改备注数据类上的静态方法。这样,每当 API 返回的 NoteData 对象转换为 Note 对象时,将同时加载图像。加载图像时,我们会通知 LiveData 的 UserData,以便观察者知晓进行的更改。这将触发用户界面刷新。\n打开 UserData.kt,然后**修改**备注数据类的伴生对象,如下所示:\nJava\n```\n// static function to create a Note from a NoteData API object\ncompanion object {\n fun from(noteData : NoteData) : Note {\n val result = Note(noteData.id, noteData.name, noteData.description, noteData.image)\n \n if (noteData.image != null) {\n Backend.retrieveImage(noteData.image!!) {\n result.image = it\n\n // force a UI update\n with(UserData) { notifyObserver() }\n }\n }\n return result\n }\n}\n```\n###### **5.2.9删除备注时删除图像**\n最后一步是自己清理,即,当用户删除备注时,从云存储中删除图像。如果清理不是为了节省存储空间,可以出于节省 AWS 费用的目的进行清理,因为 Amazon S3 对存储的数据按 Gb/月收费(前 5Gb 免费,运行本教程不收费)。\n打开 SwipeCallback.kt,然后在 onSwipe() 方法末尾添加以下代码:\nif (note?.imageName != null) {\n //asynchronously delete the image (and assume it will work)\n Backend.deleteImage(note.imageName!!)\n}\n###### **5.2.10构建和测试**\n要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的**运行**图标 ▶️,或按 ^ **R**。应该不会出现错误。\n假设您仍在登录中,应用程序将从未从上一部分删除的备注列表开始。再次使用“Add Note”按钮创建备注。这一次,添加从本地图像存储中选择的图片。\n退出应用程序并重新启动,以验证图像是否正确加载。\n### **总结**\n到这里,你已经使用 Amazon Amplify 构建了 Android 应用程序! 并且在应用程序中添加了身份验证,使用户可以注册、登录和管理其账户。该应用程序还使用 Amazon DynamoDB 数据库配置了一个可扩展的 GraphQL API,使用户可以创建和删除备注。您还使用 Amazon S3 添加了文件存储,从而使用户可以上传图像并在其应用程序中查看它们。\n\n![image.png](https://dev-media.amazoncloud.cn/b34291378370439bb9c9ff5a52855d32_image.png)\n\n通过本文相信你对从零使用 Amazon Amplify 创建简单的 Android 应用程序有了一个完整的认识,并且完成了入门。\n### **最后想说的是**\n最后想要说的是,一门新技术,新框架,新的解决方案来临的时候努力去学习它,并从中发现机会。本次使用该技术也算是一个新的体验,总体来说,优势很明显,开发速度很快,只要你具有一定的Android基础,那么你就可以很快的利用该产品制作属于自己的一个应用程序。总之,不断的把自己的舒适圈扩大,扩大,再扩大,主动学习和挑战新的东西 。\n最后,很感谢能够阅读到这里的读者。如果看完觉得好的话,还请轻轻点一下赞或者分享给更多的人,你们的鼓励就是作者继续行文的动力。\n亚马逊云科技专为开发者们打造了多种学习平台:\n1. 入门资源中心:从0到1 轻松上手云服务,内容涵盖:成本管理,上手训练,开发资源。[https://aws.amazon.com/cn/getting-started/?nc1=h_ls&trk=32540c74-46f0-46dc-940d-621a1efeedd0&sc_channel=el](https://aws.amazon.com/cn/getting-started/?nc1=h_ls&trk=32540c74-46f0-46dc-940d-621a1efeedd0&sc_channel=el)\n2. 架构中心:亚马逊云科技架构中心提供了云平台参考架构图表、经过审查的架构解决方案、Well-Architected 最佳实践、模式、图标等。[https://aws.amazon.com/cn/architecture/?intClick=dev-center-2021_main&trk=3fa608de-d954-4355-a20a-324daa58bbeb&sc_channel=el](https://aws.amazon.com/cn/architecture/?intClick=dev-center-2021_main&trk=3fa608de-d954-4355-a20a-324daa58bbeb&sc_channel=e)\n3. 构建者库:了解亚马逊云科技如何构建和运营软件。[https://aws.amazon.com/cn/builders-library/?cards-body.sort-by=item.additionalFields.sortDate&cards-body.sort-order=desc&awsf.filter-content-category=*all&awsf.filter-content-type=*all&awsf.filter-content-level=*all&trk=835e6894-d909-4691-aee1-3831428c04bd&sc_channel=el](https://aws.amazon.com/cn/builders-library/?cards-body.sort-by=item.additionalFields.sortDate&cards-body.sort-order=desc&awsf.filter-content-category=*all&awsf.filter-content-type=*all&awsf.filter-content-level=*all&trk=835e6894-d909-4691-aee1-3831428c04bd&sc_channel=el)\n4. 用于在亚马逊云科技平台上开发和管理应用程序的工具包:[https://aws.amazon.com/cn/tools/?intClick=dev-center-2021_main&trk=972c69e1-55ec-43af-a503-d458708bb645&sc_channel=el](https://aws.amazon.com/cn/tools/?intClick=dev-center-2021_main&trk=972c69e1-55ec-43af-a503-d458708bb645&sc_channel=el)\n最后也给大家带来了专属福利\n### **【专属福利】**\n 福利一:100余种产品免费套餐。其中,计算资源 Amazon EC2首年12个月免费,750小时/月;存储资源 Amazon S3 首年12个月免费,5GB标准存储容量。\n[https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el](https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el)","render":"<h3><a id=\"_0\"></a><strong>背景:</strong></h3>\n<p>亚马逊云科技提供了100余种产品免费套餐。其中,计算资源Amazon EC2首年12个月免费,750小时/月;存储资源 Amazon S3 首年12个月免费,5GB标准存储容量。<br />\n<a href=\"https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el\" target=\"_blank\">https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el</a><br />\n大家好,我是坚果,由于最近一直疫情居家,所以想到外边转转,但是确实出不去,那么作为程序员的我们肯定也不能闲着,于是想做一个很简单的旅行应用,具有基础的增删改查的功能。以及身份验证!这个时候考虑到 Amazon 具有高响应,高可用等特点,这个时候你只要 Amazon 结合一定的 Android 基础,就可以很方便快捷的拥有自己的应用程序。而且由于 Amazon 具有全球优势的特点,以及 Amazon Amplify 是一组位于云端的工具和无服务器服务都让他拥有了一定的优势,这样一来技术选型接确定了,那么说了这么多,如何结合 Amazon 很快的创建属于自己的 Android 应用程序呢?只需要简单的五个步骤就可以。接下来开始正文<br />\n开始之前看一下具体你要具备哪些条件:</p>\n<h3><a id=\"_5\"></a><strong>先决条件</strong></h3>\n<ul>\n<li><a href=\"https://developer.android.com/studio\" target=\"_blank\">Android Studio 4.x</a> 或更高版本</li>\n<li>一个至少具有<a href=\"https://github.com/sebsto/amplify-ios-getting-started/blob/master/amplify-policy.json\" target=\"_blank\">这些权限</a>的 <a href=\"https://aws.amazon.com/cn/getting-started/hands-on/build-android-app-amplify/?trk=6c2fcc87-4e1c-4db1-bedd-836152ec5ac8&sc_channel=el\" target=\"_blank\">Amazon</a> 账户</li>\n<li><a href=\"https://nodejs.org/en/\" target=\"_blank\">Node.js</a> 10 或更高版本</li>\n<li>Amazon 命令行界面 <a href=\"https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html\" target=\"_blank\">Amazon CLI 2.0.x</a> 或更高版本。<br />\n检查 noidejs 版本,node -v,发现版本是16.13.0,符合条件<br />\nandroid studio 版本查看方式<br />\n点击 help-about,如图所示:版本符合要求</li>\n</ul>\n<p><img src=\"https://dev-media.amazoncloud.cn/a47f4ae90660444fba425493fd56337b_image.png\" alt=\"image.png\" /></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/09dbecd7ea1d4e598f17798315f155ee_image.png\" alt=\"image.png\" /></p>\n<p>接下来构建您的首个 Android 应用程序。</p>\n<h3><a id=\"_19\"></a><strong>概览</strong></h3>\n<p>在本文中,您将使用 Amazon Amplify 创建一个简单的 Android 应用程序,Amazon Amplify 是一组位于云端的工具和无服务器服务。在第一个单元中,您将构建一个简单的 Android 应用程序。在其余单元中,您将使用 Amplify 命令行接口 (Amplify CLI) 初始化一个本地应用程序、添加用户身份验证、添加一个 GraphQL API 和一个数据库以存储您的数据,并更新您的应用程序以存储图像。<br />\n本文将引导您完成创建上面讨论的简单 Android 应用程序。<br />\n本文分为五个简短单元。您必须按顺序完成每个单元才能进入下一单元。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/ee205bcc78724130a798c70e10f1fee6_image.png\" alt=\"image.png\" /></p>\n<p>使用 Amazon Amplify 创建简单的 Android 应用qq<br />\n1.构建 Android 应用程序<br />\n在 Android 模拟器中创建 Android 应用程序并测试<br />\n2.初始化本地应用程序<br />\n使用 Amazon Amplify 初始化本地应用程序<br />\n3.添加身份验证<br />\n添加身份验证到您的应用程序中<br />\n4.添加API和数据库<br />\n创建 GraphQL API<br />\n5.添加存储<br />\n添加存储到您的应用程序中</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/2484d943c9c146f89de1a07cc587cb95_image.png\" alt=\"image.png\" /></p>\n<p>您将使用终端和 Google 的 <a href=\"https://developer.android.com/studio\" target=\"_blank\">Android Studio</a> IDE 来构建此 Android 应用程序。</p>\n<h3><a id=\"1_Android__41\"></a><strong>1.创建和部署 Android 应用程序</strong></h3>\n<p>在这,您将创建 Android 应用程序并使用 Amazon Amplify 的 Web 托管服务将其部署到云</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/8c442d8140d44705a0ff7f51cb67e8f4_image.png\" alt=\"image.png\" /></p>\n<h4><a id=\"11_46\"></a><strong>1.1简介</strong></h4>\n<p>Amazon Amplify 提供基于 Git 的工作流,用于创建、管理、集成和部署 Web 和移动应用程序的无服务器后端。Amplify CLI 提供了一个简单的基于文本的用户界面,用于预置和管理后端服务,如用于您的应用程序的用户身份验证或 REST 或 GraphQL API。使用 Amplify 库可以轻松地将这些后端服务与应用程序中的几行代码集成。</p>\n<h4><a id=\"12_48\"></a><strong>1.2实施</strong></h4>\n<h5><a id=\"121_Android__49\"></a><strong>1.2.1.创建 Android 项目</strong></h5>\n<p>启动 Android Studio,然后选择 New Project</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/3092c1b798444ae28d5ac860d2966403_image.png\" alt=\"image.png\" /></p>\n<p>在 <strong>Phone and Tablet</strong> 下,选择 <strong>Basic Activity</strong>,然后单击 <strong>Next</strong>:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/3794940a560d4ad5a96868f197cbdf6b_image.png\" alt=\"image.png\" /></p>\n<p>为您的项目键入名称,例如 <strong>Android Amazon Started</strong>。确保语言为 <strong>Kotlin</strong>,开发工具包最低为 <strong>API 26: Android 8.0 (oreo)</strong>,然后单击 <strong>Finish</strong>:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/2bca974217564e55a0d401a7308204de_image.png\" alt=\"image.png\" /></p>\n<p>现在,项目的框架已经存在,我们需要完成 4 个步骤来让我们的基本应用程序运行:</p>\n<ol>\n<li>删除不需要的类和文件,并添加插件</li>\n<li>创建类以保留数据结构</li>\n<li>创建视图以在列表中保留单个备注</li>\n<li>修改 MainActivity 以显示备注列表</li>\n</ol>\n<h5><a id=\"122_68\"></a><strong>1.2.2.删除不需要的类和文件,并添加插件</strong></h5>\n<p>在“res/layout”和 java/com.example.androidgettingstarted 下,删除四个<strong>片段</strong>文件(选择这 4 个文件,右键单击,然后从上下文菜单中选择 <strong>Delete</strong> ):</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/e26a3abb225445d3a37394de4ddb0604_image.png\" alt=\"image.png\" /></p>\n<p>在 <strong>Gradle Scripts</strong>下,打开 <strong>build.gradle (Module:app)</strong>,并添加 Kotlin 扩展插件。<br />\nKotlin</p>\n<pre><code class=\"lang-\">plugins {\n id 'com.android.application'\n id 'kotlin-android'\n id 'kotlin-android-extensions' // <== add this line\n}\n</code></pre>\n<p><img src=\"https://dev-media.amazoncloud.cn/86c68dd2c20d46ec95ef825b6b45bd4e_image.png\" alt=\"image.png\" /></p>\n<h5><a id=\"123_84\"></a><strong>1.2.3.创建类以保留数据结构</strong></h5>\n<p>UserData 类可保留用户状态:一个 isSignedIn 标记和一个备注值列表。<br />\n要创建新类,请右键单击 java/com.example/androidgettingstarted,然后选择 <strong>New -> Kotlin file/class</strong>。键入 <strong>UserData</strong> 作为名称。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/5c748cf53d864bfdadb004699c99e3de_image.png\" alt=\"image.png\" /></p>\n<p>将以下代码粘贴到新创建的文件 (UserData.kt) 中<br />\nKotlin</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.graphics.Bitmap\nimport android.util.Log\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveData\n\n// a singleton to hold user data (this is a ViewModel pattern, without inheriting from ViewModel)\nobject UserData {\n\n private const val TAG = "UserData"\n\n //\n // observable properties\n //\n\n // signed in status\n private val _isSignedIn = MutableLiveData<Boolean>(false)\n var isSignedIn: LiveData<Boolean> = _isSignedIn\n\n fun setSignedIn(newValue : Boolean) {\n // use postvalue() to make the assignation on the main (UI) thread\n _isSignedIn.postValue(newValue)\n }\n\n // the notes\n private val _notes = MutableLiveData<MutableList<Note>>(mutableListOf())\n\n // please check https://stackoverflow.com/questions/47941537/notify-observer-when-item-is-added-to-list-of-livedata\n private fun <T> MutableLiveData<T>.notifyObserver() {\n this.postValue(this.value)\n }\n fun notifyObserver() {\n this._notes.notifyObserver()\n }\n\n fun notes() : LiveData<MutableList<Note>> = _notes\n fun addNote(n : Note) {\n val notes = _notes.value\n if (notes != null) {\n notes.add(n)\n _notes.notifyObserver()\n } else {\n Log.e(TAG, "addNote : note collection is null !!")\n }\n }\n fun deleteNote(at: Int) : Note? {\n val note = _notes.value?.removeAt(at)\n _notes.notifyObserver()\n return note\n }\n\n fun resetNotes() {\n this._notes.value?.clear() //used when signing out\n _notes.notifyObserver()\n }\n\n\n // a note data class\n data class Note(val id: String, val name: String, val description: String, var imageName: String? = null) {\n override fun toString(): String = name\n\n // bitmap image\n var image : Bitmap? = null\n\n }\n}\n</code></pre>\n<p><strong>我们刚刚添加了哪些内容?</strong></p>\n<ul>\n<li>UserData 类负责保留用户数据,即一个 isSignedIn 标记用于跟踪当前的身份验证状态和备注对象列表。</li>\n<li>这两个属性是根据 LiveData 发布/订阅框架来实现的。它允许图形用户界面 (GUI) 订阅更改并做出相应反应。</li>\n<li>我们还添加了一个备注数据类,仅用于保留单个备注的数据。针对 ImageName 和 Image 使用了两个不同的属性。我们将在后续单元中介绍 Image。</li>\n<li>为 UserData 对象实施了单态设计模式,因为此模式允许只使用 UserData 即可从应用程序的任何位置引用此对象。</li>\n</ul>\n<h5><a id=\"124GUI_166\"></a><strong>1.2.4.为列表中的但各单元格添加GUI</strong></h5>\n<p>滚动列表中的单个单元格称为 RecyclerView,因为当用户上下滚动屏幕,屏幕上再也看不到视图时,可以回收视图。<br />\n与常规视图一样,我们也创建了布局 XML 文件和 Kotlin 类。单个单元格类似以下内容:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/4e951e9df2354c448190f35b1c682eb4_image.png\" alt=\"image.png\" /></p>\n<p>要创建布局文件,请右键单击“res/layout”,然后选择 <strong>New -> Layout Resource File</strong>。键入 content_note 作为名称,并保留所有其他值,因为我们将直接编辑 XML 文件</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/8a746fdec5bc41328a04d19c26a4d081_image.png\" alt=\"image.png\" /></p>\n<p>打开新创建的文件的 <strong>Code</strong> 视图。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/f7f0f58455cb4df2b74832687c562ede_image.png\" alt=\"image.png\" /></p>\n<p>在新创建的文件 (content_note.xml) 的“Code”视图中,通过粘贴以下代码来替代所生成的代码:<br />\nKotlin</p>\n<pre><code class=\"lang-\"><?xml version="1.0" encoding="utf-8"?>\n<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"\n android:layout_width="match_parent"\n android:layout_height="wrap_content"\n android:orientation="horizontal"\n android:paddingVertical="16dp">\n\n <ImageView\n android:id="@+id/image"\n android:layout_width="100dp"\n android:layout_height="match_parent"\n android:padding="8dp"\n android:scaleType="centerCrop"\n android:src="@drawable/ic_launcher_background" />\n\n <LinearLayout\n android:layout_width="match_parent"\n android:layout_height="wrap_content"\n android:layout_gravity="center"\n android:layout_marginLeft="5dp"\n android:orientation="vertical">\n\n <TextView\n android:id="@+id/name"\n android:layout_width="match_parent"\n android:layout_height="wrap_content"\n android:text="Title"\n android:textSize="20sp"\n android:textStyle="bold" />\n\n <TextView\n android:id="@+id/description"\n android:layout_width="match_parent"\n android:layout_height="wrap_content"\n android:text="Title"\n android:textSize="15sp" />\n\n </LinearLayout>\n</LinearLayout>\n</code></pre>\n<p>最后,创建 Kotlin 类:右键单击 java/com.example/androidgettingstarted,然后选择 <strong>New -> Kotlin file/class</strong>。键入 <strong>NoteRecyclerViewAdapter</strong> 作为名称。<br />\n将以下代码粘贴到新创建的文件 (NoteRecyclerViewAdapter.kt) 中<br />\nKotlin</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\n\n// this is a single cell (row) in the list of Notes\nclass NoteRecyclerViewAdapter(\n private val values: MutableList<UserData.Note>?) :\n RecyclerView.Adapter<NoteRecyclerViewAdapter.ViewHolder>() {\n\n override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n val view = LayoutInflater.from(parent.context)\n .inflate(R.layout.content_note, parent, false)\n return ViewHolder(view)\n }\n\n override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n\n val item = values?.get(position)\n holder.nameView.text = item?.name\n holder.descriptionView.text = item?.description\n\n if (item?.image != null) {\n holder.imageView.setImageBitmap(item.image)\n }\n }\n\n override fun getItemCount() = values?.size ?: 0\n\n inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {\n val imageView: ImageView = view.findViewById(R.id.image)\n val nameView: TextView = view.findViewById(R.id.name)\n val descriptionView: TextView = view.findViewById(R.id.description)\n }\n}\n</code></pre>\n<p><strong>我们刚刚添加了哪些内容?</strong><br />\n上述代码包含</p>\n<ul>\n<li>一个布局 xml 文件,该文件说明表示备注的单元格上的组件布局。它显示图像、备注标题和备注说明。</li>\n<li>一个支持 Kotlin 类。它在创建时接收备注数据对象,并将单个值分配给其相对应的视图(图像、标题和说明)</li>\n</ul>\n<h5><a id=\"125_271\"></a><strong>1.2.5.更新主要活动</strong></h5>\n<p>现在,我们已经有了数据类(UserData 和备注)和单个备注的视图 (NoteRecyclerViewAdapter),让我们在主要活动上创建备注列表。<br />\n从 Android Studio 左侧的文件列表,打开 res/layout/content_main.xml,并将代码替换为以下内容:<br />\nXML</p>\n<pre><code class=\"lang-\"><?xml version="1.0" encoding="utf-8"?>\n<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"\n xmlns:app="http://schemas.android.com/apk/res-auto"\n xmlns:tools="http://schemas.android.com/tools"\n android:id="@+id/frameLayout"\n android:layout_width="match_parent"\n android:layout_height="match_parent"\n >\n\n <androidx.recyclerview.widget.RecyclerView\n android:id="@+id/item_list"\n android:name="com.example.myapplication.ItemListFragment"\n android:layout_width="match_parent"\n android:layout_height="match_parent"\n android:layout_marginTop="60dp"\n\n android:paddingHorizontal="8dp"\n android:paddingVertical="8dp"\n app:layoutManager="LinearLayoutManager"\n tools:context=".MainActivity"\n tools:listitem="@layout/content_note" />\n\n</FrameLayout>\n</code></pre>\n<p>从 Android Studio 左侧的文件列表,打开 java/com.example/androidgettingstarted/MainActivity.kt,并将代码替换为以下内容:<br />\nXML</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.Observer\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\nimport kotlinx.android.synthetic.main.activity_main.*\nimport kotlinx.android.synthetic.main.content_main.*\n\nclass MainActivity : AppCompatActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_main)\n setSupportActionBar(toolbar)\n\n // prepare our List view and RecyclerView (cells)\n setupRecyclerView(item_list)\n }\n\n // recycler view is the list of cells\n private fun setupRecyclerView(recyclerView: RecyclerView) {\n\n // update individual cell when the Note data are modified\n UserData.notes().observe(this, Observer<MutableList<UserData.Note>> { notes ->\n Log.d(TAG, "Note observer received ${notes.size} notes")\n\n // let's create a RecyclerViewAdapter that manages the individual cells\n recyclerView.adapter = NoteRecyclerViewAdapter(notes)\n })\n }\n\n companion object {\n private const val TAG = "MainActivity"\n }\n}\n</code></pre>\n<p><strong>我们刚刚添加了哪些内容?</strong></p>\n<ul>\n<li>主要布局是一个 RecyclerView,用于管理我们之前创建的单个单元格列表</li>\n<li>主要活动类可观察备注列表的变化,并创建一个 NoteRecyclerViewAdapter 以创建单个单元格。</li>\n</ul>\n<h5><a id=\"126_345\"></a><strong>1.2.6.验证生成依赖项</strong></h5>\n<ul>\n<li>在 Gradle Scripts 下,打开 build.gradle (Module:app),并验证所生成的依赖关系是否正确。需要选中<br />\nlibraries versions。<br />\nKotlin</li>\n</ul>\n<pre><code class=\"lang-\">gradle\ndependencies {\n implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"\n implementation 'androidx.core:core-ktx:1.3.2'\n implementation 'androidx.appcompat:appcompat:1.2.0'\n implementation 'com.google.android.material:material:1.2.1'\n implementation 'androidx.constraintlayout:constraintlayout:2.0.2'\n implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'\n implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'\n testImplementation 'junit:junit:4.+'\n androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n}\n</code></pre>\n<h5><a id=\"127_364\"></a><strong>1.2.7.构建和测试</strong></h5>\n<ul>\n<li>现在,请在模拟器中构建并启动应用程序。单击工具栏中的运行图标 ▶️ 或按 ^ R。</li>\n</ul>\n<p><img src=\"https://dev-media.amazoncloud.cn/87a6ad453d4d460aa2c3778bcc5062ea_image.png\" alt=\"image.png\" /></p>\n<p>片刻之后,应用程序会在 Android 模拟器中启动,初始屏幕为空。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/f09db6d5333a424ea387659236ef98f6_image.png\" alt=\"image.png\" /></p>\n<p>在此阶段,没有要在运行时呈现的数据。到此您已成功创建 Android 应用程序。接下来开始使用 Amplify 进行构建!</p>\n<h3><a id=\"2_Amplify_374\"></a><strong>2.初始化 Amplify</strong></h3>\n<p>在此单元中,您将安装并配置 Amplify CLI。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/854fccc737b24fd09e6ba661c03406c3_image.png\" alt=\"image.png\" /></p>\n<h4><a id=\"21_379\"></a><strong>2.1简介</strong></h4>\n<p>现在我们已创建一个 Android 应用程序,我们想要继续开发并添加新功能。<br />\n要开始在应用程序中使用 Amazon Amplify,必须安装 Amplify 命令行,初始化 Amplify 项目目录,将项目配置为使用 Amplify 库,并在运行时初始化 Amplify 库。</p>\n<h4><a id=\"22_382\"></a><strong>2.2实施</strong></h4>\n<h5><a id=\"221_Amplify_CLI_383\"></a><strong>2.2.1安装 Amplify CLI</strong></h5>\n<p>Amazon Amplify CLI 取决于 Node.js,没有安装的化,请安装。<br />\n要安装 Amazon Amplify CLI,请打开一个终端,然后<strong>输入以下命令</strong>:<br />\nXML</p>\n<pre><code class=\"lang-\">## Install Amplify CLI\nnpm install -g @aws-amplify/cli\n</code></pre>\n<p><img src=\"https://dev-media.amazoncloud.cn/9ff3927ea71f4080805c63e66a2e93f2_image.png\" alt=\"image.png\" /></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/3fa058c3818e415bb10ff863ac0c1e2e_image.png\" alt=\"image.png\" /><br />\n查看版本:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/d3e0b4ed2bb246d4ba0f0ff51ace1029_image.png\" alt=\"image.png\" /><br />\nXML</p>\n<pre><code class=\"lang-\">## Verify installation and version\namplify --version\n\n 7.6.26\n</code></pre>\n<h5><a id=\"222_Amplify__404\"></a><strong>2.2.2初始化 Amplify 后端</strong></h5>\n<p>要创建后端基本结构,首先需要初始化 Amplify 项目目录并创建云后端。<br />\n打开项目所在目录,输入cmd,即可打开终端</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/518a4a0c0f5542a3a56ad685079f8650_image.png\" alt=\"image.png\" /></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/f221c0299308436380a7de2015ee1124_image.png\" alt=\"image.png\" /><br />\n验证您是否在正确的目录中,它应该如下所示:<br />\nXML</p>\n<pre><code class=\"lang-\">F:\\workspace\\AndroidASW>tree\n卷 开发环境 的文件夹 PATH 列表\n卷序列号为 D4D7-D5B3\nF:.\n├─.gradle\n│ ├─7.0.2\n│ │ ├─dependencies-accessors\n│ │ ├─fileChanges\n│ │ ├─fileHashes\n│ │ └─vcsMetadata-1\n│ ├─buildOutputCleanup\n│ ├─checksums\n│ └─vcs-1\n├─.idea\n│ ├─libraries\n│ └─modules\n│ └─app\n├─app\n│ ├─libs\n│ └─src\n│ ├─androidTest\n│ │ └─java\n│ │ └─com\n│ │ └─example\n│ │ └─androidasw\n│ ├─main\n│ │ ├─java\n│ │ │ └─com\n│ │ │ └─example\n│ │ │ └─androidasw\n│ │ └─res\n│ │ ├─drawable\n│ │ ├─drawable-v24\n│ │ ├─layout\n│ │ ├─menu\n│ │ ├─mipmap-anydpi-v26\n│ │ ├─mipmap-hdpi\n│ │ ├─mipmap-mdpi\n│ │ ├─mipmap-xhdpi\n│ │ ├─mipmap-xxhdpi\n│ │ ├─mipmap-xxxhdpi\n│ │ ├─navigation\n│ │ ├─values\n│ │ ├─values-land\n│ │ ├─values-night\n│ │ ├─values-w1240dp\n│ │ └─values-w600dp\n│ └─test\n│ └─java\n│ └─com\n│ └─example\n│ └─androidasw\n└─gradle\n └─wrapper\n\nF:\\workspace\\AndroidASW>\n</code></pre>\n<p>接下来,在亚马逊服务管理后台创建对应的用户,并添加权限类型,具体如下图所示:</p>\n<h5><a id=\"223_472\"></a><strong>2.2.3控制台相关操作</strong></h5>\n<p><strong><a href=\"https://portal.aws.amazon.com/billing/signup?type=resubscribe#/paymentinformation\" target=\"_blank\">第一步注册</a></strong></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/be189597410f469dbc440aab8f2310f8_image.png\" alt=\"image.png\" /></p>\n<p><a href=\"https://aws.amazon.com/cn/free/?trk=95502bdb-28e0-4dc1-895c-2e975a171d36&sc_channel=ba&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=all&awsf.Free%20Tier%20Categories=all\" target=\"_blank\">https://aws.amazon.com/cn/free/?trk=95502bdb-28e0-4dc1-895c-2e975a171d36&sc_channel=ba&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=all&awsf.Free%20Tier%20Categories=all</a><br />\n<strong>登录控制台</strong></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/b4664aea104b4304ba4be59142e994ce_image.png\" alt=\"image.png\" /></p>\n<p>添加权限</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/e278f8491a444689b895bf10ee4aa172_image.png\" alt=\"image.png\" /></p>\n<p>点击“下一步”,选择“直接附加现有策略”,一直“下一步",后会提示创建用户成功。</p>\n<p><strong>添加用户</strong></p>\n<p><img src=\"https://dev-media.amazoncloud.cn/a81fb1f130e54e32b6f48104a9ceffd7_image.png\" alt=\"image.png\" /></p>\n<p>到此,我们也就创建完成。接下来继续下面的步骤。<br />\n初始化 Amplify 项目结构和配置文件。<strong>运行以下命令</strong>:<br />\namplify init<br />\nJavaScript</p>\n<pre><code class=\"lang-\">\n? Enter a name for your project (androidgettingstarted): accept the default, press enter\n? Enter a name for the environment (dev): accept the default, press enter\n? Choose your default editor: use the arrow key to select your favorite text editor an press enter\n? Choose the type of app that you're building: android is already selected, press enter\n? Where is your Res directory: accept the default, press enter\n? Do you want to use an AWS profile?, Y, press enter\n? Please choose the profile you want to use: use the arrow keys to select your profile and press enter.\n</code></pre>\n<p>如果没有配置文件,可使用 Amazon CLI 键入命令 aws configure --profile <name> 创建一个。<br />\nAmplify 在云中初始化您的项目,可能需要几分钟。几分钟后,您应该会看到如下消息:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/be6f4d491d5d4ab59aac77454cb772e4_image.png\" alt=\"image.png\" /></p>\n<h5><a id=\"224_Amplify__511\"></a><strong>2.2.4将 Amplify 库添加到项目中</strong></h5>\n<p>Amplify for Android 是作为 Apache Maven 软件包分发的。在本部分中,您会将软件包和其他必需的指令添加到构建配置中。<br />\n返回 Android Studio,展开 Gradle Scripts 并打开 build.gradle (Project: Android_Getting_Started)。在 buildscript 和 allprojects 数据块的 repositories 数据块内添加行 mavenCentral()。<br />\nJavaScript</p>\n<pre><code class=\"lang-\">buildscript {\n ext.kotlin_version = "1.4.10"\n repositories {\n google()\n jcenter()\n\n // Add this line into `repositories` in `buildscript`\n mavenCentral()\n }\n dependencies {\n classpath "com.android.tools.build:gradle:4.0.1"\n classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"\n\n // NOTE: Do not place your application dependencies here; they belong\n // in the individual module build.gradle files\n }\n}\n\nallprojects {\n repositories {\n google()\n jcenter()\n\n // Add this line into `repositories` in `buildscript`\n mavenCentral()\n }\n}\n</code></pre>\n<p>在 <strong>Gradle Scripts</strong> 下,打开 <strong>build.gradle (Module:app)</strong>,并在 implementations 数据块中添加 Amplify 框架核心依赖项。<br />\nJavaScript</p>\n<pre><code class=\"lang-\">dependencies {\n implementation fileTree(dir: "libs", include: ["*.jar"])\n implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"\n implementation 'androidx.core:core-ktx:1.3.2'\n implementation 'androidx.appcompat:appcompat:1.2.0'\n implementation 'com.google.android.material:material:1.2.1'\n implementation 'androidx.constraintlayout:constraintlayout:2.0.1'\n implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'\n implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'\n testImplementation 'junit:junit:4.13'\n androidTestImplementation 'androidx.test.ext:junit:1.1.2'\n androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'\n\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n}\n</code></pre>\n<p>如果您使用 Java 或目标 Android SDK 21 或更早版本进行开发,请<a href=\"https://docs.amplify.aws/lib/project-setup/create-application/q/platform/android#n2-install-amplify-libraries\" target=\"_blank\">查看文档</a>了解其他配置更改。<br />\n现在,运行 <strong>Gradle Sync</strong>。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/99e6a70cb30044e5ba1901d3a1b16e16_image.png\" alt=\"image.png\" /></p>\n<p>过一会儿您将看到<br />\nBUILD SUCCESSFUL in 1s</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/5e582ca7acd04bac88a06fd591bf1062_image.png\" alt=\"image.png\" /></p>\n<h5><a id=\"225_Amplify_574\"></a><strong>2.2.5在运行时初始化 Amplify</strong></h5>\n<p>我们来创建一个后端类对代码进行分组,以便与后端交互。我使用<a href=\"https://en.wikipedia.org/wiki/Singleton_pattern\" target=\"_blank\">单例设计模式</a>,使其通过应用程序轻松可用,并确保 Amplify 库仅初始化一次。<br />\n类初始化程序负责初始化 Amplify 库。<br />\n在 java/com.example.androidgettingstarted 下创建一个新的 Kotlin 文件 Backend.kt,打开它并添加以下代码:<br />\nJavaScript</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.content.Context\nimport android.util.Log\nimport com.amplifyframework.AmplifyException\nimport com.amplifyframework.core.Amplify\n\nobject Backend {\n\n private const val TAG = "Backend"\n\n fun initialize(applicationContext: Context) : Backend {\n try {\n Amplify.configure(applicationContext)\n Log.i(TAG, "Initialized Amplify")\n } catch (e: AmplifyException) {\n Log.e(TAG, "Could not initialize Amplify", e)\n }\n return this\n }\n}\n</code></pre>\n<p>应用程序启动时,初始化单例 Backend 对象。<br />\n在 java/com.example.androidgettingstarted 下创建一个新的 Kotlin 文件 Application.kt,打开它并添加以下代码:<br />\nJavaScript</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.app.Application\n\nclass AndroidGettingStartedApplication : Application() {\n\n override fun onCreate() {\n super.onCreate()\n\n // initialize Amplify when application is starting\n Backend.initialize(applicationContext)\n }\n}\n</code></pre>\n<p>在“manifests”下,打开 AndroidManifest.xml,并将应用程序类的名称添加到 <application> 元素。<br />\nJavaScript</p>\n<pre><code class=\"lang-\"><!-- add the android:name attribute to the application node -->\n <application\n android:name="AndroidGettingStartedApplication"\n android:allowBackup="true"\n android:icon="@mipmap/ic_launcher"\n android:label="@string/app_name"\n android:roundIcon="@mipmap/ic_launcher_round"\n android:supportsRtl="true"\n android:theme="@style/Theme.GettingStartedAndroid">\n...\n</code></pre>\n<p>打开此文件后,请添加一些在本教程的后续步骤中应用程序需要的权限:<br />\nXML</p>\n<pre><code class=\"lang-\"><!-- add these nodes between manifest and application -->\n <uses-permission android:name="android.permission.INTERNET"/>\n <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>\n <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>\n</code></pre>\n<h5><a id=\"226_642\"></a><strong>2.2.6验证您的设置</strong></h5>\n<p>要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。<br />\n要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的 运行 图标 ▶️ 或按 ^ R。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/e3a0c6bfc2d74c0791e675b22265fcab_image.png\" alt=\"image.png\" /></p>\n<p>应该不会出现错误。<br />\nBUILD SUCCESSFUL in 6s<br />\n23 actionable tasks: 8 executed, 15 up-to-date</p>\n<h3><a id=\"3_652\"></a><strong>3.添加身份验证</strong></h3>\n<p>在此单元中,您将使用 Amplify CLI 和库配置和添加身份验证到您的应用程序中。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/c89263a43dc040729742921e6169b259_image.png\" alt=\"image.png\" /></p>\n<h4><a id=\"31_657\"></a><strong>3.1简介</strong></h4>\n<p>您将添加的下一个功能是用户身份验证。在此单元中,您将了解如何使用 Amplify CLI 和库对用户进行身份验证,以利用托管用户身份提供商 <a href=\"https://aws.amazon.com/cn/cognito/\" target=\"_blank\">Amazon Cognito</a>。<br />\n您还将了解如何使用 Cognito 托管用户界面展示整个用户身份验证流,从而使用户只需几行代码即可注册、登录和重置密码。<br />\n使用“托管用户界面”意味着应用程序利用 Cognito 网页登录和注册用户界面流。应用程序的用户将重定向到 Cognito 托管的网页,并在登录后重定向回应用程序。当然,Cognito 和 Amplify 也支持本机 UI。</p>\n<h4><a id=\"32_661\"></a><strong>3.2实施</strong></h4>\n<h5><a id=\"321_662\"></a><strong>3.2.1创建身份验证服务</strong></h5>\n<p>要创建身份验证服务,请打开一个终端,然后执行以下命令:<br />\namplify add auth<br />\n当您看到此消息时,即表示配置成功(资源的确切名称将有所不同):<br />\nSuccessfully added resource androidgettingstartedfc5a4717 locally</p>\n<h5><a id=\"322_667\"></a><strong>3.2.2部署身份验证服务</strong></h5>\n<p>现在,已在本地配置身份验证服务,我们可以将它部署到云:在终端中,请在项目目录中执行以下命令:<br />\namplify push</p>\n<p>#press Y when asked to continue<br />\n片刻之后,您应看到以下消息:<br />\nJavaScript</p>\n<pre><code class=\"lang-\">✔ All resources are updated in the cloud\n\nHosted UI Endpoint: https://androidgettingstarted-dev.auth.eu-central-1.amazoncognito.com/\nTest Your Hosted UI Endpoint: https://androidgettingstarted-dev.auth.eu-central-1.amazoncognito.com/login?respons\n</code></pre>\n<h5><a id=\"323_Amplify__680\"></a><strong>3.2.3向项目添加 Amplify 身份验证库</strong></h5>\n<p>在转到代码之前,请先返回 Android Studio,将以下依赖项连同您之前添加的其他<br />\n<code>amplifyframework</code> 实现一起,添加到您的单元的<br />\n<code>build.gradle</code>,然后在看到提示时,单击 <strong>Sync Now</strong>:<br />\nJavaScript</p>\n<pre><code class=\"lang-\">dependencies {\n ...\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n}\n</code></pre>\n<h5><a id=\"324Amplify_693\"></a><strong>3.2.4在运行时配置Amplify身份验证库</strong></h5>\n<p>返回 Android Studio,打开 Backend.kt 文件。在后端类中,向我们在上一部分(在 initialize() 方法中)添加的 Amplify 初始化代码<strong>添加一行</strong>。<br />\n完整代码块应如下所示:\t<br />\nJavaScript</p>\n<pre><code class=\"lang-\">// inside Backend class\nfun initialize(applicationContext: Context) : Backend {\n try {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.configure(applicationContext)\n\n Log.i(TAG, "Initialized Amplify")\n } catch (e: AmplifyException) {\n Log.e(TAG, "Could not initialize Amplify", e)\n }\n return this\n}\n</code></pre>\n<p>请不要忘记添加导入语句,Android Studio 会自动为您完成此操作(在 Mac 上,在代码编辑器检测到的每个错误上,同时按 Alt 和 Enter 键)。<br />\n像在上一步骤中那样,为每个缺少的类定义添加所需的导入语句(在红色单词上同时按 Alt 和 Enter 键)。<br />\n要验证一切是否都按预期运行,请构建项目。单击 <strong>Build</strong> 菜单,并选择 <strong>Make Project</strong>,或者,在 Mac 上按 <strong>⌘F9</strong>。应该不会出现错误。</p>\n<h5><a id=\"325_714\"></a><strong>3.2.5在运行时触发身份验证</strong></h5>\n<p>其余代码更改会跟踪用户的状态(他们是否已登录?)并在用户单击锁定图标时触发“SignIn/SignUp”用户界面。</p>\n<h6><a id=\"a_signIn__signOut__716\"></a><strong>a.添加 signIn 和 signOut 方法</strong></h6>\n<p>在后端类中的任意位置,添加以下四种方法:<br />\nJava</p>\n<pre><code class=\"lang-\">private fun updateUserData(withSignedInStatus : Boolean) {\n UserData.setSignedIn(withSignedInStatus)\n}\n\nfun signOut() {\n Log.i(TAG, "Initiate Signout Sequence")\n\n Amplify.Auth.signOut(\n { Log.i(TAG, "Signed out!") },\n { error -> Log.e(TAG, error.toString()) }\n )\n}\n\nfun signIn(callingActivity: Activity) {\n Log.i(TAG, "Initiate Signin Sequence")\n\n Amplify.Auth.signInWithWebUI(\n callingActivity,\n { result: AuthSignInResult -> Log.i(TAG, result.toString()) },\n { error: AuthException -> Log.e(TAG, error.toString()) }\n )\n}\n</code></pre>\n<p>然后为每个缺少的类定义添加所需的导入语句(在红色单词上按 Alt 和 Enter 键)。当您可以在多个类之间进行选择时,请务必从 Amplify 包中选择一个,如下面的屏幕截图所示。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/7b53ca6149004caea5522c1627342ef7_image.png\" alt=\"image.png\" /></p>\n<p>请注意,我们没有在这些方法中更新 UserData.isSignedIn 标记,该操作将在下一节中完成。</p>\n<h6><a id=\"b_748\"></a><strong>b.添加身份验证中心侦听器</strong></h6>\n<p>为了跟踪身份验证状态的变化,我们添加了代码以订阅由 Amplify 发送的身份验证事件。我们在 Backend.initialize() 方法中初始化该中心。<br />\n在收到身份验证事件时,我们将调用 updateUserData() 方法。此方法可使 UserData 对象保持同步。UserData.isSignedIn 属性是 LiveData<Boolean>,这意味着当值更改时,订阅此属性的观察者将收到通知。我们使用此机制来自动刷新用户界面。<br />\n我们还添加了代码以在应用程序启动时检查以前的身份验证状态。当应用程序启动时,它会检查 Cognito 会话是否已存在,并相应地更新 UserData。<br />\n在 Backend.initialize() 中,在 try/catch 块之后和 return 语句之前<strong>添加以下代码</strong>。<br />\nJava</p>\n<pre><code class=\"lang-\">// in Backend.initialize() function, after the try/catch block but before the return statement \n\nLog.i(TAG, "registering hub event")\n\n// listen to auth event\nAmplify.Hub.subscribe(HubChannel.AUTH) { hubEvent: HubEvent<*> ->\n\n when (hubEvent.name) {\n InitializationStatus.SUCCEEDED.toString() -> {\n Log.i(TAG, "Amplify successfully initialized")\n }\n InitializationStatus.FAILED.toString() -> {\n Log.i(TAG, "Amplify initialization failed")\n }\n else -> {\n when (AuthChannelEventName.valueOf(hubEvent.name)) {\n AuthChannelEventName.SIGNED_IN -> {\n updateUserData(true)\n Log.i(TAG, "HUB : SIGNED_IN")\n }\n AuthChannelEventName.SIGNED_OUT -> {\n updateUserData(false)\n Log.i(TAG, "HUB : SIGNED_OUT")\n }\n else -> Log.i(TAG, """HUB EVENT:${hubEvent.name}""")\n }\n }\n }\n}\n\nLog.i(TAG, "retrieving session status")\n\n// is user already authenticated (from a previous execution) ?\nAmplify.Auth.fetchAuthSession(\n { result ->\n Log.i(TAG, result.toString())\n val cognitoAuthSession = result as AWSCognitoAuthSession\n // update UI\n this.updateUserData(cognitoAuthSession.isSignedIn)\n when (cognitoAuthSession.identityId.type) {\n AuthSessionResult.Type.SUCCESS -> Log.i(TAG, "IdentityId: " + cognitoAuthSession.identityId.value)\n AuthSessionResult.Type.FAILURE -> Log.i(TAG, "IdentityId not present because: " + cognitoAuthSession.identityId.error.toString())\n }\n },\n { error -> Log.i(TAG, error.toString()) }\n)\n</code></pre>\n<p>要验证一切是否都按预期运行,请构建项目。单击 <strong>Build</strong>菜单,并选择 <strong>Make Project</strong>,或者,在 Mac 上按 <strong>⌘F9</strong>。应该不会出现错误。</p>\n<h6><a id=\"c_803\"></a><strong>c.更新用户界面代码</strong></h6>\n<p>代码中的最后一个更改与用户界面相关,我们将 <a href=\"https://developer.android.com/reference/com/google/android/material/floatingactionbutton/FloatingActionButton\" target=\"_blank\">FloatingActionButton</a> 添加到主要活动中。<br />\n在“res/layout”下,打开 activity_main.xml,并将现有的 FloatingActionButton <strong>替换</strong>为以下内容:<br />\nJava</p>\n<pre><code class=\"lang-\"><com.google.android.material.floatingactionbutton.FloatingActionButton\n android:id="@+id/fabAuth"\n android:layout_width="wrap_content"\n android:layout_height="wrap_content"\n android:layout_alignParentRight="true"\n android:layout_gravity="bottom|end"\n android:layout_margin="@dimen/fab_margin"\n android:src="@drawable/ic_baseline_lock"\n app:fabCustomSize="60dp"\n app:fabSize="auto"\n />\n</code></pre>\n<p>在“res/drawable”下添加一个锁状图标。右键单击“drawable”,选择 <strong>New</strong>,然后选择 <strong>Vector Asset</strong>。从 Clilp Art 中选择锁状图标,然后输入 <strong>ic_baseline_lock</strong>(不含 _24)作为名称,并从 Clip Art 中选择闭合的锁状图标。单击 <strong>Next</strong>,然后单击 <strong>Finish</strong>。<br />\n对打开的锁状图标重复相同的操作。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/e1b2deee21d64c8f8b4594009c28c550_image.png\" alt=\"image.png\" /></p>\n<p>执行完以上操作后,您的“drawable”目录下应具有以下文件:</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/0573d92a6f824f9c934c437f4a3d2284_image.png\" alt=\"image.png\" /></p>\n<p>现在,在代码中链接新创建的按钮。在 java/com.example.androidgettingstarted/ 下,打开 MainActivity.kt 并添加以下代码。<br />\nJava</p>\n<pre><code class=\"lang-\">// anywhere in the MainActivity class\nprivate fun setupAuthButton(userData: UserData) {\n\n // register a click listener\n fabAuth.setOnClickListener { view ->\n\n val authButton = view as FloatingActionButton\n\n if (userData.isSignedIn.value!!) {\n authButton.setImageResource(R.drawable.ic_baseline_lock_open)\n Backend.signOut()\n } else {\n authButton.setImageResource(R.drawable.ic_baseline_lock_open)\n Backend.signIn(this)\n }\n }\n}\n</code></pre>\n<p>还是在 MainActivity 中,在 onCreate() 方法的末尾<strong>添加以下代码</strong>:<br />\nJava</p>\n<pre><code class=\"lang-\">setupAuthButton(UserData)\n\nUserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp ->\n // update UI\n Log.i(TAG, "isSignedIn changed : $isSignedUp")\n\n if (isSignedUp) {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock_open)\n } else {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock)\n }\n})\n</code></pre>\n<p>以上代码会针对 Userdata.isSignedIn 值注册观察者。当 isSignedIn 值更改时调用闭包。现在,我们只是更改锁状图标:当用户通过身份验证时为打开的锁,当用户没有会话时为闭合的锁。<br />\n要验证一切是否都按预期运行,请构建项目。单击 <strong>Build</strong> 菜单,并选择 <strong>Make Project</strong>,或者,在 Mac 上按 <strong>⌘F9</strong>。应该不会出现错误。</p>\n<h6><a id=\"d_AndroidManifestxml__MainActivity_868\"></a><strong>d.更新 AndroidManifest.xml 和 MainActivity</strong></h6>\n<p>最后,我们必须确保在 Cognito 托管用户界面提供的 Web 身份验证序列结束时启动我们的应用程序。我们在清单文件中添加一个新的活动。当接收到 gettingstarted URI 方案时,将调用该活动。<br />\n在 Android Studio 中的“manifests”下,打开 AndroidManifest.xml,并在应用程序元素中添加以下活动。<br />\nJava</p>\n<pre><code class=\"lang-\"> <activity\nandroid:name="com.amazonaws.mobileconnectors.cognitoauth.activities.CustomTabsRedirectActivity">\n <intent-filter>\n <action android:name="android.intent.action.VIEW" />\n\n <category android:name="android.intent.category.DEFAULT" />\n <category android:name="android.intent.category.BROWSABLE" />\n\n <data android:scheme="gettingstarted" />\n </intent-filter>\n </activity>\n</code></pre>\n<p>在 java/com.example.androidgettingstarted/ 下,打开 MainActivity.kt 并在类中的任意位置添加以下代码。<br />\nJava</p>\n<pre><code class=\"lang-\">// MainActivity.kt\n// receive the web redirect after authentication\noverride fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n super.onActivityResult(requestCode, resultCode, data)\n Backend.handleWebUISignInResponse(requestCode, resultCode, data)\n}\n在 java/com.example.androidgettingstarted/ 下,打开 Backend.kt 并在类中的任意位置添加以下代码。\n// Backend.kt\n// pass the data from web redirect to Amplify libs \nfun handleWebUISignInResponse(requestCode: Int, resultCode: Int, data: Intent?) {\n Log.d(TAG, "received requestCode : $requestCode and resultCode : $resultCode")\n if (requestCode == AWSCognitoAuthPlugin.WEB_UI_SIGN_IN_ACTIVITY_CODE) {\n Amplify.Auth.handleWebUISignInResponse(data)\n }\n}\n</code></pre>\n<h6><a id=\"e_904\"></a><strong>e.构建和测试</strong></h6>\n<p>要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。应用程序将启动,且屏幕右下角会显示一个闭合的锁状浮动按钮。<br />\n以下是完整的注册流程。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/fbce7916dc2f489893479b94cb8994a5_image.png\" alt=\"image.png\" /><br />\n<img src=\"https://dev-media.amazoncloud.cn/8ff2e17421eb4e9b8c072b04455dc579_image.png\" alt=\"image.png\" /><br />\n<img src=\"https://dev-media.amazoncloud.cn/d51b7e3254954a2ab6ae88a4000dc5b5_image.png\" alt=\"image.png\" /><br />\n<img src=\"https://dev-media.amazoncloud.cn/a96f27872fab47f7b4b20a98ae638b7c_image.png\" alt=\"image.png\" /><br />\n<img src=\"https://dev-media.amazoncloud.cn/dd8be465bd5d4385b2289426d23a550d_image.png\" alt=\"image.png\" /></p>\n<h3><a id=\"4_GraphQL_API__914\"></a><strong>4.添加 GraphQL API 和数据库</strong></h3>\n<p>在此单元中,您将使用 Amplify CLI 和库配置和添加 GraphQL API 到您的应用程序中。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/c82a789162a14adc9ec4c608735bf542_image.png\" alt=\"image.png\" /></p>\n<h4><a id=\"41_919\"></a><strong>4.1简介</strong></h4>\n<p>现在,我们已经创建并配置了带用户身份验证功能的应用程序。接下来,我们要在数据库中添加 API 以及“创建”、“读取”、“更新”、“删除”(CRUD) 操作。<br />\n在此单元中,您将使用 Amplify CLI 和库将 API 添加到您的应用程序中。您将创建的 API 是 <a href=\"https://graphql.org/\" target=\"_blank\">GraphQL</a> API,它利用 <a href=\"https://aws.amazon.com/cn/dynamodb/\" target=\"_blank\">Amazon DynamoDB</a>(NoSQL 数据库)支持的 <a href=\"https://aws.amazon.com/cn/appsync/\" target=\"_blank\">Amazon AppSync</a>(托管 GraphQL 服务)。有关 GraphQL 的介绍,请<a href=\"https://graphql.org/learn/\" target=\"_blank\">访问此页面</a>。<br />\n您将构建的应用程序是备注记录应用程序,用户可使用它创建、删除和列出备注。本示例将让您了解如何构建很多常见类型的 CRUD+L(创建、读取、更新、删除和列出)应用程序。</p>\n<h4><a id=\"42_923\"></a><strong>4.2实施</strong></h4>\n<h5><a id=\"421_GraphQL_API__924\"></a><strong>4.2.1创建 GraphQL API 服务和数据库</strong></h5>\n<p>要创建 GraphQL API 及其后备数据库,请打开一个终端,然后从项目目录中<strong>执行此命令</strong>:<br />\namplify add api<br />\n初始化项目时选择的默认文本编辑器 (amplify init) 将使用预构建数据 schema 打开。<br />\n<strong>删除</strong>此 schema,并使用我们的应用程序 GraphQL schema <strong>替换</strong>:<br />\nJava</p>\n<pre><code class=\"lang-\">type NoteData\n@model\n@auth (rules: [ { allow: owner } ]) {\n id: ID!\n name: String!\n description: String\n image: String\n}\n</code></pre>\n<p>数据模型由一个类 NoteData 和 4 个属性组成:ID 和名称是必填项,描述和图像是选填字符串。<br />\n@model 转换器指示我们需要创建数据库来存储数据。<br />\n@auth 转换器添加了身份验证规则,以允许访问此数据。对于此项目,我们希望仅 NoteData 的拥有者有访问它们的权限。<br />\n完成后,请不要忘记<strong>保存</strong>,然后返回您的终端以告知 Amplify CLI 您已完成。<br />\n? Press enter to continue, press enter.<br />\n几秒钟后,您应该会看到一条成功消息:<br />\nGraphQL schema compiled successfully.</p>\n<h5><a id=\"422_947\"></a><strong>4.2.2生成客户端代码</strong></h5>\n<p>根据我们刚刚创建的 GraphQL 数据模型定义,Amplify 会生成客户端代码(即 Swift 代码)以代表我们应用程序中的数据。<br />\n要生成代码,请在终端<strong>执行以下命令</strong>:<br />\namplify codegen models<br />\n这将在 java/com/amplifyframework.datastore.generated.model 目录中创建 Java 文件,如以下情况所示:<br />\nJava</p>\n<pre><code class=\"lang-\">➜ Android Getting Started git:(master) ✗ ls -al app/src/main/java/com/amplifyframework/datastore/generated/model \ntotal 24\ndrwxr-xr-x 4 stormacq admin 128 Oct 7 15:27 .\ndrwxr-xr-x 3 stormacq admin 96 Oct 7 15:27 ..\n-rw-r--r-- 1 stormacq admin 1412 Oct 7 15:27 AmplifyModelProvider.java\n-rw-r--r-- 1 stormacq admin 7153 Oct 7 15:27 NoteData.java\n</code></pre>\n<p>这些文件会自动导入到您的项目中。</p>\n<h5><a id=\"422_API__962\"></a><strong>4.2.2部署 API 服务和数据库</strong></h5>\n<p>要部署我们刚刚创建的后端 API 和数据库,请转至您的终端,然后<strong>执行命令</strong>:<br />\nJava</p>\n<pre><code class=\"lang-\">amplify push\n# press Y when asked to continue\n\n? Are you sure you want to continue? accept the default Y and press enter\n? Do you want to generate code for your newly created GraphQL API type N and press enter\n</code></pre>\n<p>几分钟后,您应该会看到一条成功消息:<br />\n✔ All resources are updated in the cloud</p>\n<p>GraphQL endpoint: <a href=\"https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql\" target=\"_blank\">https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql</a></p>\n<h5><a id=\"424API_Android_Studio__976\"></a><strong>4.2.4将API客户端库添加到 Android Studio 项目</strong></h5>\n<p>在转到此代码之前,请先返回 Android Studio,将以下依赖关系连同您之前添加的其他“amplifyframework”实现一起添加到您的单元的 build.gradle,然后在看到提示时单击 <strong>Sync Now</strong>。<br />\nJava</p>\n<pre><code class=\"lang-\">dependencies {\n implementation 'com.amplifyframework:aws-api:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n}\n</code></pre>\n<h5><a id=\"425_Amplify__985\"></a><strong>4.2.5在运行时初始化 Amplify 库</strong></h5>\n<p>打开 Backend.kt,然后在 initialize() 方法的 Amplify 初始化序列中添加一行。完整的尝试/捕获代码块应如下所示:<br />\nJava</p>\n<pre><code class=\"lang-\">try {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.addPlugin(AWSApiPlugin())\n Amplify.configure(applicationContext)\n\n Log.i(TAG, "Initialized Amplify")\n} catch (e: AmplifyException) {\n Log.e(TAG, "Could not initialize Amplify", e)\n}\n</code></pre>\n<h5><a id=\"426_GraphQL__999\"></a><strong>4.2.6在 GraphQL 数据模型和应用程序模型之间添加桥接</strong></h5>\n<p>我们的项目已经有一个数据模型来表示备注。在此教程中,我们将继续使用该模型,并提供一种将 NoteData 转换为备注的简单方法。打开 UserData.kt 并添加两个组件:一个动态属性,从 UserData.Note 返回 NoteData 对象;一个相反静态方法,接收 API NoteData 并返回 Userdata.Note。<br />\n在数据类 Note 中,添加以下内容:<br />\nJava</p>\n<pre><code class=\"lang-\">// return an API NoteData from this Note object\nval data : NoteData\n get() = NoteData.builder()\n .name(this.name)\n .description(this.description)\n .image(this.imageName)\n .id(this.id)\n .build()\n\n// static function to create a Note from a NoteData API object\ncompanion object {\n fun from(noteData : NoteData) : Note {\n val result = Note(noteData.id, noteData.name, noteData.description, noteData.image)\n // some additional code will come here later\n return result\n }\n} \n</code></pre>\n<p>确保通过生成的代码导入 NoteData 类。</p>\n<h5><a id=\"427_API_CRUD__1023\"></a><strong>4.2.7将 API CRUD 方法添加到后端类</strong></h5>\n<p>我们添加 3 种方法来调用 API:一种查询 Note 的方法,一种创建新 Note 的方法,以及一种删除 Note 的方法。请注意,这些方法适用于应用程序数据模型 (Note),以便通过用户界面轻松交互。这些方法可以透明地将 Note 转换为 GraphQL 的 NoteData 对象。<br />\n<strong>打开</strong> Backend.kt 文件,然后在后端类末尾<strong>添加</strong>以下代码段:<br />\nJava</p>\n<pre><code class=\"lang-\">fun queryNotes() {\n Log.i(TAG, "Querying notes")\n\n Amplify.API.query(\n ModelQuery.list(NoteData::class.java),\n { response ->\n Log.i(TAG, "Queried")\n for (noteData in response.data) {\n Log.i(TAG, noteData.name)\n // TODO should add all the notes at once instead of one by one (each add triggers a UI refresh)\n UserData.addNote(UserData.Note.from(noteData))\n }\n },\n { error -> Log.e(TAG, "Query failure", error) }\n )\n}\n\nfun createNote(note : UserData.Note) {\n Log.i(TAG, "Creating notes")\n\n Amplify.API.mutate(\n ModelMutation.create(note.data),\n { response ->\n Log.i(TAG, "Created")\n if (response.hasErrors()) {\n Log.e(TAG, response.errors.first().message)\n } else {\n Log.i(TAG, "Created Note with id: " + response.data.id)\n }\n },\n { error -> Log.e(TAG, "Create failed", error) }\n )\n}\n\nfun deleteNote(note : UserData.Note?) {\n\n if (note == null) return\n\n Log.i(TAG, "Deleting note $note")\n\n Amplify.API.mutate(\n ModelMutation.delete(note.data),\n { response ->\n Log.i(TAG, "Deleted")\n if (response.hasErrors()) {\n Log.e(TAG, response.errors.first().message)\n } else {\n Log.i(TAG, "Deleted Note $response")\n }\n },\n { error -> Log.e(TAG, "Delete failed", error) }\n )\n}\n确保通过生成的代码导入 ModelQuery、ModelMutation 和 NoteData 类。\n最后,我们必须在应用程序启动时调用此 API,以查询当前登录用户的 Note 列表。\n在 Backend.kt 文件中,更新 updateUserData(withSignInStatus: Boolean) 方法,使其看起来类似以下内容:\n// change our internal state and query list of notes \nprivate fun updateUserData(withSignedInStatus : Boolean) {\n UserData.setSignedIn(withSignedInStatus)\n\n val notes = UserData.notes().value\n val isEmpty = notes?.isEmpty() ?: false\n\n // query notes when signed in and we do not have Notes yet\n if (withSignedInStatus && isEmpty ) {\n this.queryNotes()\n } else {\n UserData.resetNotes()\n }\n}\n</code></pre>\n<p>现在,只需创建一个用户界面即可创建新 Note 和从列表中删除 Note。</p>\n<h5><a id=\"428Edit_1100\"></a><strong>4.2.8添加“Edit"按钮以添加备注</strong></h5>\n<p>现在,后端和数据模型已到位,本节的最后一步是让用户创建新的 Note 然后将其删除。</p>\n<h6><a id=\"a_Android_Studio__reslayout__1102\"></a><strong>a.在 Android Studio 中的 res/layout 下,创建一个新布局:</strong></h6>\n<p>右键单击 <strong>layout</strong>,选择“New”,然后选择 <strong>Layout Resource File</strong>。将此文件命名为 activity_add_note 并接受所有其他默认值。单击 <strong>OK</strong>。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/94ea6d84d209458eb7b08114cdd4dc2c_image.png\" alt=\"image.png\" /></p>\n<p>打开刚刚创建的文件 activity_add_note,然后通过粘贴以下内容<strong>替换</strong>所生成的代码:<br />\nJava</p>\n<pre><code class=\"lang-\"><?xml version="1.0" encoding="utf-8"?>\n<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"\n xmlns:app="http://schemas.android.com/apk/res-auto"\n android:layout_width="match_parent"\n android:layout_height="match_parent"\n android:fitsSystemWindows="true"\n android:fillViewport="true">\n\n <LinearLayout\n android:layout_width="match_parent"\n android:layout_height="wrap_content"\n android:orientation="vertical"\n android:padding="8dp">\n\n <TextView\n android:id="@+id/title"\n android:layout_width="wrap_content"\n android:layout_height="wrap_content"\n android:layout_marginTop="8dp"\n android:text="Create a New Note"\n android:textSize="10pt" />\n\n <EditText\n android:id="@+id/name"\n android:layout_width="fill_parent"\n android:layout_height="wrap_content"\n android:layout_marginTop="8dp"\n android:hint="name"\n android:inputType="text"\n android:lines="5" />\n\n <EditText\n android:id="@+id/description"\n android:layout_width="fill_parent"\n android:layout_height="wrap_content"\n android:layout_marginBottom="8dp"\n android:hint="description"\n android:inputType="textMultiLine"\n android:lines="3" />\n\n <Space\n android:layout_width="match_parent"\n android:layout_height="0dp"\n android:layout_weight="1" />\n\n <Button\n android:id="@+id/addNote"\n style="?android:attr/buttonStyleSmall"\n android:layout_width="fill_parent"\n android:layout_height="wrap_content"\n android:layout_gravity="center_horizontal"\n android:backgroundTint="#009688"\n android:text="Add Note" />\n\n <Button\n android:id="@+id/cancel"\n style="?android:attr/buttonStyleSmall"\n android:layout_width="fill_parent"\n android:layout_height="wrap_content"\n android:layout_gravity="center_horizontal"\n android:backgroundTint="#FFC107"\n android:text="Cancel" />\n\n </LinearLayout>\n</ScrollView>\n</code></pre>\n<p>这是一个非常简单的布局,可以只输入 Note 标题和描述。</p>\n<h6><a id=\"b_AddNoteActivity__1177\"></a><strong>b.添加 AddNoteActivity 类。</strong></h6>\n<p>在 java/com.example.androidgettingstarted 下,创建一个新的 Kotlin 文件 AddActivityNote.kt,然后打开此文件并添加以下代码:<br />\nJava</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport kotlinx.android.synthetic.main.activity_add_note.*\nimport java.util.*\n\nclass AddNoteActivity : AppCompatActivity() {\n\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n setContentView(R.layout.activity_add_note)\n\n cancel.setOnClickListener {\n this.finish()\n }\n\n addNote.setOnClickListener {\n\n // create a note object\n val note = UserData.Note(\n UUID.randomUUID().toString(),\n name?.text.toString(),\n description?.text.toString()\n )\n\n // store it in the backend\n Backend.createNote(note)\n\n // add it to UserData, this will trigger a UI refresh\n UserData.addNote(note)\n\n // close activity\n this.finish()\n }\n }\n\n companion object {\n private const val TAG = "AddNoteActivity"\n }\n} \n</code></pre>\n<p>最后,在清单下打开 AndroidManifest.xml,并在应用程序节点的任何位置添加以下活动元素。<br />\nJava</p>\n<pre><code class=\"lang-\"><activity\n android:name=".AddNoteActivity"\n android:label="Add Note"\n android:theme="@style/Theme.GettingStartedAndroid.NoActionBar">\n <meta-data\n android:name="android.support.PARENT_ACTIVITY"\n android:value="com.example.androidgettingstarted.MainActivity" />\n</activity>\n</code></pre>\n<h6><a id=\"cMain_ActivityAdd_NoteFloatingActionButtonhttpsdeveloperandroidcomreferencecomgoogleandroidmaterialfloatingactionbuttonFloatingActionButton_1235\"></a><strong>c.在“Main Activity”中添加一个“<a href=\"https://developer.android.com/reference/com/google/android/material/floatingactionbutton/FloatingActionButton\" target=\"_blank\">Add Note”FloatingActionButton</a>。</strong></h6>\n<p>在 res/layout 下,打开 activity_main.xml 并将以下内容添加到现有“Floating Action Button”上方。<br />\nJava</p>\n<pre><code class=\"lang-\"><com.google.android.material.floatingactionbutton.FloatingActionButton\n android:id="@+id/fabAdd"\n android:layout_width="wrap_content"\n android:layout_height="wrap_content"\n android:layout_alignParentRight="true"\n android:layout_gravity="bottom|end"\n android:layout_margin="@dimen/fab_margin"\n android:visibility="invisible"\n android:src="@drawable/ic_baseline_post_add"\n app:fabCustomSize="60dp"\n app:fabSize="auto"/>\n</code></pre>\n<p>在 res/drawable 中添加一个“Add Note”图标。右键单击“drawable”,选择 <strong>New</strong>,然后选择 <strong>Vector Asset</strong>。输入 <strong>ic_baseline_add</strong> 作为名称,并从“Clip Art”中选择添加图标。单击 <strong>Next</strong>,然后单击 <strong>Finish</strong>。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/50c96c12d9c749eaa89a2dedcb2adb36_image.png\" alt=\"image.png\" /></p>\n<h6><a id=\"dAdd_Note_1255\"></a><strong>d.添加代码以处理“Add Note”按钮。</strong></h6>\n<p>要拥有功能完全的“Add Button”,还需要完成的最后两项工作是让按钮根据 isSignedIn 值显示或消失,显然,需要添加代码来处理按钮点击操作。<br />\n打开 mainActivity.kt,并将以下内容添加到 onCreate() 方法的末尾:<br />\nJava</p>\n<pre><code class=\"lang-\">// register a click listener\nfabAdd.setOnClickListener {\n startActivity(Intent(this, AddNoteActivity::class.java))\n}\n然后,仍然在 onCreate() 方法中,将 UserData.isSignedIn.observe 替换为以下内容:\nUserData.isSignedIn.observe(this, Observer<Boolean> { isSignedUp ->\n // update UI\n Log.i(TAG, "isSignedIn changed : $isSignedUp")\n\n //animation inspired by https://www.11zon.com/zon/android/multiple-floating-action-button-android.php\n if (isSignedUp) {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock_open)\n Log.d(TAG, "Showing fabADD")\n fabAdd.show()\n fabAdd.animate().translationY(0.0F - 1.1F * fabAuth.customSize)\n } else {\n fabAuth.setImageResource(R.drawable.ic_baseline_lock)\n Log.d(TAG, "Hiding fabADD")\n fabAdd.hide()\n fabAdd.animate().translationY(0.0F)\n }\n}) \n</code></pre>\n<p>要验证一切是否都按预期运行,请构建项目。单击 <strong>Build</strong> 菜单,并选择 <strong>Make Project</strong>,或者,在 Mac 上按 <strong>⌘F9</strong>。应该不会出现错误。<br />\n运行应用程序时,您将看到“Add Note”按钮会在用户登录时显示,在用户注销时消失。您现在可以添加备注了。</p>\n<h5><a id=\"429_1285\"></a><strong>4.2.9添加“滑动删除”行为</strong></h5>\n<p>可以通过在 Note 列表中添加触摸处理程序来添加“滑动删除”行为。触摸处理程序负责绘制红色背景、删除图标,并在释放触摸时调用 Backend.delete() 方法。</p>\n<h6><a id=\"a_SimpleTouchCallback_1287\"></a><strong>a.创建一个新类 SimpleTouchCallback。</strong></h6>\n<p>在 java/com 下,右键单击 example.androidgettingstarted,依次选择 New、Kotlin File,然后输入 SwipeCallback 作为名称。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/5d8a6018e08143b790f21ceb94e3cb8e_image.png\" alt=\"image.png\" /></p>\n<p>将以下代码粘贴到新文件中:<br />\nJava</p>\n<pre><code class=\"lang-\">package com.example.androidgettingstarted\n\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.drawable.ColorDrawable\nimport android.graphics.drawable.Drawable\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\n\n\n// https://stackoverflow.com/questions/33985719/android-swipe-to-delete-recyclerview\nclass SwipeCallback(private val activity: AppCompatActivity): ItemTouchHelper.SimpleCallback(\n 0,\n ItemTouchHelper.LEFT\n) {\n\n private val TAG: String = "SimpleItemTouchCallback"\n private val icon: Drawable? = ContextCompat.getDrawable(\n activity,\n R.drawable.ic_baseline_delete_sweep\n )\n private val background: ColorDrawable = ColorDrawable(Color.RED)\n\n override fun onChildDraw(\n c: Canvas,\n recyclerView: RecyclerView,\n viewHolder: RecyclerView.ViewHolder,\n dX: Float,\n dY: Float,\n actionState: Int,\n isCurrentlyActive: Boolean\n ) {\n super.onChildDraw(\n c,\n recyclerView,\n viewHolder,\n dX,\n dY,\n actionState,\n isCurrentlyActive\n )\n val itemView = viewHolder.itemView\n val backgroundCornerOffset = 20\n val iconMargin = (itemView.height - icon!!.intrinsicHeight) / 2\n val iconTop = itemView.top + (itemView.height - icon.intrinsicHeight) / 2\n val iconBottom = iconTop + icon.intrinsicHeight\n val iconRight: Int = itemView.right - iconMargin\n if (dX < 0) {\n val iconLeft: Int = itemView.right - iconMargin - icon.intrinsicWidth\n icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)\n background.setBounds(\n itemView.right + dX.toInt() - backgroundCornerOffset,\n itemView.top, itemView.right, itemView.bottom\n )\n background.draw(c)\n icon.draw(c)\n } else {\n background.setBounds(0, 0, 0, 0)\n background.draw(c)\n }\n }\n\n override fun onMove(\n recyclerView: RecyclerView,\n viewHolder: RecyclerView.ViewHolder,\n target: RecyclerView.ViewHolder\n ): Boolean {\n Toast.makeText(activity, "Moved", Toast.LENGTH_SHORT).show()\n return false\n }\n\n override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {\n\n Toast.makeText(activity, "deleted", Toast.LENGTH_SHORT).show()\n\n //Remove swiped item from list and notify the RecyclerView\n Log.d(TAG, "Going to remove ${viewHolder.adapterPosition}")\n\n // get the position of the swiped item in the list\n val position = viewHolder.adapterPosition\n\n // remove to note from the userdata will refresh the UI\n val note = UserData.deleteNote(position)\n\n // async remove from backend\n Backend.deleteNote(note)\n }\n}\n</code></pre>\n<p>重要的代码行位于 onSwiped() 方法中。当滑动手势完成时,将调用此方法。我们将收集列表中滑动项的位置,并会将相应备注从 UserData 结构(这将更新用户界面)和云后端中删除。</p>\n<h6><a id=\"b_resdrawable__1389\"></a><strong>b.现在,我们已经有了类,让我们在 res/drawable 中添加“删除”图标。</strong></h6>\n<p>右键单击“drawable”,选择 <strong>New</strong>,然后选择 <strong>Vector Asset</strong>。输入 <strong>ic_baseline_delete_sweep</strong> 作为名称,并从“Clip Art”中选择“删除滑动”图标。单击 <strong>Next</strong>,然后单击 <strong>Finish</strong>。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/4faf844970ba4d76ab20066f2972963a_image.png\" alt=\"image.png\" /></p>\n<h6><a id=\"c_RecyclerView__1394\"></a><strong>c.将以下代码粘贴到新文件中:为 RecyclerView 添加“滑动删除”手势处理程序。</strong></h6>\n<p>在 java/com/example.androidgettingstarted 下,打开 MainActivity.kt,并将下面两行代码添加到 setupRecyclerView 中:<br />\nJava</p>\n<pre><code class=\"lang-\">// add a touch gesture handler to manager the swipe to delete gesture\nval itemTouchHelper = ItemTouchHelper(SwipeCallback(this))\nitemTouchHelper.attachToRecyclerView(recyclerView)\n</code></pre>\n<h5><a id=\"4210_1402\"></a><strong>4.2.10构建和测试</strong></h5>\n<p>要验证一切是否都按预期运行,请构建并<strong>运行</strong>项目。单击工具栏中的运行图标 ▶️,或按 ^ R。应该不会出现错误。<br />\n假设您仍处于登录状态,应用程序会在一个空列表上启动。它现在具有一个用于添加 Note 的“Add Note”按钮。<strong>单击“Add Note”符号、输入标题和描述、单击“Add Note”按钮</strong>,随后备注应显示在列表中。<br />\n您可以通过向左滑动一行来删除 Note。</p>\n<h3><a id=\"5_1407\"></a><strong>5.添加图像存储功能</strong></h3>\n<p>在本单元中,您将添加存储以及将图像与您的应用程序中的备注关联的功能。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/0064920b16b2417b83366906550bc701_image.png\" alt=\"image.png\" /></p>\n<h4><a id=\"51_1412\"></a><strong>5.1简介</strong></h4>\n<p>现在,我们的备注应用程序已在运行,接下来添加将图像与每个备注关联的功能。在本单元中,您将使用 Amplify CLI 和库来创建利用 <a href=\"https://aws.amazon.com/cn/s3/\" target=\"_blank\">Amazon S3</a> 的存储服务。最后,您将更新 Android 应用程序以启用图像上传、获取和渲染。</p>\n<h4><a id=\"52_1415\"></a><strong>5.2实施</strong></h4>\n<h5><a id=\"521_1416\"></a><strong>5.2.1创建存储服务</strong></h5>\n<p>要添加图像存储功能,我们将使用 Amplify 存储类别:<br />\namplify add storage<br />\n一段时间后,您将看到:<br />\nJava</p>\n<pre><code class=\"lang-\">Successfully added resource image locally\n</code></pre>\n<h5><a id=\"522_1424\"></a><strong>5.2.2部署存储服务</strong></h5>\n<p>要部署我们刚刚创建的存储服务,请转至您的终端,然后执行以下命令:<br />\namplify push<br />\n按 <strong>Y</strong> 确认,一段时间后,您将看到:<br />\nJava</p>\n<pre><code class=\"lang-\">✔ Successfully pulled backend environment amplify from the cloud.\n</code></pre>\n<h5><a id=\"523_Android_Studio__Amplify__1432\"></a><strong>5.2.3向 Android Studio 项目添加 Amplify 存储库</strong></h5>\n<p>在转到此代码之前,请先返回 Android Studio,将以下依赖关系连同您之前添加的其他<br />\n<code>amplifyframework</code> 实现一起添加到您的单元的<br />\n<code>build.gradle</code>,然后在看到提示时单击 <strong>Sync Now</strong>:<br />\nJava</p>\n<pre><code class=\"lang-\">dependencies {\n ...\n // Amplify core dependency\n implementation 'com.amplifyframework:core:1.4.0'\n implementation 'com.amplifyframework:aws-auth-cognito:1.4.0'\n implementation 'com.amplifyframework:aws-api:1.4.0'\n implementation 'com.amplifyframework:aws-storage-s3:1.4.0'\n}\n</code></pre>\n<h5><a id=\"524_Amplify__1447\"></a><strong>5.2.4在运行时初始化 Amplify 存储插件</strong></h5>\n<p>返回 Android Studio,在 java/example.androidgettingstarted 下,打开 Backend.kit,并在 initialize() 方法中的 Amplify 初始化序列中添加一行。完整代码块应如下所示:<br />\nJava</p>\n<pre><code class=\"lang-\">try {\n Amplify.addPlugin(AWSCognitoAuthPlugin())\n Amplify.addPlugin(AWSApiPlugin())\n Amplify.addPlugin(AWSS3StoragePlugin())\n\n Amplify.configure(applicationContext)\n\n Log.i(TAG, "Initialized Amplify")\n} catch (e: AmplifyException) {\n Log.e(TAG, "Could not initialize Amplify", e)\n}\n</code></pre>\n<h5><a id=\"525_Image_CRUD__1463\"></a><strong>5.2.5将 Image CRUD 方法添加到后端类</strong></h5>\n<p>依然在 Backend.kt 中。在后端类的任何位置,添加以下三个方法,用于从存储中上传、下载和删除图像:<br />\nJava</p>\n<pre><code class=\"lang-\">fun storeImage(filePath: String, key: String) {\n val file = File(filePath)\n val options = StorageUploadFileOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n Amplify.Storage.uploadFile(\n key,\n file,\n options,\n { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") },\n { result -> Log.i(TAG, "Successfully uploaded: " + result.key) },\n { error -> Log.e(TAG, "Upload failed", error) }\n )\n}\n\nfun deleteImage(key : String) {\n\n val options = StorageRemoveOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n Amplify.Storage.remove(\n key,\n options,\n { result -> Log.i(TAG, "Successfully removed: " + result.key) },\n { error -> Log.e(TAG, "Remove failure", error) }\n )\n}\n\nfun retrieveImage(key: String, completed : (image: Bitmap) -> Unit) {\n val options = StorageDownloadFileOptions.builder()\n .accessLevel(StorageAccessLevel.PRIVATE)\n .build()\n\n val file = File.createTempFile("image", ".image")\n\n Amplify.Storage.downloadFile(\n key,\n file,\n options,\n { progress -> Log.i(TAG, "Fraction completed: ${progress.fractionCompleted}") },\n { result ->\n Log.i(TAG, "Successfully downloaded: ${result.file.name}")\n val imageStream = FileInputStream(file)\n val image = BitmapFactory.decodeStream(imageStream)\n completed(image)\n },\n { error -> Log.e(TAG, "Download Failure", error) }\n )\n}\n</code></pre>\n<p>这三种方法仅调用其 Amplify 对应项。Amplify 存储有三个文件保护级别:</p>\n<ul>\n<li><strong>公有</strong>:所有用户均可访问</li>\n<li><strong>受保护</strong>:所有用户均可读取,但只有创建用户可写入</li>\n<li><strong>私有</strong>:只有创建用户可读可写<br />\n对于此应用程序,我们希望仅备注拥有者可使用图像,我们将使用 StorageAccessLevel.PRIVATE 属性。</li>\n</ul>\n<h5><a id=\"526_UI__1524\"></a><strong>5.2.6添加 UI 代码以捕获图像</strong></h5>\n<p>下一步是修改 UI,以允许用户在单击 AddNoteACtivity 上的“Add image”按钮时从手机库中选择图像。<br />\n必须执行两个更改:更改“Add Note”活动布局以添加“Add image”按钮和图像视图,以及在活动类中添加处理程序代码。<br />\n在 Android Studio 中的“res/layout”下,打开 activity_add_note.xml 文件,然后<strong>将以下 Button 元素添加到</strong> addNote 按钮的正上方:<br />\nJava</p>\n<pre><code class=\"lang-\"><!-- after the description EditText -->\n<com.google.android.material.imageview.ShapeableImageView\n android:id="@+id/image"\n android:layout_width="match_parent"\n android:layout_height="280dp"\n android:layout_margin="16dp"\n android:scaleType="centerCrop" />\n\n<!-- after the Space -->\n<Button\n android:id="@+id/captureImage"\n style="?android:attr/buttonStyleSmall"\n android:layout_width="fill_parent"\n android:layout_height="wrap_content"\n android:layout_gravity="center_horizontal"\n android:backgroundTint="#009688"\n android:text="Add image" />\n</code></pre>\n<p>在 Android Studio 中的 java/example.androidgettingstarted 下,打开 AddNoteACtivity.kt 文件,然后在 onCreate() 方法中<strong>添加以下代码</strong>:<br />\nJava</p>\n<pre><code class=\"lang-\">// inside onCreate() \n// Set up the listener for add Image button\ncaptureImage.setOnClickListener {\n val i = Intent(\n Intent.ACTION_GET_CONTENT,\n MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n )\n startActivityForResult(i, SELECT_PHOTO)\n}\n\n// create rounded corners for the image\nimage.shapeAppearanceModel = image.shapeAppearanceModel\n .toBuilder()\n .setAllCorners(CornerFamily.ROUNDED, 150.0f)\n .build()\n</code></pre>\n<p>在 Intent、MediaStore 和 CornerFamily 上添加所需导入。<br />\n同时在伴生对象中添加以下常量值:<br />\n// add this to the companion object<br />\nprivate const val SELECT_PHOTO = 100<br />\n最后,添加收到的代码并将所选图像存储到临时文件。<br />\n将以下代码添加到 AddNoteACtivity 类的任何位置:<br />\nJava</p>\n<pre><code class=\"lang-\">//anywhere in the AddNoteActivity class\n\nprivate var noteImagePath : String? = null\nprivate var noteImage : Bitmap? = null\n\noverride fun onActivityResult(requestCode: Int, resultCode: Int, imageReturnedIntent: Intent?) {\n super.onActivityResult(requestCode, resultCode, imageReturnedIntent)\n Log.d(TAG, "Select photo activity result : $imageReturnedIntent")\n when (requestCode) {\n SELECT_PHOTO -> if (resultCode == RESULT_OK) {\n val selectedImageUri : Uri? = imageReturnedIntent!!.data\n\n // read the stream to fill in the preview\n var imageStream: InputStream? = contentResolver.openInputStream(selectedImageUri!!)\n val selectedImage = BitmapFactory.decodeStream(imageStream)\n val ivPreview: ImageView = findViewById<View>(R.id.image) as ImageView\n ivPreview.setImageBitmap(selectedImage)\n\n // store the image to not recreate the Bitmap every time\n this.noteImage = selectedImage\n\n // read the stream to store to a file\n imageStream = contentResolver.openInputStream(selectedImageUri)\n val tempFile = File.createTempFile("image", ".image")\n copyStreamToFile(imageStream!!, tempFile)\n\n // store the path to create a note\n this.noteImagePath = tempFile.absolutePath\n\n Log.d(TAG, "Selected image : ${tempFile.absolutePath}")\n }\n }\n}\n\nprivate fun copyStreamToFile(inputStream: InputStream, outputFile: File) {\n inputStream.use { input ->\n val outputStream = FileOutputStream(outputFile)\n outputStream.use { output ->\n val buffer = ByteArray(4 * 1024) // buffer size\n while (true) {\n val byteCount = input.read(buffer)\n if (byteCount < 0) break\n output.write(buffer, 0, byteCount)\n }\n output.flush()\n output.close()\n }\n }\n}\n</code></pre>\n<p>以上代码将所选图像作为 InputStream 使用两次。第一次,InputStream 会创建一个位图图像以显示在用户界面中,第二次,InputStream 会保存一个临时文件以发送到后端。<br />\n本单元将浏览一个临时文件,因为 Amplify API 使用 Fileobjects。尽管不是最高效的设计,但此代码十分简单。<br />\n要验证一切是否都按预期运行,请构建项目。单击 **Build **菜单,并选择 <strong>Make Project</strong>,或者,在 Mac 上按 <strong>⌘F9</strong>。应该不会出现错误。</p>\n<h6><a id=\"527_1628\"></a><strong>5.2.7创建备注时存储图像</strong></h6>\n<p>创建备注时,我们从后端调用存储方法。打开 AddNoteActivity.kt 并修改 addNote.setOnClickListener() 方法,以便在创建备注对象后添加以下代码。<br />\nJava</p>\n<pre><code class=\"lang-\">// add this in AddNoteACtivity.kt, inside the addNote.setOnClickListener() method and after the Note() object is created.\nif (this.noteImagePath != null) {\n note.imageName = UUID.randomUUID().toString()\n //note.setImage(this.noteImage)\n note.image = this.noteImage\n\n // asynchronously store the image (and assume it will work)\n Backend.storeImage(this.noteImagePath!!, note.imageName!!)\n}\n</code></pre>\n<h6><a id=\"528_1642\"></a><strong>5.2.8加载备注时加载图像</strong></h6>\n<p>要加载图像,我们需要修改备注数据类上的静态方法。这样,每当 API 返回的 NoteData 对象转换为 Note 对象时,将同时加载图像。加载图像时,我们会通知 LiveData 的 UserData,以便观察者知晓进行的更改。这将触发用户界面刷新。<br />\n打开 UserData.kt,然后<strong>修改</strong>备注数据类的伴生对象,如下所示:<br />\nJava</p>\n<pre><code class=\"lang-\">// static function to create a Note from a NoteData API object\ncompanion object {\n fun from(noteData : NoteData) : Note {\n val result = Note(noteData.id, noteData.name, noteData.description, noteData.image)\n \n if (noteData.image != null) {\n Backend.retrieveImage(noteData.image!!) {\n result.image = it\n\n // force a UI update\n with(UserData) { notifyObserver() }\n }\n }\n return result\n }\n}\n</code></pre>\n<h6><a id=\"529_1664\"></a><strong>5.2.9删除备注时删除图像</strong></h6>\n<p>最后一步是自己清理,即,当用户删除备注时,从云存储中删除图像。如果清理不是为了节省存储空间,可以出于节省 AWS 费用的目的进行清理,因为 Amazon S3 对存储的数据按 Gb/月收费(前 5Gb 免费,运行本教程不收费)。<br />\n打开 SwipeCallback.kt,然后在 onSwipe() 方法末尾添加以下代码:<br />\nif (note?.imageName != null) {<br />\n//asynchronously delete the image (and assume it will work)<br />\nBackend.deleteImage(note.imageName!!)<br />\n}</p>\n<h6><a id=\"5210_1671\"></a><strong>5.2.10构建和测试</strong></h6>\n<p>要验证一切是否都按预期运行,请构建并运行项目。单击工具栏中的<strong>运行</strong>图标 ▶️,或按 ^ <strong>R</strong>。应该不会出现错误。<br />\n假设您仍在登录中,应用程序将从未从上一部分删除的备注列表开始。再次使用“Add Note”按钮创建备注。这一次,添加从本地图像存储中选择的图片。<br />\n退出应用程序并重新启动,以验证图像是否正确加载。</p>\n<h3><a id=\"_1675\"></a><strong>总结</strong></h3>\n<p>到这里,你已经使用 Amazon Amplify 构建了 Android 应用程序! 并且在应用程序中添加了身份验证,使用户可以注册、登录和管理其账户。该应用程序还使用 Amazon DynamoDB 数据库配置了一个可扩展的 GraphQL API,使用户可以创建和删除备注。您还使用 Amazon S3 添加了文件存储,从而使用户可以上传图像并在其应用程序中查看它们。</p>\n<p><img src=\"https://dev-media.amazoncloud.cn/b34291378370439bb9c9ff5a52855d32_image.png\" alt=\"image.png\" /></p>\n<p>通过本文相信你对从零使用 Amazon Amplify 创建简单的 Android 应用程序有了一个完整的认识,并且完成了入门。</p>\n<h3><a id=\"_1681\"></a><strong>最后想说的是</strong></h3>\n<p>最后想要说的是,一门新技术,新框架,新的解决方案来临的时候努力去学习它,并从中发现机会。本次使用该技术也算是一个新的体验,总体来说,优势很明显,开发速度很快,只要你具有一定的Android基础,那么你就可以很快的利用该产品制作属于自己的一个应用程序。总之,不断的把自己的舒适圈扩大,扩大,再扩大,主动学习和挑战新的东西 。<br />\n最后,很感谢能够阅读到这里的读者。如果看完觉得好的话,还请轻轻点一下赞或者分享给更多的人,你们的鼓励就是作者继续行文的动力。<br />\n亚马逊云科技专为开发者们打造了多种学习平台:</p>\n<ol>\n<li>入门资源中心:从0到1 轻松上手云服务,内容涵盖:成本管理,上手训练,开发资源。<a href=\"https://aws.amazon.com/cn/getting-started/?nc1=h_ls&trk=32540c74-46f0-46dc-940d-621a1efeedd0&sc_channel=el\" target=\"_blank\">https://aws.amazon.com/cn/getting-started/?nc1=h_ls&trk=32540c74-46f0-46dc-940d-621a1efeedd0&sc_channel=el</a></li>\n<li>架构中心:亚马逊云科技架构中心提供了云平台参考架构图表、经过审查的架构解决方案、Well-Architected 最佳实践、模式、图标等。<a href=\"https://aws.amazon.com/cn/architecture/?intClick=dev-center-2021_main&trk=3fa608de-d954-4355-a20a-324daa58bbeb&sc_channel=e\" target=\"_blank\">https://aws.amazon.com/cn/architecture/?intClick=dev-center-2021_main&trk=3fa608de-d954-4355-a20a-324daa58bbeb&sc_channel=el</a></li>\n<li>构建者库:了解亚马逊云科技如何构建和运营软件。<a href=\"https://aws.amazon.com/cn/builders-library/?cards-body.sort-by=item.additionalFields.sortDate&cards-body.sort-order=desc&awsf.filter-content-category=*all&awsf.filter-content-type=*all&awsf.filter-content-level=*all&trk=835e6894-d909-4691-aee1-3831428c04bd&sc_channel=el\" target=\"_blank\">https://aws.amazon.com/cn/builders-library/?cards-body.sort-by=item.additionalFields.sortDate&cards-body.sort-order=desc&awsf.filter-content-category=*all&awsf.filter-content-type=*all&awsf.filter-content-level=*all&trk=835e6894-d909-4691-aee1-3831428c04bd&sc_channel=el</a></li>\n<li>用于在亚马逊云科技平台上开发和管理应用程序的工具包:<a href=\"https://aws.amazon.com/cn/tools/?intClick=dev-center-2021_main&trk=972c69e1-55ec-43af-a503-d458708bb645&sc_channel=el\" target=\"_blank\">https://aws.amazon.com/cn/tools/?intClick=dev-center-2021_main&trk=972c69e1-55ec-43af-a503-d458708bb645&sc_channel=el</a><br />\n最后也给大家带来了专属福利</li>\n</ol>\n<h3><a id=\"_1690\"></a><strong>【专属福利】</strong></h3>\n<p>福利一:100余种产品免费套餐。其中,计算资源 Amazon EC2首年12个月免费,750小时/月;存储资源 Amazon S3 首年12个月免费,5GB标准存储容量。<br />\n<a href=\"https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el\" target=\"_blank\">https://aws.amazon.com/cn/free/?nc2=h_ql_pr_ft&all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all&trk=e0213267-9c8c-4534-bf9b-ecb1c06e4ac6&sc_channel=el</a></p>\n"}