柔軟性の高い、疎結合なフラグメント間通信の実装方法

以前の記事ではフラグメント間の通信方法として、直接、他フラグメントの UI 要素を取得して利用する方法を説明しました。

» フラグメント間の通信

確かにこの方法で、片側のフラグメントで起こしたイベントをもう片方のフラグメントに反映させることができました。

しかしながらこの方法では、片側のフラグメントに含まれるウィジェット要素を直接取得するなどしているために、片方のフラグメントの変更がもう片方へ大きく影響してしまいます。

ここでは各フラグメントを可能な限り疎に結合し、フラグメントの独立性を高める実装方法を紹介します。

疎結合なフラグメント間通信の実装方法

ここで作成するサンプルの動作をみてください。

まず画面が横向き(ランドスケープモード)の場合の動作です。左側に項目のリスト (Foo, Bar, ...) があります。この部分がひとつ目のフラグメントです。 右側の青色の部分はリスト項目を選択したときに、その内容が表示されるフラグメントです。

左側の "Bar" という項目を選択すると、右側に "Bar" と表示されます。

画面を縦 (ポートレートモード) にすると、右側の青色のフラグメントが非表示になり、リストを表示するフラグメントのみの表示となります。

リスト項目を選択した場合は、トースト通知として表示されます。

ポートレートモードの場合は、片側のフラグメントが存在しないにも関わらず正常動作しているところが嬉しいですね。

このようにイベントの発行側と受信側がそれぞれ独立して存在できる状況を指して一般的に、イベント処理が疎結合 (loosely coupled) である、という風にいいます。 デザインパターンで言うところの Publisher-Subscriber パターン になります。

疎結合なフラグメント間通信の実装

上記の動作を実現するプログラムは次のように実装します。

リストの onItemClick イベント処理時に、アクティビティのインスタンスを取得し、 アクティビティに実装したイベントハンドラメソッドを呼び出します。 そのメソッドにて、フラグメントの起動状況をチェックし、ロードされていれば 青色のフラグメントに文字を表示。ロードされていなければトースト通知という風に処理を切り替えます。

まとめると、実装するものリストは次のようになります。

  • アクティビティに実装するインターフェイスを定義
  • 文字を表示する(青色の)側のフラグメントで文字設定用のメソッドを実装
    これによって、 外部から直接フラグメント内の UI 要素を取得する必要性を排除
  • リスト・フラグメント側のイベントハンドラにて、アクティビティのイベントハンドラメソッドを呼び出す
    フラグメント内では getActivity() メソッドでアクティビティを取得可能。
  • アクティビティでは FragmentManager を利用して特定のフラグメントがロードされているか確認し、 フラグメントのメソッドを呼び出すなり、必要な処理を行なう

実装例

具体的には、上記のプログラムは次のように実装します。

リストフラグメントのレイアウト: res/layout/fragment_list.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:background="#dadada">
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

コンテントフラグメントのレイアウト: res/layout/fragment_content.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="0dp"
    android:layout_weight="1"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:background="#00A6B6">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="#ffffff"
        android:textSize="36sp"
        android:layout_margin="10dp"/>
</LinearLayout>

アクティビティのレイアウト (ポートレイト): res/layout/activity_main.xml

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:id="@+id/linerLayout1"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:baselineAligned="false">
  <fragment
    android:name="com.example.listcontent1.ListFragment1"
    android:tag="listFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
  />
</LinearLayout>

アクティビティのレイアウト (ランドスケープ): res/layout-land/activity_main.xml

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:id="@+id/linerLayout1"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:baselineAligned="false">
  <fragment
    android:name="com.example.listcontent1.ListFragment1"
    android:tag="listFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
  />
  <fragment
    android:name="com.example.listcontent1.ContentFragment1"
    android:tag="contentFragment"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="1"
  />
</LinearLayout>

それぞれのフラグメントに tag をつけています。FragmentManager の findFragmentByTag メソッドを使うとタグでフラグメントを探すことができます。

フラグメントは動的に入れ替えることが可能ですが、横向き・縦向きというだけでロードするフラグメントがきまるなら、 ランドスケープ用のリソースを定義するのが簡単です。

アクティビティのイベント処理インターフェイス: OnSampleListChangeListener.java

package com.example.listcontent1;

public interface OnSampleListChangeListener {
  void onListSelectedChanged( String s );
}

アクティビティ: MainActivity.java

package com.example.listcontent1;

import android.app.Activity;
import android.app.FragmentManager;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends Activity
  implements OnSampleListChangeListener {

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

  @Override
  public void onListSelectedChanged(String s) {
    FragmentManager fragmentManager
      = getFragmentManager();
    ContentFragment1 contentFragment =
      (ContentFragment1) fragmentManager
        .findFragmentByTag("contentFragment");
    if(contentFragment == null
      || !contentFragment.isVisible()){
      Toast.makeText(getBaseContext(),
        s,
        Toast.LENGTH_SHORT)
      .show();
    }
    else{
      contentFragment.setText(s);
    }
  }
}

リストフラグメント: ListFragment1.java

package com.example.listcontent1;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class ListFragment1 extends Fragment
  implements OnItemClickListener  {

  static List<String> dataList = new ArrayList<String>();
  static ArrayAdapter<String> adapter;
  ListView listView;

  @Override
  public View onCreateView(
    LayoutInflater inflater,
    ViewGroup container,
    Bundle savedInstanceState) {
    View v = inflater.inflate(
      R.layout.fragment_list,
      container,
      false);
    listView = (ListView)
      v.findViewById(R.id.listView1);
    return v;
  }

  @Override
  public void onActivityCreated(
    Bundle savedInstanceState) {
    super.onActivityCreated(
      savedInstanceState);

    // データ初期化
    if(dataList.size() == 0){
      dataList.add("Foo");
      dataList.add("Bar");
      dataList.add("Baz");
    }

    // アダプターの作成と設定
    adapter = new ArrayAdapter<String>(
      getActivity(),
      android.R.layout.simple_list_item_1,
      dataList);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(this);
  }

  @Override
  public void onItemClick(
    AdapterView<?> parent,
    View view,
    int position,
    long id) {

    Activity a = getActivity();
    if(a instanceof OnSampleListChangeListener){
      OnSampleListChangeListener listener =
        (OnSampleListChangeListener) getActivity();
      listener.onListSelectedChanged(dataList.get(position));
    }

  }
}

getActivity() でこのフラグメントを含むアクティビティを取得します。 instanceof を利用し、そのアクティビティが特定のインターフェイス (ここでは上で定義した OnSampleListChangeListener インターフェイス) を実装しているか確認し、 実装していればメソッドを呼び出します。

また、リストビューの利用方法については「ListView の使い方」をみてください。

コンテントフラグメント: ContentFragment1.java

package com.example.listcontent1;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class ContentFragment1 extends Fragment {

  TextView textView;

  @Override
  public View onCreateView(
    LayoutInflater inflater,
    ViewGroup container,
    Bundle savedInstanceState) {
    View v = inflater.inflate(
      R.layout.fragment_content,
      container,
      false);
    textView = (TextView)
      v.findViewById(R.id.textView1);
    return v;
  }

  public void setText(String s){
    textView.setText(s);
  }
}

このフラグメント内に含む TextView に文字を設定するための setText メソッドを実装しています。 これが無いと、プログラムのあちこちから TextView の参照を直接とることになり、疎結合が実現しません。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Android 開発入門