857 lines
24 KiB
Markdown
857 lines
24 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>
|
||
```
|
||
|
||
## DataBinding + LiveData + ViewModel 核心知识
|
||
|
||
### 🎯 最关键的设置(最常见错误)
|
||
|
||
**必须在 Activity 中设置 lifecycleOwner,否则 XML 中的 LiveData 不会自动更新 UI!**
|
||
|
||
```kotlin
|
||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||
setBackArrow("页面标题")
|
||
binding.viewModel = viewModel
|
||
|
||
// ⚠️ 关键:必须设置,否则 LiveData 无法自动更新 UI
|
||
binding.lifecycleOwner = this
|
||
}
|
||
```
|
||
|
||
**BaseBindingActivity 已自动设置**,但如果手动使用 DataBinding 时务必记住!
|
||
|
||
### 📖 ViewModel 中 LiveData 的定义规范
|
||
|
||
```kotlin
|
||
class XxxViewModel : BaseViewModel() {
|
||
// ✅ 推荐:对外暴露不可变的 LiveData
|
||
private val _dataBean = MutableLiveData<XxxBean>()
|
||
val dataBean: LiveData<XxxBean> = _dataBean
|
||
|
||
// ✅ 简化写法:直接使用 MutableLiveData(项目常用)
|
||
val searchText = MutableLiveData<String>()
|
||
val pageType = MutableLiveData(DetailsPageType.Add)
|
||
|
||
fun loadData() {
|
||
// 主线程更新
|
||
_dataBean.value = XxxBean()
|
||
|
||
// 子线程更新(协程中不需要,已在主线程)
|
||
// _dataBean.postValue(XxxBean())
|
||
}
|
||
}
|
||
```
|
||
|
||
### 📝 XML 中 LiveData 的绑定方式
|
||
|
||
#### 1. 单向绑定 `@{}`(只显示,ViewModel → UI)
|
||
|
||
```xml
|
||
<layout>
|
||
<data>
|
||
<variable
|
||
name="viewModel"
|
||
type="com.lukouguoji.xxx.XxxViewModel" />
|
||
</data>
|
||
|
||
<!-- LiveData 自动解包:直接访问 value -->
|
||
<TextView
|
||
android:text="@{viewModel.dataBean.name}" />
|
||
|
||
<!-- 条件判断 -->
|
||
<View
|
||
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" />
|
||
|
||
<!-- 空值处理 -->
|
||
<TextView
|
||
android:text="@{viewModel.dataBean.name ?? `默认值`}" />
|
||
|
||
<!-- 字符串拼接(使用反引号) -->
|
||
<TextView
|
||
android:text="@{`姓名:` + viewModel.dataBean.name}" />
|
||
</layout>
|
||
```
|
||
|
||
#### 2. 双向绑定 `@={}`(可编辑,UI ↔ ViewModel)
|
||
|
||
```xml
|
||
<!-- EditText 双向绑定 -->
|
||
<EditText
|
||
android:text="@={viewModel.searchText}" />
|
||
|
||
<!-- PadSearchLayout 双向绑定 -->
|
||
<PadSearchLayout
|
||
type="@{SearchLayoutType.INPUT}"
|
||
value="@={viewModel.waybillNo}" />
|
||
|
||
<!-- PadDataLayout 双向绑定 -->
|
||
<PadDataLayout
|
||
type="@{DataLayoutType.INPUT}"
|
||
value="@={viewModel.dataBean.name}" />
|
||
```
|
||
|
||
**双向绑定要求**:
|
||
- 字段必须是 `MutableLiveData`
|
||
- 用户输入时自动更新 ViewModel 的值
|
||
- ViewModel 更新值时自动更新 UI
|
||
|
||
#### 3. 点击事件绑定
|
||
|
||
```xml
|
||
<!-- Lambda 表达式(推荐) -->
|
||
<Button
|
||
android:onClick="@{() -> viewModel.submit()}" />
|
||
|
||
<!-- 带参数 -->
|
||
<Button
|
||
android:onClick="@{(v) -> viewModel.onItemClick(v, 1)}" />
|
||
|
||
<!-- 自定义监听器 -->
|
||
<PadSearchLayout
|
||
setOnIconClickListener="@{(v) -> viewModel.scanWaybill()}" />
|
||
```
|
||
|
||
### ⚠️ DataBinding 常见错误与解决方法
|
||
|
||
#### 错误 1:忘记设置 lifecycleOwner
|
||
|
||
```kotlin
|
||
// ❌ 错误:LiveData 变化但 UI 不更新
|
||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||
binding.viewModel = viewModel
|
||
// 忘记设置 lifecycleOwner
|
||
}
|
||
|
||
// ✅ 正确:必须设置
|
||
override fun initOnCreate(savedInstanceState: Bundle?) {
|
||
binding.viewModel = viewModel
|
||
binding.lifecycleOwner = this // 关键!
|
||
}
|
||
```
|
||
|
||
#### 错误 2:在 XML 中传递 LiveData 而非值
|
||
|
||
```xml
|
||
<!-- ❌ 错误:某些属性只接受值,不接受 LiveData -->
|
||
<View
|
||
android:visibility="@{viewModel.isVisible}" />
|
||
<!-- 如果 isVisible 是 MutableLiveData<Boolean>,可能报错 -->
|
||
|
||
<!-- ✅ 正确:DataBinding 会自动解包 LiveData -->
|
||
<View
|
||
android:visibility="@{viewModel.isVisible ? View.VISIBLE : View.GONE}" />
|
||
<!-- 三元表达式会自动解包 -->
|
||
```
|
||
|
||
#### 错误 3:import 类型错误
|
||
|
||
```xml
|
||
<!-- ❌ 错误 -->
|
||
<import type="android.view.View.VISIBLE" />
|
||
|
||
<!-- ✅ 正确 -->
|
||
<import type="android.view.View" />
|
||
```
|
||
|
||
#### 错误 4:字符串未使用反引号
|
||
|
||
```xml
|
||
<!-- ❌ 错误:普通引号会被识别为 XML 属性 -->
|
||
<TextView
|
||
android:text="@{"姓名:" + viewModel.name}" />
|
||
|
||
<!-- ✅ 正确:使用反引号 ` -->
|
||
<TextView
|
||
android:text="@{`姓名:` + viewModel.name}" />
|
||
```
|
||
|
||
#### 错误 5:访问 LiveData 的 value 属性
|
||
|
||
```xml
|
||
<!-- ❌ 错误:DataBinding 会自动解包,不需要 .value -->
|
||
<TextView
|
||
android:text="@{viewModel.dataBean.value.name}" />
|
||
|
||
<!-- ✅ 正确:直接访问 -->
|
||
<TextView
|
||
android:text="@{viewModel.dataBean.name}" />
|
||
```
|
||
|
||
#### 错误 6:修改对象属性后 UI 不更新
|
||
|
||
```kotlin
|
||
// ❌ 错误:修改对象内部属性,LiveData 不会触发更新
|
||
val bean = dataBean.value
|
||
bean?.name = "新名称"
|
||
// UI 不会更新,因为 LiveData 的引用没变
|
||
|
||
// ✅ 正确:重新赋值 LiveData
|
||
val bean = dataBean.value?.copy(name = "新名称")
|
||
dataBean.value = bean
|
||
|
||
// ✅ 或者:使用 MutableLiveData + ObservableField
|
||
// 但项目中更推荐上面的方式
|
||
```
|
||
|
||
#### 错误 7:在 XML 中调用 suspend 函数
|
||
|
||
```xml
|
||
<!-- ❌ 错误:suspend 函数不能直接在 XML 中调用 -->
|
||
<Button
|
||
android:onClick="@{() -> viewModel.loadDataSuspend()}" />
|
||
|
||
<!-- ✅ 正确:在 ViewModel 中包装 -->
|
||
```
|
||
|
||
```kotlin
|
||
// ViewModel 中
|
||
fun loadData() { // 普通函数
|
||
launchLoadingCollect({ NetApply.api.getXxx() }) {
|
||
onSuccess = { dataBean.value = it.data }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🔍 XML DataBinding 调试技巧
|
||
|
||
#### 1. 检查 Binding 类是否生成
|
||
|
||
```bash
|
||
# 清理重新构建
|
||
./gradlew clean
|
||
./gradlew assembleDebug
|
||
```
|
||
|
||
#### 2. 查看 DataBinding 错误
|
||
|
||
- XML 中的错误可能不会立即显示
|
||
- 需要 Build 项目才能看到详细错误信息
|
||
- 错误信息通常在 Build Output 中
|
||
|
||
#### 3. 常见错误提示
|
||
|
||
```
|
||
Cannot find the setter for attribute 'android:text' with parameter type...
|
||
→ 检查属性类型是否匹配
|
||
|
||
Unresolved reference: viewModel
|
||
→ 检查 <variable> 声明和 import
|
||
|
||
cannot generate view binders
|
||
→ 检查 XML 语法错误,特别是 @{} 表达式
|
||
```
|
||
|
||
### 📋 DataBinding 开发检查清单
|
||
|
||
- ✅ Activity 中设置 `binding.lifecycleOwner = this`
|
||
- ✅ ViewModel 中需要双向绑定的字段使用 `MutableLiveData`
|
||
- ✅ XML 中字符串使用反引号 `` ` ``
|
||
- ✅ XML 中不访问 LiveData 的 `.value` 属性
|
||
- ✅ 修改对象属性后重新赋值 LiveData(触发更新)
|
||
- ✅ 点击事件使用 Lambda 表达式
|
||
- ✅ 正确 import 枚举和常量类
|
||
- ✅ XML 错误需要 Build 项目才能看到
|
||
|
||
## 核心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" // 添加这个
|
||
}
|
||
```
|
||
|
||
### 10. 资源引用错误(最常见的编译失败原因)
|
||
|
||
```xml
|
||
<!-- ❌ 错误: 引用不存在的资源会导致资源合并失败 -->
|
||
<TextView
|
||
android:background="@drawable/bg_custom"
|
||
android:textColor="@color/custom_color"
|
||
android:text="@string/custom_text" />
|
||
```
|
||
|
||
**问题原因**:
|
||
- 在布局文件中引用了项目中不存在的 `drawable`、`color`、`string` 等资源
|
||
- 导致构建时资源合并失败,无法生成R文件
|
||
- 报错信息: `Resource compilation failed` 或 `AAPT: error: resource ... not found`
|
||
|
||
**正确做法**:
|
||
|
||
1. **使用已存在的资源** - 先检查资源是否存在
|
||
```bash
|
||
# 查找drawable资源
|
||
find module_base/src/main/res/drawable -name "bg_custom*"
|
||
|
||
# 查找color定义
|
||
grep "custom_color" module_base/src/main/res/values/colors.xml
|
||
|
||
# 查找string定义
|
||
grep "custom_text" module_base/src/main/res/values/strings.xml
|
||
```
|
||
|
||
2. **主动创建缺失的资源** - 如果不存在则创建
|
||
|
||
```xml
|
||
<!-- 创建 drawable: module_base/src/main/res/drawable/bg_custom.xml -->
|
||
<?xml version="1.0" encoding="utf-8"?>
|
||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||
<solid android:color="@color/white"/>
|
||
<corners android:radius="4dp"/>
|
||
</shape>
|
||
|
||
<!-- 添加 color: module_base/src/main/res/values/colors.xml -->
|
||
<color name="custom_color">#333333</color>
|
||
|
||
<!-- 添加 string: module_base/src/main/res/values/strings.xml -->
|
||
<string name="custom_text">自定义文本</string>
|
||
```
|
||
|
||
3. **使用项目现有资源** - 避免重复创建
|
||
|
||
常用资源列表:
|
||
- **背景**: `bg_white_radius_8`, `bg_gray_radius_4`, `bg_primary_radius_4`
|
||
- **颜色**: `white`, `black`, `colorPrimary`, `text_normal`, `text_gray`, `text_red`
|
||
- **文字**: 优先直接写中文字符串,少用 string 资源
|
||
|
||
**检查清单**:
|
||
- ✅ 创建/修改布局文件前,确保引用的资源都存在
|
||
- ✅ 新增drawable时,在正确的module下创建(通常是`module_base`)
|
||
- ✅ 新增color/string时,添加到对应的values文件中
|
||
- ✅ 使用IDE的自动补全和资源预览功能,避免拼写错误
|
||
- ✅ 构建失败时,优先检查资源引用问题
|
||
|
||
## 错误排查流程
|
||
|
||
1. **资源引用错误** → 检查drawable/color/string是否存在,主动创建缺失资源
|
||
2. **DataBinding错误** → 检查import包名、枚举值
|
||
3. **Unresolved reference** → 检查import语句、常量定义
|
||
4. **suspend function错误** → 在`viewModelScope.launch`中调用
|
||
5. **仍有问题** → `./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"
|
||
```
|
||
|
||
## 开发原则
|
||
|
||
- ✅ **资源引用必须存在** - 创建/修改布局前,确保drawable/color/string资源真实存在或主动创建
|
||
- ✅ **必须设置 lifecycleOwner** - Activity 中 `binding.lifecycleOwner = this`(BaseBindingActivity 已自动设置)
|
||
- ✅ 优先使用项目现有基类和封装
|
||
- ✅ 充分利用PadDataLayout和PadSearchLayout组件
|
||
- ✅ 遵循统一命名规范
|
||
- ✅ pageType用LiveData不用普通变量
|
||
- ✅ XML中字符串拼接使用反引号,不访问LiveData的.value属性
|
||
- ✅ 修改对象属性后重新赋值LiveData才能触发UI更新
|
||
- ✅ FlowBus.emit()必须在协程中调用
|
||
- ✅ 图片上传使用newName字段
|
||
- ✅ RecyclerView手动更新adapter不用items属性
|
||
- ✅ 新建Activity后必须在AndroidManifest.xml中注册
|
||
|
||
## 技术栈
|
||
|
||
- 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`
|
||
- 当使用PadDataLayoutNew 控件的时候,titleLength 通常设置为5
|
||
- 在每个页面布局时,我会给你截图,请务必尽可能还原图片上的页面设计,而不是推测、假想。如果有困难(例如图片看不清,不明白的地方)一律要询问我,禁止自己想象。
|
||
- layout xml 最佳布局实践参考:
|
||
- module_gjc/src/main/res/layout/activity_gjc_weighing_record_details.xml
|
||
- module_gjc/src/main/res/layout/item_gjc_check_in_record.xml
|
||
- module_gjc/src/main/res/layout/activity_gjc_box_weighing_details.xml
|
||
- module_gjc/src/main/res/layout/activity_gjc_inspection.xml |