AndroidプロジェクトでKotlinに切り替える:ヒントとコツ



投稿者:Sergey Yeshin、強力なミドルAndroid開発者、DataArt



GoogleがAndroidでKotlinの公式サポートを発表してから1年半以上が経過し、最も経験豊かな開発者が3年以上前に戦闘で実験を開始しました。



新しい言語はAndroidコミュニティで好評を博しており、新しいAndroidプロジェクトの大部分はKotlinを搭載して開始されます。 また、KotlinがJVMバイトコードにコンパイルされるため、Javaと完全に互換性があることも重要です。 そのため、Javaで記述された既存のAndroidプロジェクトでは、Kotlinのすべての機能を使用する機会(さらに必要性)もあります。おかげで、彼は多くのファンを獲得しました。



記事では、JavaからKotlinへのAndroidアプリケーションの移行の経験、プロセスで克服しなければならなかった困難、およびこれらすべてが無駄ではなかった理由について説明します。 この記事の主な目的は、Kotlinを習い始めたばかりのAndroid開発者であり、個人的な経験に加えて、他のコミュニティメンバーの資料にも依存しています。



コトリンを選ぶ理由



Kotlinの機能について簡単に説明します。そのため、プロジェクトで切り替えたため、「居心地がよくて痛いほど馴染みのある」Javaの世界を残しています。



  1. 完全なJava互換性
  2. ヌルの安全性
  3. 型推論
  4. 拡張メソッド
  5. ファーストクラスおよびラムダオブジェクトとして機能
  6. ジェネリック
  7. コルーチン
  8. チェック例外なし


DISCOアプリ



これは、10枚の画面で構成される割引カードを交換するための少量のアプリケーションです。 彼の例を使用して、移行を検討します。



アーキテクチャについて簡単に



アプリケーションは、ViewModel、LiveData、RoomなどのMVVMアーキテクチャとGoogle Architecture Componentsを内部で使用しています。









また、Uncle BobのClean Architectureの原則に従って、アプリケーション内の3つのレイヤー(データ、ドメイン、プレゼンテーション)を選択しました。









どこから始めますか? したがって、Kotlinの主な機能を想像し、移行する必要のあるプロジェクトについて最小限のアイデアを持っています。 自然な質問は、「どこから始めればよいのか」です。



公式のKotlin Androidドキュメントページでは、既存のアプリケーションをKotlinに移植する場合は、単体テストの作成を開始するだけでよいと書かれています。 この言語で少し経験を積んだら、Kotlinで新しいコードを書きます。既存のJavaコードを変換するだけです。



しかし、1つの「しかし」があります。 実際、単純な変換により(常にではありませんが)通常、Kotlinで動作するコードを取得できますが、そのイディオムには多くの要望が残されています。 さらに、Kotlin言語の前述の(だけでなく)機能によるこのギャップを解消する方法を説明します。



レイヤー移行



アプリケーションはすでに階層化されているため、上から順に階層ごとに移行することは理にかなっています。



移行中のレイヤーのシーケンスを次の図に示します。









上層から正確に移行を開始したのは偶然ではありません。 これにより、JavaコードでKotlinコードを使用する必要がなくなります。 それどころか、上位層のKotlinコードが下位層のJavaクラスを使用するようにします。 実際のところ、KotlinはもともとJavaと対話する必要性を考慮して設計されたものです。 既存のJavaコードは、自然な方法でKotlinから呼び出すことができます。 既存のJavaクラスから簡単に継承し、それらにアクセスして、KotlinクラスとメソッドにJavaアノテーションを適用できます。 Kotlinコードは、Javaでもあまり問題なく使用できますが、JVMアノテーションを追加するなど、余分な労力が必要になることがよくあります。 しかし、最終的にKotlinで書き直されるのに、Javaコードで余分な変換を行うのはなぜですか?



例として、オーバーロードの生成を見てみましょう。



通常、デフォルトのパラメーター値を使用してKotlin関数を記述する場合、すべてのパラメーターを含む完全な署名としてのみJavaで表示されます。 Java呼び出しに複数のオーバーロードを提供する場合は、@ JvmOverloadsアノテーションを使用できます。



class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } }
      
      





デフォルト値を持つ各パラメーターに対して、これは追加のオーバーロードを1つ作成します。これには、リモートパラメーターリストに、このパラメーターとその右側のすべてのパラメーターがあります。 この例では、次のものが作成されます。



 // Constructors: Foo(int x, double y) Foo(int x) // Methods void f(String a, int b, String c) { } void f(String a, int b) { } void f(String a) { }
      
      





Kotlinの正しい操作のためにJVMアノテーションを使用する多くの例があります。 このドキュメントページでは、JavaからKotlinへの呼び出しについて詳しく説明しています。



次に、レイヤーごとの移行プロセスについて説明します。



プレゼンテーション層



これは、ビューとViewModelを備えた画面を含むユーザーインターフェイスレイヤーであり、ViewModelは、モデルのデータを含むLiveDataの形式のプロパティを含みます。 次に、このアプリケーション層を移行するときに役立つことがわかっているトリックとツールを見ていきます。



1. Kapt注釈プロセッサ



他のMVVMと同様に、Viewはデータバインディングを通じてViewModelプロパティにバインドされます。 Androidの場合、注釈処理を使用するAndroid Databind Libraryを扱っています。 したがって、Kotlinには独自の注釈プロセッサがあり、対応するbuild.gradleファイルに変更を加えないと、プロジェクトの構築が停止します。 したがって、これらの変更を行います。



 apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar']) ///… kapt "com.android.databinding:compiler:$android_plugin_version" }
      
      





build.gradle内のannotationProcessor構成のすべての出現をkaptで完全に置き換える必要があることを覚えておくことが重要です。



たとえば、プロジェクトでDaggerまたはRoomライブラリを使用し、コード生成のためにボンネットの下の注釈プロセッサも使用する場合、注釈プロセッサとしてkaptを指定する必要があります。



2.インライン関数



関数をインラインとしてマークする場合、コンパイラーに使用場所に配置するように依頼します。 関数の本体は埋め込まれます。つまり、関数の通常の使用の代わりになります。 これにより、型の消去、つまり型の消去の制限を回避できます。 インライン関数を使用すると、実行時に型(クラス)を取得できます。



Kotlinのこの機能は、起動されたアクティビティのクラスを「抽出」するために私のコードで使用されました。



 inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } }
      
      





reifiedは、具体化されたタイプの指定です。



上記の例では、Kotlin言語の拡張機能などの機能についても触れました。



3.拡張機能



それらは拡張機能です。 ユーティリティメソッドは拡張機能で使用され、肥大化した巨大なクラスユーティリティを回避するのに役立ちました。



アプリケーションに含まれる拡張機能の例を示します。



 fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } }
      
      





Kotlin開発者は、Kotlin Android Extensionsプラグインを提供することで、便利なAndroid拡張機能を事前に考えました。 彼が提供する機能には、ビューのバインドとパーセル可能なサポートがあります。 このプラグインの機能の詳細については、 こちらをご覧ください



4.ラムダ関数と高階関数



Androidコードでラムダ関数を使用すると、ぎこちないClickListenerとコールバックを取り除くことができます。これらは、Javaで自己記述インターフェースを介して実装されました。



onClickListenerの代わりにラムダを使用する例:



 button.setOnClickListener({ doSomething() })
      
      





ラムダは、コレクション関数などの高次関数でも使用されます。



としてマップを取り上げます



 fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...}
      
      





私のコードには、その後の削除のためにIDカードを「マッピング」する必要がある場所があります。



mapに渡されたラムダ式を使用して、目的のid配列を取得します。



  val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids)
      
      





lambdaが唯一の引数であり、itキーワードが唯一のパラメーターの暗黙的な名前である場合、関数を呼び出すときに括弧をまったく省略できることに注意してください。



5.プラットフォームの種類



Java(実際にはAndroid SDKを含む)で記述されたSDKを使用する必要があります。 つまり、プラットフォームタイプなどのKotlinおよびJava Interopを常に使用する必要があります。



プラットフォームタイプは、Kotlinがnullの有効性情報を見つけることができないタイプです。 事実、デフォルトでは、Javaコードにはnullの有効性に関する情報が含まれておらず、 NotNullおよび@ Nullableアノテーションが常に使用されるわけではありません。 Javaに対応する注釈がない場合、タイプはプラットフォームになります。 nullを許可する型としても、nullを許可しない型としても使用できます。









これは、Javaの場合と同様に、開発者がこのタイプの操作を完全に担当することを意味します。 コンパイラはnullチェックランタイムを追加せず、すべてを実行できます。



次の例では、アクティビティのonActivityResultをオーバーライドします。



 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") }
      
      





この場合、データはプラットフォームタイプであり、nullを含む場合があります。 ただし、Kotlinコードの観点からは、どのような状況でもデータをnullにすることはできません。また、Intentのタイプをnull可能として指定するかどうかに関係なく、コンパイラーから警告またはエラーを受け取りません。 。 ただし、SDKの場合はこれを制御できないため、空でないデータの受信は保証されないため、この場合にnullを取得するとNPEが発生します。



また、例として、プラットフォームタイプの出現の可能性がある次の場所をリストできます。



  1. Service.onStartCommand()、Intentがnullの場合があります。
  2. BroadcastReceiver.onReceive()。
  3. Activity.onCreate()、Fragment.onViewCreate()およびその他の同様のメソッド。


さらに、メソッドのパラメーターに注釈が付けられることもありますが、何らかの理由で、オーバーライドを生成するとスタジオがNullabilityを失います。



ドメイン層



この層にはすべてのビジネスロジックが含まれており、データ層とプレゼンテーション層の間のやり取りを担当します。 ここでの主要な役割はリポジトリによって果たされます。 リポジトリでは、サーバー側とローカルの両方で必要なデータ操作を実行します。 2階のプレゼンテーション層では、データ操作の複雑さを隠すリポジトリインターフェイスメソッドのみを提供します。



前述のとおり、実装にはRxJavaが使用されました。



1. RxJava



KotlinはRxJavaと完全に互換性があり、Javaよりも簡潔です。 しかし、ここでも不愉快な問題に直面しなければなりませんでした。 このように聞こえます: andThenメソッドのパラメーターとしてラムダを渡すと、このラムダは機能しません!



これを確認するには、簡単なテストを書くだけです:



 Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe()
      
      





そして、コンテンツ失敗します。 これは、ほとんどの演算子( flatMapdeferfromActionなど)の場合で、実際にはラムダが引数として期待されています。 そして、 andThenのあるそのような記録で、 Completable / Observable / SingleSourceが予想されます。 この問題は、{}の代わりに通常の括弧()を使用することで解決されます。



この問題については、記事「Kotlin and Rx2」で詳しく説明されています 間違った括弧のために5時間を無駄にした方法



2.リストラ



また、 割り当ての破壊や破壊などの興味深いKotlin構文にも触れます。 オブジェクトを複数の変数に一度に割り当てて、オブジェクトを部分に分割できます。



APIに複数のエンティティを一度に返すメソッドがあると想像してください。



 @GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?)
      
      





次の例に示すように、このメソッドから結果を返すコンパクトな方法は、構造化です。



 syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } }
      
      





マルチ宣言は規則に基づいていることに注意してください:破棄されることになっているクラスにはcomponentN()関数が含まれている必要があります(Nは対応するコンポーネント番号-クラスのメンバー)。 つまり、上記の例は次のコードに変換されます。



 val cards = it.component1() val brands = it.component2()
      
      





この例では、componentN()関数を自動的に宣言するデータクラスを使用します。 したがって、複数の宣言がそのまま使用できます。



データ層については、次のパートでデータクラスについて詳しく説明します。



データ層



このレイヤーには、サーバーおよびベースからのデータのPOJO、ローカルデータとサーバーから受信したデータの両方を操作するためのインターフェイスが含まれます。



ローカルデータを操作するために、Roomが使用されました。これは、SQLiteデータベースを操作するための便利なラッパーを提供します。



それ自体を示唆する移行の最初の目標はPOJOです。これは、標準のJavaコードでは、多くのフィールドとそれに対応するget / setメソッドを持つバルククラスです。 Dataクラスを使用して、POJOをより簡潔にすることができます。 いくつかのフィールドを持つエンティティを記述するには、1行のコードで十分です。



 data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String)
      
      





簡潔さに加えて、以下を取得します。





もう1つの良いニュース:kaptを構成すると(上記のとおり)、データクラスはRoomアノテーションで正常に機能します。これにより、すべてのデータベースエンティティをデータクラスに変換できます。 ルームは、null許容プロパティもサポートしています。 確かに、RoomはKotlinのデフォルト値をまだサポートしていませんが、これに対応するバグはすでに発生しています。



結論



JavaからKotlinへの移行中に発生する可能性があるいくつかの落とし穴のみを調査しました。 特に理論的知識や実務経験の不足により問題が発生するが、それらはすべて解決可能であることが重要です。



しかし、コトリンで簡潔で表現力豊かで安全なコードを書くことの喜びは、移行パスで生じるすべての困難を完済する以上のものです。 DISCOプロジェクトの例がこれを確実に裏付けていると自信を持って言えます。



書籍、役立つリンク、リソース



  1. 言語の知識の理論的基礎は、スヴェトラーナ・イサコワとドミトリー・ジェメロフの言語の作者からの本Kotlin in Actionを置くことを可能にします。



    簡潔さ、情報量、幅広いトピック、Java開発者へのフォーカス、ロシア語版の入手可能性により、言語学習の開始時に可能な限り最高のマニュアルが作成されます。 私は彼女から始めました。
  2. developer.androidを使用したKotlin ソース
  3. ロシア語のKotlinガイド
  4. ジェネシスのAndroid開発者であるKonstantin Mikhailovskyによる、Kotlinへの切り替えの経験に関する素晴らしい記事



All Articles