Android
Kotlin
リファクタリング
java8

アクティビティ起動のコード重複を排除する

Android アプリケーションで、同じアクティビティへの移動を複数のフラグメントから行なおうとすると、コードの重複が生じてしまいます。この記事では、インターフェイスのデフォルト実装を活用してそのような重複を回避する方法を紹介します。

この記事では、コード例に Kotlin 1.1 を使用しています。

前提

前提として、あるフラグメントから現在のアクティビティとは別のアクティビティを起動する処理を記述するときは、フラグメントにアクティビティの起動処理を直接実装するのではなく、次のようにするのが一般的です。

  1. フラグメント1移動先のアクティビティを起動するメソッドのシグネチャを持つインターフェイスを宣言する。
  2. フラグメントが属するアクティビティが、フラグメント側で宣言されたインターフェイスを実装する。
    • 実際に移動先のアクティビティを起動する処理を呼びだすのはこの部分です。
  3. フラグメントが、自身が属するアクティビティを、宣言したインターフェイスにキャストし、メソッドを呼びだすことで間接的にアクティビティを起動する。

次に示すのは、「ホーム」画面のフラグメント(HomeFragment)から「記事」画面(ArticleActivity)を起動するという例です。

ArticleActivity.kt
class ArticleActivity : AppCompatActivity() {
    companion object {
        fun start(id: String, context: Context): Unit {
            Intent(context, ArticleActivity::class.java)
                .putExtra("id", id)
                .let { context.startActivity(it) }
        }
    }
}
HomeActivity.kt
class HomeActivity : AppCompatActivity(), HomeFragmentListener {
    override fun startArticle(id: String): Unit {
        ArticleActivity.start(id, this)
    }

    override fun doFoo(): Unit {
        // 省略
    }
}

HomeFragment.kt
class HomeFragment : Fragment() {
    lateinit var mListener: HomeFragmentListener

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        mListener = context as HomeFragmentListener
    }

    companion object {
        interface HomeFragmentListener {
            fun startArticle(id: String): Unit

            // HomeFragment から呼びだされるその他のメソッド
            fun doFoo(): Unit
        }
    }
}

ここでは、フラグメントHomeFragment属するアクティビティHomeActivity移動先のアクティビティArticleActivityです。

HomeFragmentHomeFragmentListenerインターフェイスを宣言します。
HomeActivityHomeFragmentListener実装します。
HomeFragmentが、自身が属するアクティビティであるHomeActivityHomeFragmentListenerとしてキャストし、インターフェイスを通してstartArticleを呼びだすことで間接的にArticleActivityを起動します。

課題

さて、ここで、新しく「検索結果」画面(SearchResultActivity)を追加し、そのフラグメントであるSearchResultFragmentからも「記事」画面を起動することになったとします。

ここでも、SearchResultFragmentで、ArticleActivityを起動するメソッドのシグネチャを持つSearchResultFragmentListenerインターフェイスを宣言し、SearchResultActivityでそれを実装することで、フラグメントからの移動を実現します。

SearchResultActivity.kt
class SearchResultActivity : AppCompatActivity(), SearchResultFragmentListener {
    override fun startArticle(id: String): Unit {
        ArticleActivity.start(id, this)
    }

    override fun doBar(): Unit {
        // 省略
    }
}
SearchResultFragment.kt
class SearchResultFragment : Fragment() {
    lateinit var mListener: SearchResultFragmentListener

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        mListener = context as SearchResultFragmentListener
    }

    companion object {
        interface SearchResultFragmentListener {
            fun startArticle(id: String): Unit

            // SearchResultFragment から呼びだされるその他のメソッド
            fun doBar(): Unit
        }
    }
}

SearchResultActivitystartArticleメソッドを実装することで、HomeFragmentのときと同じようにArticleActivityへの移動を実現できました。

しかし、まったく同じ実装を持つメソッドがHomeActivitySearchResultActivityの2か所に現れることになってしまいました。
このように、同じアクティビティへの移動をいくつかのフラグメントから行なおうとすると、ボイラープレートコードが生じることになるのです。

対策

インターフェイスのデフォルト実装2を使用して、このようなコードの重複を排除することができます。

アクティビティを起動する処理をインターフェイスにデフォルト実装として記述することで、各アクティビティではメソッドの実装を行なわなくてよくなります。

HomeFragmentListenerSearchResultFragmentListenerは共通のメソッドstartArticleを持っています。
これを、別のインターフェイスArticleStartingとして抽出することにしました。

ArticleStarting.kt
interface ArticleStarting {
    // インターフェイスのデフォルト実装
    fun startArticle(id: String) {
        // ここでは、実際には Context が必要です。
        ArticleActivity.start(id, this)
    }
}

しかし、実際にArticleActivity.startメソッドを呼びだすには、Contextが必要です。ArticleStarting内でContextを取得する必要があります。
そこで、Contextを取得できるインターフェイスIContextを定義し、ArticleStartingIContextを継承することにします3

IContext.kt
interface IContext {
    val context: Context
}
ArticleStarting.kt
// ArticleStarting は、 IContext を継承します。
interface ArticleStarting : IContext {
    fun startArticle(id: String) {
        ArticleActivity.start(id, this.context)
    }
}

さらに、プロジェクトのすべてのアクティビティがIContextを実装しなければなりません。実際には、IContextを実装するプロジェクト共通のアクティビティAppActivityを作り4 5、すべてのアクティビティがそのアクティビティを継承することにするのが簡単です。

AppActivity.kt
abstract class AppActivity : AppCompatActivity(), IContext {
    // アクセサーを用いた context プロパティの実装
    override val context: Context
        get() = this
}

これで、各インターフェイスが、デフォルト実装を提供する別のインターフェイスを継承することで、コードの重複なしにフラグメントからのアクティビティの起動を実現することができました。

最終的なコードは次のようになります。

IContext.kt
interface IContext {
    val context: Context
}
AppActivity.kt
abstract class AppActivity : AppCompatActivity(), IContext {
    override val context: Context
        get() = this
}
ArticleStarting.kt
interface ArticleStarting : IContext {
    fun startArticle(id: String) {
        ArticleActivity.start(id, this.context)
    }
}
HomeActivity.kt
class HomeActivity : AppActivity(), HomeFragmentListener {
    override fun doFoo(): Unit {
        // 省略
    }
}
ArticleActivity.kt
class ArticleActivity : AppCompatActivity() {
    companion object {
        fun start(id: String, context: Context): Unit {
            Intent(context, ArticleActivity::class.java)
                    .putExtra("id", id)
                    .let { context.startActivity(it) }
        }
    }
}
HomeFragment.kt
class HomeFragment : Fragment() {
    lateinit var mListener: HomeFragmentListener

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        mListener = context as HomeFragmentListener
    }

    companion object {
        interface HomeFragmentListener : ArticleStarting {
            fun doFoo(): Unit
        }
    }
}
SearchResultActivity.kt
class SearchResultActivity : AppActivity(), SearchResultFragmentListener {
    override fun doBar(): Unit {
        // 省略
    }
}
SearchResultFragment.kt
class SearchResultFragment : Fragment() {
    lateinit var mListener: SearchResultFragmentListener

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        mListener = context as SearchResultFragmentListener
    }

    companion object {
        interface SearchResultFragmentListener : ArticleStarting {
            fun doBar(): Unit
        }
    }
}

  1. フラグメント外に記述することもできますが、フラグメント内に記述するのがわかりやすいでしょう。 

  2. Java では、Java 8 以降でインターフェイスのデフォルト実装を使用できます。 

  3. Kotlinでは、インターフェイスが持つシグネチャとしてメソッドだけでなくプロパティも指定できます。Javaではメソッドしか指定できないので、Context getContext()のようなシグネチャになるでしょう。 

  4. なんらかの理由によりプロジェクト共通のアクティビティを定義できない場合、プロジェクトのすべてのアクティビティがIContextを実装しなければならなく、contextプロパティの実装が重複することになります。それでも、それ以上にコードの重複が生じることはありません。 

  5. プロジェクト共通のアクティビティを定義するなら、そのアクティビティにアクティビティの起動処理を記述すれば良いのではないかと考えるかも知れません。しかし、あるクラスに、ごく一部の下位クラスでしか使われない機能を実装するのは、意味不明であり、それによって神クラスが生じることになります。