SharedPreferences で putStringSet() が1つしか保存できない。

何ですか、この動きは。

「Preference から Set を取得後、追加して再保存する」

問題なく以下のようなテストも通過します。


@Test fun getStringSet() {

  val key = "key"
  val preferences = context.getSharedPreferences(key, Context.MODE_PRIVATE)
  val beforeData = preferences.getStringSet(key, mutableSetOf())
  val beforeSize = beforeData.size

  beforeData.add(System.currentTimeMillis().toString())
  preferences.edit()
      .putStringSet(key, beforeData)
      .commit()

  val afterData = preferences.getStringSet(key, mutableSetOf())
  val afterSize = afterData.size

  println("$beforeData -> $afterData")
  println("$beforeSize -> $afterSize")
  assertEquals(beforeSize + 1, afterSize)

}


I/System.out: [1520865219872, 1520865358333] -> [1520865219872, 1520865358333]
I/System.out: 1 -> 2

しかし、何回実行してもデータが1つのままで増えていきません。


I/System.out: [1520865219872, 1520865504185] -> [1520865219872, 1520865504185]
I/System.out: 1 -> 2

I/System.out: [1520865219872, 1520865553254] -> [1520865219872, 1520865553254]
I/System.out: 1 -> 2

実際にファイルを確認してみると、


mako:/data/data/com.example.cryptocurrency/shared_prefs # cat key.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <set name="key">
        <string>1520865219872</string>
    </set>
</map>

1つしか保存されてないですね!

クソですね!

なぜなのか?

android - Misbehavior when trying to store a string set using SharedPreferences - Stack Overflow

Android compares the modified HashSet that you are trying to save using SharedPreferences.Editor.putStringSet with the current one stored on the SharedPreference, and both are the same object!!!

A possible solution is to make a copy of the Set returned by the SharedPreferences object

どうやら、同じオブジェクトの比較となるので保存してくれないようです。

読み出し後、すぐコピーすればいいのですね!


val beforeData = preferences.getStringSet(key, mutableSetOf()).toMutableSet()

なんとなく、よくある話のような気がします。


Kotlin で 非同期処理 Coroutine #1 ~ launch(), async()

ネット上を調べてみてもよくわかりません。

難しい言葉や experimental な仕様の変更などあったりして。

少しづつ試してみながらマスターしていきましょう。

// build.gradle

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.2"

//gradle.properties

kotlin.coroutines=enable

kotlinx.coroutines/coroutines-guide-ui.md at master · Kotlin/kotlinx.coroutines

まず、これ。


for (i in 1..10) {
  Timber.d("$i")
  Thread.sleep(1000)
}

非同期にしたいですよね。
launch から始めます。


launch { // @
  for (i in 1..10) {
    Timber.d("$i")
    Thread.sleep(1000)
  }
}


launch {
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)  // @
  }
}

引数をつけて渡す。

UI :
UIスレッドで実行。

CommonPool :
バックグランドスレッドで実行。


launch(UI) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}


launch(CommonPool) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}


launch(UI + CommonPool) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}

launch() の戻りからキャンセルできます。


val job = launch(UI) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}

fab.setOnClickListener {
  job.cancel() // @
}

 

まとめ

UIスレッドに限定されたコルーチンは、UIスレッドをブロックすることなく、UI内の何かを自由に更新して中断することができます。

delay が待っている間UIスレッドはブロックされないのでUIはフリーズしません。ただ単にコルーチンを中断します。

Job.cancelは完全にスレッドセーフでノンブロッキングです。
実際に終了するのを待つことなく、コルーチンがそのジョブをキャンセルするように通知するだけです。 どこからでも呼び出すことができます。

基本的な非同期呼び出しは、launch() と async() の2つ。似ているが戻りが異なる。


【Kotlin】sortedWith + compareBy で並び替え

「C# でこんなの書けるけど Kotlin ではどう書くの?」 という話。


var list = new List<Person>();
list.Add(new Person(25, "Tom"));
list.Add(new Person(25, "Dave"));
list.Add(new Person(20, "Kate"));
list.Add(new Person(20, "Alice"));

// will produce: Alice, Kate, Dave, Tom
var sortedList = list
    .OrderBy(person => person.Age)
    .ThenBy(person => person.Name)
    .ToList();

Javaのコードから考えるとKotlinでももちろんシンプルな記述となります。


val sortedList = list
    .sortedWith(compareBy({ it.age }, { it.name }))


val sortedList = list
    .sortedWith(compareBy(Person::age, Person::name))


list.sortedWith(compareBy<Person> { it.age }.thenBy { it.name }.thenBy { it.address })


list.sortedWith(compareBy({ it.age }, { it.name }, { it.address }))

java - Sort collection by multiple fields in Kotlin - Stack Overflow

大文字小文字を無視する場合。


places
    .sortedWith(
        compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })
     )


places
    .sortedWith(
        compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }
     )


places
    .sortedWith(
        compareBy(String.CASE_INSENSITIVE_ORDER, Place::name)
     )

kotlin - How to sort objects list in case insensitive order? - Stack Overflow

降順昇順の対応やモデルに入れたり。


val sortedList = list
    .sortedWith(compareBy({ it.a }, { it.b }, { it.c }))


list.sortedWith(
    compareBy<Foo> { it.a }
        .thenByDescending { it.b }
        .thenBy { it.c }
)


class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {

  override fun compareTo(other: Foo)
      = compareValuesBy(this, other, { it.a }, { it.b }, { it.c })
}
val sortedList = list.sorted()


class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {

  override fun compareTo(other: Foo) = comparator.compare(this, other)

  companion object {
    val comparator = compareBy(Foo::a, Foo::b, Foo::c)
  }
}
val sortedList = list.sorted()

comparable - How to sort based on/compare multiple values in Kotlin? - Stack Overflow

シンプルで便利になりつつも複数の記述で書くことができてしまう最近の言語のパターン。


イベントハンドラとラムダ

おさらい


val printHelloWorld = {
   println("Hello, world!")
}

printHelloWorld()
//printHelloWorld.invoke()

これで以下が出力される。

Hello, world!

同様に引数を渡す。


val printGreeting = { word: String, name: String ->
   println("$word, $name")
}

printGreeting("おはよう", "太郎")
printGreeting.invoke("こんにちは", "次郎")

おはよう, 太郎
こんにちは, 次郎

これまでのコールバック

inferface を使ったコールバックは kotlin では以下のように書けます。


interface CallBackListener {
    fun onHoge(foo: String, bar: Int)
}

// caller
var callback: CallBackListener? = null
callback?.onHoge("foo", 100)

// callee
val callback = object : CallBackListener {
    override fun onHoge(foo: String, bar: Int) {
        print("$foo : $bar")
    }
}

これを typealias で。


typealias CallBackListener = (foo: String, bar: Int) -> Unit

// caller
var callback: CallBackListener? = null
callback?.invoke("foo", 100)

// callee
val callback = { foo, bar ->
    print("$foo : $bar")
}

【Kotlin】 TypeAliasで関数リテラルに名前を付ける - Qiita

イベントハンドラ

例えば、RecyclerView でそれぞれのアイテムのViewHolder内のある要素のクリックイベントを拾いたい時、Activity(View)までが遠いですよね。

Activity(View)
↓↑
RecyclerViewAdapter
↓↑
ViewHolder

少しでも、コールバック記述をシンプルにして見通し良くしておきたいものです。

ここで Type Alias を使います。

typealias EventHandler = (T) -> Unit

java - Kotlin: Use a lambda in place of a functional interface? - Stack Overflow

1. ViewHolder にハンドラー記述。クラスの外。

ItemViewHolder:


internal typealias ItemHandler = (Item) -> Unit

それを拡張して、ハンドラークラスを作ります。
invoke を override します。

ShareItemHandler:


internal class ShareItemHandler(
  private val activity: Activity
) : ItemHandler {

  override fun invoke(item: Item) {
    IntentBuilder.from(activity)
        .setType("text/plain")
        .setChooserTitle("Share ${item.name} to ?")
        .setText("${$item.name} ${item.address}")
        .startChooser()
  }
}

ClipboardCopyItemHandler:


internal class ClipboardCopyItemHandler(
  private val context: Context
) : ItemHandler {

  override fun invoke(item: Item) {
    val clipboard = context.systemService<ClipboardManager>()
    clipboard.primaryClip = ClipData.newPlainText(item.name, item.address)
  }
}

2. Activity で そのインスタンスを生成して
RecyclerAdapter経由でViewHolder内にセットしていきます。

以下関係部分のみ抜粋。

MainActivity:


val onCopy = ClipboardCopyItemHandler(this)
val onShare = ShareItemHandler(this)

// ...

val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val adapter = ItemAdapter(layoutInflater, onCopy, onShare)
recyclerView.adapter = adapter

ItemAdapter:


internal class ItemAdapter(
  private val inflater: LayoutInflater,
  private val onCopy: (Item) -> Unit,
  private val onShare: (Item) -> Unit
) : RecyclerView.Adapter<ItemViewHolder>() {

  // ...

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    val view = inflater.inflate(R.layout.item, parent, false)
      return ItemViewHolder(view, onCopy, onShare)
    }

ItemViewHolder:



internal typealias ItemHandler = (Item) -> Unit // ***

internal class ItemViewHolder(
  private val root: View,
  private val onCopy: ItemHandler
  private val onShare: ItemHandler,
) : ViewHolder(root), OnClickListener, OnMenuItemClickListener {

  // ...  

  override fun onClick(view: View) = when(view.id) {
    R.id.copy -> onCopy(item!!)
    R.id.share -> onShare(item!!)
    else -> throw IllegalArgumentException("Unknown menu item: $menuItem")
  }

なんだか素晴らしいですね。

RecyclerView利用時のテンプレートとして。


JetBrains社の「Edu Tools Plugin」で Kotlin に入門せよ

Kotlin Edu: Learn and teach Kotlin with easy and professional tools

Learning Kotlin with EduTools plugin - Kotlin Programming Language

Android Studio上でプラグインを検索して追加したあと

再起動ですぐに開始できます.

右の説明を読みながらエディタ上でコードを記述していきます.

チェックボタンを押すと, 記述後のチェックだけでなく, ヒントとなるメッセージが表示されます.

左のプロジェクトウィンドウを上から順番に進んでいくと基本をマスターできます.

できるのかな.

(現在, チャレンジ中・・・)