安卓开发(试水篇)

Prerequisite

Android Studio 下载网站:https://developer.android.com/studio 【默认安装最新版】

前置知识

  • SDK 全称 Software Development Kit,译为软件开发工具包
  • Android Studio 运行项目途径
    • 真机(建议)
    • 模拟器
    • IDE 自带虚拟设备(不建议)
  • 采用 XML 文件进行配置工程相关参数(如布局)

环境

【下载 SDK 组件】下载刚完成,还需要配置一些东西,但我忘了
【环境变量】打开 SDK 安装目录(C:\Users\XXX\AppData\Local\Android\Sdk),将以下两个目录添加到环境变量

  • C:\Users\XXX\AppData\Local\Android\Sdk\platform-tools
  • C:\Users\XXX\AppData\Local\Android\Sdk\tools
    【真机调试】我用的是小米 6,需要开启开发者模式,并开启 USB 调试USB 安装

终端常用命令

更多参考

# 查看设备
adb devices
# 关闭 adb
adb kill-server
# 开启 adb
adb start-server
# 进入 shell 模式
adb shell
# 查看安卓版本
adb shell getprop ro.build.version.release
# 查看 SDK 版本
adb shell getprop ro.build.version.sdk
# 获取 root 权限(确保已经获取权限)
adb root
# 安装 apk
adb install C:\Users\xxx\Desktop\Magisk.apk

快捷键(官方指南

  • 代码格式化【Ctrl + Alt + L】

入门目标

  1. 安卓 UI 和后台逻辑
  2. 网络请求
  3. 序列化和反序列化
  4. 保存 XML 文件

最终可以实现一个含有登录和跳转功能的小型 app
PS:创建项目选择 empty project

安卓 UI 和后台逻辑

切换到 Project 项目视图,其中后台逻辑和 UI 界面的功能是相对应的

  • main
    • java
      • com.example.android_1
        • MainActivity【后台逻辑】
    • res
      • layout
        • activity_main.xml【UI 界面】
      • values
        • strings.xml【字符串变量】
<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="150dp"
        android:background="#ddd"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@string/txt"
            android:textAlignment="center"
            android:textSize="20dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:paddingLeft="50dp"
            android:paddingRight="50dp">

            <TextView
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:gravity="right"
                android:text="@string/user" />

            <EditText
                android:id="@+id/txt_user"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:inputType="text"></EditText>

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:paddingLeft="50dp"
            android:paddingRight="50dp">

            <TextView
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:gravity="right"
                android:text="@string/pwd" />

            <EditText
                android:id="@+id/txt_pwd"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:inputType="textPassword"></EditText>

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center">

            <Button
                android:id="@+id/btn_login"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="10dp"
                android:text="登 录"></Button>

            <Button
                android:id="@+id/btn_reset"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="重 置"></Button>

        </LinearLayout>

    </LinearLayout>
</LinearLayout>

上面代码是实现 UI 界面,下面会详细介绍:

一、XML 基本格式

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <!-- 此时就相当于 HTML 中的空壳 <div><div> -->
    <!-- LinearLayout 表示线性布局 -->
</LinearLayout>

二、各个组件的属性

// 组件类型
LinearLayout 是线性布局
TextView 是文字
EditText 是输入框
Button 是按钮

// 组件属性(android)

// LinearLayout
layout_width="match_parent" 长度匹配父组件
layout_height="200dp" 宽度200dp
layout_marginTop="150dp" 距离顶部15dp
background="#ddd" 背景颜色是灰色
orientation="vertical" 子组件方向竖着排

// TextView
text="@string/txt" 文字引用变量txt
textAlignment="center" 文字内容居中
textSize="20dp" 文字大小20dp

// EditText
id="@+id/txt_pwd" 关联绑定
inputType="textPassword" 输入内容是密码类型

// Button
layout_height="wrap_content" 高度是内容的长度

关于 dp 和 px 的区别:

  • dp 是虚拟像素,在不同的像素密度的设备上会自动适配
  • px 是真实像素,是固定的
  • 例如当像素密度为 160,1 dp = 1 px
  • 例如当像素密度为 240,1 dp = 1.5 px

三、存储变量的 XML

<resources>
    <string name="app_name">Project_1</string>
    <string name="txt">用户登录</string>
    <string name="user">用户名:</string>
    <string name="pwd">密码:</string>
</resources>

四、AndroidManifest.xml

AndroidManifest.xml 是文件是整个应用程序的信息描述文件,每当新建一个 xml 页面时,就要多加一个 activity 组件

比如下面展示我有两个页面(HomeMainActivity):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Android_1"
        tools:targetApi="31">
        <activity android:name=".Home"></activity>
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

UI 界面写完了,就轮到交互环节了

package com.example.android_1;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.w3c.dom.Text;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView txtUser = findViewById(R.id.txt_user);
        TextView txtPwd = findViewById(R.id.txt_pwd);

        Button btnLogin = findViewById(R.id.btn_login);
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e("日志", "登入");
            }
        });

        Button btnReset = findViewById(R.id.btn_reset);
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e("日志", "重置");
                txtUser.setText("");
                txtPwd.setText("");
            }
        });
    }
}

最后点击运行即可,这就是真机上最简易的 app

网络请求

采用安卓真机模拟发送网络请求,电脑端模拟 FLASK 服务器的方式

首先安卓发送网络请求(okhttp)需要进行以下配置:

  • src/build.gradle 的 dependencies 中添加 implementation "com.squareup.okhttp3:okhttp:4.9.1"
  • src/main/AndroidManifest.xml 中添加 <uses-permission android:name="android.permission.INTERNET" />
  • src/main/res/xml 中新建 network_security_config.xml 并添加以下内容(仅测试)
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!--禁用掉明文流量请求的检查-->
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
  • src/main/AndroidManifest.xml 中添加 android:networkSecurityConfig="@xml/network_security_config"(仅测试)

下面代码是完整的 MainActivity.java 代码,用于封装和发送网络请求(其他请求方式参考这篇文章

package com.example.android_1;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.IOException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import okhttp3.Call;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class MainActivity extends AppCompatActivity {

    // 各个组件类型
    private TextView txtUser, txtPwd;
    private Button btnLogin, btnReset;
    public Context mContext;

    // 启动执行的函数,相当于主函数
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;

        initView();
        initListener();
    }

    // 获取对应组件
    private void initView() {
        txtUser = findViewById(R.id.txt_user);
        txtPwd = findViewById(R.id.txt_pwd);

        btnLogin = findViewById(R.id.btn_login);
        btnReset = findViewById(R.id.btn_reset);
    }

    // 初始化按钮
    private void initListener() {
        // 登录按钮
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginForm();
            }
        });

        // 重置按钮
        btnReset.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("日志", "重置");
                txtUser.setText("");
                txtPwd.setText("");
            }
        });
    }

    // 登录按钮执行的函数
    private void LoginForm() {
        Log.e("日志", "登录");

        // 用于存储用户信息
        // 效果:{username:"123456", password:"666666"}
        TreeMap<String, String> dataMap = new TreeMap<String, String>();

        // 获取用户输入的用户名和密码
        HashMap<String, TextView> objMap = new HashMap<String, TextView>();
        objMap.put("username", txtUser);
        objMap.put("password", txtPwd);
        for (Map.Entry<String, TextView> entry : objMap.entrySet()) {
            String key = entry.getKey();
            TextView obj = entry.getValue();
            String value = String.valueOf(obj.getText());
            dataMap.put(key, value);
        }

        // 用于校验用户输入信息
        // 效果:password666666username123456
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : dataMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            sb.append(key);
            sb.append(value);
        }
        String dataString = sb.toString();
        String signString = md5(dataString);
        // dataMap = {username:"123456", password:"666666", sign:"5a231fcdb710d73268c4f44283487ba2"}
        dataMap.put("sign", signString);

        // 使用线程,发送 HTTP 网络请求
        new Thread() {
            @Override
            public void run() {
                OkHttpClient client = new OkHttpClient.Builder().build();
                // 发送 post 请求
                FormBody form = new FormBody.Builder()
                        .add("user", dataMap.get("username"))
                        .add("pwd", dataMap.get("password"))
                        .add("sign", dataMap.get("sign"))
                        .build();
                // 请求网址为测试用本地 FLASK 服务器 IP
                Request req = new Request.Builder().url("http://10.10.10.111:5000/auth").post(form).build();
                Call call = client.newCall(req);
                try {
                    Response res = call.execute();
                    ResponseBody body = res.body();
                    String bodyString = body.string();
                    // 接收示例:{"status":true, "token":"b96efd24-e323-4efd-8813-659570619cde"}
                    Log.e("获取相应的内容 -->", bodyString);
                } catch (IOException ex) {
                    Log.e("请求异常 -->", "网络错误");
                }
            }
        }.start();
    }

    // MD5 加密函数
    private String md5(String dataString) {
        try {
            MessageDigest instance = MessageDigest.getInstance("MD5");
            byte[] nameBytes = instance.digest(dataString.getBytes());

            // 十六进制展示
            StringBuilder sb = new StringBuilder();
            for (byte nameByte : nameBytes) {
                int val = nameByte & 255; // 负数转换为正数
                if (val < 16) {
                    sb.append("0");
                }
                sb.append(Integer.toHexString(val));
            }
            return sb.toString();
        } catch (Exception e) {
            return null;
        }

    }
}

接着需要获取本机 IP(局域网),方法如下:

# cmd 直接运行下面命令
python -c "import os, re; print(''.join(re.findall('无线局域网适配器 WLAN:?\n.*\n.*\n.*\n.*IPv4 地址 [\. ]+:(.*)', os.popen('ipconfig').read())[0].split()))"

下面代码是完整的 FLASK 服务器 Python 代码,用于模拟服务器接收网络请求,并返回对应内容

#! /usr/bin/env python
# -*- coding: UTF-8 -*-
# @Author: Xiaotuan

from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route('/auth', methods=['POST'])
def auth():
    print(request.form)
    # 1. 获取各个参数
    user = request.form.get("user")
    pwd = request.form.get("pwd")
    sign = request.form.get('sign')

    # 2. sign 签名的校验(略)

    # 3. 根据用户名和密码去数据库校验,并返回对应内容
    return jsonify({
        'status': True,
        'token': "b96efd24-e323-4efd-8813-659570619cde"
    })


if __name__ == '__main__':
    # 电脑本机 IP
    app.run(host='10.10.10.111')

序列化和反序列化

首先简单介绍一下概念:

  • 序列化:对象 -> 字符串
  • 反序列化:字符串 -> 对象

然后进行配置(添加 Gson 组件):

  • src/build.gradle 的 dependencies 中添加 implementation 'com.google.code.gson:gson:2.8.6'"

接着会用到反序列化(获取登录状态 + token),并创建新的 HttpResponse.java 类文件专门处理

package com.example.android_1;

class HttpResponse {
    public boolean status;
    public String token;
}

MainActivity.java 文件中添加下面代码:

// 获取登录状态 + token
HttpResponse obj = new Gson().fromJson(bodyString, HttpResponse.class);

保存 XML 文件

在安卓真机上,app 保存的 token 会存储在 XML 文件中(data/data/com.example.android_1),可使用 adb 来查看,也可以直接在 Android Studio 右侧查看(也只能查看,毕竟没有 root 权限)

使用下面 java 代码可直接处理 XML 文件(三种方式):

  • 保存
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("token", "b96efd24-e323-4efd-8813-659570619cde");
editor.commit();
  • 删除
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove("token");
editor.commit();
  • 读取
SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
String token = sp.getString("token", "");

最后更新在 MainActivity.java 文件中修改的代码

try {
  Response res = call.execute();
  ResponseBody body = res.body();
  String bodyString = body.string();
  // 接收示例:{"status":true, "token":"b96efd24-e323-4efd-8813-659570619cde"}

  // 1. 获取登录状态 + token
  HttpResponse obj = new Gson().fromJson(bodyString, HttpResponse.class);

  // 2. token 保存手机 -> 本地 XML 文件(登录凭证保存到 cookie)
  SharedPreferences sp = getSharedPreferences("sp_city", MODE_PRIVATE);
  SharedPreferences.Editor editor = sp.edit();
  editor.putString("token", obj.token);
  editor.commit();

  // 3. 验证成功,跳转到新页面
  Intent in = new Intent(mContext, Home.class);
  startActivity(in);

  Log.e("获取相应的内容 -->", bodyString);
} catch (IOException ex) {
  Log.e("请求异常 -->", "网络错误");
}
posted @ 2022-12-03 21:21  筱团  阅读(233)  评论(0编辑  收藏  举报