本文へスキップ
BecomeCoder

Java MVVMコース · 第6章 ViewModel ― 画面の状態と操作 · レッスン21

ViewModelBase ― 変更通知の重複をなくす

ブラウザで完結

導入

ViewModelのプロパティが変わったとき、画面(View)に「変わったよ」と伝える仕組みが必要です。これがないと、コードの上では値が変わっていても画面表示は古いままになります。ただし、ViewModelを作るたびに「通知する仕組み」を1から書くのは面倒です。共通の土台ViewModelBaseを作り、この面倒を1箇所に押し込みましょう。

図解

classDiagram
    class PropertyChangedListener {
        <<interface>>
        +onPropertyChanged(propertyName)
    }
    class ViewModelBase {
        +addPropertyChangedListener(listener)
        #raisePropertyChanged(propertyName)
    }
    class TaskListViewModel {
        +getTasks()
        +getNewTitle()
    }
    ViewModelBase --> PropertyChangedListener : 通知する
    ViewModelBase <|-- TaskListViewModel

説明

ViewModelBase.java

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

public interface PropertyChangedListener {
    void onPropertyChanged(String propertyName);
}

public abstract class ViewModelBase {
    private final List<PropertyChangedListener> listeners = new ArrayList<>();

    public void addPropertyChangedListener(PropertyChangedListener listener) {
        listeners.add(listener);
    }

    protected void raisePropertyChanged(String propertyName) {
        for (PropertyChangedListener listener : listeners) {
            listener.onPropertyChanged(propertyName);
        }
    }
}

PersonViewModel.java(使い方の例)

class PersonViewModel extends ViewModelBase {
    private String name = "";

    public String getName() { return name; }

    public void setName(String value) {
        name = value;
        raisePropertyChanged("name");   // 変更を登録済みの全リスナーへ通知する
    }
}
PersonViewModel person = new PersonViewModel();
person.addPropertyChangedListener(name -> System.out.println("変わったプロパティ: " + name));

person.setName("Alice");
// 変わったプロパティ: name
  • PropertyChangedListenerは「プロパティ名を1つ受け取るだけ」の単純なインターフェース。ラムダで渡せる
  • ViewModelBaseはリスナーの一覧を持ち、raisePropertyChangedが呼ばれたら全員に通知する
  • ViewModelBase継承するだけで、以降のViewModelは値が変わったところでraisePropertyChanged("プロパティ名")を呼ぶだけでよくなる

やってみよう

下のエディタで、リスナーを2つ登録して、どちらにも通知が届くことを確かめてみましょう。

演習

エディタはViewModelBase.java(あなたが実装するファイル)とViewModelBaseTest.javaテスト)の2つのタブに分かれています。

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

public interface PropertyChangedListener {
    void onPropertyChanged(String propertyName);
}

// TODO: ViewModelBase を完成させてください
//       ・addPropertyChangedListener(listener) でリスナーを登録できる
//       ・raisePropertyChanged(propertyName) は、登録されている全リスナーに
//         propertyName を渡して通知する(サブクラスから呼べるように protected にする)
public abstract class ViewModelBase {
}
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

public class ViewModelBaseTest {
    @Test
    void ataiGaKawaruto_ListenerNiTsuchiSareru() {
        List<String> notified = new ArrayList<>();
        PersonViewModel person = new PersonViewModel();
        person.addPropertyChangedListener(name -> notified.add(name));

        person.setName("Alice");

        assertEquals("Alice", person.getName(), "値が変わる");
        assertEquals(1, notified.size(), "1回だけ通知される");
        assertEquals("name", notified.get(0), "プロパティ名がnameで通知される");
    }

    @Test
    void listenerWoFukusuTourokuSuruto_ZenInNiTsuchiSareru() {
        List<String> a = new ArrayList<>();
        List<String> b = new ArrayList<>();
        PersonViewModel person = new PersonViewModel();
        person.addPropertyChangedListener(name -> a.add(name));
        person.addPropertyChangedListener(name -> b.add(name));

        person.setName("Bob");

        assertEquals(1, a.size(), "1人目にも通知される");
        assertEquals(1, b.size(), "2人目にも通知される");
    }
}

class PersonViewModel extends ViewModelBase {
    private String name = "";

    public String getName() { return name; }

    public void setName(String value) {
        name = value;
        raisePropertyChanged("name");
    }
}
  • 期待される結果: 2件のテストがすべて成功
ヒント1を見る

private final List<PropertyChangedListener> listeners = new ArrayList<>(); public void addPropertyChangedListener(PropertyChangedListener listener) { listeners.add(listener); }

ヒント2を見る

protected void raisePropertyChanged(String propertyName) { for (PropertyChangedListener listener : listeners) { listener.onPropertyChanged(propertyName); } }

まとめ

  • PropertyChangedListenerは「プロパティが変わったこと」を画面へ知らせる単純な仕組み
  • 通知処理はViewModelBaseに共通化し、サブクラスraisePropertyChangedを呼ぶだけにする
  • リスナーは複数登録でき、raisePropertyChangedは登録されている全員に通知する

次回: この土台の上に、実際に状態を持つTaskListViewModelを作ります。

実際に動かしてみよう

このレッスンのサンプルは、実務と同じように役割ごとの .java ファイルへ分けてあります。下のエディタは最初からその複数ファイルが入った状態で、上のタブでファイルを切り替えられます。そのまま「▶ 実行」を押せば全ファイルをまとめて解釈して動かせます。@Test の付いたテストがあるレッスンでは、テストメソッドごとに ✅/❌ の一覧(Red/Green)が出るので、まずテストを赤くしてから実装で緑にする、というTDDの回し方をその場で体験できます(本物のJVMではなく、JUnit・Mockitoの主要な書き方まで再現した学習用シミュレータです)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。