0. 环境:
电脑:Windows10
Android Studio: 2024.3.2
编程语言: Java
Gradle version:8.11.1
Compile Sdk Version:35
Java 版本:Java11
1. 首先、简单实现井字棋的功能。
功能拆解:
1. 棋盘为3x3
2. 点击棋盘button,判断是否有效
3. 如果有效,判断是否赢得游戏
4. 如果赢得游戏,则显示胜利
5. 如果未赢得游戏,判断是否平局
6. 如果平局,则显示平局
7. 如果没赢得游戏,也没平局,则轮换选手下棋
关键部分代码:
package com.liosen.androidnote;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
/**
* 井字棋的activity
* 一个文件实现功能
*/
public class TicTacToeActivity extends AppCompatActivity {
// --------------------- model ---------------------
public enum Player {X, O} // 枚举两位玩家,一位执棋X,一位执棋O
public class Chessboard {
private Player value;
}
private Chessboard[][] board = new Chessboard[3][3]; // 棋盘为 3x3
private Player winner; // 定义胜利者
private enum GameState { // 枚举当前游戏状态:游戏中,游戏结束
GAMING, FINISHED
}
private GameState state; // 定义当前游戏状态
private Player currentTurn; // 定义当前轮次,当前执棋手
// --------------------- View ---------------------
private GridLayout glChessboard;
private LinearLayout llWinner;
private TextView tvWinner, tvTips;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
findView();
// 重置游戏
restartGame();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.reset) {
restartGame();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void findView() {
glChessboard = findViewById(R.id.gl_chessboard);
llWinner = findViewById(R.id.ll_winner);
tvWinner = findViewById(R.id.tv_winner);
tvTips = findViewById(R.id.tv_tips);
}
private void restartGame() {
// 重置数据
clearChessboard();
winner = null;
currentTurn = Player.X;
state = GameState.GAMING;
// 重置UI
llWinner.setVisibility(View.GONE);
tvWinner.setText("");
for (int i = 0; i < glChessboard.getChildCount(); i++) {
((Button) glChessboard.getChildAt(i)).setText("");
}
}
/**
* 清空棋盘上的棋子数据
*/
private void clearChessboard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
board[i][j] = new Chessboard();
}
}
}
/**
* 棋盘上的格子,点击事件
*/
public void clickButton(View view) {
Button btn = (Button) view;
String tag = btn.getTag().toString(); // 通过tag获取行列数据
int row = Integer.parseInt(tag.substring(0, 1));
int col = Integer.parseInt(tag.substring(1, 2));
Player currentPlayer = mark(row, col);
if (currentPlayer != null) {
btn.setText(currentPlayer.toString());
if (winner != null) { // 如果胜利的棋手 不为空,则显示胜利的信息
tvWinner.setText(currentPlayer.toString());
llWinner.setVisibility(View.VISIBLE);
} else if (state == GameState.FINISHED) {
tvWinner.setText("");
tvTips.setText("本局平局");
llWinner.setVisibility(View.VISIBLE);
}
}
}
/**
* 下棋 的函数
*/
private Player mark(int row, int col) {
Player currentPlayer = null;
if (isValid(row, col)) { // 这一步下棋,是否有效
// 如果有效
board[row][col].value = currentTurn; // 将这一步棋存入二维数组
currentPlayer = currentTurn;
if (isWinningGame(currentTurn, row, col)) {
// 如果这一步棋 赢下了游戏
// 游戏状态改为结束
state = GameState.FINISHED;
// 胜者为刚刚这一轮的执棋者
winner = currentTurn;
} else if (isNoChessboard()) {
state = GameState.FINISHED;
winner = null;
} else {
// 如果这一步棋没有赢下游戏,则轮换选手
flipPlayerTurn();
}
}
return currentPlayer;
}
/**
* 轮换选手
*/
private void flipPlayerTurn() {
currentTurn = currentTurn == Player.X ? Player.O : Player.X;
}
/**
* 判断 赢下游戏的条件
*/
private boolean isWinningGame(Player currentTurn, int row, int col) {
return (board[row][0].value == currentTurn &&
board[row][1].value == currentTurn &&
board[row][2].value == currentTurn) // 同一行三个棋子相同
||
(board[0][col].value == currentTurn &&
board[1][col].value == currentTurn &&
board[2][col].value == currentTurn) // 同一列三个棋子相同
||
((row == col) &&
board[0][0].value == currentTurn &&
board[1][1].value == currentTurn &&
board[2][2].value == currentTurn) // 对角线三个棋子相同
||
((row + col == 2) &&
board[0][2].value == currentTurn &&
board[1][1].value == currentTurn &&
board[2][0].value == currentTurn) //反向对角线棋子相同
;
}
private boolean isNoChessboard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j].value == null) {
return false;
}
}
}
return true;
}
/**
* 判断这一步棋 是否有效
*/
private boolean isValid(int row, int col) {
if (state == GameState.FINISHED) {
return false;
} else if (isAlreadySet(row, col)) {//当前棋盘按钮,已经下过棋子了
return false;
} else {
return true;
}
}
private boolean isAlreadySet(int row, int col) {
return board[row][col].value != null;
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TicTacToeActivity"
android:gravity="center_horizontal"
android:orientation="vertical"
android:fitsSystemWindows="true"
android:layout_marginTop="60dp"
>
<GridLayout
android:id="@+id/gl_chessboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="3"
>
<Button
android:tag="00"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="01"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="02"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="10"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="11"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="12"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="20"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="21"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
<Button
android:tag="22"
android:onClick="clickButton"
style="@style/chessboard_btn"
/>
</GridLayout>
<LinearLayout
android:id="@+id/ll_winner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/tv_winner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="40sp"
android:layout_margin="20dp"
tools:text="X"
/>
<TextView
android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:text="@string/winner"
/>
</LinearLayout>
</LinearLayout>
2. MVC实现井字棋功能
先拆分功能:
MVC:
M:model数据
V:view视图C:controller逻辑
model部分,分为 Plyaer、GameState、Chessboard、Board(棋盘)
view部分,依然是activity
controller部分,将在Board棋盘中实现
文件结构如下:
重点代码:
activity部分:
package com.liosen.androidnote.mvc.controller;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.liosen.androidnote.R;
import com.liosen.androidnote.mvc.model.Board;
import com.liosen.androidnote.mvc.model.GameState;
import com.liosen.androidnote.mvc.model.Player;
public class TicTacToeControllerActivity extends AppCompatActivity {
// --------------------- model ---------------------
Board model;
// --------------------- View ---------------------
private GridLayout glChessboard;
private LinearLayout llWinner;
private TextView tvWinner, tvTips;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
model = new Board();
findView();
// 重置游戏
restartGame();
}
private void findView() {
glChessboard = findViewById(R.id.gl_chessboard);
llWinner = findViewById(R.id.ll_winner);
tvWinner = findViewById(R.id.tv_winner);
tvTips = findViewById(R.id.tv_tips);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.reset) {
restartGame();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void restartGame() {
model.restartGame();
resetView();
}
private void resetView() {
// 重置UI
llWinner.setVisibility(View.GONE);
tvWinner.setText("");
for (int i = 0; i < glChessboard.getChildCount(); i++) {
((Button) glChessboard.getChildAt(i)).setText("");
}
}
/**
* 棋盘上的格子,点击事件
*/
public void clickButton(View view) {
Button btn = (Button) view;
String tag = btn.getTag().toString(); // 通过tag获取行列数据
int row = Integer.parseInt(tag.substring(0, 1));
int col = Integer.parseInt(tag.substring(1, 2));
Player currentPlayer = model.mark(row, col);
if (currentPlayer != null) {
btn.setText(currentPlayer.toString());
if (model.getWinner() != null) { // 如果胜利的棋手 不为空,则显示胜利的信息
tvWinner.setText(currentPlayer.toString());
llWinner.setVisibility(View.VISIBLE);
} else if (model.getState() == GameState.FINISHED) {
tvWinner.setText("");
tvTips.setText("本局平局");
llWinner.setVisibility(View.VISIBLE);
}
}
}
}
可以看到,代码中,几乎只剩下对UI视图操作的部分。逻辑部分,都交给Board棋盘来实现。
下面看Board棋盘部分的代码:
package com.liosen.androidnote.mvc.model;
/**
* 计分板
*/
public class Board {
private Chessboard[][] board = new Chessboard[3][3]; // 棋盘为 3x3
private Player winner; // 定义胜利者
private GameState state; // 定义当前游戏状态
private Player currentTurn; // 定义当前轮次,当前执棋手
public GameState getState() {
return state;
}
public void setState(GameState state) {
this.state = state;
}
public Player getWinner() {
return winner;
}
public void setWinner(Player winner) {
this.winner = winner;
}
/**
* 重置游戏,清空数据
*/
public void restartGame() {
// 重置数据
clearChessboard();
winner = null;
currentTurn = Player.X;
state = GameState.GAMING;
}
/**
* 清空棋盘上的棋子数据
*/
public void clearChessboard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
board[i][j] = new Chessboard();
}
}
}
/**
* 下棋 的函数
*/
public Player mark(int row, int col) {
Player currentPlayer = null;
if (isValid(row, col)) { // 这一步下棋,是否有效
// 如果有效
board[row][col].setValue(currentTurn); // 将这一步棋存入二维数组
currentPlayer = currentTurn;
if (isWinningGame(currentTurn, row, col)) {
// 如果这一步棋 赢下了游戏
// 游戏状态改为结束
state = GameState.FINISHED;
// 胜者为刚刚这一轮的执棋者
winner = currentTurn;
} else if (isNoChessboard()) {
state = GameState.FINISHED;
winner = null;
} else {
// 如果这一步棋没有赢下游戏,则轮换选手
flipPlayerTurn();
}
}
return currentPlayer;
}
/**
* 轮换选手
*/
private void flipPlayerTurn() {
currentTurn = currentTurn == Player.X ? Player.O : Player.X;
}
/**
* 判断 赢下游戏的条件
*/
private boolean isWinningGame(Player currentTurn, int row, int col) {
return (board[row][0].getValue() == currentTurn &&
board[row][1].getValue() == currentTurn &&
board[row][2].getValue() == currentTurn) // 同一行三个棋子相同
||
(board[0][col].getValue() == currentTurn &&
board[1][col].getValue() == currentTurn &&
board[2][col].getValue() == currentTurn) // 同一列三个棋子相同
||
((row == col) &&
board[0][0].getValue() == currentTurn &&
board[1][1].getValue() == currentTurn &&
board[2][2].getValue() == currentTurn) // 对角线三个棋子相同
||
((row + col == 2) &&
board[0][2].getValue() == currentTurn &&
board[1][1].getValue() == currentTurn &&
board[2][0].getValue() == currentTurn) //反向对角线棋子相同
;
}
private boolean isNoChessboard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j].getValue() == null) {
return false;
}
}
}
return true;
}
/**
* 判断这一步棋 是否有效
*/
private boolean isValid(int row, int col) {
if (state == GameState.FINISHED) {
return false;
} else if (isAlreadySet(row, col)) {//当前棋盘按钮,已经下过棋子了
return false;
} else {
return true;
}
}
private boolean isAlreadySet(int row, int col) {
return board[row][col].getValue() != null;
}
}
model数据部分,主要通过棋盘实现以下功能:
1. 重置游戏数据
2. 下棋动作 及是否有效
3. 判断是否赢得游戏
4. 判断是否平局
5. 轮换选手
这样,就可以把model从activity中抽离出来。减少了activity的臃肿
但是controller部分依然在activity中,随着功能增多,activity依然会变臃肿
于是引入MVP
3. MVP实现井字棋功能
先拆分功能:
M:model数据
V:view视图
P:presenter逻辑
model部分,依然是 Plyaer、GameState、Chessboard、Board,所有不变
view部分,依然是activity,同时增加IView接口
presenter部分,将在TicTacToePresenter逻辑层中实现
文件结构如下:(忽略mvc文件夹)
代码部分:
IView:
package com.liosen.androidnote.mvp.view;
public interface TicTacToeView {
void showWinner(String winner); // 显示胜利玩家
void noWinner(); // 无胜利玩家,即平局
void clearButton(); // 清空棋盘按钮
void clearWinner(); // 清空胜利玩家
void setBtnText(int row, int col, String player); // 下棋动作后,棋盘显示棋子
}
Activity:
package com.liosen.androidnote.mvp.view;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.liosen.androidnote.R;
import com.liosen.androidnote.mvp.presenter.TicTacToePresenter;
public class TicTacToeViewActivity extends AppCompatActivity implements TicTacToeView {
// --------------------- View ---------------------
private GridLayout glChessboard;
private LinearLayout llWinner;
private TextView tvWinner, tvTips;
// --------------------- Presenter ---------------------
TicTacToePresenter presenter = new TicTacToePresenter(this); // 实例化,传入IView接口
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
findView();
// 重置游戏
presenter.reset();
}
private void findView() {
glChessboard = findViewById(R.id.gl_chessboard);
llWinner = findViewById(R.id.ll_winner);
tvWinner = findViewById(R.id.tv_winner);
tvTips = findViewById(R.id.tv_tips);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.reset) {
presenter.reset();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
/**
* 棋盘上的格子,点击事件
*/
public void clickButton(View view) {
Button btn = (Button) view;
String tag = btn.getTag().toString(); // 通过tag获取行列数据
int row = Integer.parseInt(tag.substring(0, 1));
int col = Integer.parseInt(tag.substring(1, 2));
presenter.clickBtn(row, col); // 通过presenter来实现棋子的点击事件逻辑
}
// ------------------------------- 以下为IView的接口实现
public void showWinner(String winner) {
tvWinner.setText(winner);
llWinner.setVisibility(View.VISIBLE);
}
public void noWinner() {
tvWinner.setText("");
tvTips.setText("本局平局");
llWinner.setVisibility(View.VISIBLE);
}
public void clearButton() {
for (int i = 0; i < glChessboard.getChildCount(); i++) {
((Button) glChessboard.getChildAt(i)).setText("");
}
}
public void clearWinner() {
llWinner.setVisibility(View.GONE);
tvWinner.setText("");
}
public void setBtnText(int row, int col, String player) {
Button btn = glChessboard.findViewWithTag("" + row + col);
if (btn != null) {
btn.setText(player);
}
}
}
Presenter:
package com.liosen.androidnote.mvp.presenter;
import android.view.View;
import com.liosen.androidnote.mvp.model.GameState;
import com.liosen.androidnote.mvp.model.Board;
import com.liosen.androidnote.mvp.model.Player;
import com.liosen.androidnote.mvp.view.TicTacToeView;
public class TicTacToePresenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
public void clickBtn(int row, int col) {
Player player = model.mark(row, col);
if (player != null) {
view.setBtnText(row, col, player.toString());
if (model.getWinner() != null) { // 如果胜利的棋手 不为空,则显示胜利的信息
view.showWinner(player.toString());
} else if (model.getState() == GameState.FINISHED) {
view.noWinner();
}
}
}
public void reset() {
model.restartGame();
view.clearButton();
view.clearWinner();
}
}
可以看到,逻辑部分:点击棋盘、重置游戏,都在presenter中实现。如果需要修改view部分,通过IView接口来传递数据。
这样,通过presenter,就可以完成view和model之间的交互。
但是MVP软件架构有个问题,就是IView接口设计会越来越多。增加一个功能,需要修改的部分变更多了。有点为了架构而架构的味道
接下来引入第三个软件架构:MVVM
4. MVVM实现井字棋功能
文件结构如下:(忽略mvc文件夹和mvp文件夹)
4.1 第一步增加dataBinding
在app级别(如果有使用其他module,那该module也需要增加)的build.gradle中,android下增加,如下所示:
android { ··· dataBinding { enable true } }
这里我插一嘴:dataBinding和viewBinding的区别
viewBinding:省略findViewById 的功能
dataBinding:除了viewBinding的功能,还能绑定data部分
4.2 修改activity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.liosen.androidnote.mvvm.viewmodel.TicTacToeViewModel" />
</data>
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="60dp"
android:fitsSystemWindows="true"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".TicTacToeActivity">
<GridLayout
android:id="@+id/gl_chessboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="3">
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(0,0)}"
android:text='@{viewModel.board["00"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(0,1)}"
android:text='@{viewModel.board["01"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(0,2)}"
android:text='@{viewModel.board["02"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(1,0)}"
android:text='@{viewModel.board["10"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(1,1)}"
android:text='@{viewModel.board["11"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(1,2)}"
android:text='@{viewModel.board["12"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(2,0)}"
android:text='@{viewModel.board["20"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(2,1)}"
android:text='@{viewModel.board["21"]}' />
<Button
style="@style/chessboard_btn"
android:onClick="@{()->viewModel.onClickedChessboard(2,2)}"
android:text='@{viewModel.board["22"]}' />
</GridLayout>
<LinearLayout
android:id="@+id/ll_winner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="@{viewModel.winner == null ? View.GONE : View.VISIBLE}"
>
<TextView
android:id="@+id/tv_winner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:textSize="40sp"
tools:text="X"
android:text="@{viewModel.winner}"
/>
<TextView
android:id="@+id/tv_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.result}"
android:textSize="30sp" />
</LinearLayout>
</LinearLayout>
</layout>
可以看到有一些新内容:
首先,必须要用<layout></layout>包裹原有的xml
然后,<variable>标签需要至少引入viewModel,其中name为自定义的名称,type为绑定的ViewModel。此处,我需要将该xml和TicTacToeViewModel.java 进行绑定。
最后,看到<Button>标签中onClick方法变成了
android:onClick="@{()->viewModel.onClickedChessboard(0,0)}"
格式为 "@{}" ,此处中间为lamda,viewModel中的onClickedChessboard方法,在后面的viewModel文件中会有。
text文字变成了
android:text='@{viewModel.board["00"]}'
数据来源为viewModel中的board,同样的 在viewModel文件中会有。
4.3 增加viewModel文件
package com.liosen.androidnote.mvvm.viewmodel;
import androidx.databinding.ObservableArrayMap;
import androidx.databinding.ObservableField;
import com.liosen.androidnote.mvvm.model.GameState;
import com.liosen.androidnote.mvvm.model.Board;
import com.liosen.androidnote.mvvm.model.Player;
public class TicTacToeViewModel {
public Board model;
public final ObservableArrayMap<String, String> board = new ObservableArrayMap<>(); // 此处为被观察者,被观察的数据为map,用于存 <棋盘格子, 棋手>
public final ObservableField<String> winner = new ObservableField<>(); // 此处也为被观察者,被观察的数据为String类型的对象
public final ObservableField<String> result = new ObservableField<>();
public TicTacToeViewModel() {
model = new Board();
}
public void reset() {
model.restartGame();
winner.set(null);
board.clear();
}
public void onClickedChessboard(int row, int col) {
Player player = model.mark(row, col);
if (player != null) {
// 棋盘格子 显示玩家X或者O
board.put("" + row + col, player == null ? null : player.toString());
if (model.getWinner() != null) { // 如果胜利的棋手 不为空,则显示胜利的信息
winner.set(model.getWinner() == null ? null : model.getWinner().toString());
result.set("你赢了");
} else if (model.getState() == GameState.FINISHED) {
winner.set("");
result.set("本局平局");
}
}
}
}
4.4 最后activity文件
package com.liosen.androidnote.mvvm.view;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.databinding.DataBindingUtil;
import com.liosen.androidnote.R;
import com.liosen.androidnote.databinding.ActivityMainMvvmBinding;
import com.liosen.androidnote.mvvm.viewmodel.TicTacToeViewModel;
public class TicTacToeMVVMActivity extends AppCompatActivity {
// --------------------- View ---------------------
TicTacToeViewModel viewModel = new TicTacToeViewModel(); //
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
// setContentView(R.layout.activity_main); // 此时已经不需要该行代码了,通过下面两行实现xml和activity的绑定
ActivityMainMvvmBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main_mvvm); //
binding.setViewModel(viewModel); //
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 重置游戏
viewModel.reset();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.reset) {
viewModel.reset();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
}
activity中绑定viewModel即可。
甚至连findViewById也省了。如果需要在activity中操作UI,可以直接通过binding获取,例如:
binding.tvWinner
至于tvWinner是哪里来的:是由于在xml文件中,设置的id。这样生成binding文件(编译时自动生成)时,就会自动生成该field,可以通过binding获取到。
5. 写在最后
至此我们就学会了用3种软件架构:MVC、MVP、MVVM
MVVM是目前使用最广泛、最好用、最易维护的架构。一定要掌握