1. 实现与服务器交互平台
1.1 Google 平台的 Firebase (需要科学网络)
Firebasehttps://firebase.google.cn/
1.2 LeanCloud 平台
LeanCloudhttps://www.leancloud.cn/
2. 配置信息
2.1 在 LeanCloud 控制台创建应用, 根据 SDK下载 开发指南配置应用
2.2 配置文件 build.gradle 添加库
//视图绑定
buildFeatures {
viewBinding = true
}
//使用存储功能
implementation 'cn.leancloud:storage-android:8.2.5'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
//使用即时通讯 / 推送服务
implementation 'cn.leancloud:realtime-android:8.2.5'
2.3 实现 Application, MyApplication.kt
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
LeanCloud.initialize(this, "bl8p52gwz7pVkOhMg3unRaT4-gzGzoHsz", "dL6QC52vonm41TegX4i5jB7S", "https://bl8p52gw.lc-cn-n1-shared.com")
}
}
2.4 配置文件 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 基本模块(必须)START -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LeanDemo"
tools:targetApi="31">
<activity
android:name=".SignUpActivity"
android:exported="false">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity android:name=".MainActivity" />
<!-- 即时通讯和推送 START -->
<!-- 即时通讯和推送都需要 PushService -->
<service android:name="cn.leancloud.push.PushService" />
<receiver
android:name="cn.leancloud.push.LCBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_PRESENT" />
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
</intent-filter>
</receiver>
</application>
</manifest>
2.5 退出菜单布局 menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuLogout"
android:title="退出" />
</menu>
3. 登录页面实现
3.1 布局文件 activity_login.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=".LoginActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<Button
android:id="@+id/buttonSignUp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="注册"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout2" />
<Button
android:id="@+id/buttonLogin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:enabled="false"
android:text="登录"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout2" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonSignUp" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.2 实现事件, LoginActivity.kt
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.editTextUsername.addTextChangedListener(watcher)
binding.editTextPassword.addTextChangedListener(watcher)
//注册
binding.buttonSignUp.setOnClickListener {
startActivity(Intent(this, SignUpActivity::class.java))
}
//登录
binding.buttonLogin.setOnClickListener {
login()
}
}
//登录
private fun login() {
binding.progressBar.visibility = View.VISIBLE
val name = binding.editTextUsername.text?.trim().toString()
val pwd = binding.editTextPassword.text?.trim().toString()
LCUser.logIn(name, pwd).subscribe(object : Observer<LCUser> {
override fun onSubscribe(d: Disposable) {}
//登录成功
override fun onNext(t: LCUser) {
binding.progressBar.visibility = View.INVISIBLE
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
}
//登录失败
override fun onError(e: Throwable) {
Toast.makeText(this@LoginActivity, "${e.message}", Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.INVISIBLE
}
override fun onComplete() {}
})
}
//监听 EditText
private val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val t1 = binding.editTextUsername.text.toString().isNotEmpty()
val t2 = binding.editTextPassword.text.toString().isNotEmpty()
binding.buttonLogin.isEnabled = t1 and t2
}
override fun afterTextChanged(s: Editable?) {}
}
}
4. 注册页面实现
4.1 布局文件 activity_sign_up.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=".SignUpActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout3">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/buttonSignUpConfirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:enabled="false"
android:text="注册并登录"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout4" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonSignUpConfirm" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.2 实现事件 SignUpActivity.kt
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.editTextUsername.addTextChangedListener(watcher)
binding.editTextPassword.addTextChangedListener(watcher)
//注册按钮
binding.buttonSignUpConfirm.setOnClickListener {
signUp()
}
}
//注册
private fun signUp() {
binding.progressBar.visibility = View.VISIBLE
val name = binding.editTextUsername.text?.trim().toString()
val pwd = binding.editTextPassword.text?.trim().toString()
LCUser().apply {
username = name
password = pwd
signUpInBackground().subscribe(object : Observer<LCUser> {
override fun onSubscribe(d: Disposable) {}
//注册成功
override fun onNext(t: LCUser) {
Toast.makeText(this@SignUpActivity, "注册成功", Toast.LENGTH_SHORT).show()
LCUser.logIn(name, pwd).subscribe(object : Observer<LCUser> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: LCUser) {
binding.progressBar.visibility = View.INVISIBLE
startActivity(Intent(this@SignUpActivity, MainActivity::class.java).also {
it.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
})
//finish()
}
override fun onError(e: Throwable) {
binding.progressBar.visibility = View.INVISIBLE
Toast.makeText(this@SignUpActivity, "${e.message}", Toast.LENGTH_SHORT)
.show()
}
override fun onComplete() {}
})
}
//注册失败
override fun onError(e: Throwable) {
binding.progressBar.visibility = View.INVISIBLE
Toast.makeText(this@SignUpActivity, "${e.message}", Toast.LENGTH_SHORT).show()
}
//注册完成
override fun onComplete() {}
})
}
}
//监听 EditText
private val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val t1 = binding.editTextUsername.text.toString().isNotEmpty()
val t2 = binding.editTextPassword.text.toString().isNotEmpty()
binding.buttonSignUpConfirm.isEnabled = t1 and t2
}
override fun afterTextChanged(s: Editable?) {}
}
}
5. 主页实现
5.1 添加词汇布局, new_word_dialog.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="match_parent">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:hint="输入单词"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.2 适配器布局 view_holder.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="60dp">
<TextView
android:id="@+id/textViewContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="TextView"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textViewTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:text="TextView"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.3 实现 Adapter 及 ViewHolder, MyAdapter.kt
class MyAdapter : Adapter<ViewHolder>() {
private var _dataList = listOf<LCObject>()
fun updateDataList(newList: List<LCObject>) {
_dataList = newList
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.view_holder, parent, false)
return object : ViewHolder(v) {}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val obj = _dataList[position]
holder.itemView.findViewById<TextView>(R.id.textViewContent).text =
obj.get("word").toString()
holder.itemView.findViewById<TextView>(R.id.textViewTime).text =
obj.get("createdAt").toString()
}
override fun getItemCount(): Int = _dataList.size
}
5.4 创建 ViewModel, MyViewModel.kt
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val _dataListLive = MutableLiveData<List<LCObject>>()
val dataListLive: LiveData<List<LCObject>> = _dataListLive
init {
val query = LCQuery<LCObject>("Word")
query.whereEqualTo("user", LCUser.getCurrentUser())
query.findInBackground().subscribe(object : Observer<List<LCObject>> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: List<LCObject>) {
_dataListLive.value = t
}
override fun onError(e: Throwable) {
Toast.makeText(application, "${e.message}", Toast.LENGTH_SHORT).show()
}
override fun onComplete() {}
})
//数据推送及同步
val liveQuery = LCLiveQuery.initWithQuery(query)
liveQuery.subscribeInBackground(object : LCLiveQuerySubscribeCallback() {
//订阅成功
override fun done(e: LCException?) {}
})
//推送回调
liveQuery.setEventHandler(object : LCLiveQueryEventHandler() {
//添加
override fun onObjectCreated(LCObject: LCObject?) {
super.onObjectCreated(LCObject)
val t = _dataListLive.value?.toMutableList()
t?.add(LCObject!!)
_dataListLive.value = t
}
//删除
override fun onObjectDeleted(objectId: String?) {
super.onObjectDeleted(objectId)
val t = _dataListLive.value?.toMutableList()
val obj = t?.find {
it.get("objectId") == objectId
}
t?.remove(obj)
_dataListLive.value = t
}
//更新
override fun onObjectUpdated(LCObject: LCObject?, updateKeyList: MutableList<String>?) {
super.onObjectUpdated(LCObject, updateKeyList)
val obj = _dataListLive.value?.find {
it.get("objectId") == LCObject?.get("objectId")
}
updateKeyList!!.forEach {
obj?.put(it, LCObject?.get(it))
}
_dataListLive.value = _dataListLive.value
}
})
}
//添加表
fun addWord(newWord: String) {
LCObject("Word").apply {
put("word", newWord)
put("user", LCUser.getCurrentUser())
saveInBackground().subscribe(object : Observer<LCObject> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: LCObject) {
Toast.makeText(getApplication(), "添加成功", Toast.LENGTH_SHORT).show()
}
override fun onError(e: Throwable) {
Toast.makeText(getApplication(), "${e.message}", Toast.LENGTH_SHORT).show()
}
override fun onComplete() {}
})
}
}
}
5.5 主页布局 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/recyclerView"
android:layout_width="409dp"
android:layout_height="729dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.915"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.976"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.6 实现事件,MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))[MyViewModel::class.java]
val myAdapter = MyAdapter()
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
addItemDecoration(DividerItemDecoration(this@MainActivity, DividerItemDecoration.VERTICAL))
adapter = myAdapter
}
viewModel.dataListLive.observe(this) {
myAdapter.updateDataList(it)
myAdapter.notifyDataSetChanged()
}
binding.floatingActionButton.setOnClickListener {
addNewWord()
}
}
//添加单词
private fun addNewWord() {
val view = LayoutInflater.from(this).inflate(R.layout.new_word_dialog, null)
AlertDialog.Builder(this)
.setTitle("添加")
.setView(view)
.setPositiveButton("确定") { _, _ ->
val newWord = view.findViewById<EditText>(R.id.editText).text.trim().toString()
viewModel.addWord(newWord)
}
.setNegativeButton("取消") { dialog, _ ->
dialog.dismiss()
}
.show()
}
//退出
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuLogout) {
LCUser.logOut()
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
return super.onOptionsItemSelected(item)
}
//菜单项
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return super.onCreateOptionsMenu(menu)
}
}
6. 效果图