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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86163.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86163.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

git学习资源

动画演示&#xff1a;Learn Git Branching 终极目标&#xff08;能看懂即入门&#xff09;&#xff1a;git 简明指南 Git 教程 | 菜鸟教程

C++ 第二阶段:模板编程 - 第一节:函数模板与类模板

目录 一、模板编程的核心概念 1.1 什么是模板编程&#xff1f; 二、函数模板详解 2.1 函数模板的定义与使用 2.1.1 基本语法 2.1.2 示例&#xff1a;通用交换函数 2.1.3 类型推导规则 2.2 函数模板的注意事项 2.2.1 普通函数与函数模板的调用规则 2.2.2 隐式类型转换…

Docker 报错“x509: certificate signed by unknown authority”的排查与解决实录

目录 &#x1f527;Docker 报错“x509: certificate signed by unknown authority”的排查与解决实录 &#x1f4cc; 问题背景 &#x1f9ea; 排查过程 步骤 1&#xff1a;确认加速器地址是否可访问 步骤 2&#xff1a;检查 Docker 是否真的使用了镜像加速器 步骤 3&…

达梦以及其他图形化安装没反应或者报错No more handles [gtk_init_check() failed]

本人安装问题和解决步骤如下&#xff0c;仅供参考 执行 DMInstall.bin 报错 按照网上大部分解决方案 export DISPLAY:0.0 xhost 重新执行 DMInstall.bin&#xff0c;无报错也无反应 安装xclock测试也是同样效果&#xff0c;无报错也无反应 最开始猜测可能是连接工具问题&a…

项目节奏不一致时,如何保持全局平衡

项目节奏不一致时&#xff0c;如何保持全局平衡的关键在于&#xff1a;构建跨项目协调机制、合理配置资源、建立共享节奏看板、优先明确战略驱动、引入缓冲与预警机制。其中&#xff0c;构建跨项目协调机制尤为关键&#xff0c;它能将各项目的排期、优先级和风险实时联动&#…

macOS - 安装微软雅黑字体

文章目录 1、下载资源2、安装3、查看字体 app4、卸载字体 macOS 中打开 Windows 传输过来的文件的时候&#xff0c;经常会提示 xxx 字体缺失。下面以安装 微软雅黑字体为例。 1、下载资源 https://github.com/BronyaCat/Win-Fonts-For-Mac 2、安装 双击 Fonts 文件夹下的 msy…

ArkUI-X资源分类与访问

应用开发过程中&#xff0c;经常需要用到颜色、字体、间距、图片等资源&#xff0c;在不同的设备或配置中&#xff0c;这些资源的值可能不同。 应用资源&#xff1a;借助资源文件能力&#xff0c;开发者在应用中自定义资源&#xff0c;自行管理这些资源在不同的设备或配置中的…

11-StarRocks故障诊断FAQ

StarRocks故障诊断FAQ 概述 本文档整理了StarRocks故障诊断过程中常见的问题和解决方案,涵盖了故障排查、日志分析、性能诊断、问题定位等各个方面,帮助用户快速定位和解决StarRocks相关问题。 故障排查FAQ Q1: 如何排查连接故障? A: 连接故障排查方法: 1. 网络连通性…

敏捷项目管理怎么做?4大主流方法论对比及工具适配方案

在传统瀑布式项目管理中&#xff0c;需求定义、设计、开发、测试等环节如同工业流水线般严格线性推进&#xff0c;展现出强大的流程控制能力。不过今天的软件迭代周期已压缩至周级乃至日级&#xff0c;瀑布式管理难以应对需求的快速变化&#xff0c;敏捷式项目管理则以“小步快…

解决YOLO模型从Python迁移到C++时目标漏检问题——跨语言部署中的关键陷阱与解决方案

问题背景 当我们将Python训练的YOLO模型部署到C环境时&#xff0c;常遇到部分目标漏检问题。这通常源于预处理/后处理差异、数据类型隐式转换或模型转换误差。本文通过完整案例解析核心问题并提供可落地的解决方案。 一、常见原因分析 预处理不一致 Python常用OpenCV&#xff…

【2025CCF中国开源大会】开放注册与会议通知(第二轮)

点击蓝字 关注我们 CCF Opensource Development Committee 2025 CCF中国开源大会 由中国计算机学会主办的 2025 CCF中国开源大会&#xff08;CCF ChinaOSC&#xff09;拟于 2025年8月2日-3日 在上海召开。本届大会以“蓄势引领、众行致远”为主题&#xff0c;由上海交通大学校长…

本地聊天室

测试版还没测试过&#xff0c;后面更新不会继续开源&#xff0c;有问题自行修复 开发环境: PHP版本7.2 Swoole扩展 本地服务器环境&#xff08;如XAMPP、MAMP&#xff09; 功能说明: 注册/登录系统&#xff0c;支持本地用户数据存储 ​ 发送文本、图片和语音消息 ​ 实…

golang学习随便记x-调试与杂类(待续)

编译与调试 调试时从终端键盘输入 调试带有需要用户键盘输入的程序时&#xff0c;VSCode报错&#xff1a;Unable to process evaluate: debuggee is running&#xff0c;因为调试器不知道具体是哪个终端输入。需要配置启动文件 .vscode/launch.json 类似如下&#xff08;注意…

MultipartFile、File 和 Mat

1. MultipartFile (来自 Spring Web) 用途&#xff1a; 代表通过 multipart 形式提交&#xff08;通常是 HTTP POST 请求&#xff09;接收到的文件。 它是 Spring Web 中用于处理 Web 客户端文件上传的核心接口。 关键特性&#xff1a; 抽象&#xff1a; 这是一个接口&#xf…

.NET 9.0 SignalR 支持修剪和原生 AOT

什么是 SignalR&#xff1f; SignalR 是一个库&#xff0c;可用于向应用程序添加实时 Web 功能。它提供了一个简单的 API&#xff0c;用于创建可从服务器和客户端调用的服务器到客户端远程过程调用 (RPC)。现在&#xff0c;SignalR 在 .NET 8.0 和 .NET 9.0 中支持修剪和原生 …

下载资源管理

本文章仅用于作者管理自己的站内资源&#xff0c;方便日后查找&#xff0c;后续更新资源该文章持续更新。 1、环境安装 python3.11.11环境 python3.7.9 ARM.CMSIS.5.6.0(这个在站内重复上传了) Nordic8.32.1 java8 2、工具类软件安装包 2.1、蓝牙类 SI Connect 蓝牙OT…

​​FFmpeg命令全解析:三步完成视频合并、精准裁剪​​、英伟达显卡加速

一、裁剪 常规裁剪 根据时长裁剪&#xff0c;常规的裁剪 -c copy 表示直接复制流&#xff08;不重新编码&#xff09;&#xff0c;速度极快&#xff0c;但要求切割时间必须是关键帧。否则裁剪下来的画面开头/结尾 会模糊花屏 ffmpeg -i input.mp4 -ss 00:00:30 -to 00:01:00 …

HTML5 更新的功能

文章目录 前言**一、语义化标签&#xff08;Semantic Elements&#xff09;****二、多媒体支持&#xff08;Audio & Video&#xff09;****三、图形与绘图&#xff08;Canvas & SVG&#xff09;****1. <canvas>****2. SVG 内联支持** **四、表单增强&#xff08;…

React 全面入门与进阶实战教程

文章目录 一、认识 React1.1 核心特点 二、快速搭建 React 项目2.1 使用 Create React App2.2 使用 Vite 创建更轻量的 React 项目2.3 项目结构概览 三、React 核心语法基础3.1 JSX&#xff1a;React 的模板语法3.2 函数组件与 Props3.3 useState&#xff1a;定义响应式状态3.4…

牛津大学开源视频中的开放世界目标计数!

视频中的开放世界目标计数 GitHub PaPer Niki Amini-Naieni nikianrobots.ox.ac.uk Andrew Zisserman azrobots.ox.ac.uk 视觉几何组&#xff08;VGG&#xff09;&#xff0c;牛津大学&#xff0c;英国 ​ 图 1&#xff1a;视频中的目标计数&#xff1a;给定顶行的视频&#…