kotlin小ネタ:Activity 関連の試行錯誤

Activity を扱う際の実験コード。

とりあえず思い付きで作ってみた。
ユニットテスト重視版。
たぶんおいらのプロジェクトではこんなことやらんだろうけどw

/**
 * Activity を扱う際のいろんなネタを詰め込んでみた実験コード。
 * ※書式だけまとめたもので、実際に動くコードではない。
 *
 * ポイントは以下。
 * ・Intent を利用せずに Activity を起動するチート。
 * ・XxxActivityHelper を分離することにより Activity の制約とは無縁のインスタンスで Activity の処理を記述できる。
 * ・コンストラクタ引数の評価時(thisが生成される前)に可能な限り初期化を行うことにより初期化前のプロパティを参照
 *  してしまうバグの入れ込みを不可能にする。※コンパイラレベルで排除される
 * ・テスト用にコンストラクタの動的な差し替えが可能。※newInstance を var で定義している。
 * ・ほぼすべてのメソッドが単体でユニットテスト可能。this も差し替えられるように extension を使用している。
 * ・メソッド階層をすべて辿ったうえで var への代入が行われるコードかどうかをコンパイラレベルでチェック可能。
 */
package com.example

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.example.XxxActivityHelperImplDelegate.getPcaSomethingCreatedByConstructor
import com.example.XxxActivityHelperImplDelegate.onBackPressed_
import com.example.XxxActivityHelperImplDelegate.onBeforeConstructor
import com.example.XxxActivityHelperImplDelegate.onPause_
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.startActivity
import com.example.XxxActivityHelperImpl as Assignable
import com.example.XxxActivityHelperImpl as Impl

/**
 * [Activity] 本体
 *
 * 以下の理由で [onCreate] 以外の処理をすべて [XxxActivityHelper] に委譲している。
 * ・ヘルパーを利用することにより [onCreate] 以降に生成された値を  [XxxActivityHelper] の val として扱える。
 * ・[Activity] の仕様に特化した処理とそれ以外を分離できる。※[onPause] で super.onPause() を呼ぶ処理など。
 */
class XxxActivity : AppCompatActivity(), AnkoLogger {
    // backing field を持つ唯一のプロパティ
    // これ以外はすべて Helper 側に含まれる
    private lateinit var helper: XxxActivityHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Intent を利用せずに Activity を起動するチート用の処理。
        // XxxHelper に委譲する前に必要な処理なのでここに記述している。
        val activityArg1 = activityArg1
        val activityArg2 = activityArg2
        if (activityArg1 != null && activityArg2 != null) {
            helper = XxxActivityHelper.newInstance(this@XxxActivity, activityArg1, activityArg2)
            Companion.activityArg1 = null
            Companion.activityArg2 = null
        }
        else {
            finish()
        }
    }
    
    override fun onBackPressed() {
        helper.onBackPressed()
    }
    
    override fun onPause() {
        super.onPause()
        helper.onPause()
    }
    
    companion object {
        /**
         * [Intent] を利用せずに [Activity] を起動するチート用の情報
         * [startActivity] 経由で起動される保証はないので lateinit にはできない。※端末の履歴機能からの Activity 起動など。
         */
        private var activityArg1: Something1? = null
        private var activityArg2: Something2? = null
        
        /**
         * [Intent] を利用せずに [Activity] を起動するチート
         */
        fun startActivity(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
            Companion.activityArg1 = activityArg1
            Companion.activityArg2 = activityArg2
            activity.startActivity<XxxActivity>()
        }
    }
}

/**
 * [XxxActivity] から処理を移譲されるヘルパークラス
 */
interface XxxActivityHelper {
    fun onBackPressed()
    
    fun onPause()
    
    companion object {
        // テスト時には必要に応じて newInstance を差し替える
        var newInstance = ::Impl
    }
}

class XxxActivityHelperImpl private constructor(
        override val activity: XxxActivity,
        override val activityArg: Something1,
        override val somethingCreatedByConstructor: Something3
) : XxxActivityHelper, XxxActivityHelperImplDelegate.NonAssignable {
    // Secondary constructors to create primary constructor arguments --------------------------------------------------
    // this を参照しなくても生成可能な情報はすべてコンストラクタ引数として扱う。
    // これにより、property initializer もしくは initializer block にて非 null に初期化のプロパティが null にならない
    // という勘違いによるバグを防ぐ。※初期化前に参照できるので
    @Suppress("unused") // for inspector's bug.
    constructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) : this(
            // どうしても評価前に行わなきゃならん処理がある場合の苦肉の策。
            onBeforeConstructor(activity, activityArg1, activityArg2).run {
                activity
            },
            activityArg1,
            getPcaSomethingCreatedByConstructor(activityArg2)) {
        // ここには可能な限りコードを書かない。
    }
    
    // Properties with backing fields that refer to this@Impl ----------------------------------------------------------
    override var isBackPressed = false
    
    // Initializer block -----------------------------------------------------------------------------------------------
    init {
        // ここには可能な限りコードを書かない。
    }
    
    // Delegate functions ----------------------------------------------------------------------------------------------
    
    override fun onBackPressed() {
        onBackPressed_()
    }
    
    override fun onPause() {
        onPause_()
    }
}

object XxxActivityHelperImplDelegate : AnkoLogger {
    // Constants -------------------------------------------------------------------------------------------------------
    
    // onBeforeConstructor ---------------------------------------------------------------------------------------------
    fun onBeforeConstructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
    }
    
    // Primary constructor argument initializers -----------------------------------------------------------------------
    fun getPcaSomethingCreatedByConstructor(activityArg: Something2): Something3 {
        return Something3()
    }
    
    // Property initializers for properties that internally refer to this@Impl -----------------------------------------
    
    // Initializer block delegation ------------------------------------------------------------------------------------
    
    // Direct delegates ------------------------------------------------------------------------------------------------
    
    fun Assignable.onBackPressed_() {
        isBackPressed = true
    }
    
    fun NonAssignable.onPause_() {
        with(activity) {
            // プロパティへの代入の無い処理をここで行う
        }
    }
    
    // Members that assign values to backing fields --------------------------------------------------------------------
    
    // Members that have side effects ----------------------------------------------------------------------------------
    
    // Non-assignable interface of implementation class ----------------------------------------------------------------
    // NonAssignable が receiver のメソッドはプロパティへの代入が無いことがコンパイラにより保証される。保守時に便利。
    // NonAssignable であってもイミュータブルである保証はない点に注意。
    interface NonAssignable : AnkoLogger {
        val activity: XxxActivity
        val activityArg: Something1
        val somethingCreatedByConstructor: Something3
        val isBackPressed: Boolean
    }
}

class Something1
class Something2
class Something3