14 KiB
14 KiB
CLAUDE.md
项目开发指南 - 航空物流App
项目概述
AirLogistics - Android原生应用,航空物流全流程管理
- 包名: com.lukouguoji.aerologic
- 版本: 1.7.9 (API 24-30)
- 架构: MVVM + 组件化 + Kotlin + DataBinding
- 屏幕: 横屏 1152dp × 720dp
快速构建
./gradlew assembleDebug # 构建Debug版本
./gradlew clean # 清理构建
核心架构
MVVM层级
Activity → BaseBindingActivity → ViewModel → BaseViewModel/BasePageViewModel → API
关键基类
- BaseBindingActivity: DataBinding + ViewModel自动绑定
- BaseViewModel: Loading管理、协程支持
- BasePageViewModel: 分页列表(含PageModel)
- CommonAdapter + BaseViewHolder: 列表适配器
- PadSearchLayout: 搜索区域输入控件
- PadDataLayout: 数据展示/编辑控件
标准Activity模板
@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:
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:
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:
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()
}
}
}
}
网络请求
请求方法
// 带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接口定义
// 位置: 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 - 搜索输入框
<!-- 文本输入+扫码 -->
<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 - 数据展示/编辑
<!-- 文本输入 -->
<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步)
- 创建Bean (
module_base/.../bean/XxxBean.kt) - 添加API接口 (
Api.kt→getXxxList()) - 创建ViewHolder (继承
BaseViewHolder) - 创建ViewModel (继承
BasePageViewModel) - 创建Activity (继承
BaseBindingActivity) - 创建Layout (
activity_xxx_list.xml+item_xxx.xml) - 注册路由 (
ARouterConstants) - ⚠️ 在AndroidManifest.xml中注册Activity (
app/src/main/AndroidManifest.xml)
AndroidManifest.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" />
关键代码:
// Activity中绑定分页
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl, binding.recyclerView, viewModel, this
)
binding.recyclerView.addOnItemClickListener(viewModel)
详情页开发(5步)
- 添加API接口 (
getXxxDetails()) - 创建ViewModel (继承
BaseViewModel) - 创建Activity (含
companion object静态start方法) - 创建Layout
- ⚠️ 在AndroidManifest.xml中注册Activity
静态启动方法:
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步)
- 添加API接口 (
saveXxx()+getXxxDetails()) - 创建ViewModel (
pageType使用MutableLiveData) - 创建Activity (多个静态start方法:
startForAdd/Edit/Details) - 创建Layout (根据
pageType控制enable) - FlowBus发送刷新事件
- ⚠️ 在AndroidManifest.xml中注册Activity
Activity多入口:
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 ... */
}
}
常见业务场景
扫码
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()
}
}
图片上传
val result = UploadUtil.upload(filePath)
if (result.verifySuccess()) {
val imageUrl = result.data?.newName ?: "" // 注意是newName不是url
}
列表刷新事件
// 发送事件(在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()
}
常用扩展函数
// Toast
showToast("提示信息")
// 验证非空
if (text.verifyNullOrEmpty("请输入内容")) return
// 空处理
val text = nullableString.noNull("默认值")
// 日期格式化
val dateStr = Date().formatDate() // "2025-11-12"
// 权限申请
permission(Manifest.permission.CAMERA) { openCamera() }
常见编译错误
1. DetailsPageType包名错误
<!-- ❌ 错误 -->
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
<!-- ✅ 正确 -->
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
2. DataLayoutType枚举值错误
<!-- ❌ 错误: INTEGER不存在 -->
type="@{DataLayoutType.INTEGER}"
<!-- ✅ 正确: 使用INPUT -->
type="@{DataLayoutType.INPUT}"
可用类型: INPUT / SPINNER / DATE
3. DetailsPageType枚举值错误
// ❌ 错误: Edit不存在
DetailsPageType.Edit
// ✅ 正确: 使用Modify
DetailsPageType.Modify
可用类型: Add / Modify / Details
4. IOnItemClickListener包名错误
// ❌ 错误
import com.lukouguoji.module_base.impl.IOnItemClickListener
// ✅ 正确
import com.lukouguoji.module_base.interfaces.IOnItemClickListener
5. FlowBus使用错误
// ❌ 错误: 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. 图片上传字段错误
// ❌ 错误: UploadBean没有url字段
val imageUrl = result.data?.url
// ✅ 正确: 使用newName字段
val imageUrl = result.data?.newName
7. pageType必须用LiveData
// ❌ 错误: DataBinding无法绑定
var pageType: DetailsPageType = DetailsPageType.Add
// ✅ 正确: 使用LiveData
val pageType = MutableLiveData(DetailsPageType.Add)
8. RecyclerView不支持items属性
<!-- ❌ 错误: items属性会导致编译错误 -->
<RecyclerView
items="@{viewModel.list}" />
<!-- ✅ 正确: 在Activity中手动更新 -->
<RecyclerView android:id="@+id/recyclerView" />
// Activity中
viewModel.list.observe(this) { data ->
binding.recyclerView.commonAdapter()?.refresh(data)
}
9. Constant.Key.PAGE_TYPE未定义
在module_base/.../common/Constant.kt中添加:
object Key {
const val ID = "id"
const val PAGE_TYPE = "pageType" // 添加这个
}
错误排查流程
- DataBinding错误 → 检查import包名、枚举值
- Unresolved reference → 检查import语句、常量定义
- suspend function错误 → 在
viewModelScope.launch中调用 - 仍有问题 →
./gradlew clean后重新构建
快速修复命令
# 查找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