【安卓筆記】用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,一位執棋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;根據先后…