スマホ(Android)の基礎を知らない人のための基礎 Activity(コントローラ)とレイアウトファイル(ビュー)の関係などなど

現在ドラマでガッキー主演の「掟上今日子の備忘録」をやっているせいで、
タイトル説明に入れた「備忘録」が、ドラマの影響やろ、と嫁から疑われています。


現在、会社でスマホアプリの設計を行っているのですが、レビューで下記のような指摘を受けます。

画面毎にコントローラクラスを定義するのは何故?似たような画面多いしまとめようよ、という話です。

レビュアー(上司)が組み込み開発者であり、スマホアプリ開発に関する知見を持っているわけでは無いため、コントローラクラスは一般的にこういうもん(画面毎に定義する)なんです、が通じません。また、自分自身も、画面毎のコントローラクラスをまとめると、どうマズイかを理論立てて説明出来ていないため、余計に事をややこしくしています。あと前回話題にしたMVCとかも安易に使うと、定義が曖昧なため、傷口が広がります。

一番早いのは、レビュアーにスマホアプリ開発の基礎を学んでもらう事なのですが、そんな生意気な事を言えば、自分の首が飛びかねないので、ここは社畜として説明出来ない自分が悪いと反省し、スマホ開発経験がない人でも分かるよう論理的に説明しようと思います。

スマホアプリ開発基礎

とりあえず、本やネットでも情報溢れてますが、自分なりの説明で。また、ひとまずAndroidをベースで。
スマホアプリの最小構成は、前回もサラっと説明しましたが、一つのレイアウトファイルと、一つのアクティビティクラスファイルです。

下記が、最小構成のデフォルトパッケージになります。

赤枠がアクティビティ(概念で言うところのコントローラ)
※今回のサンプルでは、作成時のデフォルト名:MainActivity.java
青枠がレイアウトファイル(概念で言うところのビュー)
※今回のサンプルでは、作成時のデフォルト名:activity_main.xml

f:id:hide1024:20151121153423j:plain

アクティビティとレイアウトファイル各々の説明に入りたいところですが、まずはアプリを起動した時に、最初に行われる処理の流れも説明しときましょう。
下記はmanifestファイルというやつです。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.sampleapp"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="18"
android:targetSdkVersion="21" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

下記に抜粋した内容が、MainActivityというアクティビティ(コントローラ)がいますよ、という意味を指しています

<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

そして、加えて、下記抜粋した内容が

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

アプリ起動時に最初に呼ぶコントローラですよ、という事の定義であり、アプリのエントリポイントを意味しています。C言語で言うmain関数みたいなもん。なので、アプリ起動時にMainActivityが実行されるわけです。また"intent-filter"と記載されてますが、intentとは一般的に現在のアクティビティから別のアクティビティに移る際に使うメッセージみたいなもんで、要は画面遷移時に使うもんです。アクティビティ間の画面遷移の処理は、アクティビティクラスファイルにintentの処理を書きますが、起動時に呼ばれるアクティビティは、このようにmanifestファイルに書いておく必要があります。
起動時の動きが分かった事ところで、アクティビティとレイアウトファイルの話に戻しましょう。まずは、レイアウトファイル(ビュー)から。

レイアウトファイル
<RelativeLayout 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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.sampleapp.MainActivity" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

</RelativeLayout>

ちなみに上記レイアウトファイルによって、どんな外観になるかというとこんな感じ。f:id:hide1024:20151121225952j:plain
"@string/hello_world"の部分は、文字列だけをまとめたstring.xmlというのが別途あり、そこを参照してます、という意味になります。

<string name="hello_world">Hello world!</string>

なので、画面上に”Hello world!”が表示されるわけです。

さて、それでは次にこのレイアウトファイル(ビュー)がどうやって表示されるか、アクティビティ(コントローラ)を見てみましょう。まずは全体が下記になります。

package com.example.sampleapp;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

}

下記の部分が、アクティビティクラスを継承したMainActivityを定義してますよという意味で、

public class MainActivity extends Activity {

下記の、Activityクラス内で定義されているonCreateメソッドは必ずオーバーライドしておく必要があります。
他にも、画面の状態によって、onResumeやonPauseや、onDestoryなどメソッドがあるのですが、それらはアプリの都合に応じて必要であれば書きます(今は詳しい説明は省略します)

	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	}

で、このonCreateメソッド内の処理ですが、super.onCreate(savedInstanceState)はおまじない的に呼ぶ必要があります。
で、次の、

setContentView(R.layout.activity_main);

ここの処理でレイアウトファイル(ビュー)とアクティビティ(コントローラ)との紐付けを行っています。MainActivity が呼ばれると、レイアウトファイル:activity_mainを画面上に表示しますよ、という意味です。これによりアプリ起動後に、先程見た"Hello World!"の画面がちゃんと表示されるわけです。
で、ようやく真の本題に入ります。"何故コントローラクラスをまとめないのか"についてです。
まず、コントローラクラスというのは、アクティビティを指すのですが、実装をした事が無い人としては、コントローラという名前が付いているせいか、何かアプリ全体のイベントハンドリングをするものと勘違いしている可能性があるのでは、と思います。なので、何故画面毎にアクティビティを用意する必要があるのか?、まとめて管理するヤツが一つおればえーやんという発想です。では、逆にアクティビティを画面毎に用意しなかった(まとめた)場合、どうなるかという視点でアプローチしましょう。

アクティビティを画面毎に用意しなかった(まとめた)場合、どうなるか

入門書等は、画面毎にアクティビティを用意するのを前提として説明している事がほとんどで、あまり理由付けまで詳しく説明しません。自分もそれが当たり前として受け入れましたし、何より、その通り実装して違和感なり不便を感じてない事が大きいです。では、今回は少しまとめる試みをやってみようと思います。

まずは、前提条件として、アプリとしてA,Bの2画面があるとしましょう。
一般的な実装では、
A画面用のアクティビティ(A_Activity)と、レイアウトファイル(A.xml)
B画面用のアクティビティ(B_Activity)と、レイアウトファイル(B.xml)となります。
f:id:hide1024:20151122214918p:plain
各画面のレイアウト自体はとりあえず上図で書いているようなイメージとしておきます。
さて、では実際にアクティビティをまとめる方法ですが、先程、少し説明した「setContentView」を使います。
例えば何かトリガを与えたタイミングで画面遷移(A画面→B画面)したい場合は、トリガ発生時に呼ばれる処理内で、再度「setContentView」で別のレイアウトファイルをセットするよう書いとけば、画面遷移(レイアウトファイルの切替)自体は可能です。と、やっぱアクティビティまとめれるやんけ!という話になりそうですが、これだと大きな問題があります。

レイアウトファイル内のイベント取得問題

レイアウトファイル内のイベント取得というのは、レイアウトファイル内に配置されているウィジェット(ボタンとか)が押された時に呼ばれる処理の事です。Androidに限らず、HTMLとかでもそうですが、ビュー(レイアウトファイル)で発生したイベントをコントローラ(アクティビティ)で補足するために、一般的にビュー内のウィジェットに識別用のIDを振っておきます。それにより、どのボタンが押された(ビュー→コントローラ)、とか、どのテキストの表示内容を書き換えよう(コントローラ→ビュー)とか、コントローラ⇔ビューでの相互のやり取りが可能になるわけです。
で、今回のアクティビティ1つでレイアウトのみ切替方式だと、どういう問題になるかと言うと、保守性の悪化です。一つのコントローラで処理するためには、各レイアウトに依存したイベント補足用メソッドを全て定義しておく必要があります。上図のA画面のようにウィジェットの数が多いと、どっちの画面の処理なのか一見分からず、バグの温床となる可能性が高いです。また例のように2画面程度でしたら、まだ何とかなるかもしれませんが、ドンドン画面が増えだすと、もうどこのレイアウトファイル用の処理なのか、分からなくなってきます。
以降はコントローラまとめたい人の心理に立って検討します。

命名規則や状態を設けたらいけるやん!

そんな事するぐらいなら、素直にアクティビティをレイアウトファイル単位で作れば良い話と思うのです。

じゃあ複雑度が一定上超えたらアクティビティを分割しよう

どの単位で分割するかは正直人依存になります(個人個人でバラつきがあります)。なので、初期実装した人と、後にメンテする人が違う場合に、よー分からんという事が発生する可能性があります。画面単位で、アクティビティとレイアウトファイルを定義しています、という事であれば、その通りに見れば良いだけで、人によってバラつきが発生しません。
で、最後に言うのもなんですが、下記の記事でご説明されている通り、Androidの場合はシステム都合上、onCreate時のレイアウトファイルしかイベント補足が出来ないようです(一応解決方法もあるようですが)d.hatena.ne.jp
ふぅー。。。ここまで長く記事書いた事が無いため、すんませんが、適当にまとめに入ります(もう見直す気にもならねぇorz)
一般的に"コントローラ"は、アプリ全体のコントロールを意味する意味ケースが多いですが、ActivityやViewConrollerなどスマホ開発用に提供されるコントローラは、"画面を"コントロールするものです。そういう前提で考えれば、画面単位にコントローラを定義するのもスンナリかと。。。

今回は、画面単位毎にコントローラ定義する、としましたが、組織的にある程度開発ノウハウが成熟し、ポリシー(指針)もしっかり定義されており、各メンバにも定着しているのであれば、当然そちらを選択するべきです。まだそのような環境が整備されていない場合は、多少冗長になっても、なるだけブレが発生し難いプロセスを採用したほうがリスクは少ないと思います。