KotlinFest 2024 で話すはずだった講演内容です。
みなさん、こんにちは。あんざいゆきです。Android の Google Developer Expert をしています。よろしくお願いします。
私はいろんなクライアントさんの Android アプリ開発のお手伝いをさせていただいていまして、Java から Kotlin に変換した Pull Request のレビューをすることがあります。
プロジェクトの大多数がまだ Java だったり、最近 Android 開発をはじめたばかりだったりして Kotlin になじみがない場合だと、自動変換されただけのような状態でレビュー依頼されることがままあります。
そこでこのセッションでは、Java から Kotlin に自動変換したあと、より Kotlin らしいコードにするためにどういうことをしてほしいのかを紹介したいと思います。
Kotlin らしいコードの話をする前に、Java から Kotlin に変換する Pull Request について話したいと思います。
1 commit で Kotlin 化すると、Java ファイルの削除と Kotlin のファイルの新規追加の履歴になり、Kotlin 化前後のコードを比較するのがけっこう大変です。
そこで、Kotlin 化する前に拡張子を .java から .kt に rename する commit を入れておきます。中身は Java のままです。
commit したら拡張子をまた .kt から .java に戻し、その後に Convert Java File to Kotlin File などで Kotlin 化します。
こうした場合、rename と Kotlin 化の 2つの commit の Pull Request になります。
Files changed タブだと 1 commit で Kotlin 化したときと同じように Kotlin 化前後のコードを比較するのが大変なんですが、
Commits タグを開いて
2つめの commit をみると
同じ .kt ファイルの変更なので、Kotlin 化前後のコードが比較しやすい表示になります。
では本題に入りましょう。
残念ながら Java コード側の情報が少ないと変換したコードに !! が出てくることがあります。
!! が不要になるようにコードを修正しましょう。
この例では引数の newItems を non-null にすれば !! は不要になります。
@NonNull アノテーションがついていない場合、@Nullable なメソッドに渡されている変数は nullable だと解釈されます。
例えば Bundle の putString() メソッドは key も value も @Nullable アノテーションがついています。
そのため Kotlin に自動変換すると、createInstance() メソッドの title 引数は nullable String になります。
もともと null が来ることを想定しているならこのままでよいですが、
null が来ることがありえない、あってはいけないという場合はそれを表現するよう non-null にしましょう。
型パラメータも自動変換時に nullable と判定されることがあるので注意しましょう。
初期化の後に利用されることが保証できる場合(初期化前アクセスが発生するのはコーディングエラー時のみという場合)、
lateinit var を使うことで non-null にすることができます。
var を val にできないか考えましょう。
Kotlin の標準ライブラリに用意されている関数を利用することで val にできることがよくあります。
様々な便利関数があるので、変換されたやつでいいやってなる前に、活用できるものがないか調べましょう。
?.let や ?: を使ってより簡潔に記述できるようにならないか考えましょう。
apply や also は初期化処理をまとめるのによく使います。
Kotlin では使っていない lambda の引数を _ にすることができます。
自動変換ではやってくれませんが、変換後のコードでグレーの波線が出るので対応しましょう。
A から B に変換する処理は Aの拡張関数にすると呼び出し側がすっきりします。
自動変換では Smart cast が効いている部分の cast を外してくれないので自分で外しましょう。
また、as? を使うことでチェックと呼び出し部分を1行で書くこともできます。
Java の switch 文は自動で when にしてくれますが、if else の連続は自動で when にしてくれません。
必要に応じて自分で when にしましょう。
Java から Kotlin の lambda を引数にとるメソッドを呼んでいる部分があるとします。
これを Kotlin 化した場合、関数参照を使ったほうが記述が簡潔になる場合があります。
List を操作する Java コードを Kotlin に自動変換した場合、MutableList を使ったものになります。
Kotlin std lib に用意されているメソッドを使うと MutableList を不要にできることがあります。
Java では Mutable Collection と Immutable Collection で型が分かれていないので、自動変換すると mutable として外部に公開するべきでないところでも mutable として公開されてしまいます。
Mutable Collection の変数は private にし、公開用に Immutable Collection 型のプロパティを定義するようにします。
Java で ArrayList を new しているところは自動変換しても同じです。
mutableListOf() を使ってより Kotlin らしいコードにしましょう。
同様に HashMap, LinkedHashMap には mutableMapOf(), HashSet, LinkedHashSet には mutableSetOf() が使えます。
List や Map を構成する部分は、自動変換ではほぼそのままの形にしかなりません。
初期化時のみ List や Map を編集するのであれば buildList { } や buildMap { } を使って Mutable Collection の変数が定義されないようにしましょう。
Android 特有の内容も紹介します。
Bundle の生成用に bundleOf() メソッドが用意されています。
TextUtils.isEmpty() は Kotlin の isNullOrEmpty() に置き換えましょう。
TextUtils.equals() は Kotlin の == に置き換えましょう。
Kotlin では、Activity や Fragment で ViewModel のインスタンスを取得するのに by viewModels() を利用することができます。
DI で値がセットされるフィールドは Java から Kotlin に自動変換すると nullable の var になってしまいます。
しかしこれらは参照されるときには値がすでにセットされていることが期待されるものなので、lateinit var に変えましょう。
最後にチェックリストをまとめました。ありがとうございました。
2024年6月23日日曜日
2024年2月27日火曜日
Android Studio (IntelliJ IDEA) の Replace の正規表現モードを使う
例えば
Cmd + Shift + R で Replace in Files window を開き、
(そのファイルだけ置き換えたいときは Cmd + R)
変換元を入力するフィールドの右端の 「.*」 部分をオンにする。
変換元に
assertThat(answer).isEqualTo(2)
を
assertEquals(2, answer)
に置き換えたい場合、Replace の正規表現モードを使うことで簡単に変換できる。
Cmd + Shift + R で Replace in Files window を開き、
(そのファイルだけ置き換えたいときは Cmd + R)
変換元を入力するフィールドの右端の 「.*」 部分をオンにする。
変換元に
assertThat\((.*)\)\.isEqualTo\((.*)\)
変換先に
assertEquals\($2, $1\)
と入れて、Replace All ボタンを押すと全部置き換わる。便利!
2023年5月27日土曜日
Compose Multiplatform で Image loading ってどうやるの?
技術書典14用に書いたんだけど mhidaka 多忙により寝かされてしまったため、ここで供養します。
夏コミ(C102)は別の記事を書く予定です。
==========================
今年(2023年)のKotlinConf(https://kotlinconf.com/)でCompose Multiplatformが発表されました。これまではCompose Desktopを使うことでAndroidとDesktopでUIコードを共通化することができました。Compose Multiplatformではこれに加えiOSとWebもサポートしています。2023年5月時点ではiOSはAlpha版、WebはExperimental版です。
この章ではAndroidとiOSのUIをCompose Multiplatformで共通化したときに、画像をネットワークから非同期に読み込む方法を紹介します。
ここで紹介する方法は2023年5月時点のものであり、今後APIが変わる可能性が大いにあります。注意してください。
夏コミ(C102)は別の記事を書く予定です。
==========================
今年(2023年)のKotlinConf(https://kotlinconf.com/)でCompose Multiplatformが発表されました。これまではCompose Desktopを使うことでAndroidとDesktopでUIコードを共通化することができました。Compose Multiplatformではこれに加えiOSとWebもサポートしています。2023年5月時点ではiOSはAlpha版、WebはExperimental版です。
この章ではAndroidとiOSのUIをCompose Multiplatformで共通化したときに、画像をネットワークから非同期に読み込む方法を紹介します。
ここで紹介する方法は2023年5月時点のものであり、今後APIが変わる可能性が大いにあります。注意してください。
Resources
Compose MultiplatformにはResourceというinterfaceが用意されています。 Resourceにはsuspend関数のreadBytes()が定義されており、戻り値はByteArrayです。
@ExperimentalResourceApi
interface Resource {
suspend fun readBytes(): ByteArray
}
このResourceを実装したLoadImageResourceを用意します。
LoadImageResourceのreadBytes()では指定されたURLからByteArrayを取得する処理を実装します。LoadImageResourceではKtor(https://ktor.io/)を使っています。
@OptIn(ExperimentalResourceApi::class)
class LoadImageResource(private val url: String) : Resource {
override suspend fun readBytes(): ByteArray {
return HttpClient().use {
it.get(url).readBytes()
}
}
}
rememberImageBitmap
Resourceの拡張関数としてrememberImageBitmap()が用意されています。 rememberImageBitmap()はComposable関数で、戻り値はLoadState<ImageBitmap>です。 LaunchedEffectのなかでResourceのreadBytes()を呼び出し、返ってきたByteArrayをtoImageBitmap()でImageBitmapに変換しています。
@ExperimentalResourceApi
@Composable
fun Resource.rememberImageBitmap(): LoadState<ImageBitmap> {
val state: MutableState<LoadState<ImageBitmap>> = remember(this) { mutableStateOf(LoadState.Loading()) }
LaunchedEffect(this) {
state.value = try {
LoadState.Success(readBytes().toImageBitmap())
} catch (e: Exception) {
LoadState.Error(e)
}
}
return state.value
}
LoadStateはsealed classでLoading、Success、Errorの3つのサブクラスがあります。
sealed class LoadState<T> {
class Loading<T> : LoadState<T>()
data class Success<T>(val value: T) : LoadState<T>()
data class Error<T>(val exception: Exception) : LoadState<T>()
}
AsyncImage
画像を読み込んで表示したいURLに対してLoadImageResourceを作り、rememberImageBitmap()の返す LoadStateに応じてComposableを表示します。 LoadState.SuccessのvalueプロパティからImageBitmapを取得し、BitmapPainterでPainterを作ります。 最後にBitmapPainterをImage composableにセットすることで画像が表示されます。
@OptIn(ExperimentalResourceApi::class)
@Composable
fun AsyncImage(
url: String,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
) {
val resource = remember(url) { LoadImageResource(url) }
when (val loadState = resource.rememberImageBitmap()) {
is LoadState.Loading,
is LoadState.Error -> {
Spacer(modifier = modifier.background(Color.LightGray))
}
is LoadState.Success -> {
Image(
painter = BitmapPainter(loadState.value),
contentDescription = contentDescription,
contentScale = contentScale,
modifier = modifier
)
}
}
}
まとめ
これでCompose MultiplatformのAndroid、iOS両方でネットワークから読み込んだ画像が表示されます。 実際のアプリで使うにはキャッシュ機能などが必要になりますし、そのうちライブラリもでてくるでしょう。 Compose Multiplatformぜひ試してみてください。2022年8月8日月曜日
kotlin coroutines 1.6.4 で TestScope.backgroundScope が追加された
https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.6.4
background で実行してテスト終了時にキャンセルされる coroutines を起動できます。
いままでは明示的に cancelAndJoin() していたのがいらなくなりますね。
前
background で実行してテスト終了時にキャンセルされる coroutines を起動できます。
いままでは明示的に cancelAndJoin() していたのがいらなくなりますね。
前
@Test
fun test() = runTest {
val list = mutableListOf<SomeValue>()
val job = launch(UnconfinedTestDispatcher()) {
repository.someValueFlow().collect {
list.add(it)
}
}
...
assertEquals(expectedSomeValueList, list)
job.cancelAndJoin()
}
後
@Test
fun test() = runTest {
val list = mutableListOf<SomeValue>()
backgroundScope.launch(UnconfinedTestDispatcher()) {
repository.someValueFlow().collect {
list.add(it)
}
}
...
assertEquals(expectedSomeValueList, list)
}
2022年2月23日水曜日
StateFlow.collectAsState() だと現在の値を初期値として使ってくれる
StateFlow.collectAsState() だと StateFlow の value を初期値として使ってくれます。一方 Flow.collectAsState() では initial に指定した値が初期値になります。
例えばサーバーからデータをとってきて表示する画面があり、画面の状態を表す UiState が次のようになっているとします。
(* 私は基本的には ViewModel からは StateFlow ではなく State を公開するようにしています。)
Flow.collectAsState() の場合
UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success
ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってくると、一瞬 UiState.Initial になります。
UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Initial → UiState.Success
StateFlow.collectAsState() の場合
UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success
一方、ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってきても UiState.Initial にはなりません。
UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Success
そのため、ViewModel から StateFlow を公開して StateFlow の collectAsState() を使ったほうがよいです。
stateIn() を使えば Flow を StateFlow に変換することができます。
@Composable
fun <T> StateFlow<T>.collectAsState(
context: CoroutineContext = EmptyCoroutineContext
): State<T> = collectAsState(value, context)
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
例えばサーバーからデータをとってきて表示する画面があり、画面の状態を表す UiState が次のようになっているとします。
sealed interface UiState {
object Initial : UiState
object Loading : UiState
data class Error(val e: Exception) : UiState
data class Success(val profile: Profile) : UiState
}
(* 私は基本的には ViewModel からは StateFlow ではなく State を公開するようにしています。)
Flow.collectAsState() の場合
class ProfileViewModel : ViewModel() {
val uiState: Flow<UiState> = ...
...
}
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel
) {
ProfileContent(
uiState = viewModel.uiState.collectAsState(initial = UiState.Initial).value
)
}
@Composable
private fun ProfileContent(
uiState: UiState
) {
when (uiState) {
UiState.Initial,
UiState.Loading -> {
...
}
is UiState.Error -> {
...
}
is UiState.Success -> {
...
}
}
}
Flow.collectAsState() の場合、ProfileContent の (re)compose およびそのときの UiState は次のようになります。
UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success
ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってくると、一瞬 UiState.Initial になります。
UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Initial → UiState.Success
StateFlow.collectAsState() の場合
class ProfileViewModel : ViewModel() {
val uiState: StateFlow<UiState> = ...
...
}
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel
) {
ProfileContent(
uiState = viewModel.uiState.collectAsState().value
)
}
StateFlow.collectAsState() の場合、データ取得完了までの UiState の流れは同じです。
UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success
一方、ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってきても UiState.Initial にはなりません。
UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Success
そのため、ViewModel から StateFlow を公開して StateFlow の collectAsState() を使ったほうがよいです。
stateIn() を使えば Flow を StateFlow に変換することができます。
2021年12月16日木曜日
Dagger Hilt 2.40.2 で EntryPoints.get() の便利 overloads である EntryPointAccessors が追加された
今まで
val entryPoint = EntryPoints.get(activity, ActivityCreatorEntryPoint::class.java)
EntryPointAccessors を使うと
val entryPoint = EntryPointAccessors.fromActivity<ActivityCreatorEntryPoint>(activity)
fromApplication(), fromActivity(), fromFragment(), fromView() が用意されている。
2021年6月1日火曜日
Kotlin の sealed interface が必要になる例
(本当は Compose のコード例(State/MutableState)にしたかったんだけど、まだ Jetpack Compose は Kotlin 1.5 に対応してないので sealed interface 使えないんだよね...)
ViewModel で持ってる LiveData を Activity で observe してるとする。
mutableValue に private は MainViewModel から見えなくなるのでダメ。
ということで sealed interface を使います。
ViewModel で持ってる LiveData を Activity で observe してるとする。
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.state.observe(this) {
when (it) {
is State.Data -> {
println(it.value)
}
State.Loading -> {
}
}
}
}
}
class MainViewModel : ViewModel() {
private val _state = MutableLiveData<State>()
val state: LiveData<State>
get() = _state
fun doSomething() {
val state = _state.value
if (state is State.Data) {
state.mutableValue = Random.nextInt()
}
}
}
sealed class State {
object Loading : State()
data class Data(val id: String) : State() {
// 変更できるのは MainViewModel からだけにしたいが、
// private にすると MainViewModel からも見えなくなる
var mutableValue: Int = -1
val value: Int
get() = mutableValue
}
}
↑ State.Data が持つ mutableValue は MainViewModel からのみ変更できるようにしたい。
mutableValue に private は MainViewModel から見えなくなるのでダメ。
private var mutableValue: Int = -1
これもダメ。
private sealed class State {
private data class Data(val id: String) : State() {
Data を top level にすると private をつけても MainViewModel から見えるけど、MainActivity から見えなくなるのでダメ。
sealed class State
object Loading : State()
// MainViewModel から mutableValue は見えるが
// Data が MainActivity からは見えなくなる
private data class Data(val id: String) : State() {
var mutableValue: Int = -1
val value: Int
get() = mutableValue
}
ということで sealed interface を使います。
class MainViewModel : ViewModel() {
private val _state = MutableLiveData<State>()
val state: LiveData<State>
get() = _state
fun doSomething() {
val state = _state.value
if (state is DataImpl) {
state.mutableValue = Random.nextInt()
}
}
}
sealed interface State
object Loading : State
sealed interface Data : State {
val value: Int
}
private class DataImpl(val id: Int) : Data {
// private class なので変更できるのは同じファイルからだけ
var mutableValue: Int = id
override val value: Int
get() = mutableValue
}
2021年5月14日金曜日
inline class および value class で kotlinx.serialization (JSON) が動く組み合わせ
inline class および value class で kotlinx.serialization (JSON) が動く組み合わせを調べてみた
inline class のときのコード
e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation
e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation
Kotlin を 1.5.0 にすれば kotlinx.serialization を 1.2 にしなくても inline class と value class 両方で動いた。
inline class のときのコード
@Serializable
inline class ItemId(val value: String)
@Serializable
data class Item(val id: ItemId, val name: String)
fun main() {
val item = Item(ItemId("1"), "Android")
val json = Json.encodeToString(item)
println(json)
println(Json.decodeFromString<Item>(json))
}
value class のときのコード
@Serializable
@JvmInline
value class ItemId(val value: String)
@Serializable
data class Item(val id: ItemId, val name: String)
fun main() {
val item = Item(ItemId("1"), "Android")
val json = Json.encodeToString(item)
println(json)
println(Json.decodeFromString<Item>(json))
}
Kotlin: 14.32, kotlinx.serialization: 1.1.0 + inline class
ビルドエラーになるe: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation
Kotlin: 1.4.32, kotlinx.serialization: 1.2.1 + inline class
ビルドエラーになるe: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation
Kotlin: 1.4.32, kotlinx.serialization: 1.1.0 + value class
@JvmInline が無いのでビルドエラーになるKotlin: 1.4.32, kotlinx.serialization: 1.2.1 + value class
@JvmInline が無いのでビルドエラーになるKotlin: 1.5.0, kotlinx.serialization: 1.1.0 + inline class
動くKotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class
動くKotlin: 1.5.0, kotlinx.serialization: 1.1.0 + value class
動くKotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class
動くKotlin を 1.5.0 にすれば kotlinx.serialization を 1.2 にしなくても inline class と value class 両方で動いた。
2021年3月23日火曜日
Kotlin Serialization は sealed class も対応していて便利
Kotlin Serialization
plugins {
...
id "org.jetbrains.kotlin.plugin.serialization" version "1.4.31"
}
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
}
@Serializable
data class Dog(val name: String, val age: Int, val sex: Sex, val kind: Kind)
enum class Sex {
MALE,
FEMALE
}
@Serializable
sealed class Kind {
@Serializable
object Hybrid : Kind()
@Serializable
data class PureBlood(val name: String) : Kind()
}
class DogTest {
@Test
fun list() {
val dogs = listOf(
Dog("White", 10, Sex.MALE, Kind.Hybrid),
Dog("Black", 20, Sex.FEMALE, Kind.PureBlood("Husky"))
)
val json = Json.encodeToString(dogs)
println(json)
// [{"name":"White","age":10,"sex":"MALE","kind":{"type":"net.yanzm.serialize.Kind.Hybrid"}},{"name":"Black","age":20,"sex":"FEMALE","kind":{"type":"net.yanzm.serialize.Kind.PureBlood","name":"Husky"}}]
val decoded = Json.decodeFromString<List<Dog>>(json)
assertThat(decoded).isEqualTo(dogs)
}
}
2021年3月9日火曜日
AppEngine に Ktor アプリをデプロイする
1. Google Cloud SDK をインストールする
https://cloud.google.com/sdk/docs/install2. 認証 & プロジェクト選択
> gcloud init
3. Ktor アプリを作る
IntelliJ IDEA に Ktor plugin を入れて、New Project wizard から Ktor プロジェクトを作る4. AppEngine の設定を追加する
build.gradle.kts
...
plugins {
...
// ↓ 追加
id("com.google.cloud.tools.appengine") version "2.2.0"
// ↓ 追加
war
}
...
dependencies {
...
// ↓ 追加
implementation("io.ktor:ktor-server-servlet:$ktor_version")
// ↓ 追加
compileOnly("com.google.appengine:appengine:$appengine_version")
}
// ↓ 追加
appengine {
deploy {
projectId = "GCLOUD_CONFIG"
version = "GCLOUD_CONFIG"
}
}
// ↓ 追加
tasks.named("run") {
dependsOn(":appengineRun")
}
settings.gradle.kts
...
// https://stackoverflow.com/questions/48502220/how-to-configure-appengine-gradle-plugin-using-kotlin-dsl/48510049#48510049
// ↓ 追加
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.google.cloud.tools.appengine") {
useModule("com.google.cloud.tools:appengine-gradle-plugin:${requested.version}")
}
}
}
}
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<!-- path to application.conf file, required -->
<!-- note that this file is always loaded as an absolute path from the classpath -->
<context-param>
<param-name>io.ktor.ktor.config</param-name>
<param-value>application.conf</param-value>
</context-param>
<servlet>
<display-name>KtorServlet</display-name>
<servlet-name>KtorServlet</servlet-name>
<servlet-class>io.ktor.server.servlet.ServletApplicationEngine</servlet-class>
<!-- required! -->
<async-supported>true</async-supported>
<!-- 100mb max file upload, optional -->
<multipart-config>
<max-file-size>304857600</max-file-size>
<max-request-size>304857600</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>KtorServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
src/main/webapp/WEB-INF/appengine-web.xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<threadsafe>true</threadsafe>
<runtime>java8</runtime>
</appengine-web-app>
5. ローカルで実行する
> ./gradlew appengineRun
// 止めるとき
> ./gradlew appengineStop
6. デプロイする
> ./gradlew appengineDeploy
参考
- Run a Kotlin Ktor app on App Engine standard environment (注 Groovy なのとちょっと内容が古いです。(2021/3/9現在))
- https://ktor.io/docs/war.html (web.xml の設定)
- https://ktor.io/docs/google-app-engine.html
- https://github.com/ktorio/ktor-documentation/tree/main/codeSnippets/snippets/google-appengine-standard
2021年2月12日金曜日
viewLifecycleOwnerLiveData を使って Fragment の onDestroyView() で自動で null がセットされる ViewBinding 用の property delegates を作る
ViewBinding のドキュメントでは Fragment で使う時の実装はこのようになっています。
「Fragment.viewLifecycleOwnerLiveData で LiveData<LifecycleOwner?> が取れます」で紹介した viewLifecycleOwnerLiveData を使うと、onDestroyView() で null を代入する処理を自動でやってくれる ViewBinding 用の property delegates を作ることができます。
https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c を参考に変更を加えています。
class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null
private val binding: FragmentLoginBinding
get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Fragment はそのライフサイクル中にViewが破棄されることがあるので onDestroyView() で View への参照を外しておく必要があります。
「Fragment.viewLifecycleOwnerLiveData で LiveData<LifecycleOwner?> が取れます」で紹介した viewLifecycleOwnerLiveData を使うと、onDestroyView() で null を代入する処理を自動でやってくれる ViewBinding 用の property delegates を作ることができます。
https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c を参考に変更を加えています。
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
private val viewLifecycleOwnerObserver = Observer<LifecycleOwner?> {
if (it == null) {
binding = null
}
}
private val observer = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver)
}
override fun onDestroy(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver)
fragment.lifecycle.removeObserver(this)
}
}
init {
if (fragment.lifecycle.currentState != Lifecycle.State.DESTROYED) {
fragment.lifecycle.addObserver(observer)
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = binding
if (binding != null) {
return binding
}
val view = thisRef.view
checkNotNull(view) {
"Should get bindings when the view is not null."
}
return viewBindingFactory(view).also { this.binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
これを使うと最初のコードはこうなります。
class LoginFragment : Fragment() {
private val binding by viewBinding(FragmentLoginBinding::bind)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_login, container, false)
}
}
2020年12月26日土曜日
Dispatchers.Main.immediate ってなに?
Dispatchers.Main は MainCoroutineDispatcher です。
invokeImmediately は isDispatchNeeded() で使われます。isDispatchNeeded() の実装をみると、invokeImmediately が false のときは常に isDispatchNeeded() が true を返すことがわかります。また Looper.myLooper() != handler.looper のときも isDispatchNeeded() が true を返すことがわかります。つまり、invokeImmediately が true かつ Looper.myLooper() == handler.looper のときだけ isDispatchNeeded() は false を返します。
このことから、immediate にセットされる HandlerContext では、Looper.myLooper() が handler.looper と同じだと isDispatchNeeded() が false を返すということがわかります。
isDispatchNeeded() は coroutine を dispatch メソッドで実行するべきかどうか判定するときに呼ばれます。デフォルトは true を返すようになっています。
つまり、(kotlinx-coroutines-android の) Dispatchers.Main.immediate はすでに UI スレッドにいる場合(現在の Looper.myLooper() が handler.looper と同じ場合)そのまますぐに実行される Dispatcher ということです。
例えば Dispatchers.Main を使った以下のコードだと
viewModelScope, lifecycleScope は dispatcher として Dispatchers.Main.immediate が指定されています。
package kotlinx.coroutines
...
public actual object Dispatchers {
...
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
...
}
Dispatchers.Main.immediate は MainCoroutineDispatcher に定義されており、Dispatchers.Main.immediate 自体も MainCoroutineDispatcher です。
package kotlinx.coroutines
...
public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {
...
public abstract val immediate: MainCoroutineDispatcher
...
}
kotlinx-coroutines-android では HandlerDispatcher が MainCoroutineDispatcher を継承し、
package kotlinx.coroutines.android
...
public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
...
public abstract override val immediate: HandlerDispatcher
}
HandlerContext が HandlerDispatcher を継承しています。
package kotlinx.coroutines.android
...
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
...
@Volatile
private var _immediate: HandlerContext? = if (invokeImmediately) this else null
override val immediate: HandlerContext = _immediate ?:
HandlerContext(handler, name, true).also { _immediate = it }
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return !invokeImmediately || Looper.myLooper() != handler.looper
}
...
}
HandlerContext では immediate にセットされる HandlerContext は invokeImmediately プロパティが true になる、ということがわかります。
invokeImmediately は isDispatchNeeded() で使われます。isDispatchNeeded() の実装をみると、invokeImmediately が false のときは常に isDispatchNeeded() が true を返すことがわかります。また Looper.myLooper() != handler.looper のときも isDispatchNeeded() が true を返すことがわかります。つまり、invokeImmediately が true かつ Looper.myLooper() == handler.looper のときだけ isDispatchNeeded() は false を返します。
このことから、immediate にセットされる HandlerContext では、Looper.myLooper() が handler.looper と同じだと isDispatchNeeded() が false を返すということがわかります。
isDispatchNeeded() は coroutine を dispatch メソッドで実行するべきかどうか判定するときに呼ばれます。デフォルトは true を返すようになっています。
package kotlinx.coroutines
...
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
...
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
...
}
つまり、(kotlinx-coroutines-android の) Dispatchers.Main.immediate はすでに UI スレッドにいる場合(現在の Looper.myLooper() が handler.looper と同じ場合)そのまますぐに実行される Dispatcher ということです。
例えば Dispatchers.Main を使った以下のコードだと
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.Main).launch {
println("1 : ${Thread.currentThread().name}")
}
println("2 : ${Thread.currentThread().name}")
}
}
出力は 2 が 1 より先になります。
I/System.out: 2 : main
I/System.out: 1 : main
CoroutineScope の dispatcher を Dispatchers.Main.immediate に変えると
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.Main.immediate).launch {
println("1 : ${Thread.currentThread().name}")
}
println("2 : ${Thread.currentThread().name}")
}
}
1 が先に出力されるようになります。
I/System.out: 1 : main
I/System.out: 2 : main
UI スレッドにいない場合はすぐには実行されず dispatch されます。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CoroutineScope(Dispatchers.Default).launch {
println("3 : ${Thread.currentThread().name}")
CoroutineScope(Dispatchers.Main.immediate).launch {
println("1 : ${Thread.currentThread().name}")
}
println("4 : ${Thread.currentThread().name}")
}
println("2 : ${Thread.currentThread().name}")
}
}
I/System.out: 2 : main
I/System.out: 3 : DefaultDispatcher-worker-2
I/System.out: 4 : DefaultDispatcher-worker-2
I/System.out: 1 : main
viewModelScope, lifecycleScope は dispatcher として Dispatchers.Main.immediate が指定されています。
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
...
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
...
}
}
val ViewModel.viewModelScope: CoroutineScope
get() {
...
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
2020年12月21日月曜日
Jupyter notebooks + Kotlin で移動平均を描画する(その3): 移動平均を計算する
このエントリは Fintalk Advent Calendar 2020 の21日目です。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。
移動平均(Moving Average)は簡単に言うと、時系列データで平均をとって滑らかにする方法です。
Wikipedia にはこう書いてあります。
移動平均()は、時系列データ(より一般的には時系列に限らず系列データ)を平滑化する手法である。音声や画像等のデジタル信号処理に留まらず、金融(特にテクニカル分析)分野、気象、水象を含む計測分野等、広い技術分野で使われる。有限インパルス応答に対するローパスフィルタ(デジタルフィルタ)の一種であり、分野によっては移動積分とも呼ばれる。
主要なものは、単純移動平均と加重移動平均と指数移動平均の3種類である。普通、移動平均といえば、単純移動平均のことをいう。
by https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87
ここに書いてあるとおり、主なものとして
それぞれ計算してみましょう。
前回CSVデータを操作して得られた日本の新規感染者の最初の20日間のデータを使ってみます。
例えば 01-26 の単純移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)を足して4で割るので 0.5 です。
01-26 の単純移動平均 = (01-23 + 01-24 + 01-25 + 01-26) / 4 = (0 + 0 + 0 + 2) / 4 = 0.5
01-27 の単純移動平均を計算するとき、01-24 〜 01-27 までのデータ(0,0,2,0)を足して4で割ってもいいのですが、すでに01-26 の単純移動平均が計算してあるなら
01-27 の単純移動平均
= (01-24 + 01-25 + 01-26 + 01-27) / 4
= (- 01-23 + (01-23 + 01-24 + 01-25 + 01-26) + 01-27) / 4
= - 01-23 / 4 + (01-23 + 01-24 + 01-25 + 01-26) / 4 + 01-27 / 4
= - 01-23 / 4 + 01-26 の単純移動平均 + 01-27 / 4
= 01-26 の単純移動平均 - 01-23 / 4 + 01-27 / 4
このように、01-26 の単純移動平均から古い値(01-23)を4で割った数を引いて、新しい値(01-27)を4で割った値を足せば求めることができます。
01-23 〜 01-25 の単純移動平均は4日間分のデータがないので計算できません。
例えば 01-26 の加重移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)から次のように計算します。
現在に最も近い日の 01-26 のデータには 4 を掛けます。次に近い日の 01-25 のデータには 4 から 1 を引いた値を掛けます。その前の日は 4-2、その前の日は 4-3 を掛けます。
それを 4 + 3 + 2 + 1 = 10 で割ります。
01-26 の加重移動平均 = (01-23 * (4-3) + 01-24* (4-2) + 01-25 * (4-1) + 01-26 * (4-0)) / (4 + 3 + 2 + 1)
= (0 * 1 + 0 * 2 + 0 * 3 + 2 * 4) / 10 = 0.8
= (0 + 0 + 0 + 8) / 10 = 0.8
01-27 の加重移動平均には、単純移動平均と同じように 01-26 の加重移動平均を利用します。
01-27 の加重移動平均
= (01-24 * (4-3) + 01-25* (4-2) + 01-26 * (4-1) + 01-27 * (4-0)) / 10
= (- 01-23 - 01-24 - 01-25 - 01-26 + (01-23 * (4-3) + 01-24 * (4-2) + 01-25* (4-1) + 01-26 * (4-0)) + 01-27 * (4-0)) / 10
= (- (01-23 ~ 01-26の総和)/10) + 01-26 の加重移動平均 + 01-27 * 4 / 10
= 01-26 の加重移動平均 - (01-23 ~ 01-26の総和) / 10 + 01-27 * 4 / 10
このように、01-26 の加重移動平均に、01-23 ~ 01-26の総和を10で割った数を引いて、新しい値(01-27)に4を掛けて10で割った値を足せば求めることができます。
01-23 〜 01-25 の加重移動平均は4日間分のデータがないので計算できません。
重みの減少度合いは平滑化係数と呼ばれる0~1の間の値をとる定数 α で決まり、αを時系列区間 N で表した場合 α = 2 / (N+1) となります。
最初の値での EMA は定義しません。2番目の値での EMA をどう設定するかにはいくつかの手法があるそうですが、ここでは単純に2番目の値とします。
3番目以降の場合の EMA の計算式はこうなります。
EMA_t = α * value_t + (1 - α) * EMA_(t-1)
α = 2 / (N + 1) = 2 / (4 + 1) = 0.4 として計算すると
01-24 の指数移動平均 : 0
01-25 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0 = 0
01-26 の指数移動平均 : 0.4 * 2 + (1 - 0.4) * 0 = 0.8
01-27 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0.8 = 0.48
移動平均は自分で実際に計算してグラフにするとよくわかると思うので、Kotlin じゃなくても、好きな言語でぜひやってみてください。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。
- Jupyter notebooks + Kotlin で移動平均を描画する(その1): lets-plot-kotlin で線を描画する
- Jupyter notebooks + Kotlin で移動平均を描画する(その2): krangl で csv データを操作する
- Jupyter notebooks + Kotlin で移動平均を描画する(その3): 移動平均を計算する
移動平均(Moving Average)は簡単に言うと、時系列データで平均をとって滑らかにする方法です。
Wikipedia にはこう書いてあります。
移動平均()は、時系列データ(より一般的には時系列に限らず系列データ)を平滑化する手法である。音声や画像等のデジタル信号処理に留まらず、金融(特にテクニカル分析)分野、気象、水象を含む計測分野等、広い技術分野で使われる。有限インパルス応答に対するローパスフィルタ(デジタルフィルタ)の一種であり、分野によっては移動積分とも呼ばれる。
主要なものは、単純移動平均と加重移動平均と指数移動平均の3種類である。普通、移動平均といえば、単純移動平均のことをいう。
by https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87
ここに書いてあるとおり、主なものとして
- 単純移動平均
- 加重移動平均
- 指数移動平均
それぞれ計算してみましょう。
前回CSVデータを操作して得られた日本の新規感染者の最初の20日間のデータを使ってみます。
A DataFrame: 318 x 2
date new_cases_double
1 2020-01-23 0
2 2020-01-24 0
3 2020-01-25 0
4 2020-01-26 2
5 2020-01-27 0
6 2020-01-28 3
7 2020-01-29 0
8 2020-01-30 4
9 2020-01-31 4
10 2020-02-01 5
11 2020-02-02 0
12 2020-02-03 0
13 2020-02-04 2
14 2020-02-05 1
15 2020-02-06 0
16 2020-02-07 0
17 2020-02-08 1
18 2020-02-09 0
19 2020-02-10 2
20 2020-02-11 1
わかりやすいように4日間の移動平均(4日移動平均)を計算してみます。
単純移動平均(Simple Moving Average)
単純移動平均は値をそのまま足して平均を計算する方法です。例えば 01-26 の単純移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)を足して4で割るので 0.5 です。
01-26 の単純移動平均 = (01-23 + 01-24 + 01-25 + 01-26) / 4 = (0 + 0 + 0 + 2) / 4 = 0.5
01-27 の単純移動平均を計算するとき、01-24 〜 01-27 までのデータ(0,0,2,0)を足して4で割ってもいいのですが、すでに01-26 の単純移動平均が計算してあるなら
01-27 の単純移動平均
= (01-24 + 01-25 + 01-26 + 01-27) / 4
= (- 01-23 + (01-23 + 01-24 + 01-25 + 01-26) + 01-27) / 4
= - 01-23 / 4 + (01-23 + 01-24 + 01-25 + 01-26) / 4 + 01-27 / 4
= - 01-23 / 4 + 01-26 の単純移動平均 + 01-27 / 4
= 01-26 の単純移動平均 - 01-23 / 4 + 01-27 / 4
このように、01-26 の単純移動平均から古い値(01-23)を4で割った数を引いて、新しい値(01-27)を4で割った値を足せば求めることができます。
01-23 〜 01-25 の単純移動平均は4日間分のデータがないので計算できません。
A DataFrame: 318 x 2
date new_cases_double simple_moving_average
1 2020-01-23 0
2 2020-01-24 0
3 2020-01-25 0
4 2020-01-26 2 0.50
5 2020-01-27 0 0.50
6 2020-01-28 3 1.25
7 2020-01-29 0 1.25
8 2020-01-30 4 1.75
9 2020-01-31 4 2.75
10 2020-02-01 5 3.25
11 2020-02-02 0 3.25
12 2020-02-03 0 2.25
13 2020-02-04 2 1.75
14 2020-02-05 1 0.75
15 2020-02-06 0 0.75
16 2020-02-07 0 0.75
17 2020-02-08 1 0.50
18 2020-02-09 0 0.25
19 2020-02-10 2 0.75
20 2020-02-11 1 1.00
val newCases3 = newCases2.head(20)
val values = newCases3["new_cases_double"].asDoubles()
val size = values.size
var sma = arrayOfNulls<Double?>(size)
val n = 4
// calculate 01-26
var sum = 0.0
for(i in 0 until n) {
sum += values[i]!!
}
sma[n-1] = sum/n
// calculate 01-27 ~
for(i in n until values.size) {
sma[i] = sma[i-1]!! - values[i-n]!!/n + values[i]!!/n
}
val newCases4 = newCases3.addColumn("simple_moving_average") { sma }
赤の単純移動平均の線が滑らかになっているのがわかります。
加重移動平均(Weighted Moving Average)
加重移動平均は各値に重みをつけたものを足して平均を計算する方法です。例えば線形加重移動平均(Linear Weighted Moving Average)だと、現在に最も近い日の重みが一番大きくなり、そこから過去に行くほど線形に(一定量ずつ)重みが減っていきます。例えば 01-26 の加重移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)から次のように計算します。
現在に最も近い日の 01-26 のデータには 4 を掛けます。次に近い日の 01-25 のデータには 4 から 1 を引いた値を掛けます。その前の日は 4-2、その前の日は 4-3 を掛けます。
それを 4 + 3 + 2 + 1 = 10 で割ります。
01-26 の加重移動平均 = (01-23 * (4-3) + 01-24* (4-2) + 01-25 * (4-1) + 01-26 * (4-0)) / (4 + 3 + 2 + 1)
= (0 * 1 + 0 * 2 + 0 * 3 + 2 * 4) / 10 = 0.8
= (0 + 0 + 0 + 8) / 10 = 0.8
01-27 の加重移動平均には、単純移動平均と同じように 01-26 の加重移動平均を利用します。
01-27 の加重移動平均
= (01-24 * (4-3) + 01-25* (4-2) + 01-26 * (4-1) + 01-27 * (4-0)) / 10
= (- 01-23 - 01-24 - 01-25 - 01-26 + (01-23 * (4-3) + 01-24 * (4-2) + 01-25* (4-1) + 01-26 * (4-0)) + 01-27 * (4-0)) / 10
= (- (01-23 ~ 01-26の総和)/10) + 01-26 の加重移動平均 + 01-27 * 4 / 10
= 01-26 の加重移動平均 - (01-23 ~ 01-26の総和) / 10 + 01-27 * 4 / 10
このように、01-26 の加重移動平均に、01-23 ~ 01-26の総和を10で割った数を引いて、新しい値(01-27)に4を掛けて10で割った値を足せば求めることができます。
01-23 〜 01-25 の加重移動平均は4日間分のデータがないので計算できません。
val newCases3 = newCases2.head(20)
val values = newCases3["new_cases_double"].asDoubles()
val size = values.size
var wma = arrayOfNulls<Double?>(size)
var sums = arrayOfNulls<Double?>(size)
val n = 4
val n2 = n*(n + 1)/2
// calculate 01-26
var sum = 0.0
var sum_wma = 0.0
for(i in 0 until n) {
sum += values[i]!!
sum_wma += values[i]!! * (i + 1)
}
sums[n-1] = sum
wma[n-1] = sum_wma/n2
// calculate 01-27 ~
for(i in n until values.size) {
sums[i] = sums[i-1]!! - values[i-n]!! + values[i]!!
wma[i] = wma[i-1]!! - sums[i-1]!!/n2 + n * values[i]!!/n2
}
val newCases5 = newCases4.addColumn("weighted_moving_average") { wma }
赤が単純移動平均、緑が加重移動平均です。
指数移動平均(Exponential Moving Average)
指数移動平均は加重移動平均のように各値に重みをつけたものを足して平均を計算する方法です。各値につける重みが指数関数的に減っていきます。重みの減少度合いは平滑化係数と呼ばれる0~1の間の値をとる定数 α で決まり、αを時系列区間 N で表した場合 α = 2 / (N+1) となります。
最初の値での EMA は定義しません。2番目の値での EMA をどう設定するかにはいくつかの手法があるそうですが、ここでは単純に2番目の値とします。
3番目以降の場合の EMA の計算式はこうなります。
EMA_t = α * value_t + (1 - α) * EMA_(t-1)
α = 2 / (N + 1) = 2 / (4 + 1) = 0.4 として計算すると
01-24 の指数移動平均 : 0
01-25 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0 = 0
01-26 の指数移動平均 : 0.4 * 2 + (1 - 0.4) * 0 = 0.8
01-27 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0.8 = 0.48
val newCases3 = newCases2.head(20)
val values = newCases3["new_cases_double"].asDoubles()
val size = values.size
var ema = arrayOfNulls<Double?>(size)
var sums = arrayOfNulls<Double?>(size)
val n = 4
val alpha = 2.0 / (n + 1)
ema[0] = null
ema[1] = values[1]
// calculate 01-25 ~
for(i in 2 until values.size) {
ema[i] = alpha * values[i]!! + (1 - alpha) * ema[i - 1]!!
}
val newCases6 = newCases5.addColumn("exponential_moving_average") { ema }
赤が単純移動平均、緑が加重移動平均、灰色が指数移動平均です。
移動平均は自分で実際に計算してグラフにするとよくわかると思うので、Kotlin じゃなくても、好きな言語でぜひやってみてください。
登録:
コメント (Atom)




















































