compose UI(一)属于compose的页面导航,自定义实现ViewPager(Demo)

1. 声明式compose ui在简单页面可以通过隐藏,显示来实现页面切换。

简单的if else 举例:

@Composable
fun App() {
    val context = LocalContext.current
    var isOnline by remember { mutableStateOf(checkIfOnline(context)) }

    if (isOnline) {
        Home()
    } else {
        OfflineDialog { isOnline = checkIfOnline(context) }
    }
}

2. "androidx.navigation:navigation-compose"导航库使用方式

首先定义一个Compose组件,然后给组件一个导航状态。

@Composable
fun navController() {
    val navController = rememberNavController()
    val context = LocalContext.current
}

然后建立路由表

sealed class NavScreen(val route: String) {
    object AScreen : NavScreen("AScreen ")

    object BScreen: NavScreen("BScreen")

    object CScreen: NavScreen("CScreen")
}

建立导航关系

@Composable
fun navController() {
    val navController = rememberNavController()
    val context = LocalContext.current
	//指定起始页面 startDestination = NavScreen.AScreen.route
	NavHost(navController = navController, startDestination = NavScreen.AScreen.route) {
        composable(NavScreen.AScreen .route) { backStackEntry ->
        	//从堆返回一个hilt ViewModel
            val viewModel = hiltNavGraphViewModel<MainViewModel>(backStackEntry = backStackEntry)
            AScreen (
                mainViewModel = viewModel,
                selectScreen = {
                    navController.navigate(it)
                }
            )

        }
        composable(
            route = NavScreen.BScreen .route,
            arguments = listOf(navArgument("动画") { type = NavType.LongType })
        ) { backStackEntry ->
            val viewModel = hiltNavGraphViewModel<MainViewModel>(backStackEntry = backStackEntry)
            BScreen (
                mainViewModel = viewModel,
                pressOnBack = {
                	//返回前一个页面
                    navController.popBackStack(navController.graph.startDestination, false)
                })
        }
        composable(NavScreen.CScreen.route) { backStackEntry ->
            val viewModel = hiltNavGraphViewModel<MainViewModel>(backStackEntry = backStackEntry)
            CScreen(
                mainViewModel = viewModel,
                pressOnBack = {
                    navController.popBackStack(navController.graph.startDestination, false)
                })
        }
    }
}

使用

onClick = { selectScreen("CScreen") }

优点:可以不用再通过反射拦截StartActivity去启动没有注册的Activity!

缺点: 做为单Activity,没有用navigation可视化页面托动Fragment方便!

3. 通过StartActivity导航

这个就不介绍了。


6月9日更新文章:

1.0.0-bate06 升级 1.0.0-bate07 需要修改navigation-compose版本
根据官网描述

implementation "androidx.navigation:navigation-compose:1.0.0-alpha10"

改为

implementation "androidx.navigation:navigation-compose:2.4.0-alpha01"

2.4.0-alpha01 弃用了上面的navController.graph.startDestination 改为 navController.graph.startDestinationId


7月10日更新

单Activity如何导航

对if else方式的一种补充,可以利用Crossfade配合when,状态通过单Activity的viewModel管理,简单示例:
MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor(
    private val dbRepository: DbRepository
) : BaseViewModel() {

    private val _selectedTab: MutableState<Int> = mutableStateOf(R.string.nav_a)
    val selectedTab: State<Int> get() = _selectedTab

    @MainThread
    fun selectTab(@StringRes tab: Int) {
        _selectedTab.value = tab
    }
}

然后在导航页面,比如:
Choose.kt

@Composable
fun ChooseScreen(
    viewModel: MainViewModel
) {
    val selectedTab = NavScreen.getNavScreen(viewModel.selectedTab.value)
    Crossfade(targetState = selectedTab) { destination ->
        when (destination) {
            NavScreen.A -> { AScreen() }
            NavScreen.B-> { BScreen() }
            NavScreen.C-> { CScreen(viewModel) }
            NavScreen.D-> {DScreen(mainViewModel = viewModel)}
        }
    }
}

enum class NavScreen(
    @StringRes val route: Int
) {
    A(R.string.nav_a),
    B(R.string.nav_b),
    C(R.string.nav_c,
    D(R.string.nav_d);

    companion object {
        fun getNavScreen(@StringRes route: Int): NavScreen {
            for (e in values()) {
                if (e.route == route) {
                    return e
                }
            }
            return A
        }
    }
}

需要导航时,我们只要在任何onClick方法中改变viewModel中selectedTab(所以上面代码是用=,不用remember)

onClick= { mainViewModel.selectTab(R.string.A) }

这里引申一下,就可以实现我们自定义的ViewPager

自定义ViewPager

因为需要跨项目引用,我把通用Compose组件模块化了,所以有一个接口。不用密闭类接口是因为不能跨包访问,最后选择枚举+接口封装。
ITabPage.kt

interface ITabPage {
    val title: Int
    val icon: Int
    val backColor: Int
    val serial: Int
}

ViewPager.kt

@Composable
fun ViewPager(
    backgroundColor: Color,
    settingTabPage: ITabPage,
    tabPagesList: List<ITabPage>,
    onTabSelected: (settingTabPage: ITabPage) -> Unit
) {
    TabRow(
    	// 切换动画是仿造google AnimationCodeLab
        //google在这使用ordinal 是不规范的,Enum规范中说明,ordinal是给EnumSet和EnumMap使用
        selectedTabIndex = settingTabPage.serial,
        backgroundColor = backgroundColor,
        indicator = { tabPositions ->
            HomeTabIndicator(tabPositions, settingTabPage)
        }
    ) {

        tabPagesList.forEach { tab ->
            TabItem(icon = painterResource(tab.icon), title = stringResource(tab.title), onClick = {
                onTabSelected(tab)
            })
        }

    }
}

/**
 * 显示选项卡的指示器。
 */
@Composable
private fun HomeTabIndicator(
    tabPositions: List<TabPosition>,
    settingTabPage: ITabPage
) {
    val transition = updateTransition(
        settingTabPage,
        label = "Tab indicator"
    )
    val indicatorLeft by transition.animateDp(
        transitionSpec = {
            if (this.initialState.serial < this.targetState.serial){
                spring(stiffness = Spring.StiffnessVeryLow)
            }else{
                spring(stiffness = Spring.StiffnessLow)
            }
        },
        label = "Indicator left"
    ) { page ->
        tabPositions[page.serial].left
    }
    val indicatorRight by transition.animateDp(
        transitionSpec = {
            if (this.initialState.serial < this.targetState.serial){
                spring(stiffness = Spring.StiffnessLow)
            }else{
                spring(stiffness = Spring.StiffnessVeryLow)
            }
        },
        label = "Indicator right"
    ) { page ->
        tabPositions[page.serial].right
    }

    val color by transition.animateColor(
        label = "Border color"
    ) { page ->
        colorResource(id = page.backColor)
    }
    Box(
        Modifier
            .fillMaxSize()
            .wrapContentSize(align = Alignment.BottomStart)
            .offset(x = indicatorLeft)
            .width(indicatorRight - indicatorLeft)
            .padding(4.dp)
            .fillMaxSize()
            .background(color = color.copy(alpha = 0.3f))
        /*.border(
            BorderStroke(2.dp, color),
            RoundedCornerShape(4.dp)
        )*/
        //需求说要切换底色不要框,要框可以放开注释,过着增加入参封装
    )
}

/**
 * 显示一个标签。
 */
@Composable
private fun TabItem(
    icon: Painter,
    title: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier
            .clickable(onClick = onClick)
            .padding(16.dp),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Image(
            painter = icon,
            modifier = Modifier.size(30.dp),
            contentDescription = null
        )
        Spacer(modifier = Modifier.width(16.dp))
        Text(text = title)
    }
}

使用示例(修正,ViewPager和Crossfade应该被Cloum或Scaffold布局):

@Composable
fun ConfigScreen(){

    var tabPage: ITabPage by remember { mutableStateOf(TabPage.INFORMATION) }
    val tabPages : List<ITabPage> by remember{ mutableStateOf(TabPage.values().toList()) }

    ViewPager(
        backgroundColor = MaterialTheme.colors.background,
        settingTabPage = tabPage,
        tabPagesList = tabPages) { tabPage = it }

    Crossfade(targetState = tabPage) { destination ->
        when(destination) {
            TabPage.INFORMATION -> { InformationConfigPage() }
            TabPage.OPERATOR -> { OperatorConfigPage() }
            TabPage.SPECIMEN -> { SpecimenConfigPage() }
        }
    }
}

internal enum class TabPage(
    @StringRes override val title: Int,
    @DrawableRes override val icon: Int,
    override val serial: Int,
    @ColorRes override val backColor: Int
) : ITabPage{
    INFORMATION(R.string.tabpage_information,R.drawable.information_config,0,R.color.primaryDarkColor),
    OPERATOR(R.string.tabpage_operator,R.drawable.config,1,R.color.primaryDarkColor),
    SPECIMEN(R.string.tabpage_specimen,R.drawable.specimen_config,2,R.color.primaryDarkColor);
}

Crossfade不封装进ViewPager是因为在外层配置导航比较通用,不过可以把导航信息配置在枚举类中,这样封装可能更彻底。


7月27日更新 :

androidx.hilt:hilt-navigation-compose版本升级到 1.0.0-alpha03
hiltNavGraphViewModel 弃用,用法更新为:

	 val viewModel = hiltViewModel<MainViewModel>()

或者

//推荐
composable(NavScreen.DataManager.route) { backStackEntry ->
	 val viewModel = hiltViewModel<MainViewModel>(backStackEntry)
}

个人经验和一些看法。compose ui 非常适合单Activity应用,切换页面时可以做到基于view级别。这样之后MVVM架构理解起来会有些困难。单一个viewModel会导致高度耦合,其实是理解viewModel的偏差,个人理解viewModel是view的model,应该是与view绑定,至于实际使用总是绑定Activity是因为Activity有一个ViewGroup。还有一个是Lifecycle的误导。我用profiler测试val viewModel = hiltViewModel(backStackEntry)这种方式,内存中是会回收实例的。单Activity如果app不复杂的情况下,可以不使用导航,用前面Crossfade,然后viewmodel可以这样:

@AndroidEntryPoint
class MainActivity() : ComponentActivity() {

    @VisibleForTesting
    val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LocalMainViewModel = compositionLocalOf { viewModel }
            TemplateTheme {
                Surface(color = MaterialTheme.colors.background) {
                    Screen()
                }
            }
        }
    }
}

之后在任何的screen中就可以获取viewmodel

val viewModel = LocalMainViewModel.current

你可能感兴趣的:(android,android)