# 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() { 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() 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() 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(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 @POST("api/xxx/details") suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean @POST("api/xxx/save") suspend fun saveXxx(@Body data: RequestBody): BaseResultBean ``` ## 核心UI组件 ### PadSearchLayout - 搜索输入框 ```xml ``` **类型**: `INPUT` / `INTEGER` / `SPINNER` / `DATE` ### PadDataLayout - 数据展示/编辑 ```xml ``` **类型**: `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 ``` **关键代码:** ```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(ConstantEvent.EVENT_REFRESH).emit("refresh") } // 接收事件(在Activity中) import com.lukouguoji.module_base.impl.observe // 必须导入 FlowBus.with(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 ``` ### 2. DataLayoutType枚举值错误 ```xml type="@{DataLayoutType.INTEGER}" 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(event).emit("data") // ✅ 正确 viewModelScope.launch { FlowBus.with(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 ``` ```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`