NagisaでAndroidエンジニアをしている@sekitobaと申します。現在マンガZEROというアプリのAndroid版をピュアKotlinで実装しています。
現在、Nagisaで開発しているマンガZEROのアプリの実装をKotlinで行うに当たり、Kotlinの言語仕様はドキュメント等を読んで一通り押さえていたのですが、いきなり業務で利用するのは少し怖いと感じ、ウォーミングアップとしてKotlin KoansというKotlin例題集(ブラウザがあれば試せるオンライン版もあります)をやってみたところ、なかなか良かったのでKotlinを始めてみようかなという方にお勧めしています。現在、問題文に英語しか用意されていないため、Introduction章を日本語訳してみました。(おかしな点がありましたら指摘いただけると嬉しいです!)
Koansに解答のベースとなるコードがある場合は、
//回答欄
というコメント付きでコードを載せています。
Introduction
Hello, world!
関数定義を見て、文字列 「OK」 を返すstart関数を実装してください。
問題では、例外をスローする関数TODO()が使用されます。koans中のあなたの任務は、このTODO()を問題に応じて意味のあるコードに置き換えていくことになります。
// 回答欄
fun start(): String = TODO()
メモ
Kotlinにおいて関数が第一級オブジェクトであることを感じさせてくれた問題でした。
Java to Kotlin conversion(Java→Kotlin変換)
Java→Kotlinコンバーターという、Java開発者にとって便利なツールを用意しています。IntelliJ IDEAの方がうまく機能しますが、オンラインでも試してみることができます。下記のコードを変換してみてください。Javaコードをコピーし、上の 「Convert from Java」 を選択し、その結果生成された関数を回答欄にコピーしてください
public class JavaCode {
public String toJSON(Collection<Integer> collection) {
StringBuilder sb = new StringBuilder();
sb.append("[");
Iterator<Integer> iterator = collection.iterator();
while (iterator.hasNext()) {
Integer element = iterator.next();
sb.append(element);
if (iterator.hasNext()) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
}
// 回答欄
fun toJSON(collection: Collection): String = TODO()
メモ
コードコンバーターが変換したコードが最適ではない事に気がつけた時、すこしKotlinと仲良くなれた気がしました。
Named arguments(名前付き引数)
デフォルト引数および名前付き引数は、オーバーロードの数を最小化し、関数呼び出しの読みやすさを向上させるために役立ちます。ライブラリ関数joinToStringは、パラメータのデフォルト値で宣言されます。
fun joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
/* ... */
): String
上記関数は文字列のコレクションから呼び出すことができます。2つの引数を指定して、JSON形式のリストを返す(例:[a, b, c])関数joinOptions()を実装してください
// 回答欄
fun joinOptions(options: Collection) = options.joinToString(TODO())
メモ
下記のような怒りの引数Boolean祭りやnull祭りとなるメソッドを用意し、呼び出さなくてはいけないときに何をわたしているのか明確にできます。関数を宣言する時は必須な引数を最初に寄せて、名前付きで渡す引数は後ろに回すと良いと思います。(名前付きで引数を渡すとそれ以降の引数は名前付きで渡さなくてはいけないため)
- statusesService.homeTimeline(10, null, null, false, false, false, false, callBack)
- getContentResolver().query(UserColumns.CONTENT_URI, null, null, null, null)
Default arguments(デフォルト引数)
下記はJavaにおける’foo()’関数に対する幾つかのオーバーロードです。
public String foo(String name, int number, boolean toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
return foo(name, 42, toUpperCase);
}
public String foo(String name) {
return foo(name, 42);
}
これらのJavaオーバーロードはすべて、Kotlinの1つの関数に置き換えることができます。
foo関数を1つにまとめる方法で関数fooの宣言を変更してください。デフォルト引数と名前付き引数を利用してください。
// 回答欄
fun foo(name: String, number: Int, toUpperCase: Boolean) =
(if (toUpperCase) name.toUpperCase() else name) + number
fun useFoo() = listOf(
foo("a"),
foo("b", number = 1),
foo("c", toUpperCase = true),
foo(name = "d", number = 2, toUpperCase = true)
)
メモ
Javaにおいてオーバーロードは必要な機能ですが、見た目も関数の呼び出し経路も分かりづらくなりがちです。Kotlinはこれをスッキリ、そして読みやすく書けるのが嬉しいです
Lambdas(ラムダ)
Kotlinは関数型プログラミングをサポートしています。Kotlinの高次関数と関数リテラル(ラムダ)について読んでください。
ラムダ式を関数anyに渡して、コレクションに偶数が含まれているかどうかを確認してください。関数anyは、predicate(戻り値がbooleanな関数)としてラムダを取得し、predicateの戻り値がtrueとなる要素が少なくとも1つある場合はtrueを返します。
// 回答欄
fun containsEven(collection: Collection): Boolean = collection.any { TODO() }
メモ
問題の意図はわかったので解くことができたのですが訳す時にpredicateが示す意味が最初分からず手こずりました…
Strings(文字列)
Kotlinにおける異なる文字列リテラルと文字列テンプレートについて読んで下さい。
Raw文字列は、正規表現パターンを書くには便利です。バックスラッシュでバックスラッシュをエスケープする必要はありません。以下に、形式13.06.1992(2桁の数字、ドット、2桁の数字、ドット、4桁の数字)の日付と一致するパターンを示します。
fun getPattern() = """\d{2}\.\d{2}\.\d{4}"""
month
変数を使用してこのパターンを、フォーマット13 JUN 1992(2桁の数字、空白、月の省略形、空白、4桁の数字)の日付と一致するように、このパターンに書き換えてください。
// 回答欄
val month = "(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)"
fun getPattern(): String = TODO()
メモ
変数埋め込みなども含めて文字列周りはかなり使いやすくなっている印象です。
Data classes(データクラス)
次のJavaコードをKotlinに書き換えてください。
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
次に、結果のクラスに修飾子dataを追加してください。このアノテーションは、コンパイラーがequals/hashCode、toStringなど多くの有用なメソッドを生成することを意味します。そうするとgetPeople関数はコンパイルが通るはずです。
// 回答欄
class Person
fun getPeople(): List<Person> {
return listOf(Person("Alice", 29), Person("Bob", 31))
}
メモ
Kotlin1.1からデータクラスが他のクラスの継承ができるようになりました。Android開発において
DataBindingやORMapperを利用する等、インターフェースの継承・実装が必要なクラスもデータクラスに出来るようになりました
Nullable types(Nullable型)
Kotlinでのnull安全と安全呼び出しについて読んで、1つのif
のみ利用して次のJavaコードを書き換えてください。
public void sendMessageToClient(
@Nullable Client client,
@Nullable String message,
@NotNull Mailer mailer
) {
if (client == null || message == null) return;
PersonalInfo personalInfo = client.getPersonalInfo();
if (personalInfo == null) return;
String email = personalInfo.getEmail();
if (email == null) return;
mailer.sendMessage(email, message);
}
// 回答欄
fun sendMessageToClient(
client: Client?, message: String?, mailer: Mailer
){
TODO()
}
class Client (val personalInfo: PersonalInfo?)
class PersonalInfo (val email: String?)
interface Mailer {
fun sendMessage(email: String, message: String)
}
メモ
null安全な領域を広げることを意識して実装することを心がけています。
WebAPIを利用してデータを取得する際に、サーバーサイドとクライアントでNullableなのか、必須項目なのかをしっかり決めておかないとNullable祭りになってしまい、とても辛い思いをします…
Smart casts(スマートキャスト)
スマートキャストとwhen
を使用して、次のJavaコードを書き換えてください。
public int eval(Expr expr) {
if (expr instanceof Num) {
return ((Num) expr).getValue();
}
if (expr instanceof Sum) {
Sum sum = (Sum) expr;
return eval(sum.getLeft()) + eval(sum.getRight());
}
throw new IllegalArgumentException("Unknown expression");
}
// 回答欄
fun eval(expr: Expr): Int =
when (expr) {
is Num -> TODO()
is Sum -> TODO()
else -> throw IllegalArgumentException("Unknown expression")
}
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
Extension functions(拡張関数)
拡張機能について読んでください。次に、拡張機能Int.r()
およびPair.r()
を実装し、IntとPairをRationalNumberに変換してください。
// 回答欄
fun Int.r(): RationalNumber = TODO()
fun Pair<Int, Int>.r(): RationalNumber = TODO()
data class RationalNumber(val numerator: Int, val denominator: Int)
メモ
Kotlinの便利機能の一番有名所です。実際に非常に使い勝手がよく、ついつい使ってしまいがちですが用法、用量を守って正しく使うことが必要と感じています。
Object expressions(オブジェクト式)
Javaの匿名クラスと同じ役割をKotlinで果たすオブジェクト式について読んで下さい。
java.util.Collectionsクラスを使用して降順でリストをソートするための比較器を提供するオブジェクト式を追加してください。Kotlinでは、java.util.Collectionsの代わりにKotlinライブラリ拡張を使用していますが、この例では、KotlinとJavaコードを混在させることができます。
// 回答欄
import java.util.*
fun getList(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList, object {})
return arrayList
}
SAM conversions(SAM変換)
オブジェクトがSAMインターフェース(抽象メソッドを1つだけ持つ)を実装する場合、代わりにラムダを渡すことができます。SAM変換の詳細を参照してください。
前の問題の、オブジェクト式をラムダに変更してください。
// 回答欄
import java.util.*
fun getList(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList, { x, y -> TODO() })
return arrayList
}
メモ
最初KotlinのインターフェースはSAM変換が行われないことを知らず
KotlinでSAMインターフェース定義して利用する際にラムダで渡そうとしてエラーになり、ハマりました…
Extension functions on collections(Collection関数)
KotlinコードはJavaコードと簡単に混合できます。実際、Kotlinでは独自のコレクションを導入しておらず、標準のJavaを使用しています(若干改善された)。Javaコレクションの読み取り専用ビューと可変ビューについて読んでください。
Kotlin標準ライブラリーには、コレクションを使って作業をより便利にする拡張機能がたくさんあります。拡張機能のソートを使用して、前の例をもう一度書き直してください。
// 回答欄
fun getList(): List<Int> {
return arrayListOf(1, 5, 2)//TODO("return the list sorted in descending order")
}
マンガZEROを開発する上で実際に便利だった点
実際にマンガZEROを開発開発する上でKoansの内容で役に立った点を幾つか挙げたいと思います。
(本質ではない部分のコードは省略しています)
拡張関数
StringにDate型変換する拡張関数を追加
すこし乱暴な使い方なのですが、現在マンガZEROではAPIから応答される時間のフォーマットが決まっているため
Stringに以下の様な拡張関数を生やすことで頻繁に行われる文字列→日付の変換が劇的に楽になりました。
fun String.toDate(): Date? {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.JAPAN).parse(this)
}
RxJavaで冗長なコードを減らす
また、Android開発時、RxJavaで以下のようなsubscribe/ovserveするスレッドを指定する記述が頻繁に登場します。
ScreenApi.getDisplayService()
.getTopScreenData()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
Observableに以下のような拡張関数を追加することで
fun <T> Observable<T>.schedule(): Observable<T> {
return this.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
地味ですが冗長なコードを減らすことができました。
ScreenApi.getDisplayService()
.getTopScreenData()
.schedule()
.subscribe({}, {})
データクラス
Kotlin1.1からデータクラスの継承ができるようになったので基底クラスの継承が必要なORM、Databinding、Parcelableを利用したい場合でもデータクラスとすることができるようになりました。
他にもあるのですが次回以降に。
まとめ
Kotlinで本格的な実装を始める前にウォーミングアップとしてKoansを是非活用してみてください!