Skip to content

Commit 844cc3a

Browse files
committed
提交4.3文章及案例
1 parent f59a107 commit 844cc3a

31 files changed

+730
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Kotlin 兼容 Java 遇到的最大的“坑”
2+
3+
前言:上周我发了一篇文章[Kotlin 遇到 MyBatis:到底是 Int 的错,还是 data class 的错?](https://mp.weixin.qq.com/s?__biz=MzIzMTYzOTYzNA==&mid=2247483908&idx=1&sn=0c072a630198d4a23a7d3aec700c138b&chksm=e8a05d39dfd7d42f1494c5f0fcc0562112be6d8912e44fc51f17dee472f9ebdfaad7d930e3b1#rd)讲如何解决群里面一兄弟遇到的 data class 与 MyBatis 相克的问题,其中提到了几种外门邪道的方法,也提到了官方的解决思路,有些朋友看了之后还是不太明白,甚至紧接着就有小伙伴在使用 Realm 的时候遇到了类似的问题,看来,我还是得再写一篇来进一步告诉大家,这究竟是个什么问题,以及该如何面对它。
4+
5+
本文源码在 [Github:Kotlin-Tutorials](https://github.com/enbandari/Kotlin-Tutorials) 这个项目当中,微信公众号无法添加外链,请大家点击“阅读原文”获取。
6+
7+
## 一个 Realm 的小例子
8+
9+
Realm 在 2016 年与 RxJava、Retrofit 这样的框架一起,在 Android 开发领域内着实小小的火了一把,如果大家对它不了解,没关系,传送门 biu ~ [Realm](https://realm.io/)
10+
11+
我们先按照官网的说明配置好 gradle 依赖,话说呀,这互联网发展这么快,新时代的框架一出来,逼格果断就体现在完善的构建和开发生态,你发布的东西还只是一个 jar 包,人家呢,早上了 maven 不说,还要搞几个 gradle 任务来方便你开发:
12+
13+
```groovy
14+
buildscript {
15+
ext.kotlin_version = '1.1.1'
16+
repositories {
17+
jcenter()
18+
}
19+
dependencies {
20+
classpath 'com.android.tools.build:gradle:2.3.0'
21+
//这里将 realm 的 gradle 插件加入 gradle 构建的运行时
22+
classpath "io.realm:realm-gradle-plugin:3.0.0"
23+
}
24+
}
25+
...
26+
```
27+
28+
```groovy
29+
apply plugin: 'com.android.application'
30+
apply plugin: 'realm-android' //应用插件,Realm 会在这里添加自己的一些构建任务
31+
...
32+
```
33+
其实 gradle 插件开发也是一个很有意思的话题,如果大家有需要,我后面也可以写几篇文章介绍下(悄悄告诉你们,其实我早就想写了,这不是 kotlin 版的 gradle 还没有正式发布么!)。
34+
35+
有了这个我们就可以开始写个 Realm 的 demo 了。
36+
37+
小明:等等!我还有一事不明,你怎么不添加 realm 的依赖就要开始写 demo 了啊!
38+
39+
艾玛,要么说小明人家就是明白人呢,我前面写了一大堆,只不过是添加了 gradle 构建的依赖而已,而我们的程序想要使用 realm,必须依赖 realm 的运行时库才行。那这么说我是不是漏掉了什么?当然没有,怎么会呢,我这么聪(dou)明(bi)的人,我可是一步一步照着官网的步骤抄的!
40+
41+
其实呀,realm 的运行时依赖早在我们 apply plugin 的时候就已经添加进来的, realm-android 这个插件除了添加了一些它需要的 gradle 任务之外,也顺手帮我们把依赖添加了。嗯,就是酱紫,如果有那个同学学(xian)有(de)余(dan)力(teng),可以翻一翻 realm 插件的源码。
42+
43+
来来来,赶紧看 demo,不然有些人该内急了~
44+
45+
首先在 Application 当中初始化它:
46+
47+
```kotlin
48+
class App : Application() {
49+
override fun onCreate() {
50+
super.onCreate()
51+
Realm.init(this)
52+
Realm.setDefaultConfiguration(
53+
RealmConfiguration.Builder()
54+
.deleteRealmIfMigrationNeeded()
55+
.schemaVersion(1)
56+
.build())
57+
}
58+
}
59+
```
60+
61+
定义一个 User 类:
62+
63+
```kotlin
64+
data class User(@PrimaryKey var id: Int, val name: String) : RealmObject()
65+
```
66+
67+
接着我们开始存数据和查数据啦:
68+
69+
```kotlin
70+
add.setOnClickListener {
71+
Realm.getDefaultInstance().use {
72+
it.beginTransaction()
73+
val d = it.createObject(User::class.java, it.where(User::class.java).count())
74+
d.name = "User ${d.id}"
75+
it.commitTransaction()
76+
}
77+
78+
}
79+
80+
query.setOnClickListener {
81+
Realm.getDefaultInstance().use {
82+
it.where(User::class.java).findAll().map {
83+
Log.d(TAG, it.toString())
84+
}
85+
}
86+
}
87+
```
88+
89+
想得挺美,结果呢?编译不通过。
90+
91+
```
92+
Error:A default public constructor with no argument must be declared in User if a custom constructor is declared.
93+
```
94+
95+
## 无参构造方法
96+
97+
这就让我想到上周的文章,那篇文章里面我们其实就发现症结根本不是什么 Int 和 Integer,而是无参构造方法。JavaBean 是 Java 的一个概念,我其实甚至有些觉得 Java 的设计者们通过 JavaBean 这样的概念来弥补语言本身的缺陷——不管怎样,JavaBean 是不能没有无参构造的,。
98+
99+
Kotlin 呢,语言层面就有类似于 JavaBean 的东西,那就是 data class,这俩孩子实在太像了,以至于大家经常把 data class 当做 JavaBean 来使。嗯,你信不信 Kotlin 的设计者也是这么想的呢?当然,用 data class 这样一个名正言顺的“亲儿子”数据类来替代 JavaBean 这么个语言层面没有任何支持和认可的“野孩子”,应该算是 JavaBean 莫大的荣幸了,可问题又出在 Java 语言本身构造方法滥用的潜在问题上了。在 Java 中,构造方法真心是一个很没有存在感的东西,大家总是根据自己的喜好来随意的定义很多个构造方法的版本,而最终忽视掉它们的内在联系,导致没有正常走完初始化逻辑的实例满天飞,这家伙如果是导弹,我估计也不需要解放军就可以直接把台湾给统一了。
100+
101+
说了这么多,我主要是想吐槽两个点:第一个就是 Java 本身语言设计层面几乎没有任何照顾到数据类的体现(可千万别说 clone 和 Serialize),第二个就是 Java 对其对象的实例化过程的把控太过于儿戏。
102+
103+
这两点呢,Kotlin 都做的很好,我现在写 Kotlin 经常被迫认真思考一个类该如何正确初始化,这显然对于我们的程序结构和逻辑梳理有莫大的好处。可是结果呢?Java 时代的那些框架们受不了了。Kotlin 背靠着 Java 这座大山,Java 就像它的父母一样,父母的观念再老再陈旧,Kotlin 也得做好自己该做的,一方面是向现在看来陈旧但在过去已经非常革命的观念致敬,另一方面嘛,如果 Java 不支持个几十万首付,Kotlin 能买得起房吗?
104+
105+
哇塞,我好能扯啊。
106+
107+
其实想要解决 default public constructor 这样的问题,Kotlin 官方已经想到了,那就是 noarg。嗯,我原以为我提一句 noarg 大家就会知道是什么了,看来是我想的简单了,毕竟这个东西在 1.0.6 才出来,当时我还在介绍这个版本的时候提到了它的使用方法,朋友们可能还没有接触过,没关系,下面我再贴一些写法,大家一看就明白:
108+
109+
首先你要做的就是定义一个注解:
110+
111+
```kotlin
112+
annotation class PoKo
113+
```
114+
接着 gradle 配置一下脚本的依赖:
115+
116+
```groovy
117+
buildscript {
118+
...
119+
dependencies {
120+
...
121+
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
122+
...
123+
}
124+
}
125+
```
126+
加了运行时环境,那么我们就可以使用 noarg 插件了:
127+
128+
```groovy
129+
apply plugin: "kotlin-noarg"
130+
131+
noArg {
132+
annotation("net.println.kotlin.realm.PoKo")
133+
}
134+
```
135+
配置完之后,PoKo 这个注解就有了超能力,所有被它标注的类在编译时都会生成一个无参的构造方法,于是我们给 User 加一个 PoKo 的注解:
136+
137+
```kotlin
138+
@PoKo data class User(...) : RealmObject()
139+
```
140+
搞定,果断去编译一下!!
141+
142+
## final 还是不 final,这是个问题
143+
144+
本来兴高采烈的以为不就是个无参构造的问题嘛,结果编译的时候又爆出了新的问题:
145+
146+
```kotlin
147+
Error:(31, 61) error: cannot inherit from final User
148+
```
149+
150+
好家伙,这究竟发生了什么。。原来 Realm 在编译的时候生成了一个类:
151+
152+
```java
153+
public class UserRealmProxy extends net.println.kotlin.realm.User
154+
implements RealmObjectProxy, UserRealmProxyInterface
155+
```
156+
157+
这个类要继承我这个 User 类,结果就报错了。
158+
159+
下面是理(che)论(dan)时间。我们说在 C++ 当中给合适的变量、函数参数、函数返回值甚至函数加上 const 是个好习惯,大家没有意见吧?同样的,Java 当中给那些不变的量、不能被继承的类、不能被覆写的方法加上 final 也是个好习惯,大家也没有意见吧?那么问题来了,大家有几个人这么干了?是不是不到万不得已,才懒得写那个 final 呢,五个字母呢,你是想累死宝宝啊?我就知道 Effective Java 这本书看了也白看,因为大家经常明知道什么是好习惯却还是要对着干,这个不是因为大家不喜欢好习惯,而是因为坚持好习惯需要成本!不瞒各位说,我中午为了坚持午休的好习惯,牺牲了跟组里面的小伙伴一起开黑上分的机会,还得装着拥护“人民ri报”关于“小学生打排位太坑”的评论,我容易么我。。
160+
161+
嗯,扯远了。还是说 final 的事儿,Kotlin 就做的很好,它默认所有的类、变量、方法都是 final 的,想要继承?来,过来申请我给你审批。。。你看,这样从根儿解决问题,我们再也不用为了坚持好习惯而发愁了,因为我们根本不需要坚持,难道你想要坚持坏习惯嘛?
162+
163+
可是 Java 及其框架们呢?原来到北京买房有钱就行,现在呢,商住都不让买了啊(什么?你说广州都不让卖了?)。那叫一个不适应,这可不是得闹事儿么。
164+
165+
Kotlin 官方考虑到 Java 帮它出首付买房的事儿,想了想算了,还是出个什么插件,解决下这个问题吧,于是 allopen 闪亮登场!allopen 的原理跟 noarg 极其类似,它是在编译器对指定的类进行去“final”化,你别看你写代码的时候 User 还是个 final 的类,不过编译成字节码之后这天呀可就变了。
166+
167+
关于 allopen 的使用,跟 noarg 简直不要太像,先定义一个注解:
168+
169+
```kotlin
170+
annotation class PoKo // How old r U!
171+
```
172+
可以跟 noarg 公用同一个注解,也可以自己另外单独定义一个,这个不要紧。
173+
174+
接着 gradle 配置搞起:
175+
176+
```groovy
177+
buildscript {
178+
...
179+
dependencies {
180+
...
181+
classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version"
182+
...
183+
}
184+
}
185+
```
186+
接着就是应用插件,配置注解一气呵成:
187+
188+
```groovy
189+
apply plugin: "kotlin-allopen"
190+
191+
allOpen {
192+
annotation("net.println.kotlin.realm.PoKo")
193+
}
194+
```
195+
编译运行~
196+
197+
ps:如果加了 allopen 和 noarg 之后编译仍然提示原来的错误,记得狠狠地 clean 一下才行哈。
198+
199+
##
200+
201+
## 认真脸:究竟什么是 “坑”
202+
203+
前面说了 Kotlin 的两个 “坑”,都是关于 data class 的。有人认为这么说 Kotlin 不公平,毕竟人家 Kotlin 也是可以写出下面的代码的:
204+
205+
```kotlin
206+
class User{
207+
var id: String? = null
208+
var name: String? = null
209+
}
210+
```
211+
212+
尽管你在为 Kotlin 打抱不平,不过如果你真要写这样的代码,我建议你还是用 Java 吧。你不属于 Kotlin。。。
213+
214+
Kotlin 这么美的语言,怎么能写这么丑陋的东西呢?这就好比有人说为什么空类型强转为非空类型一定要两个感叹号呢,用一个不就够了么,两个看起来好丑呀!
215+
216+
```kotlin
217+
var user: User? = getUser()
218+
user!!.name = "小明" //小明,他们说你丑!
219+
```
220+
221+
有人回答说:明明这就是丑陋的东西,为什么要美化?掩盖事物的本质只能让事情变得更糟糕!
222+
223+
我们用 Kotlin 企图兼容 Java 的做法,本来就是权宜之计,兼容必然带来新旧两种观念的冲突以及丑陋的发生,这么说来,我倒是更愿意期待 Kotlin Native 的出现了。

code/RealmIssue/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/workspace.xml
5+
/.idea/libraries
6+
.DS_Store
7+
/build
8+
/captures
9+
.externalNativeBuild

code/RealmIssue/app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

code/RealmIssue/app/build.gradle

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
apply plugin: 'com.android.application'
2+
apply plugin: 'kotlin-android'
3+
apply plugin: 'kotlin-android-extensions'
4+
apply plugin: 'realm-android'
5+
apply plugin: "kotlin-allopen"
6+
apply plugin: "kotlin-noarg"
7+
8+
android {
9+
compileSdkVersion 25
10+
buildToolsVersion "25.0.2"
11+
defaultConfig {
12+
applicationId "net.println.kotlin.realm"
13+
minSdkVersion 16
14+
targetSdkVersion 25
15+
versionCode 1
16+
versionName "1.0"
17+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18+
}
19+
buildTypes {
20+
release {
21+
minifyEnabled false
22+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23+
}
24+
}
25+
}
26+
27+
allOpen {
28+
annotation("net.println.kotlin.realm.PoKo")
29+
}
30+
31+
noArg {
32+
annotation("net.println.kotlin.realm.PoKo")
33+
}
34+
35+
36+
dependencies {
37+
compile fileTree(dir: 'libs', include: ['*.jar'])
38+
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
39+
exclude group: 'com.android.support', module: 'support-annotations'
40+
})
41+
compile 'com.android.support:appcompat-v7:25.3.1'
42+
compile 'com.android.support.constraint:constraint-layout:1.0.2'
43+
testCompile 'junit:junit:4.12'
44+
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
45+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Add project specific ProGuard rules here.
2+
# By default, the flags in this file are appended to flags specified
3+
# in C:\Users\kanade\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4+
# You can edit the include path and order by changing the proguardFiles
5+
# directive in build.gradle.
6+
#
7+
# For more details, see
8+
# http://developer.android.com/guide/developing/tools/proguard.html
9+
10+
# Add any project specific keep options here:
11+
12+
# If your project uses WebView with JS, uncomment the following
13+
# and specify the fully qualified class name to the JavaScript interface
14+
# class:
15+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16+
# public *;
17+
#}
18+
19+
# Uncomment this to preserve the line number information for
20+
# debugging stack traces.
21+
#-keepattributes SourceFile,LineNumberTable
22+
23+
# If you keep the line number information, uncomment this to
24+
# hide the original source file name.
25+
#-renamesourcefileattribute SourceFile
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="net.println.kotlin.realm">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:name=".App"
8+
android:icon="@mipmap/ic_launcher"
9+
android:label="@string/app_name"
10+
android:roundIcon="@mipmap/ic_launcher_round"
11+
android:supportsRtl="true"
12+
android:theme="@style/AppTheme">
13+
<activity android:name="net.println.kotlin.realm.MainActivity">
14+
<intent-filter>
15+
<action android:name="android.intent.action.MAIN" />
16+
17+
<category android:name="android.intent.category.LAUNCHER" />
18+
</intent-filter>
19+
</activity>
20+
</application>
21+
22+
</manifest>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.println.kotlin.realm
2+
3+
import android.app.Application
4+
5+
import io.realm.Realm
6+
import io.realm.RealmConfiguration
7+
8+
class App : Application() {
9+
override fun onCreate() {
10+
super.onCreate()
11+
Realm.init(this)
12+
Realm.setDefaultConfiguration(
13+
RealmConfiguration.Builder()
14+
.deleteRealmIfMigrationNeeded()
15+
.schemaVersion(1)
16+
.build())
17+
}
18+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.println.kotlin.realm
2+
3+
import android.os.Bundle
4+
import android.support.v7.app.AppCompatActivity
5+
import android.util.Log
6+
import io.realm.Realm
7+
import kotlinx.android.synthetic.main.activity_main.*
8+
9+
class MainActivity : AppCompatActivity() {
10+
companion object{
11+
const val TAG = "KotlinRealm"
12+
}
13+
14+
override fun onCreate(savedInstanceState: Bundle?) {
15+
super.onCreate(savedInstanceState)
16+
setContentView(R.layout.activity_main)
17+
add.setOnClickListener {
18+
Realm.getDefaultInstance().use {
19+
it.beginTransaction()
20+
val d = it.createObject(User::class.java, it.where(User::class.java).count())
21+
d.name = "User ${d.id}"
22+
it.commitTransaction()
23+
}
24+
25+
}
26+
27+
query.setOnClickListener {
28+
Realm.getDefaultInstance().use {
29+
it.where(User::class.java).findAll().map {
30+
Log.d(TAG, it.toString())
31+
}
32+
}
33+
}
34+
}
35+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package net.println.kotlin.realm
2+
3+
annotation class PoKo

0 commit comments

Comments
 (0)