> = flow {
45 | try {
46 | withTimeout(timeoutMs) {
47 | emit(InvokeStatus.Start())
48 | val result:R = doWork(params)
49 | emit(InvokeStatus.Success(result))
50 | }
51 | } catch (t: TimeoutCancellationException) {
52 | val msg = t.message ?: "TimeoutCancellationException"
53 | emit(InvokeStatus.Error(message = msg))
54 | }
55 | }.catch { t ->
56 | val msg = t.message ?: "Interactor Invoke Error"
57 | emit(InvokeStatus.Error(message = msg))
58 | }
59 |
60 | suspend fun executeSync(params: P) = doWork(params)
61 |
62 | protected abstract suspend fun doWork(params: P):R
63 |
64 | companion object {
65 | private val defaultTimeoutMs = TimeUnit.MINUTES.toMillis(5)
66 | }
67 | }
68 |
69 | abstract class SubjectInteractor {
70 | private val paramState = MutableSharedFlow
(
71 | replay = 1,
72 | extraBufferCapacity = 1,
73 | onBufferOverflow = BufferOverflow.DROP_OLDEST
74 | )
75 |
76 | @OptIn(ExperimentalCoroutinesApi::class)
77 | val flow: Flow = paramState
78 | .distinctUntilChanged()
79 | .flatMapLatest {
80 | createObservable(it)
81 | }
82 | .distinctUntilChanged()
83 |
84 | operator fun invoke(params: P) {
85 | paramState.tryEmit(params)
86 | }
87 |
88 | protected abstract fun createObservable(params: P): Flow
89 | }
90 |
91 | abstract class PagingInteractor, T : Any> : SubjectInteractor
>() {
92 | interface Parameters {
93 | val pagingConfig: PagingConfig
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/lib_compose/src/main/java/com/rock/lib_compose/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.rock.lib_compose.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_theme_light_primary = Color(0xFF6750A4)
6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_theme_light_primaryContainer = Color(0xFFE9DDFF)
8 | val md_theme_light_onPrimaryContainer = Color(0xFF22005D)
9 | val md_theme_light_secondary = Color(0xFF4A58A9)
10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
11 | val md_theme_light_secondaryContainer = Color(0xFFDEE0FF)
12 | val md_theme_light_onSecondaryContainer = Color(0xFF000F5D)
13 | val md_theme_light_tertiary = Color(0xFF215FA6)
14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
15 | val md_theme_light_tertiaryContainer = Color(0xFFD5E3FF)
16 | val md_theme_light_onTertiaryContainer = Color(0xFF001C3B)
17 | val md_theme_light_error = Color(0xFFBA1A1A)
18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
19 | val md_theme_light_onError = Color(0xFFFFFFFF)
20 | val md_theme_light_onErrorContainer = Color(0xFF410002)
21 | val md_theme_light_background = Color(0xFFFFFBFF)
22 | val md_theme_light_onBackground = Color(0xFF1C1B1E)
23 | val md_theme_light_surface = Color(0xFFFFFBFF)
24 | val md_theme_light_onSurface = Color(0xFF1C1B1E)
25 | val md_theme_light_surfaceVariant = Color(0xFFE7E0EB)
26 | val md_theme_light_onSurfaceVariant = Color(0xFF49454E)
27 | val md_theme_light_outline = Color(0xFF7A757F)
28 | val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
29 | val md_theme_light_inverseSurface = Color(0xFF313033)
30 | val md_theme_light_inversePrimary = Color(0xFFCFBCFF)
31 | val md_theme_light_shadow = Color(0xFF000000)
32 | val md_theme_light_surfaceTint = Color(0xFF6750A4)
33 |
34 | val md_theme_dark_primary = Color(0xFFCFBCFF)
35 | val md_theme_dark_onPrimary = Color(0xFF381E72)
36 | val md_theme_dark_primaryContainer = Color(0xFF4F378A)
37 | val md_theme_dark_onPrimaryContainer = Color(0xFFE9DDFF)
38 | val md_theme_dark_secondary = Color(0xFFBBC3FF)
39 | val md_theme_dark_onSecondary = Color(0xFF182778)
40 | val md_theme_dark_secondaryContainer = Color(0xFF313F90)
41 | val md_theme_dark_onSecondaryContainer = Color(0xFFDEE0FF)
42 | val md_theme_dark_tertiary = Color(0xFFA6C8FF)
43 | val md_theme_dark_onTertiary = Color(0xFF00315F)
44 | val md_theme_dark_tertiaryContainer = Color(0xFF004787)
45 | val md_theme_dark_onTertiaryContainer = Color(0xFFD5E3FF)
46 | val md_theme_dark_error = Color(0xFFFFB4AB)
47 | val md_theme_dark_errorContainer = Color(0xFF93000A)
48 | val md_theme_dark_onError = Color(0xFF690005)
49 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
50 | val md_theme_dark_background = Color(0xFF1C1B1E)
51 | val md_theme_dark_onBackground = Color(0xFFE6E1E6)
52 | val md_theme_dark_surface = Color(0xFF1C1B1E)
53 | val md_theme_dark_onSurface = Color(0xFFE6E1E6)
54 | val md_theme_dark_surfaceVariant = Color(0xFF49454E)
55 | val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4CF)
56 | val md_theme_dark_outline = Color(0xFF948F99)
57 | val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1E)
58 | val md_theme_dark_inverseSurface = Color(0xFFE6E1E6)
59 | val md_theme_dark_inversePrimary = Color(0xFF6750A4)
60 | val md_theme_dark_shadow = Color(0xFF000000)
61 | val md_theme_dark_surfaceTint = Color(0xFFCFBCFF)
62 |
63 |
64 | val seed = Color(0xFF6750A4)
65 |
--------------------------------------------------------------------------------
/lib_compose/src/main/java/com/rock/lib_compose/widget/WanScaffolds.kt:
--------------------------------------------------------------------------------
1 | package com.rock.lib_compose.widget
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.SideEffect
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.unit.dp
10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
11 |
12 |
13 |
14 | @Composable
15 | fun PageScaffold(
16 | modifier: Modifier = Modifier,
17 | statusBarColor: Color = Color.Transparent,
18 | navigationBarColor: Color = Color.Transparent,
19 | //是否使用暗模式 Icon ,true 灰色, false 白色
20 | useDarkModeIcons:(() -> Boolean)? = null,
21 | topBar: @Composable () -> Unit = {},
22 | bottomBar: @Composable () -> Unit = {},
23 | snackbarHost: @Composable () -> Unit = {},
24 | floatingActionButton: @Composable () -> Unit = {},
25 | floatingActionButtonPosition: FabPosition = FabPosition.End,
26 | containerColor: Color = MaterialTheme.colorScheme.background,
27 | contentColor: Color = contentColorFor(containerColor),
28 | content: @Composable (PaddingValues) -> Unit
29 | ) {
30 |
31 | val systemUiController = rememberSystemUiController()
32 | val isUseDarkModeIcons = if (useDarkModeIcons != null){
33 | useDarkModeIcons()
34 | }else{
35 | shouldUseDarkModeIcons(bgColor = statusBarColor)
36 | }
37 |
38 | SideEffect {
39 | setSystemBarsColor(systemUiController,isUseDarkModeIcons,statusBarColor, navigationBarColor)
40 | }
41 |
42 | Scaffold(
43 | modifier = modifier,
44 | topBar = topBar,
45 | bottomBar = bottomBar,
46 | snackbarHost = snackbarHost,
47 | floatingActionButton = floatingActionButton,
48 | floatingActionButtonPosition = floatingActionButtonPosition,
49 | containerColor = containerColor,
50 | contentColor = contentColor,
51 | contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
52 | content = content
53 | )
54 | }
55 |
56 | @Composable
57 | fun FullScreenScaffold(
58 | modifier: Modifier = Modifier,
59 | topBar: @Composable () -> Unit = {},
60 | bottomBar: @Composable () -> Unit = {},
61 | snackbarHost: @Composable () -> Unit = {},
62 | floatingActionButton: @Composable () -> Unit = {},
63 | floatingActionButtonPosition: FabPosition = FabPosition.End,
64 | containerColor: Color = MaterialTheme.colorScheme.background,
65 | contentColor: Color = contentColorFor(containerColor),
66 | content: @Composable (PaddingValues) -> Unit
67 | ) {
68 | val systemUiController = rememberSystemUiController()
69 | val isUseDarkModeIcons = shouldUseDarkModeIcons()
70 | SideEffect {
71 | transparentSystemBars(systemUiController,isUseDarkModeIcons)
72 | }
73 | Scaffold(
74 | modifier = modifier,
75 | topBar = topBar,
76 | bottomBar = bottomBar,
77 | snackbarHost = snackbarHost,
78 | floatingActionButton = floatingActionButton,
79 | floatingActionButtonPosition = floatingActionButtonPosition,
80 | containerColor = containerColor,
81 | contentColor = contentColor,
82 | contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
83 | content = content
84 | )
85 | }
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/java/com/rock/wanandroid/ui/WanAppState.kt:
--------------------------------------------------------------------------------
1 | package com.rock.wanandroid.ui
2 |
3 | import android.content.res.Resources
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.*
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.runtime.*
8 | import androidx.navigation.NavGraph.Companion.findStartDestination
9 | import androidx.navigation.NavHostController
10 | import androidx.navigation.compose.currentBackStackEntryAsState
11 | import androidx.navigation.compose.rememberNavController
12 | import com.rock.lib_compose.arch.ComposeState
13 | import com.rock.lib_compose.arch.resources
14 | import com.rock.ui_discovery.route.DiscoveryScreens
15 | import com.rock.ui_home.route.HomeScreens
16 | import com.rock.ui_profile.route.ProfileScreens
17 | import com.rock.ui_project.route.ProjectScreens
18 | import com.rock.ui_square.route.SquareScreens
19 | import kotlinx.coroutines.CoroutineScope
20 |
21 |
22 | @Composable
23 | fun rememberWanAppState(
24 | navController: NavHostController = rememberNavController(),
25 | selectedBottomBarIndexState:MutableState = remember { mutableStateOf(0) },
26 | resources: Resources = resources(),
27 | coroutineScope:CoroutineScope = rememberCoroutineScope()
28 | ) = remember(navController,selectedBottomBarIndexState){
29 | WanAppState(selectedBottomBarIndexState,navController,resources,coroutineScope)
30 | }
31 |
32 | class WanAppState(
33 | private val selectedBottomItemState:MutableState,
34 | override val navController: NavHostController,
35 | override val resources: Resources,
36 | override val coroutineScope: CoroutineScope,
37 | ):ComposeState() {
38 | val bottomBarItems = BottomBarItem.values()
39 |
40 | private val bottomBarRoutes = bottomBarItems.map { it.route }
41 |
42 | val shouldShowNavBar: Boolean
43 | @Composable get() = navController
44 | .currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes
45 | val selectedBottomItemIndex
46 | get() = selectedBottomItemState.value
47 |
48 | val title:String
49 | get() = bottomBarItems[selectedBottomItemIndex].label
50 |
51 | fun navigateToBottomItem(index: Int) {
52 | if (index != selectedBottomItemIndex) {
53 | selectedBottomItemState.value = index
54 | navController.navigate(bottomBarItems[index].route) {
55 | popUpTo(navController.graph.findStartDestination().id) {
56 | saveState = true
57 | }
58 | launchSingleTop = true
59 | restoreState = true
60 | }
61 | }
62 | }
63 |
64 | fun navigateToProfile(){
65 | this.navigate(ProfileScreens.Index.createRoute())
66 | }
67 |
68 | }
69 |
70 | enum class BottomBarItem(
71 | val label: String,
72 | val icon: @Composable () -> Unit,
73 | val route: String
74 | ) {
75 | Home(
76 | "首页",
77 | { Icon(imageVector = Icons.Filled.Home, contentDescription = "") },
78 | HomeScreens.Index.route
79 | ),
80 | Discovery(
81 | "发现",
82 | { Icon(imageVector = Icons.Filled.Search, contentDescription = "") },
83 | DiscoveryScreens.Index.route
84 | ),
85 | Project(
86 | "项目",
87 | { Icon(imageVector = Icons.Filled.Menu, contentDescription = "") },
88 | ProjectScreens.Index.route
89 | ),
90 | Compose(
91 | "Compose",
92 | { Icon(imageVector = Icons.Filled.Build, contentDescription = "") },
93 | SquareScreens.Index.route
94 | ),
95 | }
--------------------------------------------------------------------------------
/lib_compose/src/main/java/com/rock/lib_compose/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.rock.lib_compose.navigation
2 |
3 | import androidx.navigation.NamedNavArgument
4 | import androidx.navigation.NavDeepLink
5 | import androidx.navigation.NavType
6 | import androidx.navigation.navArgument
7 | import java.lang.IllegalArgumentException
8 | import java.lang.StringBuilder
9 |
10 |
11 | interface OutRoutes
12 | abstract class Screen(private val path:String){
13 | //根路由,相当于分组标签
14 | abstract val root:String
15 |
16 |
17 | open val arguments:List = emptyList()
18 |
19 | val argumentsWithRequestCode:List = mutableListOf().apply {
20 | addAll(arguments)
21 | add(navArgument(name = NavRequestCodeKey) {
22 | type = NavType.StringType
23 | nullable = true
24 | })
25 | }
26 |
27 |
28 | open val deepLinks:List = emptyList()
29 |
30 | lateinit var outRoutes:T
31 |
32 | //必填参数名
33 | private val requiredArgs:MutableList = mutableListOf()
34 | //可选参数名+默认值 map
35 | private val optionalArgs:MutableMap = mutableMapOf(NavRequestCodeKey to null)
36 |
37 | private val routePath by lazy {
38 | if (root.isEmpty()) path else "$root/${path}"
39 | }
40 |
41 | /**
42 | * 自动生成的 route ,配置 DSL 时使用
43 | * route = rootRoute/path/必填参数?可选参数
44 | */
45 | val route:String by lazy {
46 | val sb = StringBuilder(routePath)
47 | //解析参数时将 必选/可选参数 分别保存起来
48 | for (arg in argumentsWithRequestCode){
49 | if (arg.argument.isNullable || arg.argument.isDefaultValuePresent){
50 | //可选参数
51 | sb.append("?${arg.name}={${arg.name}}")
52 | optionalArgs[arg.name] = arg.argument.defaultValue
53 | }else{
54 | sb.append("/{${arg.name}}")
55 | requiredArgs.add(arg.name)
56 | }
57 | }
58 | sb.toString()
59 | }
60 |
61 | /**
62 | * 通用方法
63 | * 根据传入的 map 生成 navigate 方法所需 route
64 | * map 中必须包括所有的必填参数
65 | * @param args Map key:参数名,value:参数值
66 | * @return String 调用 navigate 方法所需 route
67 | */
68 | fun createRoute(args:Map = emptyMap()):String{
69 | val sb = StringBuilder(routePath)
70 | if (args.isEmpty() && requiredArgs.isNotEmpty()){
71 | throw IllegalArgumentException("param [args:Map] can't be empty")
72 | }else{
73 | for (requiredArg in requiredArgs){
74 | if (!args.containsKey(requiredArg)){
75 | throw IllegalArgumentException("required argument $requiredArg can't find in param [args:Map]")
76 | }
77 | sb.append("/${args[requiredArg]}")
78 | }
79 | for (optionalArg in optionalArgs){
80 | if (args.containsKey(optionalArg.key)){ //参数中有可选参数
81 | sb.append("?${optionalArg.key}=${args[optionalArg.key]}")
82 | }else if (optionalArg.value != null){ // 可选参数有默认值
83 | sb.append("?${optionalArg.key}=${optionalArg.value}")
84 | }
85 | }
86 | }
87 | return sb.toString()
88 | }
89 |
90 | fun createForResultRoute(requestCode:String,args:Map = emptyMap()):String{
91 | val withRequestCode = mutableMapOf(NavRequestCodeKey to requestCode)
92 | withRequestCode.putAll(args)
93 | return createRoute(withRequestCode)
94 | }
95 | }
--------------------------------------------------------------------------------
/lib_compose/src/main/java/com/rock/lib_compose/theme/WindowSize.kt:
--------------------------------------------------------------------------------
1 | package com.rock.lib_compose.theme
2 |
3 | import android.app.Activity
4 | import android.content.res.Configuration
5 | import androidx.compose.material3.windowsizeclass.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.staticCompositionLocalOf
8 | import androidx.compose.ui.platform.LocalConfiguration
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.compose.ui.unit.Dp
11 | import androidx.compose.ui.unit.DpSize
12 | import androidx.compose.ui.unit.dp
13 | import kotlin.math.roundToInt
14 |
15 |
16 | interface WindowCompatConfig {
17 | //小型设备设计稿宽高
18 | val designCompactSize: DpSize?
19 | //中型设备设计稿宽高
20 | val designMediumSize: DpSize?
21 | //大型设备设计稿宽高
22 | val designExpandSize: DpSize?
23 |
24 | companion object None:WindowCompatConfig{
25 | override val designCompactSize: DpSize?
26 | get() = null
27 | override val designMediumSize: DpSize?
28 | get() = null
29 | override val designExpandSize: DpSize?
30 | get() = null
31 | }
32 | }
33 |
34 |
35 |
36 | data class AutoWindowInfo(
37 | val density: Float, // 适配后的 density
38 | val screenWidthDp:Dp,// 适配后宽度 dp
39 | val screenHeightDp:Dp,
40 | val windowSizeClass: WindowSizeClass,//当前 WindowSize 级别
41 | )
42 |
43 | val LocalAutoWindowInfo = staticCompositionLocalOf { error("No AutoWindowInfo provided") }
44 |
45 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
46 | @Composable
47 | fun getAutoWindowInfo(activity: Activity, config: WindowCompatConfig): AutoWindowInfo{
48 | val sizeClass = calculateWindowSizeClass(activity)
49 | val orientation = LocalConfiguration.current.orientation
50 | val displayMetrics = LocalContext.current.resources.displayMetrics
51 | val screenWidth = displayMetrics.widthPixels
52 | val screenHeight = displayMetrics.heightPixels
53 | var density = displayMetrics.density
54 | if (orientation == Configuration.ORIENTATION_PORTRAIT){
55 | when (sizeClass.widthSizeClass) {
56 | WindowWidthSizeClass.Compact -> {
57 | config.designCompactSize?.let {
58 | density = getDensity(screenWidth,it.width)
59 | }
60 | }
61 | WindowWidthSizeClass.Medium -> {
62 | config.designMediumSize?.let {
63 | density = getDensity(screenWidth,it.width)
64 | }
65 | }
66 | WindowWidthSizeClass.Expanded -> {
67 | config.designExpandSize?.let {
68 | density = getDensity(screenWidth,it.width)
69 | }
70 | }
71 | else -> {
72 | }
73 | }
74 | }else{
75 | when (sizeClass.heightSizeClass) {
76 | WindowHeightSizeClass.Compact -> {
77 | config.designCompactSize?.let {
78 | density = getDensity(screenWidth,it.height)
79 | }
80 | }
81 | WindowHeightSizeClass.Medium -> {
82 | config.designMediumSize?.let {
83 | density = getDensity(screenWidth,it.height)
84 | }
85 | }
86 | WindowHeightSizeClass.Expanded -> {
87 | config.designExpandSize?.let {
88 | density = getDensity(screenWidth,it.height)
89 | }
90 | }
91 | else -> {
92 | }
93 | }
94 | }
95 |
96 | val screenWidthDp = (screenWidth / density).roundToInt().dp
97 | val screenHeightDp = (screenHeight / density).roundToInt().dp
98 |
99 | return AutoWindowInfo(density,screenWidthDp,screenHeightDp,sizeClass)
100 | }
101 |
102 | private fun getDensity(deviceWidth:Int,designWidth: Dp): Float = deviceWidth / designWidth.value
103 |
--------------------------------------------------------------------------------
/ui-home/src/main/java/com/rock/ui_home/UiHome.kt:
--------------------------------------------------------------------------------
1 | package com.rock.ui_home
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.KeyboardArrowUp
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.FloatingActionButton
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalLifecycleOwner
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.navigation.NavController
17 | import androidx.paging.LoadState
18 | import androidx.paging.compose.items
19 | import com.rock.lib_base.ktx.fromHtml
20 | import com.rock.lib_compose.widget.ArticleCard
21 | import com.rock.lib_compose.widget.Banner
22 | import com.rock.lib_compose.widget.ImageBannerItem
23 | import com.rock.lib_compose.widget.PageScaffold
24 | import com.rock.lib_compose.widget.RefreshLazyColumn
25 |
26 | @Composable
27 | fun UiHome(navController: NavController, viewModel: HomeViewModel = hiltViewModel()) {
28 | LocalLifecycleOwner.current
29 | val homeState = rememberHomeState(viewModel = viewModel, navController = navController)
30 |
31 | PageScaffold(
32 | floatingActionButton = {
33 | if (homeState.shouldShowToTopButton) {
34 | FloatingActionButton(onClick = { homeState.dispatchAction(HomeAction.ToListTop) }) {
35 | Icon(imageVector = Icons.Default.KeyboardArrowUp, contentDescription = "")
36 | }
37 | }
38 | }
39 | ) { paddingValues ->
40 | Column() {
41 | Banner(items = homeState.banners) {
42 | ImageBannerItem(modifier = Modifier.height(200.dp), imageUrl = it.imagePath)
43 | }
44 | RefreshLazyColumn(
45 | modifier = Modifier
46 | .fillMaxSize()
47 | .padding(top = 4.dp),
48 | state = homeState.lazyListState,
49 | contentPadding = PaddingValues(horizontal = 4.dp),
50 | verticalArrangement = Arrangement.spacedBy(2.dp),
51 | onRefresh = { homeState.dispatchAction(HomeAction.RefreshList) },
52 | isRefreshing = homeState.pagedArticle.loadState.refresh == LoadState.Loading,
53 | shouldShowLoadingState = { homeState.pagedArticle.loadState.append == LoadState.Loading }
54 | ) {
55 | //置顶文章
56 | items(homeState.topics, key = { it.id }) { topic ->
57 | val title = topic.title.fromHtml()
58 | ArticleCard(
59 | title = title,
60 | author = topic.author,
61 | date = topic.niceDate,
62 | onCollectedClick = {
63 | homeState.dispatchAction(
64 | HomeAction.CollectArticle(
65 | topic.id
66 | )
67 | )
68 | },
69 | onClick = {}
70 | )
71 | }
72 | //分页文章
73 | items(homeState.pagedArticle, key = { it.id }) { article ->
74 | val title = article?.title?.fromHtml()
75 | ArticleCard(
76 | title = title,
77 | author = article?.author,
78 | date = article?.niceDate,
79 | showPlaceHolder = article == null,
80 | onCollectedClick = {},
81 | onClick = {}
82 | )
83 | }
84 | }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/ui-discovery/src/main/java/com/rock/ui_discovery/UiDiscoveryState.kt:
--------------------------------------------------------------------------------
1 | package com.rock.ui_discovery
2 |
3 | import android.content.res.Resources
4 | import android.util.Log
5 | import androidx.compose.foundation.lazy.LazyListState
6 | import androidx.compose.foundation.lazy.rememberLazyListState
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.RememberObserver
9 | import androidx.compose.runtime.State
10 | import androidx.compose.runtime.remember
11 | import androidx.navigation.NavController
12 | import androidx.paging.compose.LazyPagingItems
13 | import androidx.paging.compose.collectAsLazyPagingItems
14 | import com.rock.lib_compose.arch.ComposeVmState
15 | import com.rock.lib_compose.arch.commonState
16 | import com.rock.lib_compose.widget.LazyListFirstItemInfo
17 | import com.rock.wan_data.entity.Article
18 | import kotlinx.coroutines.CoroutineScope
19 |
20 |
21 | @Composable
22 | fun rememberUiDiscoveryState(
23 | navController: NavController,
24 | viewModel: DiscoveryViewModel
25 | ): UiDiscoveryState {
26 |
27 | val selectedTabIndex = viewModel.selectedTabIndexState
28 | val pagedWenda = viewModel.wendaPagingDataFlow.collectAsLazyPagingItems()
29 | val pagedSquare = viewModel.squarePagingDataFlow.collectAsLazyPagingItems()
30 | val wendaListState = rememberLazyListState(viewModel.wendaFirstItemInfo.index,viewModel.wendaFirstItemInfo.scrollOffset)
31 | val squareListState = rememberLazyListState(viewModel.squareFirstItemInfo.index,viewModel.squareFirstItemInfo.scrollOffset)
32 |
33 | val (isLoading, resources, coroutineScope) = commonState(vm = viewModel)
34 | return remember(viewModel) {
35 | UiDiscoveryState(
36 | selectedTabIndex = selectedTabIndex,
37 | pagedWenda = pagedWenda,
38 | pagedSquare = pagedSquare,
39 | wendaListState = wendaListState,
40 | squareListState = squareListState,
41 | navController = navController,
42 | viewModel = viewModel,
43 | resources = resources,
44 | coroutineScope = coroutineScope,
45 | isLoading = isLoading
46 | )
47 | }
48 | }
49 | private const val TAG = "UiDiscoveryState"
50 |
51 | class UiDiscoveryState(
52 | val selectedTabIndex: State,
53 | private val pagedWenda: LazyPagingItems,
54 | private val pagedSquare: LazyPagingItems,
55 | private val wendaListState: LazyListState,
56 | private val squareListState: LazyListState,
57 | override val navController: NavController,
58 | override val viewModel: DiscoveryViewModel,
59 | override val resources: Resources,
60 | override val coroutineScope: CoroutineScope,
61 | override val isLoading: Boolean
62 | ) : ComposeVmState(),RememberObserver {
63 |
64 | val currentPagingItems
65 | get() = if (selectedTabIndex.value == 0)pagedSquare else pagedWenda
66 | val currentListState
67 | get() = if (selectedTabIndex.value == 0) squareListState else wendaListState
68 |
69 | override fun dispatchAction(action: DiscoveryAction) {
70 | when(action){
71 | is DiscoveryAction.SelectTab -> selectTable(action.index)
72 | DiscoveryAction.RefreshList -> currentPagingItems.refresh()
73 | }
74 | wendaListState.firstVisibleItemScrollOffset
75 | }
76 |
77 | private fun selectTable(tabIndex: Int) {
78 | Log.e(TAG, "selectTable: $tabIndex" )
79 | viewModel.selectedTabIndexState.value = tabIndex
80 | }
81 |
82 | override fun onAbandoned() {
83 | }
84 |
85 | override fun onForgotten() {
86 | viewModel.wendaFirstItemInfo = LazyListFirstItemInfo(wendaListState.firstVisibleItemIndex,wendaListState.firstVisibleItemScrollOffset)
87 | viewModel.squareFirstItemInfo = LazyListFirstItemInfo(squareListState.firstVisibleItemIndex,squareListState.firstVisibleItemScrollOffset)
88 | }
89 |
90 | override fun onRemembered() {
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/lib_compose/src/main/java/com/rock/lib_compose/widget/ArticleCard.kt:
--------------------------------------------------------------------------------
1 | package com.rock.lib_compose.widget
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material.Card
8 | import androidx.compose.material.Icon
9 | import androidx.compose.material.IconToggleButton
10 | import androidx.compose.material.Text
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.Favorite
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.contentColorFor
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.alpha
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextOverflow
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 |
28 | @Composable
29 | fun ArticleCard(
30 | modifier: Modifier = Modifier,
31 | title:String?,
32 | author:String?,
33 | date:String?,
34 | collected:Boolean = false,
35 | showPlaceHolder:Boolean = false,
36 | onCollectedClick:(Boolean)->Unit,
37 | onClick:()->Unit,
38 | ){
39 | val collectedState = remember(collected) { mutableStateOf(collected) }
40 |
41 | Card(
42 | modifier = Modifier.then(modifier).clickable(onClick = onClick),
43 | shape = RoundedCornerShape(4.dp),
44 | backgroundColor = MaterialTheme.colorScheme.surface,
45 | contentColor = contentColorFor(backgroundColor = MaterialTheme.colorScheme.surface)
46 | ) {
47 | Row{
48 | Column(modifier = Modifier
49 | .weight(1f)
50 | .padding(8.dp)) {
51 | Text(
52 | modifier = Modifier
53 | .defaultPlaceHolder(showPlaceHolder)
54 | .defaultMinSize(minWidth = 150.dp),
55 | text = title ?: "",
56 | fontSize = 16.sp,
57 | fontWeight = FontWeight.Bold,
58 | maxLines = 1,
59 | overflow = TextOverflow.Ellipsis
60 | )
61 | Spacer(modifier = Modifier.height(8.dp))
62 | Row {
63 | Text(
64 | modifier = Modifier
65 | .defaultPlaceHolder(showPlaceHolder)
66 | .defaultMinSize(minWidth = 20.dp)
67 | .alpha(0.8f)
68 | ,
69 | text = author ?: "",
70 | fontSize = 12.sp,
71 | )
72 | if (author?.isEmpty() == false) {
73 | Spacer(modifier = Modifier.width(6.dp))
74 | }
75 |
76 | Text(
77 | modifier = Modifier
78 | .defaultPlaceHolder(showPlaceHolder)
79 | .defaultMinSize(minWidth = 30.dp)
80 | .alpha(0.8f),
81 | text = date ?: "",
82 | fontSize = 12.sp,
83 | )
84 | }
85 | }
86 |
87 | IconToggleButton(
88 | modifier = Modifier.align(Alignment.CenterVertically),
89 | checked = collectedState.value,
90 | onCheckedChange = { onCollectedClick(it) }) {
91 | val tint by animateColorAsState(targetValue = if (collectedState.value) Color.Magenta else Color.LightGray)
92 | Icon(Icons.Filled.Favorite, tint = tint, contentDescription = "")
93 | }
94 | }
95 | }
96 | }
97 |
98 |
--------------------------------------------------------------------------------