Refactor: Upgrade Coil to v3 and update dependencies

- Upgraded image loading library from Coil 2 to Coil 3, updating related APIs across the app.
- Migrated `viewModel()` to a singleton pattern for `AgentViewModel` to optimize instantiation.
- Updated various dependencies, including Android Gradle Plugin, Kotlin, Compose, and other libraries.
- Upgraded Gradle wrapper to version 8.11.1.
- Removed deprecated `windowInsets` and `animateItemPlacement` parameters in Compose components to align with latest API versions.
This commit is contained in:
2025-11-11 18:44:01 +08:00
parent 71718ee9c9
commit 45c5aa29b0
27 changed files with 152 additions and 123 deletions

View File

@@ -7,11 +7,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil3.ImageLoader
import coil3.compose.rememberAsyncImagePainter
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.request.ImageRequest
import coil3.request.crossfade
import okio.Path.Companion.toPath
data class ImageItem(val url: String)
@@ -53,14 +55,15 @@ fun ImageItem(item: ImageItem, imageLoader: ImageLoader, context: Context) { //
fun getImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
MemoryCache.Builder()
.maxSizePercent(context,0.25) // 设置内存缓存大小为可用内存的 25%
.build()
}
.diskCache {
val cacheDir = context.cacheDir.resolve("image_cache")
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
.directory(cacheDir.absolutePath.toPath())
.maxSizeBytes(250L * 1024 * 1024) // 250MB
.build()
}
.build()

View File

@@ -71,7 +71,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(16.dp))
// app version
Text(
text = stringResource(R.string.version_text, versionText),
text = stringResource(R.string.version_text, versionText ?: ""),
fontSize = 16.sp,
color = appColors.secondaryText,
fontWeight = FontWeight.Normal

View File

@@ -118,8 +118,7 @@ fun CommentModalContent(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
CommentMenuModal(
onDeleteClick = {

View File

@@ -8,9 +8,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toDrawable
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.request.fallback
import coil3.request.placeholder
import com.aiosman.ravenow.utils.BlurHashDecoder
import com.aiosman.ravenow.utils.Utils.getImageLoader

View File

@@ -2,6 +2,7 @@ package com.aiosman.ravenow.ui.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -68,7 +69,6 @@ fun Modifier.debouncedClickableWithRipple(
clickable(
enabled = enabled && isClickable,
interactionSource = remember { MutableInteractionSource() },
indication = androidx.compose.material.ripple.rememberRipple()
) {
if (isClickable) {
isClickable = false

View File

@@ -123,7 +123,7 @@ fun LazyGridItemScope.DraggableItem(
translationY = dragDropState.previousItemOffset.value.y
}
} else {
Modifier.animateItemPlacement()
Modifier
}
Box(modifier = modifier.then(draggingModifier).clip(RoundedCornerShape(8.dp)), propagateMinConstraints = true) {
content(dragging)

View File

@@ -16,11 +16,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil3.ImageLoader
import coil3.asDrawable
import coil3.asImage
import coil3.compose.AsyncImage
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.crossfade
import com.aiosman.ravenow.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -59,7 +64,11 @@ fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? {
.build()
val result = withContext(Dispatchers.IO) {
(imageLoader.execute(request) as? SuccessResult)?.drawable?.toBitmap()
val successResult = imageLoader.execute(request) as? SuccessResult
successResult?.let {
val drawable = it.image.asDrawable(context.resources)
drawable.toBitmap()
}
}
bitmap = result
@@ -138,25 +147,33 @@ fun CustomAsyncImage(
}
// 处理字符串URL
val ctx = context ?: localContext
val placeholderImage = remember(placeholderRes, ctx) {
placeholderRes?.let { resId ->
ContextCompat.getDrawable(ctx, resId)?.asImage()
}
}
val errorImage = remember(errorRes, ctx) {
errorRes?.let { resId ->
ContextCompat.getDrawable(ctx, resId)?.asImage()
}
}
if (showShimmer) {
var isLoading by remember { mutableStateOf(true) }
Box(modifier = modifier) {
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)
model = ImageRequest.Builder(ctx)
.data(imageUrl)
.crossfade(200)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.apply {
// 设置占位符图片
if (placeholderRes != null) {
placeholder(placeholderRes)
}
placeholderImage?.let { placeholder(it) }
// 设置错误时显示的图片
if (errorRes != null) {
error(errorRes)
}
errorImage?.let { error(it) }
}
.build(),
contentDescription = contentDescription,
@@ -177,20 +194,16 @@ fun CustomAsyncImage(
}
} else {
AsyncImage(
model = ImageRequest.Builder(context ?: localContext)
model = ImageRequest.Builder(ctx)
.data(imageUrl)
.crossfade(200)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.apply {
// 设置占位符图片
if (placeholderRes != null) {
placeholder(placeholderRes)
}
placeholderImage?.let { placeholder(it) }
// 设置错误时显示的图片
if (errorRes != null) {
error(errorRes)
}
errorImage?.let { error(it) }
}
.build(),
contentDescription = contentDescription,

View File

@@ -450,7 +450,6 @@ fun MomentBottomOperateRowGroup(
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
windowInsets = WindowInsets(0),
dragHandle = {
Box(
modifier = Modifier

View File

@@ -71,7 +71,6 @@ fun PolicyCheckbox(
showModal = false
},
sheetState = modalSheetState,
windowInsets = WindowInsets(0),
containerColor = Color.White,
) {
WebViewDisplay(

View File

@@ -47,7 +47,6 @@ fun CreateBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
windowInsets = BottomSheetDefaults.windowInsets,
containerColor = appColors.background,
dragHandle = null,
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)

View File

@@ -265,7 +265,6 @@ fun IndexScreen() {
modifier = Modifier
.background(AppColors.background)
.padding(0.dp),
beyondBoundsPageCount = 4,
userScrollEnabled = false
) { page ->
when (page) {

View File

@@ -54,7 +54,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.GuestLoginCheckOut
@@ -115,7 +114,7 @@ fun Agent() {
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
val viewModel: AgentViewModel = viewModel()
val viewModel: AgentViewModel = AgentViewModel
// 确保推荐Agent数据已加载
LaunchedEffect(Unit) {
@@ -183,7 +182,6 @@ fun Agent() {
colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColors.background
),
windowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
.padding(top = statusBarPaddingValues.calculateTopPadding())
@@ -899,7 +897,7 @@ fun ChatRoomCard(
) {
val AppColors = LocalAppTheme.current
val cardSize = 180.dp
val viewModel: AgentViewModel = viewModel()
val viewModel: AgentViewModel = AgentViewModel
val context = LocalContext.current
// 防抖状态

View File

@@ -156,10 +156,10 @@ object HotAgentViewModel : ViewModel() {
try {
// 预加载头像图片到缓存
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(agent.avatar)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
preloadedImageIds.add(agent.id)

View File

@@ -841,20 +841,20 @@ fun Explore() {
if (bannerItem.backgroundImageUrl.isNotEmpty()) {
// 预加载背景图片
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(bannerItem.backgroundImageUrl)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
}
if (bannerItem.imageUrl.isNotEmpty()) {
// 预加载头像图片
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
coil3.request.ImageRequest.Builder(context)
.data(bannerItem.imageUrl)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.memoryCachePolicy(coil3.request.CachePolicy.ENABLED)
.diskCachePolicy(coil3.request.CachePolicy.ENABLED)
.build()
)
}

View File

@@ -59,7 +59,6 @@ fun FullArticleModal(
.height(sheetHeight),
containerColor = appColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
Column(
modifier = Modifier

View File

@@ -154,7 +154,6 @@ fun NewsCommentModal(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {

View File

@@ -194,7 +194,6 @@ fun NewsScreen() {
.height(sheetHeight),
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
) {
NewsCommentModal(
postId = selectedMoment?.id,

View File

@@ -258,7 +258,6 @@ fun ProfileV3(
sheetState = agentMenuModalState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
AgentMenuModal(
agent = contextAgent,
@@ -535,7 +534,7 @@ fun ProfileV3(
containerColor = Color.Transparent, // 设置容器背景透明
contentColor = Color.Transparent, // 设置内容背景透明
dragHandle = null, // 移除拖拽手柄
windowInsets = androidx.compose.foundation.layout.WindowInsets(0) // 移除窗口边距
contentWindowInsets = {androidx.compose.foundation.layout.WindowInsets(0)},
) {
Box(
modifier = Modifier
@@ -567,7 +566,7 @@ fun ProfileV3(
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
contentWindowInsets = { androidx.compose.foundation.layout.WindowInsets(0) }
) {
Box(
modifier = Modifier

View File

@@ -114,11 +114,14 @@ fun getFeedItems(): List<FeedItem> {
@Composable
fun LocationDetailScreen(x: Float, y: Float) {
val scope = rememberCoroutineScope()
val density = LocalDensity.current
val scaffoldState = rememberBottomSheetScaffoldState(
SheetState(
bottomSheetState = SheetState(
skipPartiallyExpanded = false,
density = LocalDensity.current, initialValue = SheetValue.PartiallyExpanded,
skipHiddenState = true
initialValue = SheetValue.PartiallyExpanded,
skipHiddenState = true,
positionalThreshold = { 0.5f },
velocityThreshold = { with(density) { 125.dp.toPx() } }
)
)
val configuration = LocalConfiguration.current

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -30,7 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -67,7 +68,7 @@ fun NewPostImageGridScreen() {
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back",
modifier = Modifier
.size(24.dp)
@@ -84,7 +85,7 @@ fun NewPostImageGridScreen() {
fontSize = 18.sp,
)
Icon(
Icons.Default.Delete,
Icons.Filled.Delete,
contentDescription = "delete",
modifier = Modifier
.size(24.dp)

View File

@@ -173,7 +173,6 @@ fun PostScreen(
sheetState = commentModalState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
CommentMenuModal(
onDeleteClick = {
@@ -262,7 +261,6 @@ fun PostScreen(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
EditCommentBottomModal(replyComment) {
viewModel.viewModelScope.launch {
@@ -849,7 +847,6 @@ fun Header(
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
windowInsets = WindowInsets(0)
) {
PostMenuModal(

View File

@@ -4,14 +4,16 @@ import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import coil.request.ImageRequest
import coil.request.SuccessResult
import androidx.core.graphics.drawable.toBitmap
import coil3.asDrawable
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.allowHardware
import com.aiosman.ravenow.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -30,8 +32,9 @@ object FileUtil {
.allowHardware(false) // Disable hardware bitmaps.
.build()
val result = (loader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val result = loader.execute(request) as? SuccessResult ?: return
val drawable = result.image.asDrawable(context.resources)
val bitmap = drawable.toBitmap()
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "image_${System.currentTimeMillis()}.jpg")

View File

@@ -4,8 +4,9 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import coil.ImageLoader
import coil.request.CachePolicy
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.CachePolicy
import com.aiosman.ravenow.data.api.AuthInterceptor
import com.aiosman.ravenow.data.api.getSafeOkHttpClient
import java.io.File
@@ -32,7 +33,15 @@ object Utils {
val okHttpClient = getSafeOkHttpClient(authInterceptor = AuthInterceptor())
val loader = ImageLoader.Builder(appContext)
.okHttpClient(okHttpClient)
.components {
add(
OkHttpNetworkFetcherFactory(
callFactory = {
okHttpClient
}
)
)
}
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build()