Files
aerologic-app/CLAUDE.md

62 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Android 项目特定说明

项目概况

项目名称: AirLogistics - 航空物流信息管理系统 项目类型: Android 原生应用 架构模式: MVVM + 组件化 开发语言: Kotlin 1.6.21 + Java 当前版本: 1.8.4 (versionCode 84)

SDK 版本要求:

  • minSdkVersion: 24 (Android 7.0)
  • targetSdkVersion: 30 (Android 10)
  • compileSdkVersion: 31

核心架构

MVVM 基类体系

  • BaseActivity: 提供协程支持、Loading管理、扫码功能、键盘控制
  • BaseBindingActivity: DataBinding自动绑定、ViewModel生命周期管理
  • BaseViewModel: Loading管理、Activity结果处理
  • BasePageViewModel: 分页列表专用,集成PageModel自动处理分页
  • CommonAdapter + BaseViewHolder: 统一列表适配器封装

组件化模块划分

  • app/: 应用壳层,整合所有业务模块
  • module_base/: 核心基础库(MVVM基类、网络框架、UI组件)
  • module_gnc/: 国内出港业务模块
  • module_gnj/: 国内进港业务模块
  • module_gjc/: 国际出港业务模块
  • module_gjj/: 国际进港业务模块
  • module_hangban/: 航班管理模块
  • module_cargo/: 货物追踪模块
  • module_mit/: 监装监卸管理模块
  • module_p/: PDA专用功能模块
  • Printer/: 蓝牙打印模块
  • MPChartLib/: 图表库模块

模块间通信

  • 路由: ARouter 1.5.2 实现模块间页面跳转
  • 事件总线: FlowBus(基于Flow) + EventBus 3.1.1
  • 依赖注入: 基于ServiceLoader的服务发现机制

网络请求框架

  • 技术栈: Retrofit 2.6.1 + OkHttp 3.12.12 + Kotlin Coroutines
  • 扩展函数:
    • launchCollect: 无Loading的后台请求
    • launchLoadingCollect: 带Loading的关键操作
    • toRequestBody: Map/Bean自动转JSON
  • 拦截器: 自动添加Token、时间戳,统一错误处理

关键目录结构

aerologic-app/
├── app/src/main/java/com/lukouguoji/aerologic/
│   ├── ui/viewModel/          # ViewModel文件
│   ├── ui/fragment/           # Fragment文件 (HomeFragment, MineFragment等)
│   └── page/                  # 业务页面
├── module_base/src/main/java/com/lukouguoji/module_base/
│   ├── BaseActivity.kt        # 基础Activity类
│   ├── BaseFragment.kt        # 基础Fragment类
│   ├── bean/                  # 数据模型 (BaseResultBean, BaseListBean)
│   ├── service/viewModel/     # ViewModel层
│   ├── ui/page/               # UI页面
│   ├── ui/weight/             # 自定义UI组件 (PadSearchLayout, PadDataLayout)
│   ├── http/                  # 网络请求框架
│   └── ktx/                   # Kotlin扩展函数
├── module_gnc/src/main/       # 国内出港业务代码
├── module_gnj/src/main/       # 国内进港业务代码
└── 其他业务模块...

开发规范

命名约定

  • Activity: XxxActivity (例: LoginActivity)
  • Fragment: XxxFragment (例: HomeFragment)
  • ViewModel: XxxViewModel (例: LoginViewModel)
  • Adapter: XxxAdapter (例: CargoListAdapter)
  • ViewHolder: XxxViewHolder (例: CargoItemViewHolder)
  • Layout文件: activity_xxx.xml, fragment_xxx.xml, item_xxx.xml

文件组织规范

  • 业务页面放在对应模块的 ui/page/ 目录下
  • ViewModel放在 service/viewModel/ 目录下
  • 数据模型放在 bean/ 目录下
  • 适配器放在 adapter/ 目录下

DataBinding 使用要点

  • 布局文件使用 <layout> 标签包裹
  • 定义 <variable> 绑定 ViewModel
  • 使用 @{} 表达式进行数据绑定
  • Activity/Fragment 中使用 DataBindingUtil 或自动生成的 Binding 类

协程使用规范

  • 在 ViewModel 中使用 viewModelScope 启动协程
  • 网络请求使用 launchCollectlaunchLoadingCollect 扩展函数
  • Flow 用于响应式数据流处理
  • 使用 withContext(Dispatchers.IO) 进行IO操作

常用构建命令

# 清理构建缓存
./gradlew clean

# 构建 Debug APK
./gradlew assembleDebug

# 构建 Release APK (已签名)
./gradlew assembleRelease

# 安装到设备
./gradlew installDebug

# 运行 Lint 检查
./gradlew lint

# 查看已连接设备
adb devices -l

# 查看应用日志
adb logcat | grep "com.lukouguoji.aerologic"

快捷命令

项目已配置以下快捷命令 (在 .claude/commands/ 目录):

  • /build-debug - 构建 Debug APK
  • /build-release - 构建 Release APK
  • /install - 安装到设备
  • /clean-build - 清理并构建
  • /check-modules - 检查所有模块
  • /lint - 运行代码检查
  • /devices - 列出已连接设备
  • /logs - 查看应用日志

组件化开发模式

项目支持模块独立运行调试:

  1. 编辑 gradle.properties
  2. 设置 isBuildModule=true (独立模式) 或 false (集成模式)
  3. Sync项目并运行对应模块

注意: 独立模式下,各模块作为独立应用运行;集成模式下,所有模块整合到app壳层。

环境配置

开发环境要求

  • IDE: Android Studio Arctic Fox (2020.3.1) 或更高版本
  • JDK: 1.8
  • Gradle: 7.3.3
  • Kotlin: 1.6.21

服务器配置

  • 配置文件: module_base/src/main/res/values/strings.xml
  • 主服务器: system_url_inner
  • 地磅服务器: weight_url
  • 运行时: 可通过 SharedPreferences 动态修改IP地址

签名配置

  • KeyStore: key.jks (项目根目录)
  • Store密码: 123321
  • Key密码: 123321
  • 别名: key

常见问题解决

依赖下载失败

  1. 检查网络连接
  2. 使用阿里云Maven镜像 (已在 build.gradle 中配置)
  3. 如需手动配置 Gradle,参考 README.md 中的依赖配置章节

模块编译错误

  1. 执行 ./gradlew clean
  2. 检查 gradle.properties 中的 isBuildModule 设置
  3. Sync Project with Gradle Files

ADB 连接问题

# 重启 ADB 服务
adb kill-server && adb start-server

# 查看设备连接状态
adb devices -l

# 无线调试 (Android 11+)
adb pair <IP>:<PORT>
adb connect <IP>:<PORT>

详细开发指南

标准代码模板

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()
            }
        }
    }
}

DataBinding + LiveData 核心知识

最关键的设置 (最常见错误)

必须在 Activity 中设置 lifecycleOwner,否则 XML 中的 LiveData 不会自动更新 UI!

override fun initOnCreate(savedInstanceState: Bundle?) {
    setBackArrow("页面标题")
    binding.viewModel = viewModel

    // ⚠️ 关键:必须设置,否则 LiveData 无法自动更新 UI
    binding.lifecycleOwner = this
}

BaseBindingActivity 已自动设置,但如果手动使用 DataBinding 时务必记住!

XML 中 LiveData 的绑定方式

1. 单向绑定 @{}(只显示,ViewModel → UI)

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

<!-- 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. 点击事件绑定

<!-- Lambda 表达式(推荐) -->
<Button
    android:onClick="@{() -> viewModel.submit()}" />

<!-- 带参数 -->
<Button
    android:onClick="@{(v) -> viewModel.onItemClick(v, 1)}" />

<!-- 自定义监听器 -->
<PadSearchLayout
    setOnIconClickListener="@{(v) -> viewModel.scanWaybill()}" />

DataBinding 常见错误与解决方法

错误 1: 忘记设置 lifecycleOwner

// ❌ 错误:LiveData 变化但 UI 不更新
override fun initOnCreate(savedInstanceState: Bundle?) {
    binding.viewModel = viewModel
    // 忘记设置 lifecycleOwner
}

// ✅ 正确:必须设置
override fun initOnCreate(savedInstanceState: Bundle?) {
    binding.viewModel = viewModel
    binding.lifecycleOwner = this  // 关键!
}

错误 2: 字符串未使用反引号

<!-- ❌ 错误:普通引号会被识别为 XML 属性 -->
<TextView
    android:text="@{"姓名:" + viewModel.name}" />

<!-- ✅ 正确:使用反引号 ` -->
<TextView
    android:text="@{`姓名:` + viewModel.name}" />

错误 3: 访问 LiveData 的 value 属性

<!-- ❌ 错误:DataBinding 会自动解包,不需要 .value -->
<TextView
    android:text="@{viewModel.dataBean.value.name}" />

<!-- ✅ 正确:直接访问 -->
<TextView
    android:text="@{viewModel.dataBean.name}" />

错误 4: 修改对象属性后 UI 不更新

// ❌ 错误:修改对象内部属性,LiveData 不会触发更新
val bean = dataBean.value
bean?.name = "新名称"
// UI 不会更新,因为 LiveData 的引用没变

// ✅ 正确:重新赋值 LiveData
val bean = dataBean.value?.copy(name = "新名称")
dataBean.value = bean

核心 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 注意: 使用 PadDataLayout 时,titleLength 通常设置为 5

PadDataLayoutNew - 输入完成回调

使用场景: 当需要在用户完成输入(失去焦点)时触发自动查询或其他操作

正确用法: 使用方法引用语法 viewModel::methodName

<!-- 输入完成后自动查询 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
    android:id="@+id/carIdInput"
    hint='@{"请输入架子车号"}'
    setRefreshCallBack="@{viewModel::onCarIdInputComplete}"
    title='@{"架子车号"}'
    titleLength="@{5}"
    type="@{DataLayoutType.INPUT}"
    value='@={viewModel.carId}'
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />

<!-- 日期选择完成后触发回调 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
    setRefreshCallBack="@{viewModel::onFlightDateInputComplete}"
    title='@{"航班日期"}'
    titleLength="@{5}"
    type="@{DataLayoutType.DATE}"
    value='@={viewModel.flightDate}'
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1" />

ViewModel 中的实现:

/**
 * 架子车号输入完成时调用
 */
fun onCarIdInputComplete() {
    val id = carId.value
    if (!id.isNullOrEmpty() && id != lastQueriedCarId) {
        lastQueriedCarId = id
        queryFlatcarInfo(id)
    }
}

/**
 * 航班日期选择完成时调用
 */
fun onFlightDateInputComplete() {
    // 清除查询标记,以便重新查询
    lastQueriedFlight = ""
    queryFlightIfReady()
}

关键要点:

  • 正确: setRefreshCallBack="@{viewModel::methodName}" - 使用方法引用
  • 错误: setRefreshCallBack="@{() -> viewModel.methodName()}" - Lambda 表达式会导致编译错误
  • 回调在输入框失去焦点时触发 (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) {
        val starter = Intent(context, XxxAddActivity::class.java)
            .putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Modify.name)
            .putExtra(Constant.Key.ID, id)
        context.startActivity(starter)
    }
}

列表查询+多选+批量处理页面开发 (完整指南)

这是项目中最常见的业务场景之一列表查询、多选Item、批量操作。本指南基于IntExpOutHandoverActivity(国际出港-出库交接)的实际案例总结。

典型场景特征

  • 顶部多条件搜索区域
  • 列表支持多选(图片切换表示选择状态)
  • 底部全选按钮+统计信息+批量操作按钮
  • 分页加载数据

开发步骤总览 (8步)

  1. 修改/创建Bean (添加ObservableBoolean选择状态)
  2. 定义API接口 (列表查询+统计查询+批量操作)
  3. 创建ViewHolder (处理选择图标点击)
  4. 创建ViewModel (继承BasePageViewModel)
  5. 创建Activity布局 (搜索区+列表+底部栏)
  6. 创建列表项布局 (使用completeSpace对齐)
  7. 创建Activity (绑定数据+观察全选状态)
  8. 在AndroidManifest.xml中注册

步骤1: 修改/创建Bean

关键点: 使用ObservableBoolean支持实时UI更新

import androidx.databinding.ObservableBoolean

class GjcUldUseBean {
    // ... 业务字段 ...

    // ========== UI扩展字段 ==========
    val checked: ObservableBoolean = ObservableBoolean(false)  // 选中状态

    // 兼容现有API的isSelected属性
    var isSelected: Boolean
        get() = checked.get()
        set(value) = checked.set(value)
}

为什么用ObservableBoolean而不是Boolean?

  • DataBinding会自动观察ObservableBoolean的变化
  • 调用checked.set(true)会立即触发UI刷新
  • 普通Boolean需要手动调用notifyDataSetChanged()

步骤2: 定义API接口

// Api.kt
/**
 * 列表查询(分页)
 */
@POST("IntExpOutHandover/pageQuery")
suspend fun getIntExpOutHandoverList(@Body data: RequestBody): BaseListBean<GjcUldUseBean>

/**
 * 统计查询(合计信息)
 */
@POST("IntExpOutHandover/pageQueryTotal")
suspend fun getIntExpOutHandoverTotal(@Body data: RequestBody): BaseResultBean<ManifestTotalDto>

/**
 * 批量操作(交接完成)
 */
@POST("IntExpOutHandover/handover")
suspend fun completeHandover(@Body data: RequestBody): BaseResultBean<Boolean>

步骤3: 创建ViewHolder

关键点: 添加选择图标点击事件

class IntExpOutHandoverViewHolder(view: View) :
    BaseViewHolder<GjcUldUseBean, ItemIntExpOutHandoverBinding>(view) {

    override fun onBind(item: Any?, position: Int) {
        val bean = getItemBean(item) ?: return
        binding.bean = bean
        binding.position = position
        binding.executePendingBindings()

        // 添加图标点击事件 - 切换选择状态
        binding.ivIcon.setOnClickListener {
            // 反转checked状态
            bean.checked.set(!bean.checked.get())

            // 立即更新UI (图片自动切换)
            binding.executePendingBindings()
        }
    }
}

步骤4: 创建ViewModel

关键点: 继承BasePageViewModel,实现全选逻辑

class IntExpOutHandoverViewModel : BasePageViewModel() {

    // ========== 搜索条件 ==========
    val flightDate = MutableLiveData("")
    val flightNo = MutableLiveData("")
    val fdest = MutableLiveData("")
    val uldNo = MutableLiveData("")

    // ========== 统计信息 ==========
    val totalCount = MutableLiveData("0")
    val totalPc = MutableLiveData("0")
    val totalWeight = MutableLiveData("0")

    // ========== 全选状态 ==========
    val isAllChecked = MutableLiveData(false)

    init {
        // 监听全选状态,自动更新所有列表项
        isAllChecked.observeForever { checked ->
            val list = pageModel.rv?.commonAdapter()?.items as? List<GjcUldUseBean> ?: return@observeForever
            list.forEach { it.checked.set(checked) }
            pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
        }
    }

    // ========== 适配器配置 ==========
    val itemViewHolder = IntExpOutHandoverViewHolder::class.java
    val itemLayoutId = R.layout.item_int_exp_out_handover

    /**
     * 搜索按钮点击
     */
    fun searchClick() {
        refresh()
    }

    /**
     * 全选按钮点击 (切换全选状态)
     */
    fun checkAllClick() {
        val list = pageModel.rv?.commonAdapter()?.items as? List<GjcUldUseBean> ?: return

        // 切换全选状态
        val shouldCheckAll = !isAllChecked.value!!
        list.forEach { it.checked.set(shouldCheckAll) }
        isAllChecked.value = shouldCheckAll

        pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
    }

    /**
     * 扫码ULD
     */
    fun scanUld() {
        ScanModel.startScan(getTopActivity(), Constant.RequestCode.ULD)
    }

    /**
     * 完成交接 (批量操作)
     */
    fun completeHandover() {
        val list = pageModel.rv?.commonAdapter()?.items as? List<GjcUldUseBean> ?: return
        val selectedItems = list.filter { it.isSelected }

        if (selectedItems.isEmpty()) {
            showToast("请选择要交接的ULD")
            return
        }

        val requestData = selectedItems.toRequestBody()

        launchLoadingCollect({ NetApply.api.completeHandover(requestData) }) {
            onSuccess = {
                showToast("交接完成")
                viewModelScope.launch {
                    FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
                }
                refresh()
            }
        }
    }

    /**
     * 获取数据 (重写BasePageViewModel)
     */
    override fun getData() {
        // 构建搜索条件
        val filterParams = mapOf(
            "fdate" to flightDate.value?.ifEmpty { null },
            "fno" to flightNo.value?.ifEmpty { null },
            "fdest" to fdest.value?.ifEmpty { null },
            "uld" to uldNo.value?.ifEmpty { null }
        )

        // 列表参数 (含分页)
        val listParams = (filterParams + mapOf(
            "pageNum" to pageModel.page,
            "pageSize" to pageModel.limit
        )).toRequestBody()

        // 统计参数 (无分页)
        val totalParams = filterParams.toRequestBody()

        // 获取列表 (带Loading)
        launchLoadingCollect({ NetApply.api.getIntExpOutHandoverList(listParams) }) {
            onSuccess = { pageModel.handleListBean(it) }
        }

        // 获取统计信息 (后台请求,不阻塞列表)
        launchCollect({ NetApply.api.getIntExpOutHandoverTotal(totalParams) }) {
            onSuccess = { result ->
                val data = result.data
                totalCount.value = (data?.wbNumber ?: 0).toString()
                totalPc.value = (data?.totalPc ?: 0).toString()
                totalWeight.value = (data?.totalWeight ?: 0.0).toString()
            }
        }
    }
}

步骤5: 创建Activity布局

关键要素: 搜索区 + 列表 + 底部栏

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 标题栏 -->
    <include layout="@layout/title_tool_bar" />

    <!-- 搜索区域 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <!-- 航班日期 -->
        <com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            type="@{SearchLayoutType.DATE}"
            value="@={viewModel.flightDate}" />

        <!-- 航班号 -->
        <com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            type="@{SearchLayoutType.INPUT}"
            value="@={viewModel.flightNo}" />

        <!-- ULD编号 (带扫码) -->
        <com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            type="@{SearchLayoutType.INPUT}"
            value="@={viewModel.uldNo}"
            icon="@{@drawable/scan_code}"
            setOnIconClickListener="@{(v)-> viewModel.scanUld()}" />

        <!-- 搜索按钮 -->
        <ImageView
            style="@style/iv_search_action"
            android:onClick="@{()-> viewModel.searchClick()}"
            android:src="@drawable/img_search" />

    </LinearLayout>

    <!-- 列表 -->
    <com.scwang.smart.refresh.layout.SmartRefreshLayout
        android:id="@+id/srl"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            itemLayoutId="@{viewModel.itemLayoutId}"
            viewHolder="@{viewModel.itemViewHolder}" />

    </com.scwang.smart.refresh.layout.SmartRefreshLayout>

    <!-- 底部栏: 全选 + 统计 + 操作按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/color_bottom_layout"
        android:gravity="center_vertical">

        <!-- 全选按钮 (图标+文字) -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:onClick="@{()-> viewModel.checkAllClick()}">

            <ImageView
                android:id="@+id/checkIcon"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:src="@drawable/img_check_all" />

            <TextView
                android:text="全选"
                android:textColor="@color/white"
                android:textSize="18sp" />

        </LinearLayout>

        <!-- 统计信息 -->
        <LinearLayout
            android:layout_width="0dp"
            android:layout_weight="1"
            android:orientation="horizontal">

            <TextView
                android:text='@{"合计:"+viewModel.totalCount+"票"}'
                android:textColor="@color/white" />

            <TextView
                android:text='@{"总件数:"+viewModel.totalPc}'
                android:textColor="@color/white" />

            <TextView
                android:text='@{"总重量:"+viewModel.totalWeight}'
                android:textColor="@color/white" />

        </LinearLayout>

        <!-- 批量操作按钮 -->
        <TextView
            style="@style/tv_bottom_btn"
            android:onClick="@{()-> viewModel.completeHandover()}"
            android:text="交接完成" />

    </LinearLayout>

</LinearLayout>

步骤6: 创建列表项布局

关键点: 使用completeSpace属性实现Key左对齐

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_white_radius_8"
    android:orientation="horizontal">

    <!-- 选中图标 (根据checked状态切换图片) -->
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="40dp"
        android:layout_height="40dp"
        loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}"
        android:src="@drawable/img_plane" />

    <!-- 数据展示区域 -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_weight="1"
        android:orientation="vertical">

        <!-- 第一行数据 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <!-- ULD编号 (weight=1.5) -->
            <LinearLayout
                android:layout_width="0dp"
                android:layout_weight="1.5"
                android:orientation="horizontal">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    completeSpace="@{5}"
                    android:text="ULD编号"
                    android:textColor="@color/text_normal" />

                <TextView
                    android:text="@{bean.uld}"
                    android:textColor="@color/colorPrimary"
                    android:textStyle="bold" />

            </LinearLayout>

            <!-- 架子车号 (weight=1) -->
            <LinearLayout
                android:layout_width="0dp"
                android:layout_weight="1"
                android:orientation="horizontal">

                <TextView
                    completeSpace="@{5}"
                    android:text="架子车号:"
                    android:textColor="@color/text_normal" />

                <TextView
                    android:text="@{String.valueOf(bean.carId)}"
                    android:textColor="@color/text_normal" />

            </LinearLayout>

            <!-- 总重 (weight=1) -->
            <LinearLayout
                android:layout_width="0dp"
                android:layout_weight="1"
                android:orientation="horizontal">

                <TextView
                    completeSpace="@{3}"
                    android:text="总重:"
                    android:textColor="@color/text_normal" />

                <TextView
                    android:text="@{String.valueOf((int)bean.totalWeight)}"
                    android:textColor="@color/text_normal" />

            </LinearLayout>

            <!-- 装机重量 (weight=1) -->
            <!-- 货重 (weight=1.5) -->
            <!-- ... 其他字段 ... -->

        </LinearLayout>

        <!-- 第二行数据 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:orientation="horizontal">

            <!-- 航班日期 (weight=1.5) -->
            <!-- 航班号 (weight=1) -->
            <!-- 目的港 (weight=1) -->
            <!-- 交接人 (weight=1) -->
            <!-- 交接时间 (weight=1.5) -->
            <!-- ... -->

        </LinearLayout>

    </LinearLayout>

</LinearLayout>

权重分配原则:

  • 较长字段(如"ULD编号"、"交接时间")使用较大权重(1.5)
  • 较短字段(如"总重"、"航班号")使用较小权重(1.0)
  • completeSpace根据文字字数设置(3-5个字符宽度)

步骤7: 创建Activity

@Route(path = ARouterConstants.ACTIVITY_URL_INT_EXP_OUT_HANDOVER)
class IntExpOutHandoverActivity :
    BaseBindingActivity<ActivityIntExpOutHandoverBinding, IntExpOutHandoverViewModel>() {

    override fun layoutId() = R.layout.activity_int_exp_out_handover
    override fun viewModelClass() = IntExpOutHandoverViewModel::class.java

    override fun initOnCreate(savedInstanceState: Bundle?) {
        setBackArrow("出库交接")
        binding.viewModel = viewModel

        // 观察全选状态,更新图标透明度
        viewModel.isAllChecked.observe(this) { isAllChecked ->
            binding.checkIcon.alpha = if (isAllChecked) 1.0f else 0.5f
        }

        // 绑定分页
        viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this)

        // 设置item点击监听
        binding.rv.addOnItemClickListener(viewModel)

        // 监听刷新事件
        FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) {
            viewModel.refresh()
        }

        // 初始加载数据
        viewModel.refresh()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == Constant.RequestCode.ULD && resultCode == Activity.RESULT_OK) {
            viewModel.uldNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
            viewModel.searchClick()
        }
    }
}

步骤8: 注册Activity

<!-- app/src/main/AndroidManifest.xml -->
<activity
    android:name="com.lukouguoji.gjc.activity.IntExpOutHandoverActivity"
    android:configChanges="orientation|keyboardHidden"
    android:exported="false"
    android:screenOrientation="userLandscape" />

关键技术点总结

1. ObservableBoolean vs Boolean
特性 ObservableBoolean Boolean
DataBinding支持 自动观察 不支持
UI实时更新 调用set()自动刷新 需手动notifyDataSetChanged()
代码简洁性 更简洁 需额外代码
2. 全选交互逻辑
用户点击全选按钮
    ↓
checkAllClick() 被调用
    ↓
遍历列表,调用 bean.checked.set(shouldCheckAll)
    ↓
ObservableBoolean触发DataBinding更新
    ↓
列表项图片自动切换 (img_plane ↔ img_plane_s)
3. 图片资源切换
<!-- loadImage 是自定义BindingAdapter -->
loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}"

图片资源:

  • img_plane.png - 未选中状态
  • img_plane_s.png - 已选中状态 (通常是高亮/彩色版本)
  • img_check_all.png - 全选图标
4. completeSpace属性原理

completeSpace是自定义BindingAdapter用于实现Key左对齐:

// TextViewAdapter.kt
@BindingAdapter("completeSpace")
fun completeSpace(tv: TextView, count: Int) {
    // 1. 根据count个"一"字宽度设置TextView宽度
    val s = StringBuilder()
    (1..count).forEach { _ -> s.append("一") }
    val measureText = tv.paint.measureText(s.toString())
    ViewUtils.setWidth(tv, measureText.roundToInt())

    // 2. 自动填充全角空格使文本均匀分布
    // 确保"航班日期:"与"航班号:"的冒号位置对齐
}

使用示例:

  • completeSpace="@{5}" - 5个"一"字宽度 (适合"ULD编号"、"航班日期:")
  • completeSpace="@{4}" - 4个"一"字宽度 (适合"航班号:"、"交接人:")
  • completeSpace="@{3}" - 3个"一"字宽度 (适合"总重:"、"货重:")
5. 分页处理

BasePageViewModel自动处理分页逻辑:

  • pageModel.page - 当前页码
  • pageModel.limit - 每页条数
  • pageModel.handleListBean(it) - 自动处理列表数据和分页状态
6. 批量操作最佳实践
fun completeHandover() {
    // 1. 获取列表
    val list = pageModel.rv?.commonAdapter()?.items as? List<GjcUldUseBean> ?: return

    // 2. 过滤选中项
    val selectedItems = list.filter { it.isSelected }

    // 3. 验证
    if (selectedItems.isEmpty()) {
        showToast("请选择要交接的ULD")
        return
    }

    // 4. 转换为RequestBody
    val requestData = selectedItems.toRequestBody()

    // 5. 发起请求
    launchLoadingCollect({ NetApply.api.completeHandover(requestData) }) {
        onSuccess = {
            showToast("交接完成")
            // 6. 发送刷新事件
            viewModelScope.launch {
                FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
            }
            // 7. 刷新当前页
            refresh()
        }
    }
}

常见错误与解决方案

错误1: 点击图标不切换
// ❌ 错误: 使用普通Boolean
var isSelected: Boolean = false

// ✅ 正确: 使用ObservableBoolean
val checked: ObservableBoolean = ObservableBoolean(false)
错误2: 全选不生效
// ❌ 错误: 直接修改isSelected
list.forEach { it.isSelected = checked }

// ✅ 正确: 调用ObservableBoolean的set方法
list.forEach { it.checked.set(checked) }
错误3: 布局不对齐
<!-- ❌ 错误: 直接拼接key和value -->
<TextView android:text='@{"航班日期 " + bean.fdate}' />

<!-- ✅ 正确: 使用completeSpace属性 -->
<LinearLayout>
    <TextView completeSpace="@{5}" android:text="航班日期:" />
    <TextView android:text="@{bean.fdate}" />
</LinearLayout>
错误4: 忘记观察全选状态
// ❌ 错误: 没有观察isAllChecked

// ✅ 正确: 在Activity中观察全选状态
viewModel.isAllChecked.observe(this) { isAllChecked ->
    binding.checkIcon.alpha = if (isAllChecked) 1.0f else 0.5f
}

参考示例

完整实现参考:

  • Activity: module_gjc/src/main/java/com/lukouguoji/gjc/activity/IntExpOutHandoverActivity.kt
  • ViewModel: module_gjc/src/main/java/com/lukouguoji/gjc/viewModel/IntExpOutHandoverViewModel.kt
  • ViewHolder: module_gjc/src/main/java/com/lukouguoji/gjc/holder/IntExpOutHandoverViewHolder.kt
  • Activity布局: module_gjc/src/main/res/layout/activity_int_exp_out_handover.xml
  • Item布局: module_gjc/src/main/res/layout/item_int_exp_out_handover.xml
  • Bean: module_base/src/main/java/com/lukouguoji/module_base/bean/GjcUldUseBean.kt

其他类似实现:

  • GjcAssembleAllocateActivity - 国际出港组装分配

常见业务场景

扫码

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. 资源引用错误 (最常见的编译失败原因)

<!-- ❌ 错误: 引用不存在的资源会导致资源合并失败 -->
<TextView
    android:background="@drawable/bg_custom"
    android:textColor="@color/custom_color"
    android:text="@string/custom_text" />

问题原因:

  • 在布局文件中引用了项目中不存在的 drawablecolorstring 等资源
  • 导致构建时资源合并失败,无法生成R文件
  • 报错信息: Resource compilation failedAAPT: error: resource ... not found

正确做法:

  1. 使用已存在的资源 - 先检查资源是否存在
# 查找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
  1. 主动创建缺失的资源 - 如果不存在则创建
<!-- 创建 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>
  1. 使用项目现有资源 - 避免重复创建

常用资源列表:

  • 背景: bg_white_radius_8, bg_gray_radius_4, bg_primary_radius_4
  • 颜色: white, black, colorPrimary, text_normal, text_gray, text_red
  • 文字: 优先直接写中文字符串,少用 string 资源

10. DataBinding中View类未导入

<!-- ❌ 错误: 使用View.VISIBLE但未导入View -->
<data>
    <variable name="viewModel" type="..." />
</data>

<View android:visibility="@{viewModel.isVisible ? View.VISIBLE : View.GONE}" />

<!-- ✅ 正确: 必须导入View类 -->
<data>
    <import type="android.view.View" />
    <variable name="viewModel" type="..." />
</data>

<View android:visibility="@{viewModel.isVisible ? View.VISIBLE : View.GONE}" />

11. DataBinding中textStyle属性错误

<!-- ❌ 错误: textStyle不支持DataBinding字符串 -->
<TextView
    android:textStyle="@{viewModel.isBold ? `bold` : `normal`}" />

<!-- ✅ 正确: 直接使用固定值或删除该属性 -->
<TextView
    android:textStyle="bold" />

原因: textStyle属性只接受整数值(如Typeface.BOLD),不接受字符串

常用Import路径速查表

⚠️ 重要提醒

在创建新的Activity、ViewModel、Fragment时,以下import路径最容易出错。务必使用正确的包名:

基础类Import (module_base)

// ==================== 基类 ====================
import com.lukouguoji.module_base.base.BaseActivity              // Activity基类
import com.lukouguoji.module_base.base.BaseBindingActivity      // DataBinding Activity基类
import com.lukouguoji.module_base.base.BaseViewModel            // ViewModel基类 ⚠️ 不是service.viewModel!
import com.lukouguoji.module_base.base.BasePageViewModel        // 分页列表ViewModel基类
import com.lukouguoji.module_base.base.BaseFragment             // Fragment基类
import com.lukouguoji.module_base.base.BaseBindingFragment      // DataBinding Fragment基类
import com.lukouguoji.module_base.base.BaseViewHolder           // ViewHolder基类
import com.lukouguoji.module_base.base.CustomVP2Adapter         // ViewPager2适配器

// ==================== 常量类 ====================
import com.lukouguoji.module_base.common.Constant               // 常量类 ⚠️ 不是根包下的Constant!
import com.lukouguoji.module_base.common.DetailsPageType        // 详情页类型(Add/Modify/Details)
import com.lukouguoji.module_base.common.ConstantEvent          // 事件常量

// ==================== 网络相关 ====================
import com.lukouguoji.module_base.http.net.NetApply             // API调用入口
import com.lukouguoji.module_base.http.net.Api                  // API接口定义
import com.lukouguoji.module_base.bean.BaseResultBean           // 通用返回结果
import com.lukouguoji.module_base.bean.BaseListBean             // 列表返回结果
import com.lukouguoji.module_base.bean.PageInfo                 // 分页信息

// ==================== Kotlin扩展函数 ====================
import com.lukouguoji.module_base.ktx.launchCollect             // 协程扩展(无Loading)
import com.lukouguoji.module_base.ktx.launchLoadingCollect      // 协程扩展(带Loading)
import com.lukouguoji.module_base.ktx.showToast                 // Toast扩展
import com.lukouguoji.module_base.ktx.toRequestBody             // Map转RequestBody ⚠️ 不是ext包!
import com.lukouguoji.module_base.ktx.verifyNullOrEmpty         // 非空验证
import com.lukouguoji.module_base.ktx.noNull                    // 空值处理
import com.lukouguoji.module_base.ktx.formatDate                // 日期格式化

// ==================== 事件总线 ====================
import com.lukouguoji.module_base.impl.FlowBus                  // FlowBus事件总线
import com.lukouguoji.module_base.impl.observe                  // FlowBus观察扩展 ⚠️ 必须单独导入!

// ==================== 接口 ====================
import com.lukouguoji.module_base.interfaces.IOnItemClickListener  // 列表项点击接口 ⚠️ 不是impl包!

// ==================== 路由 ====================
import com.lukouguoji.module_base.router.ARouterConstants       // 路由常量
import com.alibaba.android.arouter.facade.annotation.Route      // ARouter注解

// ==================== UI组件 ====================
import com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew      // 数据展示组件
import com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType        // 数据组件类型
import com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout     // 搜索组件
import com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType    // 搜索组件类型

Android标准库Import

// ==================== Activity & Fragment ====================
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment

// ==================== Lifecycle & ViewModel ====================
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope

// ==================== DataBinding ====================
import androidx.databinding.DataBindingUtil

// ==================== ViewPager2 ====================
import androidx.viewpager2.widget.ViewPager2

// ==================== Coroutines ====================
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

// ==================== View ====================
import android.view.View
import android.view.LayoutInflater
import android.view.ViewGroup

业务模块Bean Import示例

// 国际出港模块
import com.lukouguoji.module_base.bean.GjcMaWb           // 国际出港主单
import com.lukouguoji.module_base.bean.GjcHaWb           // 国际出港分单
import com.lukouguoji.module_base.bean.GjcStorageUse     // 库位使用

// 国内出港模块
import com.lukouguoji.module_base.bean.GncMaWb           // 国内出港主单

常见错误对照表

错误写法 正确写法 说明
com.lukouguoji.module_base.Constant com.lukouguoji.module_base.common.Constant Constant在common包下
com.lukouguoji.module_base.service.viewModel.BaseViewModel com.lukouguoji.module_base.base.BaseViewModel BaseViewModel在base包下
com.lukouguoji.module_base.ext.toRequestBody com.lukouguoji.module_base.ktx.toRequestBody 扩展函数在ktx包下
com.lukouguoji.module_base.impl.IOnItemClickListener com.lukouguoji.module_base.interfaces.IOnItemClickListener 接口在interfaces包下
com.lukouguoji.module_base.constant.DetailsPageType com.lukouguoji.module_base.common.DetailsPageType 枚举在common包下

快速查找正确Import路径

# 查找类的完整路径
find module_base/src/main/java -name "Constant.kt"
find module_base/src/main/java -name "BaseViewModel.kt"

# 查找函数定义位置
grep -r "fun.*toRequestBody" module_base/src/main/java --include="*.kt"
grep -r "class BaseViewModel" module_base/src/main/java --include="*.kt"

# 查找接口定义
find module_base/src/main/java -name "IOnItemClickListener.kt"

Activity/ViewModel/Fragment模板

ViewModel模板 (带正确import):

package com.lukouguoji.xxx.viewModel

import android.content.Intent
import androidx.lifecycle.MutableLiveData
import com.lukouguoji.module_base.base.BaseViewModel                    // ⚠️ 正确路径
import com.lukouguoji.module_base.common.Constant                       // ⚠️ 正确路径
import com.lukouguoji.module_base.http.net.NetApply
import com.lukouguoji.module_base.ktx.launchLoadingCollect
import com.lukouguoji.module_base.ktx.showToast
import com.lukouguoji.module_base.ktx.toRequestBody                     // ⚠️ 正确路径

class XxxViewModel : BaseViewModel() {
    val data = MutableLiveData<Any>()

    fun loadData() {
        val params = mapOf("key" to "value").toRequestBody()
        launchLoadingCollect({ NetApply.api.getXxx(params) }) {
            onSuccess = { data.value = it.data }
        }
    }
}

Activity模板 (带正确import):

package com.lukouguoji.xxx.activity

import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.alibaba.android.arouter.facade.annotation.Route
import com.lukouguoji.xxx.R
import com.lukouguoji.xxx.databinding.ActivityXxxBinding
import com.lukouguoji.xxx.viewModel.XxxViewModel
import com.lukouguoji.module_base.base.BaseBindingActivity
import com.lukouguoji.module_base.common.Constant                       // ⚠️ 正确路径
import com.lukouguoji.module_base.router.ARouterConstants

@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
    }
}

Fragment模板 (带正确import):

package com.lukouguoji.xxx.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import com.lukouguoji.xxx.R
import com.lukouguoji.xxx.databinding.FragmentXxxBinding
import com.lukouguoji.xxx.viewModel.XxxViewModel

class XxxFragment : Fragment() {
    private lateinit var binding: FragmentXxxBinding
    private lateinit var viewModel: XxxViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_xxx,
            container,
            false
        )
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = viewModel
        return binding.root
    }

    companion object {
        @JvmStatic
        fun newInstance(vm: XxxViewModel) =
            XxxFragment().apply { viewModel = vm }
    }
}

错误排查流程

  1. Import错误 (Unresolved reference) → 参考上方"常用Import路径速查表",使用正确包名
  2. 资源引用错误 → 检查drawable/color/string是否存在,主动创建缺失资源
  3. DataBinding错误 → 检查import包名、枚举值、是否导入View类
  4. suspend function错误 → 在viewModelScope.launch中调用
  5. 仍有问题./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"

标题栏统一规范

重要规则: 所有 Activity 布局必须使用统一的 title_tool_bar 组件,禁止手动编写 Toolbar。

正确做法

布局文件 (activity_xxx.xml):

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 标题栏 -->
    <include layout="@layout/title_tool_bar" />

    <!-- 其他内容 -->
    ...
</LinearLayout>

Activity 文件 (XxxActivity.kt):

override fun initOnCreate(savedInstanceState: Bundle?) {
    setBackArrow("页面标题")  // 自动设置标题和返回事件
    binding.viewModel = viewModel
    // 其他初始化...
}

错误做法

不要手动编写 Toolbar:

<!-- 错误:手动配置 Toolbar -->
<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="@color/colorPrimary">

    <LinearLayout android:id="@+id/tool_back">...</LinearLayout>
    <TextView android:id="@+id/title_name">...</TextView>
</androidx.appcompat.widget.Toolbar>

不要手动查找 tool_back 并设置点击事件:

// 错误:手动处理返回按钮
binding.root.findViewById<LinearLayout>(R.id.tool_back)?.setOnClickListener {
    finish()
}

title_tool_bar 工作原理

title_tool_bar.xml 包含三个关键 ID:

  • toolbar - BaseBindingActivity 自动查找
  • tool_back - 自动绑定 finish() 点击事件
  • title_name - 通过 setBackArrow() 设置文字

优点:

  • 统一视觉风格
  • 减少重复代码
  • 自动处理返回逻辑
  • 维护简单 (修改一处,全局生效)

参考示例

  • module_gjc/src/main/res/layout/activity_gjc_inspection.xml - 第21行
  • module_gjc/src/main/res/layout/activity_gjc_box_weighing_details.xml - 第21行
  • module_gjc/src/main/res/layout/activity_gjc_query_details.xml - 第19行

布局最佳实践参考

参考以下文件进行布局设计:

  • 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

开发原则

  • 资源引用必须存在 - 创建/修改布局前,确保drawable/color/string资源真实存在或主动创建
  • 标题栏统一使用 title_tool_bar - 禁止手动编写 Toolbar,必须使用 <include layout="@layout/title_tool_bar" />,Activity 中调用 setBackArrow("标题")
  • 必须设置 lifecycleOwner - Activity 中 binding.lifecycleOwner = this(BaseBindingActivity 已自动设置)
  • 新建Activity后必须在AndroidManifest.xml中注册
  • 优先使用项目现有基类和封装
  • 充分利用PadDataLayout和PadSearchLayout组件
  • 遵循统一命名规范
  • pageType用LiveData不用普通变量
  • XML中字符串拼接使用反引号,不访问LiveData的.value属性
  • 修改对象属性后重新赋值LiveData才能触发UI更新
  • FlowBus.emit()必须在协程中调用
  • 图片上传使用newName字段
  • RecyclerView手动更新adapter不用items属性
  • 在每个页面布局时,如有截图,务必尽可能还原图片上的页面设计,而不是推测假想。如有困难(图片看不清、不明白的地方)一律要询问,禁止自己想象。

Universal Development Guidelines

Code Quality Standards

  • Write clean, readable, and maintainable code
  • Follow consistent naming conventions across the project
  • Use meaningful variable and function names
  • Keep functions focused and single-purpose
  • Add comments for complex logic and business rules

Git Workflow

  • Use descriptive commit messages following conventional commits format
  • Create feature branches for new development
  • Keep commits atomic and focused on single changes
  • Use pull requests for code review before merging
  • Maintain a clean commit history

Documentation

  • Keep README.md files up to date
  • Document public APIs and interfaces
  • Include usage examples for complex features
  • Maintain inline code documentation
  • Update documentation when making changes

Testing Approach

  • Write tests for new features and bug fixes
  • Maintain good test coverage
  • Use descriptive test names that explain the expected behavior
  • Organize tests logically by feature or module
  • Run tests before committing changes

Security Best Practices

  • Never commit sensitive information (API keys, passwords, tokens)
  • Use environment variables for configuration
  • Validate input data and sanitize outputs
  • Follow principle of least privilege
  • Keep dependencies updated

Project Structure Guidelines

File Organization

  • Group related files in logical directories
  • Use consistent file and folder naming conventions
  • Separate source code from configuration files
  • Keep build artifacts out of version control
  • Organize assets and resources appropriately

Configuration Management

  • Use configuration files for environment-specific settings
  • Centralize configuration in dedicated files
  • Use environment variables for sensitive or environment-specific data
  • Document configuration options and their purposes
  • Provide example configuration files

Development Workflow

Before Starting Work

  1. Pull latest changes from main branch
  2. Create a new feature branch
  3. Review existing code and architecture
  4. Plan the implementation approach

During Development

  1. Make incremental commits with clear messages
  2. Run tests frequently to catch issues early
  3. Follow established coding standards
  4. Update documentation as needed

Before Submitting

  1. Run full test suite
  2. Check code quality and formatting
  3. Update documentation if necessary
  4. Create clear pull request description

Common Patterns

Error Handling

  • Use appropriate error handling mechanisms for the language
  • Provide meaningful error messages
  • Log errors appropriately for debugging
  • Handle edge cases gracefully
  • Don't expose sensitive information in error messages

Performance Considerations

  • Profile code for performance bottlenecks
  • Optimize database queries and API calls
  • Use caching where appropriate
  • Consider memory usage and resource management
  • Monitor and measure performance metrics

Code Reusability

  • Extract common functionality into reusable modules
  • Use dependency injection for better testability
  • Create utility functions for repeated operations
  • Design interfaces for extensibility
  • Follow DRY (Don't Repeat Yourself) principle

Review Checklist

Before marking any task as complete:

  • Code follows established conventions
  • Tests are written and passing
  • Documentation is updated
  • Security considerations are addressed
  • Performance impact is considered
  • Code is reviewed for maintainability