kotlin 扩展属性_22个Kotlin扩展以获取更清洁的代码

kotlin 扩展属性

I’m a mobile developer, and one of the reasons I switched to Kotlin immediately after it appeared is because of its support of extensions. Extensions allow you to add methods to any existing class, even to Any or an optional type (for example, Int?).

我是一名移动开发人员,出现后立即转到Kotlin的原因之一是因为它对扩展的支持。 扩展允许您将方法添加到任何现有类,甚至添加到Any或可选类型(例如Int? )。

If you extend a base class, all derived classes automatically get this extension. You can also override methods from extensions, which makes this mechanism even more flexible and powerful.

如果扩展基类,则所有派生类都会自动获得此扩展。 您还可以覆盖扩展中的方法,这使该机制更加灵活和强大。

I use Kotlin 1.4.0 for Android in Android Studio 4.0.1. And I assume that all methods will be called from Kotlin, not Java. Even so, most of the extensions will work in other versions of Kotlin, some of them in other environments.

我在Android Studio 4.0.1中将Kotlin 1.4.0用于Android。 而且我假设所有方法都是从Kotlin而不是Java调用的。 即使这样,大多数扩展也可以在Kotlin的其他版本中使用,其中一些可以在其他环境中使用。

'Int.toDate()'和'Int.asDate' (‘Int.toDate()’ and ‘Int.asDate’)

Very often we get the date and time as a timestamp. A timestamp is the number of seconds (sometimes milliseconds) since January 1, 1970. This moment is called the epoch.

通常,我们将日期和时间作为时间戳记。 时间戳是自1970年1月1日以来的秒数(有时是毫秒)。此刻称为epoch

This simple extension converts seconds into a Date object:

这个简单的扩展将秒转换为Date对象:

There are two options: a function or a read-only property. They are functionally equal. Which one to use is a matter of taste.

有两个选项: functionread-only属性。 它们在功能上是相等的。 使用哪一个取决于口味。

import java.util.Date


fun Int.toDate(): Date = Date(this.toLong() * 1000L)


val Int.asDate: Date
get() = Date(this.toLong() * 1000L)

用法 (Usage)

val json = JSONObject();
json.put("date", 1598435781)val date = json.getIntOrNull("date")?.asDate

Note: In this example, I use another extension — getIntOrNull. It returns an int value if it exists in JSON or null otherwise. You can find the full source code here:

注意:在此示例中,我使用了另一个扩展名— getIntOrNull 。 如果它存在于JSON中,则返回一个int值;否则返回null 。 您可以在此处找到完整的源代码:

'String.toDate(...)'和'Date.toString(...)' (‘String.toDate(…)’ and ‘Date.toString(…)’)

Another popular conversion for a Date object is to make it into a string and then go back again. I’m not talking about the standard Java/Kotlin toString() method. In our case, we need to specify a format.

Date对象的另一个流行转换是将其转换为字符串,然后再次返回。 我不是在谈论标准的Java / Kotlin toString()方法。 在我们的情况下,我们需要指定一种格式。

import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale


fun String.toDate(format: String): Date? {
  val dateFormatter = SimpleDateFormat(format, Locale.US)
  return try {
    dateFormatter.parse(this)
  } catch (e: ParseException) {
    null
  }
}


fun Date.toString(format: String): String {
  val dateFormatter = SimpleDateFormat(format, Locale.US)
  return dateFormatter.format(this)
}

Performance warning: In this example, we create aSimpleDateFormat object every time. If you use this extension inside a list or parse a big response from an API, consider removing theval dateFormatter = SimpleDateFormat(format, Locale.US) from the extension code and putting it in as a global variable or a static class member.

性能警告:在此示例中,我们每次都创建一个SimpleDateFormat对象。 如果在列表中使用此扩展或从API解析较大的响应,请考虑从扩展代码中删除val dateFormatter = SimpleDateFormat(format, Locale.US) ,并将其作为全局变量或静态类成员放入。

用法 (Usage)

val format = "yyyy-MM-dd HH:mm:ss"val date = Date()val str = date.toString(format)val date2 = str.toDate(format)

'Int.centsToDollars()'和'Int.centsToDollarsFormat()' (‘Int.centsToDollars()’ and ‘Int.centsToDollarsFormat()’)

If you work with prices, you can have precision problems with Floats and Doubles. One popular ways of representing prices is Ints. But instead of storing values in the main currency (e.g., dollars or euros), you represent them in monetary units (e.g., cents).

如果您使用价格,则可能会出现FloatsDoubles精度问题。 表示价格的一种流行方法是Ints 。 但是,您不用以主要货币(例如美元或欧元)存储值,而是以货币单位(例如美分)表示它们。

You need to make all the calculations within Int and only show the user the final value (converting to Double or directly to String).

您需要在Int内进行所有计算,并且仅向用户显示最终值(转换为Double或直接转换为String )。

import java.util.Locale


fun Int.centsToDollars(): Double = this.toDouble() / 100.0


fun Int.centsToDollarsFormat(currency: String): String {
  val dollars = this / 100
  val cents = this % 100
  return String.format(Locale.US, "%s%d.%02d", currency, dollars, cents)
}

用法 (Usage)

val amount = 4999val doubleAmount = amount.centsToDollars()val priceTag = amount.centsToDollarsFormat("\$")

'Double.toPrice()' (‘Double.toPrice()’)

Another useful extension when working with prices is formatting with group separation. In most cases, inside one app, we have only one set of rules for price formatting. One extension can be used in the whole app to show prices.

使用价格时,另一个有用的扩展是使用组分隔进行格式化。 在大多数情况下,在一个应用程序内,我们只有一套定价格式规则。 整个应用程序都可以使用一个扩展程序来显示价格。

In this extension, we use two fractional (monetary) digits, show a comma between, and split each of our three digits with a dot to make the price easier to read. This time we’ll use Double. Converting Int to Double can be done with thecentsToDollars() extension from the previous section.

在此扩展程序中,我们使用两个小数(货币)数字,在两者之间显示逗号,并用点将三个数字中的每一个分开,以使价格更易于阅读。 这次我们将使用Double 。 可以使用上一节中的centsToDollars()扩展centsToDollars() Int转换为Double

import java.text.DecimalFormat


fun Double.toPrice(): String {
  val pattern = "#,###.00"
  val decimalFormat = DecimalFormat(pattern)
  decimalFormat.groupingSize = 3


  return "€" + decimalFormat.format(this)
}

用法 (Usage)

val price = 123456789.5.toPrice()

'String.toLocation(...)' (‘String.toLocation(…)’)

When you work with an API and get the coordinates of an object, you may get them as two different fields. But sometimes it’s one field with a comma-separated latitude and longitude.

当您使用API​​并获取对象的坐标时,可以将它们作为两个不同的字段来获取。 但有时这是一个用逗号分隔的纬度和经度的字段。

With this extension, we can convert them to the Android location in one line:

通过此扩展程序,我们可以将它们转换为一行到Android位置:

import android.location.Location


fun String.toLocation(provider: String): Location? {
  val components = this.split(",")
  if (components.size != 2)
    return null


  val lat = components[0].toDoubleOrNull() ?: return null
  val lng = components[1].toDoubleOrNull() ?: return null
  val location = Location(provider);
  location.latitude = lat
  location.longitude = lng
  return location
}

用法 (Usage)

val apiLoc = "41.6168, 41.6367".toLocation("API")

Similarly, you can create an extension converting String to LatLng from the Google Maps package. In this case, you won’t even need to specify the location provider.

同样,您可以创建一个扩展程序,将扩展名从Google Maps包将String转换为LatLng 。 在这种情况下,您甚至不需要指定位置提供者。

'String.containsOnlyDigits'和'String.isAlphanumeric' (‘String.containsOnlyDigits’ and ‘String.isAlphanumeric’)

Let’s talk about the properties of a String. They can be empty or not — for example, they can contain only digits or only alphanumeric characters. These extensions will allow you to validate a string in one line:

让我们谈谈String的属性。 它们可以为空,也可以为空,例如,它们只能包含数字或字母数字字符。 这些扩展名使您可以在一行中验证字符串:

val String.containsDigit: Boolean
get() = matches(Regex(".*[0-9].*"))

用法 (Usage)

val digitsOnly = "12345".containsDigitOnly
val
notDigitsOnly = "abc12345".containsDigitOnly
val
alphaNumeric = "abc123".isAlphanumeric
val
notAlphanumeric = "ab.2a#1".isAlphanumeric

'Context.versionNumber'和'Context.versionCode' (‘Context.versionNumber’ and ‘Context.versionCode’)

In Android apps, it’s a good practice to show the version number on the about or support screen. It’ll help users see if they need an update and gives valuable information when reporting a bug. Standard Android functions require several lines of code and exception handling. This extension will allow you to get the version number or code in one line:

在Android应用中,最好在“关于”或“支持”屏幕上显示版本号。 它可以帮助用户查看他们是否需要更新,并在报告错误时提供有价值的信息。 标准的Android功能需要几行代码和异常处理。 此扩展名使您可以在一行中获取版本号或代码:

import android.content.Context
import android.content.pm.PackageManager


val Context.versionName: String?
get() = try {
  val pInfo = packageManager.getPackageInfo(packageName, 0);
  pInfo?.versionName
} catch (e: PackageManager.NameNotFoundException) {
  e.printStackTrace()
  null
}


val Context.versionCode: Long?
get() = try {
  val pInfo = packageManager.getPackageInfo(packageName, 0)
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
    pInfo?.longVersionCode
  } else {
    @Suppress("DEPRECATION")
    pInfo?.versionCode?.toLong()
  }
} catch (e: PackageManager.NameNotFoundException) {
  e.printStackTrace()
  null
}

Usage:

用法:

val vn = versionName ?: "Unknown"val vc = versionCode?.toString() ?: "Unknown"val appVersion = "App Version: $vn ($vc)"

'Context.screenSize' (‘Context.screenSize’)

Another useful extension for Android is the Context extension, which allows you to get the device screen size. This extension returns the screen size in pixels:

Android的另一个有用扩展是Context扩展,它允许您获取设备的屏幕尺寸。 此扩展名返回屏幕尺寸(以像素为单位):

import android.content.Context
import android.graphics.Point
import android.view.WindowManager


@Suppress("DEPRECATION")
val Context.screenSize: Point
get() {
  val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
  val display = wm.defaultDisplay
  val size = Point()
  display.getSize(size)
  return size
}

用法 (Usage)

Log.d(TAG, "User's screen size: ${screenSize.x}x${screenSize.y}")

'Any.deviceName' (‘Any.deviceName’)

It’s questionable if you should extend Any. If you do, the function appears globally, so basically, it’s the same as declaring a global function.

是否应扩展Any值得怀疑。 如果这样做,该函数将全局显示,因此从根本上说,它与声明全局函数相同。

In our next extension, we’ll get an Android device’s name. As it doesn’t require any context, we’ll make it as an Any extension:

在我们的下一个扩展中,我们将获得一个Android设备的名称。 由于它不需要任何上下文,因此将其作为Any扩展名:

import android.os.Build
import java.util.Locale


val Any.deviceName: String
get() {
  val manufacturer = Build.MANUFACTURER
  val model = Build.MODEL
  return if (model.startsWith(manufacturer))
    model.capitalize(Locale.getDefault())
  else
    manufacturer.capitalize(Locale.getDefault()) + " " + model
}

用法 (Usage)

Log.d(TAG, "User's device: $deviceName")

'T.弱' (‘T.weak’)

The next situation is a little more complicated. Let’s say we have an Activity and a ListView in it with many cells. Each cell can give some feedback. Let’s say it has a delegate interface, and we pass the activity itself to a cell because it implements the cell’s interface:

接下来的情况要复杂一些。 假设我们有一个Activity和一个包含许多单元格的ListView 。 每个单元都可以提供一些反馈。 假设它具有委托接口,并且我们将活动本身传递给单元,因为它实现了单元的接口:

interface CellDelegate {fun buttonAClicked()fun buttonBClicked()
}class Cell(context: Context?) : View(context) {// ...
var
delegate: CellDelegate? = null
fun
prepare(arg1: String, arg2: Int, delegate: CellDelegate) {this.delegate = delegate
}
}class Act: Activity(), CellDelegate {// ...
fun
createCell(): Cell {val cell = Cell(this)
cell.prepare("Milk", 10, this)return cell
}override fun buttonAClicked() {TODO("Not yet implemented")
}override fun buttonBClicked() {TODO("Not yet implemented")
}// ...}

I’m just showing the structure here. In a real app, we can use ViewHolder and reuse cells (which is the right thing to do).

我只是在这里显示结构。 在真实的应用程序中,我们可以使用ViewHolder用单元格(这是正确的做法)。

One of the problems here is that Act and Cell have references to each other, which can lead to a memory leak. A good solution here is to use a WeakReference. Delegate variables wrapped into a WeakReference won’t affect the reference counter of our Act, so as soon as we close the screen, it will be destroyed (or added to a queue to destroy later — we’ll allow Android OS to decide) together with all of the allocated cells.

这里的问题之一是ActCell相互引用,这可能导致内存泄漏。 一个好的解决方案是使用WeakReference 。 封装到WeakReference委托变量不会影响Act的引用计数器,因此,一旦我们关闭屏幕,它将一起被销毁(或添加到队列中销毁以供以后销毁-我们将允许Android OS决定)与所有分配的单元格。

This simple extension allows you to get a weak reference by adding .weak to any object:

这个简单的扩展名允许您通过向任何对象添加.weak来获得弱引用:

val <T> T.weak: WeakReference<T>
get() = WeakReference(this)

用法 (Usage)

class Cell(context: Context?) : View(context) {// ...
private var
delegate: WeakReference<CellDelegate>? = null
fun
prepare(arg1: String, arg2: Int, delegate: CellDelegate) {this.delegate = delegate.weak}fun callA() {
delegate?.get()?.buttonAClicked()
}fun callB() {
delegate?.get()?.buttonBClicked()
}
}

I want to emphasise this extension is generic, and it’ll work with any type.

我想强调一下,该扩展名是通用的,并且可以与任何类型一起使用。

'Context.directionsTo(...)' (‘Context.directionsTo(…)’)

Opening navigation from an Android app is a popular feature. Android is a Google product, same as Google Maps. The Google Maps app is preinstalled on most Android phones and tablets. The easiest solution is to open navigation in the Android Maps app. And if it’s not installed, just open it in a web browser.

从Android应用程序打开导航是一项受欢迎的功能。 Android是Google产品,与Google Maps相同。 大多数Android手机和平板电脑上都预先安装了Google Maps应用。 最简单的解决方案是在Android Maps应用中打开导航。 如果尚未安装,只需在Web浏览器中将其打开。

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.location.Location
import android.net.Uri
import java.util.Locale


fun Context.directionsTo(location: Location) {
  val lat = location.latitude
  val lng = location.longitude
  val uri = String.format(Locale.US, "http://maps.google.com/maps?daddr=%f,%f", lat, lng)
  try {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
    intent.setClassName("com.google.android.apps.maps", "com.google.android.maps.MapsActivity")
    startActivity(intent)
  }
  catch (e: ActivityNotFoundException) {
    e.printStackTrace()


    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
    startActivity(intent)
  }
}

This is an extension of Context, and from the point of view of coding, it’s OK. But logically, I’d make it more specific. It can extend Activity or AppCompatActivity to avoid side effects like using it from Service. You can change the extendable class to whatever you use in your app.

这是Context的扩展,从编码的角度来看,还可以。 但从逻辑上讲,我将使其更加具体。 它可以扩展ActivityAppCompatActivity以避免副作用,例如从Service使用它。 您可以将可扩展类更改为您在应用程序中使用的任何类。

'AppCompatActivity.callTo(...)'或'Activity.callTo(...)' (‘AppCompatActivity.callTo(…)’ or ‘Activity.callTo(…)’)

We’re using the same logic as in the previous extension. But instead of navigating to an object, we try to call it. These two extensions can be used side by side in the same app.

我们使用与上一个扩展相同的逻辑。 但是,我们没有导航到对象,而是尝试调用它。 这两个扩展可以在同一应用中并排使用。

The complication of this situation is the permission (or the lack of it). Android requires permission to make a call. But unlike iPhone, it does make a call, it doesn’t just open the caller.

这种情况的复杂之处在于许可(或缺乏许可)。 Android需要获得许可才能拨打电话。 但是,与iPhone不同,它可以拨打电话,而不仅仅是打开呼叫者。

This extension will have two parameters and extend Activity directly or a similar class. The first parameter is a phone number, and the second one is a request code. In the event if we need permission, but we don’t have it:

此扩展将具有两个参数,并直接扩展Activity或类似的类。 第一个参数是电话号码,第二个参数是请求代码。 如果我们需要许可,但没有许可,请执行以下操作:

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat


fun AppCompatActivity.callTo(phoneNumber: String, requestCode: Int) {
  val intent = Intent(Intent.ACTION_CALL)


  intent.data = Uri.parse("tel:$phoneNumber")
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
      val permissions = arrayOfNulls<String>(1)
      permissions[0] = Manifest.permission.CALL_PHONE
      requestPermissions(permissions, requestCode)
    } else {
      startActivity(intent)
    }
  } else {
    startActivity(intent)
  }
}

用法(下面的代码是“ AppCompatActivity”的一部分) (Usage (the code below is a part of ‘AppCompatActivity’))

private val phone: String = "+1234567890"private fun call() {callTo(phone, callPermissionRequestCode)
}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {if (requestCode == callPermissionRequestCode) {if (permissions.isNotEmpty() && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
}
} else {super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}companion object {const val callPermissionRequestCode = 2001
}

'String.asUri' (‘String.asUri’)

We usually think about an internet address as a string. We can type it and put it into quotes. For example, “https://medium.com.”

我们通常将互联网地址视为一个字符串。 我们可以输入并用引号引起来。 例如,“ https://medium.com”。

But for internal use, Android has a special type: Uri. It’s easy to convert one into the other. The extension below allows us to convert aString into anUri with verification. If it’s not a valid Uri, it returns null:

但对于内部使用,Android有一个特殊类型: Uri 。 将一个转换为另一个很容易。 以下扩展名允许我们通过验证将String转换为Uri 。 如果不是有效的Uri ,则返回null

import android.net.Uri
import android.webkit.URLUtil
import java.lang.Exception


val String.asUri: Uri?
get() = try {
  if (URLUtil.isValidUrl(this))
    Uri.parse(this)
  else
    null
} catch (e: Exception) {
  null
}

用法 (Usage)

val uri = "invalid_uri".asUri
val
uri2 = "https://medium.com/@alex_nekrasov".asUri

“ Uri.open(…)”,“ Uri.openInside(…)”和“ Uri.openOutside(…)” (‘Uri.open(…)’, ‘Uri.openInside(…)’, and ‘Uri.openOutside(…)’)

Now when we have an Uri, we may want to open it in a browser. There are two ways to do this:

现在,当我们有了一个Uri ,我们可能想在浏览器中打开它。 有两种方法可以做到这一点:

  • Open it inside the app

    在应用内打开
  • Open it in an external browser

    在外部浏览器中打开

We usually want to keep users inside our app, but some schemas can’t be opened inside. For example, we don’t want to open instagram:// in the in-app browser. Even more, we want to open only http:// and https://.

我们通常希望将用户保留在我们的应用程序中,但某些模式无法在其中打开。 例如,我们不想在应用内浏览器中打开instagram:// 。 甚至更多,我们只想打开http://https://

We’ll add three different extensions. One will open Uri inside the app, another will open an external browser, and the last one will decide dynamically, based on the schema.

我们将添加三个不同的扩展名。 一个将在应用程序内打开Uri ,另一个将打开外部浏览器,最后一个将基于架构动态决定。

To open a web page inside an app, we need to either create a separate activity, or use a library that’ll do it for us. For simplicity, I chose the second way and included the FinestWebView library.

要在应用程序内打开网页,我们需要创建一个单独的活动,或者使用一个可以为我们做的库。 为简单起见,我选择了第二种方法,并包含FinestWebView库。

In your app gradle filem include this line:

在应用程序gradle文件中包括以下行:

dependencies {
implementation 'com.thefinestartist:finestwebview:1.2.7'
}

And in the manifest:

并在清单中:

<uses-permission android:name="android.permission.INTERNET" />
<activity
android:name="com.thefinestartist.finestwebview.FinestWebViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="sensor"
android:theme="@style/FinestWebViewTheme.Light" />

Here are our extensions:

这是我们的扩展名:

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.thefinestartist.finestwebview.FinestWebView


fun Uri.open(context: Context): Boolean =
  if (this.scheme == "http" || this.scheme == "https") {
    openInside(context)
    true
  } else
    openOutside(context)


fun Uri.openInside(context: Context) =
  FinestWebView.Builder(context).show(this.toString())


fun Uri.openOutside(context: Context): Boolean =
  try {
    val browserIntent = Intent(Intent.ACTION_VIEW, this)
    context.startActivity(browserIntent)
    true
  } catch (e: ActivityNotFoundException) {
    e.printStackTrace()
    false
  }

用法 (Usage)

val uri2 = "https://medium.com/@alex_nekrasov".asUriuri2?.open(this)

“ Context.vibrate(...)” (‘Context.vibrate(…)’)

Sometimes we want some physical feedback from our phone. For example, the device can vibrate when the user taps on some button. I’ll leave the discussion aside regarding if it’s a good practice or if it’s beyond the scope and it’d be better to concentrate on functionality.

有时,我们希望手机提供一些物理反馈。 例如,当用户点击某个按钮时,设备可能会振动。 我将把讨论抛在一边,如果这是一种好的做法,还是超出了范围,那么最好还是专注于功能。

First of all, add this permission to your manifest:

首先,将此权限添加到清单中:

<uses-permission android:name="android.permission.VIBRATE" />

延期 (Extension)

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator


fun Context.vibrate(duration: Long) {
  val vib = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    vib.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE))
  } else {
    @Suppress("DEPRECATION")
    vib.vibrate(duration)
  }
}

用法 (Usage)

vibrate(500) // 500 ms
// Should be called from Activity or other Context

or

要么

context.vibrate(500) // 500 ms
// Can be called from any place having context variable

结论 (Conclusion)

I hope these extensions were useful for you and made your code shorter and cleaner. You can modify them to meet your requirements and include them in your projects. Don’t forget to add all the necessary permissions to your manifest.

我希望这些扩展对您有用,并使您的代码更短,更清晰。 您可以修改它们以满足您的要求,并将它们包括在您的项目中。 不要忘记将所有必要的权限添加到清单中。

Happy coding, and see you next time!

祝您编程愉快,下次再见!

翻译自: https://medium.com/better-programming/22-kotlin-extensions-for-cleaner-code-acadcbd49357

kotlin 扩展属性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值