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,一位执棋Opublic class Chessboard {private Player value;}private Chessboard[][] board = new Chessboard[3][3];    // 棋盘为 3x3private Player winner;  // 定义胜利者private enum GameState {    // 枚举当前游戏状态:游戏中,游戏结束GAMING, FINISHED}private GameState state;    // 定义当前游戏状态private Player currentTurn; // 定义当前轮次,当前执棋手// --------------------- View ---------------------private GridLayout glChessboard;private LinearLayout llWinner;private TextView tvWinner, tvTips;@Overrideprotected 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();}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {MenuInflater inflater = getMenuInflater();inflater.inflate(R.menu.menu_tictactoe, menu);return super.onCreateOptionsMenu(menu);}@Overridepublic 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;// 重置UIllWinner.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"><GridLayoutandroid:id="@+id/gl_chessboard"android:layout_width="wrap_content"android:layout_height="wrap_content"android:columnCount="3"android:rowCount="3"><Buttonandroid:tag="00"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="01"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="02"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="10"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="11"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="12"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="20"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="21"android:onClick="clickButton"style="@style/chessboard_btn"/><Buttonandroid:tag="22"android:onClick="clickButton"style="@style/chessboard_btn"/></GridLayout><LinearLayoutandroid:id="@+id/ll_winner"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/tv_winner"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="40sp"android:layout_margin="20dp"tools:text="X"/><TextViewandroid: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;@Overrideprotected 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);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {MenuInflater inflater = getMenuInflater();inflater.inflate(R.menu.menu_tictactoe, menu);return super.onCreateOptionsMenu(menu);}@Overridepublic 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() {// 重置UIllWinner.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];    // 棋盘为 3x3private 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接口@Overrideprotected 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);}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {MenuInflater inflater = getMenuInflater();inflater.inflate(R.menu.menu_tictactoe, menu);return super.onCreateOptionsMenu(menu);}@Overridepublic 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" /><variablename="viewModel"type="com.liosen.androidnote.mvvm.viewmodel.TicTacToeViewModel" /></data><LinearLayoutandroid: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"><GridLayoutandroid:id="@+id/gl_chessboard"android:layout_width="wrap_content"android:layout_height="wrap_content"android:columnCount="3"android:rowCount="3"><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(0,0)}"android:text='@{viewModel.board["00"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(0,1)}"android:text='@{viewModel.board["01"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(0,2)}"android:text='@{viewModel.board["02"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(1,0)}"android:text='@{viewModel.board["10"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(1,1)}"android:text='@{viewModel.board["11"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(1,2)}"android:text='@{viewModel.board["12"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(2,0)}"android:text='@{viewModel.board["20"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(2,1)}"android:text='@{viewModel.board["21"]}' /><Buttonstyle="@style/chessboard_btn"android:onClick="@{()->viewModel.onClickedChessboard(2,2)}"android:text='@{viewModel.board["22"]}' /></GridLayout><LinearLayoutandroid: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}"><TextViewandroid: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}"/><TextViewandroid: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或者Oboard.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();    //@Overrideprotected 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();}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {MenuInflater inflater = getMenuInflater();inflater.inflate(R.menu.menu_tictactoe, menu);return super.onCreateOptionsMenu(menu);}@Overridepublic 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是目前使用最广泛、最好用、最易维护的架构。一定要掌握

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/90678.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/90678.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/90678.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【洛谷】单向链表、队列安排、约瑟夫问题(list相关算法题)

文章目录单向链表题目描述题目解析代码队列安排题目描述题目解析代码约瑟夫问题题目描述题目解析代码单向链表 题目描述 题目解析 这道题因为有大量的任意位置插入删除&#xff0c;所以肯定不能用数组&#xff0c;用链表是最合适的&#xff0c;而在算法竞赛通常都用静态链表&a…

当人机交互迈向新纪元:脑机接口与AR/VR/MR的狂飙之路

从手机到 “头盔”&#xff1a;交互终端的变革猜想​​在当今数字化时代&#xff0c;智能手机无疑是我们生活中不可或缺的一部分。它集通讯、娱乐、办公等多种功能于一身&#xff0c;成为了人们与外界交互的主要窗口。然而&#xff0c;随着科技的飞速发展&#xff0c;智能手机作…

InfluxDB HTTP API 接口调用详解(二)

实际应用案例演示 1. 数据写入案例 假设在一个物联网设备数据采集场景中&#xff0c;有多个传感器设备持续采集环境的温度和湿度数据。我们以 Python 语言为例&#xff0c;使用requests库来调用 InfluxDB 的 Write 接口将数据写入 InfluxDB。 首先&#xff0c;确保已经安装了…

世运会线上知识竞赛答题pk小程序怎么做

随着2025年成都世界运动会的来临&#xff0c;越来越多的企事业单位组织员工进行线上知识竞赛&#xff0c;那么答题PK小程序该怎么做&#xff0c;接下来我们来一一分析&#xff1a; 世运会线上知识竞赛答题pk小程序怎么做一、答题功能&#xff1a;支持多种题型&#xff0c;如选择…

Java毕业设计 | 基于微信小程序的家校互动作业管理系统(Spring Boot+Vue.js+uni-app+AI,附源码+文档)

Java毕业设计 | 基于微信小程序的家校互动作业管理系统&#xff08;Spring BootVue.jsuni-app&#xff0c;附源码文档&#xff09;&#x1f3af; 毕业设计私人教练 专注计算机毕设辅导第 6 年&#xff0c;累计 1v1 带飞 800 同学顺利通关。从选题、开题、代码、论文到答辩&…

CentOS8 使用 Docker 搭建 Jellyfin 家庭影音服务器

CentOS8 使用 Docker 搭建 Jellyfin 家庭影音服务器 一、前言 由于 Jellyfin 的 GPL 协议和 Intel 的 media-driver (iHD) Linux 驱动&#xff08;部分开源&#xff09;在协议上不兼容的缘故&#xff0c;Jellyfin 官方的 Docker 镜像&#xff1a;jellyfin/jellyfin 并不包含 …

PyTorch武侠演义 第一卷:初入江湖 第4章:损失玉佩的评分风波

第一卷&#xff1a;初入江湖 第4章&#xff1a;损失玉佩的评分风波比武开幕 晨钟响彻山谷&#xff0c;PyTorch派三年一度的"模型比武大会"正式开始。各分舵弟子列队入场&#xff0c;林小码跟在Tensor大师身后&#xff0c;眼睛瞪得溜圆——只见&#xff1a; "卷积…

HttpServletRequestWrapper存储Request

HTTP请求的输入流只能被读取一次&#xff0c;再想获取就获取不到了&#xff0c;那有什么方法可以缓存呢&#xff0c;我们可以自定义一个HttpServletRequest&#xff0c;或者是想在请求参数中统一添加或删除参数也可以使用此类进行改造&#xff0c;然后通过过滤器继续向下流转。…

算法:数组part02: 209. 长度最小的子数组 + 59.螺旋矩阵II + 代码随想录补充58.区间和 + 44. 开发商购买土地

算法&#xff1a;数组part02: 209. 长度最小的子数组 59.螺旋矩阵II 代码随想录补充58.区间和 44. 开发商购买土地 209. 长度最小的子数组题目&#xff1a;https://leetcode.cn/problems/minimum-size-subarray-sum/description/ 文章讲解&#xff1a;https://programmercarl…

Spring 核心知识点梳理 1

目录 Spring Spring是什么&#xff1f; Spring中重要的模块 Spring中最重要的就是IOC(控制反转)和AOP(面向切面编程) 什么是IOC DI和IOC之间的区别 为什么要使用IOC呢&#xff1f; IOC的实现机制 什么是AOP Aop的核心概念 AOP的环绕方式 AOP发生的时期 AOP和OOP的…

Kafka运维实战 07 - kafka 三节点集群部署(混合模式)(KRaft 版本3.7.0)

目录环境准备主机准备补充说明JDK安装 (三台主机分别执行)下载jdkjdk安装kafka 部署(三台主机分别执行)kafka 下载kafka 版本号结构解析kafka 安装下载和解压安装包(3台主机都执行)配置 server.properties &#xff08;KRaft 模式&#xff09;192.168.37.10192.168.37.11192.16…

linux内核与GNU之间的联系和区别

要理解操作系统&#xff08;如 GNU/Linux&#xff09;的组成&#xff0c;需要明确 内核&#xff08;Kernel&#xff09; 和 GNU 工具链 各自的功能&#xff0c;以及它们如何协作构成完整的操作系统。以下是详细分析&#xff1a;1. 内核&#xff08;Kernel&#xff09;的功能 内…

文件包含学习总结

目录 漏洞简介 漏洞原理 漏洞分类 漏洞防御 漏洞简介 程序开发人员一般会把重复使用的函数写到单个文件中&#xff0c;需要使用某个函数时直接调用此文件&#xff0c;而无需再次编写&#xff0c;这种文件调用的过程一般被称为文件包含。程序开发人员一般希望代码更灵活&…

TQZC706开发板教程:创建PCIE项目

本例程基于zc706开发板&#xff0c;使用xdma核创建PCIE项目&#xff0c;最终实现插入主机可识别出Xilinx设备。在vivado中创建一个空的706项目。创建完成后添加IP核-->搜索xdma-->双击打开配置。添加XDMA核如下所示basic配置peic id中设置设备号等信息&#xff0c;这里保…

科技赋能景区生.态,负氧离子气象监测站筑牢清新防线

负氧离子气象监测站&#xff0c;如同景区空气质量的坚固防线&#xff0c;默默守护着每一寸土地的清新。​它以精准的监测能力为防线基石。借助 “吸入式电容收集法”&#xff0c;能敏锐捕捉空气中负氧离子的踪迹&#xff0c;精准测量其浓度&#xff0c;同时将温度、湿度、PM2.5…

AMD官网下载失败,不让账户登录下载

别使用163邮箱 使用QQ邮箱&#xff0c;然后用GPT生成一个外国&#xff0c;比如日本的地区信息填上去就可以下载了

Elasticsearch-8.17.0 centos7安装

下载链接 https://www.elastic.co/downloads/past-releases/elasticsearch-8-17-0 https://www.elastic.co/downloads/past-releases/logstash-8-17-0 https://www.elastic.co/cn/downloads/past-releases/kibana-8-17-0https://artifacts.elastic.co/downloads/elasticsearch/…

windows下SAS9.4软件下载与安装教程

SAS 9.4是SAS公司推出的一款功能强大的统计分析软件&#xff0c;广泛应用于数据分析、商业智能、预测分析、数据挖掘及统计建模等多个领域。数据处理与管理能力&#xff1a;SAS 9.4支持多种数据格式的导入导出&#xff0c;包括JSON、XML等&#xff0c;便于处理来自Web和API的数…

MyBatis-Plus极速开发指南

MyBatis-Plus简介MyBatis-Plus 是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;简化开发&#xff0c;提高效率。它提供了以下主要特性&#xff1a;无侵入&#xff1a;只做增强不做改变&#xff0c;引入它不会对现有工程产生影响强大的 …

Django接口自动化平台实现(五)

8. 测试用例执行 预期效果如下&#xff1a;用例执行逻辑如下&#xff1a;前端提交用例 id 列表到后台&#xff0c;后台获取每一条用例的信息&#xff1b;后台获取域名信息、用例 id 列表&#xff1b;对用例的请求数据进行变量的参数化、函数化等预处理操作&#xff1b;根据先后…