Files
aerologic-app/CLAUDE.md
2025-11-14 12:15:33 +08:00

14 KiB
Raw Blame History

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步)

  1. 创建Bean (module_base/.../bean/XxxBean.kt)
  2. 添加API接口 (Api.ktgetXxxList())
  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注册示例:

<!-- 在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步)

  1. 添加API接口 (getXxxDetails())
  2. 创建ViewModel (继承BaseViewModel)
  3. 创建Activity (含companion object静态start方法)
  4. 创建Layout
  5. ⚠️ 在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步)

  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多入口:

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"  // 添加这个
}

错误排查流程

  1. DataBinding错误 → 检查import包名、枚举值
  2. Unresolved reference → 检查import语句、常量定义
  3. suspend function错误 → 在viewModelScope.launch中调用
  4. 仍有问题./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