【无标题】

EE308FZ Lab_2 Back-end separation calculator programming based on Android studio

The Link Your Class2301-MUSE Community
The Link of Requirement of This AssignmentSecond assignment-- Back-end separation calculator programming
The Link of The Code of This ProjectEE308FZ Lab_2 Back-end separation calculator programming
Second assignment-- Back-end separation calculator programmingCreate a calculator with a visual interface and write a blog to record your work content and process based on the front and back end separation.
MU STU ID and FZU STU ID21126569_832102101

Ⅰ. PSP form

Personal Software Process StagesEstimated Time(minutes)Actual Time(minutes)
Planning6550
• Estimate6550
Development395450
• Analysis5050
• Design Spec3545
• Design Review3540
• Coding Standard3540
• Design8080
• Coding180195
• Code Review8080
• Test80120
Reporting6080
• Test Repor1010
• Size Measurement1010
• Postmortem & Process Improvement Plan4060
Sum700760

Ⅱ. Introduction

This blog will guide you through the process of creating a personalized Android calculator application utilizing the Android studio IDE, which covers fundamental mathematical operations, UI design, SQLite database and the front-end and back-end separation techniques.
Particularly, this blog provides insights into implementing diverse functions, for an instance, addition, subtraction, multiplication, division, power, reverse and basic trigonometric functions. The scientific calculator implemented in this project includes the following features:
· Basic arithmetic operations (addition, subtraction, multiplication, division).
· Exponentiation (x^y) operations, logarithmic(log) operations
· Trigonometric functions (sin(x), cos(x), and tan(x)) operations.
· Percentage calculation (%)
· Prioritized bracket() operation
· Read the last ten history records or clear them
· Other innovative features: Calculation result precision selection and Clipboard Copy action.
Link to the finished project code:EE308FZ Lab_2 Back-end separation calculator programming

Ⅲ. Description of problem-solving ideas.

Planning

In order to make an Android calculator with front end separation, we need to solve three parts: the first part is the design of the front end. The front-end realizes the interaction between mobile software and users through simple Activity and layout. The second part is how the data is transferred to the server. The third part is the back-end writing.

Front - end – Utilizing Android Studio

Android Studio is a powerful Integrated development environment (IDE) specifically tailored for Android application development. It includes excellent code editors, debugging tools, and UI design tools that significantly improve development efficiency. Additionally, Android is a massive platform with billions of device users around the world. This means that applications developed have the opportunity to reach a broad global user base. This broad user base can help your app get more downloads and usage, contributing to the success and popularity of your app.

Server communication protocol – Utilizing MQTT protocol

Modern Android apps no longer allow data transfer using the HTTP protocol. HTTP is no longer considered a secure data transfer protocol in Android app because it transmits data in plain text, which means that data can be easily stolen and tampered with in transit. Therefore, this project uses MQTT communication protocol. MQTT is a lightweight and efficient messaging protocol suitable for scenarios with low bandwidth, high latency or unstable connections. And MQTT supports real-time messaging and is ideal for applications that require real-time data synchronization, such as your calculator.

Back-end – Java Spring Frame

The back-end application uses the Java programming language, built on the Spring framework, and uses Spring’s JdbcTemplate to perform database operations, while using Java’s ScriptEngine to perform mathematical expression calculations. Together, these technologies and tools enable back-end applications to handle database operations and mathematical calculations, and provide rich functionality and flexibility.

Ⅳ. Design and implementation process

The user enters mathematical expressions through buttons provided in the front-end application interface. These buttons can be used to build expressions, such as addition, subtraction, multiplication, etc. The user-built expression forms a mathematical computation request. The user triggers the “Calculate” operation in the front-end application, which sends the mathematical calculation request to the back-end server through the MQTT communication protocol.
The backend server receives the MQTT request, parses the mathematical expression submitted by the user, and performs the calculation operation. This involves transforming the user’s input from a textual form into a computable mathematical expression. The backend server performs the computation, gets the result, and stores the result in the database. Database records include computation results and related information such as timestamps. The back-end server returns the calculation results to the front-end application through the MQTT protocol. The front-end application displays this result in the TextView interface.
After the “history” operation is triggered by the front-end application, the corresponding request is sent to the back-end server via MQTT. The backend server reads the history in the database and sends them back to the frontend as a response. When the front-end application receives the history, it displays it on the screen, usually as a list. The user can also click the “Delete History” button on the front-end application interface to request the deletion of history. After the “Delete history” operation is triggered by the front-end application, the corresponding request is sent to the back-end server via MQTT. The backend server performs a database operation to delete the data corresponding to the history selected by the user.
The flow chart of the function structure is as follows:
Flow Chart of the Implementation Process

Ⅴ. Code description

This project mainly contains five Java files and several xml auxiliary files. In this section, I’ll delve into the key aspects of the critical codes. Understanding the design decisions, implementation logic, and underlying concepts is crucial for gaining insights into how the calculator functions.

Front-end Design

1. User Interface Design

Integrated layout

The file defines a layout including the calculator interface. The interface is divided into top and bottom two parts, the top display calculation results and accuracy, the bottom contains the number buttons, operator buttons and other function buttons, through the grid layout of buttons, so as to achieve the calculator UI layout.
(The detailed code has been uploaded to GitHub)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <RelativeLayout
    // Relative layout codes that display calculation results and accuracy
    </RelativeLayout>
 
    <GridLayout
    // Grid layout codes that display buttons
    </GridLayout>
</RelativeLayout>
Buttons styling design

Place the style and shape definitions in XML files. Helps improve code maintainability, readability, reusability, and ease of maintenance and update, while reducing code redundancy.
(The detailed code has been uploaded to GitHub)

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <solid		// codes that set the solid attribute
    <corners	// codes that set the corners attribute
</shape>
Dynamic buttons design

Touch event listener: handles the user’s touch actions on the screen, including not only clicking, but also swiping, dragging, zooming, and other touch gestures. Hence, we can dynamically resize the buttons by monitoring their status.
The interactive effect of button pressing and automatically shrinking can improve user interface interactivity, visual feedback and user experience. This design choice often makes the user interface more appealing, making it easier for users to understand and interact with the application.
(The detailed code has been uploaded to GitHub)

public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                view.setScaleX(0.9f);
                view.setScaleY(0.9f);
                break;
            case MotionEvent.ACTION_UP:
                view.setScaleX(1);
                view.setScaleY(1);
                break;
            default:
        }
        return false;
    }

The final User Interface outcome is as follows:
UI

2. Main activity class

Initialization of variables and buttons

The onCreate method is the method called when the activity is created to set the layout and initialization of the activity. And the init method takes care of further initialization, including setting up the touch event listener, creating the Calculator object (to perform the calculation logic), and getting and setting the TextView control on the interface.

Button click event handling

The onClick method handles what the user does when he clicks individual buttons.
Based on the button ID, use the switch statement to determine which button the user clicked, and call the appropriate method in the Calculator class to handle the button click event.

// Initialization the listener in init function
btn.setOnClickListener(this);
        
public void onClick(View view) {
        // Gets the text content of the current TextView
        String numberText = txContent.getText().toString();
        switch (view.getId()) {
            case R.id.btn_clear:
                numberText = calculator.clickOperator(CalculatorOperator.CLEAR, numberText);
                break;
            case ......}
        txContent.setText(numberText);
    }
                
Set the decimal digits

Display a text box when users click on precision (txPrecision), set will trigger the operation of decimal digits. In click event processing, check the current precision value and reset the precision to 0 if it is greater than 8, otherwise increment the precision value and apply it to the Calculator object. Update the text box content showing precision.
(The exact decimal point is not verified in the database because doing so would affect data storage. Therefore, it is more effective to set the accuracy in the display interface.)

txPrecision.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(precision > 8)
                    precision = 0;
                precision++;
                calculator.setDecimalPlaces(precision);
//                txPrecision.setText(precision);
                txPrecision.setText(String.valueOf(precision));
            }
        });
Clipboard Copy action

When the user clicks the Copy button, this action copies the text content currently displayed to the clipboard. Copy the text to the clipboard by getting the ClipboardManager and creating a ClipData object. At the same time, a short notification message is displayed informing the user that the text has been copied to the clipboard.

				// Get the clipboard manager
                ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
                // Create a ClipData object that contains the text to copy
                ClipData clipData = ClipData.newPlainText("text", numberText);
                // 将ClipData对象复制到剪贴板
                clipboardManager.setPrimaryClip(clipData);
                // Prompts the user that the text has been copied to the clipboard
                Toast.makeText(this, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show();
                break;

3. Handle User Input:

Button Handling

When the button is clicked, two basic functions are generated as well as a few special extras:

  1. Appends the text content displayed on the button to the text editing box called editText. Used to display text
  2. The text content on the button is appended to a StringBuilder object named str for transmission of the formula via MQTT protocol
// 处理按钮点击事件
    public void clickButton(View view) {
        Button button = (Button) view;
        // 将按钮上显示的文本内容追加到名为 editText 的文本编辑框
        editText.append(button.getText());
        // 将按钮上的文本内容追加到名为 str 的 StringBuilder 对象中
        currentNumber.append(button.getText());
        formula += " " + button.getText(); // 将按钮文本添加到 formula 中
    }

    public void emptyButtonClicked(View view) {
        editText.setText(null);
        // 对象的长度设置为 0,实际上是清空了它的内容
        currentNumber.setLength(0);
        formula = "";
        finalResult = "";
    }

    public void equalButtonClicked(View view) {
        indexYN = 0;
        estimate();
        if (indexYN == 0) {
        	String formularSend = "formula=" + formula;
        	SendFormularMqttUtil sendFormularMqttUtil = new SendFormularMqttUtil(data, MainActivity.this);
        	sendFormularMqttUtil.sendFormular();
        	while(sendFormularMqttUtil.arrivedResult != "noResult"){}
        	editText.append("\n" + sendFormularMqttUtil.arrivedResult);
        	formula += " =";
        	addToHistory(); // 这里将 formula 和 finalResult 添加到数据库
        	currentNumber.setLength(0);
        	currentNumber.sendFormularMqttUtil.arrivedResult);
        }
    }

    public void powerButtonClicked(View view) {
        editText.append("^");
        currentNumber.append("^");
        formula += " ^";
    }

    public void eulerButtonClicked(View view) {
        editText.append("e");
        currentNumber.append("e");
        formula += " e";
    }

    public void bracketClicked(View view) {
        Button button = (Button) view;
        // 将按钮上显示的文本内容追加到名为 editText 的文本编辑框
        editText.append(button.getText());
        // 将按钮上的文本内容追加到名为 str 的 StringBuilder 对象中
        currentNumber.append(button.getText());
        formula += " " + button.getText(); // 将按钮文本添加到 formula 中
    }

    public void factorialButtonClicked(View view) {
        editText.append("!");
        currentNumber.append("!");
        formula += " !";
    }

    public void deleteButtonClicked(View view) {
        String nowText = editText.getText().toString();
        if (!nowText.isEmpty() && currentNumber.length() != 0) {
            editText.setText(nowText.substring(0, nowText.length() - 1));
            currentNumber.deleteCharAt(currentNumber.length() - 1);
        }
    }

    public void percentageButtonClicked(View view) {
        editText.append("%");
        currentNumber.append("*0.01");
        formula += " %";
    }

    public void sinButtonClicked(View view) {
        editText.append("sin");
        currentNumber.append("s");
        formula += " sin";
    }

    public void cosButtonClicked(View view) {
        editText.append("cos");
        currentNumber.append("c");
        formula += " cos";
    }

    public void tanButtonClicked(View view) {
        editText.append("tan");
        currentNumber.append("t");
        formula += " tan";
    }

    public void logButtonClicked(View view) {
        editText.append("log");
        currentNumber.append("o");
        formula += " log";
    }
Set decimal places

setDecimalPlaces function is used to set the number of decimal places selected by the user. The default number is 6. The formatResult function is used to format the result according to the number of decimal places selected by the user, and is formatted using DecimalFormat. Together, these methods realize the function of controllable display of decimal places.

	// 将一个 Double 类型的数转换为字符串,并根据是否可以转换为整数来决定返回的字符串形式。
    // 如果可以转换为整数,返回整数形式的字符串,否则返回原始的双精度浮点数形式的字符串。
    // 这种操作可能用于确保显示数字时不显示多余的小数位。
    private String doubleWithInt(Double number) {
        boolean flag = number.longValue() == number;
        if (flag) {
            return String.valueOf(number.longValue());
        }
        return number.toString();
    }
    // 额外功能:实现可控的小数位选择
    private int decimalPlaces = 6; // 用户选择的小数位数,默认为6位

    // 设置小数位数
    public void setDecimalPlaces(int decimalPlaces) {
        this.decimalPlaces = decimalPlaces;
    }
    private String formatResult(double result) {
        // 构造格式化字符串,根据用户选择的小数位数来设置格式
        StringBuilder formatBuilder = new StringBuilder("#.");
        for (int i = 0; i < decimalPlaces; i++) {
            formatBuilder.append("#");
        }
        String formatPattern = formatBuilder.toString();

        // 使用 DecimalFormat 格式化结果
        DecimalFormat decimalFormat = new DecimalFormat(formatPattern);
        return decimalFormat.format(result);
    }

4. Transmission by MQTT protocol

Use the MQTT protocol to establish connections in Android applications, subscribe to topics, publish messages, and handle connection status and callbacks to messages. To communicate with remote servers and transfer mathematical expressions.

Subscribe to specific topics

The subscribeTopic method is used to subscribe to the specified MQTT topic. This method is implemented by calling the subscribe method, and if the subscription is successful, the corresponding log message is printed.

/**
     * 订阅特定的主题
     * @param topic mqtt主题
     */
    public void subscribeTopic(String topic) {
        try {
            mqttAndroidClient.subscribe(topic, 0, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    Log.i(TAG, "subscribed succeed");
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    Log.i(TAG, "subscribed failed2");
                }
            });

        } catch (MqttException e) {
            e.printStackTrace();
        }
    }
publish to specific topics

The publishMessage method is used to publish messages to the MQTT server. This method is implemented by calling the publish method, which prints the corresponding log information if the publication is successful.

/**
     * 向默认的主题/user/update发布消息
     * @param formularString 消息载荷
     */
    public void publishMessage(String formularString) {
        try {
            if (mqttAndroidClient.isConnected() == false) {
                mqttAndroidClient.connect();
            }

            MqttMessage message = new MqttMessage();
            message.setPayload(formularString.getBytes());
            message.setQos(0);
            mqttAndroidClient.publish(PUB_TOPIC, message, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    Log.i(TAG, "publish succeed!");
                    Log.i("TestAccount", formularString);
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    Log.i(TAG, "publish failed!");
                }
            });
        } catch (MqttException e) {
            Log.e(TAG, e.toString());
            e.printStackTrace();
        }
    }
Send the formular via MQTT

The sendFormularmethod is used to send mathematical expressions to the MQTT server.
First, create the MqttConnectOptions object and configure the user name and password. Then, it creates the MqttAndroidClient object and set the callback interface. Invoke the connect method to establish an MQTT connection. The process is asynchronous and the result of the connection is notified via a callback. If the connection is successful, call subscribeTopic to subscribe to a topic and call publishMessage to publish the mathematical expression. If the connection fails, the corresponding log information is printed.

protected void sendFormular() {
        /* 创建MqttConnectOptions对象并配置username和password */
        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
        mqttConnectOptions.setUserName(userName);
        mqttConnectOptions.setPassword(passWord.toCharArray());

        /* 创建MqttAndroidClient对象, 并设置回调接口 */
        //host:表示 MQTT 服务器的主机地址,即要连接的服务器的地址。
        //clientId:表示客户端的唯一标识符,用于在 MQTT 服务器上标识客户端。
        mqttAndroidClient = new MqttAndroidClient(context, host, clientId);
        mqttAndroidClient.setCallback(new MqttCallback() {
            @Override
            public void connectionLost(Throwable cause) {   //当与 MQTT 服务器的连接丢失时调用
                Log.i(TAG, "connection lost");
            }
            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {    //当从 MQTT 服务器接收到新消息时调用 - 获取返回的结果
                Log.i(TAG, "topic: " + topic + ", msg: " + new String(message.getPayload()));
                String payload = new String(message.getPayload());
                if (topic.equals(PUB_TOPIC)) {
                    processReceivedData(payload);
                }
            }
            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {    //当成功传递消息到 MQTT 服务器时调用
                Log.i(TAG, "msg delivered");
            }
        });
        /* Mqtt建连 */
        //整个建连过程是异步的,connect 方法会立即返回,实际连接的结果会通过回调函数通知。
        try {
            mqttAndroidClient.connect(mqttConnectOptions,null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    // 当连接成功建立时,会打印日志信息表示连接成功,并调用 subscribeTopic 方法来订阅指定的主题
                    Log.i(TAG, "connect succeed");

                    subscribeTopic(SUB_TOPIC);
                    publishMessage(formularString);
                }
                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    //当连接建立失败时,会打印日志信息表示连接失败
                    Log.i(TAG, "connect failed1");
                }
            });
        } catch (MqttException e) {
            e.printStackTrace();
        }

    }

5. Back-end Design

The backend mainly contains the calculationResult class, the controller class, the expressionRequest class. The back-end primarily has 2 functions:

  1. Receive the formula from the front end, and then calculate it and return the calculation result.
  2. Store calculation formulas and results in the database. And we can obtain the historical records or delete the history records.
Handling MQTT requests

It receives MQTT messages, evaluate expressions, and store the back-end logic of the results. It performs these operations when a new message arrives on the MQTT topic, allowing you to send mathematical expressions through MQTT and receive the corresponding calculation results.

//接收通过mqtt请求接收表达
public void calculateExpression (String topic) throws MqttException {
    mqttClient.subscribe(topic, (topic1, message) -> {
    String expression = request.getExpression();
    double result = evaluateExpression(expression);
    saveHistory(result);
    });
}
//储存结果到数据库
 public void saveHistory(double result) {
    int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM history", Integer.class);
    if (count >= 10) {
        jdbcTemplate.update("DELETE FROM history ORDER BY timestamp LIMIT 1");
    }
    jdbcTemplate.update("INSERT INTO history (result) VALUES (?)", result);
}
Calculate the expression

This function is used to convert mathematical expressions into JavaScript expressions, then perform expression evaluation using ScriptEngine, and finally return the result of the evaluation. This allows you to perform mathematical calculations in the back-end server and return the results to the client.

//计算
public double evaluateExpression(String expression) {
    try {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        Object result = engine.eval(expression);
        if (result instanceof Number) {
            BigDecimal bd = new BigDecimal(((Number) result).doubleValue()).setScale(7, RoundingMode.HALF_UP);
            return bd.doubleValue();
        } else {
            throw new RuntimeException("Invalid expression result");
        }
    } catch (ScriptException | javax.script.ScriptException e) {
        e.printStackTrace();
        throw new RuntimeException("Failed to evaluate expression: " + expression, e);
    }
}
Return to front-end history

This function queries the results of the most recent calculations in the database and sends them back to the front-end application in the form of MQTT messages so that the front-end application can obtain and display these historical calculations. This provides a way to send data forward to fulfill front-end requests.

//返回前端
public void subscribeAudio(String topic) throws MqttException {
    mqttClient.subscribe(topic, (topic2, message) -> {
               MqttMessage mqttMessage = new MqttMessage();
        String test= String.valueOf(jdbcTemplate.queryForList("SELECT result FROM history ORDER BY id DESC LIMIT 10", Double.class);
);
        mqttMessage.setPayload(test.getBytes(StandardCharsets.UTF_8));
        mqttClient.publish("frontend",mqttMessage);
    });
}

6. Testing, Debugging and Error Handling

In software development, testing and debugging are critical steps to ensure the accuracy and stability of a calculator application. By thoroughly testing a wide range of mathematical calculations and user input situations, we can ensure that the calculator works correctly in all situations
In order to ensure that the calculator can perform accurate mathematical calculations, I have conducted thorough testing, which includes testing basic arithmetic operations (addition, subtraction, multiplication, division) as well as special functions (such as trigonometric functions, percentages, etc.).
Users may enter invalid expressions or operators, such as dividing by zero, invalid operator combinations, and so on. When user input is invalid, the user is alerted to an error by toasting.

Additional content

Separating the front end and the back end makes it easier to scale the functionality of the application, because the front end and the back end are separate and can be developed and maintained separately. But there are some limitations:

  1. Network latency: Compared to native applications, applications with separate front and back ends usually need to communicate with the server over the network, which can cause some network latency, especially if the network connection is unstable.
  2. Server cost: Maintaining and hosting the server incurs a certain cost, especially when dealing with a large number of user requests.

For my Android calculator, I completed the “use front end and back-end separation” requirement while also implementing it by using an SQLite database in the native application. I think this has 2 advantages:

  1. Offline operation: Native applications do not need to rely on a network connection and can continue to function without it, which is important for some applications.
  2. Speed: Native applications are often able to perform computations and data processing faster because they do not need to communicate over the network.

Since it is not the focus of the assignment, I will only give a brief introduction here.

  1. Helper classes for creating and managing the SQLite database, which is used to store the history of the calculator
public class CalculatorDatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "calculator_history.db";
    private static final int DATABASE_VERSION = 1;

    public static final String TABLE_HISTORY = "calculation_history";
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_TIMESTAMP = "timestamp";
    public static final String COLUMN_FORMULA = "formula";
    public static final String COLUMN_RESULT = "result";

    public CalculatorDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE " + TABLE_HISTORY + " (" +
                COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                COLUMN_TIMESTAMP + " DATETIME DEFAULT CURRENT_TIMESTAMP, " +
                COLUMN_FORMULA + " TEXT, " +
                COLUMN_RESULT + " TEXT)";
        db.execSQL(createTable);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
        onCreate(db);
    }

    public long insertHistoryRecord(String formula, String result) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_FORMULA, formula);
        values.put(COLUMN_RESULT, result);
        long newRowId = db.insert(TABLE_HISTORY, null, values);
        db.close();
        return newRowId;
    }
}

  1. Ability to load and display history and delete history. These functions are part of the interaction with the computation history stored in the SQLite database
private void loadHistoryRecords() {
        CalculatorDatabaseHelper dbHelper = new CalculatorDatabaseHelper(this);
        SQLiteDatabase db = dbHelper.getReadableDatabase();

        String[] columns = {CalculatorDatabaseHelper.COLUMN_FORMULA, CalculatorDatabaseHelper.COLUMN_RESULT};
        // 使用 db.query 方法执行数据库查询
        Cursor cursor = db.query(
                CalculatorDatabaseHelper.TABLE_HISTORY,
                columns,            //要检索的列
                null,               //不添加任何筛选条件
                null,               //选择条件的参数
                null,               //分组方式
                null,               //筛选组的筛选条件
                CalculatorDatabaseHelper.COLUMN_TIMESTAMP + " DESC", // 指定排序方式,按时间戳降序排序,以便最新的历史记录显示在前面。
                "10" // 限制获取的历史记录数量,这里限制为10
        );

        while (cursor.moveToNext()) {
            @SuppressLint("Range") String formula = cursor.getString(cursor.getColumnIndex(CalculatorDatabaseHelper.COLUMN_FORMULA));
            @SuppressLint("Range") String result = cursor.getString(cursor.getColumnIndex(CalculatorDatabaseHelper.COLUMN_RESULT));

            // 创建视图并将其添加到 llHistoryRecords 中
            // 你可以使用 TextView 或 CardView 来显示 formula 和 result
            // 添加点击事件监听器,以便用户可以点击历史记录项进行操作

            // 以下是一个示例,创建一个 TextView 来显示 formula 和 result
            TextView historyItem = new TextView(this);
            historyItem.setText("Formula: " + formula + "\nResult: " + result + "\n");

            // 可以设置 TextView 的样式,如文本颜色、字体大小等
            historyItem.setTextSize(25);
            historyItem.setTextColor(Color.BLACK);

            // 添加点击事件监听器,以便用户可以点击历史记录项进行操作
            historyItem.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 在这里添加点击历史记录项后的操作
                    // 例如,可以在计算器中重新运行这个历史记录项的计算
                }
            });

            // 将创建的视图添加到 llHistoryRecords 中
            llHistoryRecords.addView(historyItem);
        }
        cursor.close();
        db.close();
    }

    private void deleteHistoryRecords() {
        CalculatorDatabaseHelper dbHelper = new CalculatorDatabaseHelper(this);
        SQLiteDatabase db = dbHelper.getWritableDatabase();

        int rowsDeleted = db.delete(CalculatorDatabaseHelper.TABLE_HISTORY, null, null);

        // 根据 rowsDeleted 的值来判断是否删除成功
        if (rowsDeleted > 0) {
            // 删除成功,可以在界面上清空历史记录列表
            llHistoryRecords.removeAllViews();
        } else {
            // 删除失败,可以显示相应的提示信息
             Toast.makeText(this, "删除历史记录失败", Toast.LENGTH_SHORT).show();
        }

        db.close();
    }
// 添加方法来将公式和结果添加到数据库
    public void addToHistory() {
        if (formula != null && finalResult != null) {
            SQLiteDatabase db = databaseHelper.getWritableDatabase();
            ContentValues values = new ContentValues();
            values.put(CalculatorDatabaseHelper.COLUMN_FORMULA, formula);
            values.put(CalculatorDatabaseHelper.COLUMN_RESULT, finalResult);
            long newRowId = db.insert(CalculatorDatabaseHelper.TABLE_HISTORY, null, values);
            db.close();
        }
    }
  1. Calculating:
// 将一个中缀表达式(常见的数学表达式写法,如 "2 + 3")的字符串转换成一个列表,该列表包含表达式中的各个操作数、操作符和括号等部分。
    private List<String> tokenizeInfixExpression(String infixExpression) {
        int index = 0; // 追踪在字符串 str 中的当前字符位置
        List<String> tokens = new ArrayList<>();    // 创建了一个字符串列表 list,用于存储中缀表达式中的各个部分

        do {
            char currentChar = infixExpression.charAt(index);

            // 是否是中缀表达式中的操作符或括号之一
            if ("+-*/^!logsct()ep".indexOf(currentChar) >= 0) {
                // If the character is an operator or parentheses, add it to the list.
                index++;
                // 将当前字符 currentChar 转换为字符串,并将其添加到 tokens 列表中
                tokens.add(String.valueOf(currentChar));
            } else if ("0123456789".indexOf(currentChar) >= 0) {
                // If the character is a digit, parse the number and add it to the list.
                String number = "";
                while (index < infixExpression.length() && "0123456789.".indexOf(infixExpression.charAt(index)) >= 0) {
                    number += infixExpression.charAt(index);
                    index++;
                }
                tokens.add(number);
            }
        } while (index < infixExpression.length());

        return tokens;
    }


    // 将中缀表达式转换为后缀表达式,也称为逆波兰表达式。
    // 后缀表达式更容易进行计算,因为它不需要括号来明确操作符的优先级
    public List<String> convertToPostfix(List<String> list) {
        // 栈,用于暂时存储操作符和括号,以便进行后缀表达式的转换。
        Stack<String> fuZhan = new Stack<>();
        // 用于存储最终后缀表达式的字符串列表。
        List<String> list2 = new ArrayList<>();
        if (!list.isEmpty()) {
            for (String item : list) {
                if (isNumber(item)) {
                    // 如果 item 是数字(操作数),将其添加到后缀表达式列表 list2 中
                    list2.add(item);
                } else if (isOperator(item)) {
                    if (item.charAt(0) != '(') {// 如果 item 不是左括号 '(',
                        if (fuZhan.isEmpty()) {//如果操作符栈 fuZhan 为空,将 item 推入栈中
                            fuZhan.push(item);
                        } else {
                            // 如果操作符栈不为空,比较栈顶操作符和当前操作符的优先级(由 adv 函数确定):
                            if (item.charAt(0) != ')') {
                                // 如果当前操作符的优先级大于等于栈顶操作符的优先级,将当前操作符推入栈中
                                if (getOperatorPriority(fuZhan.peek()) <= getOperatorPriority(item)) {
                                    fuZhan.push(item);
                                } else {
                                    // 否则,循环弹出操作符栈中的操作符,并将其添加到后缀表达式列表 list2 中,直到栈顶操作符的优先级小于当前操作符,或者直到栈为空。
                                    while (!fuZhan.isEmpty() && !("(".equals(fuZhan.peek()))) {
                                        if (getOperatorPriority(item) <= getOperatorPriority(fuZhan.peek())) {
                                            list2.add(fuZhan.pop());
                                        }
                                    }
                                    fuZhan.push(item);
                                }
                            } else if (item.charAt(0) == ')') {// 如果 item 是右括号 ')'
                                // 循环弹出操作符栈中的操作符,并将其添加到后缀表达式列表 list2 中,直到遇到左括号 '('。
                                while (!fuZhan.isEmpty() && !("(".equals(fuZhan.peek()))) {
                                    list2.add(fuZhan.pop());
                                }
                                if (!fuZhan.isEmpty() && fuZhan.peek().charAt(0) == '(') {
                                    fuZhan.pop();
                                }
                            }
                        }
                    } else if (item.charAt(0) == '(') {
                        fuZhan.push(item);
                    }
                }
            }
            while (!fuZhan.isEmpty()) {
                list2.add(fuZhan.pop());
            }
        } else {
            editText.setText("");
        }
        return list2;
    }


    public static boolean isOperator(String op) {
        return "0123456789.ep".indexOf(op.charAt(0)) == -1;
    }

    public static boolean isNumber(String num) {
        return "0123456789ep".indexOf(num.charAt(0)) >= 0;
    }

    public static int getOperatorPriority2(String operator) {
        int priority = 0;
        switch (operator) {
            case "+":
            case "-":
                priority = 1;
                break;
            case "*":
            case "/":
                priority = 2;
                break;
            case "^":
            case "!":
            case "g":
            case "l":
            case "o":
            case "s":
            case "c":
            case "t":
                priority = 3;
                break;
        }
        return priority;
    }

    // 定义一个映射表,将操作符映射到它们的优先级
    private static final Map<String, Integer> operatorPriority = new HashMap<>();
    static {
        operatorPriority.put("+", 1);
        operatorPriority.put("-", 1);
        operatorPriority.put("*", 2);
        operatorPriority.put("/", 2);
        operatorPriority.put("^", 3);
        operatorPriority.put("!", 3);
        operatorPriority.put("g", 3);
        operatorPriority.put("l", 3);
        operatorPriority.put("o", 3);
        operatorPriority.put("s", 3);
        operatorPriority.put("c", 3);
        operatorPriority.put("t", 3);
    }
    public static int getOperatorPriority(String f) {
        // 如果操作符在映射表中存在,返回相应的优先级;否则,返回默认值 0
        return operatorPriority.getOrDefault(f, 0);
    }


    public double calculateExpression(List<String> list2) {
        // 存储操作数和中间计算结果
        Stack<String> stack = new Stack<>();
        // 遍历后缀表达式的标记列表 list2,每个标记可以是操作符、操作数或特定的常数
        for (String item : list2) {
            if (isNumber(item)) {
                stack.push(item);
            } else if (isOperator(item)) {
                double res = 0;
                double num1, num2;

                switch (item) {
                    case "e":
                        stack.push(String.valueOf(Math.E));
                        break;
                    case "+":
                        num2 = Double.parseDouble(stack.pop());
                        num1 = Double.parseDouble(stack.pop());
                        res = num1 + num2;
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "-":
                        num2 = Double.parseDouble(stack.pop());
                        num1 = Double.parseDouble(stack.pop());
                        res = num1 - num2;
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "*":
                        num2 = Double.parseDouble(stack.pop());
                        num1 = Double.parseDouble(stack.pop());
                        res = num1 * num2;
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "/":
                        num2 = Double.parseDouble(stack.pop());
                        num1 = Double.parseDouble(stack.pop());
                        if (num2 != 0) {
                            res = num1 / num2;
                            res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                            finalResult = res + "";
                            stack.push(String.valueOf(res));
                        } else {
                            editText.setText("除数不能为0");
                            indexYN = 1;
                        }
                        break;

                    case "^":
                        num2 = Double.parseDouble(stack.pop());
                        num1 = Double.parseDouble(stack.pop());
                        res = Math.pow(num1, num2);
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "!":
                        num1 = Double.parseDouble(stack.pop());
                        if (num1 == 0 || num1 == 1) {
                            res = 1;
                            stack.push(String.valueOf(res));
                        } else if (num1 == (int) num1 && num1 > 1) {
                            int d = 1;
                            for (int j = (int) num1; j > 0; j--) {
                                d *= j;
                            }
                            res = d;
                            res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                            finalResult = res + "";
                            stack.push(String.valueOf(res));
                        } else {
                            editText.setText("阶乘必须为自然数");
                            indexYN = 1;
                        }
                        break;

                    case "g":
                        num1 = Double.parseDouble(stack.pop());
                        res = Math.sqrt(num1);
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "l":
                        num1 = Double.parseDouble(stack.pop());
                        if (num1 > 0) {
                            res = Math.log(num1);
                            res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                            finalResult = res + "";
                            stack.push(String.valueOf(res));
                        } else {
                            editText.setText("ln的x必须大于0");
                            indexYN = 1;
                        }
                        break;

                    case "o":
                        num1 = Double.parseDouble(stack.pop());
                        if (num1 > 0) {
                            res = Math.log(num1) / Math.log(2);
                            res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                            finalResult = res + "";
                            stack.push(String.valueOf(res));
                        } else {
                            editText.setText("log的x必须大于0");
                            indexYN = 1;
                        }
                        break;

                    case "s":
                        num1 = Double.parseDouble(stack.pop());
                        res = Math.sin(num1);
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "c":
                        num1 = Double.parseDouble(stack.pop());
                        res = Math.cos(num1);
                        res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                        finalResult = res + "";
                        stack.push(String.valueOf(res));
                        break;

                    case "t":
                        num1 = Double.parseDouble(stack.pop());
                        if (Math.cos(num1) != 0) {
                            res = Math.tan(num1);
                            res = Double.parseDouble(mathUtils.formatResult(res)); // 格式化结果并重新解析
                            finalResult = res + "";
                            stack.push(String.valueOf(res));
                        } else {
                            editText.setText("tan的x不能为+-(π/2 + kπ)");
                            indexYN = 1;
                        }
                        break;
                }

            }
        }
        if (indexYN == 0) {
            if (!stack.isEmpty()) {
                return Double.parseDouble(stack.pop());
            } else {
                return 0;
            }
        } else {
            return -999999;
        }
    }
  1. Error handling
// 检查用户输入的表达式是否合法,它会检测各种不合法的情况并设置 indexYN 标志以指示错误
    public void estimate() {
        text.setText("");
        int i = 0;
        if (currentNumber.length() == 0) {
            text.setText("输入为空!");
            indexYN = 1;
        }
        if (currentNumber.length() == 1) {
            if ("0123456789ep".indexOf(currentNumber.charAt(0)) == -1) {
                text.setText("输入错误!");
                indexYN = 1;
            }
        }
        if (currentNumber.length() > 1) {
            for (i = 0; i < currentNumber.length() - 1; i++) {
                if ("losctg(0123456789ep".indexOf(currentNumber.charAt(0)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if ("+-*/".indexOf(currentNumber.charAt(i)) >= 0 && "0123456789losctg(ep".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (currentNumber.charAt(i) == '.' && "0123456789".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (currentNumber.charAt(i) == '!' && "+-*/^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if ("losctg".indexOf(currentNumber.charAt(i)) >= 0 && "0123456789(ep".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (currentNumber.charAt(0) == '0' && currentNumber.charAt(1) == '0') {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (i >= 1 && currentNumber.charAt(i) == '0') {
                    int m = i;
                    int n = i;
                    int is = 0;
                    if ("0123456789.".indexOf(currentNumber.charAt(m - 1)) == -1 && "+-*/.!^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                    if (currentNumber.charAt(m - 1) == '.' && "0123456789+-*/.^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                    n -= 1;
                    while (n > 0) {
                        if ("(+-*/^glosct".indexOf(currentNumber.charAt(n)) >= 0) {
                            break;
                        }
                        if (currentNumber.charAt(n) == '.') {
                            is++;
                        }
                        n--;
                    }
                    if ((is == 0 && currentNumber.charAt(n) == '0') || "0123456789+-*/.!^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                    if (is == 1 && "0123456789+-*/.^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                    if (is > 1) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                }
                if ("123456789".indexOf(currentNumber.charAt(i)) >= 0 && "0123456789+-*/.!^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (currentNumber.charAt(i) == '(' && "0123456789locstg()ep".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (currentNumber.charAt(i) == ')' && "+-*/!^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if ("0123456789!)ep".indexOf(currentNumber.charAt(currentNumber.length() - 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
                if (i > 2 && currentNumber.charAt(i) == '.') {
                    int n = i - 1;
                    int is = 0;
                    while (n > 0) {
                        if ("(+-*/^glosct".indexOf(currentNumber.charAt(n)) >= 0) {
                            break;
                        }
                        if (currentNumber.charAt(n) == '.') {
                            is++;
                        }
                        n--;
                    }
                    if (is > 0) {
                        text.setText("输入错误!");
                        indexYN = 1;
                    }
                }
                if ("ep".indexOf(currentNumber.charAt(i)) >= 0 && "+-*/^)".indexOf(currentNumber.charAt(i + 1)) == -1) {
                    text.setText("输入错误!");
                    indexYN = 1;
                }
            }
        }

Ⅵ. Final result display

To conduct a comprehensive evaluation of the developed Android calculator, I performed extensive testing on virtual machine, capturing real-time demonstrations to visually display its capabilities.
After rigorous testing and evaluation, the application software exhibits remarkable proficiency in handling a wide array of fundamental mathematical calculations, showcasing its exceptional accuracy in performing addition, subtraction, multiplication, and division operations. It not only excels in addressing basic arithmetic needs but also caters to more advanced requirements. The Android calculator seamlessly executes power operations, enabling users to calculate power efficiently. Additionally, it seamlessly handles complex mathematical tasks such as three-piece function calculations, demonstrating its versatility and capacity to accommodate various mathematical scenarios and parentheses change the priority.
Furthermore, the calculator’s utility extends beyond mathematical operations, offering convenient features such as a copy function, enabling users to replicate results effortlessly. Its precision extends to decimal place selection, allowing users to tailor the level of precision to their specific needs. These additional functionalities enhance the calculator’s overall appeal and user-friendliness, making it a valuable tool for both basic and advanced mathematical tasks.

The final demonstration of the Android calculator is as follows:

  1. Basic calculator functions:

basic operation

  1. Extended function: Scientific calculator:

Additional operation

  1. My Own Functions:

my own operation

  1. Display of the database:
    在这里插入图片描述

Ⅶ. Assignment Summary

In the process of developing this Android calculator application, I acquired a wealth of knowledge and skills related to front-end and back-end interaction, user input processing, and data presentation. Through this project, I not only gained proficiency in specific technologies and tools, but also developed a deeper understanding of the architecture and interaction principles of Android applications. I now have a clearer grasp of front-end and back-end development, and I’ve accumulated valuable experience in problem-solving and optimizing the user experience.
Here are some key takeaways from what I learned during this project:

  1. Understanding Front-End and Back-End Interaction: By implementing the MQTT protocol for data exchange between the Android front-end and the Java-based Spring back-end, I gained a deeper understanding of how front-end and back-end systems effectively communicate, especially in real-time data synchronization scenarios.
  2. User Input Processing: Designing a user interface for entering mathematical expressions and handling user input was a crucial learning experience. This involved understanding user behavior, designing user-friendly interfaces, and managing various input scenarios.
  3. Data Presentation: Displaying calculation results and history records in an organized and readable manner is a critical part of the application. This required working with Android’s UI components and conducting database queries to present data in an accessible way.
  4. Back-End Technologies: The back-end was built using Java and the Spring framework. Learning to use Spring’s JdbcTemplate for database operations and Java’s ScriptEngine for mathematical expression evaluation was a significant part of the project. This experience expanded my back-end development skills.
  5. Database Management: Storing calculation history records in a database and efficiently retrieving records was a key task, providing insights into database management, especially data storage and retrieval optimization.
  6. Architecture and Principles: This project deepened my understanding of software architecture, particularly the importance of separating concerns between the front-end and back-end. It emphasized the significance of choosing the right communication protocol for specific use cases.
  7. Future Development: The project’s architecture and the use of MQTT make it highly scalable. I gained insights into planning future features and scalability while ensuring that the application remains maintainable and responsive.

In summary, this project equipped me with valuable knowledge and experience in front-end and back-end development, user interaction, data handling, problem-solving, and user experience optimization. It was an excellent opportunity to apply and deepen various skills in the context of a real-world application. Additionally, this project made me realize my lack of proficiency in frameworks, motivating me to learn more about framework-related knowledge while boosting my confidence and passion for Android development.

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值