62 KiB
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启动协程 - 网络请求使用
launchCollect或launchLoadingCollect扩展函数 - 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- 查看应用日志
组件化开发模式
项目支持模块独立运行调试:
- 编辑
gradle.properties - 设置
isBuildModule=true(独立模式) 或false(集成模式) - 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
常见问题解决
依赖下载失败
- 检查网络连接
- 使用阿里云Maven镜像 (已在 build.gradle 中配置)
- 如需手动配置 Gradle,参考 README.md 中的依赖配置章节
模块编译错误
- 执行
./gradlew clean - 检查
gradle.properties中的isBuildModule设置 - 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步)
- 创建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) {
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步)
- 修改/创建Bean (添加ObservableBoolean选择状态)
- 定义API接口 (列表查询+统计查询+批量操作)
- 创建ViewHolder (处理选择图标点击)
- 创建ViewModel (继承BasePageViewModel)
- 创建Activity布局 (搜索区+列表+底部栏)
- 创建列表项布局 (使用completeSpace对齐)
- 创建Activity (绑定数据+观察全选状态)
- 在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" />
问题原因:
- 在布局文件中引用了项目中不存在的
drawable、color、string等资源 - 导致构建时资源合并失败,无法生成R文件
- 报错信息:
Resource compilation failed或AAPT: error: resource ... not found
正确做法:
- 使用已存在的资源 - 先检查资源是否存在
# 查找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
- 主动创建缺失的资源 - 如果不存在则创建
<!-- 创建 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>
- 使用项目现有资源 - 避免重复创建
常用资源列表:
- 背景:
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 }
}
}
错误排查流程
- Import错误 (Unresolved reference) → 参考上方"常用Import路径速查表",使用正确包名
- 资源引用错误 → 检查drawable/color/string是否存在,主动创建缺失资源
- DataBinding错误 → 检查import包名、枚举值、是否导入View类
- 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"
标题栏统一规范
重要规则: 所有 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.xmlmodule_gjc/src/main/res/layout/item_gjc_check_in_record.xmlmodule_gjc/src/main/res/layout/activity_gjc_box_weighing_details.xmlmodule_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
- Pull latest changes from main branch
- Create a new feature branch
- Review existing code and architecture
- Plan the implementation approach
During Development
- Make incremental commits with clear messages
- Run tests frequently to catch issues early
- Follow established coding standards
- Update documentation as needed
Before Submitting
- Run full test suite
- Check code quality and formatting
- Update documentation if necessary
- 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