一些让代码更加干净的Kotlin扩展函数

Bilbo 2020-12-20 20:54:26 5910

我是一名移动开发人员,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中。

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
Bilbo
红包 点赞 收藏 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
Bilbo
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区