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

发布于:2025-07-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

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是目前使用最广泛、最好用、最易维护的架构。一定要掌握

 


网站公告

今日签到

点亮在社区的每一天
去签到