サンクトペテルブルクで開催されたMobius 2017カンファレンスでのAntonのプレゼンテーションに基づいた、多くのコード例とイラストを含む記事。 AntonはJunoのAndroidアプリケーション開発者であり、彼の仕事では多くの関連技術に触れています。 このレポートはAndroidに関するものではなく、Kotlinに関するものではなく、一般的なテスト、プラットフォーム上および言語上にあり、あらゆるコンテキストに適応できるアイデアに関するものです。
なぜテストが必要なのですか?
まず、コードのテストを作成する理由またはテストを作成する理由を決定する価値があります。 いくつかの理由が考えられます。
- コードを信頼するため。
- ドキュメントの準備のため。
- リファクタリング後、ぐっすり眠ります。
- コードをより速く書くため。
- 同僚に自慢する。
そして、おそらく最も重要な理由は、プロジェクトが長期にわたって生きて発展できるということです(つまり、変更)。 開発とは、新しい機能の追加、バグの修正、リファクタリングを意味します。
コードベースが大きくなると、ベースが複雑になるため、ミスをする可能性が高くなります。 そして、彼女が生産に入ると、エラーの代償が高くなります。 その結果、多くの場合、変更が非常に困難であり、対処が非常に困難です。
長命プロジェクトを作成するときに解決する2つのグローバルタスクを次に示します。
- システムの複雑さ、つまり特定のビジネス要件に応じてシステムをできるだけシンプルにする方法を管理します。
- システムテスト(今日、これについて話している)。
テストコードとは何ですか?
テストを書き込もうとすると何がうまくいかないのでしょうか? 多くの場合、システムはこれに対応できていません。 隣接するパーツと非常に関連しているため、入力パラメーターを設定してすべてが正しく機能することを確認することはできません。

このような状況を回避するには、コードを正しく記述する、つまりテスト可能にする必要があります。
テストコードとは何ですか? この質問に答えるには、まずテストとは何かを理解する必要があります。 テストが必要なシステムがあるとしましょう(SUT-System Under Test)。 テストとは、いくつかの入力データの転送と、予想される結果に対する結果の検証です。 テストコードは、入力および出力パラメーターを完全に制御できることを意味します。

テストコードを記述するための3つのルール
コードをテスト可能にするには、3つの規則に従うことが重要です。 それらのそれぞれを例で詳しく見てみましょう。
規則1.引数を渡し、値を明示的に渡します。
関数(N個の引数を取り、特定の数の値を返すバキューム内の特定の関数)のテストを見てみましょう。
f(Arg[1], ... , Arg[N]) -˃ (R[1], ... , R[L])
そして、きれいではない関数があります:
fun nextItemDescription(prefix: String): String { GLOBAL_VARIABLE++ return "$prefix: $GLOBAL_VARIABLE" }
どの入力がここにあるかを検討してください。 まず、引数として渡されるプレフィックス。 また、関数の結果にも影響するため、入力はグローバル変数の値です。 関数の結果は戻り値(文字列)であり、グローバル変数の増加です。 これが出口です。
概略的には、次の図のようになります。

入力(明示的および暗黙的)と出力(明示的および暗黙的)があります。 そのような関数から純粋な関数を作成するには、暗黙的な入力と出力を削除する必要があります。 この場合、制御された方法でテストされます。 たとえば、次のように:
fun ItemDescription(prefix: String, itemIndex: Int): String { return "$prefix: $itemIndex" }
言い換えると、関数は、そのすべての入力と出力が明示的に渡されるかどうか、つまり引数と戻り値のリストを介して渡されるかどうかを簡単にテストできます。
ルール2.依存関係を明示的に渡す
2番目のルールを理解するために、モジュールを関数として考えることをお勧めします。 モジュールは、呼び出しが時間の経過とともに延長される関数であると仮定します。つまり、入力パラメーターの一部はある時点で転送され、一部-次の行で、一部-タイムアウト後、他の一部などに転送されます。 .d。 そして、出口についても同じです:今、一部-少し後で、など。
M(In[1], ... , In[N]) -˃ (Out[1], ... , Out[L])
そのようなモジュール関数の入力と出力はどのように見えますか? 最初にコードを見てから、より一般的な図を作成してみましょう。
class Module( val title: String //input ){}
このようなクラスのコンストラクターを呼び出すという事実は、関数の入力であり、文字列を出力に渡すことは明らかに入力でもあります。 結果はメソッドが呼び出されるかどうかに依存するため、クラスのメソッドを呼び出すという事実も関数の入力になります。
class Module( val title: String // input ){ fun doSomething() { // input // … } }
明示的な依存関係から値を取得することも入力です。 使用前にモジュールAPIを介して渡された場合、依存関係を明示的に呼び出します。
class Module( val title: String // input val dependency: Explicit // dependency ){ fun doSomething() { // input val explicit = dependency.getCurrentState() //input // … } }
暗黙的な依存関係から入力を取得することも入力です。
class Module( val title: String // input val dependency: Explicit // dependency ){ fun doSomething() { // input val explicit = dependency.getCurrentState() //input val implicit = Implicit.getCurrentState() //input // … } }
出口に進みましょう。 フィールドから特定の値が返されると、抜け出します。 外部から確認できるため、この値の変更は関数の出力です。
class Module( ){ var state = "Some state" fun doSomething() { state = "New state" // output // … } }
一部の外部状態の変更も出力です。 次のように明示的にすることができます。
class Module( val dependency: Explicit // dependency ){ var state = "Some State" fun doSomething() { state = "New State" // output dependency.setCurrentState("New state") //output // … } }
または、暗黙的に、次のように:
class Module( val dependency: Explicit // dependency ){ var state = "Some state" fun doSomething() { state = "New state" // output dependency.setCurrentState("New state") //output Implicit.setCurrentState("New state") //input // … } }
まとめましょう。
In[1], … , In[N]
このような汎用モジュールの入力は次のとおりです。
- モジュールAPIおよびその依存関係のAPIとの相互作用。
- それらで伝えた意味。
- これらの相互作用を行った順序。
- これらの相互作用の間の時間。
出力とほぼ同じ:
Out[1], … , Out[N]
汎用モジュールの出力は次のとおりです。
- モジュールAPIおよびその依存関係のAPIとの相互作用。
- それらで伝えた意味。
- これらの相互作用を行った順序。
- これらの相互作用の間の時間。
- 監視可能なモジュールの特定の状態の変更は、外部から取得されます。
この方法でモジュールを定義すると、モジュールのテストプロセス、つまりこのモジュールで記述されたテストは、この関数の呼び出しと結果の検証であることがわかります。 つまり、givenブロックとwhenブロック(givenとwhenアノテーションを使用する場合)に記述するのは、関数を呼び出すプロセスであり、次に結果を検証するプロセスです。

したがって、モジュールのすべての入力と出力が、モジュールAPIまたは明示的な依存関係のAPIを介して送信される場合、モジュールのテストが容易になります。
M(In[1], ... , In[N]) -˃ (Out[1], ... , Out[L])
ルール3.テストで依存関係の互換性を制御する
明示的な引数と明示的な依存関係があっても、私たちはまだ完全に制御することはできません。そのためです。
たとえば、モジュールには明示的な依存関係があります。 モジュールは何もせず、3倍して、あるフィールドに書き込みます。
class Module(explicit: Explicit) { val tripled = 3 * explicit.getValue() }
このためのテストを作成します。
class Module(explicit: Explicit) { val tripled = 3 * explicit.getValue() } @Test fun testValueGetsTripled() { }
どういうわけか、モジュールを準備し、そこからTripledフィールドの値を取得し、結果に書き込み、15であることを期待し、15が結果と等しいことを確認します。
class Module(explicit: Explicit) { val tripled = 3 * explicit.getValue() } @Test fun testValueGetsTripled() { // prepare Explicit dependency val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
最大の質問は、上位5つを返し、結果として15を取得する必要があると言うために、明示的な依存関係をどのように準備するかです。 明示的な依存関係に大きく依存します。
明らかな依存関係がシングルトンである場合、テストでは「5つを返す!」と言うことはできません。コードはすでに記述されているため、テストでコードを変更することはできません。
// 'object' stands for Singleton in Kotlin object Explicit { fun getValue(): Int = ... }
したがって、テストは機能しません。通常の依存関係をそこに転送することはできません。
// 'object' stands for Singleton in Kotlin object Explicit { fun getValue(): Int = ... } @Test fun testValueGetsTripled() { val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
finalクラスでも同じです-それらの動作を変更することはできません。
// Classes are final by default in Kotlin Class Explicit { fun getValue(): Int = ... } @Test fun testValueGetsTripled() { val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
最後の適切なケースは、明示的な依存関係が何らかの実装を持つインターフェイスである場合です。
interface Explicit { fun getValue(): Int class Impl: Explicit { override fun getValue(): Int = ... } }
次に、テストでこのインターフェイスを既に準備し、上位5つを返すテスト実装を作成し、最後にモジュールクラスに渡してテストを実行します。
@Test fun testValueGetsTripled() { val mockedExplicit = object : Explicit { override fun getValue(): Int = 5 } val result = Module(mockedExplicit).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
関数はプライベートな場合があります。ここでは、プライベートな実装とは何かを調べ、暗黙的な依存関係がないこと、シングルトーンや暗黙的な場所からは何も得られないことを確認する必要があります。 そして、原則として、パブリックAPIを介したコードのテストに問題はないはずです。 つまり、パブリックAPIが入力と出力を完全に記述している場合(他にない場合)、パブリックAPIは事実上十分です。
テスト可能なコードを実際に書くための3つのルール
ある種のアーキテクチャのないテスト済みコードを想像するのは難しいので、例としてMVPを使用します。 ユーザーインターフェイス、ビューレイヤー、ビジネスロジックが条件付きで収集されるモデル、プラットフォームアダプターのレイヤー(APIからモデルを分離するように設計されている)、ユーザーインターフェイスに関連付けられていないプラットフォームAPIおよびサードパーティAPIがあります。

モデルとプレゼンターはプラットフォームから完全に分離されているため、ここでテストしています。
明示的な入力と出力は何ですか
クラスとさまざまな明示的な入力があります。入力文字列、ラムダ、Observable、メソッド呼び出し、および明示的な依存関係を通じて行われる同じことです。
class ModuleInputs( input: String, inputLambda: () -> String, inputObservable: Observable<String>, dependency: Explicit ) { private val someField = dependency.getInput() fun passInput(input: String) { } }
出口の状況は非常に似ています。 出力は、メソッドから何らかの値を返し、ラムダ、Observable、および明示的な依存関係を介して何らかの値を返す場合があります。
class ModuleOutputs( outputLambda: (String) -> Unit, dependency: Explicit ) { val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init{ outputLambda("Output") dependency.passOutput("Output") } }
暗黙的な入力と出力はどのように見え、それらを明示的に変換する方法
暗黙的な入力および出力は次のとおりです。
- シングルトン
- 乱数ジェネレーター
- ファイルシステムとその他のストレージ
- 時間
- 書式設定とロケール
次に、それぞれについて詳しく説明します。
シングルトン
テストでシングルトーンの動作を変更することはできません。
class Module { private val state = Implicit.getCurrentState() }
したがって、それらは明示的な依存関係として取り出す必要があります。
class Module (dependency: Explicit){ private val state = dependency.getCurrentState() }
乱数ジェネレーター
以下の例では、シングルトンを呼び出しませんが、ランダムクラスのオブジェクトを作成します。 しかし、彼はいくつかの静的メソッドを自分の中に引っ張っていますが、それはいかなる方法(たとえば、現在の時間)にも影響を与えることはできません。
class Module { private val fileName = "some-file${Random().nextInt()}" }
したがって、私たちが制御できないサービスは、制御できるインターフェースに渡すのが理にかなっています。
class Module(rng: Rng) { private val fileName = "some-file${Random(rng.nextInt()}" }
ファイルシステムとその他のストレージ
ストレージを初期化する特定のモジュールがあります。 彼がすることは、何らかの方法でファイルを作成することだけです。
class Module { fun initStorage(path: String) { File(path).createNewFile() } }
ただし、このAPIは非常に潜行的です。成功した場合はtrueを返し、同じファイルがある場合はfalseを返します。 また、たとえば、ファイルを作成するだけでなく、理解する必要もあります。ファイルが作成されたかどうか、作成されていない場合は、その理由は何ですか。 したがって、型付きエラーを作成し、それを出力に返します。 または、エラーがない場合は、nullを返します。
class Module { fun initStorage(path: String): FileCreationError? { return if (File(path).createNewFile()) { null } else { FileCreationError.Exists } } }
さらに、このAPIは2つの例外をスローします。 それらをキャッチし、再度、型付きエラーを返します。
class Module { fun initStorage(path: String): FileCreationError? = try { if (File(path).createNewFile()) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
OK、処理しました。 ここでテストします。 問題は、ファイルを作成すると副作用があることです(つまり、ファイルシステムにファイルを作成します)。 したがって、何らかの方法でファイルシステムを準備するか、インターフェイスの背後に副作用があるすべてのものを作成する必要があります。
class Module (private val fileCreator: FileCreator){ fun initStorage(path: String): FileCreationError? = try { if (fileCreator.createNewFile(path)) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
時間
これが入り口であることはすぐには明らかではありませんが、これがそうであることはすでに上でわかっています。
class Module { private val nowTime = System.current.TimeMillis() private val nowDate = Date() // and all other time/date APIs }
たとえば、モジュールには30分間待機するロジックがあります。 このためのテストを作成する予定がある場合、すべてのユニットテストは通常30分間合格する必要があるため、テストを30分間待機する必要はありません。 時間を制御できるようにしたいので、時間の経過とともにすべての作業をインターフェイスに移動してシステム内の1つのポイントにすることは理にかなっています。 。 次に、たとえば、時計が移動した場合のモジュールの動作をテストできます。
class Module (time: TimeProvider) { private val nowTime = time.nowMillis() private val nowDate = time.nowDate() // and all other time/date APIs }
書式設定とロケール
これは最も陰湿な暗黙の入り口です。 通常のプレゼンターが何らかの種類のタイムスタンプを取得し、厳密に指定されたパターン(AMまたはPMなし、コンマなし、すべてが設定されているようです)に従ってフォーマットし、フィールドに格納するとします。
class MyTimePresenter(timestamp: Long) { val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm").format(timestamp) }
このためのテストを書いています。 書式設定されたフォームでこれが何であるかを考えたくありません。その上でモジュールを実行し、表示されるものを確認し、ここに書き留める方が簡単です。
class MyTimePresenter(timestamp: Long) { val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm").format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "" val actual = MyTimePresenter(timestamp).formattedTimeStamp assertThat(actual).isEqualTo(expected) }
メビウスは4月21日の10時から始まるのを見ました:
class MyTimePresenter(timestamp: Long) { val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimeStamp assertThat(actual).isEqualTo(expected) }
OK、これをローカルマシンで実行すると、すべてが機能します。
>> `actual on dev machine` = "2017-04-21 10:00" // UTC +3
CIで開始し、何らかの理由でMobiusが7から開始します。
>> `actual on CI` = "2017-04-21 07:00" // UTC
CIには異なるタイムゾーンがあります。 SimpleDateFormatはデフォルトのタイムゾーンを使用するため、UTC + 0のタイムゾーンであり、それに応じて、そこでの時刻のフォーマットが異なります。 テストでは、GMTがゼロのCIサーバーではそれぞれこれを再定義しませんでしたが、別の方法があります。 そして、これは、以下を含む、場所に関連付けられているすべての入力を暗黙のうちに示します。
- 通貨
- 数値形式
- タイムゾーン
- ロケール
テストで依存関係を濡らす方法
彼らは、「銀の弾丸」はないと言っていますが、私には、それはmo笑に関するものがあるようです。 インターフェースはどこでも機能するからです。 インターフェースの背後に実装を隠した場合、インターフェースが置き換えられるため、テストで確実に置き換えることができます。
interface MyService { fun doSomething() class Impl(): MyService { override fun doSomething() { /* ... */ } } } class TestService: MyService { override fun doSomething() { /* ... */ } } val mockService = mock<MYService>()
インターフェイスは、シングルトーンで何かをするのに役立つことさえあります。 プロジェクトにシングルトンがあり、それがGodObjectであるとしましょう。 一度に複数の個別のモジュールに解析することはできませんが、ある種のDI、ある種のテスト容易性をゆっくりと導入したいと考えています。 これを行うには、このシングルトンのパブリックAPIまたはパブリックAPIの一部を繰り返すインターフェイスを作成し、シングルトンにこのインターフェイスを実装させます。 また、モジュールでシングルトン自体を使用する代わりに、このインターフェイスを明示的な依存関係として渡すことができます。 もちろん、外部では同じGetInstanceの転送になりますが、内部では既にクリーンなインターフェイスで作業しています。 これは、すべてがモジュールとDIに渡されるまでの中間ステップです。
interface StateProvider { fun getCurrentState(): String } object Implicit: StateProvider { override fun getCurrentState(): String = "State" } class SomeModule(stateProvider: StateProvider) { init { val state = StateProvider.getCurrentState() } }
もちろん、他の選択肢もあります。 上で言ったように、最終クラス、静的メソッド、シングルトーンをウェットアウトすることはできません。 もちろん、ミュートすることもできます。最終クラスにはMockito2、最終クラス、静的メソッド、シングルトーンにはPowerM®ckがありますが、それらには多くの問題があります。
- ほとんどの場合、設計上の問題を通知します(これは主にPowerMockに適用されます)
- たとえば、1501テストなどのある時点で動作を停止する場合があるため、テストに適したアーキテクチャをすぐに考えて、そのようなフレームワークを使用しない方がよいでしょう。
プラットフォームからの抽象化とそれが必要な理由
抽象化は、Viewレイヤーとプラットフォームアダプターのレイヤーで行われます。

レイヤーの抽象化を表示
ビューレイヤーは、UIフレームワークをPresenterモジュールから分離します。 主に2つのアプローチがあります。
- ActivityがViewインターフェイス自体を実装する場合。
- Viewが別のクラスである場合。
最初のオプションを見てみましょう:アクティビティはビューを実装します。 次に、ビューインターフェイスを入力として受け取り、そのメソッドを呼び出す、ある種の簡単なプレゼンターがあります。
class SplashPresenter(view: SplashView) { init { view.showLoading() } }
簡単なViewインターフェースがあります:
interface SplashView { fun showLoading() }
そして、アクティビティがあり、onCreateメソッドの形式で入力があり、SplashViewインターフェースの実装があります。これは、何らかのプログレスを表示するために必要なことをプラットフォームの方法で既に直接実装しています。
interface SplashView { fun showLoading() } class SplashActivity: Activity, SplashView { override fun onCreate() { } override fun showLoading() { findViewById(R.id.progress).show() } }
したがって、Presenterは次のように行います。OnCreateで作成し、これをViewとして渡します。 多くの場合、完全に有効なオプションです。
interface SplashView { fun showLoading() } class SplashActivity: Activity, SplashView { override fun onCreate() { SplashPresenter(view = this) } override fun showLoading() { findViewById(R.id.progress).show() } }
2番目のオプションがあります-別のクラスとして表示します。 ここでは、Presenterはまったく同じで、インターフェイスはまったく同じですが、実装はActivityに関連しない別個のクラスです。
class SplashPresenter(view: SplashView) { init { view.showLoading() } interface SplashView { fun showLoading() class Impl : SplashView { override fun showLoading() { } } } }
したがって、プラットフォームコンポーネントで動作できるように、プラットフォームビューが入力で送信されます。 そして、彼はすでに彼が必要とするすべてをしています。
interface SplashView { fun showLoading() class Impl(private val viewRoot: View) : SplashView { override fun showLoading() { viewRoot.findViewById(R.id.progress).show() } } }
この場合、アクティビティはわずかにオフロードされます。 つまり、インターフェイスを組織化する代わりに、このプラットフォームビューを取得してSplashPresenterを作成し、別のクラスをビューとして作成します。
class SplashActivity: Activity, SplashView { override fun onCreate() { // Platform View class val rootView: View = ... SplashPresenter( view = SplashView.Impl(rootView) ) } }
実際、テストの観点から見ると、これら2つのアプローチは同じです。なぜなら、私たちはまだインターフェースから作業しているからです。 モックビューを作成し、それを渡すプレゼンターを作成し、特定のメソッドが呼び出されることを確認します。
@Test fun testLoadingIsShown() { val mockedView = mock<SplashView>() SplashPresenter(mockedView) verify (mockedView).showLoading() }
唯一の違いは、アクティビティロールとビューロールの表示方法です。 Viewロールが他のActivityロールと混ざらないほど大きいと思われる場合は、別のクラスに配置することをお勧めします。
プラットフォームラッパーレイヤーの抽象化
次に、プラットフォームアダプターレイヤー上のプラットフォームからの抽象化について説明します。 プラットフォームラッパーは、モデルレイヤーの分離です。 問題は、プラットフォーム側のこの層の背後にプラットフォームAPIとサードパーティAPIがあり、それらが異なる形式で提供されるため、一般的にそれらを変更できないことです。 これらは、静的メソッド、シングルトーン、最終クラス、および非最終クラスとして提供されます。 最初の3つのケースでは、実装に影響を与えることはできません;テストでの動作を置き換えることはできません。 そして、それらが最終クラスでない場合にのみ、テストでの動作に何らかの形で影響を与えることができます。
したがって、このようなAPIを直接使用する代わりに、ラッパーを作成するのが理にかなっている場合があります。 これは、APIが直接使用される場所です。
class Module { init { ThirdParty.doSomething() } }
そうする代わりに、サードパーティのAPIのメソッドを転送するだけの最も簡単なケースのラッパーを作成します。
interface Wrapper { fun doSomething() class Impl: Wrapper { override fun doSomething() { ThirdParty.doSomething() } } }
実装のラッパーを取得し、インターフェイスの背後に隠しました。したがって、モジュール内で既にWrapperを呼び出しています。これは明示的な依存関係として提供されます。
class Module(wrapper: Wrapper) { init { wrapper.doSomething() } }
保証されたテスト容易性に加えて、これは以下を提供します。
- プラットフォームAPIの設計にバインドする代わりに、便利な設計を使用する機能
- (Single Responsibility God Object);
- API ( ).
Design Smell, , . , – . , Smell. , , Smell, . .
Android ID- . -, . , , R- .
class SplashPresenter(view: SplashView, resources: Resources) { init { view.setTitle(resources.getString(R.string.welcome)) view.showLoading() } }
– , Android, ID-. , , , :
class SplashPresenter(view: SplashView, resources: Resources) { init { view.setTitle(resources.getString(R.string.welcome)) view.showLoading() } } interface Resources { fun getString(id: Int): String }
, , ID, , . .
public final class R { public static final class string { public static final int welcome=0x7f050000; } }
, , - . iOS Android, .
, . - , .
class SomeModule(input: String) { val state = calculateInitialState(input) // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" }
, , , .
class SomeModule(input: String) { val state = calculateInitialState(input) // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" } class AnotherModule(input: String) { val state = calculateInitialState(input) // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" }
, , - .
class SomeModule(input: String) { val state = calculateInitialState(input) // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" } object StateCalculator { fun calculateInitialState(input: String): String = "Some complex computation for $input" }
? calculateInitialState, . , , , .
class SomeModule(input: String, stateCalculator: StateCalculator){ val state = stateCalculator.calculateInitialState(input) } interface StateCalculator { fun calculateInitialState(input: String): String class Impl: StateCalculator { override fun calculateInitialState(input: String): String = "Some complex computation for $input" } }
, , calculateInitialState - . , extension- ( Kotlin), static-, , .
,
, , ( , ). , , , , , .

, .

:
- , (), ( DI);
- ;
- .
, - (Activity, , broadcast-…), - ( Activity View), , , Presenter. , Presenter, DI.

, .

, ( View), . , ( ).

結論の代わりに
: « , , , – ».
. , , . , . , - . , . , .
– , Mobius 2017 Moscow :
- 空の遺産:プロジェクトの根本的な改善のための戦略 (ウラジミール・イワノフ、EPAM Systems)
- ( , )
- クラッシュAndroid NDKレポート (Ivan Ponomarev、Akvelon)