一些让代码更加干净的Kotlin扩展函数
我是一名移动开发人员,Kotlin出现后我立即切换到它的原因之一是它支持扩展。扩展允许我们向任何现有类添加方法,甚至向任何或可选类型(例如,Int?)添加方法。
如果我们在开发过程中扩展基类,所有派生类都会自动获得此扩展。我们还可以覆盖来自扩展的方法,这使得该机制更加灵活和强大。
这篇文章主要介绍几个非常有用的扩展函数,来让我们的代码更加的简洁和干净。
‘Int.toDate()’ and ‘Int.asDate’
我们在开发过程中经常会使用时间戳timestamp并表示日前或者时分,它的单位是秒或者毫秒。同时,我们我们也常常有需求将时间戳转化为日前,如果通过扩展函数可以简单是实现
有两种方式可以实现:通过扩展方法或者扩展只读属性,这两个方式都可以实现,没有差异,用哪一种只是个人喜好问题。
import java.util.Date
fun Int.toDate(): Date = Date(this.toLong() * 1000L)
val Int.asDate: Date
get() = Date(this.toLong() * 1000L)
用法
val json = JSONObject();
json.put("date", 1598435781)
val date = json.getIntOrNull("date")?.asDate
注意:在这个例子中,我使用了另一个扩展- getIntOrNull。如果JSON中存在,则返回int值,否则返回null,我相信大家可以很容易实现这个功能。
‘String.toDate(…)’ and ‘Date.toString(…)’
日期对象的另一种流行转换是日前和字符串相互转换。我不是在谈论标准的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)
}
注意:在这个例子中,我们每次都创建一个SimpleDateFormat对象。如果我们在列表中使用此扩展,请考虑从扩展代码中删除val dateFormatter = SimpleDateFormat(format, Locale.US),我们应该在外部创建一个SimpleDateFormat对象并将其作为全局变量或静态类成员。
用法
val format = "yyyy-MM-dd HH:mm:ss"
val date = Date()
val str = date.toString(format)
val date2 = str.toDate(format)
‘String.toLocation(…)’
当我们使用API并获得对象的坐标时,我们可以将它们作为两个不同的字段获得。但有时它是一个字段,用逗号分隔纬度和经度。
有了这个扩展,我们可以将一个字段的转换成两个字段的位置信息:
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
}
用法
val apiLoc = "41.6168, 41.6367".toLocation("API")
‘String.containsOnlyDigits’ and ‘String.isAlphanumeric’
我们来讨论一下字符串的属性。它们可以是空的,也可以不是——例如,它们可以只包含数字或字母数字字符。上面这些扩展可以帮助我们通过一行代码实现这个功能:
val String.containsDigit: Boolean
get() = matches(Regex(".*[0-9].*"))
val String.isAlphanumeric: Boolean
get() = matches(Regex("[a-zA-Z0-9]*"))
用法
val digitsOnly = "12345".containsDigitOnly
val notDigitsOnly = "abc12345".containsDigitOnly
val alphaNumeric = "abc123".isAlphanumeric
val notAlphanumeric = "ab.2a#1".isAlphanumeric
‘Context.versionNumber’ and ‘Context.versionCode’
在Android app中,一个i 叫好的用户体验是在关于或支持反馈页面上需要显示版本号。它将帮助用户了解他们是否需要更新,并在报告错误时提供有价值的信息。标准的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
}
用法
val vn = versionName ?: "Unknown"
val vc = versionCode?.toString() ?: "Unknown"
val appVersion = "App Version: $vn ($vc)"
‘Context.screenSize’
另一个对Android有用的扩展是上下文扩展,它允许你获得设备屏幕大小。这个扩展返回屏幕尺寸的像素
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
}
用法
Log.d(TAG, "User's screen size: ${screenSize.x}x${screenSize.y}")
‘Any.deviceName’
为Any添加扩展函数需要仔细思考它的必要性。如果这样做,函数就会显示为全局的,所以基本上,这与声明一个全局函数是一样的。
在我们的下一个扩展,我们将获得一个Android设备的名称。因为它不需要任何上下文,我们将它作为任何扩展:
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
}
用法
Log.d(TAG, "User's device: $deviceName")
‘T.weak’
这个case有点复杂但也是非常常见的case。假设我们有一个Activity和一个包含许多item的RecycleView。每个item都需要回调。假设它有一个委托接口,为实现这个功能我们需要将Activity本身传递给每个item,实现如下:
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")
}
// ...
}
上面代码只是在展示它的结构。实际应用中,我们可以使用ViewHolder。
但是上面代码存在一个问题之一是Act和Cell相互引用,这可能导致内存泄漏。一个好的解决方案是使用WeakReference。包装在WeakReference中的委托变量不会影响我们的Act的引用计数器,所以只要我们关闭屏幕,它就会连同所有分配的item一起被销毁(或添加到队列中稍后销毁)。
我们通过简单的扩展允许得到任何对象来获得弱引用:
val <T> T.weak: WeakReference<T>
get() = WeakReference(this)
用法
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()
}
}
我想强调这个扩展是通用的,它将与任何类型的兼容。
‘Context.directionsTo(…)’
从Android应用程序打开导航是一个很受欢迎的功能。Android是谷歌产品,和谷歌Maps一样。大多数安卓手机和平板电脑都预装了谷歌地图应用程序。最简单的解决方案是在Android地图应用程序中打开导航。如果没有安装导航,只需在网络浏览器中打开它。
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)
}
}
这是context的扩展,从编程的角度来看,这没问题。但从逻辑上讲,我会让它更具体。它可以是Activity的扩展或AppCompatActivity的扩展,这样可以避免Context在其他组建中使用,比如被Service使用。我们可以将可扩展类更改为我们在应用程序中使用的任何类。
‘AppCompatActivity.callTo(…)’ or ‘Activity.callTo(…)’
我们使用与前一个扩展相同的逻辑,同样可以定义类似权限请求的扩展方法,这个扩展方法可以在所有的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)
}
}
用法(在Activity中使用)
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’
我们通常认为互联网地址是一个字符串。我们可以把它输入引号。例如,“https://www.ebaina.com/articles/140000005260”
但是对于内部使用,Android有一种特殊类型:Uri。把一种转换成另一种很容易。下面的扩展允许我们将aString转换为具有验证的anUri。如果它不是一个有效的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
}
用法
val uri = "invalid_uri".asUri
val uri2 = "https://medium.com/@alex_nekrasov".asUri
‘Uri.open(…)’, ‘Uri.openInside(…)’, and ‘Uri.openOutside(…)’
现在,当我们有一个Uri时,我们可能希望在浏览器中打开它。有两种方法可以做到这一点:
1)app内打开
2)通过手机浏览器打开
我们通常想把用户留在app中,但有些schemas不能在app中打开。比如我们只想在app中打开http:// 或者是https:// 的链接
针对上面描述的上下文,我们可以添加三种不同的扩展函数。一个将在app内部打开Uri,另一个将打开外部浏览器,最后一个将根据schemas动态决定。
要在app中打开网页,我们需要创建一个单独的活动,或者使用一个库来为我们做这件事。为了简单起见,我选择了第二种方法用FinestWebView库。
我们可以在Gradle中注入如下:
dependencies {
implementation 'com.thefinestartist:finestwebview:1.2.7'
}
在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" />
扩展函数如下:
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
}
用法
val uri2 = "https://medium.com/@alex_nekrasov".asUri
uri2?.open(this)
‘Context.vibrate(…)’
325/5000
有时候我们需要手机的一些物理反馈。例如,当用户点击某个按钮时,设备会震动。我将把讨论放在一边,不管它是否是一个好的实践,或者它是否超出了范围,最好还是专注于功能。
首先,将此权限添加到我们的manifest中:
<uses-permission android:name="android.permission.VIBRATE" />
扩展函数
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)
}
}
用法
vibrate(500) // 500 ms
// Should be called from Activity or other Context
或者
context.vibrate(500) // 500 ms
// Can be called from any place having context variable
结论
我希望这些扩展对大家有用,并使大家的代码更简介、更干净。大家可以修改它们以满足大家的需求,并将它们包含在大家的项目中。不要忘记添加所有必要的权限到manifest中。
- 分享
- 举报
-
浏览量:4475次2021-11-20 15:17:49
-
浏览量:1720次2020-05-06 09:55:45
-
浏览量:2628次2022-11-10 10:02:57
-
浏览量:964次2023-04-19 09:11:57
-
浏览量:4595次2020-08-26 17:30:20
-
浏览量:5736次2019-05-23 09:24:31
-
浏览量:2053次2020-08-14 18:32:35
-
浏览量:3366次2023-04-16 20:29:23
-
浏览量:2054次2019-11-13 08:51:50
-
浏览量:4440次2021-04-19 14:55:00
-
浏览量:2887次2020-08-24 21:15:42
-
浏览量:2611次2020-03-26 10:21:51
-
浏览量:2746次2018-01-07 15:19:24
-
浏览量:1971次2018-04-13 15:26:46
-
浏览量:1073次2024-01-04 17:47:00
-
浏览量:2034次2020-08-14 18:38:14
-
浏览量:2056次2020-04-02 10:29:26
-
浏览量:2208次2020-02-26 10:38:33
-
浏览量:1244次2020-01-16 10:07:13
-
广告/SPAM
-
恶意灌水
-
违规内容
-
文不对题
-
重复发帖
Bilbo
感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~
举报类型
- 内容涉黄/赌/毒
- 内容侵权/抄袭
- 政治相关
- 涉嫌广告
- 侮辱谩骂
- 其他
详细说明