1. Android开发第五天:从零构建完整登录模块
今天是我系统学习Android开发的第五天,决定挑战一个完整的登录功能模块开发。这个看似基础的功能实际上涵盖了Android开发的多个核心知识点,包括UI设计、数据验证、网络请求和本地存储等关键技术点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 登录模块技术架构设计
2.1 整体技术选型
采用MVVM架构模式,使用以下技术栈:
- View层:XML布局 + DataBinding
- ViewModel层:AndroidViewModel + LiveData
- Model层:Retrofit + Room
选择这种架构主要考虑三点:
- 数据驱动UI更新更高效
- 生命周期感知避免内存泄漏
- 各层职责分离便于测试维护
2.2 关键组件依赖
在app/build.gradle中添加以下依赖:
groovy复制// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
// LiveData
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Room
implementation 'androidx.room:room-runtime:2.4.3'
kapt 'androidx.room:room-compiler:2.4.3'
3. UI界面实现细节
3.1 登录页面布局
使用ConstraintLayout构建登录表单:
xml复制<EditText
android:id="@+id/etUsername"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="用户名"
android:inputType="textEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<EditText
android:id="@+id/etPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etUsername"/>
<Button
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="登录"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etPassword"/>
3.2 输入验证逻辑
在ViewModel中实现实时验证:
kotlin复制val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isLoginEnabled: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
fun validate() {
value = !username.value.isNullOrEmpty()
&& !password.value.isNullOrEmpty()
&& (password.value?.length ?: 0) >= 6
}
addSource(username) { validate() }
addSource(password) { validate() }
}
4. 网络请求实现
4.1 Retrofit接口定义
kotlin复制interface AuthService {
@POST("auth/login")
suspend fun login(
@Body request: LoginRequest
): Response<LoginResponse>
}
data class LoginRequest(
val username: String,
val password: String
)
data class LoginResponse(
val token: String,
val user: User
)
4.2 网络请求封装
kotlin复制class AuthRepository(private val service: AuthService) {
suspend fun login(username: String, password: String): Result<LoginResponse> {
return try {
val response = service.login(LoginRequest(username, password))
if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("Login failed"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
5. 数据持久化方案
5.1 Room数据库配置
kotlin复制@Entity
data class User(
@PrimaryKey val id: String,
val username: String,
val email: String
)
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Query("SELECT * FROM user LIMIT 1")
suspend fun getCurrentUser(): User?
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
5.2 Token管理
使用EncryptedSharedPreferences存储敏感信息:
kotlin复制class TokenManager(context: Context) {
private val prefs = EncryptedSharedPreferences.create(
"auth_prefs",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String) {
prefs.edit().putString("auth_token", token).apply()
}
fun getToken(): String? {
return prefs.getString("auth_token", null)
}
}
6. 完整登录流程实现
6.1 ViewModel业务逻辑
kotlin复制class LoginViewModel(
private val authRepo: AuthRepository,
private val tokenManager: TokenManager,
private val userDao: UserDao
) : AndroidViewModel() {
private val _loginState = MutableLiveData<LoginState>()
val loginState: LiveData<LoginState> = _loginState
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = LoginState.Loading
when (val result = authRepo.login(username, password)) {
is Result.Success -> {
tokenManager.saveToken(result.data.token)
userDao.insertUser(result.data.user)
_loginState.value = LoginState.Success
}
is Result.Failure -> {
_loginState.value = LoginState.Error(result.exception.message)
}
}
}
}
}
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
object Success : LoginState()
class Error(val message: String?) : LoginState()
}
6.2 Activity中的调用
kotlin复制class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel by viewModels<LoginViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
binding.lifecycleOwner = this
binding.viewModel = viewModel
setupObservers()
}
private fun setupObservers() {
viewModel.loginState.observe(this) { state ->
when (state) {
is LoginState.Loading -> showLoading()
is LoginState.Success -> navigateToHome()
is LoginState.Error -> showError(state.message)
else -> Unit
}
}
}
}
7. 安全增强措施
7.1 密码加密传输
使用SHA-256加盐哈希处理密码:
kotlin复制fun encryptPassword(password: String): String {
val salt = "fixed_salt_placeholder" // 实际项目应使用随机salt
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest("$salt$password".toByteArray())
return Base64.encodeToString(hash, Base64.NO_WRAP)
}
7.2 防暴力破解机制
在ViewModel中添加登录尝试限制:
kotlin复制private var loginAttempts = 0
fun login(username: String, password: String) {
if (loginAttempts >= 3) {
_loginState.value = LoginState.Error("尝试次数过多,请稍后再试")
return
}
loginAttempts++
// 原有登录逻辑...
}
8. 测试方案设计
8.1 单元测试用例
kotlin复制@Test
fun `login with valid credentials should return success`() = runTest {
// Given
val mockService = mock<AuthService> {
onBlocking { login(any()) } doReturn Response.success(
LoginResponse("token", User("1", "test", "test@test.com"))
)
}
val repo = AuthRepository(mockService)
// When
val result = repo.login("valid", "valid")
// Then
assertTrue(result.isSuccess)
}
@Test
fun `login with invalid credentials should return failure`() = runTest {
// Given
val mockService = mock<AuthService> {
onBlocking { login(any()) } doReturn Response.error(400, "".toResponseBody())
}
val repo = AuthRepository(mockService)
// When
val result = repo.login("invalid", "invalid")
// Then
assertTrue(result.isFailure)
}
8.2 UI自动化测试
使用Espresso编写界面测试:
kotlin复制@Test
fun loginButton_shouldBeDisabledWhenFieldsEmpty() {
// Given
val scenario = launchFragmentInContainer<LoginFragment>()
// When
onView(withId(R.id.etUsername)).perform(typeText(""))
onView(withId(R.id.etPassword)).perform(typeText(""))
// Then
onView(withId(R.id.btnLogin)).check(matches(not(isEnabled())))
}
9. 性能优化要点
9.1 网络请求缓存
配置OkHttp拦截器实现缓存:
kotlin复制val cacheSize = 10 * 1024 * 1024 // 10MB
val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize.toLong())
val client = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addNetworkInterceptor(CacheInterceptor())
.cache(cache)
.build()
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
return response.newBuilder()
.header("Cache-Control", "public, max-age=60")
.build()
}
}
9.2 数据库查询优化
使用Room的索引提升查询效率:
kotlin复制@Entity(indices = [Index(value = ["username"], unique = true)])
data class User(
@PrimaryKey val id: String,
val username: String,
val email: String
)
10. 常见问题解决方案
10.1 内存泄漏预防
使用LifecycleObserver管理资源:
kotlin复制class LocationObserver(
private val lifecycle: Lifecycle,
private val callback: (Location) -> Unit
) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// 注册位置监听
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
// 注销位置监听
}
}
10.2 多线程冲突处理
使用CoroutineDispatcher控制线程:
kotlin复制class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun fetchUser(): User = withContext(ioDispatcher) {
// 执行IO操作
}
}
11. 项目扩展方向
11.1 第三方登录集成
添加Google登录支持:
groovy复制implementation 'com.google.android.gms:play-services-auth:20.3.0'
kotlin复制private fun signInWithGoogle() {
val signInIntent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_GOOGLE_SIGN_IN)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_GOOGLE_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
val account = task.getResult(ApiException::class.java)
handleGoogleSignInResult(account)
} catch (e: ApiException) {
// 处理错误
}
}
}
11.2 生物识别认证
集成指纹/面部识别:
kotlin复制val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
// 认证成功处理
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("生物识别登录")
.setSubtitle("使用指纹或面部识别登录")
.setNegativeButtonText("使用账号密码")
.build()
biometricPrompt.authenticate(promptInfo)
