> 技术文档 > 【安卓笔记】用MVC、MVP、MVVM来实现井字棋案例

【安卓笔记】用MVC、MVP、MVVM来实现井字棋案例


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; }}

布局文件:

  

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

然后,标签需要至少引入viewModel,其中name为自定义的名称,type为绑定的ViewModel。此处,我需要将该xml和TicTacToeViewModel.java 进行绑定。

最后,看到