Kotlin スコープ関数 の上手な使い分け その3 - with

kotlin scope function

 

■ with の便利な使い方 (公式)


public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
  contract {...}
  return receiver.block()
}

with is a non-extension function that can access members of its argument concisely: you can omit the instance name when referring to its members.

with は非拡張関数で、引数のメンバに簡潔にアクセスできる。メンバを参照する際にインスタンス名を省略できる。

Grouping function calls on an object

オブジェクトの関数呼び出しをグループ化する。

Call multiple methods on an object instance (with)

オブジェクトのインスタンスに対して複数のメソッドを呼び出す。

👉 Kotlin Examples: Learn Kotlin Programming By Example hatena-bookmark
👉 Scope functions | Kotlin hatena-bookmark
👉 Idioms | Kotlin hatena-bookmark

 

■ まとめ

「with」は「記述の長い View などの連続操作をまとめる」のに多く使わています。


with(binding) {
  progressBar.visibility = View.VISIBLE
  cancelButton.visibility = View.VISIBLE
  goButton.visibility = View.GONE
  seeFileButton.visibility = View.GONE
}


with(viewBinding.recyclerView) {
  setHasFixedSize(true)
  adapter = ProductSearchAdapter()
  layoutManager =
    LinearLayoutManager(
      this@ProductSearchActivity,
      LinearLayoutManager.VERTICAL,
      false
    )
}


with(holder.binding) {
  person = items[position]
  executePendingBindings()
}

with の戻り値を使わないことのほうが多く見えますが、戻り値はあります。


val windowDpSize = with(LocalDensity.current) {
  windowSize.toDpSize()
}

👉 Kotlin スコープ関数 の上手な使い分け その1 - apply hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その2 - also hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その3 - with hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その4 - let hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その5 - run hatena-bookmark


Kotlin スコープ関数 の上手な使い分け その2 - also

kotlin scope function

apply と also は似ています。

👉 Kotlin スコープ関数 の上手な使い分け その1 - apply hatena-bookmark

 

■ also の便利な使い方 (公式)


public inline fun <T> T.also(block: (T) -> Unit): T {
  contract {...}
  block(this)
  return this
}

also works like apply: it executes a given block and returns the object called. Inside the block, the object is referenced by it, so it's easier to pass it as an argument. This function is handy for embedding additional actions, such as logging in call chains.

also も apply と同じように動作します。与えられたブロックを実行し、呼び出されたオブジェクトを返します。ブロックの内部では、オブジェクトは it によって参照されるので、引数として渡すのは簡単です。この関数は、コールチェーンにロギングなどの追加アクションを埋め込むのに便利です。


val jake = Person("Jake", 30, "Android developer") 
  .also {                                          
    writeCreationLog(it)                         
  }

Additional effects

追加効果

👉 Kotlin Examples: Learn Kotlin Programming By Example hatena-bookmark
👉 Scope functions | Kotlin hatena-bookmark
👉 Idioms | Kotlin hatena-bookmark

 

■ まとめ

「also」は「オブジェクトに対しての追加処理」に使う と良さそうです。

あと、Singleton インスタンスの生成時のコードで見かけることが印象に強いです。


@JvmStatic
fun getInstance(context: Context): PowerSpinnerPersistence =
  instance ?: synchronized(this) {
    instance ?: PowerSpinnerPersistence().also {
      instance = it
      sharedPreferenceManager =
        context.getSharedPreferences("com.skydoves.powerspinenr", Context.MODE_PRIVATE)
    }
  }


companion object {
  @Volatile
  private var INSTANCE: MySingleton? = null
  fun getInstance(context: Context) =
    INSTANCE ?: synchronized(this) {
      INSTANCE ?: MySingleton(context).also {
        INSTANCE = it
      }
    }
}

👉 Kotlin で書きたい「正しいシングルトン(Singleton)」 hatena-bookmark

こんな使い方も公式に書いてました。

Swap two variables

2つの変数を入れ替える


var a = 1
var b = 2
a = b.also { b = a }

👉 Kotlin スコープ関数 の上手な使い分け その1 - apply hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その2 - also hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その3 - with hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その4 - let hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その5 - run hatena-bookmark


Kotlin スコープ関数 の上手な使い分け その1 - apply

kotlin scope function

使い分けが難しいと言われているスコープ関数ですが。

以下、どれでも同じ結果を取得できます。


val bundle = Bundle()
bundle.putInt("x", 1)
bundle.putInt("y", 2)

val run = Bundle().run {
  putInt("x", 1)
  putInt("y", 2)
  this
}

val let = Bundle().let {
  it.putInt("x", 1)
  it.putInt("y", 2)
  it
}

val with = with(Bundle()) {
  putInt("x", 1)
  putInt("y", 2)
  this
}

val apply = Bundle().apply {
  putInt("x", 1)
  putInt("y", 2)
}

val also = Bundle().also {
  it.putInt("x", 1)
  it.putInt("y", 2)
}

val runNE = run {
  val bundleNE = Bundle()
  bundleNE.putInt("x", 1)
  bundleNE.putInt("y", 2)
  bundleNE
}

println("bundle = $bundle")
println("run    = $run")
println("let    = $let")
println("with   = $with")
println("apply  = $apply")
println("also   = $also")
println("runNE  = $runNE")


I/System.out: bundle = Bundle[{x=1, y=2}]
I/System.out: run    = Bundle[{x=1, y=2}]
I/System.out: let    = Bundle[{x=1, y=2}]
I/System.out: with   = Bundle[{x=1, y=2}]
I/System.out: apply  = Bundle[{x=1, y=2}]
I/System.out: also   = Bundle[{x=1, y=2}]
I/System.out: runNE  = Bundle[{x=1, y=2}]

どれが書きやすいですかね。

どれが分かりやすく管理しやすいですか。

 

■ apply の便利な使い方 (公式)

実装を見てみます。


public inline fun <T> T.apply(block: T.() -> Unit): T {
  contract {...}
  block()
  return this
}

Kotlin 公式リファレンスには使い分けとして以下の説明が書かれてます。

apply executes a block of code on an object and returns the object itself. Inside the block, the object is referenced by this. This function is handy for initializing objects.

apply は、オブジェクトに対してコードのブロックを実行し、そのオブジェクト自身を返します。ブロックの内部では、オブジェクトはthisで参照されます。
この関数は、オブジェクトを初期化するのに便利です。

Object configuration
オブジェクトの設定

Builder-style usage of methods that return Unit
Unitを返すメソッドのBuilder的な使い方


fun arrayOfMinusOnes(size: Int): IntArray {
  return IntArray(size).apply { fill(-1) }
}

Configure properties of an object (apply)
オブジェクトのプロパティを設定する


val myRectangle = Rectangle().apply {
  length = 4
  breadth = 5
  color = 0xFAFAFA
}

👉 Kotlin Examples: Learn Kotlin Programming By Example hatena-bookmark
👉 Scope functions | Kotlin hatena-bookmark
👉 Idioms | Kotlin hatena-bookmark

 

■ まとめ

「apply」は「オブジェクトの初期設定」に使う と良いです。

コンストラクタの引数 や Builder の有無に関係なしに使えます。

使い方のイメージは Builder パターンのスタイルです。


fun newIntent(context: Context, movie: Movie): Intent {
  return Intent(context, PlaybackActivity::class.java)
    .apply {
       putExtra(MOVIE, movie)
    }
}


private val messagesSent = MutableLiveData<Int>().apply { value = 0 }
private val dinosClicked = MutableLiveData<Int>().apply { value = 0 }
private val dropText = MutableLiveData<String>().apply { value = "Drop Things Here!" }


private val paint = Paint().apply {
  color = drawColor
  isAntiAlias = true
  isDither = true
  style = Paint.Style.STROKE // default: FILL
  strokeJoin = Paint.Join.ROUND // default: MITER
  strokeCap = Paint.Cap.ROUND // default: BUTT
  strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}

apply ブロック内に if を使うこともできます。

👉 Kotlin スコープ関数 の上手な使い分け その1 - apply hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その2 - also hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その3 - with hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その4 - let hatena-bookmark
👉 Kotlin スコープ関数 の上手な使い分け その5 - run hatena-bookmark


【JetpackCompose】BottomNavigation の使い方

アプリの基本的な画面です。

【JetpackCompose】BottomNavigation の使い方

シンプルなもので眺めてみます。

 

■ 構成

「Column」を使って2つのパーツを縦に並べます。

【JetpackCompose】BottomNavigation の使い方

メイン画面を表示する部分は

「NavHost」

それを表示切り替えする下側に並んだ操作部分は

「BottomNavigation」

です。

コードは以下のように構成できます。


@Composable
fun MainScreen() {

  val navController = rememberNavController()

  Column {

    NavHost(
      navController = navController,
    ) {
      composable(NavigationItem.HOME.route) {
        HomeScreen()
      }
      composable(NavigationItem.CRYPT.route) {
        CryptScreen()
      }
      // ...
    }

    BottomNavigation {
      val backStack by navController.currentBackStackEntryAsState()
      val current = backStack?.destination?.route
      NavigationItem.values().forEach { item ->
        BottomNavigationItem(
          // ...
        )
      }
    }

  }

}


enum class NavigationItem(
  // ...
)

 

■ NavController

NavHost を BottomNavigation を紐付けるコントローラーです。


val navController = rememberNavController()

両方からアクセスしやすい場所に置くことになります。

 

■ NavigationItem

BottomNavigation に並べるアイテムの情報をまとめておきます。

以下、3つの組み合わせになります。


- 画面を表す内部文字列(遷移先)

- アイコン画像

- 表示する画面名称文字列

すべて定数なので enum クラスで作成します。


enum class NavigationItem(
  val route: String,
  val icon: ImageVector,
  @StringRes val label: Int,
) {

  HOME("home", Icons.Outlined.Home, R.string.nav_label_home),
  CRYPT("crypt", Icons.Outlined.MonetizationOn, R.string.nav_label_crypt),
  // ...

}

ここでは多言語化を考慮して、

遷移を表す内部で使用する文字列と、画面名称を表示する文字列を区別しています。

必要なければまとめてもいいと思います。

 

■ BottomNavigation

NavigationItem を BottomNavigation に配置します。


BottomNavigation {

  val backStack by navController.currentBackStackEntryAsState()
  val current = backStack?.destination?.route

  NavigationItem.values().forEach { item ->
    BottomNavigationItem(
      selected = current == item.route,
      onClick = {
        navController.navigate(item.route) {

          // no back stacks. 0 is the root navigation
          popUpTo(id = 0) {  
            saveState = true
          }
          launchSingleTop = true
          restoreState = true
        }
      },
      icon = { Icon(item.icon, null) },
      label = {
        Text(
          text = stringResource(item.label),
          maxLines = 1
        )
      },
      alwaysShowLabel = false
    )
  }
}

onClick のラムダブロックでは、


- バックスタックなし。

- 画面の state は保存/リストアする。

としています。

それぞれの画面内での遷移がない場合は、これがシンプルで自然だと思います。

画面のスクロールポジションもこれで保持できます。

 

■ NavHost

画面のメイン表示部分です。

NavigationItem に紐づけて各画面の Screen-level Composable を記述しておきます。


NavHost( 
) {
  composable(NavigationItem.HOME.route) {
    HomeScreen()
  }
  composable(NavigationItem.CRYPT.route) {
    CryptScreen()
  }
  // ...
}

 

■ まとめ


@Composable
fun MainScreen() {

  val navController = rememberNavController()

  Column(
    modifier = Modifier.fillMaxSize(),
    verticalArrangement = Arrangement.Bottom
  ) {

    NavHost(
      navController = navController,
      startDestination = NavigationItem.HOME.route,
      modifier = Modifier
        .weight(1f)
    ) {
      composable(NavigationItem.HOME.route) {
        HomeScreen()
      }
      composable(NavigationItem.CRYPT.route) {
        CryptScreen()
      }
      // ...
    }

    BottomNavigation {

      val backStack by navController.currentBackStackEntryAsState()
      val current = backStack?.destination?.route

      NavigationItem.values().forEach { item ->
        BottomNavigationItem(
          selected = current == item.route,
          onClick = {
            navController.navigate(item.route) {
              // no back stacks. 0 is the root navigation
              popUpTo(id = 0) { 
                saveState = true
              }
              launchSingleTop = true
              restoreState = true
            }
          },
          icon = { Icon(item.icon, null) },
          label = {
            Text(
              text = stringResource(item.label),
              maxLines = 1
            )
          },
          alwaysShowLabel = false
        )
      }

    }

  }

}


enum class NavigationItem(
  val route: String,
  val icon: ImageVector,
  @StringRes val label: Int,
) {

  HOME("home", Icons.Outlined.Home, R.string.nav_label_home),
  CRYPT("crypt", Icons.Outlined.MonetizationOn, R.string.nav_label_crypt),
  // ...

}

Scaffold って使い勝手悪くね?

👉 JetpackCompose Coil で GIF - Qiita hatena-bookmark


KMM や マルチプラットフォーム を見据えて SQLDelight で Repository

👉 Getting Started - Multiplatform - SQLDelight hatena-bookmark
👉 SQLDelight 1.x Quick Start Guide for Android – Handstand Sam hatena-bookmark

マルチプラットフォームに対応していますが、

まずは、Android のみで使ってみると良いです。

👉 Getting Started - Android - SQLDelight hatena-bookmark

Todo アプリ向けの Repository を作ります。

Hilt と Flow を使っています。

 

■ インストール

Gradleまわりは書き方いろいろですけども適宜書き換えてください。


buildscript {
  repositories {
    google()
    mavenCentral()
  }
  dependencies {
    classpath 'com.squareup.sqldelight:gradle-plugin:1.5.3'
  }
}

apply plugin: 'com.squareup.sqldelight'

// android driver
implementation "com.squareup.sqldelight:android-driver:1.5.3"

// flow-coroutine extension 
implementation "com.squareup.sqldelight:coroutines-extensions:1.5.3"

 

■ スキーマ / クエリー

パッケージ名を、com.your.package としています。


-- app/src/main/sqldelight/com/your/package/data/Todo.sq

CREATE TABLE todo (
  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  text TEXT NOT NULL,
  updated INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

INSERT INTO todo(text) VALUES ('宿題をする');
INSERT INTO todo(text) VALUES ('マンガを読む');
INSERT INTO todo(text) VALUES ('プールに行く');

selectAll:
SELECT * FROM todo ORDER BY updated DESC;

deleteAll:
DELETE FROM todo;

insert:
INSERT INTO todo (text) VALUES (:text);

update:
UPDATE todo SET text = :text, updated = (strftime('%s', 'now')) WHERE id = :entryId;

delete:
DELETE FROM todo
WHERE id = :entryId;

count:
SELECT COUNT(id) FROM todo;

Todo.sq ファイルをテキストで作成して、テーブル定義、実行クエリーとメソッド名を箇条書きします。

配置位置は、上記コメントの位置が自然で分かりやすいと思います。

この場合、SQLDelight によって以下に実装に利用するクラス群が生成されます。


app/build/generated/sqldelight/code/Database/debug/com/your/package/

 

■ Database Module

SQLDelightによって生成された Database クラスを利用して書きます。

今回は Android向けなので、AndroidSqliteDriver を使っています。


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

  @Provides
  @Singleton
  fun provideDatabase(@ApplicationContext context: Context): Database {
    val driver = AndroidSqliteDriver(Database.Schema, context, DB_NAME)
    return Database(driver)
  }

  private const val DB_NAME = "database.db"
}

JetpackCompose の UI である Screen-level の Composable はマルチに稼働しているので、 @Singleton としておくことを忘れてはなりません。

 

■ Repository

時間のかかる処理は Flow を使っておきます。


class TodoRepository @Inject constructor(
  private val database: Database
) {

  fun load(): Flow<List<Todo>> {
    return database.todoQueries.selectAll().asFlow().mapToList(Dispatchers.IO)
  }

  fun count(): Flow<Long> {
    return database.todoQueries.count().asFlow().mapToOne(Dispatchers.IO)
  }

  fun insert(text: String) {
    database.todoQueries.insert(text)
  }

  fun update(id: Long, text: String) {
    database.todoQueries.update(text, id)
  }

  fun delete(id: Long) {
    database.todoQueries.delete(id)
  }

}

Flow - Couroutine Extension は非常に便利です。


fun load(): Flow<List<Todo>> {
  return database.todoQueries.selectAll().asFlow().mapToList(Dispatchers.IO)
}

Cold な Flow で単発の List<Todo> の emit ではなく、データベースが変更されるたびに、新しい List<Todo> を emit してくれます。


@JvmName("toFlow")
fun <T : Any> Query<T>.asFlow(): Flow<Query<T>> = flow {
  val channel = Channel<Unit>(CONFLATED)
  channel.trySend(Unit)

  val listener = object : Query.Listener {
    override fun queryResultsChanged() {
      channel.trySend(Unit)
    }
  }

  addListener(listener)
  try {
    for (item in channel) {
      emit(this@asFlow)
    }
  } finally {
    removeListener(listener)
  }
}

👉 sqldelight/FlowExtensions.kt at master · cashapp/sqldelight hatena-bookmark

(更新中...)

👉 Jetpack Compose で Todo アプリを作ってみた - Qiita hatena-bookmark