2021年3月29日月曜日

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

  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         val editText = findViewById<EditText>(R.id.editText)  
  8.         editText.filters = editText.filters + MyInputFilter()  
  9.     }  
  10. }  
  11.   
  12. class MyInputFilter : InputFilter {  
  13.   
  14.     override fun filter(  
  15.         source: CharSequence,  
  16.         start: Int,  
  17.         end: Int,  
  18.         dest: Spanned,  
  19.         dstart: Int,  
  20.         dend: Int  
  21.     ): CharSequence? {  
  22.   
  23.         var i: Int = start  
  24.         while (i < end) {  
  25.             if (source[i] == '\n') {  
  26.                 break  
  27.             }  
  28.             i++  
  29.         }  
  30.   
  31.         if (i == end) {  
  32.             return null  
  33.         }  
  34.   
  35.         val filtered = SpannableStringBuilder(source, start, end)  
  36.         val start2 = i - start  
  37.         val end2 = end - start  
  38.   
  39.         for (j in end2 - 1 downTo start2) {  
  40.             if (source[j] == '\n') {  
  41.                 filtered.delete(j, j + 1)  
  42.             }  
  43.         }  
  44.   
  45.         return filtered  
  46.     }  
  47. }  

2021年3月23日火曜日

Kotlin Serialization は sealed class も対応していて便利

Kotlin Serialization

  1. plugins {  
  2.     ...  
  3.     id "org.jetbrains.kotlin.plugin.serialization" version "1.4.31"  
  4. }  
  5.   
  6. dependencies {  
  7.     ...  
  8.     implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"  
  9. }  
  1. @Serializable  
  2. data class Dog(val name: String, val age: Int, val sex: Sex, val kind: Kind)  
  3.   
  4. enum class Sex {  
  5.     MALE,  
  6.     FEMALE  
  7. }  
  8.   
  9. @Serializable  
  10. sealed class Kind {  
  11.   
  12.     @Serializable  
  13.     object Hybrid : Kind()  
  14.   
  15.     @Serializable  
  16.     data class PureBlood(val name: String) : Kind()  
  17. }  
  1. class DogTest {  
  2.   
  3.     @Test  
  4.     fun list() {  
  5.         val dogs = listOf(  
  6.             Dog("White"10, Sex.MALE, Kind.Hybrid),  
  7.             Dog("Black"20, Sex.FEMALE, Kind.PureBlood("Husky"))  
  8.         )  
  9.   
  10.         val json = Json.encodeToString(dogs)  
  11.         println(json)  
  12.         // [{"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"}}]  
  13.   
  14.   
  15.         val decoded = Json.decodeFromString<List<Dog>>(json)  
  16.   
  17.         assertThat(decoded).isEqualTo(dogs)  
  18.     }  
  19. }  

2021年3月9日火曜日

AppEngine に Ktor アプリをデプロイする

1. Google Cloud SDK をインストールする

https://cloud.google.com/sdk/docs/install

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

  1. > gcloud init  

3. Ktor アプリを作る

IntelliJ IDEA に Ktor plugin を入れて、New Project wizard から Ktor プロジェクトを作る

4. AppEngine の設定を追加する

build.gradle.kts
  1. ...  
  2.   
  3. plugins {  
  4.     ...  
  5.     // ↓ 追加  
  6.     id("com.google.cloud.tools.appengine") version "2.2.0"  
  7.     // ↓ 追加  
  8.     war  
  9. }  
  10.   
  11. ...  
  12.   
  13. dependencies {  
  14.     ...  
  15.   
  16.     // ↓ 追加  
  17.     implementation("io.ktor:ktor-server-servlet:$ktor_version")  
  18.     // ↓ 追加  
  19.     compileOnly("com.google.appengine:appengine:$appengine_version")  
  20. }  
  21.   
  22. // ↓ 追加  
  23. appengine {  
  24.     deploy {  
  25.         projectId = "GCLOUD_CONFIG"  
  26.         version = "GCLOUD_CONFIG"  
  27.     }  
  28. }  
  29.   
  30. // ↓ 追加  
  31. tasks.named("run") {  
  32.     dependsOn(":appengineRun")  
  33. }  
settings.gradle.kts
  1. ...  
  2.   
  3. // https://stackoverflow.com/questions/48502220/how-to-configure-appengine-gradle-plugin-using-kotlin-dsl/48510049#48510049  
  4. // ↓ 追加  
  5. pluginManagement {  
  6.     repositories {  
  7.         gradlePluginPortal()  
  8.         google()  
  9.     }  
  10.     resolutionStrategy {  
  11.         eachPlugin {  
  12.             if (requested.id.id == "com.google.cloud.tools.appengine") {  
  13.                 useModule("com.google.cloud.tools:appengine-gradle-plugin:${requested.version}")  
  14.             }  
  15.         }  
  16.     }  
  17. }  
src/main/webapp/WEB-INF/web.xml
  1. <?xml version="1.0" encoding="ISO-8859-1" ?>  
  2. <web-app xmlns="http://java.sun.com/xml/ns/javaee"  
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"  
  5.          version="3.0">  
  6.     <!-- path to application.conf file, required -->  
  7.     <!-- note that this file is always loaded as an absolute path from the classpath -->  
  8.     <context-param>  
  9.         <param-name>io.ktor.ktor.config</param-name>  
  10.         <param-value>application.conf</param-value>  
  11.     </context-param>  
  12.   
  13.     <servlet>  
  14.         <display-name>KtorServlet</display-name>  
  15.         <servlet-name>KtorServlet</servlet-name>  
  16.         <servlet-class>io.ktor.server.servlet.ServletApplicationEngine</servlet-class>  
  17.   
  18.         <!-- required! -->  
  19.         <async-supported>true</async-supported>  
  20.   
  21.         <!-- 100mb max file upload, optional -->  
  22.         <multipart-config>  
  23.             <max-file-size>304857600</max-file-size>  
  24.             <max-request-size>304857600</max-request-size>  
  25.             <file-size-threshold>0</file-size-threshold>  
  26.         </multipart-config>  
  27.     </servlet>  
  28.   
  29.     <servlet-mapping>  
  30.         <servlet-name>KtorServlet</servlet-name>  
  31.         <url-pattern>/</url-pattern>  
  32.     </servlet-mapping>  
  33.   
  34. </web-app>  
src/main/webapp/WEB-INF/appengine-web.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">  
  3.     <threadsafe>true</threadsafe>  
  4.     <runtime>java8</runtime>  
  5. </appengine-web-app>  

5. ローカルで実行する

  1. > ./gradlew appengineRun  
  2.     
  3. // 止めるとき  
  4. > ./gradlew appengineStop  

6. デプロイする

  1. > ./gradlew appengineDeploy  



参考

2021年3月5日金曜日

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

最初 index : 0

タブクリックで index : 15 のページに移動

タブクリックで index : 4 のページに移動


したときの Fragment のライフサイクルの結果は次のようになります。
  1. : onCreate : 0  
  2. : onCreate : 12  
  3. : onCreate : 13  
  4. : onCreate : 14  
  5. : onCreate : 15  
  6. : onDestroy : 0  
  7. : onDestroy : 12  
  8. : onCreate : 7  
  9. : onDestroy : 13  
  10. : onCreate : 6  
  11. : onCreate : 5  
  12. : onDestroy : 14  
  13. : onCreate : 4  
  14. : onDestroy : 15  
  15. : onDestroy : 7  
index : 0 から index : 15 のページに移動するとき、index : 12 のページから作られていることがわかります。
同じように index : 15 から index : 4 のページに移動するときは index : 7 のページから作られていることがわかります。


この 3 ページ前からというロジックは ViewPager2 の setCurrentItemInternal() に実装されています。
  1. void setCurrentItemInternal(int item, boolean smoothScroll) {  
  2.   
  3.     ...  
  4.   
  5.     // For smooth scroll, pre-jump to nearby item for long jumps.  
  6.     if (Math.abs(item - previousItem) > 3) {  
  7.         mRecyclerView.scrollToPosition(item > previousItem ? item - 3 : item + 3);  
  8.         // TODO(b/114361680): call smoothScrollToPosition synchronously (blocked by b/114019007)  
  9.         mRecyclerView.post(new SmoothScrollToPosition(item, mRecyclerView));  
  10.     } else {  
  11.         mRecyclerView.smoothScrollToPosition(item);  
  12.     }  
  13. }  




2021年3月4日木曜日

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

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

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

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