老生常谈:Android极简MVVM,从一个基类库谈起

Hello啊各位老铁,今天带来一个老生常谈的技术,MVVM,这篇文章,主要详细介绍如何封装一个MVVM的基类库,以及MVVM架构模式在实际业务中的用法,最后会把实际的封装代码开源,并提供远程依赖,方便给到大家使用以及二次修改,尽量做到细致入微,浅显易懂,OK,废话不多赘述,我们进入正文。

这篇文章大概会按照以下几个模块进行阐述,此次封装,做到绝无第三方依赖,都是Android原生的代码封装,请放心使用,如果您想直接进行使用,请直接跳到第4步,集成使用即可,此次的封装,和目前主流的MVVM架构模式,会完美契合,让架构模式简单化,让业务代码清晰化,必须值得推荐使用。

一、MVVM简单概括

二、基于MVVM模式如何封装基类库

三、实战封装

四、封装后在业务中如何使用

五、开源以及Demo查看

温馨提示:内容稍多,请合理安排好时间,如果不想查阅具体封装过程,底部有开源地址,可以直接查看。

一、MVVM简单概括

MVVM的开发模式,相对来说低耦合,业务之间逻辑显得也十分分明,Model层负责将请求的数据交给ViewModel层;ViewModel层负责将请求到的数据做业务逻辑处理,最后交给View层去展示,与View一一对应;View层只负责界面绘制刷新,不处理业务逻辑,非常适合进行独立模块开发。

三层简单概括

1、Model:数据层,包含数据实体和对数据实体的操作。

2、View:视图层,对应于Activity,XML,View,负责数据显示以及用户交互。

3、ViewModel:关联层,将Model和View进行绑定,Model或者View更改时,实时刷新对方。

需要注意:

1、View只做和UI相关的工作,不涉及任何业务逻辑,不涉及操作数据,不处理数据,也就是UI和数据是严格分开的。

2、ViewModel只做和业务逻辑相关的工作,不涉及任何和UI相关的操作,不持有控件引用,不更新UI。

二、基于MVVM模式如何封装基类库

MVVM我们已经清晰,然而针对现有的三层,我们如何进行拆解封装呢?面对这样的一个问题,我们也是需要从三层以及和实际的业务进行相结合,从实际业务中来,也要从实际业务中去,这是我们封装的一个潜在因素,一旦脱离了实际,封装的再优秀,也只是一个花瓶,中看不中用。

针对MVVM中的三层,其实,我们在封装中,也是基于这三层,View,ViewModel和Model。View中,在实际的开发中,一般针对Activity和Fragment进行系统的抽取封装,ViewModel一般会抽取一个父类,做一些公共的方法或属性配置,Model层一般封装的较少,根据实际业务,需要具体问题具体分析。

Activity和Fragment的封装思路,其实是一致的,需要以简单和复杂两种方向进行抽取,一种是简单的页面继承使用,一种是复杂的页面继承使用,这样区分的一个目的,就是,专职专用,避免大材小用,而具体的封装,除了使得代码简洁化,更重要的拓展化,方便子类的调用。

在具体封装的时候,与实际业务相结合,这个无比重要,比如实际的大部分页面,都带有一个标题栏,那么标题栏就可以直接封装父类里面,像子类拓展出,更改标题,右侧按钮,左侧按钮等功能属性;除了统一的标题栏,另外就是子类的视图了,关于子类的视图传递,这个是必须的,可以直接抽象出一个必须要实现的方法,其他的,比如状态栏的改变,缺省页的设置等等,也需要在父类中统一的给出。

复杂的页面是基于简单的页面而来的,这里的复杂,一般是包含很多逻辑的处理,那么,我们就可以增加ViewModel层和Model层了,目前基于DataBinding的实现方式,无论简单和复杂,都是必须需要考虑的,也就是说在父类中,我们就需要向子类提供出可以拿到的databinding和viewmodel,一般以泛型的方式引入,这样子类再继承的时候,就可以很方便的进行调用。

在复杂的页面,也就是包含ViewModel层和Model层的时候,需要考虑绑定视图variable的传递,也就是当前的ViewModel和那个xml进行绑定,当然这是在需要的时候,必须要操作的,除了视图绑定,常见的,数据请求状态,比如请求成功,请求失败,缺省页显示和隐藏,Dialog的显示和隐藏,LiveData的数据回传等等,在复杂的页面中也是需要我们考虑的,除此之外,ViewModel中如何和View层的生命周期绑定,在实际的业务中也是不得不需要考虑的。

除了以上的常规考虑,在实际的业务中,比如事件消息传递,PagerAdapter使用,状态栏透明等很多和基类的相关的功能,我们其实也可以进行封装进去,便于子类的调用。

三、实战封装

通过第2条中的拆解和具体的封装思路,不妨我们进行实战一下,由于Activity和Fragment的封装思路以及相关属性和方法,大部分都是雷同的,所以目前只介绍Activity,更详细的封装,还请大家参考源码。

1、Activity的简单封装

简单封装,不携带ViewModel,只传递ViewDataBinding,子类必须重写的方法只有一个initData,其他均为选择性重写,如果相对逻辑比较简单的页面,可以继承此类。

一个很简单的普通封装,就是把共有的常见的,封装到父类里,便于子类的调用,具体什么方法,什么逻辑进行采取封装,需要我们根据具体业务或者公司的相关情况而定,以下是源码。

abstract class BaseActivity<VB : ViewDataBinding>(@LayoutRes layoutId: Int = 0) :
    AppCompatActivity(layoutId) {

    private var mActionBarView: ActionBarView? = null
    private var mLayoutError: LinearLayout? = null
    private var mLayoutId = layoutId
    lateinit var mBinding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        try {
            //默认状态栏为白底黑字
            darkMode(BaseConfig.statusBarDarkMode)
            statusBarColor(ContextCompat.getColor(this, BaseConfig.statusBarColor))
            setContentView(R.layout.activity_base)
            val baseChild = findViewById<LinearLayout>(R.id.layout_base_child)
            mLayoutError = findViewById(R.id.layout_empty_or_error)
            mActionBarView = findViewById(R.id.action_bar)
            if (mLayoutId == 0) {
                mLayoutId = getLayoutId()
            }

            if (savedInstanceState != null && getIntercept()) {
                noEmptyBundle()
                return
            }

            val childView = layoutInflater.inflate(mLayoutId, null)
            baseChild.addView(childView)
            mBinding = DataBindingUtil.bind(childView)!!

            initView()
            initData()
        } catch (e: Exception) {
            e.printStackTrace()
            noEmptyBundle()
        }
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取视图id
     */
    open fun getLayoutId(): Int {
        return 0
    }

    open fun initView() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:初始化数据
     */
    abstract fun initData()

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:动态改变状态栏颜色和标题
     */
    fun setDarkTitle(dark: Boolean, color: Int, title: String) {
        try {
            darkMode(dark)
            statusBarColor(ContextCompat.getColor(this, color))
            setBarTitle(title)

        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:设置标题
     */
    fun setBarTitle(title: String) {
        mActionBarView!!.visibility = View.VISIBLE
        mActionBarView!!.setBarTitle(title)
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:隐藏左侧按钮
     */

    fun hintLeftMenu() {
        mActionBarView!!.hintLeftBack()
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取ActionBarView
     */
    fun getActionBarView(): ActionBarView {
        return mActionBarView!!
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:隐藏标题栏
     */
    fun hintActionBar() {
        mActionBarView?.visibility = View.GONE
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:Bundle为空进行拦截,解决改变权限后重回App崩溃问题
     */
    open fun getIntercept(): Boolean {
        return false
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:Bundle为空时的逻辑处理,解决改变权限后重回App崩溃问题
     */
    open fun noEmptyBundle() {}

    override fun onDestroy() {
        super.onDestroy()
        try {
            LiveDataBus.removeObserve(this)
            LiveDataBus.removeStickyObserver(this)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:透明状态栏
     */
    fun translucentWindow(dark: Boolean) {
        try {
            immersive(0, dark)
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:设置缺省页
     */
    fun setEmptyOrError(view: View) {
        mLayoutError?.visibility = View.VISIBLE
        mLayoutError?.removeAllViews()
        mLayoutError?.addView(view)
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:隐藏
     */
    fun hintEmptyOrErrorView() {
        mLayoutError?.visibility = View.GONE
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取错误或为空的view
     */
    fun getEmptyOrErrorView(): LinearLayout {
        return mLayoutError!!
    }
}

涉及的方法概述

方法名参数概述
getLayoutId无参子类传递的layout,用于加载视图,可以通过构造方法传递,也可以通过此方法传递。
initView无参初始化View,非必须重写
initData无参初始化数据
setDarkTitledark: Boolean, color: Int, title: String,1、dark: Boolean,状态栏颜色,true就是黑色,false就是白色。2、color: Int,状态栏背景颜色,3、title: String,标题栏内容设置标题,状态栏背景及颜色
setBarTitletitle: String,标题栏内容设置标题
hintLeftMenu无参隐藏左侧按钮
getActionBarView无参获取标题栏View,可以操作标题栏里的任何控件
hintActionBar无参隐藏标题栏
translucentWindowdark: Boolean,状态栏颜色,true就是黑色,false就是白色透明状态栏
setEmptyOrErrorview: View,传递的缺省View视图设置缺省视图
hintEmptyOrErrorView无参隐藏缺省视图
getEmptyOrErrorView无参获取缺省视图

简单的Activity没有什么好说的,都是中规中矩,具体的使用请大家看第四条,具体使用即可。

2、Activity的复杂封装

也谈不上复杂,只是在继承简单页面的基础之上多加了一个ViewModel,相对于比较复杂的页面,就可以继承此类,此类,拓展了ViewModel,可以在ViewModel里进行逻辑的书写,此类也是MVVM的标准执行,V继承于BaseVMActivity,VM继承于BaseViewModel,至于M,可以在VM中通过getRepository方法进行获取。

具体代码逻辑如下:

BaseVMActivity继承于BaseActivity。

abstract class BaseVMActivity<VB : ViewDataBinding, BM : BaseViewModel>(@LayoutRes layoutId: Int = 0) :
    BaseActivity<VB>(layoutId) {

    lateinit var mViewModel: BM

    override fun initData() {
        mViewModel = getViewModel()!!
        val variableId = getVariableId()
        if (variableId != -1) {
            mBinding.setVariable(getVariableId(), mViewModel)
            mBinding.executePendingBindings()
        }
        initVMData()
        observeLiveData()
        initState()
        lifecycle.addObserver(mViewModel)
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取绑定的xml id
     */
    open fun getVariableId(): Int {
        return -1
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:初始化状态
     */
    private fun initState() {
        mViewModel.mStateViewLiveData.observe(this, {
            when (it) {
                StateLayoutEnum.DIALOG_LOADING -> {
                    dialogLoading()
                }
                StateLayoutEnum.DIALOGD_DISMISS -> {
                    dialogDismiss()
                }
                StateLayoutEnum.DATA_ERROR -> {
                    dataError()
                }
                StateLayoutEnum.DATA_NULL -> {
                    dataEmpty()
                }
                StateLayoutEnum.NET_ERROR -> {
                    netError()
                }
                StateLayoutEnum.HIDE -> {
                    hide()
                }
            }
        })
    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:初始化数据
     */
    abstract fun initVMData()

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:LiveData的Observer
     */
    open fun observeLiveData() {

    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:dialog加载
     */
    open fun dialogLoading() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:dialog隐藏
     */
    open fun dialogDismiss() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:数据错误
     */
    open fun dataError() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:数据为空
     */
    open fun dataEmpty() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:网络错误或请求错误
     */
    open fun netError() {}

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:隐藏某些布局或者缺省页等
     */
    open fun hide() {}

    private fun getViewModel(): BM? {
        //这里获得到的是类的泛型的类型
        val type = javaClass.genericSuperclass
        if (type != null && type is ParameterizedType) {
            val actualTypeArguments = type.actualTypeArguments
            val tClass = actualTypeArguments[1]
            return ViewModelProvider(
                this,
                ViewModelProvider.AndroidViewModelFactory.getInstance(application)
            )
                .get(tClass as Class<BM>)
        }
        return null
    }

override fun onDestroy() {
    super.onDestroy()
    try {
        lifecycle.removeObserver(mViewModel)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

}

封装涉及的方法概述

方法名参数概述
getVariableId无参获取绑定的xml variable,也就是当前的xml和哪个对象进行绑定,用于xml里直接数据绑定
initVMData无参初始化数据,必须要实现的方法
observeLiveData无参LiveData的Observer,UI层监听ViewModel层的数据改变
dialogLoading无参dialog加载
dialogDismiss无参dialog隐藏
dataError无参数据错误
dataEmpty无参数据为空
netError无参数据错误
hide无参隐藏缺省页等其他页面

BaseViewModel

BaseViewModel相对比较简单,只提供了一个可以获取Repository的方法,还有一个是刷新UI视图的一个LiveData,就是数据请求,Dialog加载,缺省页加载的状态。更改状态,只需要调用changeStateView方法即可,子类可以重写生命周期方法,便于生命周期的考虑。


open class BaseViewModel : ViewModel() , BaseObserver{
    /**
     * 控制状态视图的LiveData
     */
    val mStateViewLiveData = MutableLiveData<StateLayoutEnum>()

    /**
     * 更改状态视图的状态
     */
    public fun changeStateView(
        state: StateLayoutEnum
    ) {
        // 对参数进行校验
        when (state) {
            StateLayoutEnum.DIALOG_LOADING -> {
                mStateViewLiveData.postValue(StateLayoutEnum.DIALOG_LOADING)
            }
            StateLayoutEnum.DIALOGD_DISMISS -> {
                mStateViewLiveData.postValue(StateLayoutEnum.DIALOGD_DISMISS)
            }
            StateLayoutEnum.DATA_ERROR -> {
                mStateViewLiveData.postValue(StateLayoutEnum.DATA_ERROR)
            }
            StateLayoutEnum.DATA_NULL -> {
                mStateViewLiveData.postValue(StateLayoutEnum.DATA_NULL)
            }
            StateLayoutEnum.NET_ERROR -> {
                mStateViewLiveData.postValue(StateLayoutEnum.NET_ERROR)
            }
            StateLayoutEnum.HIDE -> {
                mStateViewLiveData.postValue(StateLayoutEnum.HIDE)
            }
        }

    }

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取Repository
     */
    inline fun <reified R> getRepository(): R? {
        try {
            val clazz = R::class.java
            return clazz.newInstance()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期初始化
 */
override fun onCreate() {
}

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期页面可见
 */
override fun onStart() {
}

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期页面获取焦点
 */
override fun onResume() {
}

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期页面失去焦点
 */
override fun onPause() {

}

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期页面不可见
 */
override fun onStop() {

}

/**
 * AUTHOR:AbnerMing
 * INTRODUCE:生命周期页面销毁
 */
override fun onDestroy() {
}

}

复杂的Activity,大家可以发现,其实就是标准的MVVM形式封装,Fragment的封装也是基于此,搞清楚上述,基本上我们这个基类库就完成了大半,确实也没什么好说的,大家直接看使用吧。

四、封装后在业务中如何使用

通过以上的封装,我们在业务层所有的页面就可以继承父类进行使用,以达到代码的高度统一,使得架构模式简单化,让业务代码清晰化,目前的封装,大家可以直接封装成库或者打成aar给到其他开发者使用,目前我已经上传到远程,不想麻烦的老铁,可以直接按照下面的步骤进行使用。

1、在你的根项目下的build.gradle文件下,引入maven。


allprojects {
    repositories {
      maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
    }
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


dependencies {
  implementation 'com.vip:base:1.0.2'
}

通过以上的Maven仓库依赖,我们就可以愉快的进行使用了,下面针对各个封装的功能进行一个简单的演示,当然,大家可以直接看源码中的实例,那里相对比较全面。

1、普通的Activity的继承

如果,你的Activity页面逻辑比较简单,建议继承BaseActivity,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。

class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) {
    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:初始化数据
    */
    override fun initData() {
        setBarTitle("主页")
    }
}

2、ViewModel形式Activity的继承

View层,需要继承BaseVMActivity

class TestViewModelActivity : BaseVMActivity<ActivityViewModelBinding,
        TestViewModel>(R.layout.activity_view_model) {

    override fun initVMData() {
        setBarTitle("ViewModel方式使用")
    }

}

ViewModel层,需要继承BaseViewModel

实际的业务中,遇到网络请求,缺省页展示,Dialog显示隐藏,调用changeStateView方法,UI层只需要重写对应的方法即可。


class TestViewModel : BaseViewModel() {

      /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取需要的Repository
     */
    private val repository by lazy {
        getRepository<TestRepository>()
    }

}

Model层,一般根据实际需要,进行具体的封装使用。


class TestRepository {

}

3、DataBinding形式使用

View层,继承BaseVMActivity,返回当前视图的绑定variable


class DataBindActivity :
    BaseVMActivity<ActivityDataBindBinding,
            DataBindViewModel>(R.layout.activity_data_bind) {

    override fun initVMData() {
        setBarTitle("DataBinding使用")
    }

    override fun getVariableId(): Int {
        return BR.data
    }
}

ViewModel层继承BaseViewModel


class DataBindViewModel : BaseViewModel() {

    var oneWayContent = "单向绑定数据测试"

    var twoWayContent = "双向绑定数据测试"

    /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取双向绑定数据
     */
    var clickListener = View.OnClickListener {

        Toast.makeText(it.context, twoWayContent, Toast.LENGTH_SHORT).show()
    }
}

XML视图,直接绑定


<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="data"
            type="com.abner.base.bind.DataBindViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="@dimen/gwm_dp_20"
        android:paddingRight="@dimen/gwm_dp_20">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="@dimen/gwm_dp_20"
            android:text="@{data.oneWayContent}" />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/gwm_dp_20"
            android:hint="双向绑定"
            android:text="@={data.twoWayContent}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="@dimen/gwm_dp_20"
            android:onClick="@{data.clickListener}"
            android:text="获取双向绑定数据" />

    </LinearLayout>
</layout>

4、Fragment的简单使用

如果,你的Fragment页面逻辑比较简单,建议继承BaseFragment,此父类,没有与ViewModel相结合,只包含正常且简单的逻辑处理,目前必须重写的只有一个initData方法,其他方法,大家可以根据业务重写即可。

class TestPagerFragment : BaseFragment
    <FragmentTestPagerBinding>(R.layout.fragment_test_pager) {

    override fun initData() {

    }
}

5、ViewModel形式Fragment的继承

View层,需要继承BaseVMFragment

class TestViewModelPagerFragment :
    BaseVMFragment<FragmentTestPagerBinding,
            TestFragmentViewModel>(R.layout.fragment_test_pager) {

    override fun initVMData() {

    }

}

ViewModel层,需要继承BaseViewModel

class TestFragmentViewModel :BaseViewModel(){

        /**
     * AUTHOR:AbnerMing
     * INTRODUCE:获取需要的Repository
     */
    private val repository by lazy {
        getRepository<TestRepository>()
    }

}

Model层,一般根据实际需要,进行具体的封装使用。


class TestRepository {

}

6、Fragment的DataBinding形式使用和Activity类似,就不赘述了。

7、事件消息总线使用

普通事件发送

    LiveDataBus.send("send", "我发送了一条普通消息")

普通发送事件接收


LiveDataBus.observe(this, "send", Observer<String> {
            Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        })

粘性事件发送


LiveDataBus.sendSticky("sendSticky", "我发送了一条粘性事件消息")

粘性事件接收


  LiveDataBus.observeSticky(this, "sendSticky", Observer<String> {
            Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
        })

更多的其他功能使用,大家直接看Github即可,上边有比较清晰的介绍。

五、开源以及Demo查看

以上的封装,目前已经开源,大家可以下载查看源码,或者进行二次更改使用,地址是:

github.com/AbnerMing88…

相关Demo,大家可以down下项目,运行即可,这里简单贴张效果图:

目前的封装,没有过多的冗余代码,完全可以满足实际的业务需要,大家可以按照这种模式试验一番,遇到问题,可以多多交流,毕竟,技术是开放的,交流中才能不断的进步,当然了,需要结合自己的实际业务进行使用,毕竟项目中不应存在多个架构模式,MVC也好,MVP,MVVM,MVI也罢,无论使用哪种,适合的才是最好的。

作者:二流小码农
链接:https://juejin.cn/post/7140455497454321700
来源:稀土掘金

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值