Jetpack Compose初体验

1,519 阅读6分钟

Compose简介

Jetpack Compose 是一种全新的 Android UI 工具包,它允许开发者使用 Kotlin 语言编写声明式用户界面。

Jetpack Compose 的设计思想是使用函数式编程和响应式编程的模式来构建用户界面。开发者可以将 UI 组件定义为不可变的函数,这些组件可以接受参数、返回值,并且可以通过组合、嵌套等方式来创建复杂的 UI 层次结构。而且,在 Jetpack Compose 中,可以使用状态、事件等响应式数据流来处理 UI 更新和交互。 Jetpack Compose 提供了丰富的 UI 组件库,并且支持自定义组件的开发和集成。它还提供了许多便捷的工具和功能,例如动画、主题、布局系统等,可以帮助开发者快速构建高质量的用户体验。

总之,Jetpack Compose 是 Android 开发领域中一项重要的技术创新,它可以帮助开发者更加轻松、高效地构建和设计用户界面,并且提供了更加现代化和强大的工具和功能。

优势

  1. 更少的代码量。只需要使用kotlin而不必再分开编写kotlin与XML,跟踪变得跟容易。
  2. 更直观的代码。声明式UI,只需要构筑界面。
  3. 加速开发过程。传统View与Compose可以互调用,开发过程可以实时预览布局

劣势

  1. Compose与原生View体系混合开发时,包体积增大
  2. 性能与传统XML布局相比并没有优势,经过多次迭代,目前与XML性能持平

开发实践

Talk is cheap, show me the code

登录页面

一个包含了头部图片、Logo、用户名密码输入框和一个登录按钮的登录页。实现效果如下:

image.png

具体实现(解析在注释中):

// Composable注解函数表明此方法返回值为可组合视图
// 刷新次数和时机不定,不要在其中处理业务(复杂逻辑全部放入ViewModel)  
@Composable  
fun LoginPage(loginViewModel: LoginViewModel, navigationActions: NavigationActions) {  
  
  
    // 在生命周期内采集StateFlow最新数据,以State形式呈现  
    val uiState by loginViewModel.uiState.collectAsStateWithLifecycle()  
  
    // 获取Context方法  
    val context = LocalContext.current  
  
    // Box 可堆叠布局  
    Box {  
  
        // 图片  
        Image(  
            // 调整样式(width、height、background、padding、border、clickable等)  
            modifier = Modifier  
                // 扩展函数方法,转换成dp单位  
                .height(height = 265.dp)  
                .fillMaxWidth(),  
            // 图片缩放类型  
            contentScale = ContentScale.FillHeight,  
            // 图片内容  
            painter = painterResource(id = R.drawable.icon_heading_common),  
            // 图片描述,xml中也有此属性  
            contentDescription = stringResource(  
                id = R.string.app_name  
            )  
        )  
  
  
        // 纵向布局  
        Column {  
            // 间隔,充当margin  
            Spacer(modifier = Modifier.height(250.dp))  
  
            Column(  
                Modifier  
                    // 圆角  
                    .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp))  
                    .background(Color.White)  
                    .fillMaxWidth(),  
                horizontalAlignment = Alignment.CenterHorizontally  
  
            ) {  
  
  
                // 间距  
                Spacer(modifier = Modifier.height(48.dp))  
  
                Image(  
                    painter = painterResource(id = R.drawable.icon_login_header_pos),  
                    contentDescription = stringResource(  
                        id = R.string.app_name  
                    )  
                )  
  
                Spacer(modifier = Modifier.height(48.dp))  
  
				// 抽象出来的输入框方法
                InputText(  
                    hint = "用户名",  
                    needEncrypt = false,  
                    value = uiState.userName,  
                    onValueChanged = {  
                        loginViewModel.onUserNameChanged(it)  
                        loginViewModel.judgeCanLogin()  
                    })  
  
  
                Spacer(modifier = Modifier.height(16.dp))  
  
  
                InputText(  
                    hint = "密码",  
                    needEncrypt = true,  
                    value = uiState.userPassword,  
                    onValueChanged = {  
                        loginViewModel.onUserPasswordChanged(it)  
                        loginViewModel.judgeCanLogin()  
                    })  
  
  
                Spacer(modifier = Modifier.height(32.dp))  
  
  
                // 绘制登录按钮  
                Button(  
                    shape = RoundedCornerShape(4.dp),  
                    modifier = Modifier  
                        .height(54.dp)  
                        .fillMaxWidth()  
                        .padding(horizontal = 20.dp)  
                        .background(Color(247, 248, 249)),  
                    colors = ButtonDefaults.buttonColors(  
                        backgroundColor = Color(0xff287dfa),  
                        contentColor = Color(0xffffffff),  
                        // 不可点击时的样式  
                        disabledBackgroundColor = Color(0xffbfbfbf),  
                        disabledContentColor = Color(0xffffffff),  
                    ),  
                    // 能否点击  
                    enabled = uiState.isLoginEnable,  
                    onClick = {  
                        // 执行ViewModel中的登录逻辑  
                        loginViewModel.login(loginSuccess = {  
                            // 登录成功回调,此为跳转到首页  
                            navigationActions.navigateToHome()  
                        }, loginFailed = {})  
                    }) {  
                    Text(  
                        "登录",  
                        color = Color(0xffffffff),  
                        fontSize = 16.sp  
                    )  
                }  
  
            }        }  
  
        // Loading显隐,只需要直接根据UiState中的布尔值,判断是否执行Loading的Composable方法  
        if (uiState.showLoading) {  
            Row {  
                Loading()  
            }  
        }  
  
  
    }  
  
  
}


  
/**  
 * 自定义的EditText  
 */
@Composable  
fun InputText(  
    hint: String,  
    needEncrypt: Boolean,  
    value: String,  
    onValueChanged: (String) -> Unit  
) {  
	// 等价于EditText
    OutlinedTextField(  
        // 圆角  
        shape = RoundedCornerShape(4.dp),  
        modifier = Modifier  
            .height(54.dp)  
            .fillMaxWidth()  
            .padding(horizontal = 20.dp)  
            .background(Color(247, 248, 249)),  
        colors = TextFieldDefaults.outlinedTextFieldColors(  
            backgroundColor = Color(  
                0xfff7f8f9  
            ),  
            // 边框颜色  
            focusedBorderColor = Color(  
                0xfff7f8f9  
            ),  
            unfocusedBorderColor = Color(  
                0xfff7f8f9  
            ),  
            textColor = Color(0xff666666)  
        ), placeholder = {  
            // Hint现需要传入组件  
            Text(  
                hint,  
                color = Color(0xffbfbfbf),  
                fontSize = 16.sp  
            )  
        },  
        // 字体选项  
        textStyle = TextStyle(fontSize = 16.sp),  
        // 输入内容显隐  
        visualTransformation = if (needEncrypt) PasswordVisualTransformation() else VisualTransformation.None,  
        // 输入框内显示的值  
        value = value,  
        // 输入内容改变回调  
        onValueChange = onValueChanged  
    )  
  
}

首页

效果如下:

image.png


// Preview注解可以实时预览UI效果
@Preview  
@Composable  
fun Home(homeViewModel: HomeViewModel = viewModel(factory = HomeViewModel.provideFactory())) {  

  
    // 返回键拦截(在后面会提到)  
    BackPressHandler(onBackPressed = {  
        "点击了返回键".toast()  
    })  
  
    val uiState by homeViewModel.uiState.collectAsStateWithLifecycle()  
  
  
    Column(  
        modifier = Modifier  
            .fillMaxHeight()  
            .fillMaxWidth(),  
    ) {  
  
        Column(  
            modifier = Modifier.padding(horizontal = 16.dp)  
        ) {  
  
  
            Spacer(modifier = Modifier.height(40.dp))  
  
            Column(  
                modifier = Modifier  
                    .height(125.dp)  
                    .fillMaxWidth()  
                    .clip(  
                        shape = RoundedCornerShape(10.dp),  
                    )  
                    .background(color = Color(0xff287dfa))  
            ) {}  
        }        Spacer(modifier = Modifier.height(20.dp))  
  
  
        MenuGrid(itemDatas = uiState.menuList)  
  
    }  
}  
  
  
@Composable  
fun MenuGrid(itemDatas: List<MenuBean>) {  
	// 懒加载列表,可以实现RecyclerView的作用。
	// 同样的使用还有LazyColum、LazyRow、LazyHorizontalGrid等
    LazyVerticalGrid(  
        modifier = Modifier.padding(horizontal = 8.dp),  
        // 分为3列
        columns = GridCells.Fixed(3),  
        content = {  
            this.items(itemDatas) {  
	            // 遍历,根据数据列表数量返回每个Item的布局
                MenuItem(itemData = it)  
            }  
        })  
}  
  
  
/**  
 * 列表子项  
 */  
@Composable  
fun MenuItem(itemData: MenuBean) {  
    val context = LocalContext.current  
    Column(modifier = Modifier  
        .padding(6.dp)  
        .clickable {  
            // 点击事件  
            MenuBean  
                .getMenuName(ActivityUtils.getTopActivity(), itemData.key)  
                .toast()  
        }) {  
        Column(  
            modifier = Modifier  
                .border(  
                    width = 1.dp,  
                    color = Color(0xfff5f5f5),  
                    shape = RoundedCornerShape(10.dp),  
                )  
                .height(90.dp)  
                .fillMaxWidth()  
                .padding(8.dp)  
                .background(color = Color(0xffffffff))  
        ) {  
            Spacer(modifier = Modifier.height(2.dp))  
  
            Image(  
                painter = painterResource(id = MenuBean.getMenuIcon(itemData.key)),  
                contentDescription = MenuBean.getActionName(itemData.key)  
            )  
  
            Spacer(modifier = Modifier.height(6.dp))  
  
            Text(  
                MenuBean.getMenuName(context, itemData.key),  
                color = Color(0xff333333),  
                fontSize = 13.sp  
            )  
  
        }  
    }  
}

有关Effect



/**  
 * 返回键监听回调  
 */  
@Composable  
fun BackPressHandler(onBackPressed: () -> Unit, enabled: Boolean = true) {  
  
    // 系统返回键拦截器  
    val dispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher  
  
    // remember函数可以理解为:写在函数内部的全局变量。不会跟随Composable方法反复执行。  
    val backCallback = remember {  
        object : OnBackPressedCallback(enabled) {  
            override fun handleOnBackPressed() {  
                onBackPressed()  
            }  
        }  
    }  
  
    // 在Effect中对外部回调进行注册,在dispatcher改变时执行  
    DisposableEffect(dispatcher) {  
        // DisposableEffect默认在每次onCommit时都会执行,  
  
        // 每次重新注册  
        dispatcher?.addCallback(backCallback)  
  
        // DisposableEffect中必须实现onDispose,如不需要,可使用简化API-SideEffect  
        onDispose {  
            backCallback.remove() // 避免泄露  
        }  
    }    // 可以在Effect中处理订阅逻辑  
}

ViewModel

class LoginViewModel : ViewModel() {  
    // viewModel中处理逻辑  
  
  
    // 私有可变UiState  
    private val _uiState = MutableStateFlow(  
        LoginUiState(  
            isLoginEnable = false,  
            showLoading = false,  
            userName = "",  
            userPassword = ""  
        )  
    )  
    // 公开的不可变UiState  
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()  
  
  
    /**  
     * 更新用户名  
     */  
    fun onUserNameChanged(userName: String) {  
        _uiState.update {  
            // MVI架构,仅更新entity中的指定字段,前台Compose代码中检测到字段变动会直接重新执行  
            it.copy(userName = userName)  
        }  
    }  
  
    /**  
     * 更新用户密码  
     */  
    fun onUserPasswordChanged(userPassword: String) {  
        _uiState.update {  
            it.copy(userPassword = userPassword)  
        }  
    }  
  
    /**  
     * 判断能否登录  
     */  
    fun judgeCanLogin() {  
        _uiState.update {  
            it.copy(isLoginEnable = it.userName.isNotEmpty() && it.userPassword.isNotEmpty())  
        }  
    }  
	/**
	 * 执行登录
	 */
    fun login(loginSuccess: () -> Unit, loginFailed: () -> Unit) {  
  
        viewModelScope.launch {  
            showLoading()  
            // 延迟1秒模拟请求场景  
            delay(1000)  
            hideLoading()  
  
            loginSuccess()  
        }  
  
  
    }  
  
    private fun showLoading() {  
        _uiState.update {  
            it.copy(showLoading = true)  
        }  
    }  
  
    private fun hideLoading() {  
        _uiState.update {  
            it.copy(showLoading = false)  
        }  
    }  
  
    companion object {  
        fun provideFactory(): ViewModelProvider.Factory = object : ViewModelProvider.Factory {  
            @Suppress("UNCHECKED_CAST")  
            override fun <T : ViewModel> create(modelClass: Class<T>): T {  
                return LoginViewModel() as T  
            }  
        }  
    }  
  
}

路由逻辑

  
object DemoDestinations {  
    const val SPLASH_ROUTE = "splash"  
    const val HOME_ROUTE = "home"  
    const val LOGIN_ROUTE = "login"  
}  
  
class NavigationActions(navController: NavHostController) {  
    val navigateToHome: () -> Unit = {  
	    // 在具体页面中执行 navigationActions.navigateToHome() 即可跳转
        navController.navigate(DemoDestinations.HOME_ROUTE) {  
            popUpTo(navController.graph.findStartDestination().id) {  
                saveState = true  
            }  
            launchSingleTop = true  
            restoreState = true  
        }  
    }    val navigateToLogin: () -> Unit = {  
        navController.navigate(DemoDestinations.LOGIN_ROUTE)  
  
    }  
    val navigatePopBack: () -> Unit = {  
        // 返回,相当于finish()  
        navController.navigateUp()  
    }  
}  
  
  
@Composable  
fun NaviGraph(  
    navController: NavHostController = rememberNavController(),  
    startDestination: String = DemoDestinations.SPLASH_ROUTE,  
    navigationActions: NavigationActions  
  
) {  
  
  
    NavHost(  
        navController = navController,  
        startDestination = startDestination  
    ) {  
  
        // 定义Splash页  
        composable(DemoDestinations.SPLASH_ROUTE) {  
  
            SplashPage(navigationActions)  
        }  
  
  
  
        // 定位登录页  
        composable(DemoDestinations.LOGIN_ROUTE) {  
            // ViewModel-Compose库中,专为Compose设计的全局ViewModel初始化方法  
            val loginViewModel: LoginViewModel =  
                viewModel(factory = LoginViewModel.provideFactory())  
            LoginPage(  
                loginViewModel, navigationActions  
            )  
        }  
  
  
  
  
        // 定义首页  
        composable(DemoDestinations.HOME_ROUTE) {  
            val homeViewModel: HomeViewModel = viewModel(factory = HomeViewModel.provideFactory())  
            Home(  
                homeViewModel  
            )  
        }  
  
  
    }}

将写好的页面放入Activity中

class MainActivity : ComponentActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        WindowCompat.setDecorFitsSystemWindows(window, false)  
        setContent {  
  
            // 和Flutter一样,Activity成为了Compose的展示容器  
            ComposeDemoApp()  
        }  
    }  
}

  
@Composable  
fun ComposeDemoApp() {  
    ComposeDemoTheme {  
        // remember函数是Compose中的重要API,用于包装一些计算成本较高的逻辑或一些state数据,避免函数重复执行  
        // 可以理解为:写在函数内部的全局变量。不会跟随Composable方法的反复执行而重复初始化。  
        // 有很多remember开头的API,都是不同功能的remember的具体实现  
        val systemUiController = rememberSystemUiController()  
        val useDarkIcons = MaterialTheme.colors.isLight  
  
        // SideEffect相当于DisposableEffect的简化版,可以用来更新外部状态。  
        SideEffect {  
            // 透明状态栏  
            systemUiController.setStatusBarColor(  
                color = Color.Transparent,  
                darkIcons = useDarkIcons  
            )  
        }  
        Surface(  
            modifier = Modifier  
                .fillMaxSize()  
                .fillMaxHeight(),  
            color = MaterialTheme.colors.background,  
        ) {  
  
            val navController = rememberNavController()  
            val navigationActions = remember(navController) {  
                NavigationActions(navController)  
            }  
  
  
            NaviGraph(navController = navController, navigationActions = navigationActions)  
  
        }  
    }}

参考链接

Jetpack Compose 使用入门  |  Android Developers