かこーサーコイ

なんでもいいから書いていこう

EditTextからカーソルが外れたらキーボードを隠す

EditTextってキーボードが隠れないですよね。 普通、以下のような動作に時にはキーボードが自動で隠れてほしいものです。

  • エンターを押した  - 何ならアクションもしたい
  • EditTextからカーソルが外れた
  • 画面から離れた

そんな時のために以下のようなクラスを作りました。

クラス

class EditTextHelper(val view: EditText) {

    init {
        setOffCursolHideKeyboard()
    }

    fun setOnKeyListener(lister: (v: View, keyCode: Int, event: KeyEvent) -> Unit) {
        view.setOnKeyListener { v, keyCode, event ->
            lister(v, keyCode, event)
            if (isEnterAction(keyCode, event)) offCursolHideKeyboard()
            false
        }
    }

    // キーボードでエンターキー押した時
    fun isEnterAction(keyCode: Int, event: KeyEvent): Boolean = (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP)

    // キーボードを閉じる
    fun offCursolHideKeyboard() {
        val inputMethodManager: InputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(view.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
    }

    // カーソルが外れた時
    fun setOffCursolHideKeyboard() {
        view.setOnFocusChangeListener { v, hasFocus ->
            if (!hasFocus) offCursolHideKeyboard()
        }
    }
}

使い方

val editTextHelper = EditTextHelper(R.id.editText)
// エンター押した時にアクションを起こしたい場合
editTextHelper.setOnKeyListener { v, keyCode, event ->
    if (editTextHelper.isEnterAction(keyCode, event)) {
        Log.d("editTextHelper", "isEnterAction")
    }
    false
}

これでキーボードが自動で隠れてくれます。

アプリ内課金勉強会in-Tamachi-Billing-Night参加記録

in-Tamachi-Billing-Night

先日、マネーフォワード社で開かれたin-Tamachi-Billing-Nightに行ってきました。

billing-night.connpass.com

アプリ内課金の勉強会で、iOSAndroid、サーバーサイドそれぞれの知見が集まってました。

Androidのアプリ内課金をAACで実装する

Androidのアプリ内課金の話。マネーフォワードで年額プランがリリースし、その時の知見でした。

実装のみならず定期講読のプラン変更した場合の課金支払いタイミングの説明もありました。

実装については、Google Play Billing LibraryをViewModelにimplementして実装した例が紹介されてました。

ビルダーを生成して用いる感じがOKHttpぽさあるなと思い、課金回りの処理をリポジトリパターンにしても良さそうだなと思っていたけど、実際どうなのか...?

最後に知見をどんどん公開しましょうと想いを話されていて、ホントにその通りだよなと。上記のリポジトリパターン試す機会があれば公開したいですね。

クロスグレードの実装とつらみの話

iOSのアプリ内課金の話。こちらもマネーフォワードの年額プランでの知見でした。

プラン変更の購入トランザクションが失敗して、しかもエラー内容が「Cannot connect to iTunesStore.」

これはつらそう。

結局AppStoreの登録管理での変更を案内する形になったそうです。

プロい。

ただ、登録管理はアプリを未インストールでも操作可能らしく(なんだそりゃ)、Status Update Notificationを採用したとのこと。

登録管理はSandbox環境ないのでそれもつらみポイントのようです。

iOSしんどそう。。

運用から学ぶPlay Billing Library

実際にアプリ内課金を実装するときに真似したいポイントがたくさんでした。

  • BillingClientをラップしてコールバックの深くなりすぎを回避
  • リポジトリ層内のリモート層でそのクラスを使う
  • interfaceはrxJavaのsingleかcompletable を返す
  • 課金画面は透過したActivityにする
  • さまざまなところから呼び出せる
  • デバッグメニューを通知欄に表示

また、クライアントだけでは解決できない問題として、

支払い猶予期間の判定

が紹介されており、それはサーバーサイドで、

Purchases.subscripthioAPI

で判定したと説明ありました。

Promoting IAP対応から学ぶ外部アプリ内課金実装

サーバーサイドの話でした。

PromotingAPI によるアプリ内課金のポイントが紹介されてました。

shouldAddStorePaymentでreturn trueを返すとすぐ課金が始まってしまう件はなるほどと思いました。

利用規約画面はさむのありますよね。

最後の方に紹介されていたGoogle Play Points は気になってます。 今度試せたらいいな。

Androidアプリ内課金実装ポイント

レシートが残る残らないの観点と、不正対策がタメになりました。

  • 購入後にGoogleアカウント消すと410になる

    • invokeAPIで投げて無料会員にする
  • 複数アカウントで有料会員になれる

    • linkedPurchaseTokenで検証

GoogleのSandbox課金を取得してきた歴史

サーバーサイドの話。 Androidのsandbox環境でorderIdが消えたり復活したり……

現在のsandbox課金を取得する方法は、

  • Purchases Product API
    • PurchasesType = 0ならSandBox
  • 課金キャンセルも取れるAPI

とのことです。

予告なくorderIdが消えることあるので、公式ドキュメントを追いましょう!と締めくくりでした。 予告ないの辛い……


全体を通してアプリ内課金はなんだかAndroidの方がやり易い印象でした。 アプリ内課金実装したいなー

ChipのTextSizeを変更する

最近Chipを使ったのですが、TextSizeの変更がxmlから出来なかったのでTextSizeの変更方法書きます。chip textsize not working

ちなみにChipとはこれのことです。

material.io

デフォルトだと18spなのでちょっとでかいんデスヨネ。

動的に変える

fragment内想定です。

private val chip = view.findViewById<Chip>(R.id.chip)
chip.textSize = context.resource.getDimension(R.dimen.text_chip)
<dimen name="text_chip">12sp</dimen>

R.id.chipR.dimen.text_chip は環境にあわせて変更してください。

Android Architecture Components 勉強会 #5 【個人的なまとめポイント】

GDG Tokyo 主催のAndroid Architecture Components 勉強会 #5に参加しました。

gdg-tokyo.connpass.com

資料

個人的なまとめポイント

普段から仕事でもプライベートでもLiveData,ViewModelは使用しており、LifeCycleはそこまで活用しきれてない状況での参加でした。 今回初めて知ったこともあり「へ〜」と思ったことを中心にまとめます。

LifeCycle

LifecycleObserver

  • @OnLifecycleEventをつけると指定したイベントが発行された時に呼ばれる
val observer = object : LifecycleObserver {
     @OnLifecycleEvent(Lifecycle.Event.ON_START)
      fun onStart(source: LifecycleOwner) {
             println("onStart : ${source.lifecycle.currentState.name}")
      }
}

ProcessLifecycleOwner

  • ProcessLifecycleOwner を使えば、アプリのフォアグラウンド/バックグラウンドが簡単に判定できる(便利)
ProcessLifecycleOwner.get()
            .lifecycle
            .addObserver(lifecycleObserver)

LiveData

active/inactive

  • active

    • アクティブなObserverが1つ以上の時呼ばれる
  • inactive

    • アクティブなObserverが1つ未満の時呼ばれる

MediatorLiveData

  • 複数のLiveDataを重ねて管理できる

Transformation

  • MediatorLiveDataを使いやすくしたユースリティ
  • 中でMediatorLiveDataを呼んでいる(知らなかった。。)

ViewModel

  • プロセス停止後は復旧できない
    • ViewModeはプロセス停止後は復旧できない。 アプリをバックグラウンドしてゲームなどやっている間にアプリがプロセス終了した後に復帰しても戻らない
    • ↑これをどうにかしたいって開発がalphaだけど始まっている
    • Saved State module for ViewModel : https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate

インスタンス

  • ViewModelProvidersを使う

kotlin val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

普段はDIライブラリを使うことがほとんどなのでこの書き方は実際には使わないかも。 Koinを使用しているので

val viewModel : MainViewModel by viewModel()
val shareViewModel : ShareViewModel by shareViewModel()

って書いています。おすすめ。

KTX-LiveData

  • いままでktxはviewmodelだけだったが、LiveDataも登場した。(alpha)

ViewModel.viewModelScope

  • CoroutineScopeをサポートする

課題

結構多かったですが、課題15まではときました。 課題16は間に合わなかったですが、あとでトライしてみます。 後ほどgithubにでもあげるかも。

RecyclerViewをEpoxyで楽に実装する

RecyclewViewってよく使うのですが、ボイラープレート多いなーって思うことがあります。 Epoxyを使ってみたら最高だったので紹介します。

ゴール

実際に以下のようなサンプルアプリを作成するつもりでEpoxyの使い方を紹介します。

epoxy sample アプリ
epoxy samle

ソースコード

全体のソースコードはこちらに上げております。

github.com

Epoxy

Airbnb製のRercyclerViewライブラリです。

github.com

インストール

Readmeの通りに進めれば問題ありません。

appのgradleに以下を追記します。現時点での最新バージョン3.3.1を指定します。 recyclerviewも入れておきます。

...

dependencies {
    ...
    // epoxy
    def epoxy_version = '3.3.1'
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
    implementation "com.airbnb.android:epoxy-databinding:$epoxy_version"

    // recyclerview
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

今回、Kotlinでの使用と合わせてDataBindingも使いたいので以下の追記もします。

apply plugin: 'kotlin-kapt'  // kotlinでAnnotation Processingを有効にする

kapt {
    correctErrorTypes = true
}

android {
    ...
    // dataBindingを有効にする
    dataBinding {
        enabled = true
    }
}

追記したら Gradle Sync しておきます。 gradleを編集するとAndroidStudio上部に「Sync Now」って出てくると思うのでそれをクリックすれば良いです。

リスト用のFragment作成

New > Fragment > Fragment (Blank) でFragmentを作ります。

設定は以下のようにします。

  • Fragment Name : AACListFragment
  • Create layout XML?: チェックつける
  • Fragment Layout Name: fragment_aaclist

以下のチェックは外しておきましょう。

  • Include fragment factory methods?
  • Include intarface callback?

f:id:h3-birth:20190324204902p:plain
newfragment

データ用のdata class作成

データ用のdata classを作成します。

data class AACItem(
    val name: String,
    val description: String,
    val url: String
)
data class AACList(
    val aacItems: List<AACItem>
) {
    companion object {
        // ref. https://developer.android.com/topic/libraries/architecture
        private val aacList = listOf(
            AACItem("LiveData", "Use LiveData to build data objects that notify views when the underlying database changes.", "https://developer.android.com/topic/libraries/architecture/livedata"),
            AACItem("ViewModel", "Stores UI-related data that isn't destroyed on app rotations.", "https://developer.android.com/topic/libraries/architecture/viewmodel"),
            AACItem("Room", "Room is an a SQLite object mapping library. Use it to Avoid boilerplate code and easily convert SQLite table data to Java objects.", "https://developer.android.com/topic/libraries/architecture/room"),
            AACItem("DataBinding", "The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically.", "https://developer.android.com/topic/libraries/data-binding"),
            AACItem("Handling Lifecycles", "Lifecycle-aware components perform actions in response to a change in the lifecycle status of another component, such as activities and fragments.", "https://developer.android.com/topic/libraries/architecture/lifecycle"),
            AACItem("Paging library", "The Paging Library helps you load and display small chunks of data at a time. Loading partial data on demand reduces usage of network bandwidth and system resources." , "https://developer.android.com/topic/libraries/architecture/paging"),
            AACItem("WorkManager", "The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.", "https://developer.android.com/topic/libraries/architecture/workmanager")
        )
        fun getList() = AACList(aacList)
    }
}

Values XML 編集

dimens.xml

values内にdimes.xmlを作成します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- text -->
    <dimen name="text_l">18sp</dimen>
    <dimen name="text_s">10sp</dimen>
    <!-- space -->
    <dimen name="space_m">8dp</dimen>
    <dimen name="space_l">16dp</dimen>
</resources>

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    ...
    <color name="colorAACName">#212121</color> <!-- 追加 -->
    <color name="colorAACUrl">#2196F3</color> <!-- 追加 -->
</resources>

strings.xml

<resources>
    ...
    <!-- tools text -->
    <string name="tools_item_name">LiveData</string> <!-- 追加 -->
    <string name="tools_item_description">Use LiveData to build data objects that notify views when the underlying database changes.</string>  <!-- 追加 -->
    <string name="tools_item_url">https://developer.android.com/topic/libraries/architecture/livedata</string> <!-- 追加 -->
</resources>

Layout XML 編集

それぞれ以下のように編集します。 [packege名]のところは適宜修正してください。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <fragment
            android:layout_width="match_parent"
            android:layout_height="match_parent" android:name="[packege名].AACListFragment"
            android:id="@+id/fragment"/>

</androidx.constraintlayout.widget.ConstraintLayout>

fragment_aaclist.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             tools:context=".AACListFragment">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/aac_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
</FrameLayout>

item_aac.xml

新規に作成します。 このXML内でDataBindingを使うので全体を <layout> で括ります。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable name="item"
                  type="birth.h3.app.sunaba.epoxysample.model.AACItem" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/space_l">

        <TextView
                android:id="@+id/aac_name"
                android:text="@{item.name}"
                android:layout_width="wrap_content"
                android:layout_height="0dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                tools:text="@string/tools_item_name"
                android:textSize="@dimen/text_l"
                android:textColor="@color/colorAACName"/>

        <TextView
                android:id="@+id/aac_description"
                android:text="@{item.description}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@+id/aac_name"
                app:layout_constraintStart_toStartOf="@+id/aac_name"
                tools:text="@string/tools_item_description"
                android:layout_marginTop="@dimen/space_m"
                android:layout_marginStart="@dimen/space_m"/>

        <TextView
                android:id="@+id/aac_url"
                android:text="@{item.url}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@+id/aac_description"
                app:layout_constraintEnd_toEndOf="parent"
                tools:text="@string/tools_item_url"
                android:textSize="@dimen/text_s"
                android:layout_marginTop="@dimen/space_m"
                android:textColor="@color/colorAACUrl"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

EpoxyModel作成

いよいよEpoxyの具体的な使用法に入っていきます。

package-info.java

package直下に package-info.java を作成します。 [package名]のところは適宜置き換えてください。

@EpoxyDataBindingLayoutsの中にDataBindingを使用したいlayoutファイルを指定します。 ここでは先ほど作成した item_aac を指定します。

@EpoxyDataBindingLayouts({
        R.layout.item_aac
})

package [package名];

import com.airbnb.epoxy.EpoxyDataBindingLayouts;

ここで一度ビルドをしておきます。 ビルドすることでEpoxyModelが作成されます。

AACListController作成

EpoxyではRecyclerView.Adapterの代わりにControllerを作成していきます。 TypedEpoxyControllerを継承し、buildModelsをoverrideして中に処理を実装していきます。 ここでは、AACListを受け取りaacItemsの数分だけItemAacBindingModel_をリスト内に追加していきます。

class AACListController: TypedEpoxyController<AACList>() {
    override fun buildModels(data: AACList) {
        data.aacItems.forEach {
            ItemAacBindingModel_()
                .item(it)
                .id(modelCountBuiltSoFar)
                .addTo(this)
        }
    }
}

AACListFragment

AACListFragmentに処理をRecyclerViewを表示する処理を書いていきます。 onActivityCreatedをoverrideしてその中に実装していきます。

RecyclerViewのAdapterには、controler.adapterを指定します。 controler.setData()を呼ぶことでアイテムを表示してくれます。

class AACListFragment : Fragment() {

    private val controler by lazy { AACListController() }

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        aac_list.let {
            it.adapter = controler.adapter
            it.layoutManager = LinearLayoutManager(this.activity, RecyclerView.VERTICAL, false)
            it.addItemDecoration(DividerItemDecoration(this.activity, DividerItemDecoration.VERTICAL))
        }

        controler.setData(getData())
    }

    private fun getData() = AACList.getList()
}

完了!

お疲れ様でした。 👏

ビルドしてサンプルアプリを実行しましょう。

epoxy sample アプリ
epoxy samle

おまけ

アイテムをクリックしたらリンクを開くようにする

せっかく、URLもデータとして持たせているのでアイテムをクリックしたらリンクを開くようにしてみます。

ChromeCustomTabsを使います。

androidx.browserをインストール

 // customtabs
    implementation 'androidx.browser:browser:1.0.0'

onClickListner を設定

item_aac.xml編集
<layout .../>
    <data>
         ...
         <variable name="itemClickListener"
                  type="android.view.View.OnClickListener" /> <!-- 追加 -->
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
             ...
             android:onClick="@{itemClickListener}" /> <!-- 追加 -->
</layout>
AACListControllerを編集

ClickListener を interfaceとして追加します。

class AACListController(val callback: ClickListener): TypedEpoxyController<AACList>() {

    interface ClickListener {
        fun itemClickListener(item: AACItem)
    }

    override fun buildModels(data: AACList) {
        data.aacItems.forEach {
            ItemAacBindingModel_()
                .item(it)
                .itemClickListener { _, _, _, _ ->
                    callback.itemClickListener(it)
                }
                .id(modelCountBuiltSoFar)
                .addTo(this)
        }
    }
}
AACListControllerを編集

AACListController.ClickListenerをimplementしてitemClickListenerの処理を実装していきます。

class AACListFragment : Fragment(), AACListController.ClickListener {
    private val controler by lazy { AACListController(this) }

   ...

    override fun itemClickListener(item: AACItem) =
        CustomTabsIntent.Builder()
            .setShowTitle(true)
            .setToolbarColor(ContextCompat.getColor(this.activity!!, R.color.colorPrimary))
            .build().launchUrl(this.activity, Uri.parse(item.url))
}
スクショ

f:id:h3-birth:20190325011255g:plain:w300

引用

Android Architecture Components  |  Android Developers

https://github.com/airbnb/epoxy/wiki