Jetpack Compose初体验--(导航、生命周期等)
普通导航
在Jetpack Compose中导航可以使用Jetpack中的Navigation组件,引入相关的扩展依赖就可以了 Navigation官方文档
implementation "androidx.navigation:navigation-compose:2.4.0-alpha01"
使用Navigation导航用到两个比较重要的对象NavHost和NavController。
- NavHost用来承载页面,和管理导航图
- NavController用来控制如何导航还有参数回退栈等
导航的路径使用字符串来表示,当使用NavController导航到某个页面的时候,NavHost内部会自动进行页面重组。
来个小栗子实践一下
@Composable
fun MainView(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first_screen"){
composable("first_screen"){
FirstScreen(navController = navController)
}
composable("second_screen"){
SecondScreen(navController = navController)
}
composable("third_screen"){
ThirdScreen(navController = navController)
}
}
}
- 通过
rememberNavController()
方法创建navController对象 - 创建NavHost对象,传入navController并指定首页
- 通过composable()方法来往NavHost中添加页面,构造方法中的字符串就代表该页面的路径,后面的第二个参数就是具体的页面。
下面把这三个页面写出来,每个页面里面都有个按钮继续执行其他导航
@Composable
fun FirstScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize().background(Color.Blue),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
navController.navigate("second_screen")
}) {
Text(text = "I am First 点击我去Second")
}
}
}
@Composable
fun SecondScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("third_screen")
}) {
Text(text = "I am Second 点击我去Third")
}
}
}
@Composable
fun ThirdScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("first_screen")
}) {
Text(text = "I am Third 点击我去first")
}
}
}
这样一个简单的导航效果就完成了,感觉用了这个之后,要跟activity和fragment说拜拜了~~ ,全场只需一个activity加一堆可组合项(@Composable),新建一个页面简单了太多太多。
当然页面之间跳转传参是少不了的,Compose中如何传参呢?
参数传递肯定有发送端和接收端,navController是发送端,NavHost是接收端。先在NavHost中配置参数占位符,和接收取参数的方法。
@Composable
fun MainView(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first_screen"){
composable("first_screen"){
FirstScreen(navController = navController)
}
composable("second_screen/{userId}/{isShow}",
//默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type
arguments = listOf(navArgument("isShow"){type = NavType.BoolType})
){ backStackEntry ->
SecondScreen(navController = navController,
backStackEntry.arguments?.getString("userId"),
backStackEntry.arguments?.getBoolean("isShow")!!
)
}
composable("third_screen?selectable={selectable}",
arguments = listOf(navArgument("selectable"){defaultValue = "哈哈哈我是可选参数的默认值"})){
ThirdScreen(navController = navController,it.arguments?.getString("selectable"))
}
composable("four_screen"){
FourScreen(navController = navController)
}
}
}
如上代码,接收参数直接在在该页面地址后面添加参数占位符类似second_screen/{userId}/{isShow}
,然后通过arguments参数来接收arguments = listOf(navArgument("isShow"){type = NavType.BoolType})
。还可以通过defaultValue来定义参数的默认值。
默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type。
参数发送端更简单,参数直接跟到页面路径后面就可以,类似navController.navigate("second_screen/12345/true")
下面给前面的页面添加上参数
@Composable
fun FirstScreen(navController: NavController){
Column(modifier = Modifier
.fillMaxSize()
.background(Color.Blue),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
navController.navigate("second_screen/12345/true"){
}
}) {
Text(text = "I am First 点击我去Second")
}
Spacer(modifier = Modifier.size(30.dp))
}
}
@Composable
fun SecondScreen(navController: NavController,userId:String?,isShow:Boolean){
Column(modifier = Modifier
.fillMaxSize()
.background(Color.Green),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("third_screen?selectable=测试可选参数"){
popUpTo(navController.graph.startDestinationId){saveState = true}
}
}) {
Text(text = "I am Second 点击我去Third")
}
Spacer(modifier = Modifier.size(30.dp))
Text(text = "arguments ${userId}")
if(isShow){
Text(text = "测试boolean值")
}
}
}
@Composable
fun ThirdScreen(navController: NavController,selectable:String?){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("first_screen")
}) {
Text(text = "I am Third 点击我去first")
}
Spacer(modifier = Modifier.size(30.dp))
Button(onClick = {
navController.navigate("four_screen")
}) {
Text(text = "I am Third 点击我去four")
}
selectable?.let { Text(text = it) }
}
}
效果如下
生命周期
既然新的界面不使用activity或者fragment了,但是activity和fragment中的生命周期是非常有用的比如创建和销毁某些对象。那么Jetpack Compose中的每个组合函数的生命周期是怎样的呢?
可组合项的生命周期比视图比activity 和 fragment 的生命周期更简单,一般是进入组合、执行0次或者多次重组、退出组合。生命周期相关的函数主要有下面的几个,使用@Composable修饰的可组合函数中没有自带的生命周期函数,想要监听其生命周期,需要使用Effect API
- LaunchedEffect:第一次调用Compose函数的时候调用
- DisposableEffect:内部有一个 onDispose()函数,当页面退出时调用
- SideEffect:compose函数每次执行都会调用该方法
来个小例子体验一下
@Composable
fun LifecycleDemo() {
val count = remember { mutableStateOf(0) }
Column {
Button(onClick = {
count.value++
}) {
Text("Click me")
}
LaunchedEffect(Unit){
Log.d("Compose", "onactive with value: " + count.value)
}
DisposableEffect(Unit) {
onDispose {
Log.d("Compose", "onDispose because value=" + count.value)
}
}
SideEffect {
Log.d("Compose", "onChange with value: " + count.value)
}
Text(text = "You have clicked the button: " + count.value.toString())
}
}
效果如下:
然后把前面的例子稍微改一下,我们把LaunchedEffect和DisposableEffect一起放到一个if语句里面。
@Composable
fun LifecycleDemo() {
val count = remember { mutableStateOf(0) }
Column {
Button(onClick = {
count.value++
}) {
Text("Click me")
}
if (count.value < 3) {
LaunchedEffect(Unit){
Log.d("Compose", "onactive with value: " + count.value)
}
DisposableEffect(Unit) {
onDispose {
Log.d("Compose", "onDispose because value=" + count.value)
}
}
}
SideEffect {
Log.d("Compose", "onChange with value: " + count.value)
}
Text(text = "You have clicked the button: " + count.value.toString())
}
}
那么此时的生命周期就是:当首次进入if语句的时候执行LaunchedEffect函数,离开if语句的时候,就执行DisposableEffect方法。
底部导航
说到导航就不得不说底部导航和顶部导航,底部导航的实现非常简单,直接使用JetPack Compose提供的脚手架在结合navController和NavHost就能轻松实现
@Composable
fun BottomMainView(){
val bottomItems = listOf(Screen.First,Screen.Second,Screen.Third)
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
bottomItems.forEach{screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite,"") },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route){
//当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页
popUpTo(navController.graph.startDestinationId){saveState = true}
//从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例
launchSingleTop = true
//切换状态的时候保存页面状态
restoreState = true
}
})
}
}
}
){
NavHost(navController = navController, startDestination = Screen.First.route ){
composable(Screen.First.route){
First(navController)
}
composable(Screen.Second.route){
Second(navController)
}
composable(Screen.Third.route){
Third(navController)
}
}
}
}
@Composable
fun First(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "First",fontSize = 30.sp)
}
}
@Composable
fun Second(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Second",fontSize = 30.sp)
}
}
@Composable
fun Third(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Third",fontSize = 30.sp)
}
}
效果如下
顶部导航
顶部导航使用TabRow和ScrollableTabRow这两个组件,其内部都是由一个一个的Tab组件组成。TabRow是平分整个屏幕的宽度,ScrollableTabRow可以超出屏幕宽度并且可以滑动,用法都是一样。
@Composable
fun TopTabRow(){
var state by remember { mutableStateOf(0) }
var titles = listOf("Java","Kotlin","Android","Flutter")
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed{index,title ->
run {
Tab(
selected = state == index,
onClick = { state = index },
text = {
Text(text = title)
})
}
}
}
Column(Modifier.weight(1f)) {
when (state){
0 -> TopTabFirst()
1 -> TopTabSecond()
2 -> TopTabThird()
3 -> TopTabFour()
}
}
}
}
@Composable
fun TopTabFirst(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Java")
}
}
@Composable
fun TopTabSecond(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Kotlin")
}
}
@Composable
fun TopTabThird(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Android")
}
}
@Composable
fun TopTabFour(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Flutter")
}
}
上面只能实现点击每个Tab 切换不同的页面,如果我们想要实现类似我们在xml布局中的ViewPage+TabLayout的效果呢
在Jetpack中怎么实现ViewPage的效果呢,Google的github上提供了一个半官方的库名字叫pager:github.com/google/acco…
implementation "com.google.accompanist:accompanist-pager:0.13.0"
该库目前还是实验性的,以后API都可能会修改,目前使用的时候需要使用@ExperimentalPagerApi注解标记。
@ExperimentalPagerApi
@Composable
fun TopScrollTabRow(){
var titles = listOf("Java","Kotlin","Android","Flutter","scala","python")
val scope = rememberCoroutineScope()
var pagerState = rememberPagerState(
pageCount = titles.size, //总页数
initialOffscreenLimit = 2, //预加载的个数
infiniteLoop = true, //无限循环
initialPage = 0, //初始页面
)
Column {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
modifier = Modifier.wrapContentSize(),
edgePadding = 16.dp
) {
titles.forEachIndexed{index,title ->
run {
Tab(
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.scrollToPage(index)
}
},
text = {
Text(text = title)
})
}
}
}
HorizontalPager(
state=pagerState,
modifier = Modifier.weight(1f)
) {index ->
Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = titles[index])
}
}
}
}
pagerState.scrollToPage(index)
方法可以控制pager滚动,不过它是一个suspend修饰的方法,需要运行在协程中,在jetpack compose中使用协程可以使用rememberCoroutineScope()
方法来获取一个compose中的协程的作用域
效果如下:
Banner
pager库都引入了那顺便吧Banner效果也练习一下,为了显示网络图片还得引入一个新的库,accompanist-coil。在JetPack Compose中官方提供了两个显示网络图片的库accompanist-coil和accompanist-glide,这里使用accompanist-coil。
implementation 'com.google.accompanist:accompanist-coil:0.11.1'
@ExperimentalPagerApi
@Composable
fun Third(navController: NavController){
var pics = listOf("https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png",
"https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
"https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png")
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Third",fontSize = 30.sp)
var pagerState = rememberPagerState(
pageCount = 4, //总页数
initialOffscreenLimit = 2, //预加载的个数
infiniteLoop = true, //无限循环
initialPage = 0, //初始页面
)
Box(modifier = Modifier
.fillMaxWidth()
.height(260.dp)
.background(color = Color.Yellow)) {
HorizontalPager(
state=pagerState,
modifier = Modifier.fillMaxSize()
) {index ->
Image(modifier = Modifier.fillMaxSize(),
painter = rememberCoilPainter(request = pics[index]),
contentScale=ContentScale.Crop,
contentDescription = "图片描述")
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(16.dp).align(Alignment.BottomStart),
)
}
}
}
使用Jetpack Compose写页面感觉比使用xml简单了很多,相信未来Android中的xml布局会像前端的jquary一样用的越来越少。
链接:https://juejin.cn/post/6983968223209193480
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。