Open8

Kotlin+Android業務アプリで何度も使いまわしている記法まとめ

レモンレモン

Android開発でよく使いまわしているコードの備忘録
毎回忘れるよね
※ほぼ自分用なので、ソースコードを殴り書いているだけです

レモンレモン

ツールメニューバー

ツールバーがあるレイアウト

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:title="@string/app_title"
        app:titleTextColor="@android:color/primary_text_dark" />
</androidx.constraintlayout.widget.ConstraintLayout>

ツールバー自体のレイアウト

drawableにVector Assetsでアイコンをダウンロードしておく。

res/menu/menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu 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"
    tools:context="com.gologius.infoclip.MainActivity">
    <item
        android:id="@+id/action_settings"
        android:title="settings"
        android:icon="@drawable/ic_baseline_settings_24"
        app:showAsAction="ifRoom"/>
</menu>

ツールバーを表示させるプログラム

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ツールバーの設定
        val toolbar: Toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)
    }


    /**
     * @override
     * オプションメニューのビューをセットする
     */
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater: MenuInflater = menuInflater
        inflater.inflate(R.menu.menu, menu)
        // 戻る(←)ボタンを表示する
        // supportActionBar?.setDisplayHomeAsUpEnabled(true)
        // supportActionBar?.title = "在庫移動"
        return true
    }
    /**
     * @override
     * オプションメニューが押下されたときの動作
     */
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            // 設定マークが押されたら設定アクティビティに移動する処理 などを書く
            R.id.action_settings -> {
                //val intent = Intent(application, SettingActivity::class.java)
                //startActivity(intent)
            }
            // 戻る(←)ボタンを表示しているときは、ActivityをFinishする
            android.R.id.home -> {
                //finish()
            }
        }
        return super.onOptionsItemSelected(item)
    }
}
レモンレモン

SharedPreference

アプリケーションの設定項目などを保存しておくエリア
ユーザー名、ディレクトリ名、など好きな項目をまとめておけて便利。

Preference.kt
object Preference {
    /**
     * Preferenceの初期値を設定する
     */
    fun init(activity: Activity) {
        val sharedPref = activity.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE)
        // 既に初期化されているときは、初期化しない
        if (sharedPref.getBoolean("init", false)) {
            return
        }
        println("Preference has been initialized")
        sharedPref.edit()
            .putString(KEY.USER.key, "レモン")
            .putString(KEY.TERMINAL.key, "ANDROID-01")
            .putBoolean("init", true)
            .apply()
    }
    fun clear(activity: Activity) {
        val sharedPref = activity.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE)
        sharedPref.edit().putBoolean("init", false).apply()
        Preference.init(activity)
    }

    /**
     * Preferenceに保存されるデータの一覧
     */
    enum class KEY(val key: String) {
        USER("user"),
        TERMINAL("terminal"),
        // ... 以降も、増やしたい設定項目を記述する
    }

    /**
     * Preferenceの値を設定する
     */
    fun setValue(activity: Activity, key: KEY, value: String) {
        val sharedPref = activity.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE)
        sharedPref.edit().putString(key.key, value).apply()
    }
    /**
     * Preferenceの値を取得する
     */
    fun getValue(activity: Activity, key: KEY): String {
        val sharedPref = activity.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE)
        return sharedPref.getString(key.key, "") ?: ""
    }
}

使い方のサンプル

SampleActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sample)

        // Preferenceの初期化
        Preference.init(this)

        // Preference項目の取得
        var user: String = Preference.getValue(this, Preference.KEY.USER)
        Log.d("Pref", user)
        // -> レモン

        // Preference項目の設定
        Preference.setValue(this, Preference.KEY.USER, "aiueo")
        user = Preference.getValue(this, Preference.KEY.USER)
        Log.d("Pref", user)
        // -> aiueo
    }
レモンレモン

バイブレーションエフェクト

業務アプリではバイブレーションは割と大事です。
ハンディターミナルでQRを読み込んだ時にブブッって震えたりすると。

FX.kt
object FxVibration {
    /**
     * 短いバイブレーションを鳴らす
     */
    fun play(ctx: Context) {
        if (!ctx.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE).getBoolean("vibration", true)) {
            return
        }
        val vibrator = ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val vibrationEffect =
                VibrationEffect.createOneShot (
                    120,
                    VibrationEffect.DEFAULT_AMPLITUDE
                )
            vibrator.vibrate(vibrationEffect)
        }
    }
    /**
     * 長いバイブレーション(アラート)を鳴らす
     */
    fun playAlert(ctx: Context) {
        if (!ctx.getSharedPreferences("setting", AppCompatActivity.MODE_PRIVATE).getBoolean("vibration", true)) {
            return
        }
        val vibrator = ctx.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val vibrationEffect =
                VibrationEffect.createOneShot(
                    480,
                    VibrationEffect.DEFAULT_AMPLITUDE
                )
            vibrator.vibrate(vibrationEffect)
        }
    }
}

使い方のサンプル

Sample.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sample)

        findViewById(R.id.Button).setOnClickListener {
            FxVibration.play(this)
        }
        findViewById(R.id.Button2).setOnClickListener {
            FxVibration.playAlert(this)
        }
    }
レモンレモン

サウンドエフェクト

絶対にもっといい作り方がありそう。
でも動くからいいか~。

res/rawに、効果音ファイルを入れること

FX.kt
/**
 * サウンド再生用のシングルトン
 */
class FxSound constructor(context: Context) {
    private var soundPool: SoundPool? = null

    companion object {
        var SOUND_CLICK     = 0
        var SOUND_BUZZER    = 0
        var SOUND_SEND      = 0
        var SOUND_CLEAR     = 0

        var INSTANCE: FxSound? = null
        fun instance(context: Context) =
            INSTANCE ?: FxSound(context).also {
                INSTANCE = it
            }
    }

    init {
        createSoundPool()
        loadSoundIDs(context)
    }

    private fun createSoundPool() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val attributes = AudioAttributes.Builder().apply {
                setUsage(AudioAttributes.USAGE_GAME)
                setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            }.build()
            soundPool = SoundPool.Builder().apply {
                setMaxStreams(2)
                setAudioAttributes(attributes)
            }.build()
        }
        else {
            @Suppress("DEPRECATION")
            soundPool = SoundPool(2, AudioManager.STREAM_MUSIC, 0)
        }
    }

    private fun loadSoundIDs(context: Context) {
        soundPool?.let {
            SOUND_CLICK     = it.load(context, R.raw.scan,1)
            SOUND_BUZZER    = it.load(context, R.raw.buzzer,1)
            SOUND_SEND      = it.load(context, R.raw.send2,1)
            SOUND_CLEAR     = it.load(context, R.raw.clear,1)
        }
    }

    fun playSound(soundID: Int) {
        soundPool?.let {
            it.play(soundID, 1.0f,1.0f,1,0,1.0f)
        }
    }

    fun close() {
        soundPool?.release()
        soundPool = null
        INSTANCE = null
    }
}

}

使い方のサンプル

Sample.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sample)

        findViewById(R.id.Button).setOnClickListener {
            FxSound.instance(this).playSound(FxSound.SOUND_BUZZER)
        }
    }
レモンレモン

RecyclerView

めっちゃむずい。Androidぜんぜん分からん。の要因の一つ、RecyclerViewくん。
4つのモノが必要です

  1. RecyclerViewで表示する一要素のビュー(fragment_recycler.xml)
  2. RecyclerViewを含んだレイアウト(activity_data.xml)
  3. RecyclerViewのAdapterを作成するクラス(ItemListAdapter.kt)
  4. RecyclerViewのAdapterを接続するアクティビティ(DataActivity.kt)

RecyclerViewのフォームを作成

fragment_recycler.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_width="match_parent"
    android:layout_height="70dp"
    android:background="@drawable/border_1"
    android:orientation="vertical">

    <View
        android:id="@+id/divider10"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="?android:attr/listDivider" />

    <LinearLayout
        android:id="@+id/list_Header"
        android:layout_width="match_parent"
        android:layout_height="24dp"
        android:background="@color/purple_200"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/list_Head"
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:drawableStart="@drawable/ic_baseline_location_on_16"
            android:gravity="center_vertical"
            android:text="A-123456"
            android:textSize="14sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/list_Body"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="0123456789"
            android:textSize="18sp" />
    </LinearLayout>

    <View
        android:id="@+id/divider5"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="?android:attr/listDivider" />
</LinearLayout>

RecyclerViewを含んだレイアウトの作成

activity_data.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

RecyclerViewのアダプターを作成するクラス

むずかしい!boundsEffectは、RecyclerViewをフリックしたときのアニメーションを表示するだけなので、不要な場合は不要です。

ItemListAdapter.kt
/**
 * アイテムリスト
 */
class ItemListAdapter(private val dataSet: Array<ItemList>) :
    RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {

    lateinit var _listener: OnItemClickListener

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val head: TextView = view.findViewById(R.id.list_Head)
        val body: TextView = view.findViewById(R.id.list_Body)

        val header: LinearLayout = view.findViewById(R.id.list_Header)

        val springAnimY: SpringAnimation = SpringAnimation(itemView, SpringAnimation.TRANSLATION_Y)
            .setSpring(SpringForce().apply {
                finalPosition = 0f
                dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
                stiffness = SpringForce.STIFFNESS_VERY_LOW
            })
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.fragment_historylist, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.body.text = dataSet[position].body
        holder.head.text = dataSet[position].head


        holder.itemView.setOnClickListener {
            _listener.onItemClickListener(it, position, dataSet[position])
        }
    }

    interface OnItemClickListener {
        fun onItemClickListener(view: View, position: Int, clickedValue: ItemList)
    }

    fun setOnItemClickListener(listener: OnItemClickListener) {
        _listener = listener
    }

    override fun getItemCount() = dataSet.size

    /**
     * @summary リサイクルビューをスクロールアニメーションさせる(画面端でばうんどする)
     */
    val boundsEffect = object : RecyclerView.EdgeEffectFactory() {
        override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
            return object : EdgeEffect(view.context) {
                val OVER_SCROLL_COEF = 0.2f  // リスト端にスクロールしたときにどのくらいまでリスト外までスクロールさせるかを決める比率係数
                val OVER_FLICK_COEF = 0.8f   // リスト端にフリックしたときに程度リスト外までスクロールさせるかを決める比率係数

                // リストの端に行ったときに呼び出される
                override fun onPull(deltaDistance: Float, displacement: Float) {
                    super.onPull(deltaDistance, displacement)
                    // deltaDistance 0~1f 前回からの変化した割合
                    // displacement 0~1f タップした位置の画面上の相対位置
                    val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
                    val deltaY = sign * view.height * deltaDistance * OVER_SCROLL_COEF
                    // view holderのアニメーションを移動距離に合わせて更新
                    for (i in 0 until view.childCount) {
                        view.apply {
                            val holder =
                                getChildViewHolder(getChildAt(i)) as ItemListAdapter.ViewHolder
                            holder.springAnimY.cancel()
                            holder.itemView.translationY += deltaY
                        }
                    }
                }

                // 指を離したとき
                override fun onRelease() {
                    super.onRelease()
                    // アニメーションスタート
                    for (i in 0 until view.childCount) {
                        view.apply {
                            val holder =
                                getChildViewHolder(getChildAt(i)) as ItemListAdapter.ViewHolder
                            holder.springAnimY.start()
                        }
                    }
                }

                // リストをフリックして画面端に行ったとき
                override fun onAbsorb(velocity: Int) {
                    super.onAbsorb(velocity)
                    val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
                    val translationVelocity = sign * velocity * OVER_FLICK_COEF
                    for (i in 0 until view.childCount) {
                        view.apply {
                            val holder =
                                getChildViewHolder(getChildAt(i)) as ItemListAdapter.ViewHolder
                            holder.springAnimY
                                .setStartVelocity(translationVelocity)
                                .start()
                        }
                    }
                }
            }
        }
    }
}

/**
 * RecyclerViewで使用するアイテムリスト
 */
data class ItemList(
    val head: String,
    val body: String,
)

DataActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_history)

        val itemList: RecyclerView = findViewById(R.id.recyclerView)
        val rLayoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this)
        itemList.layoutManager = rLayoutManager

        val items = arrayOf<ItemList>(ItemList("Head1", "Body1"), ItemList("Head2", "Body2"))
        val adapter = ItemListAdapter(items)
        adapter.setOnItemClickListener(listClick)
        itemList.adapter = adapter
        itemList.edgeEffectFactory = adapter.boundsEffect
    }
レモンレモン

ダイアログ

Android開発で面食らう最初の関門

AlertDialog.builderを使ってもいいけど、画面回転時のメモリリークなどもあってこちらを使うほうがいい?

AndroidDialog.kt
/**
 * OK、もしくはYes、Noだけを展開するAlertDialog
 * @param title ダイアログのタイトル
 * @param message ダイアログのメッセージ
 * @param type ダイアログタイプ(OK, OK Cancel)
 * @param onPressOK OKボタンを押したときの動作
 */
class SimpleAlertDialog(title: String, message: String, type: TYPE = TYPE.OK, onPressOK: () -> Unit = {}): DialogFragment() {
    val _title = title
    val _message = message
    val _onPressOK = onPressOK
    val _type = type

    enum class TYPE() {
        OK,
        OK_CANCEL,
        YES_NO
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder: AlertDialog.Builder = AlertDialog.Builder(activity)
        builder.setTitle(_title)
            .setMessage(_message)

        when (_type) {
            TYPE.OK -> {
                builder.setPositiveButton("OK") { _, _ ->
                    _onPressOK()
                }
            }
            
            TYPE.OK_CANCEL -> {
                builder.setPositiveButton("OK") { _, _ ->
                    _onPressOK()
                }.setNegativeButton("Cancel", null)
            }
            else -> {

            }
        }

        return builder.create()
    }

    /**
     * 画面回転時のメモリリーク対策用
     */
    override fun onPause() {
        super.onPause()
        dismiss()
    }
}

使い方

MainActivity.kt
    SimpleAlertDialog("確認", "送りますか?", SimpleAlertDialog.TYPE.OK_CANCEL) {
        Toast.makeText(this, "送りました", Toast.LENGTH_SHORT).show()
    }.show(supportFragmentManager, "msg_confirm")
レモンレモン

リストビュー(simple_list_item_2)

いちいちRecyclerView使ってListAdapter用意して……ってのが面倒くさいときにパッと使えると便利
simple_list_item_2は簡易的なタイトルとボディがつきます
タイトルだけのもっと簡素なsimple_list_item_1もあります

MainActivity.kt
    lateinit var listView: ListView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sample)
        listView = findViewById(R.id.listView)

        // タイトル・ボディ付の2種リストを用意する
        val titles = listOf("title1", "title2", "title3")
        val bodies = listOf("body1", "body2", "body3")
        val items = titles.zip(bodies).map {
            mapOf("Title" to it.first, "Body" to it.second)
        }
        /// こちらの書き方でも可能
        // val items2 = listOf(
        //     mapOf("Title" to "title 1", "Body" to "body 1"),
        //     mapOf("Title" to "title 2", "Body" to "body 2"),
        //     mapOf("Title" to "title 3", "Body" to "body 3")
        // )

        // アダプターの作成
        val adapter = SimpleAdapter(
            this,
            items,
            android.R.layout.simple_list_item_2,
            arrayOf("Title", "Body"),
            intArrayOf(android.R.id.text1, android.R.id.text2)
        )
        listView.adapter = adapter
    }