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.
有两个选项: function
或read-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).
如果您使用价格,则可能会出现Floats
和Doubles
精度问题。 表示价格的一种流行方法是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.
这里的问题之一是Act
和Cell
相互引用,这可能导致内存泄漏。 一个好的解决方案是使用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
的扩展,从编码的角度来看,还可以。 但从逻辑上讲,我将使其更加具体。 它可以扩展Activity
或AppCompatActivity
以避免副作用,例如从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 扩展属性