FlutterでMVVMは合わない問題を整理する


https://qiita.com/karamage/items/8a9c76caff187d3eb838

本件、色んな議論があって面白かったです。MVVMとみなす範囲に解釈違いがあるので、結論の出にくい「そもそも論に」なった部分もあると思います。

からまげさんのご主張は「UIを構成するPageに対して、Pageに原則1:1で呼応するViewModelのクラスを用意してバインドするやり方をMVVMと置いており、このアーキテクチャはFlutterにフィットしない」だと思います。これであれば、私も100%同意です。Hooksがあるし。

MVVMは、WPF・Androidのように、ビューをXML形式で記載するアーキテクチャに沿わせるものなんじゃないかと感じました。Flutter/Reactのようにビューも全部Dart/TypeScriptで記述できる形式であれば、ViewModelクラスは必須ではないでしょう。ビューはあくまでもテンプレートエンジンなフレームワークだと、ロジックを埋め込むクラスがどうしても必要。それだけかもしれん。

MVVMの定義を「UIを構成するPageに対して、Pageに1:1で呼応するViewModelのクラスを用意して、リアクティブにバインドする」アーキテクチャとして、話を進めます。この世界線で生きているサンプルコード、用意しました。WPFのコードです。Windowsのデスクトップアプリを作るためのフレームワークです。

MVVMライブラリである「Prism」を使っており、顧客一覧検索の画面の実装を想定しています。

  • prism:ViewModelLocator.AutoWireViewModel="True"が肝で、これをTrueにしていると、Xamlのクラス名+ViewModelが自動的にこの画面のViewModelであると認識されます。
  • {Binding...}から始める記述が、ViewModelとのプロパティに呼応しています。
  • Modeで単方向 or 双方向かを示します。入力可能にするならTwoWay、読み取り専用はOneWayです。
  • UpdateSourceTriggerは更新を伝えるタイミングです。PropertyChangedとあるので、該当するプロパティの再代入が走ったらUIを更新するようになっています。
ClientSearchPage.xaml
<UserControl x:Class="Sample.Views.ClientSearchPage"
	     xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <Label Content="コード"/>
            <TextBox Text="{Binding ClientCode,
			   Mode=TwoWay,
			   UpdateSourceTrigger=PropertyChanged}" />
            <Label Content="ふりがな"/>
            <TextBox Text="{Binding ClientName,
			   Mode=TwoWay,
			   UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="検索" Command="{Binding FindClientCommand}" />
        </StackPanel>
        <DataGrid Grid.Row="1"
                ItemsSource="{Binding ClientList,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" >
                <DataGrid.Columns>
                <DataGridTextColumn Header="コード" Width="Auto" Binding="{Binding code,Mode=OneWay}" CanUserSort="True"/>
                <DataGridTextColumn Header="名称" Width="*" Binding="{Binding fullname,Mode=OneWay}"/>
                <DataGridTextColumn Header="かな" Width="Auto" Binding="{Binding kana,Mode=OneWay}"/>
            </DataGrid.Columns>
            </DataGrid>
    </Grid>
</UserControl>

ViewModel側のコードはこんな感じです。

  • SetPropertyを呼び出すと、UpdateSourceTrigger=PropertyChangedがフックされ、UIが更新されます。
namespace Sample.ViewModels
{
    public class ClientSearchPageViewModel : BindableBase
    {
        private string _client_code = "";
        private string _client_name = "";
        private List<ClientModel> _client_list;

        public string ClientCode
        {
            set { SetProperty(ref _client_code, value); }
            get { return _client_code; }
        }
        public string ClientName
        {
            set { SetProperty(ref _client_name, value); }
            get { return _client_name; }
        }
        public List<ClientModel> ClientList
        {
            set { SetProperty(ref _client_list, value); }
            get { return _client_list; }
        }

        public DelegateCommand FindClientCommand { set;  get; }

        public ClientSearchPageViewModel()
        {
            _client_list = new List<ClientModel>();
            this.FindClientCommand = new DelegateCommand(() => {
                findClient();
            });
        }
        public void findClient()
        {
            var dict = new Dictionary<string, string>()
                {
                    { "code",ClientCode},
                    { "name",ClientName},
                };
            this.ClientList = ApiClient.GetClientList(dict);
        }
    }
}

WPFにはDataContextというプロパティがUIの各コンポーネントにあります。そこにViewModelのインスタンスをセットしてリアクティブにUIを管理する方式です。

ビューはあくまでもテンプレートエンジンです。Reactのようにロジックの内容を記載できません。イベントハンドラだけや状態の定義だけを行い、それに呼応するクラスを差し込む形をとっていることが伝わればと思います。

WPFには「Command」というインターフェイスがあり、イベントハンドラの処理をCommandに委譲するようになっています。移譲先はバインドされたViewModelのCommandです。

こーゆーコテコテのViewModelなアーキテクチャにそもそもFlutterはなっていないし、HooksでUI操作できるなら、それが一番シンプルで良い。

自分のFlutterのコードは、Flutter HooksでUIのページ単位の状態更新を行い、レポジトリを経由して取得するデータ・画面遷移の受け渡しデータは、Rivderpodで管理しています。