refactor: jetpack compose (3/n)
This commit is contained in:
@@ -57,6 +57,8 @@ android {
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
@@ -2,42 +2,19 @@ package com.lsp.view
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import android.content.ServiceConnection
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.IBinder
|
||||
import com.lsp.view.service.DownloadService.DownloadBinder
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.lsp.view.service.DownloadService
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
class YandViewApplication : Application(), DefaultLifecycleObserver {
|
||||
lateinit var downloadBinder:DownloadBinder
|
||||
|
||||
private val connection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
|
||||
downloadBinder = iBinder as DownloadBinder
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
context = applicationContext
|
||||
val serviceIntent = Intent(this, DownloadService::class.java)
|
||||
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
super.onDestroy(owner)
|
||||
unbindService(connection)
|
||||
}
|
||||
|
||||
companion object {
|
||||
var context: Context? = null
|
||||
private set
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lsp.view.bean
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import java.io.Serializable
|
||||
|
||||
open class Post(
|
||||
open val postId: String,
|
||||
@@ -11,7 +12,7 @@ open class Post(
|
||||
open val sampleHeight: Int,
|
||||
open val sampleWidth: Int,
|
||||
open val tags:String?
|
||||
): Parcelable {
|
||||
): Parcelable,Serializable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!,
|
||||
|
||||
@@ -7,6 +7,9 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
object ServiceCreator {
|
||||
|
||||
val CLIENT = OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}).build()
|
||||
|
||||
/**
|
||||
* @param serviceClass:所属类的Service接口
|
||||
@@ -14,16 +17,11 @@ object ServiceCreator {
|
||||
*
|
||||
*/
|
||||
fun <T> create(serviceClass: Class<T>, source: String): T {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build()
|
||||
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(source)
|
||||
.client(client)
|
||||
.client(CLIENT)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
return retrofit.create(serviceClass)
|
||||
|
||||
@@ -6,7 +6,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.os.*
|
||||
import okhttp3.OkHttpClient
|
||||
import com.lsp.view.repository.network.retrofit.ServiceCreator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -17,48 +21,47 @@ class DownloadService : Service() {
|
||||
|
||||
class DownloadBinder(val context: Context) : Binder() {
|
||||
|
||||
fun downloadImage(fileUrl: String):Result<Serializable> {
|
||||
|
||||
val fileDir =
|
||||
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/")
|
||||
if (!fileDir.exists()) {
|
||||
fileDir.mkdirs()
|
||||
}
|
||||
val split = fileUrl.split("/")
|
||||
val fileName = split[split.size - 1]
|
||||
val file =
|
||||
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$fileName")
|
||||
if (file.exists()) {
|
||||
return Result.failure<Exception>(Exception("File exists"))
|
||||
}
|
||||
val fos = FileOutputStream(file)
|
||||
try {
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(fileUrl)
|
||||
.build()
|
||||
val response = client.newCall(request).execute()
|
||||
val responseData = response.body?.bytes()
|
||||
return if (response.code == 200) {
|
||||
fos.write(responseData)
|
||||
//通知媒体更新
|
||||
MediaScannerConnection.scanFile(
|
||||
context, arrayOf(file.path),
|
||||
null, null
|
||||
)
|
||||
Result.success("File download successfully")
|
||||
} else {
|
||||
file.delete()
|
||||
Result.failure<Exception>(NetworkErrorException("Connection timed out"))
|
||||
fun downloadImage(fileUrl: String): Deferred<Result<Serializable>> {
|
||||
return CoroutineScope(Dispatchers.IO).async {
|
||||
val fileDir =
|
||||
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/")
|
||||
if (!fileDir.exists()) {
|
||||
fileDir.mkdirs()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure<Exception>(e)
|
||||
} finally {
|
||||
fos.close()
|
||||
val split = fileUrl.split("/")
|
||||
val fileName = split[split.size - 1]
|
||||
val file =
|
||||
File("${Environment.getExternalStorageDirectory()}/${Environment.DIRECTORY_PICTURES}/LspMake/$fileName")
|
||||
if (file.exists()) {
|
||||
return@async Result.failure<Exception>(Exception("File exists"))
|
||||
}
|
||||
val fos = FileOutputStream(file)
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(fileUrl)
|
||||
.build()
|
||||
val response = ServiceCreator.CLIENT.newCall(request).execute()
|
||||
val responseData = response.body?.bytes()
|
||||
return@async if (response.code == 200) {
|
||||
fos.write(responseData)
|
||||
//通知媒体更新
|
||||
MediaScannerConnection.scanFile(
|
||||
context, arrayOf(file.path),
|
||||
null, null
|
||||
)
|
||||
Result.success("File download successfully")
|
||||
} else {
|
||||
file.delete()
|
||||
Result.failure<Exception>(NetworkErrorException("Connection timed out"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure<Exception>(e)
|
||||
} finally {
|
||||
fos.close()
|
||||
}
|
||||
|
||||
return@async Result.failure(Exception("Unknown error"))
|
||||
}
|
||||
|
||||
return Result.failure(Exception("Unknown error"))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
package com.lsp.view.ui.compose
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
@@ -18,9 +26,19 @@ 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.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
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.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -29,39 +47,117 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.lsp.view.bean.Post
|
||||
import com.lsp.view.ui.compose.theme.LspViewTheme
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.compose.SubcomposeAsyncImageContent
|
||||
import com.lsp.view.R
|
||||
import com.lsp.view.service.DownloadService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val TAG = "Compose_PostActivity"
|
||||
|
||||
class PostActivity : ComponentActivity() {
|
||||
lateinit var downloadBinder: DownloadService.DownloadBinder
|
||||
val viewModel: PostViewModel by viewModels<PostViewModel>()
|
||||
|
||||
private val connection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
|
||||
downloadBinder = iBinder as DownloadService.DownloadBinder
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
LspViewTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
PostList()
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.downloadAction.observe(this){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val result =
|
||||
downloadBinder.downloadImage(viewModel.uiState.value.selectPost?.fileUrl ?: "")
|
||||
.await()
|
||||
launch(Dispatchers.Main) {
|
||||
if (result.isSuccess) {
|
||||
Toast.makeText(
|
||||
this@PostActivity,
|
||||
"Download successful",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@PostActivity,
|
||||
"Download fail",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val serviceIntent = Intent(this, DownloadService::class.java)
|
||||
bindService(serviceIntent, connection, BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unbindService(connection)
|
||||
}
|
||||
}
|
||||
|
||||
const val NAV_ROUTE_POSTLIST_SCREEN = "NAV_ROUTE_POSTLIST_SCREEN"
|
||||
const val NAV_ROUTE_DETAIL_SCREEN = "NAV_ROUTE_DETAIL_SCREEN"
|
||||
|
||||
@Composable
|
||||
fun App(viewModel: PostViewModel = viewModel()) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController, startDestination = NAV_ROUTE_POSTLIST_SCREEN) {
|
||||
composable(NAV_ROUTE_POSTLIST_SCREEN) {
|
||||
PostListScreen(onNavigateToDetail = {
|
||||
viewModel.uiState.value.selectPost = it
|
||||
navController.navigate(NAV_ROUTE_DETAIL_SCREEN)
|
||||
}, navController = navController, viewModel = viewModel)
|
||||
}
|
||||
|
||||
composable(NAV_ROUTE_DETAIL_SCREEN) {
|
||||
DetailScreen(navController = navController, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PostList(viewModel: PostViewModel = viewModel()) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
fun PostListScreen(
|
||||
navController: NavController,
|
||||
onNavigateToDetail: (Post) -> Unit,
|
||||
viewModel: PostViewModel
|
||||
) {
|
||||
val postList by viewModel.postData.collectAsState()
|
||||
val listState = rememberLazyStaggeredGridState()
|
||||
|
||||
@@ -78,18 +174,22 @@ fun PostList(viewModel: PostViewModel = viewModel()) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyVerticalStaggeredGrid(
|
||||
state = listState,
|
||||
columns = StaggeredGridCells.Adaptive(200.dp),
|
||||
verticalItemSpacing = 4.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
content = {
|
||||
items(items=postList, key = {
|
||||
items(items = postList, key = {
|
||||
it.postId//提供key以保持列表顺序稳定
|
||||
}) {
|
||||
Row(Modifier.animateItemPlacement(tween(durationMillis = 250))) {
|
||||
PostItem(it)
|
||||
PostItem(
|
||||
it,
|
||||
clickable = {
|
||||
onNavigateToDetail.invoke(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -99,19 +199,22 @@ fun PostList(viewModel: PostViewModel = viewModel()) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PostItem(post:Post){
|
||||
fun PostItem(post: Post, clickable: (Post) -> Unit) {
|
||||
SubcomposeAsyncImage(
|
||||
model = post.sampleUrl,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
contentDescription = null,
|
||||
loading ={
|
||||
Box(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)) {
|
||||
loading = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(50.dp)
|
||||
.align(Alignment.Center))
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
},
|
||||
success = {
|
||||
@@ -121,5 +224,43 @@ fun PostItem(post:Post){
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clickable { clickable.invoke(post) }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DetailScreen(navController: NavController, viewModel: PostViewModel) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.actionDownload()}) {
|
||||
Icon(painter = painterResource(id = R.drawable.ic_twotone_arrow_downward_24), contentDescription = "Download")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
content = {
|
||||
AsyncImage(
|
||||
model = uiState.selectPost?.sampleUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = it.calculateTopPadding())
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.lsp.view.ui.compose
|
||||
|
||||
import com.lsp.view.bean.Post
|
||||
|
||||
data class PostUiState(
|
||||
var refresh:Boolean = false,
|
||||
var searchTarget: String = "",
|
||||
val source: String = "",
|
||||
val safeModel: Boolean = false, //安全模式
|
||||
var page:Int = 1,
|
||||
var selectPost: Post? = null
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lsp.view.ui.compose
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lsp.view.bean.YandPost
|
||||
@@ -17,6 +18,7 @@ class PostViewModel:ViewModel() {
|
||||
private val repository by lazy {
|
||||
PostRepository()
|
||||
}
|
||||
val downloadAction = MutableLiveData<Unit>()
|
||||
|
||||
init {
|
||||
initData()
|
||||
@@ -26,6 +28,10 @@ class PostViewModel:ViewModel() {
|
||||
fetchPost()
|
||||
}
|
||||
|
||||
fun actionDownload(){
|
||||
downloadAction.postValue(Unit)
|
||||
}
|
||||
|
||||
fun fetchPost(){
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_uiState.value.refresh = true
|
||||
|
||||
@@ -130,17 +130,6 @@ class ImageFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun download(fileUrl: String){
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
activityContext.viewModel.postNewToast("Start download")
|
||||
val result = (activityContext.application as YandViewApplication).downloadBinder.downloadImage(fileUrl)
|
||||
if (result.isSuccess){
|
||||
activityContext.viewModel.postNewToast("Download successfully")
|
||||
}else{
|
||||
val exception = result.exceptionOrNull()
|
||||
activityContext.viewModel.postNewToast(exception?.message.toString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user