Jetpack Compose最强导航框架 Voyager 完全使用指南

本文介绍了Voyager,一个专为Compose设计的跨平台页面导航框架,对比了其与JetpackNavigation的区别,详细阐述了如何使用Screen、Navigator、ViewModel和依赖注入,以及处理透明页面、Tabnavigation和跨模块导航的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

Voyager 是一个专为 Compose 页面导航编写的框架,类似于 Jetpack Navigation,但它支持 Compose 跨平台,以 API 简洁好用而广受好评,我之前用的也是 Jetpack Navigation,后面转到 Voyager 之后就再也回不去了,API 设计的非常巧妙,文档很详细,社区也很活跃。

目前 Voyager 已经支持了几乎所有的使用场景,现在我大概用了一年了,今天写一篇文章来总结一下使用经验,以及一些官方文档没有的使用技巧。

简介

Voyager 官网地址:voyager.adriel.cafe/

对于没用过单 Activity + Compose UI 的架构的朋友来说可能导航框架有点陌生。在该架构下,页面的定义跟传统 Android 开发有些区别,对于传统 Android 开发来说,页面就是指 Activity/Fragment,但对于单 Activity + Compose 页面的架构来说,页面的定义就由导航框架来定义了。

这是因为,单 Activity 的情况下,我们只需要在 Activity 中设置一下 setContent 将 Compose UI 注入到 Activity 中,剩下的对于 Activity 来说就都只是 Compose UI 了,但对于 Compose UI 来说,我们仍然是由页面的区别的,业务上也都是不同的页面,此时就需要一个工具用来将这一大坨 Compose UI 代码按照 Activity 一样组织成页面,那么 Voyager 就是用来解决这个问题的,并且解决的方式非常优雅。

目前 Voyager 不仅支持页面定义和导航,同样也支持 ViewModel 和依赖注入,几乎可以无缝对接使用。

使用起来大概会是这样:

class HomeScreenModel : ScreenModel {
    // ...
}

class HomeScreen : Screen {

    @Composable
    override fun Content() {
        val screenModel = rememberScreenModel { HomeScreenModel() }
        // ...
    }
}

class SingleActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Navigator(HomeScreen())
        }
    }
}

使用

Screen-页面

在 Voyager 中,页面被定义名为 Screen 的对象(所以这里称呼为屏幕似乎更合适,作为一个跨平台的导航框架来说,叫屏幕好像也没什么问题),我们可以类比 Activity/Fragment,只不过它要简单很多。

public actual interface Screen : Serializable {
    public actual val key: ScreenKey
        get() = commonKeyGeneration()

    @Composable
    public actual fun Content()
}

这个接口没有任何特殊之处,它只是一个普通的 Kotlin 接口,简单明了。

然后我们创建一个页面:

class HomeScreen : Screen {

    @Composable
    override fun Content() {
        val screenModel = rememberScreenModel { HomeScreenModel() }
        // ...
    }
}

HomeScreen 也是个普通的 Kotlin 类,也同样没有任何特殊之处,假如这个页面有入参的话,我们甚至可以把它定义为 data class

data class HomeScreen(val title: String) : Screen {

    @Composable
    override fun Content() {
    }
}

由于 Voyager 的状态持久化存储特性,Screen 构造器中的参数需要支持序列化

当然,我们也可以定义成单例类

object HomeScreen : Screen {

    @Composable
    override fun Content() {
    }
}

可以看到 Screen 中提供了一个 Composable 函数,我们的 Compose 代码写在这个函数里面即可。

Navigation-导航

Navigator

Navigator 是 Voyager 导航的起点和入口,它是一个 Composable 函数,负责管理生命周期、返回事件、状态恢复以及嵌套导航等

@Composable
public fun Navigator(
    screen: Screen,
    disposeBehavior: NavigatorDisposeBehavior = NavigatorDisposeBehavior(),
    onBackPressed: OnBackPressed = { true },
    key: String = compositionUniqueId(),
    content: NavigatorContent = { CurrentScreen() }
)

第一个入参就表示该导航区域的第一个屏幕。

class SingleActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Navigator(HomeScreen)
        }
    }
}

当我们希望跳转到其它屏幕时,可以通过 LocalNavigator 获取到当前范围内最近的那个 Navigator 对象,通过它就可以向其他页面跳转。

@Composable
private fun PostCard(post: Post) {
    val navigator = LocalNavigator.currentOrThrow
    
    Card(
        modifier = Modifier.clickable { 
            navigator.push(PostDetailsScreen(post.id))
            // Also works:
            // navigator push PostDetailsScreen(post.id)
            // navigator += PostDetailsScreen(post.id)
        }
    ) {
        // ...
    }
}

当然,如果需要嵌套页面,例如顶部 TAB 切换不同的 Screen,Voyager 也可以轻易实现。

@Composable
override fun Content() {
    Navigator(HomeScreen) { navigator ->
        Scaffold(
            topBar = { /* ... */ },
            content = { CurrentScreen() },
            bottomBar = { /* ... */ }
        )
    }
}

Navigator 类实现了 Stack 接口,这意味着我们可以像管理 Stack 一样轻松的管理 Screen。

val stack = mutableStateStackOf("🍇", "🍉", "🍌", "🍐", "🥝", "🍋")
// 🍇, 🍉, 🍌, 🍐, 🥝, 🍋

stack.lastItemOrNull
// 🍋

stack.push("🍍")
// 🍇, 🍉, 🍌, 🍐, 🥝, 🍋, 🍍

stack.pop()
// 🍇, 🍉, 🍌, 🍐, 🥝, 🍋

stack.popUntil { it == "🍐" }
// 🍇, 🍉, 🍌, 🍐

stack.replace("🍓")
// 🍇, 🍉, 🍌, 🍓

stack.replaceAll("🍒")
// 🍒

通过上面的 API 我们可以轻松的控制页面关系。

BottomSheet navigation

Voyager 同样也支持 BottomSheet navigation,只需要在 Navigator 最外层设置一下即可。

setContent {
    BottomSheetNavigator {
        Navigator(HomeScreen)
    }
}

然后通过 LocalBottomShetNavigator.current 获取对应的 Navigator 对象。

@Composable
override fun Content() {
    val bottomSheetNavigator = LocalBottomSheetNavigator.current

    Button(
        onClick = { 
            bottomSheetNavigator.show(FrontScreen())
        }
    ) {
        Text(text = "Show BottomSheet")
    }
}

但需要注意的是,这里用的不是 Stack 的 API,而是 BottomSheetNavigator 独有的 show/hide 函数来控制显示或隐藏。

Tab navigation

Voyager 还支持 Tab navigation。

Tab 接口继承了 Screen 接口,另外还提供了 options 属性用于描述 Tab 的一些信息。

public data class TabOptions(
    val index: UShort,
    val title: String,
    val icon: Painter? = null
)

public interface Tab : Screen {

    public val options: TabOptions
        @Composable get
}

使用起来跟 Navigator 也很类似:

TabNavigator(HomeTab) {
    Scaffold(
        content = { 
            CurrentTab() 
        },
        bottomBar = {
            BottomNavigation {
                TabNavigationItem(HomeTab)
                TabNavigationItem(FavoritesTab)
                TabNavigationItem(ProfileTab)
            }
        }
    )
}

然后通过 LocalTabNavigator.current 获取到 TabNavigator。

@Composable
private fun RowScope.TabNavigationItem(tab: Tab) {
    val tabNavigator = LocalTabNavigator.current

    BottomNavigationItem(
        selected = tabNavigator.current == tab,
        onClick = { tabNavigator.current = tab },
        icon = { Icon(painter = tab.icon, contentDescription = tab.title) }
    )
}

跨模块导航

Voyager 提供了跨模块导航的能力,主要有如下几个 API 来完成:

  • ScreenProvider:在公共模块中注册所有需要跨模块跳转的 Screen。
  • ScreenRegistry:将 ScreenProvider 中提供的 Screen 注册到 Voyager 中。
  • screenModule:ScreenRegistry 辅助工具。
  • rememberScreen:获取 ScreenProvider 中注册的 Screen 实例。

具体使用可以去看下官方文档,里面写的很详细,而且有 Demo。

不过,关于 Voyager 的跨模块使用我感觉还是有点麻烦,而且我目前的项目是插件化架构,跨模块很多情况下需要通过路由沟通,所以我需要先用路由框架获取到其他模块的 Screen,然后再去打开。

此外,Voyager Navigation 也支持嵌套导航,多层嵌套,这里就不多做介绍了。

ScreenModel/ViewModel

Voyager 中的 ScreenModel 和我们平时用的 ViewModel 几乎一致,只不过要更简单:

public interface ScreenModel {

    public fun onDispose() {}
}

ScreenModel 提供了对 Kotlin 协程的支持:

class PostDetailsScreenModel(
    private val repository: PostRepository
) : ScreenModel {

    fun getPost(id: String) {
        screenModelScope.launch {
            val post = repository.getPost(id)
            // ...
        }
    }
}

然后通过 rememberScreenModel 函数获取到 ScreenModel 对象。

@Composable
override fun Content() {
    val screenModel = rememberScreenModel { HomeScreenModel() }
}

不喜欢使用 ScreenModel 的话也可以接着使用 ViewModel,这俩都是 Voyager 支持的,我用的就是 ViewModel。

@Composable
override fun Content() {
    val viewModel = viewModel<PostListViewModel>()
    // ...
}

依赖注入

Voyager 支持三种依赖注入框架:Koin、Kodein、Hilt。

这里介绍下 Hilt 的使用。

跟普通的类一样,ScreenModel 需要加上 @Inject 注解。

class HomeScreenModel @Inject constructor() : ScreenModel

然后通过 getScreenModel() 获取到实例。

@Composable
override fun Content() {
    val screenModel = getScreenModel<HomeScreenModel>()
}

@AssistedInject 同样也是支持的。

class PostDetailsScreenModel @AssistedInject constructor(
    @Assisted val postId: Long
) : ScreenModel {

    @AssistedFactory
    interface Factory : ScreenModelFactory {
        fun create(postId: Long): PostDetailsScreenModel
    }
}

@Composable
override fun Content() {
    val screenModel = getScreenModel<PostDetailsScreenModel, PostDetailsScreenModel.Factory> { factory ->
        factory.create(postId)
    }
}

上面就是 Voyager 基本使用的简单介绍,此外还有一些本文没有介绍的特性:

  • Stack Api
  • State restoration
  • Transitions(转场动画)
  • Lifecycle
  • Back pres
  • Deep links

这些都可以在官方文档上找到使用方式,本文就不多做介绍了。

下面介绍一些我平时使用时遇到的一些特化场景和解决方案。

一些特殊的场景

透明页面

透明页面在传统 Activity/Fragment 中很容易实现,但在 Jetpack Navigation 和 Voyager 中都不是很容易,这里面的问题在于,Compose 中的页面并不是真正意义上的一个页面,例如 Voyager 中的 Navigator,导航到一个新的页面之后上一个页面并不会被渲染,只会渲染新的页面,虽然实际上 Navigator API 是用 Stack 管理的,页面也是会叠加的,但目前的导航框架都是只会渲染最新的页面,导致即使新的页面是透明的也没用。

我的解决方案是使用类似 BottomSheetNavigator 的机制,额外提供一个 TransparentNavigator 用于管理透明页面。

typealias TransparentNavigatorContent =
        @Composable (transparentNavigator: TransparentNavigator) -> Unit

val LocalTransparentNavigator: ProvidableCompositionLocal<TransparentNavigator> =
    staticCompositionLocalOf { error("TransparentNavigator not initialized") }

@Composable
fun TransparentNavigator(
    key: String = currentCompositeKeyHash.toString(35),
    transparentContent: TransparentNavigatorContent = { CurrentScreen() },
    content: TransparentNavigatorContent
) {
    Navigator(HiddenTransparentScreen, onBackPressed = null, key = key) { navigator ->
        val transparentNavigator = remember(navigator) {
            TransparentNavigator(navigator)
        }

        CompositionLocalProvider(
            LocalTransparentNavigator provides transparentNavigator
        ) {
            Box(modifier = Modifier.fillMaxSize()) {
                content(transparentNavigator)
                val lastItem = transparentNavigator.lastItemOrNull
                if (lastItem != null) {
                    BackHandler {
                        transparentNavigator.pop()
                    }
                    Box(
                        modifier = Modifier
                            .noRippleClick {}
                    ) {
                        transparentContent(transparentNavigator)
                    }
                }
            }
        }
    }
}

class TransparentNavigator internal constructor(
    private val navigator: Navigator,
) : Stack<Screen> by navigator

private object HiddenTransparentScreen : Screen {

    @Composable
    override fun Content() {
        Spacer(modifier = Modifier.height(1.dp))
    }
}

然后把 TransparentNavigator 套在根 Navigator 的外面。

TransparentNavigator {
    Navigator(HomeScreen)
}

使用也很简单:

val transparentNavigator = LocalTransparentNavigator.current
// ...
transparentNavigator.push(PostListScreen)

页面事件回调

类似于 startActivityFroResult ,我们有时候希望打开 Screen 之后能接收到这个 Screen 的一些回调,Voyager 本身是没有对这种场景的支持的,后来我找到了个办法可以解决这个问题,期待后面官方可以支持。

大体上就是利用 NavigatorLifecycleStore API 存储一个生命周期可以跨越页面的自定义的对象,在这个对象中维护一个数据结构,在这个数据结构中存储页面的返回数据。

val Navigator.navigationResult: VoyagerResultExtension
    @Composable get() = remember {
        NavigatorLifecycleStore.get(this) {
            VoyagerResultExtension(this)
        }
    }

class VoyagerResultExtension(
    private val navigator: Navigator
) : NavigatorDisposable {
    private val results = mutableStateMapOf<String, Any?>()

    override fun onDispose(navigator: Navigator) {
        // not used
    }

    public fun popWithResult(result: Any? = null) {
        val currentScreen = navigator.lastItem
        results[currentScreen.key] = result
        navigator.pop()
    }

    public fun clearResults() {
        results.clear()
    }

    public fun popUntilWithResult(predicate: (Screen) -> Boolean, result: Any? = null) {
        val currentScreen = navigator.lastItem
        results[currentScreen.key] = result
        navigator.popUntil(predicate)
    }

    @Composable
    public fun <T> getResult(screenKey: String): State<T?> {
        val log = results.keys.joinToString(", ") { key ->
            "$key:${results[key]}"
        }
        val result = results[screenKey] as? T
        val resultState = remember(screenKey, result) {
            derivedStateOf {
                results.remove(screenKey)
                result
            }
        }
        return resultState
    }
}

用法如下:

// Screen A
val result by navigator.navigationResult.getResult<String>(lastItemKey)

// Screen B
val navigationResult = navigator.navigationResult
navigationResult.popWithResult("result")

上面的代码中用 A 打开了页面 B,并获取它的返回值,其中的 lastItemKey 是指 B 页面的 Key。

文章的开头我们可以看到 Screen 接口中包含一个 Key 属性,这个属性有个默认实现,但我们也可以自定义。如果与 B 页面约定好一个页面的 Key,那么就可以用这个 Key 来传输数据了。

全局导航

在使用 TabNavigator 时我遇到了一个问题,我的页面布局是底部多个导航按钮,点击按钮切换 TAB,在 TAB 内部点击某个按钮跳转到二级页面时一般是直接使用 LocalNavigator.current 来获取当前的 Navigator 进行跳转,但此时拿到的 Navigator 其实是 TabNavigator,跳转到新的页面会发现二级页面的底部仍然有首页的几个底部导航按钮。

Navigator 被设计为链表,我们可以通过这个链表向上追溯到根 Navigator 然后跳转就行了,或者追溯到上一个 Navigator,但是因为我们的 Navigator 嵌套很复杂,我们获取到的可能是 BottomSheetNavigatorTransparentNavigator 等,有点麻烦。

因此,我提供了一个全局 Navigator 用于页面跳转以及控制导航方向。

val LocalGlobalNavigator: ProvidableCompositionLocal<Navigator> =
    staticCompositionLocalOf { error("LocalGlobalNavigator not initialized") }

然后在根 Navigator 初提供值:

// MainActivity.kt
setContent {
	TransparentNavigator {
	    BottomSheetNavigator {
	        Navigator(HomeScreen)
	    }
	}
}

// HomeScreen.kt
object HomeScreen : Screen {

    @Composable
    override fun Content() {
        CompositionLocalProvider(
            LocalGlobalNavigator provides LocalNavigator.currentOrThrow
        ) {

        }
    }
}

这样,我们就可以通过 LocalGlobalNavigator 拿到全局 Navigator 了。

后面如果我们在 TabNavigator 的内部,就可以通过 LocalGlobalNavigator 来跳转到二级页,从而逃脱 TabNavigator 的束缚。

HorizontalPager 结合使用

在使用 HorizontalPager 时,如果我们希望每个 Page 都有一个独立的 ViewModel,那么直接使用 Voyager 是有点麻烦的,如果在 Pager 内部直接使用 Navigator 创建独立 Screen 也可以实现这样的需求,但是 Page 切换时状态会完全丢失。

我仿照 Voyager Tab 做了一个 PagerTab 用于解决该问题。

interface PagerTab {

    val options: PagerTabOptions?
        @Composable get

    @Composable
    fun Screen.TabContent()
}

data class PagerTabOptions(
    val title: String,
    val icon: Painter? = null
)

TabContent 函数之所以有个 Screen Receiver 是因为需要创建 ViewModel/ScreenModel。

@Composable
override fun Content() {
    val tab = remember {
        listOf(ProfileTab(), MessageTab())
    }
    val state = rememberPagerState {
        tab.size
    }
    HorizontalPager(
        state = state,
    ) { pageIndex ->
        with(tab[pageIndex]) {
            TabContent()
        }
    }
}

这样,每个 Page 就都有了独立的 ViewModel,这在有多种不同 TAB 的情况下尤其重要。

好了,关于 Voyager 的介绍就到这里了,再次推荐这个框架,真的很好用。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

### 回答1: PSPICE 17.2 是一种用于电子电路仿真和分析的软件工具。下面是一份简单的 PSpice 17.2 使用初级教程: 1. 安装和启动:首先,你需要下载并安装 PSpice 17.2 软件。安装完成后,双击图标启动软件。 2. 创建电路:在软件界面上,选择“文件”>“新建”,然后在电路编辑器中创建你的电路。你可以从元件库中选择组件,并将其拖放到画布上。连接元件的引脚以构建电路。 3. 设置元件参数:双击元件以打开元件参数设置对话框。在对话框中,设置元件的值、名称和其他参数。对于电阻、电容等基本元件,可以直接输入数值。 4. 设置仿真配置:选择“仿真”>“设置和校验”,然后在仿真设置对话框中选择仿真的类型和参数。你可以选择直流分析、交流分析、暂态分析等。设置仿真参数后,点击“确定”。 5. 运行仿真:选择“仿真”>“运行”来启动仿真。在仿真过程中,软件将模拟电路的响应,并将结果输出到仿真波形窗口中。 6. 查看仿真结果:在仿真波形窗口中,你可以查看各个元件的电流、电压等参数随时间变化的波形。你还可以对波形进行放大、缩小、平移等操作,以更详细地分析电路的性能。 7. 保存和导出结果:在仿真过程中,你可以选择将结果保存为文件或导出为其他格式,如图像文件或数据文件。 以上是 PSpice 17.2 使用初级教程的基本步骤。随着实践的深入,你可以进一步了解复杂电路的建模和分析方法,并尝试更高级的功能和技术。 ### 回答2: PSPICE 17.2是一款电子电路仿真软件,用于对电路进行分析和验证。以下是PSPICE 17.2的使用初级教程: 1. 下载和安装:在官方网站上下载PSPICE 17.2并进行安装。 2. 组件库:打开PSPICE软件后,点击“Capture CIS”图标,进入组件库界面。选择适当的电子元件,如电阻、电容、二极管等,将它们拖放到画布上。 3. 电路连接:在画布上拖放所需元件后,使用导线工具连接它们。点击导线图标,选择合适的连接方式,并将其拖动到适当的端口上。 4. 参数设定:双击元件,弹出元件属性对话框。在这里设置元件的数值,例如电阻的阻值、电容的电容值等。 5. 电源设置:在画布上点击右键,选择“Power Sources”,然后选择适当的电源,如直流电源或交流电源。设置电源的电压或电流数值。 6. 仿真设置:点击画布上方的“PSpice”选项,选择“Edit Simulation Profile”打开仿真配置对话框。在仿真配置中,设置仿真参数,如仿真类型(直流、交流、脉冲等)、仿真时间等。 7. 仿真运行:在仿真配置对话框中点击“Run”按钮,开始进行电路仿真运行。仿真完成后,可以查看并分析仿真结果,如电流、电压、功率等。 8. 结果分析:通过菜单栏中的“PSpice>Probe”选项,打开特定信号的仿真结果。通过选择信号节点,可以显示该信号的波形、幅值和频谱等信息。 9. 数据输出:仿真结束后,可以通过“PSpice>Results”菜单栏选项,导出仿真结果到文本文件,以供后续分析。 10. 误差调整:如果仿真结果与预期不符,可以检查电路连接、元件参数等以找出问题。根据需要进行调整,重新运行仿真以验证改进效果。 以上就是PSPICE 17.2使用初级教程的简要介绍。在使用过程中,请参考软件的帮助文件和官方文档,以获取更详细的指导和解决方法。任何新的软件都需要不断的实践和尝试,希望这个教程能对你有所帮助。 ### 回答3: PSPICE 17.2是一款常用的电路仿真软件,用于电路设计和分析。下面是一个简要的PSPICE 17.2的初级教程: 1. 下载和安装:首先,从官方网站下载PSPICE 17.2,并按照安装向导进行安装。安装完成后,打开软件。 2. 创建新工程:在PSPICE 主界面上,点击“File”菜单,然后选择“New Project”来创建一个新的工程。给工程起一个适当的名字,并选择工程的存储位置。 3. 添加电路元件:在工程界面上,点击“Place”图标,然后选择不同的元件来构建你的电路。你可以从库中选择各种电子元件,如电阻、电容、电感等,并将它们拖放到工程界面上。 4. 连接元件:选择“Wire”图标,然后点击元件的引脚来连接它们。确保连接顺序正确,以保证电路的正确性。 5. 设置元件参数:对于每个添加的元件,你需要设置它们的参数。右键点击元件,选择“Edit Propertiess”,然后在弹出的窗口中输入适当的参数值。 6. 添加电源:在电路中添加电源,以提供电路所需的电能。选择“Place”图标,然后选择合适的电源元件并将其拖放到电路中。同样,设置电源的参数值。 7. 设置仿真配置:在工程界面上,点击“PSpice”菜单,然后选择“Edit Simulation Profile”来设置仿真配置参数。你可以选择仿真类型、仿真时间和仿真步长等。 8. 运行仿真:点击“PSpice”菜单,选择“Run”来运行仿真。PSPICE将自动运行仿真并显示结果。 9. 分析和优化:根据仿真结果,可以分析和优化电路的性能。你可以观察电流、电压和功率等参数,以评估电路的性能,并根据需要进行调整。 10. 保存和导出结果:在分析和优化完成后,可以保存你的工程并导出结果。点击“File”菜单,选择“Save Project”来保存工程,然后选择“Outut”菜单,选择“Export”来导出结果。 以上是PSPICE 17.2的初级教程的简要介绍。通过以上步骤,你可以开始使用PSPICE 17.2进行电路设计和仿真。在实践中不断探索和学习,你将成为一个熟练的PSPICE用户。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值