任务四 调用登录接口
一、任务目标
将通过校验的用户名和密码提交给登录接口,请求期间显示加载提示;收到结果后关闭提示,并根据服务器返回的信息显示登录成功或失败。
二、配置网络请求环境
本任务需要三个库:OkHttp 负责发送请求,Gson 负责读取服务器返回的 JSON 数据,Lottie 负责播放加载动画。
1. 添加依赖
打开 app/build.gradle.kts,在 dependencies 中加入三个依赖:
dependencies { // ... 省略项目中已有的依赖。
implementation("com.google.code.gson:gson:2.13.2") implementation("com.squareup.okhttp3:okhttp:5.2.0") implementation("com.airbnb.android:lottie:6.6.10")}初始工程已在 AndroidManifest.xml 中声明联网权限,这里只需添加依赖。
2. 同步项目
点击 Android Studio 工具栏中的 Sync Project with Gradle Files,等待依赖下载完成。
三、查看登录接口信息
登录页向服务器提交账号信息,接口要求如下:
| 项目 | 内容 |
|---|---|
| 请求方式 | POST |
| 请求地址 | /order-account/login |
| 请求体格式 | application/x-www-form-urlencoded |
| 请求参数 | name:用户名;password:密码 |
登录成功时,服务器返回:
{ "data": { "token": "登录凭证" }, "code": 200, "msg": "登录成功"}code 表示处理结果,msg 是提示文字,data.token 是后续请求使用的登录凭证。登录失败时,msg 会说明原因。
四、创建登录响应模型
先创建接收接口结果的 Response<T>,再创建保存登录凭证的 LoginData。
1. 创建通用响应类
在 com.android.practice.food 包下创建 data.model 包,再在其中创建 Response.kt,写入以下完整内容:
package com.android.practice.food.data.model
import com.google.gson.annotations.SerializedName
class Response<T>( @SerializedName("code") val code: Int = 0, @SerializedName("msg") val msg: String = "", // 登录失败时,服务器可能不返回 data。 @SerializedName("data") val data: T? = null) { val success: Boolean get() = code == CODE_SUCCESS
companion object { // 1 表示请求异常;200 表示服务器处理成功。 const val CODE_EXCEPTION = 1 const val CODE_SUCCESS = 200 }}类名后的括号直接定义三个字段。请求异常时,只需传入 code 和 msg,data 使用默认值 null。@SerializedName 对应 JSON 字段,success 检查 code 是否为 200。
2. 创建登录数据类
在同一包中创建 LoginData.kt,写入以下完整内容:
package com.android.practice.food.data.model
import com.google.gson.annotations.SerializedName
class LoginData {
@SerializedName("token") val token: String = ""}登录成功时,LoginData.token 用来接收服务器返回的凭证。
3. 运行验证
在 Android Studio 中构建并运行应用,确认能够进入登录页。若构建时报 Response 或 LoginData 错误,先检查文件路径和导入语句。
五、实现登录接口
把发送请求和读取响应的代码放在 FoodApiClient 中。登录页只需传入用户名、密码,并根据返回结果更新提示。
1. 创建网络请求类
在 com.android.practice.food.core 包下创建 network 包,再在其中创建 FoodApiClient.kt,写入以下完整内容:
package com.android.practice.food.core.network
import com.google.gson.Gsonimport okhttp3.OkHttpClient
object FoodApiClient {
private const val SERVER_HOST = "http://服务器地址:端口"
private val httpClient by lazy { OkHttpClient() } private val gson by lazy { Gson() }}2. 添加登录方法
在 FoodApiClient.kt 中导入模型和请求类,并在 gson 属性后添加 login():
import com.android.practice.food.data.model.LoginDataimport com.android.practice.food.data.model.Responseimport com.google.gson.reflect.TypeTokenimport okhttp3.FormBodyimport okhttp3.Request
object FoodApiClient { // ... 省略服务器地址、OkHttp 客户端和 Gson 对象。
fun login(username: String, password: String): Response<LoginData> { val request = Request.Builder() .url("$SERVER_HOST/order-account/login") .post( FormBody.Builder() .add("name", username) .add("password", password) .build() ) .build()
return try { // use 会在读取结束后关闭本次响应。 httpClient.newCall(request).execute().use { response -> val json = response.body.string() gson.fromJson( json, object : TypeToken<Response<LoginData>>() {}.type ) } } catch (throwable: Throwable) { // 将请求错误转为统一的失败结果,交给页面处理。 Response(Response.CODE_EXCEPTION, throwable.message.orEmpty()) } }}Request.Builder() 组合接口地址和请求数据,FormBody 按表单格式提交 name 和 password。execute() 发送请求,response.body.string() 读取服务器返回的 JSON。Gson 再把它转换为 Response<LoginData>;TypeToken 用来告诉 Gson:data 中应当是 LoginData。
如果请求或读取数据时出错,catch 会返回失败结果,登录页仍能显示错误提示。
3. 运行验证
构建并运行应用,确认能够进入登录页。此时点击“登录”仍不会发送请求;下一节先做好等待时的提示,再调用 login()。
六、显示登录加载状态
服务器返回结果需要一点时间。等待时,登录页会显示“登录中…”的加载对话框,并暂时挡住页面,避免重复点击“登录”。
1. 创建加载对话框布局
在 app/src/main/res/layout 中创建 dialog_loading.xml,写入以下完整内容:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/black_50" android:gravity="center" android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView android:layout_width="150dp" android:layout_height="80dp" android:layout_gravity="center_horizontal" app:lottie_autoPlay="true" app:lottie_fileName="lottie_loading.json" app:lottie_loop="true" />
<TextView android:id="@+id/loading_message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="6dp" android:textColor="@color/white" android:textSize="16sp" /></LinearLayout>布局上方显示加载动画,下方显示提示文字。
2. 创建加载对话框类
在 com.android.practice.food.core.ui 包下创建 component.dialog 包,再在其中创建 LoadingDialog.kt,写入以下完整内容:
package com.android.practice.food.core.ui.component.dialog
import android.content.Contextimport android.graphics.Colorimport android.os.Bundleimport android.view.WindowManagerimport android.widget.TextViewimport androidx.appcompat.app.AlertDialogimport androidx.core.graphics.drawable.toDrawableimport com.android.practice.food.R
class LoadingDialog( context: Context, private val message: String) : AlertDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.dialog_loading) // 等待请求时,不允许用返回键或点击空白处关闭提示。 setCancelable(false) findViewById<TextView>(R.id.loading_message)?.text = message }
override fun onStart() { super.onStart() // 去掉系统默认遮罩和窗口背景,只显示自定义加载布局。 window?.let { val lp = it.attributes lp.dimAmount = 0f it.attributes = lp it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) } }}setContentView() 显示刚创建的布局,findViewById() 把“登录中…”写到提示文字控件中。setCancelable(false) 防止等待时关闭对话框。onStart() 中的设置去掉系统默认的深色遮罩和窗口背景,只显示自定义加载内容。
3. 临时显示加载对话框
打开 LoginActivity.kt,导入 LoadingDialog。在 handleLogin() 的三项校验之后,删掉任务三中显示“登录中…”的临时 Toast,改为显示加载对话框。先让它在 1.5 秒后自动关闭,以便单独检查效果。
import com.android.practice.food.core.ui.component.dialog.LoadingDialog
class LoginActivity : BaseActivity() { // ... 省略成员变量、onCreate() 和 initViews()。
private fun handleLogin() { // ... 省略已经完成的用户名、密码和用户协议校验。 Toast.makeText( this, R.string.message_logging, Toast.LENGTH_SHORT ).show()
val dialog = LoadingDialog(this, getString(R.string.message_logging)) dialog.show() // 仅用于检查加载效果;接入请求后删除这行延时关闭代码。 binding.root.postDelayed({ dialog.dismiss() }, 1500) }}postDelayed() 会在 1.5 秒后关闭对话框。下一节接入请求时,将删掉这行临时代码,改为收到结果后关闭。
4. 运行验证
填写用户名和密码,勾选协议,点击“登录”。页面中央应出现加载动画和“登录中…”,约 1.5 秒后消失。
七、调用登录接口
加载提示已经可以显示。现在接入登录请求:点击“登录”后显示对话框,请求结束后再关闭它,并提示服务器返回的结果。网络请求要放到后台执行,避免页面在等待时无法响应操作。
1. 发起登录请求
打开 LoginActivity.kt,导入请求类和协程相关类。在 handleLogin() 中保留创建、显示对话框的两行代码,删除上一节的 postDelayed(),接着加入请求和结果处理:
import androidx.lifecycle.lifecycleScopeimport com.android.practice.food.core.network.FoodApiClientimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContext
class LoginActivity : BaseActivity() { // ... 省略成员变量、onCreate() 和 initViews()。
private fun handleLogin() { // ... 省略用户名、密码和用户协议校验。 // username 和 password 已在前面的校验代码中完成定义。
val dialog = LoadingDialog(this, getString(R.string.message_logging)) dialog.show()
binding.root.postDelayed({ dialog.dismiss() }, 1500)
lifecycleScope.launch { // 网络请求放到后台,避免等待服务器时页面卡住。 val response = withContext(Dispatchers.IO) { FoodApiClient.login(username, password) } dialog.dismiss()
// 业务成功且拿到 token,才继续按登录成功处理。 val token = response.data?.token if (!response.success || token.isNullOrEmpty()) { Toast.makeText( this@LoginActivity, response.msg, Toast.LENGTH_SHORT ).show() return@launch }
Toast.makeText( this@LoginActivity, response.msg, Toast.LENGTH_SHORT ).show() } }}lifecycleScope 可以看作登录页的任务管理器。点击“登录”后,launch 把大括号中的处理交给它;登录页关闭时,它会取消尚未完成的处理。withContext(Dispatchers.IO) 把网络请求放到后台,等待服务器时页面不会卡住。收到结果后,再关闭加载对话框并显示提示。
如果服务器表示失败,或者没有返回 token,就显示 msg,并用 return@launch 停止本次处理。否则显示成功提示。此时先不跳转页面;任务五会用 token 获取用户信息。不要把完整 token 显示在页面或日志中。
2. 验证登录失败结果
在用户名输入框中填写 sjse,在密码输入框中填写错误密码 000000,勾选用户协议,然后点击“登录”按钮。
加载对话框关闭后,页面应显示服务端返回的“密码错误”,并停留在登录页。用户可以修改密码后再次提交。
3. 验证登录成功结果
在用户名输入框中填写 sjse,在密码输入框中填写 123456,勾选用户协议,然后点击“登录”按钮。
加载对话框关闭后,页面应显示“登录成功”。本任务尚未处理页面跳转,因此应用仍停留在登录页。
关闭设备网络后再次提交登录请求,加载对话框应在请求结束后关闭,应用显示网络异常信息且不会退出。完成检查后重新打开设备网络。
任务小结
本任务完成了登录请求:提交账号信息,等待服务器返回结果,并根据 code 和 token 提示登录成功或失败。请求期间,页面显示加载对话框。