ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

Androidアプリで活用しているKotlin・RxKotlinの紹介

Androidアプリで活用しているKotlin・RxKotlinの紹介

Yahoo!ニュースでAndroidアプリを開発している池田 惇です。2017年はKotlinがAndroidアプリの公式開発言語になり、開発者にとってうれしい年になったと思います。今回は、Kotlin, RxJava, RxKotlinについて簡単に紹介します。

Kotlinの特徴

2017年5月のGoogle I/OでAndroidの開発言語として公式にサポートされ、利用が急速に広がっています。Androidだけではなくサーバーサイドでも活用できます。

実用性を重視した言語である

Kotlinは新しい概念を提案したり革新的な機能を提供したりしません。その代わりに、他の言語の良いところを多く採用しています。また、Javaの代替言語として扱いやすいように設計されています。公式サイトに「Javaと100%相互運用できる」と書かれている通り、Javaで開発されたアプリケーションの一部にKotlinを使うことも容易です。

また、もともとKotlinはJetBrains社で開発されていたことから、JetBrains社製IDEのIntelliJやAndroid Studioでは手厚いサポートを受けることができます。例えば、Javaのコードをコピーして上記IDEでKotlinのコード内にペーストすると、貼り付けたコードはJavaからKotlinに自動で変換されます。

関数型プログラミング要素

Kotlinでは関数型プログラミングとオブジェクト指向のどちらでも開発できます。開発者は場面によって最適な手法を選択できるのです。例えばAndroid開発では、再利用性やテストしやすさが重要となるライブラリでは関数型プログラミングで開発し、副作用が多く発生するアプリケーションのView周辺ではオブジェクト指向で開発するといったことも可能です。

第一級関数

Kotlinで利用できる代表的な関数型プログラミングの要素として第一級関数を挙げてみます。第一級関数とは、関数を別の関数の引数として渡したり、戻り値として返したりできることです。Javaでは関数を引数として渡すことができないため、代わりにクラスを渡す必要がありました。例えば、Androidでよくある処理としてViewクリック時のコールバックが挙げられます。

// コールバック用のインターフェースを定義
public interface OnClickListener {
    void onClick(View v);
}

// 使う側
// 無名クラスとしてインスタンス生成して渡す
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 必要な処理を書く
        }
});

このように、コールバック時の振る舞いを実装するためにインターフェースを定義して冗長な記述になっていました。(※ Java8が利用できる環境ではラムダ式を使って冗長な記述を省略できます。本記事のJavaコードはJava7です。)一方、Kotlinでは関数を引数として渡すことができるのでコールバックのためにインターフェースは必要ありません。

// コンストラクタ変数でコールバック用関数を受け取るボタン
// 型「() -> Unit」は引数なし戻り値なしの関数型
class SomeButton(val clickCallback: () -> Unit) {
    fun clickCallback() {
        clickCallback.invoke()
    }
}

// 使う側
// クラスやインターフェースは不要、無名関数を渡す
val button = SomeButton(fun() {
    // 必要な処理を書く
})

SomeButtonクラスは関数型の引数を受け取っており、clickCallback.invoke()によって受け取った関数を実行します。関数を渡せることから振る舞いの変更をシンプルな記述で行うことができます。

安全性

Kotlinには、バグを防いでプログラムの安全性を高めるための機能がいくつか備わっています。基礎的な2点を挙げます。

nullable

nullの代入可否により別の型として区別されます。例えばString型であれば、Stringはnull非許容でString?はnullを代入できます。

val nonNullString: String = null   // nullを代入できずエラーになる
val nullableString: String? = null // nullを代入できる

明確に区別しておくことで想定外のnull代入をなくし、NullPointerExceptionを防止できます。

イミュータブル

複数のタイミングや場所で値が書き換えることはプログラムの複雑性を増してしまいます。一度生成した値はできる限り再代入せず、シンプルなコードを目指しましょう。

再代入不可の変数はval、再代入可能な変数はvarで宣言します。valで宣言した変数に再代入しようとするとコンパイルエラーになります。

val immutable = "value1"    
immutable = "value2"  // コンパイルエラー

var mutable = "value1"
mutable = "value2"

リストについてもList型とMutableList型として区別されます。

val immutableList: List<Int> = listOf(1, 2, 3)
immutableList.add(4) // コンパイルエラー

val mutableList: MutableList<Int> = mutableListOf(1, 2, 3)
mutableList.add(4)
println(mutableList) // [1, 2, 3, 4]

List型へMutableList型の配列を代入する際には注意が必要です。下記の例では、結果としてval b: List<Int>の内容が書き換わってしまいます。

val a: MutableList<Int> = mutableListOf(1, 2, 3)
val b: List<Int> = a // MutableList型をList型変数に代入

println(b) // [1, 2, 3]
a.add(4) // MutableListへadd
println(b) // [1, 2, 3, 4]

このようなコードは気づきにくいバグを生んでしまいます。対策として、配列を別の変数へ代入する際にはtoList()(toMutableList())関数を使ってコピーするのが良いでしょう。

val a: MutableList<Int> = mutableListOf(1, 2, 3)
val b: List<Int> = a.toList() // コピー

println(b) // [1, 2, 3]
a.add(4)
println(b) // [1, 2, 3]

RxJavaとRxKotlinについて

RxJava

RxJavaはリアクティブプログラミングをJavaで実現するためのライブラリです。Android開発ではRxJavaを採用しているアプリも多く、既にスタンダードな知識になりつつあります。詳しい解説はここでは難しいですが、ReactiveXのサイトに見やすくまとまっています。

モバイルアプリ環境ではユーザーアクションが絶えず発生し、非同期通信を多用します。それによってコードが複雑化しがちですが、リアクティブプログラミングを導入して宣言的に記述することでシンプルなコードを目指すことができます。

RxKotlin

RxKotlinはRxJavaをKotlinで使いやすくするための軽量なライブラリです。実装にはKotlinの拡張関数が活用されています。単純な例としてListからObservableを生成するコードを見てみます。

val list = listOf(1, 2, 3)

// RxJavaのみ使用
Observable.fromIterable(list)
        .subscribe { println(it) }

// RxKotlinを使用
list.toObservable()
    .subscribe { println(it) }

RxJavaのみ使った場合はObservable.fromIterable()の引数にlistを渡してObservableを生成します。一方、RxKotlinを使った場合はlist.toObservable()と直接つなげて記述できます。「listをObservableに変換」のようにコードの記述順が言葉と一致し、やりたいことをよりシンプルに表現できていると言えます。

このような記述が可能なのは、RxKotlinがIterable型の拡張関数としてtoObservable()を提供しているためです。

// https://github.com/ReactiveX/RxKotlin/ より引用
fun <T : Any> Iterable<T>.toObservable(): Observable<T> = Observable.fromIterable(this)

また、煩わしい型記述などを解決してくれる便利な拡張も用意されています。下記の例はInt型とString型の2つのObservablezipオペレーターを使ってPairに変換している例です。

val intObservable = listOf(1, 2, 3).toObservable()
val stringObservable = listOf("a", "b", "c").toObservable()

// RxJavaのみ使用
Observable.zip(intObservable, stringObservable,
        BiFunction<Int, String, Pair<Int, String>> { t1, t2 -> Pair(t1, t2) })
        .subscribe { println(it) }

// RxKotlinを使用
Observables.zip(intObservable, stringObservable, { t1, t2 -> Pair(t1, t2) })
        .subscribe { println(it) }

// いずれも出力結果は同じ
// (1, a)
// (2, b)
// (3, c)

違いはzipの第三引数で「2つの値からPair型を生成する」という振る舞いを記述している部分です。RxJavaのみ使用した場合はBiFunctionインターフェースを実装した無名クラスを渡しており、さらに型推論が効かないため3つの型を明示しています。このような記述があちこちに登場すると非常に読みづらいコードになってしまいます。

一方、RxKotlinを使用した場合はObservableの拡張であるObservablesを利用しています。第三引数にはクラスを渡す必要はなく、Kotlinらしく関数を渡すことができます。型を記述する必要もなく読みやすいコードになりました。

このように、RxKotlinを使うとコードの可読性を高めることができます。Kotlin環境でRxJavaを利用する場合はRxKotlinの併用をおすすめします。

Kotlin・RxJava・RxKotlinを使ってコールバックをシンプルに記述する例

Viewのクリック、WebAPIからのデータ取得など、Androidアプリでは処理の結果をコールバックで受け取る箇所が多くなります。コールバックは煩雑な記述になりがちです。Javaで例を作り、Kotlin・RxJava・RxKotlinを使う場合と比較します。

Javaの例

まずはJavaで作った例です。ClassAClassBはどちらもexecで処理を行い、実行結果をListenerインターフェースを使って返却します。

public class ClassA {
    interface Listener {
        void completed(int result);
    }

    private Listener listener;

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    void exec() {
        /* 実際にはここでWebAPIからのデータ取得など即時に値を返せない処理を実行 */
        listener.completed(123);
    }
}

public class ClassB {
    interface Listener {
        void completed(String result);
    }

    private Listener listener;

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    void exec(int input) {
         /* 実際にはここでWebAPIからのデータ取得など即時に値を返せない処理を実行 */
        listener.completed("結果は" + input + "です。");
    }
}

Listenerの定義やセットなどボイラープレート的なコードが多くなっており冗長です。

Activityで使う場合のコードはこのようになります。ClassAの処理結果を使ってClassBの処理を実行してみました。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ClassA a = new ClassA();
        ClassB b = new ClassB();

        a.setListener(new ClassA.Listener() {
            @Override
            public void completed(int result) {
                // ClassAの処理結果をClassBの処理に利用 -- ②
                b.exec(result);
            }
        });

        b.setListener(new ClassB.Listener() {
            @Override
            public void completed(String result) {
                // 最終結果を表示 -- ③
                System.out.println(result);
            }
        });

        // ClassAの処理を開始 -- ①
        a.exec();
    }
}

Listener周りの記述が煩雑になっており、それに加えて処理の流れが分断されていて可読性が低くなっています。

Kotlin・RxJava・RxKotlinを利用した場合

上記のJavaの例を同様の処理を Kotlin・RxJava・RxKotlinを使って書き直してみます。利用される側のClassA, ClassBです。

class ClassA {
    fun exec(): Observable<Int> = Observable.just(123)
}

class ClassB {
    fun exec(input: Int): Observable<String> = Observable.just("結果は" + input + "です。")
}

Listenerを定義せず、即時にObservableを返却できるようになりました。記述量が少なくシンプルなコードになりました。

次は使う側のコードです。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val a = ClassA()
        val b = ClassB()

        // ClassAの処理 -- ①
        a.exec()
                .flatMap { b.exec(it) } // ClassAの処理結果をClassBの処理に利用 -- ②
                .subscribeBy(
                        onNext = { println(it) }, // 最終結果を表示 -- ③
                        onError = { println("エラーが発生しました") })
    }
}

先ほどの例と比較して、処理を1つの流れにできました。「ClassAの結果を使ってClassBの処理を行い結果を表示する」のように、言葉で考える時と同じ順序で宣言的に記述することでシンプルで読みやすいコードになりました。

終わりに

Androidアプリ開発で活用しているKotlin、RxJava、RxKotlinを紹介しました。モバイルアプリはWebAPIなどの非同期通信が多く、ユーザーアクションも絶えず発生するため複雑なコードになりがちです。そのような環境で柔軟性を保った仕組みを構築するため、これからも新しい技術を積極的に活用していきたいと思います。

参考書籍

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ