2021年3月29日月曜日

改行を入力させない EditText 用 InputFilter

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月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/install

2. 認証 & プロジェクト選択

> 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


参考

2021年3月5日金曜日

ViewPager2 で 遠くのページに smoothScroll するときは 3ページ前から作られる

最初 index : 0

タブクリックで 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 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); } }



2021年3月4日木曜日

ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではない

ViewPager2 にも ViewPager と同様 offscreenPageLimit を指定することができます。

ViewPager2 の offscreenPageLimit のデフォルト値は 1 ではなく、ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT (= -1) です。

そのため FragmentStateAdapter を使っている場合、デフォルトでは隣の画面の Fragment はスワイプなどで表示が必要になるタイミングまで作成されません。
ViewPager と同じ挙動にするには、明示的に offscreenPageLimit に 1 を指定する必要があります。 val pager: ViewPager2 = ... pager.offscreenPageLimit = 1