当前位置: 首页 > news >正文

3.0 compose学习:MVVM框架+Hilt注解调用登录接口

文章目录

  • 前言:
  • 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>

相关文章:

  • 目前做网站流行的语言网站公司
  • 用路由器建设网站google play服务
  • 三晋联盟做网站需要多钱传统营销方式有哪些
  • 怎么用nat做网站临沂网站建设
  • 进一步加强政府网站建设百度用户服务中心官网电话
  • 用mac做网站福建seo
  • 领域驱动设计(DDD)【9】之代码初始部分实现和问题解决
  • 仓颉语言语法特点、使用范围、编译及环境搭建:从零开始第一个cangjie程序
  • 变电站自动化系统有哪些设备?
  • 如何通过FEMFAT许可证进行数据分析和处理
  • lz4库使用
  • 洛谷P1092 [NOIP 2004 提高组] 虫食算
  • 29.设计模式的选择与应用
  • windows 上 build 时,微软给出的 vcpkg 工具,如何使用
  • 关于数据编码、进制、位运算的详细讲解(从属GESP三级)
  • C#调用MATLAB函数
  • [Linux] Linux用户和组管理
  • 用福昕阅读器打开pdf文件,整个程序窗口自动缩小的问题
  • Python邮件自动化完全指南:从基础到高级应用
  • 如何通过nvm切换本地node环境详情教程(已装过node.js更改成nvm)
  • 【Game】Powerful——Pet Skin(13)
  • gitlab-ce安装
  • RISC-V三级流水线项目:总体概述和取指模块
  • 基于版本控制+WORM的OSS数据保护:防勒索攻击与法规遵从实践
  • 软件工程:从理论到实践,构建可靠软件的艺术与科学
  • iwebsec靶场-文件上传漏洞