项目结构
app/
├── src/
│ ├── main/
│ │ ├── java/com/example/login/
│ │ │ ├── data/
│ │ │ │ ├── UserRepository.kt # 数据仓库
│ │ │ │ └── model/User.kt # 数据模型
│ │ │ ├── ui/
│ │ │ │ ├── LoginActivity.kt # View 层
│ │ │ │ └── LoginViewModel.kt # ViewModel 层
│ │ │ └── di/ # 依赖注入(可选)
│ │ └── res/
│ │ └── layout/activity_login.xml
1. Model 层:数据模型与仓库
User.kt
- 用户数据模型
data class User(
val username: String,
val password: String
)
UserRepository.kt
- 数据仓库(模拟登录逻辑)
class UserRepository {
// 模拟用户数据(实际项目中可能从网络/数据库获取)
private val validUser = User("user@example.com", "123456")
suspend fun login(username: String, password: String): Boolean {
delay(1000) // 模拟网络请求延迟
return username == validUser.username && password == validUser.password
}
}
2. ViewModel 层:业务逻辑与状态管理
LoginViewModel.kt
- 处理登录逻辑
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class LoginViewModel(private val repository: UserRepository) : ViewModel() {
// UI 状态(使用密封类封装)
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val user: User) : LoginState()
data class Error(val message: String) : LoginState()
}
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState
fun login(username: String, password: String) {
if (username.isEmpty() || password.isEmpty()) {
_loginState.value = LoginState.Error("用户名或密码不能为空")
return
}
_loginState.value = LoginState.Loading
viewModelScope.launch {
try {
val success = repository.login(username, password)
if (success) {
_loginState.value = LoginState.Success(User(username, password))
} else {
_loginState.value = LoginState.Error("用户名或密码错误")
}
} catch (e: Exception) {
_loginState.value = LoginState.Error("网络请求失败: ${e.message}")
}
}
}
}
关键设计
- 状态封装:使用
Sealed Class
明确区分不同 UI 状态(加载、成功、错误)。 - 协程作用域:通过
viewModelScope
自动取消后台任务,避免内存泄漏。 - 单向数据流:
StateFlow
保证状态更新的一致性和可观察性。
3. View 层:UI 控制与数据绑定
activity_login.xml
- 布局文件(使用 Data Binding)
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.example.login.ui.LoginViewModel" />
</data>
<LinearLayout>
<EditText
android:id="@+id/et_username"
android:hint="用户名"
android:text="@={viewModel.username}" /> <!-- 双向绑定 -->
<EditText
android:id="@+id/et_password"
android:inputType="textPassword"
android:hint="密码"
android:text="@={viewModel.password}" />
<Button
android:text="登录"
android:onClick="@{() -> viewModel.login()}" /> <!-- 绑定点击事件 -->
<ProgressBar
android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />
<TextView
android:text="@{viewModel.errorMessage}" /> <!-- 显示错误信息 -->
</LinearLayout>
</layout>
LoginActivity.kt
- Activity 实现
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels {
LoginViewModelFactory(UserRepository())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
binding.viewModel = viewModel
binding.lifecycleOwner = this // 启用生命周期感知
// 监听登录状态变化
viewModel.loginState.onEach { state ->
when (state) {
is LoginViewModel.LoginState.Success ->
startActivity(Intent(this, MainActivity::class.java))
is LoginViewModel.LoginState.Error ->
Toast.makeText(this, state.message, Toast.LENGTH_SHORT).show()
else -> {} // 忽略 Loading/Idle
}
}.launchIn(lifecycleScope) // 使用协程收集状态
}
}
关键功能
- Data Binding:减少
findViewById
和手动更新 UI 的样板代码。 - 生命周期感知:通过
lifecycleOwner
确保数据绑定仅在活跃状态下更新。 - 状态驱动 UI:根据
loginState
自动切换界面(如显示加载条、错误提示)。
总结
组件 | 职责 | 技术实现 |
---|---|---|
Model | 数据获取与验证 | Repository + 协程 |
ViewModel | 状态管理、逻辑处理 | StateFlow + Sealed Class |
View | 数据绑定、事件转发 | Data Binding + Lifecycle |