Jetpack Compose Navigation 2.x 详解

简单的页面跳转

在 Compose 中,我们可以借助 State 实现一个非常简单的屏幕内容切换效果。

kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposeNavigationTestTheme {
                MyApp()
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    // 当前显示的页面
    var currentScreen by remember { mutableIntStateOf(0) }

    when (currentScreen) {
        0 -> ScreenA(modifier = modifier, onClick = {
            currentScreen = 1
        })

        1 -> ScreenB(modifier = modifier, onClick = {
            currentScreen = 2
        })

        2 -> ScreenC(modifier = modifier, onClick = {
            currentScreen = 0
        })
    }
}

@Composable
fun ScreenA(modifier: Modifier = Modifier, onClick: () -> Unit) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 A")
        Button(onClick = onClick) {
            Text("跳转到页面 B")
        }
    }
}

@Composable
fun ScreenB(modifier: Modifier = Modifier, onClick: () -> Unit) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 B")
        Button(onClick = onClick) {
            Text("跳转到页面 C")
        }
    }
}

@Composable
fun ScreenC(modifier: Modifier = Modifier, onClick: () -> Unit) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 C")
        Button(onClick = onClick) {
            Text("跳转到页面 A")
        }
    }
}

运行效果:

但当前的效果只有页面切换,没有页面导航。按下返回键时,只会回到桌面,无法回到上一个页面。

当然,你可以自己来实现导航功能,但我们更多会使用 Navigation 组件。

添加依赖

build.gradle.kts 文件中添加依赖:

kotlin 复制代码
dependencies {
    implementation("androidx.navigation:navigation-compose:2.9.6")
}

简单用法

修改 MyApp 函数:

kotlin 复制代码
const val SCREEN_A = "screen_a"
const val SCREEN_B = "screen_b"
const val SCREEN_C = "screen_c"

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = SCREEN_A, modifier = modifier) {
        // 路由字符串映射到Composable页面
        composable(route = SCREEN_A) {
            ScreenA {
                // 跳转
                navController.navigate(route = SCREEN_B)
            }
        }
        composable(route = SCREEN_B) {
            ScreenB {
                navController.navigate(route = SCREEN_C)
            }
        }
        composable(route = SCREEN_C) {
            ScreenC {
                navController.navigate(route = SCREEN_A)
            }
        }
    }
}

我们定义了每个页面的路由,设置起始路由为 SCREEN_A,也就是启动页为 ScreenA

这样就具备导航功能了,运行效果:

当前所在页面

要知道当前所在页面,可以使用 NavDestination 获取当前页面的路由。

kotlin 复制代码
val navController = rememberNavController()
navController.currentDestination?.route

在 Compose 中,我们更多会使用 NavController.currentBackStackEntryAsState(),因为它返回的是一个 State

kotlin 复制代码
val navController = rememberNavController()

val currentBackStack by navController.currentBackStackEntryAsState()
currentBackStack?.destination?.route

启动模式

跳转的默认行为对应的是 Activity 中的 Standard 启动模式。

我们还可以在跳转时指定额外的行为。

popUpTo

kotlin 复制代码
// 假设在 ScreenC 中跳转到 ScreenA
navController.navigate(route = SCREEN_A) {
    popUpTo(route = SCREEN_A) {
        inclusive = true
    }
}

上面的代码表示在跳转到 ScreenA 之前,会不断让页面出栈,直到遇到传入 popUpTo() 函数的路由参数对应的页面(ScreenA),将 inclusive(包括)属性设为 true,那么在遇到目标页面时,也会将目标页面弹出栈。

这个组合常用于模拟 Activity 的 singleTask 启动模式的栈行为。

比如当前的返回栈是 A -> B -> C,如果从 C 跳回 A,我们希望清除掉 A 上面的 BC,并且不希望有两个 A(销毁旧的 A),就可以使用它。

但请注意,这和 Activity 的 singleTask 行为并不完全一致,具体可以看我的这篇博客:精通 Activity 四大启动模式

launchSingleTop

launchSingleTop 属性的效果类似于 Activity 的 singleTop 启动模式。

kotlin 复制代码
composable(route = SCREEN_A) {
    ScreenA {
        navController.navigate(route = SCREEN_A) {
            launchSingleTop = true
        }
    }
}

当指定 launchSingleToptrue 时,NavController 会判断返回栈的顶部是否为导航目标页。如果是,NavController 就不会创建新的实例,而是会复用栈顶的实例。

避免了在栈顶多次创建相同的页面实例。

跳转时传递参数

必选参数

首先,修改 ScreenB,让它能够接收字符串参数 userId

kotlin 复制代码
@Composable
fun ScreenB(modifier: Modifier = Modifier, userId: String? = null, onClick: () -> Unit = {}) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 B")
        Text(text = "用户id为:$userId")
        Button(onClick = onClick) {
            Text("跳转到页面 C")
        }
    }
}

接着修改 NavHost,使得路由节点能够支持参数传递。

kotlin 复制代码
composable(
    // 新增userId路由参数 
    route = "${SCREEN_B}/{userId}",
    arguments = listOf(
        // 指定路由参数的类型
        navArgument(name = "userId") {
            type = NavType.StringType
        }
    )
) { navBackStackEntry ->
    // 获取参数数据
    val userId = navBackStackEntry.arguments?.getString("userId")
    ScreenB(userId = userId) {
        navController.navigate(route = SCREEN_C)
    }
}

最后,在页面跳转时,将参数传递进来。

kotlin 复制代码
composable(route = SCREEN_A) {
    ScreenA {
        val userId = "123"
        navController.navigate(route = "${SCREEN_B}/$userId")
    }
}

运行效果:

可选参数

Navigation 也支持可选参数,类似于 URL 的查询参数。

修改 NavHost 中 ScreenB 的路由定义:

kotlin 复制代码
composable(
    // 定义可选参数
    route = "${SCREEN_B}?userId={userId}",
    arguments = listOf(
        navArgument(name = "userId") {
            type = NavType.StringType
            nullable = true // 必须要允许为空
            // defaultValue = "0" // 可提供默认值
        }
    )
) { navBackStackEntry ->
    val userId = navBackStackEntry.arguments?.getString("userId") // 可能为 null
    ScreenB(userId = userId) {
        navController.navigate(route = SCREEN_C)
    }
}

跳转时,可以携带参数,也可以不传入:

kotlin 复制代码
composable(route = SCREEN_A) {
    ScreenA {
        // 带参数跳转
        navController.navigate(route = "${SCREEN_B}?userId=456")

        // 不带参数跳转
        navController.navigate(route = SCREEN_B)
    }
}

页面间返回结果

A 跳转到 B,如果还需要在 B 页面返回一个结果给 A,我们可以使用 SavedStateHandle 来完成

修改 ScreenB,使其能够返回结果:

kotlin 复制代码
@Composable
fun ScreenB(modifier: Modifier = Modifier, onClick: (String) -> Unit = {}) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 B")
        Button(onClick = { onClick("这是B返回的结果") }) {
            Text("返回结果")
        }
    }
}

修改 NavHostScreenB 的路由定义:

kotlin 复制代码
composable(
    route = SCREEN_B
) { navBackStackEntry ->
    ScreenB { result ->
        // 将结果设置到上一个页面的SavedStateHandle中
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("result_key", result)
        // 回到上一个页面
        navController.popBackStack() // 将当前页面弹出栈
    }
}

现在,就能够在 ScreenA 中接收结果了:

kotlin 复制代码
composable(route = SCREEN_A) { navBackStackEntry ->

    // 这样也能获取到当前的栈节点
    // navController.currentBackStackEntry

    // 监听栈节点的参数
    val result = navBackStackEntry.savedStateHandle.getStateFlow<String?>(
        key = "result_key",
        initialValue = null
    ).collectAsState()

    ScreenA(resultFromB = result.value) {
        // 移除参数
        navBackStackEntry.savedStateHandle.remove<String>("result_key")

        navController.navigate(route = SCREEN_B)
    }
}

@Composable
fun ScreenA(modifier: Modifier = Modifier, resultFromB: String?, onClick: () -> Unit) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("这里是页面 A")
        resultFromB?.let {
            Text("从B返回的结果: $it")
        }
        Button(onClick = onClick) {
            Text("跳转到页面 B")
        }
    }
}

高级导航技巧

当应用变得复杂时,我们可以嵌套导航图来让路由更加清晰。

kotlin 复制代码
const val SCREEN_A = "screen_a"
const val SCREEN_B = "screen_b"
const val AUTH_FLOW_ROUTE = "auth_flow"
const val LOGIN_ROUTE = "login"
const val REGISTER_ROUTE = "register"

@Composable
fun MyComplexApp(modifier: Modifier = Modifier) {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = SCREEN_A) {
        // 主应用页面
        composable(route = SCREEN_A) {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text("A")
                Button(onClick = {
                    // 将跳转到子图的起始页 (LOGIN_ROUTE)
                    navController.navigate(route = AUTH_FLOW_ROUTE)
                }) {
                    Text("跳转到登录页")
                }
            }
        }
        composable(route = SCREEN_B) {
            Text("B")
        }

        // 子导航图-认证流程
        navigation(
            startDestination = LOGIN_ROUTE, // 子图的起始页
            route = AUTH_FLOW_ROUTE         // 子图的路由名
        ) {
            composable(route = LOGIN_ROUTE) {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    Text("Login")
                }
            }
            composable(route = REGISTER_ROUTE) {
                Text("Register")
            }
        }
    }

}

导航完成时,回退栈将会是 [ SCREEN_A, LOGIN_ROUTE ]

ViewModel 作用域

默认情况下,ViewModel 的生命周期绑定到了 NavBackStackEntry 返回栈节点。

如果我们希望多个页面(Composable)共享一个 ViewModel,我们可以提升 ViewModel 的作用域到这些屏幕共同的嵌套导航图。

比如我们希望登录和注册页面能够共享一个邮箱数据,我们可以这样做:

kotlin 复制代码
class SharedAuthViewModel : ViewModel() {

    // 邮箱
    private val _email = MutableStateFlow("")
    val email: StateFlow<String> = _email

    fun updateEmail(newEmail: String) {
        _email.value = newEmail
    }

    init {
        Log.d("AuthViewModel", "ViewModel created!")
    }

    override fun onCleared() {
        super.onCleared()
        Log.d("AuthViewModel", "ViewModel destroyed!")
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNavigationTestTheme {
                MyApp()
            }
        }
    }
}

const val HOME_ROUTE = "home"
const val AUTH_FLOW_ROUTE = "auth_flow" // 嵌套图的路由
const val LOGIN_ROUTE = "login"
const val REGISTER_ROUTE = "register"

@Composable
fun MyApp() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = HOME_ROUTE) {
        composable(HOME_ROUTE) {
            HomeScreen {
                navController.navigate(AUTH_FLOW_ROUTE)
            }
        }

        // 认证流程子图
        navigation(
            startDestination = LOGIN_ROUTE,
            route = AUTH_FLOW_ROUTE
        ) {
            composable(LOGIN_ROUTE) { navBackStackEntry ->
                // 获取子图的 NavBackStackEntry
                val authGraphEntry = remember(navBackStackEntry) {
                    val parentRoute = navBackStackEntry.destination.parent!!.route!!
                    navController.getBackStackEntry(parentRoute)
                }
                // 将 ViewModel 作用域限定到这个子图
                val authViewModel: SharedAuthViewModel = viewModel(authGraphEntry)

                LoginScreen(
                    viewModel = authViewModel,
                    toRegister = { navController.navigate(REGISTER_ROUTE) },
                    toHome = {
                        // 回到主页
                        navController.popBackStack(HOME_ROUTE, inclusive = false)
                    })
            }

            composable(REGISTER_ROUTE) { navBackStackEntry ->
                val authGraphEntry = remember(navBackStackEntry) {
                    val parentRoute = navBackStackEntry.destination.parent!!.route!!
                    navController.getBackStackEntry(parentRoute)
                }
                // 与 LoginScreen 获取到完全相同的 ViewModel 实例
                val authViewModel: SharedAuthViewModel = viewModel(authGraphEntry)

                RegisterScreen(viewModel = authViewModel) {
                    // 回到登录页
                    navController.navigate(LOGIN_ROUTE) {
                        popUpTo(route = LOGIN_ROUTE) { 
                            inclusive = true
                        }
                    }
                    // 或者直接使用 navController.popBackStack()
                }
            }
        }
    }
}

@Composable
fun HomeScreen(onClick: () -> Unit = {}) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("主屏幕")
        Button(onClick = onClick) {
            Text("进入登录/注册流程")
        }
    }
}

@Composable
fun LoginScreen(
    viewModel: SharedAuthViewModel,
    toRegister: () -> Unit,
    toHome: () -> Unit
) {
    val email by viewModel.email.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("登录页")
        TextField(
            value = email,
            onValueChange = { viewModel.updateEmail(it) },
            label = { Text("共享的 Email") }
        )
        Spacer(Modifier.height(16.dp))
        Button(onClick = toRegister) {
            Text("跳转到注册页")
        }
        Spacer(Modifier.height(8.dp))
        Button(onClick = toHome) {
            Text("返回主页")
        }
    }
}

@Composable
fun RegisterScreen(viewModel: SharedAuthViewModel, toLogin: () -> Unit) {
    val email by viewModel.email.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("注册页")
        TextField(
            value = email,
            onValueChange = { viewModel.updateEmail(it) },
            label = { Text("共享的 Email (与登录页同步)") }
        )
        Spacer(Modifier.height(16.dp))
        Button(onClick = toLogin) {
            Text("返回登录页")
        }
    }
}

popBackStack(route, inclusive) 的行为是不断从栈顶弹出页面,直到找到函数参数 route,如果 inclusive 标志为 true,那么 route 也会一并弹出。

底部导航栏的状态保存与恢复

在实现底部导航栏时,切换 Tab 往往会导致前一个 Tab 的状态(如滚动位置)丢失。

为了实现状态的保存与恢复,我们可以使用三个关键选项:

kotlin 复制代码
// 当点击底部 Tab 时,调用此方法
fun NavController.onBottomNavClicked(route: String) {
    this.navigate(route) {
        // 弹出到图的起始点,避免回退栈无限累积
        popUpTo(this@onBottomNavClicked.graph.findStartDestination().id) {
            saveState = true // 关键:弹出时保存栈中页面的状态
        }
        // 避免在已位于栈顶时重复创建
        launchSingleTop = true
        // 关键:导航到目的地时,恢复其之前保存的状态
        restoreState = true
    }
}

这样能实现状态不丢失,并且在 首页 -> 搜索 -> 我的 来回切换时,回退栈只会是这几个:首页首页 -> 搜索首页 -> 我的

示例代码:

kotlin 复制代码
class TabViewModel(private val tabName: String) : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count = _count.asStateFlow()

    fun increment() {
        _count.value++
    }

    init {
        Log.d("ViewModelLifecycle", "$tabName ViewModel CREATED")
    }

    override fun onCleared() {
        Log.d("ViewModelLifecycle", "$tabName ViewModel CLEARED")
        super.onCleared()
    }
}

// 底部 Tab 的路由
sealed class Screen(val route: String, val icon: ImageVector, val label: String) {
    data object Home : Screen("home", Icons.Filled.Home, "首页")
    data object Search : Screen("search", Icons.Filled.Search, "搜索")
    data object Profile : Screen("profile", Icons.Filled.Person, "我的")
}

val bottomNavItems = listOf(
    Screen.Home,
    Screen.Search,
    Screen.Profile
)


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNavigationTestTheme {
                MainScreen()
            }
        }
    }
}

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    Scaffold(
        bottomBar = {
            MyBottomNavBar(navController = navController)
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = Screen.Home.route,
            modifier = Modifier.padding(paddingValues)
        ) {
            composable(Screen.Home.route) {
                // 手动创建 factory 来传递 tabName,以便观察日志
                val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Home"))
                TabScreenContent(vm)
            }
            composable(Screen.Search.route) {
                val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Search"))
                TabScreenContent(vm)
            }
            composable(Screen.Profile.route) {
                val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Profile"))
                TabScreenContent(vm)
            }
        }
    }
}

class TabViewModelFactory(private val tabName: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(TabViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return TabViewModel(tabName) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}


// 底部导航栏
@Composable
fun MyBottomNavBar(navController: NavController) {
    NavigationBar {
        // 当前路由状态
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route

        bottomNavItems.forEach { screen ->
            NavigationBarItem(
                selected = currentRoute == screen.route,
                onClick = {
                    navController.navigate(screen.route) {
                        // 弹出到图的起始点,在这里相当于是:
                        // popUpTo(Screen.Home.route) {
                        //     saveState = true
                        //     inclusive = false
                        // }
                        popUpTo(navController.graph.findStartDestination().id) {
                            // 关键:弹出屏幕时,保存它们的状态
                            saveState = true
                        }
                        // 避免在已位于栈顶时重复创建实例
                        launchSingleTop = true
                        // 关键:导航到目的地时,恢复其之前保存的状态
                        restoreState = true
                    }
                },
                icon = { Icon(screen.icon, contentDescription = screen.label) },
                label = { Text(screen.label) }
            )
        }
    }
}

// Tab 屏幕内容
@Composable
fun TabScreenContent(viewModel: TabViewModel) {
    val count by viewModel.count.collectAsState()
    val listState = rememberLazyListState() // 用于演示滚动位置的保存

    Column(modifier = Modifier.fillMaxSize()) {
        Text(text = "当前计数值: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { viewModel.increment() }) {
            Text("点我 +1")
        }

        LazyColumn(state = listState) {
            items(100) {
                Text(text = "我是列表项 $it", modifier = Modifier.padding(16.dp))
            }
        }
    }
}

在这里起始页是 home,假设当前的返回栈是 [home, profile],点击 search 标签页时将会发生:

  • popUpTo(navController.graph.findStartDestination().id):将除了起始页(home)的页面都弹出栈,返回栈会变为 [home]。(inclusive 默认为 false

  • saveState = true:将弹出的页面的 ViewModel 和 UI 状态进行保存。

  • launchSingleTop = true:防止重复点击同一个 Tab 时,创建多个页面实例。

  • restoreState = true:导航到 search 页时,会先看看之前有没有为 search 页保存过状态。

    • 如果有,会直接唤醒之前的 search 实例,恢复其 ViewModel 和 UI 状态(这里是滚动位置)。
    • 如果没有,意味着是第一次点击,会创建一个全新的 search 页面实例。

deep link 就是可以让第三方(如浏览器、其他应用等)直接唤起指定的页面。

首先,让当前的 Activity 支持 deep link,修改 AndroidManifest.xml 文件:

xml 复制代码
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:theme="@style/Theme.ComposeNavigationTest">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        
        <!-- 支持 myapp://screen_b 格式-->
        <data
            android:host="screen_b"
            android:scheme="myapp" />
    </intent-filter>
</activity>

然后在 ScreenB 页面的路由定义处加上 deep link 的支持即可。

kotlin 复制代码
composable(
    route = "${SCREEN_B}/{userId}",
    arguments = listOf(
        navArgument(name = "userId") {
            type = NavType.StringType
        }
    ),
    // 添加 deepLinks 列表
    deepLinks = listOf(
        navDeepLink {
            // 定义 URI 格式,它会自动匹配路由参数
            uriPattern = "myapp://$SCREEN_B/{userId}"
        }
    )
) { navBackStackEntry ->
    val userId = navBackStackEntry.arguments?.getString("userId")
    ScreenB(userId = userId) {
        navController.navigate(route = SCREEN_C)
    }
}

只要进入 "myapp://screen_b/321" 链接,即可在打开页面 B 的同时传递 userId 为 "321" 的参数。

参考

参考郭霖大佬的博客:写给初学者的Jetpack Compose教程,Navigation

相关推荐
Android系统攻城狮4 小时前
Android内核进阶之获取DMA地址snd_pcm_sgbuf_get_addr:用法实例(九十一)
android·pcm·android内核·音频进阶·pcm硬件参数
清空mega5 小时前
Android Studio移动应用基础教程(前言)
android·ide·android studio
2501_937145415 小时前
2025IPTV 源码优化版实测:双架构兼容 + 可视化运维
android·源码·源代码管理·机顶盒
zhoutanooi7 小时前
安卓bp文件编译学习
android·学习
aramae8 小时前
MySQL数据库入门指南
android·数据库·经验分享·笔记·mysql
百锦再8 小时前
选择Rust的理由:从内存管理到抛弃抽象
android·java·开发语言·后端·python·rust·go
whatever who cares9 小时前
在Java/Android中,List的属性和方法
android·java
油炸小波11 小时前
09-微服务原理篇(XXLJOB-幂等-MySQL)
android·mysql·微服务
果子没有六分钟11 小时前
setprop debug.hwui.profile visual_bars有什么作用
android