Appwrite、Android 和实时
🤔 Appwrite是什么?
Appwrite 是一个全新的开源端到端后端服务器,面向 Web 和移动开发者,可显著提升应用开发速度。它将常见的开发任务抽象化并简化到 REST API 和工具背后,帮助您更快地构建高级应用。
🤖 Appwrite安卓版
Appwrite为 Web 和移动应用开发所需的各种常用功能提供了简洁的 REST API,例如云函数、数据库、存储,以及对每项服务的实时支持,使开发者能够专注于应用本身,而不是后端实现。这使得 Appwrite 非常适合想要构建 Android 应用的开发者。在本教程中,我们将使用 Appwrite 和新的实时API 构建一个 Android 实时产品应用,让我们开始吧。
📝 先决条件
要继续学习本教程,您需要访问 Appwrite 控制台,并且需要一个现有项目或拥有创建新项目的权限。如果您尚未安装 Appwrite,请先安装。按照 Appwrite 的官方安装文档进行安装非常简单,大约只需 2 分钟。安装完成后,登录您的控制台并创建一个新项目。
💾 数据库设置
在 Appwrite 控制台中,我们选择要用于 Android 应用的项目。如果您还没有项目,可以点击“创建项目”按钮轻松创建一个。进入项目后,从左侧边栏选择“数据库”。在数据库页面:
- 点击“添加收藏”按钮
- 对话内容:
- 将集合名称设置为“产品”
- 点击“创建”按钮
这将创建一个集合,您将被重定向到新集合的页面,我们可以在该页面定义其规则。定义以下规则,然后单击“更新”按钮。
- 姓名
- 标签:名称
- 键:名称
- 规则类型:文本
- 必填:是
- 数组:false
- 描述
- 标签:描述
- 图例:描述
- 规则类型:文本
- 必填:是
- 数组:false
- SKU
- 标签:SKU
- 图例:sku
- 规则类型:文本
- 必填:是
- 数组:false
- 价格
- 标签:价格
- 图例:价格
- 规则类型:数值型
- 必填:是
- 数组:false
- 图片网址
- 标签:图片网址
- 键:imageUrl
- 规则类型:文本
- 必填:是
- 数组:false
现在集合已经创建完成,我们可以开始设置 Android 应用程序了。
⚙️ 设置 Android 项目和依赖项
使用 Android Studio 创建一个新的 Android 应用程序项目,并选择“空 Activity”模板。项目创建完成后,将以下依赖项添加到应用程序的build.gradle(.kts)文件中:
// Appwrite
implementation("io.appwrite:sdk-for-android:0.2.0")
// Appcompat, LiveData, ViewModel and Activity extensions
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.activity:activity-ktx:1.3.1'
// JSON
implementation 'com.google.code.gson:gson:2.8.7'
// Image loading
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
➕️ 添加 Android 平台
要初始化 Appwrite SDK 并开始与 Appwrite 服务交互,您首先需要向项目中添加一个新的 Android 平台。要添加新平台,请转到 Appwrite 控制台,选择您的项目(如果还没有项目,请创建一个),然后单击项目控制面板上的“添加平台”按钮。
从选项中选择添加新的 Android 平台并添加您的应用凭据。
添加您的应用名称和包名。包名通常位于applicationId应用级别的build.gradle文件中。您也可以在配置文件中找到包名AndroidManifest.xml。注册新平台后,您即允许您的应用与 Appwrite API 通信。
🧱 创建产品模型
现在我们需要创建一个模型来表示 Android 端的产品。创建一个新的 Kotlin 文件Product.kt并声明一个简单的数据类:
data class Product(
val name: String,
val sku: String,
val price: Double,
val imageUrl: String
)
⚒️ 建筑景观
现在,打开您的文件app/src/main/res/layout/activity_main.xml并按如下方式更新布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerProducts"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/btnSubscribe"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="3"/>
<Button
android:id="@+id/btnSubscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Subscribe to products"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerProducts"/>
</androidx.constraintlayout.widget.ConstraintLayout>
你会注意到这个活动非常简单;我们只需要Button订阅一个应用和一个产品列表RecyclerView。产品列表RecyclerView将用于在我们添加新产品时实时显示产品集合。现在我们需要定义一个单独的视图来表示每个单独的产品。
从 创建一个新布局File > New > Layout Resource File,命名item_product.xml并添加以下内容:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="2dp">
<ImageView
android:id="@+id/imgProduct"
android:layout_width="match_parent"
android:layout_height="120dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<View
android:layout_width="match_parent"
android:layout_height="34dp"
android:alpha="0.6"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/txtName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/txtPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
我们所有的部件RecyclerView都已到位,接下来我们将着手进行ViewModel大部分繁重的工作。
👩🔧 创建视图模型
使用以下代码创建app/src/main/java/com/example/realtimestarter/RealtimeViewModel.kt并更新该文件。请务必将文件顶部附近的所有属性值替换为您自己的值,这些值可以在 AppWrite 控制台中找到。
package io.appwrite.realtimestarter
import android.content.Context
import android.util.Log
import androidx.lifecycle.*
import io.appwrite.Client
import io.appwrite.extensions.toJson
import io.appwrite.models.RealtimeResponseEvent
import io.appwrite.models.RealtimeSubscription
import io.appwrite.services.Account
import io.appwrite.services.Database
import io.appwrite.services.Realtime
import kotlinx.coroutines.launch
class RealtimeViewModel : ViewModel(), LifecycleObserver {
private val endpoint = "YOUR_ENDPOINT" // Replace with your endpoint
private val projectId = "YOUR_PROJECT_ID" // Replace with your project ID
private val collectionId = "YOUR_COLLECTION_ID" // Replace with your product collection ID
private val realtime by lazy { Realtime(client!!) }
private val account by lazy { Account(client!!) }
private val db by lazy { Database(client!!) }
private val _productStream = MutableLiveData<Product>()
val productStream: LiveData<Product> = _productStream
private val _productDeleted = MutableLiveData<Product>()
val productDeleted: LiveData<Product> = _productDeleted
private var client: Client? = null
var subscription: RealtimeSubscription? = null
private set
fun subscribeToProducts(context: Context) {
buildClient(context)
viewModelScope.launch {
// Create a session so that we are authorized for realtime
createSession()
// Attach an error logger to our realtime instance
realtime.doOnError { Log.e(this::class.java.name, it.message.toString()) }
// Subscribe to document events for our collection and attach the handle product callback
subscription = realtime.subscribe(
"collections.${collectionId}.documents",
payloadType = Product::class.java,
callback = ::handleProductMessage
)
//createDummyProducts()
}
}
private fun handleProductMessage(message: RealtimeResponseEvent<Product>) {
when (message.event) {
in
"database.documents.create",
"database.documents.update" -> {
_productStream.postValue(message.payload!!)
}
"database.documents.delete" -> {
_productDeleted.postValue(message.payload!!)
}
}
}
private suspend fun createDummyProducts() {
// For testing; insert 100 products while subscribed
val url = "https://dummyimage.com/600x400/cde/fff"
for (i in 1 until 100) {
db.createDocument(
collectionId,
Product("iPhone $i", "sku-$i", i.toDouble(), url).toJson(),
listOf("*"),
listOf("*")
)
}
}
private fun buildClient(context: Context) {
if (client == null) {
client = Client(context)
.setEndpoint(endpoint)
.setProject(projectId)
}
}
private suspend fun createSession() {
try {
account.createAnonymousSession()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun closeSocket() {
// Activity is being destroyed; close our socket connection if it's open
subscription?.close()
}
}
该ViewModel函数用于调用实时 API 并订阅与集合中 id 为 的文档相关的创建/更新/删除事件的通知collectionId,该集合对我们的用户也是可见的。
为了允许观察来自外部的实时更新,它还ViewModel公开了该LiveData属性productStream,我们Activity稍后将利用该属性在我们的系统中获取实时更新RecyclerView。
♻️ 回收站视图
我们还需要添加两个文件才能使其正常RecyclerView运行:
- 该组件将负责为每个添加到数据库的元素
ProductAdapter创建并绑定视图。构造函数中提供的参数将用于在后台线程中计算列表更新差异,然后将任何更改发布到 UI 线程,非常适合实时应用!您可以在这里找到更多信息。ProductDiffUtil.ItemCallbackDiffUtil
package io.appwrite.realtimestarter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
class ProductAdapter :
ListAdapter<Product, ProductViewHolder>(object : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product) =
oldItem.sku == newItem.sku
override fun areContentsTheSame(oldItem: Product, newItem: Product) =
oldItem == newItem
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_product, parent, false)
return ProductViewHolder(view)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
val item = currentList[position]
holder.setName(item.name)
holder.setPrice(item.price.toString())
holder.setProductImage(item.imageUrl)
}
fun submitNext(product: Product) {
val current = currentList.toMutableList()
val index = currentList.indexOfFirst {
it.sku == product.sku
}
if (index != -1) {
current[index] = product
} else {
current.add(product)
}
submitList(current)
}
fun submitDeleted(product: Product) {
submitList(
currentList.toMutableList().apply {
remove(product)
}
)
}
}
- 该
ProductViewHolder视图描述了单个Product视图及其在视图中的位置的元数据RecyclerView:
package io.appwrite.realtimestarter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var nameView: TextView = itemView.findViewById(R.id.txtName)
private var priceView: TextView = itemView.findViewById(R.id.txtPrice)
private var imageView: ImageView = itemView.findViewById(R.id.imgProduct)
fun setName(name: String) {
nameView.text = name
}
fun setPrice(price: String) {
priceView.text = "\$$price"
}
fun setProductImage(url: String) {
Glide.with(itemView)
.load(url)
.centerCrop()
.into(imageView)
}
}
💆 活动
一切准备就绪后,让我们把所有内容整合到我们的项目中MainActivity。打开app/src/main/java/com/example/realtimestarter/MainActivity.kt并更新如下:
package io.appwrite.realtimestarter
import android.os.Bundle
import android.widget.Button
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
class RealtimeActivity : AppCompatActivity() {
private val viewModel by viewModels<RealtimeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_realtime)
val button = findViewById<Button>(R.id.btnSubscribe)
button.setOnClickListener {
viewModel.subscribeToProducts(this)
}
val adapter = ProductAdapter()
val recycler = findViewById<RecyclerView>(R.id.recyclerProducts)
recycler.adapter = adapter
viewModel.productStream.observe(this) {
adapter.submitNext(it)
}
viewModel.productDeleted.observe(this) {
adapter.submitDeleted(it)
}
lifecycle.addObserver(viewModel)
}
override fun onStop() {
super.onStop()
lifecycle.removeObserver(viewModel)
}
}
🏎️ 让我们实时互动
好了,剩下的就是运行应用程序,然后添加一些文档。在模拟器或设备上运行时,点击“订阅”即可开始监听实时更新。
返回 Appwrite 控制台,导航至Products我们之前创建的集合。从这里,我们可以开始添加新文档,并在应用中查看它们的显示效果。
在控制台中添加产品后,您会立即在应用程序的用户界面中看到它们!这就是 Appwrite Realtime 的真正魅力所在,如下所示。
🥂 结论
希望您喜欢使用 Appwrite 和 Android 构建这款实时应用程序。该应用程序的完整源代码可在Demo Realtime Application代码库中找到。如果您有任何反馈或建议,请告诉我们。期待看到社区使用 Appwrite Realtime 和 Android 能创造出怎样的作品!


