535 lines
14 KiB
Markdown
535 lines
14 KiB
Markdown
# CLAUDE.md
|
||
|
||
项目开发指南 - 航空物流App
|
||
|
||
## 项目概述
|
||
|
||
**AirLogistics** - Android原生应用,航空物流全流程管理
|
||
|
||
- **包名**: com.lukouguoji.aerologic
|
||
- **版本**: 1.7.9 (API 24-30)
|
||
- **架构**: MVVM + 组件化 + Kotlin + DataBinding
|
||
- **屏幕**: 横屏 1152dp × 720dp
|
||
|
||
## 快速构建
|
||
|
||
```bash
|
||
./gradlew assembleDebug # 构建Debug版本
|
||
./gradlew clean # 清理构建
|
||
```
|
||
|
||
## 核心架构
|
||
|
||
### MVVM层级
|
||
|
||
```
|
||
Activity → BaseBindingActivity → ViewModel → BaseViewModel/BasePageViewModel → API
|
||
```
|
||
|
||
### 关键基类
|
||
|
||
- **BaseBindingActivity**: DataBinding + ViewModel自动绑定
|
||
- **BaseViewModel**: Loading管理、协程支持
|
||
- **BasePageViewModel**: 分页列表(含PageModel)
|
||
- **CommonAdapter + BaseViewHolder**: 列表适配器
|
||
- **PadSearchLayout**: 搜索区域输入控件
|
||
- **PadDataLayout**: 数据展示/编辑控件
|
||
|
||
### 标准Activity模板
|
||
|
||
```kotlin
|
||
@Route(path = ARouterConstants.ACTIVITY_URL_XXX)
|
||
class XxxActivity : BaseBindingActivity<ActivityXxxBinding, XxxViewModel>() {
|
||
override fun layoutId() = R.layout.activity_xxx
|
||
override fun viewModelClass() = XxxViewModel::class.java
|
||
|
||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||
setBackArrow("页面标题")
|
||
binding.viewModel = viewModel
|
||
// 初始化UI
|
||
}
|
||
}
|
||
```
|
||
|
||
### 标准ViewModel模板
|
||
|
||
**列表页ViewModel:**
|
||
|
||
```kotlin
|
||
class XxxListViewModel : BasePageViewModel() {
|
||
val searchText = MutableLiveData<String>()
|
||
val itemLayoutId = R.layout.item_xxx
|
||
val itemViewHolder = XxxViewHolder::class.java
|
||
|
||
override fun getData() {
|
||
val params = mapOf(
|
||
"page" to pageModel.page,
|
||
"limit" to pageModel.limit,
|
||
"searchText" to searchText.value
|
||
).toRequestBody()
|
||
|
||
launchLoadingCollect({ NetApply.api.getXxxList(params) }) {
|
||
onSuccess = { pageModel.handleListBean(it) }
|
||
}
|
||
}
|
||
|
||
override fun onItemClick(position: Int, type: Int) {
|
||
val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as XxxBean
|
||
// 跳转详情
|
||
}
|
||
}
|
||
```
|
||
|
||
**详情页ViewModel:**
|
||
|
||
```kotlin
|
||
class XxxDetailsViewModel : BaseViewModel() {
|
||
var id = ""
|
||
val dataBean = MutableLiveData<XxxBean>()
|
||
|
||
fun initOnCreated(intent: Intent) {
|
||
id = intent.getStringExtra(Constant.Key.ID) ?: ""
|
||
getData()
|
||
}
|
||
|
||
private fun getData() {
|
||
launchLoadingCollect({ NetApply.api.getXxxDetails(id) }) {
|
||
onSuccess = { dataBean.value = it.data ?: XxxBean() }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**编辑页ViewModel:**
|
||
|
||
```kotlin
|
||
class XxxAddViewModel : BaseViewModel() {
|
||
val pageType = MutableLiveData(DetailsPageType.Add) // 必须用LiveData
|
||
var id = ""
|
||
val dataBean = MutableLiveData(XxxBean())
|
||
|
||
fun initOnCreated(intent: Intent) {
|
||
pageType.value = DetailsPageType.valueOf(
|
||
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
|
||
)
|
||
if (pageType.value != DetailsPageType.Add) {
|
||
id = intent.getStringExtra(Constant.Key.ID) ?: ""
|
||
loadData()
|
||
}
|
||
}
|
||
|
||
fun submit() {
|
||
val bean = dataBean.value ?: return
|
||
if (bean.name.verifyNullOrEmpty("请输入名称")) return
|
||
|
||
launchLoadingCollect({
|
||
val params = mapOf("id" to id, "name" to bean.name)
|
||
.toRequestBody(removeEmptyOrNull = true)
|
||
NetApply.api.saveXxx(params)
|
||
}) {
|
||
onSuccess = {
|
||
showToast("保存成功")
|
||
viewModelScope.launch {
|
||
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
|
||
}
|
||
getTopActivity().finish()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 网络请求
|
||
|
||
### 请求方法
|
||
|
||
```kotlin
|
||
// 带Loading请求
|
||
launchLoadingCollect({ NetApply.api.saveXxx(params) }) {
|
||
onSuccess = { /* 成功处理 */ }
|
||
onFailed = { code, msg -> /* 失败处理 */ }
|
||
}
|
||
|
||
// 无Loading请求(后台刷新)
|
||
launchCollect({ NetApply.api.getXxx() }) {
|
||
onSuccess = { /* 成功处理 */ }
|
||
}
|
||
|
||
// 参数转换
|
||
val params = mapOf("key" to "value").toRequestBody(removeEmptyOrNull = true)
|
||
```
|
||
|
||
### API接口定义
|
||
|
||
```kotlin
|
||
// 位置: module_base/.../http/net/Api.kt
|
||
@POST("api/xxx/list")
|
||
suspend fun getXxxList(@Body data: RequestBody): BaseListBean<XxxBean>
|
||
|
||
@POST("api/xxx/details")
|
||
suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean<XxxBean>
|
||
|
||
@POST("api/xxx/save")
|
||
suspend fun saveXxx(@Body data: RequestBody): BaseResultBean<SimpleResultBean>
|
||
```
|
||
|
||
## 核心UI组件
|
||
|
||
### PadSearchLayout - 搜索输入框
|
||
|
||
```xml
|
||
<!-- 文本输入+扫码 -->
|
||
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
|
||
type="@{SearchLayoutType.INPUT}"
|
||
value="@={viewModel.waybillNo}"
|
||
hint="@{`请输入运单号`}"
|
||
icon="@{@mipmap/scan_code}"
|
||
setOnIconClickListener="@{(v)-> viewModel.scanWaybill()}" />
|
||
|
||
<!-- 日期选择 -->
|
||
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
|
||
type="@{SearchLayoutType.DATE}"
|
||
value="@={viewModel.date}"
|
||
icon="@{@mipmap/calendar}" />
|
||
|
||
<!-- 下拉选择 -->
|
||
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
|
||
type="@{SearchLayoutType.SPINNER}"
|
||
list="@{viewModel.statusList}"
|
||
value="@={viewModel.status}" />
|
||
```
|
||
|
||
**类型**: `INPUT` / `INTEGER` / `SPINNER` / `DATE`
|
||
|
||
### PadDataLayout - 数据展示/编辑
|
||
|
||
```xml
|
||
<!-- 文本输入 -->
|
||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
|
||
type="@{DataLayoutType.INPUT}"
|
||
title='@{"运单号:"}'
|
||
titleLength="@{5}"
|
||
value='@={viewModel.bean.waybillNo}'
|
||
enable="@{viewModel.pageType != DetailsPageType.Details}"
|
||
required="@{true}"
|
||
maxLength="@{11}" />
|
||
|
||
<!-- 下拉选择 -->
|
||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
|
||
type="@{DataLayoutType.SPINNER}"
|
||
title='@{"状态:"}'
|
||
list="@{viewModel.statusList}"
|
||
value='@={viewModel.bean.status}' />
|
||
|
||
<!-- 多行输入 -->
|
||
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
|
||
type="@{DataLayoutType.INPUT}"
|
||
inputHeight="@{100}"
|
||
value='@={viewModel.bean.remark}' />
|
||
```
|
||
|
||
**类型**: `INPUT` / `SPINNER` / `DATE`
|
||
|
||
## 开发检查清单
|
||
|
||
### ⚠️ 重要提醒
|
||
|
||
**新建Activity后必须在AndroidManifest.xml中注册,否则会报ActivityNotFoundException错误!**
|
||
|
||
### 列表页开发(8步)
|
||
|
||
1. **创建Bean** (`module_base/.../bean/XxxBean.kt`)
|
||
2. **添加API接口** (`Api.kt` → `getXxxList()`)
|
||
3. **创建ViewHolder** (继承`BaseViewHolder`)
|
||
4. **创建ViewModel** (继承`BasePageViewModel`)
|
||
5. **创建Activity** (继承`BaseBindingActivity`)
|
||
6. **创建Layout** (`activity_xxx_list.xml` + `item_xxx.xml`)
|
||
7. **注册路由** (`ARouterConstants`)
|
||
8. **⚠️ 在AndroidManifest.xml中注册Activity** (`app/src/main/AndroidManifest.xml`)
|
||
|
||
**AndroidManifest.xml注册示例:**
|
||
|
||
```xml
|
||
<!-- 在app/src/main/AndroidManifest.xml的<application>标签内添加 -->
|
||
<activity
|
||
android:name="com.lukouguoji.gnc.page.xxx.XxxActivity"
|
||
android:configChanges="orientation|keyboardHidden"
|
||
android:exported="false"
|
||
android:screenOrientation="userLandscape" />
|
||
```
|
||
|
||
**关键代码:**
|
||
|
||
```kotlin
|
||
// Activity中绑定分页
|
||
viewModel.pageModel.bindSmartRefreshLayout(
|
||
binding.srl, binding.recyclerView, viewModel, this
|
||
)
|
||
binding.recyclerView.addOnItemClickListener(viewModel)
|
||
```
|
||
|
||
### 详情页开发(5步)
|
||
|
||
1. **添加API接口** (`getXxxDetails()`)
|
||
2. **创建ViewModel** (继承`BaseViewModel`)
|
||
3. **创建Activity** (含`companion object`静态start方法)
|
||
4. **创建Layout**
|
||
5. **⚠️ 在AndroidManifest.xml中注册Activity**
|
||
|
||
**静态启动方法:**
|
||
|
||
```kotlin
|
||
companion object {
|
||
@JvmStatic
|
||
fun start(context: Context, id: String) {
|
||
val starter = Intent(context, XxxDetailsActivity::class.java)
|
||
.putExtra(Constant.Key.ID, id)
|
||
context.startActivity(starter)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 编辑页开发(6步)
|
||
|
||
1. **添加API接口** (`saveXxx()` + `getXxxDetails()`)
|
||
2. **创建ViewModel** (`pageType`使用`MutableLiveData`)
|
||
3. **创建Activity** (多个静态start方法: `startForAdd/Edit/Details`)
|
||
4. **创建Layout** (根据`pageType`控制enable)
|
||
5. **FlowBus发送刷新事件**
|
||
6. **⚠️ 在AndroidManifest.xml中注册Activity**
|
||
|
||
**Activity多入口:**
|
||
|
||
```kotlin
|
||
companion object {
|
||
@JvmStatic
|
||
fun startForAdd(context: Context) {
|
||
val starter = Intent(context, XxxAddActivity::class.java)
|
||
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
|
||
context.startActivity(starter)
|
||
}
|
||
|
||
@JvmStatic
|
||
fun startForEdit(context: Context, id: String) {
|
||
/* ... DetailsPageType.Modify ... */
|
||
}
|
||
}
|
||
```
|
||
|
||
## 常见业务场景
|
||
|
||
### 扫码
|
||
|
||
```kotlin
|
||
fun scanWaybill() {
|
||
ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
|
||
}
|
||
|
||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||
if (requestCode == Constant.RequestCode.WAYBILL && resultCode == Activity.RESULT_OK) {
|
||
waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
|
||
search()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 图片上传
|
||
|
||
```kotlin
|
||
val result = UploadUtil.upload(filePath)
|
||
if (result.verifySuccess()) {
|
||
val imageUrl = result.data?.newName ?: "" // 注意是newName不是url
|
||
}
|
||
```
|
||
|
||
### 列表刷新事件
|
||
|
||
```kotlin
|
||
// 发送事件(在ViewModel中)
|
||
viewModelScope.launch {
|
||
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
|
||
}
|
||
|
||
// 接收事件(在Activity中)
|
||
import com.lukouguoji.module_base.impl.observe // 必须导入
|
||
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) {
|
||
viewModel.refresh()
|
||
}
|
||
```
|
||
|
||
## 常用扩展函数
|
||
|
||
```kotlin
|
||
// Toast
|
||
showToast("提示信息")
|
||
|
||
// 验证非空
|
||
if (text.verifyNullOrEmpty("请输入内容")) return
|
||
|
||
// 空处理
|
||
val text = nullableString.noNull("默认值")
|
||
|
||
// 日期格式化
|
||
val dateStr = Date().formatDate() // "2025-11-12"
|
||
|
||
// 权限申请
|
||
permission(Manifest.permission.CAMERA) { openCamera() }
|
||
```
|
||
|
||
## 常见编译错误
|
||
|
||
### 1. DetailsPageType包名错误
|
||
|
||
```xml
|
||
<!-- ❌ 错误 -->
|
||
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
|
||
|
||
<!-- ✅ 正确 -->
|
||
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
|
||
```
|
||
|
||
### 2. DataLayoutType枚举值错误
|
||
|
||
```xml
|
||
<!-- ❌ 错误: INTEGER不存在 -->
|
||
type="@{DataLayoutType.INTEGER}"
|
||
|
||
<!-- ✅ 正确: 使用INPUT -->
|
||
type="@{DataLayoutType.INPUT}"
|
||
```
|
||
|
||
**可用类型**: `INPUT` / `SPINNER` / `DATE`
|
||
|
||
### 3. DetailsPageType枚举值错误
|
||
|
||
```kotlin
|
||
// ❌ 错误: Edit不存在
|
||
DetailsPageType.Edit
|
||
|
||
// ✅ 正确: 使用Modify
|
||
DetailsPageType.Modify
|
||
```
|
||
|
||
**可用类型**: `Add` / `Modify` / `Details`
|
||
|
||
### 4. IOnItemClickListener包名错误
|
||
|
||
```kotlin
|
||
// ❌ 错误
|
||
import com.lukouguoji.module_base.impl.IOnItemClickListener
|
||
|
||
// ✅ 正确
|
||
import com.lukouguoji.module_base.interfaces.IOnItemClickListener
|
||
```
|
||
|
||
### 5. FlowBus使用错误
|
||
|
||
```kotlin
|
||
// ❌ 错误: observe需要单独导入
|
||
import com.lukouguoji.module_base.impl.FlowBus
|
||
|
||
// ✅ 正确
|
||
import com.lukouguoji.module_base.impl.FlowBus
|
||
import com.lukouguoji.module_base.impl.observe
|
||
|
||
// ❌ 错误: emit必须在协程中
|
||
FlowBus.with<String>(event).emit("data")
|
||
|
||
// ✅ 正确
|
||
viewModelScope.launch {
|
||
FlowBus.with<String>(event).emit("data")
|
||
}
|
||
```
|
||
|
||
### 6. 图片上传字段错误
|
||
|
||
```kotlin
|
||
// ❌ 错误: UploadBean没有url字段
|
||
val imageUrl = result.data?.url
|
||
|
||
// ✅ 正确: 使用newName字段
|
||
val imageUrl = result.data?.newName
|
||
```
|
||
|
||
### 7. pageType必须用LiveData
|
||
|
||
```kotlin
|
||
// ❌ 错误: DataBinding无法绑定
|
||
var pageType: DetailsPageType = DetailsPageType.Add
|
||
|
||
// ✅ 正确: 使用LiveData
|
||
val pageType = MutableLiveData(DetailsPageType.Add)
|
||
```
|
||
|
||
### 8. RecyclerView不支持items属性
|
||
|
||
```xml
|
||
<!-- ❌ 错误: items属性会导致编译错误 -->
|
||
<RecyclerView
|
||
items="@{viewModel.list}" />
|
||
|
||
<!-- ✅ 正确: 在Activity中手动更新 -->
|
||
<RecyclerView android:id="@+id/recyclerView" />
|
||
```
|
||
|
||
```kotlin
|
||
// Activity中
|
||
viewModel.list.observe(this) { data ->
|
||
binding.recyclerView.commonAdapter()?.refresh(data)
|
||
}
|
||
```
|
||
|
||
### 9. Constant.Key.PAGE_TYPE未定义
|
||
|
||
在`module_base/.../common/Constant.kt`中添加:
|
||
|
||
```kotlin
|
||
object Key {
|
||
const val ID = "id"
|
||
const val PAGE_TYPE = "pageType" // 添加这个
|
||
}
|
||
```
|
||
|
||
## 错误排查流程
|
||
|
||
1. **DataBinding错误** → 检查import包名、枚举值
|
||
2. **Unresolved reference** → 检查import语句、常量定义
|
||
3. **suspend function错误** → 在`viewModelScope.launch`中调用
|
||
4. **仍有问题** → `./gradlew clean` 后重新构建
|
||
|
||
## 快速修复命令
|
||
|
||
```bash
|
||
# 查找DetailsPageType位置
|
||
grep -r "enum class DetailsPageType" module_base/src --include="*.kt"
|
||
|
||
# 查找IOnItemClickListener位置
|
||
find module_base/src -name "IOnItemClickListener.kt"
|
||
|
||
# 查找DataLayoutType枚举值
|
||
grep -A 5 "enum class DataLayoutType" module_base/src --include="*.kt"
|
||
```
|
||
|
||
## 开发原则
|
||
|
||
- ✅ 优先使用项目现有基类和封装
|
||
- ✅ 充分利用PadDataLayout和PadSearchLayout
|
||
- ✅ 遵循统一命名规范
|
||
- ✅ pageType用LiveData不用普通变量
|
||
- ✅ FlowBus.emit()必须在协程中调用
|
||
- ✅ 图片上传使用newName字段
|
||
- ✅ RecyclerView手动更新adapter不用items属性
|
||
|
||
## 技术栈
|
||
|
||
- Kotlin + 协程 1.6.0
|
||
- Retrofit 2.6.1 + OkHttp 3.12.12
|
||
- DataBinding + LiveData
|
||
- ARouter 1.5.2
|
||
- SmartRefreshLayout 2.0.3
|
||
- Glide 4.15.1
|
||
|
||
---
|
||
|
||
**签名配置**: `key.jks` / 密码: `123321` / 别名: `key`
|