- class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- val editText = findViewById<EditText>(R.id.editText)
- editText.filters = editText.filters + MyInputFilter()
- }
- }
- class MyInputFilter : InputFilter {
- override fun filter(
- source: CharSequence,
- start: Int,
- end: Int,
- dest: Spanned,
- dstart: Int,
- dend: Int
- ): CharSequence? {
- var i: Int = start
- while (i < end) {
- if (source[i] == '\n') {
- break
- }
- i++
- }
- if (i == end) {
- return null
- }
- val filtered = SpannableStringBuilder(source, start, end)
- val start2 = i - start
- val end2 = end - start
- for (j in end2 - 1 downTo start2) {
- if (source[j] == '\n') {
- filtered.delete(j, j + 1)
- }
- }
- return filtered
- }
- }
2021年3月29日月曜日
改行を入力させない EditText 用 InputFilter
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"
- }
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()
- }
@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)
- }
- }
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
> 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")
- }
...
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}")
- }
- }
- }
- }
...
// 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>
<?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>
<?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
> ./gradlew appengineRun
// 止めるとき
> ./gradlew appengineStop
6. デプロイする
- > ./gradlew appengineDeploy
> ./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年3月5日金曜日
ViewPager2 で 遠くのページに smoothScroll するときは 3ページ前から作られる
最初 index : 0
↓
タブクリックで index : 15 のページに移動
↓
タブクリックで index : 4 のページに移動
したときの Fragment のライフサイクルの結果は次のようになります。
同じように index : 15 から index : 4 のページに移動するときは index : 7 のページから作られていることがわかります。
この 3 ページ前からというロジックは ViewPager2 の setCurrentItemInternal() に実装されています。
↓
タブクリックで index : 15 のページに移動
↓
タブクリックで index : 4 のページに移動
したときの Fragment のライフサイクルの結果は次のようになります。
- : onCreate : 0
- : onCreate : 12
- : onCreate : 13
- : onCreate : 14
- : onCreate : 15
- : onDestroy : 0
- : onDestroy : 12
- : onCreate : 7
- : onDestroy : 13
- : onCreate : 6
- : onCreate : 5
- : onDestroy : 14
- : onCreate : 4
- : onDestroy : 15
- : onDestroy : 7
: onCreate : 0
: onCreate : 12
: onCreate : 13
: onCreate : 14
: onCreate : 15
: onDestroy : 0
: onDestroy : 12
: onCreate : 7
: onDestroy : 13
: onCreate : 6
: onCreate : 5
: onDestroy : 14
: onCreate : 4
: onDestroy : 15
: onDestroy : 7
index : 0 から index : 15 のページに移動するとき、index : 12 のページから作られていることがわかります。
同じように index : 15 から index : 4 のページに移動するときは index : 7 のページから作られていることがわかります。
この 3 ページ前からというロジックは ViewPager2 の setCurrentItemInternal() に実装されています。
- void setCurrentItemInternal(int item, boolean smoothScroll) {
- ...
- // For smooth scroll, pre-jump to nearby item for long jumps.
- if (Math.abs(item - previousItem) > 3) {
- mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);
- // TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007)
- mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));
- } else {
- mRecyclerView.smoothScrollToPosition(item);
- }
- }
void setCurrentItemInternal(int item, boolean smoothScroll) {
...
// For smooth scroll, pre-jump to nearby item for long jumps.
if (Math.abs(item - previousItem) > 3) {
mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);
// TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007)
mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));
} else {
mRecyclerView.smoothScrollToPosition(item);
}
}
2021年3月4日木曜日
ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではない
ViewPager2 にも ViewPager と同様 offscreenPageLimit を指定することができます。
ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではなく、ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT (= -1) です。
そのため FragmentStateAdapter を使っている場合、デフォルトでは隣の画面の Fragment はスワイプなどで表示が必要になるタイミングまで作成されません。
ViewPager と同じ挙動にするには、明示的に offscreenPageLimit に 1 を指定する必要があります。
ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではなく、ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT (= -1) です。
そのため FragmentStateAdapter を使っている場合、デフォルトでは隣の画面の Fragment はスワイプなどで表示が必要になるタイミングまで作成されません。
ViewPager と同じ挙動にするには、明示的に offscreenPageLimit に 1 を指定する必要があります。
- val pager: ViewPager2 = ...
- pager.offscreenPageLimit = 1
val pager: ViewPager2 = ...
pager.offscreenPageLimit = 1
登録:
投稿 (Atom)