简单的if else 举例:
@Composable
fun App() {
val context = LocalContext.current
var isOnline by remember { mutableStateOf(checkIfOnline(context)) }
if (isOnline) {
Home()
} else {
OfflineDialog { isOnline = checkIfOnline(context) }
}
}
首先定义一个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方便!
这个就不介绍了。
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
对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
因为需要跨项目引用,我把通用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是因为在外层配置导航比较通用,不过可以把导航信息配置在枚举类中,这样封装可能更彻底。
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