refactor: jetpack compose (4/n)

This commit is contained in:
AnranYus
2024-06-16 01:58:24 +08:00
parent b7f90e0848
commit 42c955be6d
12 changed files with 218 additions and 104 deletions
+10 -10
View File
@@ -60,15 +60,16 @@ dependencies {
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2023.08.00')
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
implementation 'androidx.activity:activity-compose:1.9.0'
implementation platform('androidx.compose:compose-bom:2024.06.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
androidTestImplementation platform('androidx.compose:compose-bom:2023.08.00')
implementation 'androidx.compose.material:material'
androidTestImplementation platform('androidx.compose:compose-bom:2024.06.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
@@ -91,14 +92,13 @@ dependencies {
implementation ('com.squareup.retrofit2:retrofit:2.9.0')
implementation ('com.squareup.retrofit2:converter-gson:2.9.0')
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
implementation "androidx.navigation:navigation-ui-ktx:2.7.5"
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -0,0 +1,27 @@
package com.lsp.view.common
class Config(val safeMode:Boolean,val source:String) {
companion object{
fun setConfig(config: Config){
PreUtils.putString(PreKV.SOURCE_NAME,config.source)
PreUtils.putBool(PreKV.SAFE_MODE,config.safeMode)
}
fun getConfig():Config{
return Config(PreUtils.getBool(PreKV.SAFE_MODE,true),PreUtils.getString(PreKV.SOURCE_NAME,SOURCE.YANDE_RE.values.first()))
}
fun setSafeMode(value:Boolean){
PreUtils.putBool(PreKV.SAFE_MODE,value)
}
fun getSafeMode():Boolean{
return PreUtils.getBool(PreKV.SAFE_MODE,true)
}
}
}
object SOURCE {
val YANDE_RE = mapOf("yande.re" to "https://yande.re/")
val KONACHAN = mapOf("konachan" to "https://konachan.com/")
}
@@ -2,6 +2,7 @@ package com.lsp.view.common
object PreKV {
const val SOURCE_NAME = "source_name"
const val SAFE_MODE = "safe_mode"
}
object Pre{
@@ -0,0 +1,23 @@
package com.lsp.view.common
import android.content.Context
import com.lsp.view.YandViewApplication
object PreUtils {
private val PRE = YandViewApplication.context!!.getSharedPreferences(Pre.NAME,Context.MODE_PRIVATE)
fun getString(key:String,default:String):String{
return PRE.getString(key,default)!!
}
fun putString(key: String,value:String){
PRE.edit().putString(key,value).apply()
}
fun getBool(key: String,default:Boolean):Boolean{
return PRE.getBoolean(key,default)
}
fun putBool(key: String,value: Boolean){
PRE.edit().putBoolean(key, value).apply()
}
}
@@ -6,6 +6,7 @@ import com.lsp.view.repository.exception.UnableConstructObjectException
import com.lsp.view.repository.network.api.PostApi
import com.lsp.view.repository.network.api.PostApiImpl
import com.lsp.view.bean.YandPost
import com.lsp.view.common.Config
import com.lsp.view.common.Pre
import com.lsp.view.common.PreKV
@@ -26,27 +27,13 @@ class PostDataSource {
data class Load(
val tags: String?,
val page: Int,
val safe:Boolean
val safe:Boolean,
val source:String
){
lateinit var source:String
companion object{
fun Builder(tags: String?, page: Int, safe:Boolean): Load {
val sourceNameArray = YandViewApplication.context!!.resources.getStringArray(R.array.pic_source)
val sourceUrlArray = YandViewApplication.context!!.resources.getStringArray(R.array.url_source)
val configSp = YandViewApplication.context?.getSharedPreferences(Pre.NAME, 0)
val nowSourceName: String = configSp?.getString(PreKV.SOURCE_NAME,null) ?: PostDataSource.YANDE_RE
val load = Load(tags,page,safe)
//初始化数据
for ((index,sourceName) in sourceNameArray.withIndex()){
if (sourceName == nowSourceName){
load.source = sourceUrlArray[index]
return load
}
}
throw UnableConstructObjectException("Unable construct load object.")
fun builder(tags: String?, page: Int): Load {
val config = Config.getConfig()
return Load(tags,page,config.safeMode,config.source)
}
}
@@ -1,13 +1,14 @@
package com.lsp.view.repository.network
import com.lsp.view.bean.YandPost
import com.lsp.view.common.Config
class PostRepository {
private val dataSource = PostDataSource()
//获取post
suspend fun fetchPostData(searchTarget :String?,safe:Boolean,page:Int): ArrayList<YandPost> {
suspend fun fetchPostData(searchTarget :String?,page:Int): ArrayList<YandPost> {
var target = searchTarget
var isNum = true
@@ -22,10 +23,10 @@ class PostRepository {
target = "id:$searchTarget"
}
val load = Load.Builder(target,page,safe)
val load = Load.builder(target,page)
val dataList = dataSource.fetchNewPost(load)
return if (safe){
return if (Config.getSafeMode()){
ArrayList(dataList.filter { it.rating == "s" })
}else{
dataList
@@ -1,6 +1,5 @@
package com.lsp.view.service
import android.accounts.NetworkErrorException
import android.app.Service
import android.content.Context
import android.content.Intent
@@ -14,14 +13,25 @@ import kotlinx.coroutines.async
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.Serializable
class Result(val success:Boolean,val message: String,val exception: Exception?){
companion object{
fun success(message:String):Result{
return Result(true,message,null)
}
fun failure(message: String = "",exception: Exception? = null):Result{
return Result(false,message,exception)
}
}
}
class DownloadService : Service() {
private val mBinder = DownloadBinder(this)
class DownloadBinder(val context: Context) : Binder() {
fun downloadImage(fileUrl: String): Deferred<Result<Serializable>> {
fun downloadImage(fileUrl: String): Deferred<Result> {
return CoroutineScope(Dispatchers.IO).async {
val fileDir =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/")
@@ -33,7 +43,7 @@ class DownloadService : Service() {
val file =
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$fileName")
if (file.exists()) {
return@async Result.failure<Exception>(Exception("File exists"))
return@async Result.failure("File exists")
}
val fos = FileOutputStream(file)
try {
@@ -52,15 +62,15 @@ class DownloadService : Service() {
Result.success("File download successfully")
} else {
file.delete()
Result.failure<Exception>(NetworkErrorException("Connection timed out"))
Result.failure("Connection timed out")
}
} catch (e: Exception) {
Result.failure<Exception>(e)
Result.failure(exception = e)
} finally {
fos.close()
}
return@async Result.failure(Exception("Unknown error"))
return@async Result.failure(message = "Unknown error")
}
}
@@ -103,7 +103,6 @@ class MainViewModel(private val repository: PostRepository, context: Context):Vi
try {
val newPosts = repository.fetchPostData(
_uiState.value.nowSearchText.value,
_uiState.value.isSafe,
_uiState.value.nowPage
)
val currentPosts = _postList.value ?: arrayListOf()
@@ -28,20 +28,28 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -62,6 +70,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@@ -76,13 +85,16 @@ import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import com.lsp.view.R
import com.lsp.view.common.Config
import com.lsp.view.service.DownloadService
import com.lsp.view.ui.compose.widget.SearchBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.random.Random
private const val TAG = "Compose_PostActivity"
@@ -121,8 +133,8 @@ class PostActivity : ComponentActivity() {
}
viewModel.downloadResult.observe(this){
val message = it.getOrThrow()
Toast.makeText(this,message.toString(),Toast.LENGTH_SHORT).show()
val message = it.message
Toast.makeText(this,message,Toast.LENGTH_SHORT).show()
}
val serviceIntent = Intent(this, DownloadService::class.java)
@@ -157,7 +169,9 @@ fun App(viewModel: PostViewModel = viewModel()) {
}
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class
)
@Composable
fun PostListScreen(
navController: NavController, onNavigateToDetail: (Post) -> Unit, viewModel: PostViewModel
@@ -191,6 +205,19 @@ fun PostListScreen(
mutableStateOf(24.dp)
}
var showBottomSheet by remember { mutableStateOf(false) }
val refreshing by remember {
uiState.refresh
}
val pullRefreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
scope.launch(Dispatchers.IO) {
viewModel.fetchPost(refresh = true)
}
})
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo }.also{
@@ -209,43 +236,50 @@ fun PostListScreen(
}.distinctUntilChanged().collect { reachedEnd ->
//避免初次加载数据时,list未绘制导致被判断为到达底部
if (reachedEnd && listState.firstVisibleItemIndex != 0) {
viewModel.fetchPost()
withContext(Dispatchers.IO){
viewModel.fetchPost()
}
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column{
LazyVerticalStaggeredGrid(
state = listState,
columns = StaggeredGridCells.Adaptive(200.dp),
verticalItemSpacing = 4.dp,
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = {
//放置两个item将下面的内容顶下来
items(count = 2, key = {
Random.nextInt()
}) {
Box(modifier = Modifier
.height(searchBarHeightSize + searchBarPadding)
.fillMaxWidth())
LazyVerticalStaggeredGrid(
state = listState,
columns = StaggeredGridCells.Adaptive(200.dp),
verticalItemSpacing = 4.dp,
horizontalArrangement = Arrangement.spacedBy(4.dp),
content = {
//放置两个item将下面的内容顶下来
items(count = 2, key = {
Random.nextInt()
}) {
Box(modifier = Modifier
.height(searchBarHeightSize + searchBarPadding)
.fillMaxWidth())
}
items(items = postList, key = {
it.postId//提供key以保持列表顺序稳定
}) {
Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) {
PostItem(it, clickable = {
onNavigateToDetail.invoke(it)
})
}
items(items = postList, key = {
it.postId//提供key以保持列表顺序稳定
}) {
Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) {
PostItem(it, clickable = {
onNavigateToDetail.invoke(it)
})
}
}
},
modifier = Modifier
.background(Color.Transparent)
.pullRefresh(pullRefreshState)
}
},
modifier = Modifier
.background(Color.Transparent)
)
}
)
PullRefreshIndicator(refreshing, pullRefreshState,
Modifier
.align(Alignment.TopCenter)
.padding(searchBarHeightSize+searchBarPadding))
AnimatedVisibility(
visible = scrollDirectionState != -1,
@@ -271,16 +305,37 @@ fun PostListScreen(
it.size.height.toDp()
}
},
searchTarget = uiState.searchTarget.value
) {
viewModel.fetchPost(it, refresh = true)
scope.launch {
listState.animateScrollToItem(index = 0)
searchTarget = uiState.searchTarget.value,
searchEvent = {
scope.launch(Dispatchers.IO) {
viewModel.fetchPost(it, refresh = true)
listState.animateScrollToItem(index = 0)
}
}, menuButtonAction = {
showBottomSheet = true
}
}
)
}
if (showBottomSheet){
var safeModel by remember {
mutableStateOf(Config.getSafeMode())
}
ModalBottomSheet(onDismissRequest ={showBottomSheet = false}, modifier = Modifier.height(400.dp)){
Row(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text(text = "Safe mode", modifier = Modifier.wrapContentSize(), style = MaterialTheme.typography.bodyLarge)
Switch(checked = safeModel, onCheckedChange = {
safeModel = it
Config.setSafeMode(it)
})
}
MaterialTheme.shapes.large
}
}
}
@@ -329,6 +384,7 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
scale *= zoomChange
rotation += rotationChange
}
val context = LocalContext.current
Box(
modifier = Modifier
.background(Color.Black)
@@ -347,6 +403,7 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
.transformable(state = state)
.background(Color.Black)
)
Column {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
@@ -362,7 +419,10 @@ fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
}
},
actions = {
IconButton(onClick = { viewModel.actionDownload() }) {
IconButton(onClick = {
viewModel.actionDownload()
Toast.makeText(context,"Start download",Toast.LENGTH_SHORT).show()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24),
contentDescription = "Download"
@@ -5,10 +5,8 @@ import androidx.compose.runtime.mutableStateOf
import com.lsp.view.bean.Post
data class PostUiState(
var refresh:Boolean = false,
var refresh:MutableState<Boolean> = mutableStateOf(true),
var searchTarget: MutableState<String> = mutableStateOf(""),
val source: String = "",
val safeModel: Boolean = false, //安全模式
var page:Int = 1,
var selectPost: Post? = null
)
@@ -6,11 +6,11 @@ import androidx.lifecycle.viewModelScope
import com.lsp.view.bean.YandPost
import com.lsp.view.repository.exception.NetworkErrorException
import com.lsp.view.repository.network.PostRepository
import com.lsp.view.service.Result
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.Serializable
class PostViewModel:ViewModel() {
private val _uiState = MutableStateFlow(PostUiState())
@@ -20,42 +20,46 @@ class PostViewModel:ViewModel() {
PostRepository()
}
val downloadAction = MutableLiveData<Unit>()
val downloadResult = MutableLiveData<Result<Serializable>>()
val downloadResult = MutableLiveData<Result>()
init {
initData()
}
private fun initData(){
fetchPost()
viewModelScope.launch(Dispatchers.IO) {
fetchPost()
}
}
fun actionDownload(){
downloadAction.postValue(Unit)
}
fun fetchPost(searchTarget: String = _uiState.value.searchTarget.value,refresh:Boolean = false){
viewModelScope.launch(Dispatchers.IO) {
_uiState.value.searchTarget.value = searchTarget
try {
val data = repository.fetchPostData(
searchTarget,
_uiState.value.safeModel,
_uiState.value.page
)
_uiState.value.page ++
postData.value = if (refresh){
data
}else{
postData.value + data
}
} catch (e: NetworkErrorException) {
//todo network error
suspend fun fetchPost(searchTarget: String = _uiState.value.searchTarget.value,refresh:Boolean = false){
_uiState.value.refresh.value = true
_uiState.value.searchTarget.value = searchTarget
try {
if (refresh){
_uiState.value.page = 0
}
val data = repository.fetchPostData(
searchTarget,
_uiState.value.page
)
postData.value = if (refresh){
data
}else{
_uiState.value.page ++
postData.value + data
}
} catch (e: NetworkErrorException) {
//todo network error
}
_uiState.value.refresh.value = false
}
fun setDownloadResult(result: Result<Serializable>){
fun setDownloadResult(result: Result){
downloadResult.postValue(result)
}
@@ -1,6 +1,7 @@
package com.lsp.view.ui.compose.widget
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -32,7 +33,7 @@ import androidx.compose.ui.unit.sp
@Composable
fun SearchBar(modifier: Modifier = Modifier,searchTarget: String = "", searchEvent: (String) -> Unit) {
fun SearchBar(modifier: Modifier = Modifier,searchTarget: String = "", searchEvent: (String) -> Unit,menuButtonAction: () -> Unit) {
Row(
modifier = modifier
.background(MaterialTheme.colorScheme.onPrimary, shape = RoundedCornerShape(56.dp)),
@@ -45,6 +46,9 @@ fun SearchBar(modifier: Modifier = Modifier,searchTarget: String = "", searchEve
.padding(15.dp)
.height(30.dp)
.width(30.dp)
.clickable {
menuButtonAction.invoke()
}
)
var input: String by remember { mutableStateOf(searchTarget) }