これはKotlin Advent Calendar 2016の12/11の記事です。
8月23日にこんなつぶやきをして、今日まで溜めたAndroid開発をしていて、こう書くと簡単でキモチイイ!!!というKotlinの文法を紹介したいと思います。
(あくまで自分が気持ちいいってだけだからね!)
自己紹介
Kotlin 1.0.4, Kotlin 1.0.5で名前が載ったExternal Contributorsの一人です(嬉しいから自慢)。主にKotlin Pluginの静的解析にコントリビュートしています。Kotlinで書かれているPermissionsDispatcherの開発にも参加しています。
Android開発経験3年ほどで、今はAndroidをJavaでもKotlinでも開発しています。
少しくらいならブログでAndroid/Kotlinネタ書いてもいいレベルかな?と思います。
前提条件と想定読者
- Android開発はある程度知っている
- KotlinでのAndroid興味がある人
- KotlinでのAndroid開発良いと言われてるけど、何がいいかわからねな人
Kotlinの文法はJavaコードと比較すればだいたいわかる感じで記載していきますが、もしわからなければ、@shiraj_iにメンションいただければ答えますので、お気軽に質問して下さい。
キモチイイ!文法たち
Kotlinは書きやすいとよく耳にしますが、実際どういうところでどういう文法にすると「書きやすい」になるのかJavaとの比較があまりありません。そこで独断と偏見で気持ちいい文法だこれ!と思った文法や書き方を紹介したいと思います。
Kotlinで一番有名であろう機能、Null安全やセミコロンレスに関しては多くのドキュメントやブログがありますので割愛します。
一行メソッド
ある特定のテキストを返すだけのメソッドを作る時、Javaで書くとこんな感じになります。
1
2
3
| public String getName() {
return "MyApp";
}
|
Kotlinでも同じように書けます。
1
2
3
| fun getName(): String {
return "MyApp"
}
|
ただ、Kotlinは一行でreturn出来る場合、=
をつけて{}
を省略することが出来ます。
1
| fun getName(): String = "MyApp"
|
さらに、戻り値の型が明らかな場合、型の指定しなくても良いので
1
| fun getName() = "MyApp"
|
短くてだいぶ気持ちいいですね。
こんな感じで、以下もJavaの例文を出して、Kotlinで気持ち良くなっていきます。それではどんどんいきます。
null時何する?
例えば、パラメータがnullだった場合、即メソッドを抜けるという処理を書くとします。Javaの場合、結構色々書かなきゃいけません。
1
2
3
4
5
6
| public void foo(@Nullable String text) {
if (text == null) {
return;
}
// ...
}
|
Kotlinはnull時にこれをしてくれという?:
文法が用意されています。それを使うと一行で書けちゃいます。
1
2
3
4
| fun foo(text: String?) {
text ?: return
// ...
}
|
null時に別値を代入ということも可能です。
1
2
3
4
| fun foo(text: String?) {
val bar = text ?: "" // textをbarに代入する。textがnullだった場合、空文字とする。
// ...
}
|
空クラス
Javaでは空クラスだろうと、{}
を書かなくてはなりません。特に目印用のinterfaceとかであると思いますが
Kotlinではボディが空のクラスの場合、{}
を書かなくて良いので
もちろん、classでも可能です。
空メソッド
空メソッド。Javaの場合、{}
を書かなくてはなりません。
1
2
| public void foo() {
}
|
Kotlinの場合、一行メソッドと同じように書けます。
あれ?ながk…気持ちいいですね!
getter/setter省略
Kotlinでは、getter/setterがあった場合、propertyとしてアクセス可能になります。AOSPに書いてあるgetter/setterも同様です。
Activity#getLayoutInflater()
を使うようなメソッドを定義する場合
1
2
3
| public LayoutInflater getLayoutInflater() {
return activity.getLayoutInflater();
}
|
Kotlinで書くと以下のように書けます。
1
| fun layoutInflater(): LayoutInflater = activity.layoutInflater
|
実際にはActivity内にlayoutInflater
というプロパティは存在していませんが、Kotlinが解釈してくれます。
もちろんですが、以下のようにも書けます。
1
| fun layoutInflater(): LayoutInflater = activity.getLayoutInflater()
|
ただ、Android Studioさんが「これプロパティアクセスに変えな?」というサジェストが出ます。
Javaっぽいコードを書くとこのようにワーニングを出してくれるので、都度修正していくとKotlinらしい文法の勉強も捗ります。(platform typeにはご注意下さい)
パラメータのデフォルト値
ここのパラメータだいたい同じ値なのだけど、時々違うから、overloadメソッドを用意するか!ってことありませんか?
1
2
3
4
5
6
7
| public static boolean maybeStartActivity(Context context, Intent intent) {
return maybeStartActivity(context, intent, false);
}
private static boolean maybeStartActivity(Context context, Intent intent, boolean chooser) {
// ...
}
|
かの有名なu2020にもありました。
Kotlinはパラメータのデフォルト値を定義出来ます。
1
2
3
| fun maybeStartActivity(context: Context, intent: Intent, chooser: Boolean = false): Boolean {
// ...
}
|
カスタムViewのコンストラクタ
パラメータのデフォルト値に関連して、カスタムViewのコンストラクタの定義って大変だと思います。
1
2
3
4
5
6
7
8
9
10
11
12
| public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.custom_view, this, true);
}
|
Kotlinはデフォルト値を定義したメソッドを上記のようにJavaから見たら複数あるようにする@JvmOverloads
というアノテーションがあります。
これを使うと、カスタムViewのコンストラクタは一行定義するだけで書けます。
1
2
| class @JvmOverloads CustomView(context: Context, attrs: AttributeSet = null, defStyleAttr: Int = 0) {
}
|
(Kotlinのコンストラクタ自体は気持ちよくないので説明省略します。)
キャストで括弧少ない
Javaでキャストする場合、括弧が多くなりがちです。PagerAdapterのdestroyItem
を実装してみます。
1
2
3
| @Override public void destroyItem(ViewGroup container, int position, Object obj) {
((ViewPager) container).removeView((View) obj)
}
|
Kotlinだとas
を使ってキャストします。文法的にも括弧が減り、どこで括弧が終わっているのかがわかりやすくなります。
1
2
3
| override fun destroyItem(container: ViewGroup?, position: Int, obj: Any?) {
(container as ViewPager).removeView(obj as View)
}
|
キャスト失敗時に何する?
null時に何する?との組み合わせることでキャスト失敗時に何するかの定義も簡単にできます。
1
2
3
4
| override fun destroyItem(container: ViewGroup?, position: Int, obj: Any?) {
container as? ViewPager ?: return
// ...
}
|
また、readonlyの変数のキャストに成功すると自動的にその変数をキャストしてくれます。メソッドパラメータはreadonly。各所に出てくるval
と定義されている変数もreadonlyです。(余談ですが、immutableではありませんので注意して下さい。)
1
2
3
4
5
| override fun destroyItem(container: ViewGroup?, position: Int, obj: Any?) {
container as? ViewPager ?: return
obj as? View ?: return
container.removeView(obj)
}
|
mutable変数var
の場合、as?
後に変更可能なので、自動的にキャストしてもらえないので注意。Kotlinでは理由がない限り、var
を使わないほうが良いです。
Util系
XxxUtilsとか作って、全メソッドをstaticにして、privateコンストラクタを作って・・・みたいなやり方をJavaではちょくちょくしていました。
1
2
3
4
5
6
7
| public class LogUtil {
private LogUtil() {}
public void initLog(String tag) {
Timber.plant(ExtTree(tag))
}
}
|
kotlinではTopレベルにメソッドを書けば、このようなUtil系のメソッドを書くことが出来ます。
1
| fun initLog(tag: String) = Timber.plant(ExtTree(tag))
|
使い方もJavaのときと変わりません。
1
2
3
4
5
| import package.initLog
fun foo() {
initLog("MyApp")
}
|
Stringテンプレート
JavaでStringの結合をする場合、こんな感じになります。
1
2
3
| return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=" + 60 * 3)
.build();
|
KotlinにはStringテンプレートとしてString内に${}
で変数を書くことが出来ます。
1
2
3
| return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=${60 * 3}")
.build()
|
変数一つだけである場合、{}
の省略も出来ます。
1
2
| val foo = 1
val bar = "Text$foo" // <= "Text1"という文字列に
|
複数行のString
JavaのStringで複数行を生成する場合、結構辛いです。
1
| String text = "aaa\nbbb\nccc";
|
Kotlinでは"""
を使うことで複数行のStringの定義を出来ます。
1
2
3
| val text = """aaa
bbb
ccc""".trimMargin();
|
.trimMargin()のこととか、マージンの開始位置とか複数行のStringは結構多機能ですが、詳細知りたければ公式のstring-literalsを確認してね。
複数if -> when
Javaではifが複数ある場合、ちょっとつらいです。
例えば以下のようなコードがあったとします。(Javaでbehaviorを独自実装したときに使っていたコード)
1
2
3
4
5
6
| if (isAnimating) return;
if (consumed > 0) {
hide(child);
} else {
show(child);
}
|
これをKotlinでもそのままif/else書けます。
1
2
3
4
5
6
| if (isAnimating) return
if (consumed > 0) {
hide(child)
} else {
show(child)
}
|
しかし、もう少し簡単にwhenでまとめることも可能です。
1
2
3
4
5
| when {
isAnimating -> return
consumed > 0 -> animateHide(child)
else -> animateShow(child)
}
|
whenはJavaでいうSwitch文に近いですが、上記のようにwhenの後に条件を付けなかったり、変数の型のcase文に出来たりとめちゃくちゃ気持ちよくなれます。
式
Kotlinではifやwhenなど諸々が式です。
例えば、ifの結果を変数に代入する場合、
1
2
3
4
5
6
| int foo;
if(flag) {
foo = 10;
} else {
foo = 100;
}
|
kotlinではこんな感じになります。
1
2
3
4
5
| val foo = if (flag) {
10
} else {
100
}
|
単純な場合、{}
は省略するので一行で書くことが多いです。
1
| val foo = if (flag) 10 else 100
|
条件により違った値を代入しているのですがval
で定義出来るのがポイントです。
ちなみに、PermissionsDispatcherさんでも利用していて、メソッドの引数として使ったりもしています。
1
| builder.beginControlFlow("if (\$N\$T.shouldShowRequestPermissionRationale(\$N, \$N))", if (isPositiveCondition) "" else "!", PERMISSION_UTILS, targetParam, permissionField)
|
ネストさせることも出来るのですが、複雑になるので見にくい場合はローカル変数に切り出すのが良いと思います。
Annotation
Dagger2を使う場合、ActivityやFragmentスコープを作るために独自Annotation作ったりします。
1
2
3
4
| @Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {
}
|
@interface
というキーワードを使っていましたが、Kotlinではannotation
と表現します。直感的で良いです。
1
2
3
| @Scope
@Retention
annotation class ActivityScope
|
Retention
のデフォルト値がRetentionPolicy.RUNTIME
なので、指定を省略出来るのもスッキリしていて気持ち良いです。
Singleton
Javaでは(簡易的な)シングルトンを作成する場合、以下のように書く必要がありました。
1
2
3
4
5
6
7
8
9
10
| public class MoshiUtil {
private static Moshi moshi;
public static Moshi getMoshi() {
if (moshi == null) {
moshi = Moshi.Builder().add(DateAdapter()).build();
}
return moshi;
}
}
|
Kotlinでは、object
として定義すればアプリ内でシングルトンとして利用可能です。
1
2
3
4
5
| object MoshiUtil {
val moshi: Moshi by lazy {
Moshi.Builder().add(DateAdapter()).build()
}
}
|
使い方もJavaの時と同じです。
1
| MoshiUtil.moshi.adapter(BlackjackHand::java.class)
|
rx.ObservableのThread指定方法
ちょこっとしたことなのですが、Rxの実行スレッド方法の指定も気持ちよく書けるようになります。
1
2
3
4
| load()
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
|
Kotlinの拡張プロパティを利用し、スレッドの指定方法を定義します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| val <T> Observable<T>.observeOnUI: Observable<T>
get() = observeOn(AndroidSchedulers.mainThread())
val <T> Observable<T>.observeOnIO: Observable<T>
get() = observeOn(Schedulers.io())
val <T> Observable<T>.observeOnComputation: Observable<T>
get() = observeOn(Schedulers.computation())
val <T> Observable<T>.subscribeOnUI: Observable<T>
get() = subscribeOn(AndroidSchedulers.mainThread())
val <T> Observable<T>.subscribeOnIO: Observable<T>
get() = subscribeOn(Schedulers.io())
val <T> Observable<T>.subscribeOnComputation: Observable<T>
get() = subscribeOn(Schedulers.computation()).unsubscribeOn(Schedulers.computation())
|
この拡張プロパティを利用すると以下のように書けます。
1
2
3
4
| load()
.subscribeOnIO
.observeOnIO
.subscribe()
|
DatabindingのBindingAdapter指定方法
DatabindingのBindingAdapterの公式ドキュメントのコードをKotlinで書いてみます。
1
2
3
4
| @BindingAdapter("android:bufferType")
public static void setBufferType(TextView view, TextView.BufferType bufferType) {
view.setText(view.getText(), bufferType);
}
|
これがこんな感じになります。view
が消えました。
1
2
3
4
| @BindingAdapter("android:bufferType")
fun TextView.setBufferType(TextView.BufferType bufferType) {
setText(getText(), bufferType)
}
|
DatabindingのonClickなどのイベント指定方法
これも拡張メソッドでいきます。
1
2
3
4
5
6
7
8
9
10
| <android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing"
android:onClick="@{viewModel::onClickTopFab}"
app:backgroundTint="@color/action_green"
app:fabSize="normal"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end"
app:srcCompat="@drawable/plus"/>
|
1
2
3
| fun onClickFab(View view) {
Toast.makeText(view.getContext(), R.string.message_fab, Toast.LENGTH_LONG).show();
}
|
kotlinではこんな感じ。Toast表示用の拡張メソッドも定義しています。
1
2
3
| fun View.onClickFab() {
Toast.makeText(context, R.string.message_fab, Toast.LENGTH_LONG).show()
}
|
ちょっと手が滑ると
1
2
3
4
5
6
7
| fun Context.showLongToast(@StringRes id: Int) {
Toast.makeText(this, id, Toast.LENGTH_LONG).show()
}
fun View.onClickFab() {
context.showLongToast(R.string.message_fab)
}
|
topレベルの拡張メソッドは用法・用量を守って正しくお使い下さい。
createIntent/newInstance
自分はcreateIntent/newInstanceパターン大好きです。
これも気持ちよくなれます。
1
2
3
4
| public static Intent createIntent(Context context) {
Intent intent = new Intent(context, MyActivity.class);
return intent;
}
|
applyを使って、一行に。
1
2
3
| companion object {
fun createIntent(context: Context) = Intent(context, MyActivity::class.java).apply { }
}
|
newInstanceもやってみます。
1
2
3
4
5
6
7
| public static SimpleDialogFragment newInstance(MyPacel entity) {
SimpleDialogFragment fragment = new SimpleDialogFragment();
Bundle bundle = new Bundle();
bundle.putParcelable(PARCELABLE_KEY, entity);
fragment.setArguments(bundle);
return fragment;
}
|
パラメータセットしたインスタンスを作りたいだけなのですが、結構冗長的で辛い。。。
Kotlinで短くしてみます。
1
2
3
4
5
6
| fun newInstance(entity: MyPacel) =
SimpleDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(PARCELABLE_KEY, entity)
}
}
|
nestが激しいのでバラしていきます。まずはこの部分。
1
2
3
| arguments = Bundle().apply {
putParcelable(PARCELABLE_KEY, entity)
}
|
これをメソッド化すると
1
2
3
4
5
| private fun entity(bundle: Bundle, entity: MyPacel): Bundle {
return bundle.apply {
putParcelable(PARCELABLE_KEY, entity)
}
}
|
bundle変数に関する処理なので、Bundleの拡張メソッドと考えると良さ気。
1
2
3
4
5
| private fun Bundle.entity(entity: MyPacel): Bundle {
return this.apply {
putParcelable(PARCELABLE_KEY, entity)
}
}
|
return文一文なので、省略。thisも必要ないので、省略。
1
| private fun Bundel.entity(entity: MyPacel) = apply { putParcelable(PARCELABLE_KEY, entity) }
|
イマココ
1
2
3
4
5
6
| fun newInstance(entity: MyPacel) =
SimpleDialogFragment().apply {
arguments = Bundle().entity(entity)
}
private fun Bundel.entity(entity: MyPacel) = apply { putParcelable(PARCELABLE_KEY, entity) }
|
SimpleDialogFragmentの部分も同じように拡張メソッドを使って書くと。
1
2
3
4
5
| fun newInstance(entity: MyPacel) = MyFragment().entity(entity)
private fun MyFragment.entity(entity: MyPacel) = apply { arguments = Bundle().entity(entity) }
private fun Bundle.entity(entity: MyPacel) = apply { putParcelable(KEY, entity) }
|
だいぶシンプルになりました。ヨイヨヨイヨ。
setVisibleOrGone
Databinding前には結構使うことが多かったのですが、今となっては・・・でも一応。
Viewを消してしまうか表示するかをBoolean値で判別するようなUtil系のメソッドを書くとすると
1
2
3
4
5
6
7
8
9
10
11
| public static void setVisibleOrGone(View view, boolean isVisible) {
if(view == null) {
return;
}
if(isVisible) {
view.setVisiblity(View.VISIBLE);
} else {
view.setVisiblity(View.GONE);
}
}
|
これをkotlinで書くと一行でいけます。
1
2
3
| fun View?.setVisibleOrGone(isVisible: Boolean) {
this?.visibility = if (isVisible) View.VISIBLE else View.GONE
}
|
nullableのViewの拡張メソッド、null安全、setter/getterの省略、if式利用と色々Javaにはない機能を利用しています。
BaseObservable
公式にある、以下のコードをKotlinで気持ちよく書いてみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
|
Kotlinで書く前に、そもそも具体的にやることは以下
- getterに
@Bindable
をつける
- setterの最後に
notifyPropertyChanged(BR.firstName);
を呼ぶ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class User : BaseObservable {
@get:Bindable
var firstName: String
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
@get:Bindable
var lastName: String
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
|
setter/getterを書かなくて良いのでシンプルになってます。
もうちょいキレイになればいいですが、わざわざメソッド書かなくて良いというだけでも気持ちいいです。
Delegate
継承するな!移譲しろ!と言われてもJava使ってると継承しちゃいガチなのですが、KotlinのDelegateがあるとこれを積極的に利用したいなと思えるようになります。
ViewModelにRxのSubscriptionの機能を実装し、Activity#onDestroy
時にunsubscribe()
したいという時
1
2
3
4
5
6
| public class MyViewModel(CompositeSubscription compositeSubscription) {
// ...
public void unsubscribe() {
compositeSubscription.unsubscribe();
}
}
|
Kotlinでdelegateするとunsubscribe()
は書かなくて良くなります。
1
2
3
| class MyViewModel @Inject constructor(var compositeSubscription: CompositeSubscription) : Subscription by compositeSubscription {
// ...
}
|
Activityではこんな感じ。MyViewModel
で実装していないのにunsubscribe()
を呼ぶことが出来る。
1
2
3
4
5
6
7
8
9
10
11
| class ThirdActivity : AppCompatActivity() {
@field:[Inject]
lateinit var viewModel: MyViewModel
// ...
override fun onDestroy() {
super.onDestroy()
viewModel.unsubscribe()
}
|
droidkaigi2016のArrayRecyclerAdapterを以下のようなにすることもできるけど、余計なメソッドも生えるので、用法と用量を(省略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| abstract class MutableListRecyclerAdapter<T, VH : RecyclerView.ViewHolder>(private val list: MutableList<T>) :
RecyclerView.Adapter<VH>(), Iterable<T>, MutableList<T> by list {
var itemClickListener: View.OnClickListener? = null
override fun getItemCount() = list.size
@UiThread fun addAllWithNotification(items: Collection<T>) {
val position = itemCount
addAll(items)
notifyItemChanged(position)
}
@UiThread fun reset(items: Collection<T>) {
clear()
addAll(items)
notifyDataSetChanged()
}
}
|
遅延初期化
Javaで簡単にやる場合、getterを作成し、propertyアクセスは禁止し、そのgetter経由で値を取得するというルールの下かろうじて出来る遅延初期化処理です。
例えば、DatabindingのsetContentView
を利用して、binding
変数を初期化します。
1
2
3
4
5
6
7
8
| private MyBinding binding;
private MyBinding getBinding() {
if(binding == null) {
binding = DataBindingUtil.setContentView<MyBinding>(this, R.layout.my);
}
return binding;
}
|
Kotlinではby lazy
を利用して書きます。一旦binding
変数にアクセスしたらby lazy
内の処理が実行され、初期化されます。
1
2
3
| private val binding by lazy {
DataBindingUtil.setContentView<MyBinding>(this, R.layout.my)
}
|
valであるのも良いです。
ちなみに、enumにordinal
ではない数値のidを振ることがあって、そのidから逆引きしたい時にキャッシュしたSparseArray
を利用して逆引きしますが、この時もby lazy
使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
| companion object {
private val lookup: SparseArray<MyType> by lazy {
SparseArray<MyType>().apply {
EnumSet.allOf(MyType::class.java).forEach {
this@apply.put(it.typeId, it)
}
}
}
fun fromTypeId(typeId: Int): MyType {
return lookup.get(typeId)
}
}
|
parameter name
Javaではそもそも出来ないのですが、こんなPOJOクラスがあったとします。
1
2
3
4
5
6
7
8
9
10
11
| public class User {
private String firstName;
private String lastName;
public User(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// その他メソッド省略
}
|
1
2
| new User("名", "姓");
new User("姓", "名"); // 間違っているけど、コンパイルOK
|
第一引数と第二引数がfirstなのか、lastなのかわからなくなります。ランタイムでしか検知できないのが辛いです。
1
| User(firstName = "名", lastName = "姓")
|
引数に名前を設定して、メソッドを呼び出せます。こんなことしても叱られませんし、間違った挙動を起こしません。同じような型が多いメソッド呼び出しでは積極的に使うと気持ち良いです。
1
| User(lastName = "姓", firstName = "名")
|
最後に
リストアップしてみたら結構出てきました。。。
落選した項目はgistで公開しています。