目次
概要
前回でボタンを羅列することをやった。今回は、ボタンを押下したら何のボタンをタップしたかをやってみよう。
また、本ページは実務に近いコード(今後メンテナンスしやすいなど)を意識したプログラミングを行っている。そのため、ある程度レベルが高いと思うが、頑張ろう。
また、簡単な方はこちらに記載。
ここでは、以下の知識が必要になるので、併せて勉強していこう。
- 条件分岐
- 値の変更と更新
- 生成したオブジェクトのメソッドを実行
- MVVMモデル(ここが初心者には難しいが、ネットではなかなか学べないところ)
まずは百聞は一件にしかず。
今回関係するコードをまとめておく。
ソースコード
トランプカードのオブジェクトクラス
import 'TrumpMark.dart';
class TrumpCard {
bool isSelected;
int number;
TrumpMark mark;
TrumpCard(this.isSelected, this.number, this.mark);
void tap() {
this.isSelected = !this.isSelected;
}
}
トランプマークの列挙体
enum TrumpMark {
spade,
heart,
dia,
club
}
extension TrumpMarkAdditional on TrumpMark {
String get trumpMarkString {
switch (this) {
case TrumpMark.spade:
return '♠️';
case TrumpMark.heart:
return '❤️';
case TrumpMark.dia:
return '♦️';
case TrumpMark.club:
return '♣️';
}
}
}
アプリ呼び出し部分
import 'package:concentration/TrumpButton/TrumpButtonView.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'TrumpButton/TrumpButtonView.dart';
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
useMaterial3: true,
),
home: const TrumpButtonView(title: 'Flutter Demo Home Page'),
);
}
}
View表示部分
import 'package:collection/collection.dart';
import 'package:concentration/Common/TrumpCard.dart';
import 'package:concentration/Common/TrumpMark.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'TrumpButtonViewModel.dart';
class TrumpButtonView extends ConsumerWidget {
const TrumpButtonView({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context, WidgetRef ref) {
final _viewModel = ref.watch(trumpButtonViewModelProvider);
List<OutlinedButton> trumpButtons = [];
for (TrumpCard trumpCard in _viewModel.trumpCards) {
var trumpButton = OutlinedButton(
onPressed: () { _viewModel.tapped(trumpCard); },
style: OutlinedButton.styleFrom(
minimumSize: Size(MediaQuery.of(context).size.width/4, 40),
backgroundColor: Colors.white10,
foregroundColor: Colors.black,
),
child: trumpDisplay(trumpCard)
);
trumpButtons.add(trumpButton);
}
final itemsPerRow = 13;
final slicedTrumpButtons = trumpButtons.slices(itemsPerRow).toList();
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text("TrumpButton"),
),
body:
LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth, minHeight: constraints.maxHeight),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var slicedTrumpButton in slicedTrumpButtons)
columnTrumpButtons(slicedTrumpButton),
]
),
),
)
)
);
}
Widget trumpDisplay(TrumpCard trumpCard) {
if (trumpCard.isSelected) {
return Text("${trumpCard.mark.trumpMarkString} ${trumpCard.number + 1}");
} else {
return Text("?", textAlign: TextAlign.center);
}
}
Widget columnTrumpButtons(List<OutlinedButton> trumpButtons) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var trumpButton in trumpButtons)
trumpButton,
]
);
}
}
ViewModelクラス
import 'dart:ffi';
import 'package:concentration/Common/TrumpMark.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../Common/TrumpCard.dart';
import 'TrumpButtonRepository.dart';
final trumpButtonViewModelProvider = ChangeNotifierProvider((ref) => TrumpButtonViewModel(repository: ref.read(trumpButtonRepositoryRepositoryProvider)));
class TrumpButtonViewModel extends ChangeNotifier {
TrumpButtonRepository? repository;
List<TrumpCard> trumpCards = [];
TrumpButtonViewModel({this.repository}){
List<TrumpCard> trumpCards = [];
for(TrumpMark mark in TrumpMark.values) {
for (int num = 0; num < 13; num ++) {
trumpCards.add(TrumpCard(false, num, mark));
}
}
this.trumpCards = trumpCards;
}
void tapped(TrumpCard trumpCard) {
trumpCard.tap();
notifyListeners();
}
}
Repositoryクラス
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'TrumpButtonModel.dart';
final trumpButtonRepositoryRepositoryProvider = Provider((ref) => TrumpButtonRepositoryImpl(model: ref.read(trumpButtonModelModelProvider)));
abstract class TrumpButtonRepository {
void tapped();
}
class TrumpButtonRepositoryImpl implements TrumpButtonRepository {
TrumpButtonRepositoryImpl({required TrumpButtonModel model}): _model = model;
final TrumpButtonModel _model;
void tapped() {
print("タップされたよ!");
}
}
TrumpButtonModel
import 'package:flutter_riverpod/flutter_riverpod.dart';
final trumpButtonModelModelProvider = Provider((ref) => TrumpButtonModel());
class TrumpButtonModel {
}
デモ動画
詳細
さあ、ここから詳しく説明していこう。
色々書いているが、取り出して考えると、以下の流れになっている。
- TrumpCardのオブジェクトを生成する
- TrumpCardのオブジェクトをもとに、ボタンを生成する
- ボタンの文字列はtrumpCardのisSelectedがtrueだったらトランプのマークと数字を、falseだったら「???」と表示させるようにする。
- ボタンをタップしたらTrumpButtonViewModelのvoid tapped(TrumpCard trumpCard) のメソッドが呼ばれる
- trumpCard.tap()のメソッドが呼ばれ、TrumpCardのisSelectedのtrue/falseが入れ替わる
- void tapped(TrumpCard trumpCard)のnotifyListeners()メソッドで画面が更新される。
- 画面の更新により、3の処理が行われる。
では、順番に見ていこう。
TrumpCardのオブジェクトを生成する
ここはTrumpCard.dartの内容の通り、まずはオブジェクトの設計書にあたるクラスを定義しよう。
設定内容は以下
項目 | 内容 |
クラス名 | TrumpCard |
メンバ(クラスが持つ変数) | bool isSelected; int number; TrumpMark mark; |
コンストラクタ(オブジェクト生成時の変数設定) | TrumpCard(this.isSelected, this.number, this.mark); |
メソッド | void tap() |
tapメソッドの内容 | this.isSelected = !this.isSelected; つまり、this.isSelectedがtrueならその否定のfalseを、falseならその否定のtrueを代入する。 |
そして、オブジェクトを生成している箇所はTrumpButtonViewModelクラスの20行目。
ここでTrumnCardのオブジェクトを生成して、trumpCardsの配列に格納している。
以下が該当の箇所。
TrumpButtonViewModel({this.repository}){
List<TrumpCard> trumpCards = [];
for(TrumpMark mark in TrumpMark.values) {
for (int num = 0; num < 13; num ++) {
trumpCards.add(TrumpCard(false, num, mark));
}
}
this.trumpCards = trumpCards;
}
TrumpCardのオブジェクトをもとに、ボタンを生成する
そして、そこで生成したものをTrumpButtonViewの19行目で使用して、ボタンを生成している。
その生成したボタンの内容は以下。
final _viewModel = ref.watch(trumpButtonViewModelProvider);
List<OutlinedButton> trumpButtons = [];
for (TrumpCard trumpCard in _viewModel.trumpCards) { // viewModelのtrumpCardsを一つずつ参照
var trumpButton = OutlinedButton( // 外枠のボタンを生成
onPressed: () { _viewModel.tapped(trumpCard); }, // ボタンを押下した時の処理
style: OutlinedButton.styleFrom(
minimumSize: Size(MediaQuery.of(context).size.width/4, 40),
backgroundColor: Colors.white10,
foregroundColor: Colors.black,
),
child: trumpDisplay(trumpCard) // trumpDisplayメソッドの内容を表示する
);
trumpButtons.add(trumpButton); // trumpButtonsに外枠ボタンのオブジェクトを追加
}
ボタンの文字列はtrumpCardのisSelectedがtrueだったらトランプのマークと数字を、falseだったら「???」と表示させるようにする。
先ほどボタンのオブジェクトを生成したが、ボタンの表示部分はchildの部分だ。
そのchildの部分はtrumpDisplayメソッドが呼ばれている。
いわゆる、以下の部分が表示するViewというわけだ。
Widget trumpDisplay(TrumpCard trumpCard) {
if (trumpCard.isSelected) {
return Text("${trumpCard.mark.trumpMarkString} ${trumpCard.number + 1}");
} else {
return Text("?", textAlign: TextAlign.center);
}
}
まず、この処理をするにあたって、事前にTrumpCardのオブジェクトを呼び出すときに設定する必要がある。そして、そのTrumpCardのオブジェクトのisSelectedがtrueならトランプのマークと数字を、そうでなければ?と表示させる処理を行っている。
ここは条件分岐がわかっていればできるはずだ。
ボタンをタップしたらTrumpButtonViewModelのvoid tapped(TrumpCard trumpCard) のメソッドが呼ばれる
ボタンを押下したらどんなことが起こるか。そうonPressedの内容が行われる。
今回の場合は以下だ。
var trumpButton = OutlinedButton(
onPressed: () { _viewModel.tapped(trumpCard); }, // ボタン押下時の処理はココ!
style: OutlinedButton.styleFrom(
minimumSize: Size(MediaQuery.of(context).size.width/4, 40),
backgroundColor: Colors.white10,
foregroundColor: Colors.black,
),
child: trumpDisplay(trumpCard) // trumpDisplayメソッドの内容を表示する
);
そう、行われる処理は_viewModel.tapped(trumpCard);だ。
_viewModelは以下の通り、TrumpButtonViewModel型だ。
final _viewModel = ref.watch(trumpButtonViewModelProvider);
final trumpButtonViewModelProvider = ChangeNotifierProvider((ref) => TrumpButtonViewModel(repository: ref.read(trumpButtonRepositoryRepositoryProvider)));
そのTrumpButtonViewModelクラスのtappedメソッドは以下のようになっている。
void tapped(TrumpCard trumpCard) {
trumpCard.tap();
notifyListeners();
}
trumpCard.tap()のメソッドが呼ばれ、TrumpCardのisSelectedのtrue/falseが入れ替わる
これはそのまま。TrumpButtonViewModelクラスのtappedメソッドの一行目は設定されたtrumpCardのtap()メソッドを呼び出している。
そのtap()メソッドの中身は以下。
void tap() {
this.isSelected = !this.isSelected;
}
ここでisSelectedのtrueとfalseが入れ替わっている。
つまり、未選択の状態なら選択された状態に、選択された状態なら未選択の状態にする処理を行っている。
void tapped(TrumpCard trumpCard)のnotifyListeners()メソッドで画面が更新される。
ここはnotifyListeners()を呼び出して、画面の更新を行っている。
まともに説明するとこのページのボリュームが膨大になるため、ここでは詳細を割愛する。
画面の更新により、3の処理が行われる。
画面の更新により、再度画面を更新する。その際にボタンの表示内容である以下の部分が再度読み込まれる。
var trumpButton = OutlinedButton(
onPressed: () { _viewModel.tapped(trumpCard); },
style: OutlinedButton.styleFrom(
minimumSize: Size(MediaQuery.of(context).size.width/4, 40),
backgroundColor: Colors.white10,
foregroundColor: Colors.black,
),
child: trumpDisplay(trumpCard) // ここが再度読み込まれる。
);
trumpCard.tap()のメソッドが呼ばれ、TrumpCardのisSelectedのtrue/falseが入れ替わったため、もし「?」が表示されている状態ならマークと数字が、マークと数字が表示されていたら「?」が表示されるようになる。