0%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
一、单例模式的核心思想
定义:
确保一个类 只有一个实例,并提供一个全局访问点。

生活类比:

公司只有一个CEO(所有人找CEO办事都找同一个人)

电脑只有一个任务管理器(无论打开多少次都是同一个窗口)

二、单例模式的5种Java实现
我们从基础到高级逐步优化:

1️⃣ 饿汉式(线程安全)
java
public class CEO {
// 类加载时就初始化实例
private static final CEO instance = new CEO();

// 私有构造方法
private CEO() {}

// 全局访问点
public static CEO getInstance() {
return instance;
}

public void manageCompany() {
System.out.println("CEO在管理公司");
}
}

// 使用
CEO ceo1 = CEO.getInstance();
CEO ceo2 = CEO.getInstance();
System.out.println(ceo1 == ceo2); // 输出true
特点:

线程安全(因为类加载时初始化)

可能造成资源浪费(如果从未使用过这个实例)

2️⃣ 懒汉式(非线程安全)
java
public class TaskManager {
private static TaskManager instance;

private TaskManager() {}

// 需要时再创建实例(懒加载)
public static TaskManager getInstance() {
if (instance == null) {
instance = new TaskManager();
}
return instance;
}

public void showProcesses() {
System.out.println("显示运行中的进程");
}
}
问题:多线程环境下可能创建多个实例(❌ 不安全)

3️⃣ 懒汉式(线程安全版)
java
public synchronized static TaskManager getInstance() {
if (instance == null) {
instance = new TaskManager();
}
return instance;
}
缺点:每次获取实例都要同步,性能差

4️⃣ 双重检查锁(DCL)
java
public class Database {
private static volatile Database instance;

private Database() {}

public static Database getInstance() {
if (instance == null) { // 第一次检查
synchronized (Database.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Database();
}
}
}
return instance;
}
}
关键点:

volatile 防止指令重排序

两次判空确保线程安全

只在第一次创建时同步

这是Android中最常用的单例实现方式

5️⃣ 静态内部类(推荐)
java
public class SettingsManager {
private SettingsManager() {}

private static class Holder {
static final SettingsManager INSTANCE = new SettingsManager();
}

public static SettingsManager getInstance() {
return Holder.INSTANCE;
}
}
优势:

线程安全(JVM保证类加载的线程安全)

懒加载(只有调用getInstance()时才会加载Holder类)

代码简洁

三、Android中的实际应用
案例1:全局工具类
java
// 网络工具单例
public class NetworkUtils {
private static volatile NetworkUtils instance;

public static NetworkUtils getInstance() {
if (instance == null) {
synchronized (NetworkUtils.class) {
if (instance == null) {
instance = new NetworkUtils();
}
}
}
return instance;
}

private NetworkUtils() {
// 初始化网络相关配置
}

public boolean isNetworkAvailable() {
// 实现网络检查
}
}
案例2:SharedPreferences管理
java
public class PrefsManager {
private static PrefsManager instance;
private SharedPreferences prefs;

private PrefsManager(Context context) {
prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
}

public static synchronized PrefsManager getInstance(Context context) {
if (instance == null) {
instance = new PrefsManager(context.getApplicationContext());
}
return instance;
}

public void saveString(String key, String value) {
prefs.edit().putString(key, value).apply();
}
}
四、单例模式的注意事项
内存泄漏风险:

在Android中避免持有Activity/Fragment的引用

推荐使用Application Context

单元测试困难:

单例难以被Mock或替换

解决方案:考虑依赖注入(如Dagger)

多进程问题:

每个进程会有自己的单例实例

解决方案:使用文件锁或ContentProvider

五、常见问题
Q1:为什么要用双重检查锁?
👉 既保证线程安全,又避免每次获取实例都同步,提升性能。

Q2:volatile关键字的作用?
👉 1. 保证可见性 2. 防止指令重排序(避免返回未初始化完成的对象)

Q3:单例模式违反单一职责原则吗?
👉 确实可能同时承担"创建对象"和"业务逻辑"两个职责,这是它的缺点之一。

Q4:如何破坏单例?如何防御?
👉 破坏方式:

反射:通过反射调用私有构造方法
防御:在构造方法中加判断

java
private Singleton() {
if (instance != null) {
throw new RuntimeException("请使用getInstance()方法");
}
}
反序列化:序列化后再反序列化会创建新对象
防御:实现readResolve()方法

java
protected Object readResolve() {
return getInstance();
}
六、代码模板(直接可用)
java
public class AppManager {
private static volatile AppManager instance;
private Context appContext;

private AppManager(Context context) {
this.appContext = context.getApplicationContext();
}

public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}

// 示例方法
public void doSomething() {
Toast.makeText(appContext, "单例方法被调用", Toast.LENGTH_SHORT).show();
}
}
掌握单例模式后,可以在这些场景中使用它:

全局配置管理

日志工具类

数据库访问

文件系统操作

网络请求客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
一、工厂模式核心思想
解决的问题:
当代码中需要根据不同条件创建不同对象时,避免到处使用 new 关键字,而是将对象创建过程封装起来。

生活类比:

普通方式:自己买面粉、糖、鸡蛋...做蛋糕(相当于直接 new 对象)

工厂模式:去蛋糕店直接点"巧克力蛋糕"(工厂帮你处理复杂创建过程)

二、三种工厂模式的Java实现
1️⃣ 简单工厂模式(静态工厂)
场景:根据类型创建不同类型的对话框

java
// 1. 定义产品接口
interface Dialog {
void show();
}

// 2. 具体产品实现
class AlertDialog implements Dialog {
@Override
public void show() {
System.out.println("显示警告对话框");
}
}

class ProgressDialog implements Dialog {
@Override
public void show() {
System.out.println("显示进度条对话框");
}
}

// 3. 简单工厂
class DialogFactory {
public static Dialog createDialog(String type) {
if ("alert".equals(type)) {
return new AlertDialog();
} else if ("progress".equals(type)) {
return new ProgressDialog();
}
return null;
}
}

// 4. 使用示例
public class Main {
public static void main(String[] args) {
Dialog dialog = DialogFactory.createDialog("alert");
if (dialog != null) {
dialog.show(); // 输出: 显示警告对话框
}
}
}
特点:

通过静态方法创建对象

新增类型需要修改工厂类

2️⃣ 工厂方法模式
场景:跨平台按钮创建(Android/iOS)

java
// 1. 抽象产品
interface Button {
void render();
}

// 2. 具体产品
class AndroidButton implements Button {
@Override
public void render() {
System.out.println("渲染Android风格按钮");
}
}

class IOSButton implements Button {
@Override
public void render() {
System.out.println("渲染iOS风格按钮");
}
}

// 3. 抽象工厂
interface ButtonFactory {
Button createButton();
}

// 4. 具体工厂
class AndroidButtonFactory implements ButtonFactory {
@Override
public Button createButton() {
return new AndroidButton();
}
}

class IOSButtonFactory implements ButtonFactory {
@Override
public Button createButton() {
return new IOSButton();
}
}

// 5. 使用示例
public class Main {
public static void main(String[] args) {
ButtonFactory factory = new AndroidButtonFactory(); // 只需切换工厂
Button button = factory.createButton();
button.render(); // 输出: 渲染Android风格按钮
}
}
特点:

每个产品对应一个工厂类

符合开闭原则(扩展时不修改原有代码)

3️⃣ 抽象工厂模式
场景:创建整套UI控件(按钮+文本框)

java
// 1. 抽象产品族
interface TextBox {
void display();
}

interface Button {
void click();
}

// 2. Android实现
class AndroidTextBox implements TextBox {
@Override
public void display() {
System.out.println("Android文本框");
}
}

class AndroidButton implements Button {
@Override
public void click() {
System.out.println("Android按钮被点击");
}
}

// 3. iOS实现
class IOSTextBox implements TextBox {
@Override
public void display() {
System.out.println("iOS文本框");
}
}

class IOSButton implements Button {
@Override
public void click() {
System.out.println("iOS按钮被点击");
}
}

// 4. 抽象工厂
interface UIFactory {
TextBox createTextBox();
Button createButton();
}

// 5. 具体工厂
class AndroidUIFactory implements UIFactory {
@Override
public TextBox createTextBox() {
return new AndroidTextBox();
}

@Override
public Button createButton() {
return new AndroidButton();
}
}

class IOSUIFactory implements UIFactory {
@Override
public TextBox createTextBox() {
return new IOSTextBox();
}

@Override
public Button createButton() {
return new IOSButton();
}
}

// 6. 使用示例
public class Main {
public static void main(String[] args) {
UIFactory factory = new AndroidUIFactory(); // 切换为IOSUIFactory即可换整套UI

TextBox textBox = factory.createTextBox();
Button button = factory.createButton();

textBox.display(); // 输出: Android文本框
button.click(); // 输出: Android按钮被点击
}
}
特点:

生产相关联的产品族

保证产品兼容性

三、Android实际应用案例
案例1:BitmapFactory
java
// 使用工厂隐藏复杂的Bitmap解码过程
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
案例2:RecyclerView的ViewHolder创建
java
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根据viewType返回不同的ViewHolder(工厂方法模式)
if (viewType == TYPE_HEADER) {
return new HeaderViewHolder(inflater.inflate(R.layout.header, parent, false));
} else {
return new ItemViewHolder(inflater.inflate(R.layout.item, parent, false));
}
}

LayoutInflater.from(context)

NotificationCompat.Builder

Retrofit.create()
也是用的工厂模式
四、工厂模式对比总结
模式 优点 缺点 适用场景
简单工厂 调用简单 违反开闭原则 对象类型较少且稳定
工厂方法 扩展性强 类数量增多 需要灵活扩展(如插件系统)
抽象工厂 保证产品兼容性 复杂度高 成套产品(如UI主题/跨平台)

···
AGP 版本 Gradle 版本 Kotlin 插件版本 说明
4.2.2 6.7.1 1.5.21(推荐) 最后一个支持 compile 的版本
7.0.2 7.0.2 1.5.31 Android Studio Arctic Fox
7.1.3 7.2 1.6.10 Android Studio Bumblebee
7.2.2 7.3 1.6.21 Android Studio Chipmunk
7.3.1 7.4 1.7.10 Android Studio Dolphin
7.4.2 7.5 1.8.10 Android Studio Electric Eel
8.0.2 8.0 1.8.21 Android Studio Flamingo
8.1.2 8.1 1.9.0 Android Studio Giraffe
8.2.2 8.2 1.9.10 Android Studio Hedgehog
8.3.1+ 8.4 2.0.0(兼容) Android Studio Iguana(K2 编译器)

✅ 示例配置(以 AGP 8.1.2 为例)
build.gradle(Project):

buildscript {
ext.kotlin_version = ‘1.9.0’
dependencies {
classpath ‘com.android.tools.build:gradle:8.1.2’
classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version”
}
}
gradle-wrapper.properties:

distributionUrl=https://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
✅ Kotlin DSL(build.gradle.kts)用户

plugins {
id(“com.android.application”) version “8.1.2” apply false
kotlin(“android”) version “1.9.0” apply false
}
⚠️ 注意事项
Kotlin 1.9+ 之后使用了新的 K2 编译器,速度提升但有兼容性变化。

从 AGP 8.0 开始必须使用命名空间(即 namespace = “xxx” 取代 manifest package)。

Kotlin 插件和 AGP 插件必须配套更新,否则构建会报错。

···

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
在 Android 开发中,内存泄漏是非常常见但又隐蔽的问题,严重时会导致 应用卡顿、OOM(内存溢出)或崩溃。 

✅ 一、LeakCanary(推荐,快速集成,自动分析)
🧪 LeakCanary 是什么?
LeakCanary 是 Square 出品的一个 Android 内存泄漏检测库,当你的 app 出现内存泄漏时,它会:

自动监听 Activity/Fragment 的销毁情况

如果内存泄漏,它会 弹出通知 + 详细引用链

🛠️ 集成方式(以 Java 项目为例)
在 build.gradle 中添加依赖:

dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
无需其他初始化,LeakCanary 会自动开始监控。

🧩 如何使用 LeakCanary 排查泄漏?
运行 App,进入可能内存泄漏的界面,然后退出该界面。

如果泄漏发生,会自动弹出如下通知:

Memory Leak Detected
com.xxx.MyActivity has leaked
点击通知,查看泄漏路径:

com.xxx.MyActivity → mContext → this$0 → mHandler → MessageQueue
按照引用链查看是哪个字段导致 Activity 没有被回收,例如:

静态变量持有 Activity

Handler 没有移除消息

自定义线程、定时器、RxJava 没有释放

🔍 常见内存泄漏原因
场景 原因
Handler 未 removeCallbacksAndMessages(null)
静态变量 持有 Context/Activity
单例 单例中持有非 Application 的 Context
资源未释放 Bitmap、Camera、MediaPlayer
自定义 View 监听器未解绑
RxJava 未正确 dispose()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<com.amap.api.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>


//地图显示start
private MapView mapView;
private AMap aMap;
private MyLocationStyle myLocationStyle;
private long lastSaveTime = 0;
private static final long SAVE_INTERVAL = 3000; // 每3秒最多写一次

private List<MapPoint> recordedPoints = new ArrayList<>();
private List<Marker> markers = new ArrayList<>();
private Polyline currentPolyline;

//地图显示end
// 初始化地图
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
if (aMap == null) {
aMap = mapView.getMap();
aMap.moveCamera(CameraUpdateFactory.zoomTo(15));
// 添加禁飞 区
// addNoFlyZones();
}
setupMap();

private void setupMap(){
myLocationStyle = new MyLocationStyle();//初始化定位蓝点样式类myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);//连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动。(1秒1次定位)如果不设置myLocationType,默认也会执行此种模式。
myLocationStyle.interval(20000); //设置连续定位模式下的定位间隔,只在连续定位模式下生效,单次定位模式下不会生效。单位为毫秒。
BitmapDescriptor myIcon = BitmapDescriptorFactory.fromResource(R.mipmap.ic_fly_control);
myLocationStyle.myLocationIcon(myIcon);
aMap.setMyLocationStyle(myLocationStyle);//设置定位蓝点的Style
//aMap.getUiSettings().setMyLocationButtonEnabled(true);设置默认定位按钮是否显示,非必需设置。
aMap.setMyLocationEnabled(true);// 设置为true表示启动显示定位蓝点,false表示隐藏定位蓝点并不进行定位,默认是false。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_SHOW);//只定位一次。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATE) ;//定位一次,且将视角移动到地图中心点。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_FOLLOW) ;//连续定位、且将视角移动到地图中心点,定位蓝点跟随设备移动。(1秒1次定位)
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_MAP_ROTATE);//连续定位、且将视角移动到地图中心点,地图依照设备方向旋转,定位点会跟随设备移动。(1秒1次定位)
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE);//连续定位、且将视角移动到地图中心点,定位点依照设备方向旋转,并且会跟随设备移动。(1秒1次定位)默认执行此种模式。
////以下三种模式从5.1.0版本开始提供
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,定位点依照设备方向旋转,并且蓝点会跟随设备移动。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,并且蓝点会跟随设备移动。
// myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER);//连续定位、蓝点不会移动到地图中心点,地图依照设备方向旋转,并且蓝点会跟随设备移动。




// 初始化定位客户端 也可以获取定位信息
// AMapLocationClient mLocationClient = null;
// try {
// mLocationClient = new AMapLocationClient(getApplicationContext());
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// AMapLocationClientOption mLocationOption = new AMapLocationClientOption();
// mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
// mLocationOption.setInterval(2000); // 定位间隔2秒
// mLocationClient.setLocationOption(mLocationOption);
//
// mLocationClient.setLocationListener(location -> {
// if (location != null) {
// if (location.getErrorCode() == 0) {
// // 定位成功,获取经纬度
// double latitude = location.getLatitude();
// double longitude = location.getLongitude();
// Log.d("定位", "定位成功,纬度:" + latitude + ", 经度:" + longitude);
//
// // 可更新地图中心点或UI
// runOnUiThread(() -> {
// // 比如移动摄像头到当前定位点
// aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
// new LatLng(latitude, longitude), 15));
// });
// } else {
// Log.e("定位", "定位失败,错误码:" + location.getErrorCode() +
// " 错误信息:" + location.getErrorInfo());
// }
// }
// });
// mLocationClient.startLocation();




aMap.setOnMyLocationChangeListener(new AMap.OnMyLocationChangeListener() {
@Override
public void onMyLocationChange(Location location) {
if (location != null) {
double latitude = location.getLatitude();
double longitude = location.getLongitude();

long now = System.currentTimeMillis();

// 3秒内只保存一次
if (now - lastSaveTime > SAVE_INTERVAL) {
MapPoint mapPoint = new MapPoint(latitude, longitude, "");
SPUtils.putObject(SPKeysConstance.MAP_POINT, mapPoint); // 建议内部用 apply()
lastSaveTime = now;
Log.d("定位变化", "存储纬度: " + latitude + ", 经度: " + longitude);
}

Log.d("定位变化", "纬度: " + latitude + ", 经度: " + longitude);

// 例如:更新 UI 或定位点
}
}
});

// 获取传递过来的 Bundle
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
fullView.setVisibility(GONE);
FrameLayout.LayoutParams fullParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
smallView.setLayoutParams(fullParams);

smallView.animate()
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(300)
.withStartAction(() -> {
fullView.setElevation(1f);
fullView.bringToFront();
})
.start();
String type = bundle.getString("type");
aMap.setOnMapClickListener(new AMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
mapNewCavns(latLng);
}
});

}else{
aMap.setOnMapClickListener(new AMap.OnMapClickListener() {
@Override
public void onMapClick(LatLng latLng) {
addMarker(latLng);
}
});

}


UiSettings uiSettings = aMap.getUiSettings();
uiSettings.setZoomControlsEnabled(false); // 隐藏缩放按钮

coordinateConverter = new CoordinateConverter(this);
coordinateConverter.from(CoordinateConverter.CoordType.GPS);

}


// 在指定坐标添加标记点
private Marker addMarkerNew(AMap aMap, LatLng latLng, String title) {
// 数字 Marker(模拟 InfoWindow)
Bitmap bitmap = createNumberMarker(recordedPoints.size() + 1);
BitmapDescriptor numberIcon = BitmapDescriptorFactory.fromBitmap(bitmap);
aMap.addMarker(new MarkerOptions()
.position(new LatLng(latLng.latitude + 0.00008, latLng.longitude)) // 上方偏移一点
.icon(numberIcon)
.anchor(0.5f, 1f)
.zIndex(10)); // 保证浮在上层

return aMap.addMarker(new MarkerOptions()
.position(latLng)
.title(title)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
.draggable(true));// 确保标记点是可拖动的

}
// 连接多个点形成折线
private Polyline addPolylineNew(AMap aMap, List<LatLng> points) {
return aMap.addPolyline(new PolylineOptions()
.addAll(points)
.width(15) // 线宽
.color(Color.RED) // 醒目颜色
.zIndex(10)); // 置于上层
}
private void updatePolylineNew() {
// 如果有旧折线,先移除
if (currentPolyline != null) {
currentPolyline.remove();
}

// 如果有至少2个点,绘制连线
if (recordedPoints.size() >= 2) {
List<LatLng> points = new ArrayList<>();
for (MapPoint point : recordedPoints) {
points.add(new LatLng(point.getLatitude(), point.getLongitude()));
}
currentPolyline = addPolylineNew(aMap, points);
}
}

private void clearAllMarkers() {
// 清除所有标记
for (Marker marker : markers) {
marker.remove();
}
markers.clear();

// 清除折线
if (currentPolyline != null) {
currentPolyline.remove();
currentPolyline = null;
}

// 清空记录
recordedPoints.clear();
}



@Override
protected void onResume() {
super.onResume();

if(mapView != null){
mapView.onResume();
}

}

@Override
protected void onPause() {
super.onPause();

mapView.onPause();
}

@Override
protected void onDestroy() {
super.onDestroy();

mapView.onDestroy();
}


高德地图画矩形:
center:正方形中心点,类型是 LatLng(经纬度);

size:正方形边长(单位:米);
private void createSquare(LatLng center, double size) {
double dLat = GeometryUtils.meterToLat(size / 2);
double dLng = GeometryUtils.meterToLng(size / 2, center.latitude);

LatLng p1 = new LatLng(center.latitude + dLat, center.longitude - dLng);
LatLng p2 = new LatLng(center.latitude + dLat, center.longitude + dLng);
LatLng p3 = new LatLng(center.latitude - dLat, center.longitude + dLng);
LatLng p4 = new LatLng(center.latitude - dLat, center.longitude - dLng);

List<LatLng> points = Arrays.asList(p1, p2, p3, p4);
square = aMap.addPolygon(new PolygonOptions()
.addAll(points)
.strokeColor(Color.BLUE)
.fillColor(0x220000FF)
.strokeWidth(4));
}
获取两点间的距离:
public class GeometryUtils {
private static final double R = 6378137; // 地球半径
/**
* 将“米”转换为“纬度差”(1纬度 ≈ 111km)
* @param meter 距离(米)
* @return 纬度差值
*/
public static double meterToLat(double meter) {
return (meter / R) * (180 / PI);
}
/**
* 将“米”转换为“经度差”(因纬度不同,1经度代表的实际米数不同)
* @param meter 距离(米)
* @param lat 当前纬度
* @return 经度差值
*/
public static double meterToLng(double meter, double lat) {
return (meter / (R * cos(toRadians(lat)))) * (180 / PI);
}
/**
* 计算一组“航线段”的总长度
* 假设传入的点序列是成对出现的,每两个点组成一条线段:p0→p1、p2→p3、...
* @param points 航线点列表(经纬度)
* @return 所有线段的总长度(单位:米)
*/
public static double calculateLength(java.util.List<LatLng> points) {
double sum = 0;
for (int i = 1; i < points.size(); i += 2) {
sum += distance(points.get(i - 1), points.get(i));
}
return sum;
}
/**
* 计算两个坐标点之间的球面距离(单位:米)
* 使用 Haversine 公式,适合短距离精确计算
* @param p1 起点(纬度经度)
* @param p2 终点(纬度经度)
* @return 球面两点距离(米)
*/
public static double distance(LatLng p1, LatLng p2) {
double dLat = toRadians(p2.latitude - p1.latitude);
double dLng = toRadians(p2.longitude - p1.longitude);
double a = sin(dLat / 2) * sin(dLat / 2) + cos(toRadians(p1.latitude)) * cos(toRadians(p2.latitude)) * sin(dLng / 2) * sin(dLng / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
}

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.fragment.app.DialogFragment;

import com.htnova.fly.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
public class HtUpdateDialogFragment extends DialogFragment {

private ProgressBar progressBar;
private TextView tvProgress;
private Button btnUpdate, btnSkip, btnCancel;

private volatile boolean isCancelled = false;
private final String apkUrl = "http://115.190.154.26/app-release.apk";
private final int serverVersionCode = 2;
private final int currentVersionCode;

public HtUpdateDialogFragment(Context context) {
currentVersionCode = getVersionCode(context);
}

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_update, container, false);
initView(view);
return view;
}

private void initView(View view) {
progressBar = view.findViewById(R.id.progress_bar);
tvProgress = view.findViewById(R.id.tv_progress);
btnUpdate = view.findViewById(R.id.btn_update);
btnSkip = view.findViewById(R.id.btn_skip);
btnCancel = view.findViewById(R.id.btn_cancel);

TextView tvInfo = view.findViewById(R.id.tv_update_info);
tvInfo.setText("当前版本:" + currentVersionCode + "\n服务器版本:" + serverVersionCode +
"\n\n更新内容:\n- 修复问题\n- 优化性能");

btnUpdate.setOnClickListener(v -> {
btnUpdate.setVisibility(View.GONE);
btnSkip.setVisibility(View.GONE);
btnCancel.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
tvProgress.setVisibility(View.VISIBLE);
startDownload();
});

btnSkip.setOnClickListener(v -> dismiss());

btnCancel.setOnClickListener(v -> {
isCancelled = true;
Toast.makeText(getContext(), "已取消下载", Toast.LENGTH_SHORT).show();
dismiss();
});
}

private void startDownload() {
new Thread(() -> {
HttpURLConnection conn = null;
InputStream is = null;
BufferedOutputStream bos = null;

try {
URL url = new URL(apkUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.connect();

int length = conn.getContentLength();
if (length <= 0) {
showToast("服务器未返回文件大小");
return;
}

is = new BufferedInputStream(conn.getInputStream());
File apkFile = new File(requireContext().getExternalFilesDir(null), "update.apk");
bos = new BufferedOutputStream(new FileOutputStream(apkFile));

byte[] buffer = new byte[8192]; // 8KB缓冲区
int count = 0;
int bytesRead;
int lastProgress = 0;

while ((bytesRead = is.read(buffer)) != -1 && !isCancelled) {
bos.write(buffer, 0, bytesRead);
count += bytesRead;
int progress = (int) (count * 100L / length);

if (progress != lastProgress) {
int finalProgress = progress;
requireActivity().runOnUiThread(() -> {
progressBar.setProgress(finalProgress);
tvProgress.setText("下载进度:" + finalProgress + "%");
});
lastProgress = progress;
}
}

bos.flush();

if (!isCancelled) {
requireActivity().runOnUiThread(() -> {
Toast.makeText(getContext(), "下载完成,准备安装", Toast.LENGTH_SHORT).show();
installApk(apkFile);
dismiss();
});
}

} catch (Exception e) {
Log.e("HtUpdateDialog", "下载出错:" + e.getMessage(), e);
showToast("下载失败:" + e.getMessage());
} finally {
try {
if (is != null) is.close();
if (bos != null) bos.close();
if (conn != null) conn.disconnect();
} catch (Exception ignored) {
}
}
}).start();
}

private void installApk(File apkFile) {
Context context = requireContext();
Uri apkUri;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
apkUri = Uri.fromFile(apkFile);
}

intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
}

private int getVersionCode(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
return 0;
}
}

private void showToast(String msg) {
requireActivity().runOnUiThread(() ->
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show());
}
}

R.layout.dialog_update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_update_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发现新版本..."
android:textSize="16sp" />

<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:visibility="gone" />

<TextView
android:id="@+id/tv_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下载进度:0%"
android:visibility="gone"
android:paddingTop="10dp" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp">

<Button
android:id="@+id/btn_update"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="立即更新" />

<Button
android:id="@+id/btn_skip"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="跳过" />

<Button
android:id="@+id/btn_cancel"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="取消下载"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

application中添加

1
2
3
4
5
6
7
8
9
10
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

xml文件夹下新建file_paths.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="update_apk"
path="." />
</paths>

使用new HtUpdateDialogFragment(this).show(getSupportFragmentManager(), “update_dialog”);

LiveData 是 ViewModel 用来“安全地通知 UI 层更新”的方式。

  1. ViewModel 中定义与更新 LiveData

public class MyViewModel extends ViewModel {
private final MutableLiveData messageLiveData = new MutableLiveData<>();

public LiveData<String> getMessageLiveData() {
    return messageLiveData;
}

public void updateMessage(String msg) {
    messageLiveData.setValue(msg);  // 主线程使用 setValue()
    // messageLiveData.postValue(msg); // 子线程也可以用 postValue()
}

}
2. Activity/Fragment 中观察 LiveData

MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);

viewModel.getMessageLiveData().observe(this, message -> {
textView.setText(message); // UI 自动刷新
});
3. LiveData 常用操作
方法 说明
setValue() 主线程更新数据
postValue() 子线程更新数据
observe() 观察数据变化(生命周期感知)
observeForever() 永久观察(需手动移除,慎用)

🔁 和 RxJava 比较
特性 LiveData RxJava
生命周期感知 ✅ 内建 ❌ 需手动配合如 RxLifecycle
学习成本 ✅ 简单 ❌ 复杂,操作符多
功能丰富 ❌ 仅支持数据通知 ✅ 支持变换、合并、背压等复杂操作
UI 通知 ✅ 主线程安全 ✅ 手动指定线程

推荐组合用法:业务逻辑用 RxJava,最终结果放入 LiveData 中供 UI 层观察。

🧱 高阶用法

  1. MediatorLiveData
    观察多个 LiveData 源,合并逻辑:

MediatorLiveData result = new MediatorLiveData<>();
result.addSource(liveData1, value -> {
result.setValue(“来自 liveData1: “ + value);
});
result.addSource(liveData2, value -> {
result.setValue(“来自 liveData2: “ + value);
});
2. SingleLiveEvent(防止重复触发)
防止如“跳转页面”这种事件被重复触发,常自定义如下:

public class SingleLiveEvent extends MutableLiveData {
private final AtomicBoolean pending = new AtomicBoolean(false);

@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    super.observe(owner, t -> {
        if (pending.compareAndSet(true, false)) {
            observer.onChanged(t);
        }
    });
}

@MainThread
public void setValue(T t) {
    pending.set(true);
    super.setValue(t);
}

}
🧭 总结:LiveData 用在哪里?
场景 是否推荐用 LiveData
后台网络请求结果通知 UI ✅ 推荐
定时更新状态(配合 RxJava) ✅ 推荐
复杂数据链式变换 ❌ 用 RxJava 更合适
临时事件(弹窗、跳转) ✅ 用 SingleLiveEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
支持对象存储(自动 JSON 序列化和反序列化)的 Android SPUtils 工具类,使用 Gson 实现对象与字符串的互转。
✅ Gson 依赖(如果尚未添加)
在 build.gradle 中加入:

implementation 'com.google.code.gson:gson:2.10.1'
✅ 支持对象的 SPUtils 工具类

package com.htnova.fly.util;

import android.content.Context;
import android.content.SharedPreferences;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;

public class SPUtils {
private static final String SP_NAME = "AppCache";
private static SharedPreferences sp;
private static Gson gson = new Gson();

private SPUtils() {}

public static void init(Context context) {
if (sp == null) {
sp = context.getApplicationContext().getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
}
}

// 基础类型
public static void putString(String key, String value) {
sp.edit().putString(key, value).apply();
}

public static String getString(String key, String defValue) {
return sp.getString(key, defValue);
}

public static void putBoolean(String key, boolean value) {
sp.edit().putBoolean(key, value).apply();
}

public static boolean getBoolean(String key, boolean defValue) {
return sp.getBoolean(key, defValue);
}

public static void putInt(String key, int value) {
sp.edit().putInt(key, value).apply();
}

public static int getInt(String key, int defValue) {
return sp.getInt(key, defValue);
}

public static void putLong(String key, long value) {
sp.edit().putLong(key, value).apply();
}

public static long getLong(String key, long defValue) {
return sp.getLong(key, defValue);
}

public static void putFloat(String key, float value) {
sp.edit().putFloat(key, value).apply();
}

public static float getFloat(String key, float defValue) {
return sp.getFloat(key, defValue);
}

// 存储任意对象
public static <T> void putObject(String key, T obj) {
String json = gson.toJson(obj);
sp.edit().putString(key, json).apply();
}

// 获取对象
public static <T> T getObject(String key, Class<T> clazz) {
String json = sp.getString(key, null);
return json != null ? gson.fromJson(json, clazz) : null;
}

// 获取集合或泛型对象(如 List<User>)
public static <T> T getObject(String key, Type typeOfT) {
String json = sp.getString(key, null);
return json != null ? gson.fromJson(json, typeOfT) : null;
}

public static void remove(String key) {
sp.edit().remove(key).apply();
}

public static void clear() {
sp.edit().clear().apply();
}

public static boolean contains(String key) {
return sp.contains(key);
}
}
✅ 示例用法
1. 存储/读取单个对象:

User user = new User("Tom", 25);
SPUtils.putObject("user", user);

User savedUser = SPUtils.getObject("user", User.class);
2. 存储/读取 List 对象:

List<User> userList = new ArrayList<>();
userList.add(new User("Tom", 25));
userList.add(new User("Jerry", 26));

SPUtils.putObject("user_list", userList);

// 获取时需要指定 Type
Type type = new TypeToken<List<User>>() {}.getType();
List<User> savedList = SPUtils.getObject("user_list", type);
✅ 示例数据类(User)

public class User {
private String name;
private int age;

public User() {}

public User(String name, int age) {
this.name = name;
this.age = age;
}

// Getter / Setter 省略
}

Retrofit:网络请求框架

OkHttp3:Retrofit 底层网络请求库

RxJava2:响应式编程,处理异步流

MVVM 架构:解耦视图和数据逻辑

ViewModel + LiveData:生命周期感知组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285

🔧 一、依赖配置(build.gradle)

// Retrofit 核心
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

// OkHttp3 + 日志拦截器
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'

// RxJava2 + Retrofit 支持
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'

// MVVM 架构核心
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
🧱 二、Retrofit 初始化封装(ApiClient.java)


import android.util.Log;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
* @author xqm
* @date 2025/7/12 16:20
* @description https://jsonplaceholder.typicode.com/
* retrofit+okhttp
*/
public class ApiClient {
private static final String BASE_URL = "https://xxxxx/";
private static Retrofit retrofit;

public static Retrofit getInstance() {
if (retrofit == null) {
// 添加日志拦截器
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor(
// message -> Log.e("xqm", message) // 你可以自定义日志输出位置
// );
logging.setLevel(HttpLoggingInterceptor.Level.BODY);

logging.setLevel(HttpLoggingInterceptor.Level.BODY); // 打印完整请求和响应

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HeaderInterceptor())
.addInterceptor(logging)
.build();

retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // Rx 支持
.build();
}
return retrofit;
}
}

🔐 三、自定义请求头拦截器(HeaderInterceptor.java)


import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* @author xqm
* @date 2025/7/12 16:21
* @description 用于自动在所有请求中附加 token 认证信息,可在登录成功后设置。
*/
public class HeaderInterceptor implements Interceptor {
private static String token = "";

public static void setToken(String t) {
token = t;
}

@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", token.isEmpty() ? "" : "Bearer " + token);

return chain.proceed(builder.build());
}
}



🔌 四、API 接口定义(ApiService.java)





import io.reactivex.Observable;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;

/**
* @author xqm
* @date 2025/7/12 16:20
* @description ApiService 接口定义
* 支持 GET 和 POST 请求
* 支持 @Query 参数和 @Body 参数
* 使用 RxJava 返回 Observable<T>
*/
public interface ApiService {

@GET("todos/1")
Observable<UserModel> getUserInfo();

@GET("access/auth/login")
Observable<UserModel> getlogin(@Query("username") String username, @Query("password") String password);

@POST("user/login")
Observable<LoginResponse> login(@Body LoginRequest request);
}



📦 五、数据访问封装(UserRepository.java)


import io.reactivex.Observable;

/**
* @author xqm
* @date 2025/7/12 16:21
* @description 将数据访问封装在 Repository 层,如网络请求,本地数据库访问等
*/
public class UserRepository {

private final ApiService api;

public UserRepository() {
api = ApiClient.getInstance().create(ApiService.class);
}

public Observable<LoginResponse> login(String username, String password) {
return api.login(new LoginRequest(username, password));
}

public Observable<UserModel> getUserInfo() {
return api.getUserInfo();
}

public Observable<UserModel> getLogin(String userName,String pwd) {
return api.getlogin(userName,pwd);
}
}



🧠 六、ViewModel 逻辑层(UserViewModel.java)



import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;


/**
* @author xqm
* @date 2025/7/12 16:22
* @description 调用 Repository 提供的数据接口,处理线程调度和生命周期管理。
*/
public class UserViewModel extends BaseViewModel {
private final UserRepository repository = new UserRepository();

public void login(String username, String password, RxObserver<LoginResponse> observer) {
Disposable disposable = repository.login(username, password)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}

public void getUserInfo(RxObserver<UserModel> observer) {
Disposable disposable = repository.getUserInfo()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}

public void getLogin(String userName,String pwd,RxObserver<UserModel> observer) {
Disposable disposable = repository.getLogin(userName,pwd)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer);
addDisposable(disposable);
}
}



🧼 七、BaseViewModel(统一管理 Disposable)


/**
* @author xqm
* @date 2025/7/12 16:22
* @description 避免内存泄漏,销毁 ViewModel 时自动清理订阅。
*/
public abstract class BaseViewModel extends ViewModel {
protected CompositeDisposable compositeDisposable = new CompositeDisposable();

protected void addDisposable(Disposable d) {
compositeDisposable.add(d);
}

@Override
protected void onCleared() {
compositeDisposable.clear();
super.onCleared();
}
}


🧪 八、自定义 Observer 示例(RxObserver)


/**
* @author xqm
* @date 2025/7/12 16:43
* @description 可自我管理订阅生命周期的 Observer,既能收数据,又能自己取消订阅
*/
import io.reactivex.observers.DisposableObserver;

public abstract class RxObserver<T> extends DisposableObserver<T> {
@Override
public void onNext(T t) {
onSuccess(t);
}

@Override
public void onError(Throwable e) {
onFailure(e);
}

@Override
public void onComplete() {}

public void onStart() {}

public abstract void onSuccess(T t);
public abstract void onFailure(Throwable e);
}


可以在回调里更新 UI,比如:
viewModel = new ViewModelProvider(this).get(UserViewModel.class);
viewModel.login(user, pwd, new RxObserver<LoginResponse>() {
@Override
public void onNext(LoginResponse response) {
if (response.getCode() == 200) {
// 登录成功
HeaderInterceptor.setToken(response.getToken());
Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "登录失败:" + response.getMsg(), Toast.LENGTH_SHORT).show();
}
}

@Override
public void onError(Throwable e) {
Toast.makeText(context, "请求出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});

✅ 总结
层级 类名 作用
网络配置 ApiClient Retrofit 初始化,添加拦截器
拦截器 HeaderInterceptor 动态添加统一请求头
API 接口 ApiService 定义 Retrofit 请求
数据层 UserRepository 请求封装、解耦逻辑
ViewModel UserViewModel 调用 Repository 并分发到界面
Observer RxObserver 自定义网络回调处理逻辑
📁 项目目录结构

├── activity/ # UI 层 Activity 或 Fragment
├── model/ # 数据模型类(Bean)
├── network/ # 网络核心配置(Retrofit + 拦截器)
│ ├── ApiClient.java
│ ├── ApiService.java
│ ├── HeaderInterceptor.java
│ └── RxObserver.java
├── repository/ # 数据仓库层
│ └── UserRepository.java
├── viewmodel/ # ViewModel 层
│ ├── BaseViewModel.java
│ └── UserViewModel.java

在 Android 中使用 MQTT(Message Queuing Telemetry Transport) 与服务器交互,是实现物联网、实时通信等场景的常用方式

一、准备工作
在 build.gradle(:app) 中添加:
implementation ‘org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5’
implementation ‘org.eclipse.paho:org.eclipse.paho.android.service:1.1.1’

二、配置权限
在 AndroidManifest.xml 添加:

三、初始化 MQTT 客户端

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.nio.charset.StandardCharsets;

/**

  • @author xqm

  • @date 2025/7/18 17:01

  • @description MqttManager 类功能说明
    */
    public class MqttManager {
    private static final String TAG = “MqttManager”;
    private static MqttManager instance;
    private final MqttAndroidClient mqttAndroidClient;
    private final MqttConnectOptions connectOptions;

    private String serverUri = “tcp://broker.hivemq.com:1883”; // 可替换为你自己的服务器
    private final String clientId = “AndroidClient_” + System.currentTimeMillis();
    private final Context context;

    private OnMessageReceiveListener onMessageReceiveListener;

    public MqttManager(Context context,String serverUriAdd,String clientIdAdd,String userName,String pwd) {
    this.context = context.getApplicationContext();
    mqttAndroidClient = new MqttAndroidClient(context, serverUriAdd, clientIdAdd);
    connectOptions = new MqttConnectOptions();
    connectOptions.setUserName(userName);
    connectOptions.setPassword(pwd.toCharArray());
    connectOptions.setCleanSession(true);
    connectOptions.setAutomaticReconnect(false);

    initCallback();
    }

    // 单例获取方法
    public static synchronized MqttManager getInstance(Context context, String serverUriAdd, String clientIdAdd, String userName, String pwd) {
    if (instance == null) {
    instance = new MqttManager(context, serverUriAdd, clientIdAdd, userName, pwd);
    }
    return instance;
    }

    // 可选:无参数获取(需提前初始化)
    public static synchronized MqttManager getInstance() {
    if (instance == null) {
    throw new IllegalStateException(“MqttManager 尚未初始化!”);
    }
    return instance;
    }

    public boolean isConnected() {
    return mqttAndroidClient != null && mqttAndroidClient.isConnected();
    }

    private void initCallback() {
    mqttAndroidClient.setCallback(new MqttCallback() {
    @Override
    public void connectionLost(Throwable cause) {
    Log.e(TAG, “连接丢失,准备重连”, cause);
    reconnectWithDelay();
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) {
    String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
    Log.d(TAG, “收到消息: topic=” + topic + “, message=” + payload);
    if (onMessageReceiveListener != null) {
    onMessageReceiveListener.onMessage(topic, payload);
    }
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
    Log.d(TAG, “消息发送完成”);
    }
    });
    }

    public void connect() {
    try {
    mqttAndroidClient.connect(connectOptions, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
    Log.d(TAG, “连接成功”);
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    Log.e(TAG, “连接失败”, exception);
    reconnectWithDelay();
    }
    });
    } catch (MqttException e) {
    Log.e(TAG, “连接异常”, e);
    }
    }

    private void reconnectWithDelay() {
    new Handler(Looper.getMainLooper()).postDelayed(this::connect, 3000);
    }

    public void subscribe(String topic) {
    try {
    mqttAndroidClient.subscribe(topic, 1, null, new IMqttActionListener() {
    @Override
    public void onSuccess(IMqttToken asyncActionToken) {
    Log.d(TAG, “订阅成功: “ + topic);
    }

    @Override
    public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
    Log.e(TAG, “订阅失败: “ + topic, exception);
    }
    });
    } catch (MqttException e) {
    Log.e(TAG, “订阅异常”, e);
    }
    }

    public void publish(String topic, String message) {
    try {
    MqttMessage mqttMessage = new MqttMessage();
    mqttMessage.setPayload(message.getBytes(StandardCharsets.UTF_8));
    mqttAndroidClient.publish(topic, mqttMessage);
    Log.d(TAG, “发送消息: topic=” + topic + “, message=” + message);
    } catch (MqttException e) {
    Log.e(TAG, “发送失败”, e);
    }
    }

    public void disconnect() {
    try {
    mqttAndroidClient.disconnect();
    Log.d(TAG, “断开连接”);
    } catch (MqttException e) {
    Log.e(TAG, “断开异常”, e);
    }
    }

    public void setOnMessageReceiveListener(OnMessageReceiveListener listener) {
    this.onMessageReceiveListener = listener;
    }

    public interface OnMessageReceiveListener {
    void onMessage(String topic, String message);
    }

}

四、在 Activity 中使用
mqttManager = MqttManager.getInstance(getApplicationContext(), mqttUrl, clientId, userName, pwd);
mqttManager.setOnMessageReceiveListener((topic, message) -> {
runOnUiThread(() -> {
Toast.makeText(this, “收到: “ + message, Toast.LENGTH_SHORT).show();
});
});
mqttManager.connect();
mqttManager.subscribe(“your/topic”);

订阅多个 Topic(推荐做法)
mqttManager.subscribe(“drone/telemetry”); // 无人机遥测数据
mqttManager.subscribe(“drone/status”); // 状态变化
mqttManager.subscribe(“drone/camera”); // 摄像头控制消息
mqttManager.subscribe(“drone/command”); // 控制指令

也可以封装成数组遍历订阅:
String[] topics = {
“drone/telemetry”,
“drone/status”,
“drone/camera”,
“drone/command”
};

for (String topic : topics) {
mqttManager.subscribe(topic);
}
messageArrived 回调中区分 Topic 内容
mqttManager.setOnMessageReceiveListener((topic, message) -> {
switch (topic) {
case “drone/telemetry”:
// 解析遥测 JSON
break;
case “drone/status”:
// 更新状态 UI
break;
case “drone/command”:
// 响应控制指令
break;
default:
Log.w(“MQTT”, “未知 topic:” + topic);
break;
}
});

扩展建议
如你有“Topic + 消息体 JSON”的组合格式,可以创建实体类并使用 Gson 解析:
TelemetryData data = new Gson().fromJson(message, TelemetryData.class);

五、服务端推荐 MQTT Broker
免费测试:
HiveMQ: tcp://broker.hivemq.com:1883
Eclipse: tcp://iot.eclipse.org:1883
自建服务器推荐:
EMQX
Mosquitto

六、注意事项
保持后台连接:可以考虑 MQTT 放入前台服务中,保证连接不断。
网络断开重连处理:建议实现 connectionLost() 中的自动重连机制。
安全连接:生产环境使用 ssl:// + 用户认证更安全。