文章目录
- 前言:
- 1、添加依赖
- 1.1 在settings.gradle.kts中添加
- 1.2 在应用级的build.gradle.kts添加插件依赖
- 1.3 在module级的build.gradle.kts添加依赖
- 2、实体类
- 2.1 request
- 2.2 reponse
- 3、网络请求
- 3.1 ApiService
- 3.2 NetworkModule
- 3.3 拦截器 添加token
- 3.4 Hilt 的 使用
- 4、数据类
- 4.1 服务器数据
- 4.1.1 LoginModule
- 4.1.2 Repository
- 4.2 本地数据
- 4.2.1 StorageModule
- 4.2.2
- 5、ViewModel访问接口
- 6、compose UI调用
- 6.1 CustomApplication
- 6.2 MainActivity
- 7、问题
- 7.1 问题1 网络错误
- 7.1.1 步骤 1:创建网络安全配置文件
- 7.1.2 步骤2:
前言:
新开了一个项目之后,发现MVP框架的实现代码有点多了,就想说用MVVM框架进行实现,加上发现Hilt注解相对能够更好地解耦,学习了一下之后就想说需要应用到实际引用中,就写了个简单功能实现,虽然一个登录功能看着写的代码结构多了点,但是到后期功能不断增加之后就会发现,结构比较清晰,基本机构见图所示,使用MVVM框架实现登录效果,包括retrofit+ViewModel+Hilt注解+Compose的实现。
1、添加依赖
添加依赖需要在三个部分中进行添加
1.1 在settings.gradle.kts中添加
pluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}google()mavenCentral()gradlePluginPortal()maven(url = uri("https://jitpack.io"))}
}
dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()maven(url = uri("https://jitpack.io"))}
}
上述调用中,如果无法下载或者下载失败,可以使用了阿里云镜像库,原链接可写可不写,如下所示
maven(url = uri("https://maven.aliyun.com/repository/google"))maven( url = uri("https://maven.aliyun.com/repository/public"))maven(url = uri("https://maven.aliyun.com/nexus/content/repositories/central"))maven(url = uri("https://maven.aliyun.com/repository/gradle-plugin"))
1.2 在应用级的build.gradle.kts添加插件依赖
plugins {...id("com.google.devtools.ksp") version "2.1.10-1.0.29"id("com.google.dagger.hilt.android") version "2.56.2" apply false}
需要注意的是,KSP 版本的前一部分必须与 build 中使用的 Kotlin 版本一致,上述版本中知道kotlin的版本为2.1.10,从kapt迁移到ksp官方
1.3 在module级的build.gradle.kts添加依赖
重点写的是添加网络相关、Hilt和EncryptedSharedPreferences的依赖
plugins {
...
id("com.google.devtools.ksp") version "2.1.10-1.0.29"
id("com.google.dagger.hilt.android")
}
android{
...
buildFeatures {compose = true}...
}dependencies {
...//网络相关依赖implementation("com.google.code.gson:gson:2.10.1")implementation("com.squareup.retrofit2:retrofit:2.9.0")implementation("com.squareup.retrofit2:converter-gson:2.9.0")implementation("com.squareup.okhttp3:okhttp:4.12.0")implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")//navigation页面跳转implementation("androidx.navigation:navigation-compose:2.9.0")//hilt注解implementation("com.google.dagger:hilt-android:2.56.2")ksp("com.google.dagger:hilt-android-compiler:2.56.2")implementation("androidx.hilt:hilt-navigation-compose:1.2.0")//EncryptedSharedPreferences本地持久化保存implementation("androidx.security:security-crypto:1.1.0-alpha06")
}
对于compose的依赖的导入一般新建项目的时候Build configuration language选择kotlin项目可以自动导入,
如果想要了解具体如何添加,可以参考 添加compose的依赖
2、实体类
2.1 request
请求接口数据的数实体类
data class LoginRequest(val username:String,val password:String)
2.2 reponse
接口响应的实体类
open class BaseResponse @JvmOverloads constructor(var code: Int = -1,var msg: String? = ""
)
data class LoginResponse(val token:String):BaseResponse()
数据类的形式主要看服务器的调用
3、网络请求
3.1 ApiService
import retrofit2.Responseimport retrofit2.http.Body
import retrofit2.http.POSTinterface ApiService {@POST("/openLogin")suspend fun userLogin(@Body request:LoginRequest): Response<LoginResponse>}
3.2 NetworkModule
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {private const val BASE_URL = "http://47.122.63.169:8070"@Provides@Singletonfun provideAuthInterceptor(tokenManager: LoginStorage): AuthInterceptor {return AuthInterceptor(tokenManager)}@Provides@Singletonfun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {return OkHttpClient.Builder().addInterceptor(authInterceptor) //添加拦截器.addInterceptor(HttpLoggingInterceptor().apply {level = HttpLoggingInterceptor.Level.BODY}).build()}@Provides@Singletonfun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {return Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient).addConverterFactory(GsonConverterFactory.create()).build()}@Provides@Singletonfun provideApiService(retrofit: Retrofit): ApiService {return retrofit.create(ApiService::class.java)}
}
3.3 拦截器 添加token
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import javax.inject.Inject// AuthInterceptor.kt
class AuthInterceptor @Inject constructor(private val tokenManager: LoginStorage
) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()// 检查是否需要认证val requiresAuth = request.header("No-Auth") == nullif (requiresAuth) {val token = tokenManager.getToken() ?: throw AuthException("未登录")// 添加认证头val newRequest = request.newBuilder().addHeader("Authorization", "Bearer $token").build()return chain.proceed(newRequest)}return chain.proceed(request)}
}class AuthException(message: String) : IOException(message)
ps:如果token失效了,需要及时更新
3.4 Hilt 的 使用
@Singleton 是进程级别的
最大范围: 在整个应用进程中唯一存在
依赖管理: 确保每次注入都是同一实例
Hilt注解的官方解释
使用hilt注解,需要注意的是,引用的包为import javax.inject.Singleton
,而不是jakarta.inject.Singleton,不然会出现报错,
scoped with @Singleton may not reference bindings with different scopes:public abstract static class SingletonC implements CustomApplication_GeneratedInjector
4、数据类
4.1 服务器数据
4.1.1 LoginModule
@Module
@InstallIn(SingletonComponent::class)
object LoginModule {@Provides@Singletonfun provideLoginRepository(authService: ApiService,authStorage: LoginStorage): LoginRepository {return LoginUserRepositoryImp(authService, authStorage)}}
4.1.2 Repository
访问登录接口
interface LoginRepository {suspend fun login(bean: UserBean): UiState<out LoginResponse>suspend fun isLoggedIn(): Booleansuspend fun logout()
}
import javax.inject.Injectclass LoginUserRepositoryImp @Inject constructor(private val apiService: ApiService,private val loginStorage: LoginStorage):LoginRepository {override suspend fun login(bean: UserBean): UiState<out LoginResponse> {return try {val response = apiService.userLogin(LoginRequest(username = bean.userName,password = bean.password))Log.d("lucky", "login: code ${response.code()} \nbody ${response.body()} \n message${response.message()}")if (response.isSuccessful) {response.body()?.let {loginStorage.saveToken(it.token)UiState.Success(it)} ?: UiState.Error("Empty response body")} else {UiState.Error("Login failed: ${response.code()}")}} catch (e: Exception) {UiState.Error("Network error: ${e.message}")}}override suspend fun isLoggedIn(): Boolean {return loginStorage.getToken() != null}override suspend fun logout() {loginStorage.clearToken()}
}
4.2 本地数据
4.2.1 StorageModule
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton@Module
@InstallIn(SingletonComponent::class)
object StorageModule {@Singleton@Providesfun provideLoginStorage( @ApplicationContext context: Context): LoginStorage {return SecureLoginStorageImp(context)}
}
4.2.2
保存token,可根据其获取登录状态,使用token进行实现
interface LoginStorage {suspend fun saveToken(token: String)suspend fun getToken(): String?suspend fun clearToken()
}
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Injectclass SecureLoginStorageImp @Inject constructor(@ApplicationContext context:Context) : LoginStorage{private val encryptedPreferences by lazy {val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()EncryptedSharedPreferences.create(context,Constants.LOGIN_USER_PREFERENCE,masterKey,EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)}private val tokenKey = Constants.LOGIN_USER_TOKEN// companion object {}override suspend fun saveToken(token: String) {encryptedPreferences.edit {putString(tokenKey,token)}}override suspend fun getToken(): String? {return encryptedPreferences.getString(tokenKey,"")}override suspend fun clearToken() {encryptedPreferences.edit {remove(tokenKey)}}
}
5、ViewModel访问接口
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.launch@HiltViewModel
class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository
) : ViewModel() {private val _loginState = mutableStateOf<UiState<out LoginResponse>>(UiState.Idle)val loginState: MutableState<UiState<out LoginResponse>> get() = _loginStatevar userName by mutableStateOf("")private setvar password by mutableStateOf("")private setfun updateUsername(input: String) {userName = input}fun updatePassword(input: String) {password = input}fun login() {viewModelScope.launch {_loginState.value = UiState.Loading_loginState.value = loginRepository.login(UserBean(userName = userName,password = password))}}fun resetState() {_loginState.value = UiState.Idle}}
6、compose UI调用
6.1 CustomApplication
@HiltAndroidApp
class CustomApplication : Application() {override fun onCreate() {super.onCreate()}}
6.2 MainActivity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContent {ChainOfCustodyTheme {Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->Column(modifier = Modifier.padding(innerPadding)) {Login()}}}}}}
@Composable
fun Login( viewModel: LoginViewModel = hiltViewModel()){// 登录成功处理val loginState = viewModel.loginState.valueval context = LocalContext.currentLaunchedEffect(loginState) {when (loginState) {is UiState.Success -> {viewModel.resetState()Handler(Looper.getMainLooper()).post {Toast.makeText(context,"登录成功111221212",Toast.LENGTH_LONG).show()}}is UiState.Error -> {// 显示错误提示Toast.makeText(context,loginState.message,Toast.LENGTH_LONG).show()}else -> {}}}Text(text = "登录",modifier = Modifier.clickable {viewModel.updatePassword("xxx123")viewModel.updateUsername("xxx")viewModel.login()})
}
7、问题
在Android9+的版本中,服务器的域名为http会出现的问题
7.1 问题1 网络错误
java.lang.Exception: Toast callstack! strTip=Network error: CLEARTEXT communication to 47.122.63.169 not permitted by network security policy
7.1.1 步骤 1:创建网络安全配置文件
在 res/xml 目录创建 network_security_config.xml
添加以下内容:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config><base-config cleartextTrafficPermitted="true" />
</network-security-config>
7.1.2 步骤2:
在AndroidManifest中:
<uses-permission android:name="android.permission.INTERNET" /><application...android:networkSecurityConfig="@xml/network_security_config"
...>
</application>