Kotlin-first but not kotlin-must
谷歌在 I/O 大會上宣布咸作,Kotlin 編程語言現(xiàn)在是 Android 應用程序開發(fā)人員的首選語言后锨阿,有更多的安卓程序投入Kotlin的懷抱。
Kotlin的語法糖更加提高了開發(fā)的效率记罚,加快了開發(fā)速度墅诡,使開發(fā)工作變得有趣,也讓我們有更多時間寫注釋了(笑)桐智。但是其實對于Kotlin和Java在Android開發(fā)上的選擇末早,個人覺得這個除了開發(fā)人員對語言的喜好的,同時也會應該到各自語言的魅力和特點说庭,甚至項目的需求以及后續(xù)維護等等各個因素然磷,沒有絕對的選擇的。我們要做到的是放大不同語言優(yōu)點并加以拓展刊驴,不是一味只選擇某個語言姿搜,語言不是問題,用的那個人怎么用才是關鍵捆憎。
Kotlin的DSL
一舅柜、從TextWatcher和 TabLayout.OnTabSelectedListener的優(yōu)化開始
先說說語法糖,例如下面的代碼:
infix fun <T:Any> MutableLiveData<T>.post(newValue:T){
this.postValue(newValue)
}
infix fun <T:Any> MutableLiveData<T>.set(newValue:T){
this.value = newValue
}
通過 infix 定義 中綴符號 使得操作LiveData的set和post更加好看
//初始化
private val pagerNumber = MutableLiveData<Int>()
......................省略無關內容.............................
//執(zhí)行postValue操作
pagerNumber post 0
又好像kotlin中的Iterable<T>的forEach有些人發(fā)現(xiàn)這樣用無法break躲惰,源碼如下:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
那我們不如改裝一下业踢,如下:
//定義
inline fun <T> Iterable<T>.forEachBreak(action: (T) -> Boolean ){
kotlin.run breaking@{
for (element in this)
if(!action(element)){
return@breaking
}
}
}
//應用
members.forEachBreak { call->
// true or false 控制適當?shù)胤降膔eturn值,跳出循環(huán)
true
}
回到今天的主題礁扮,Kotlin 的DSL更加語法糖升華,好像語法糖plus+瞬沦。今天是打算分享一下我開發(fā)時候對應DSL的運用太伊,如何利用DSL使得的開發(fā)變得有趣的。
先看看下面代碼
edittext.textWatcher {
afterTextChanged {
if(!isNullOrEmpty()){
button.visibility = View.VISIBLE
}else{
button.visibility = View.INVISIBLE
}
}
}
第一眼看上去逛钻,是不是很熟悉呢僚焦。不就一個EditText實現(xiàn)addTextChangedListener的方法,然后afterTextChanged里面執(zhí)行操作button是否出現(xiàn)嗎曙痘?但是你再認真看看芳悲,代碼是不是簡單了很多,好像少了什么边坤,這時候應該有人會留意到那個大括號了吧名扛。這個就是DSL,里面就一個afterTextChanged茧痒?肮韧,其實如果剛剛開始用Kotlin的開發(fā)會這樣想到,那我們建一個XXXX類繼承一下TextWatcher然后
edittext.addTextChangedListener(object:XXXX){
....................override相應方法................
}
其實這種寫法有錯嗎?當然沒有弄企,但是不美觀超燃。也不太符合Kotlin的編碼習慣。
那不如我直接POST代碼出來拘领,想讓你們看看內部封裝吧意乓。
fun EditText.textWatcher(textWatch: SimpleTextWatcher.() -> Unit) {
val simpleTextWatcher = SimpleTextWatcher(this)
textWatch.invoke(simpleTextWatcher)
}
class SimpleTextWatcher(var view: EditText) {
private var afterText: (Editable?.() -> Unit)? = null
fun afterTextChanged(afterText: (Editable?.() -> Unit)) {
this.afterText = afterText
}
private var beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null
fun beforeTextChanged(beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)) {
this.beforeText = beforeText
}
private var onTextChanged: ((s: CharSequence?,
start: Int, before: Int, count: Int) -> Unit)? = null
fun onTextChanged(onTextChanged: ((s: CharSequence?,
start: Int, before: Int, count: Int) -> Unit)) {
this.onTextChanged = onTextChanged
}
init {
view.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
afterText?.invoke(s)
}
override fun beforeTextChanged(s: CharSequence?,
start: Int, count: Int, after: Int) {
beforeText?.invoke(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?,
start: Int, before: Int, count: Int) {
onTextChanged?.invoke(s, start, before, count)
}
})
}
}
代碼有點長,當然我知道上面代碼還能繼續(xù)優(yōu)化约素,也歡迎大家提供意見届良。但是通過這樣的封裝,代碼風格更加簡便业汰。以此類推伙窃,我們也可以對TabLayout的addOnTabSelectedListener進一步封裝
//封裝
fun TabLayout.onTabSelected(tabSelect: TabSelect.() -> Unit) {
tabSelect.invoke(TabSelect(this))
}
class TabSelect(tab: TabLayout) {
private var tabReselected: ((tab: TabLayout.Tab) -> Unit)? = null
private var tabUnselected: ((tab: TabLayout.Tab) -> Unit)? = null
private var tabSelected: ((tab: TabLayout.Tab) -> Unit)? = null
fun onTabReselected(tabReselected: (TabLayout.Tab.() -> Unit)) {
this.tabReselected = tabReselected
}
fun onTabUnselected(tabUnselected: (TabLayout.Tab.() -> Unit)) {
this.tabUnselected = tabUnselected
}
fun onTabSelected(tabSelected: (TabLayout.Tab.() -> Unit)) {
this.tabSelected = tabSelected
}
init {
tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.apply { tabReselected?.invoke(tab) }
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
tab?.apply { tabUnselected?.invoke(tab) }
}
override fun onTabSelected(tab: TabLayout.Tab?) {
tab?.apply { tabSelected?.invoke(tab) }
}
})
}
}
//使用
tab.onTabSelected {
onTabSelected {
pos = position
}
}
我們其實還有更多這樣的方法可以這樣封裝,達到更加方便样漆。
二为障、DSL運用升級
我們還是先看看代碼:
//在Application內進行執(zhí)行,當然還是那個句:
//Application不要有太多的復雜耗時任務放祟,我只是舉個一個可以運用的地方而已鳍怨,保證初始化成功。
//RetroHttp就是一個Retrofit Client封裝類
//createApi(tClass)還是那個Retrofit.create(tClass)
startInit {
modules(Module{
single{ RetroHttp.createApi(auth::class.java) }
}
}
//然后我們在某個Repository內初始化 interface auth 這個接口類
val api : auth by inject()
是不是很簡單呢跪妥,我們只要inject方法就能把這個接口初始化成功了鞋喇。
先別那么快否定我,這樣寫實不實用眉撵,因為初始化這些接口侦香,對于每個安卓開發(fā)都再熟悉不過了,方法一大堆纽疟。今天我們是對DSL的進一步學習罐韩,把思路拓寬。
這個startInit內部長這樣的
fun startInit(component: Components.()->Unit){
component.invoke(Components.get())
}
class Components {
companion object{
private val entry = ArrayMap<String,Any?>()
private val module = ArrayList<Module>()
private val instant by lazy { Components() }
fun get() = instant
fun getEntry() = entry
}
fun modules(vararg modules: Module){
module.addAll(modules)
}
}
inline fun <reified T> get(name: String = T::class.java.name) : T{
return Components.getEntry()[name] as T
}
inline fun <reified T> inject(name: String = T::class.java.name) : Lazy<T> {
return lazy { Components.getEntry()[name] as T }
}
class Module(component: Component.() -> Unit){
init {
component.invoke(Component())
}
}
class Component{
inline fun <reified T>single(noinline single: Component.()->T){
val name = T::class.java.name
Components.getEntry()[name] = single()
}
}
這個reified關鍵字起到的作用很核心污朽,可以簡化模板代碼散吵,編譯器可以自動推斷類型
例如:
//定義
inline fun <reified T>startActivity(bundle: Bundle? = null) {
val intent = Intent(this, T::class.java)
if (bundle != null) {
intent.putExtras(bundle)
}
startActivity(intent)
}
//利用
startActivity<CollectActivity>()
能直接通過這個reified 拿到泛型的類型,對于Kotlin這種很注重泛型的語言尤其出色蟆肆,加上inline進一步節(jié)省調用開銷矾睦。通過startInit方法我們現(xiàn)在可以更加優(yōu)雅處理類的構造函數(shù)初始化。
實用大升級
在平時開發(fā)中炎功,DSL除了應用在一些普通方法上枚冗,我們其實還可以拓展到一些常用類的封裝,例如DialogFragment蛇损。DialogFragment其實對于安卓的開發(fā)人員來說官紫,都不是一個陌生的類肛宋。
方法 — 、可以覆寫其 onCreateDialog 利用AlertDialog或者Dialog創(chuàng)建出Dialog束世。
方法 二酝陈、 覆寫其 onCreateView 使用定義的xml布局文件展示Dialog。
那我們的DSL可以怎么進一步優(yōu)化使用過程毁涉,接下來分享一下我的處理:
一沉帮、先提取常用配置,減少重復代碼的書寫
我們平時在書寫DialogFrament時候都會有不少的模板代碼贫堰,幾乎三四個DialogFramgent 里面都有好幾十行是一模一樣的基礎設置穆壕,那我們先定義一個注解
@Target(AnnotationTarget.CLASS)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class WindowParam(val gravity:Int = Gravity.CENTER,val outSideCanceled:Boolean = true, val noAnim : Boolean = false,
val animRes :Int = -1,val canceled:Boolean = true,
val dimAmount :Float = -1f)
// gravity 是dialog中的Window的 setGravity(gravity)方法
//outSideCanceled 是 dialog.setCanceledOnTouchOutside(outSideCanceled)
//canceled 是 dialog.setCancelable(canceled)
//noAnim 是指是否使用進場出場動畫
//animRes是我們的進場出場動畫資源
//dimAmount 是window的setDimAmount(dimAmount) 用于控制彈窗后灰色蒙版的透明度
二、引入DefaultLifecycleObserver
引入這個對于DialogFragment是為方便獲取到它依附的Activity的生命周期其屏,能在適當?shù)牡胤竭M行適當操作
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context
if(context is AppCompatActivity){
init(context)
}else if(context is LifecycleOwner){
init(context)
}
}
private var owner : LifecycleOwner? = null
private fun init(owner: LifecycleOwner){
this.owner = owner
this.owner?.lifecycle?.addObserver(this)
}
在onAttach方法的地方獲取LifecycleOwner,進行生命周期的監(jiān)聽喇勋。
三、引入DSL語法
在我這個封裝中偎行,我是覆寫onCreateDialog這個方法川背,由于封裝內容很多,我先貼出代碼在慢慢一步步講
abstract class SimpleDialogFragment : DialogFragment(),DefaultLifecycleObserver {
lateinit var activity : Context
private var onCreate :(()->Int)? = null
private var onWindow :((window:Window)->Unit)? = null
private var onView :((view:View)->Unit)? = null
abstract fun build(savedInstanceState: Bundle?)
private var owner : LifecycleOwner? = null
private fun init(owner: LifecycleOwner){
this.owner = owner
this.owner?.lifecycle?.addObserver(this)
}
override fun onStop(owner: LifecycleOwner) {
dialog?.apply {
dismissAllowingStateLoss()
}
}
override fun onDestroy(owner: LifecycleOwner) {
if(this.owner != null){
dismissAllowingStateLoss()
this.owner?.lifecycle?.removeObserver(this)
}
}
fun buildDialog(onCreate :(()->Int)) : SimpleDialogFragment{
this.onCreate = onCreate
return this
}
fun onWindow(onWindow :((window:Window)->Unit)) : SimpleDialogFragment{
this.onWindow = onWindow
return this
}
fun <T : ViewDataBinding> View.onBindingView(onBindingView :((binding : T?)->Unit)){
onBindingView.invoke(DataBindingUtil.bind<T>(this))
}
fun onView(onView :((view:View)->Unit)) : SimpleDialogFragment{
this.onView = onView
return this
}
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context
if(context is AppCompatActivity){
init(context)
}else if(context is LifecycleOwner){
init(context)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
build(savedInstanceState)
val viewId = onCreate?.invoke()
if(viewId!= null){
val view = View.inflate(activity, viewId, null)
val param = javaClass.getAnnotation(WindowParam::class.java)!!
val gravity = param.gravity
val outSideCanceled = param.outSideCanceled
val canceled = param.canceled
val dimAmount = param.dimAmount
val noAnim = param.noAnim
val dialog = Dialog(activity)
dialog.setContentView(view)
dialog.setCanceledOnTouchOutside(outSideCanceled)
dialog.setCancelable(canceled)
val window = dialog.window
val dm = DisplayMetrics()
window?.apply {
windowManager.defaultDisplay.getMetrics(dm)
setLayout(dm.widthPixels, window.attributes.height)
setBackgroundDrawable(ColorDrawable(0x00000000))
setGravity(gravity)
if(!noAnim){
setWindowAnimations(R.style.LeftRightAnim)
}
if(dimAmount!=-1f){
setDimAmount(dimAmount)
}
onWindow?.invoke(this)
}
view?.apply {
onView?.invoke(this)
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
override fun dismiss() {
dialog?.apply {
if(isShowing){
if(getActivity()!=null){
super.dismiss()
}
}
}
}
override fun show(manager: FragmentManager, tag: String?) {
try {
if(!isAdded){
val transaction = manager.beginTransaction()
transaction.add(this, tag)
transaction.commitAllowingStateLoss()
transaction.show(this)
}
}catch (e: Exception){
Log.e("DialogFragment","${e.message}")
}
}
}
用的時候變成了這樣蛤袒,如下:
@WindowParam(gravity = Gravity.BOTTOM,animRes = R.style.BottomTopAnim)
class BottomDialog : SimpleDialogFragment() {
override fun build(savedInstanceState: Bundle?) {
buildDialog {
R.layout.XXXXXXXX
}
onView {
it.onBindingView<XXXXXXXBinding> { binding ->
binding?.apply {
//do something
}
}
}
}
相對的代碼量少了熄云,把基本的內容都封裝起來,
一妙真、buildDialog 方法缴允,利用onCreate :(()->Int) 這個Int的返回值,即該方法大括號的最后一行珍德,代表返回值的特性练般,把layoutId設置進去,當然你也可以采用在上面注解的地方锈候,添加也可以薄料。
二、onWindow方法晴及,該方法可以拿到dialog.window的對象,利用該對象嫡锌,我們可以再進一步進行配置
三虑稼、onView方法,該方法能拿到layout的view對象势木,這里提一個特別的地方
平時開發(fā)可以留一下在不使用databinding情況下蛛倦,Dialog中使用kotlin了
apply plugin: 'kotlin-android-extensions'這個配置可以拿到view的id,但是有一點需要注意引入的時候 xxxxxx.* 和 xxxxx.view.* (xxxxx即你的布局名字)是有區(qū)別的,各位可以留意一下啦桌。
四溯壶、onBindingView是我特意留的一個databinding的方法及皂,方便使用databinding的朋友。
結合協(xié)程實現(xiàn)倒計時功能
我們平時實現(xiàn)倒計時功能都會用到RxJava,CountDownTimer,Timer+TimerTask,線程且改,今天借此利用線程的方案验烧,即Kotlin中的協(xié)程渴杆,廢話不說先放代碼:
fun LifecycleOwner.counter(dispatcher: CoroutineContext,start:Int, end:Int, delay:Long, onProgress:((value:Int)->Unit),onFinish: (()->Unit)?= null){
val out = flow<Int> {
for (i in start..end) {
emit(i)
delay(delay)
}
}
lifecycleScope.launchWhenStarted {
withContext(dispatcher) {
out.collect {
onProgress.invoke(it)
}
onFinish?.invoke()
}
}
}
//使用
counter(Dispatchers.Main,1,3,1000,{
//倒計時過程
}){
//完成倒計時
}
利用了攜程中的flow方法丈冬,進一步的優(yōu)化了采用線程方案的倒計時。
小結
這篇文章也是我第二篇分享文章居兆,因為個人很少寫文章和博客慨蓝,也不是懶不懶的問題感混,其實就是有個感覺,覺得自己學習知識學了一段時間礼烈,是不是應該做個分享弧满,多一些交流,讓自己的思路更加拓展此熬。
個人的github地址:https://github.com/ShowMeThe
也分享一下庭呜,無聊時候寫的一個基于ViewPager2的輪播圖:https://github.com/ShowMeThe/BannerView
有問題也可以留個言,交流一下摹迷。