Jetpack Composeを使ってみたい②

Jetpack Composeを使ってみたい!と思ってCodelabを元に勉強してみた記録。
Jetpack Compose  |  Android デベロッパー  |  Android Developers

JetpackComposeの思想より、再コンポーズについてまとめる。
Compose の思想  |  Jetpack Compose  |  Android Developers

再コンポーズの呼び出しとスキップ

カウント数を表示するTextと、クリックでカウントを増やすButtonを配置したCountScreenと、
カウント数を保持し、CountScreenを呼び出すCounterを以下のように記述した。

@Composable
fun Counter(){
    var count by remember { mutableStateOf(0) }

    CountScreen(
        text = count.toString(),
        onClicked = { count ++ }
    )
}

@Composable
fun CountScreen(count:String, onClicked:() -> Unit){
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = count)
        Button(onClick =  onClicked ) {
            Text(text = "count up")
        }
    }
}

ボタンをクリックするとonClickedが実行されてCounter関数内のcountの値が更新される。
countに変更があったため、CountScreenが再度呼び出される。
CountScreen内ではcountの値を使用しているTextコンポーザブルは再コンポーズされ、
Buttonコンポーザブルはパラメータの更新がないため再コンポーズはスキップされる。

副作用と再コンポーズ

副作用とはアプリの他の部分に反映される変更のことで、
例えば

  • 共有オブジェクトのプロパティへの書き込み
  • ViewModel で監視可能なデータの更新
  • 共有設定の更新

再コンポーズはスキップされたり、アニメーションのレンダリングで何度も実行されたり、
並列実行によりバックグラウンドで実行されることがある。
このとき、コンポーズ可能な関数内に直接状態の変更(副作用)が記述されていると、
想定しない状態の変化が起こることがあるため、コンポーズ可能な関数は副作用を持ってはいけない。

副作用のある例

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0
    //リストの要素数を数えたい

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ 
                //関数内に値の変更があり、再コンポーズによって意図しないタイミングで変更が起こる可能性がある
            }
        }
        Text("Count: $items")
        //この場合myList.sizeで要素数を取得すれば、itemsを用意して副作用を持たせる必要はない
    }
}

副作用を持たせる場合はコールバックにする。

//最初のサンプルコードより
@Composable
fun CountScreen(count:String, onClicked:() -> Unit){
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = count)
        Button(onClick =  onClicked ) {
            //値の変更はonClickコールバックが持っているため、再コンポーズに依存しない
            Text(text = "count up")
        }
    }
}

任意の順序で実行される

3 つの画面を 1 つのタブレイアウトに描画する次のサンプルコードでは、
コンポーザブル可能な関数StartScreen、MiddleScreen、EndScreenは上から順に実行されるとは限らない。
例えば、StartScreenで値の変更を行い、変更した値をMiddleScreenでも使うというコードを書くと、
StartScreenが先に行われる保証はないので想定した表示にならない可能性がある。
3つのコンポーザブル可能な関数は自己完結している必要がある。

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

Jetpack Composeを使ってみたい①

Jetpack Composeを使ってみたい!と思ってCodelabを元に勉強してみた記録。
Jetpack Compose  |  Android デベロッパー  |  Android Developers

まずはチュートリアルより。
Android Compose のチュートリアル  |  Android デベロッパー  |  Android Developers

Jetpack Composeとは

Android 向けの宣言型UIフレームワーク
kotlinでコンポーズ関数としてUIを定義するとComposeコンパイラが画面を構築する。
コードがシンプルで直感的になる。

コンポーズ可能な関数

@Composableアノテーションをつけてコンポーズ可能な関数(コンポーザブル)を定義する。
このアノテーションによってコンパイラにUIに変換する関数であることを伝える。
コンポーズ関数はパラメータを受け入れることができる。
この例では、Textコンポーザブルを呼び出してテキストを表示する。

@Composable
fun MessageCard(msg: Message) {
    Text(text = msg)

@Previewアノテーションをつけるとアプリをビルドせずにプレビューが表示できる。

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}

定義したコンポーザブルはonCreate メソッド内のsetContentブロックから呼び出される。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

レイアウト

複数の要素を追加する場合、単に並べて記述するだけでは重なって表示されてしまう。

@Composable
fun MessageCard(msg: Message) {
    //NG
    Text(text = msg.author)
    Text(text = msg.body)
}

Column 関数を使用すると垂直方向に、Row関数を使用すると水平方向に要素を揃えることができ、
Box関数を使用すると要素を積み重ねることができる。
以下の例ではRowで画像とテキストが横並びに、テキストはColumnを使用して縦に2行表示される。
また、レイアウト(サイズや形、余白など)を整えるために修飾子Modifierを使用する。

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
           //連結して複数指定できる
        )

        Spacer(modifier = Modifier.width(8.dp))

        Column {
            Text(text = msg.author)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = msg.body)
        }
    }
}

マテリアルデザイン

AndroidStudioで [Empty Compose Activity] を選択してプロジェクトを作成すると、
ui.theme サブパッケージ内にファイルがデフォルトで作成されており、
「プロジェクト名Theme」という名前でマテリアルテーマが定義されている。
マテリアルテーマでコンポーザブルをラップすることで、
定義されたテーマを継承することができ、アプリ内で一貫性が保てる。

@Preview
@Composable
fun PreviewMessageCard() {
    ComposeTutorialTheme {
        MessageCard(
            msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!")
        )
    }
}

リスト

リストを表示するにはLazyColumnもしくはLazyRowを使用する。
このコンポーザブルは画面上に表示される要素のみを生成して表示するため、長いリストで効果を発揮する。
子要素としてitemsを持ち、パラメータとして表示したいリストを受け取り、
ラムダでリスト内のアイテムごとにUIを記述する。

@Composable
fun Conversation(messages: List<Message>) {
    LazyColumn {
        items(messages) { message -> 
            MessageCard(message)
            //リストmessagesのアイテムごとにMessageCardコンポーザブルを呼び出す
        }
    }
}

状態変化

状態の変更によってUIが自動的に更新(再コンポジション)されるためには、
mutableStateOf関数に変更する可能性のある状態(値)を渡し、
remember関数を使用してその状態を保存して、変更をトラッキングする。

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        ...
        var isExpanded by remember { mutableStateOf(false) }

        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            ...
        }
    }
}

この例ではColumnがクリックされてisExpandedの値が変更されると、
状態の変更を認識して再コンポジションされる。

〈kotlin〉選択ツール TimePickerDialog, DatePickerDialog

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

時計から時刻を選択するTimePickerDialogと
カレンダーから日付を選択するDatePickerDialogを使ってみた。

レイアウト

各ダイアログを呼び出すためのButtonと、
選択した結果を表示するためのTextViewを配置。
ButtonのonClickに呼び出すメソッドを指定(メソッドはこの後作成)。

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">


        <TextView
            android:id="@+id/time_picker_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="time" />

        <Button
            android:id="@+id/time_picker_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="time select" 
            android:onClick="showTimePickerDialog"/>


        <TextView
            android:id="@+id/date_picker_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="date" />

        <Button
            android:id="@+id/date_picker_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="date select"
            android:onClick="showDatePickerDialog"/>

</LinearLayout>

TimePickerDialog

TimePickerDialog のインスタンスを返すonCreateDialog()メソッドと、
時刻を設定したときにコールバックを受け取るTimePickerDialog.OnTimeSetListenerインターフェースを実装する。

class TimePickerFragment: DialogFragment(), TimePickerDialog.OnTimeSetListener {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        //最初に表示する時刻として現在の時刻を取得
        val c = Calendar.getInstance()
        val hour = c.get(Calendar.HOUR_OF_DAY)
        val minute = c.get(Calendar.MINUTE)

        return TimePickerDialog(requireContext(), this, hour, minute, DateFormat.is24HourFormat(activity))
        //引数はcontext: Context!, listener: TimePickerDialog.OnTimeSetListener!, hourOfDay: Int, minute: Int, is24HourView: Boolean
        //is24HourViewは24時間表記をするか(true)12時間表記をするか(false)
    }

    override fun onTimeSet(p0: TimePicker?, hour: Int, minute: Int) {
         //時間選択後の処理を記述
        (activity as MainActivity).findViewById<TextView>(R.id.time_picker_text).text =
            getString(R.string.time_text, hour, minute)
    }
}

DatePickerDialog

DatePickerDialog のインスタンスを返すonCreateDialog()メソッドと、
日付を設定したときにコールバックを受け取るDatePickerDialog.OnDateSetListener インターフェースを実装する。

class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val c = Calendar.getInstance()
        val year = c.get(Calendar.YEAR)
        val month = c.get(Calendar.MONTH)
        val day = c.get(Calendar.DAY_OF_MONTH)

        return DatePickerDialog(requireContext(), this, year, month, day)
        //引数はcontext: Context, listener: DatePickerDialog.OnDateSetListener?, year: Int, month: Int, dayOfMonth: Int
    }

    override fun onDateSet(p0: DatePicker?, year: Int, month: Int, day: Int) {
        //日付選択後の処理を記述
        (activity as MainActivity).findViewById<TextView>(R.id.date_picker_text).text =
            getString(R.string.date_text, year, month, day)
    }
}

onClickで呼び出すメソッドの定義

上記で作成したDialogFragmentのインスタンスで show() を呼び出す。
show()の引数としてFragmentManager のインスタンスとフラグメントの一意のタグ名が必要。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    fun showTimePickerDialog(v: View) {
        TimePickerFragment().show(supportFragmentManager, "timePicker")
    }

    fun showDatePickerDialog(v:View){
        DatePickerFragment().show(supportFragmentManager, "datePicker")
    }
}

参考
Pickers  |  Android Developers

〈kotlin〉Preferences DataStore

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

だいぶ前に小さいデータを保存しておくSharedPreferencesの記事を書いたが、
Jetpack DataStoreに移行することを検討してください」となっていて、
以下のcodelabを元にPreferences DataStoreについて勉強したのでまとめてみる。
サンプルコードも以下のcodelabから。
Preferences DataStore  |  Android Developers

Jetpack DataStore

シンプルな小規模なデータを非同期で安全に保存できる。
以下の2種類がある。

Preferences DataStore スキーマを定義せず、キーを使用してデータを保存・アクセスする。
データがkey-valueで保存できるほどシンプルな場合簡単に保存できる。
Proto DataStore プロトコル バッファを使用してスキーマを定義してデータを保存する。
設定が必要だがタイプセーフで高速。

今回はPreferences DataStoreについてのみを書く。

Preferences DataStore

設定

dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0")
}

DataStoreを作成

Kotlinファイルの最上位でpreferencesDataStoreデリゲートを使用してDataStoreインスタンスを作成し、
アプリの他の部分ではこのプロパティを介してアクセスする。
これによりDataStoreをシングルトンで保持する。
preferencesDataStoreのnameにはDataStoreの名前を設定する。

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
val Context.dataStore :DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCES_NAME
)


この先のコードはSettingsDataStoreクラスとして記述していく。

保存するキーの定義

スキーマを定義しないので、対応するキー型の関数を使用して保存するキーと値の型を定義する。

class SettingsDataStore(context: Context) {
    private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")
    //is_linear_layout_managerにboolean型の値を保存
}
書き込み

DataStore内のデータをトランザクションとして更新するsuspend関数 edit()を使用して、
上記で定義したキーに対して値を保存する。
edit関数にはtransform パラメータがあり、ここに値を更新するコードブロックを渡す。

class SettingsDataStore(context: Context) {
    private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

    suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager:Boolean, context: Context){
        context.dataStore.edit {preferences ->
            preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
        }
    }
}
読み込み

DataStoreは設定が変更されるたびに出力される Flow<Preferences>に保存されたデータを公開する。
ここからキーを使用して目的の値を取り出す。
DataStore がファイルに対してデータを読み書きする際に、
データへのアクセス時に IOExceptions が発生することがあるためキャッチしておくと良い。

class SettingsDataStore(context: Context) {
    private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

    val preferenceFlow : Flow<Boolean> = context.dataStore.data
        .catch {
            if (it is IOException){
                it.printStackTrace()
                emit(emptyPreferences())
            }else{
                throw it
            }
        }
        .map { preferences ->
            preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
            //初回は空のため、デフォルト値を渡しておく
        }
}
DataStoreを使う
class LetterListFragment : Fragment() {

    private lateinit var settingsDataStore: SettingsDataStore
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        settingsDataStore = SettingsDataStore(requireContext())
        settingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner) { value ->
           //Flowで保存されているのでLiveDataに変換して監視できる
           //変更時にしたい処理あれば書く
        }
    }

     override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_switch_layout -> {
                ...
                //コルーチンで呼び出す
                lifecycleScope.launch{
                    settingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
                }
                return true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

参考
アプリ アーキテクチャ: データレイヤー - DataStore - デベロッパー向け Android  |  Android デベロッパー  |  Android Developers

〈kotlin〉依存関係インジェクション(DI) Hilt 修飾子

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。
DIを自動で行うためのライブラリHiltの実装記事の続き。
〈kotlin〉依存関係インジェクション(DI) Hilt編 - ゆるプログラミング日記


同じ型で複数の実装を提供したい場合、修飾子を使用する。
修飾子は実装の提供方法を識別するためのアノテーション

実装例

以下のサンプルコードを元にする。
Dependency injection with Hilt  |  Android Developers

インターセプタを追加したOkHttpClient オブジェクトの作成を考える。
サービスによって種類の違うインターセプタを渡したOkHttpClient オブジェクトを使用したい場合、
OkHttpClient オブジェクトの作成方法を複数用意する必要があり、
複数の作成方法を修飾子をつけて定義することで識別していく。

修飾子の作成
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
モジュールの定義

インスタンスの作成方法を定義する@Binds メソッドまたは @Provides メソッドに、
上記で作成した修飾子のアノテーションをつける。
どちらもOkHttpClientを返すが違うインターセプタを渡している。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}
インスタンスを提供

その実装でインスタンスを作成するのか、
インスタンスの前にもモジュール内で設定したのと同じアノテーションをつけて知らせる。
(authInterceptorを追加したOkHttpClientを提供する場合は@AuthInterceptorOkHttpClientをつける。)

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }

〈kotlin〉依存関係インジェクション(DI) Hilt編

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。
DIという単語が出てきて何?と思って導入の仕方まで勉強してみた記録の続き。
〈kotlin〉依存関係インジェクション(DI) - ゆるプログラミング日記

Hiltとは

DIを行うための Jetpack の推奨ライブラリ。
DIを手動で行おうとすると依存関係を取得するためのコンテナクラスだったり、
インスタンスを作成するためのFactoryだったり、
ライフサイクルを自分で管理したりとにかく大変そう。
Manual dependency injection  |  Android Developers

Hilt はプロジェクト内のすべての Android クラスにコンテナを提供し、
そのライフサイクルを自動で管理してくれる。
HIltのもとになっているDaggerライブラリでは、
コンテナやFactoryを自動で生成するために
コンポーネントという依存関係を示すグラフを作成する必要があったが、
Hiltではコンポーネントも自動で作成する。

build.gradle

//root
buildscript {
    ...
    dependencies {
        ...
        classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1")
    }
}

//app
plugins {
    kotlin("kapt")
    id("dagger.hilt.android.plugin")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.38.1")
    kapt("com.google.dagger:hilt-android-compiler:2.38.1")
}

Hiltアプリケーションクラス

Applicationクラスに@HiltAndroidApp アノテーションをつけることで、
アプリケーションレベルでコンポーネントが利用可能になる。

@HiltAndroidApp
class ExampleApplication : Application() { ... }

上記でHilt をセットアップした後、
依存関係を提供するために他のクラスには@AndroidEntryPointアノテーションをつける。
(現在サポートされているクラスはActivity, Fragment, View, Service, BroadcastReceiver, ViewModel。
ただしViewModelは@HiltViewModel。)

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

依存関係を取得するには、@Inject アノテーションを使用してフィールド インジェクションする。

依存関係の提供方法を指示

フィールドインジェクションしたクラスのインスタンスをHiltが作成できるようにするには、
@Injectアノテーションを使用してインスタンスの作成方法を指示する必要がある。

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

@Injectアノテーションをコンストラクタにつける。この時constructorと明記する。
クラスがコンストラクタを持っていなくても@Inject constructor()をつける。
これによりHiltはクラスのインスタンスの作り方を知ることができる。

コンストラクタがある場合、さらにそのインスタンス(上記だとAnalyticsServiceのインスタンス)が必要なので、
@Injectアノテーションをつけて作成方法を指示していく。
ただし、必要なインスタンスがインターフェースや外部ライブラリのクラスなどの場合、
コンストラクタインジェクションができないため後述のモジュールを定義する。

Hiltモジュール

モジュールはインターフェースや外部ライブラリのインスタンスの作成方法をHiltに伝えるためのクラス。
@Module アノテーションをつけたクラスを用意する。

@Binds
インターフェースインスタンスを注入する場合は
@Bindsアノテーションをつけた抽象関数を用意する。
関数の戻り値でインターフェースのインスタンスを知らせ、
関数のパラメータに実際に提供する実装を知らせる。

interface AnalyticsService {
  fun analyticsMethods()
}

//インターフェースを使った実際の実装
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(analyticsServiceImpl: AnalyticsServiceImpl): AnalyticsService
}

@Provides
外部ライブラリ(Retrofit, OkHttpClient, Roomなど)のインスタンスを注入する場合は、
@Providesアノテーションをつけた関数を用意する。
関数の戻り値で提供したい型のインスタンスを知らせ、
関数の中身にインスタンスの作成方法を記述する。

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService( ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

@InstallIn
フィールド インジェクションができるAndroidクラスには、それぞれ関連付けされたHiltコンポーネントがあり、
それを@InstallInアノテーションで参照できる。
コンポーネントはそれぞれのAndroidクラスのライフサイクルに従って
作成・破棄されたり、スコープを設定したりできる。)
モジュールに@InstallInをつけて参照するコンポーネントを設定することで、
モジュール内のインスタンスがどのコンポーネントに提供されるか指定する。



ここまでで一応Hiltの導入ができる。
参考
Dependency injection with Hilt  |  Android Developers

〈kotlin〉依存関係インジェクション(DI)

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。
DIという単語が出てきて何?と思って導入の仕方まで勉強してみた記録。

DI(Dependency Injection)とは

あるクラスが別のクラスへの参照を必要とする(依存関係にある)とき、
必要とするオブジェクトを外部から渡してあげる仕組みのこと。

DIの例

Car クラスが Engine クラスへの参照を必要とする場合を例とする。

DIしない

Carクラス自身がEngineクラスのインスタンスを独自で作成する。

class Car {
    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Carの内部でEngineのインスタンスを作成しているため、
違う種類のEngineに変更したいときはCar自体を作り直す必要がある(Carの再利用ができない)。

DIする

Carに対してEngine オブジェクトをコンストラクタ内でパラメータとして渡す(コンストラクタインジェクション)。

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

違う種類のEngineに変更したいときは、
別のEngineインスタンスをCarのコンストラクタ引数として渡せば良い(Carが再利用できる)。

もしくは宣言だけしておいて、クラスの作成後に依存関係をインスタンス化する(フィールドインジェクション)。

lass Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

DIすると何が良いのか

  • 依存関係を分離できて、クラスの再利用がしやすくなる
  • リファクタリングが容易になる
  • テストがしやすい(依存関係のオブジェクトをテストダブルに変更してテストできる)