Files
aerologic-app/CLAUDE.md

3012 lines
86 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
**AirLogistics (航空物流信息App)** - Android原生应用用于管理航空物流的全流程操作包括国内外货物进出港、仓储管理、车辆调度等核心业务。
- **包名**: com.lukouguoji.aerologic
- **当前版本**: 1.7.9 (versionCode 79)
- **开发语言**: Kotlin + Java混合
- **架构模式**: MVVM + 组件化
- **最低SDK**: Android 7.0 (API 24)
- **目标SDK**: Android 10 (API 30)
## 构建与运行
### 环境准备
1. **依赖下载问题解决**:
- 下载gradle-7.3.3-bin.zip: https://pan.baidu.com/s/18wsuGRlNxjMYbxLhBH9yeg (提取码: 1029)
- 打开 Settings -> Build, Execution, Deployment > Build Tools > Gradle
- 将下载的文件解压后替换到 "Gradle user home" 目录中
2. **配置IP地址**:
- 内网地址配置在 `module_base/src/main/res/values/strings.xml` 中的 `system_url_inner`
- 地磅地址: `weight_url`
- 运行时可通过SharedPreferences修改IP地址
### 构建命令
```bash
# 组件化开发模式切换
# 编辑 gradle.properties 中的 isBuildModule
# true: 模块可独立运行调试
# false: 模块作为library集成默认
# 构建Debug版本
./gradlew assembleDebug
# 构建Release版本已签名
./gradlew assembleRelease
# 安装到设备
./gradlew installDebug
# 清理构建
./gradlew clean
```
### 测试命令
```bash
# 运行单元测试
./gradlew test
# 运行特定模块的测试
./gradlew :module_base:test
./gradlew :app:test
# 运行UI测试
./gradlew connectedAndroidTest
```
## 核心架构
### 模块化结构
项目采用**组件化架构**,通过`isBuildModule`参数控制模块独立运行或作为library集成
- **app**: 应用壳层,整合所有业务模块,提供主界面框架
- **module_base**: 核心基础库(可独立运行),提供所有通用能力
- **module_gnc**: 国内出港业务(收运、复磅、装机等)
- **module_gnj**: 国内进港业务(卸机、提货、移库等)
- **module_gjc**: 国际出港业务板箱组装、ULD管理等
- **module_gjj**: 国际进港业务(舱单、理货、交接等)
- **module_hangban**: 航班查询管理
- **module_cargo**: 货物追踪查询
- **module_mit**: 监装监卸管理
- **module_p**: PDA专用功能
- **Printer**: 蓝牙打印模块佳博SDK
- **MPChartLib**: 定制图表库
### MVVM架构模式
所有业务页面遵循统一的MVVM模式
```
Activity/Fragment (View层)
↓ 继承
BaseBindingActivity<ViewDataBinding, ViewModel>
↓ 持有
ViewModel (业务逻辑层)
↓ 继承
BaseViewModel / BasePageViewModel
↓ 调用
Repository (数据层: Retrofit API)
```
**关键基类**:
- `BaseActivity`: 协程支持、Loading管理、扫码功能、键盘控制
- `BaseBindingActivity`: 提供DataBinding和ViewModel自动绑定
- `BaseViewModel`: 提供Loading管理、Lifecycle感知、Activity结果处理
- `BasePageViewModel`: 扩展分页列表功能、PageModel集成
- `CommonAdapter + BaseViewHolder`: 列表适配器统一封装
## 基类架构详解
### BaseActivity
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/BaseActivity.kt`
**核心能力**:
- **协程支持**: 实现`CoroutineScope`,自动管理协程生命周期
- **Loading管理**: 内置LoadingDialog支持30秒超时自动关闭
- **扫码功能**: 封装ZXing扫码自动处理相机权限申请
- **键盘控制**: 点击空白区域自动隐藏软键盘
- **Activity管理**: 通过ActivityCollector统一管理生命周期
- **字体锁定**: 强制字体大小不随系统设置变化
- **屏幕适配**: 集成AutoSize自动适配横屏1152dp × 720dp
**关键方法**:
```kotlin
// 显示/隐藏Loading
fun loading()
fun loadingCancel()
// 扫码功能
fun scanCode(requestCode: Int)
// 设置标题栏
open fun setBackArrow(title: String)
```
### BaseBindingActivity
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseBindingActivity.kt`
**设计特点**:
- 使用**ViewBinding/DataBinding**自动绑定视图
- 自动创建和管理ViewModel
- 生命周期自动绑定到lifecycleOwner
- 简化Activity样板代码
**标准开发模板**:
```kotlin
@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("页面标题")
// 绑定ViewModel到布局
binding.viewModel = viewModel
// 初始化其他UI组件
initRecyclerView()
initListeners()
}
}
```
### BaseViewModel
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseViewModel.kt`
**核心能力**:
- **Loading管理**: 提供`showLoading()`/`dismissLoading()`
- **Activity结果处理**: `onActivityResult()`回调
- **顶层Activity获取**: `getTopActivity()`
- **生命周期感知**: 继承自AndroidX ViewModel
**核心方法**:
```kotlin
abstract class BaseViewModel : ViewModel(), ILoading {
// Loading管理
override fun showLoading()
override fun dismissLoading()
// Activity结果处理
open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
// 获取顶层Activity
fun getTopActivity(): Activity
}
```
### BasePageViewModel
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BasePageViewModel.kt`
**核心特性**:
- 继承自`BaseViewModel`
- 集成**PageModel**自动处理分页逻辑
- 实现**IGetData**接口,统一`getData()`方法
- 实现**IOnItemClickListener**接口,处理列表点击
**标准使用模板**:
```kotlin
class XxxListViewModel : BasePageViewModel() {
// LiveData定义
val searchText = MutableLiveData<String>()
val dataList = MutableLiveData<List<XxxBean>>()
// 适配器配置(在布局中使用)
val itemLayoutId = R.layout.item_xxx
val itemViewHolder = XxxViewHolder::class.java
// 实现数据加载
override fun getData() {
val requestBody = mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"searchText" to searchText.value
).toRequestBody()
launchLoadingCollect({
NetApply.api.getXxxList(requestBody)
}) {
onSuccess = {
pageModel.handleListBean(it) // 自动处理分页数据
}
}
}
// 实现列表点击
override fun onItemClick(position: Int, type: Int) {
val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as XxxBean
// 跳转详情页
XxxDetailsActivity.start(getTopActivity(), bean.id)
}
}
```
### PageModel - 分页工具类
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/model/PageModel.kt`
**核心功能**:
```kotlin
class PageModel {
var page: Int = 1 // 当前页码
var limit: Int = 20 // 每页数量
var rv: RecyclerView? // 列表引用
// 绑定SmartRefreshLayout在Activity中调用
fun bindSmartRefreshLayout(
srl: SmartRefreshLayout,
rv: RecyclerView,
getData: IGetData,
lifecycleOwner: LifecycleOwner
)
// 处理列表数据在ViewModel中调用
fun handleListBean(listBean: BaseListBean<*>?)
}
```
**在Activity中使用**:
```kotlin
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("列表页面")
binding.viewModel = viewModel
// 绑定分页逻辑
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl, // SmartRefreshLayout
binding.recyclerView, // RecyclerView
viewModel, // 实现了IGetData的ViewModel
this // LifecycleOwner
)
// 绑定列表点击
binding.recyclerView.addOnItemClickListener(viewModel)
}
```
### CommonAdapter - 通用列表适配器
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/CommonAdapter.kt`
**核心方法**:
```kotlin
class CommonAdapter(
val context: Context,
private val layoutId: Int,
private val viewHolderClass: Class<out BaseViewHolder<*, out ViewDataBinding>>
) : RecyclerView.Adapter<BaseViewHolder<*, out ViewDataBinding>>()
// 数据管理
fun refresh(list: List<out Any>?) // 刷新数据(清空后重新加载)
fun loadMore(list: List<out Any>?) // 加载更多(追加数据)
fun addItem(item: Any?) // 添加单个项
fun removeItem(position: Int) // 删除项
fun getItem(position: Int): Any? // 获取项
// 事件监听
fun addOnItemClickListener(listener: IOnItemClickListener)
```
**使用示例**:
```kotlin
// 在Activity中创建适配器
val adapter = CommonAdapter(
context = this,
layoutId = R.layout.item_xxx,
viewHolderClass = XxxViewHolder::class.java
)
binding.recyclerView.adapter = adapter
// 刷新数据
adapter.refresh(dataList)
// 添加点击事件
adapter.addOnItemClickListener(viewModel)
```
### BaseViewHolder - ViewHolder基类
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/base/BaseViewHolder.kt`
**标准实现模板**:
```kotlin
class XxxViewHolder(view: View) :
BaseViewHolder<XxxBean, ItemXxxBinding>(view) {
override fun onBind(item: Any?, position: Int) {
// 获取数据Bean
val bean = getItemBean(item) ?: return
// 使用DataBinding绑定数据
binding.apply {
this.bean = bean
tvName.text = bean.name
tvDate.text = bean.date
// 设置整个条目可点击
notifyItemClick(position, root)
// 或设置特定按钮可点击
notifyItemClick(position, btnEdit)
notifyItemClick(position, btnDelete)
}
}
}
```
## 网络请求架构详解
### ServiceCreator - Retrofit创建器
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/user/ServiceCreator.kt`
**核心配置**:
```kotlin
object ServiceCreator {
// 动态获取IP地址可在运行时修改
var ipAddress = SharedPreferenceUtil.getString(Constant.Share.ipAddress)
.noNull(MyApplication.context.getString(R.string.system_url_inner))
fun createRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(ipAddress)
.client(httpClientBuilder(SelfLoginInterceptor).build())
.addConverterFactory(FastJsonConverterFactory())
.build()
}
}
```
**拦截器功能**:
- 自动添加`Authorization: Bearer {token}`
- 添加`timestamp`时间戳
- 统一处理401未登录自动跳转登录页
- 统一处理500错误并Toast提示
- 打印请求和响应日志Debug模式
### NetApply - API统一入口
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/net/NetApply.kt`
```kotlin
object NetApply {
private const val DEFAULT_TIMEOUT: Long = 30000L
var api: Api by Delegates.notNull()
// Gson实例支持日期格式化、空值处理
val gson: Gson = GsonBuilder()
.setDateFormat(DevFinal.TIME.yyyyMMddHHmmss_HYPHEN)
.registerTypeAdapterFactory(NullStringAdapterFactory())
.serializeNulls()
.create()
}
```
### Api接口定义
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt`
**标准接口模式**:
```kotlin
interface Api {
companion object {
var BASE_URL = ServiceCreator.ipAddress
}
// 通用POST请求
@POST
suspend fun simplePost(
@Url url: String,
@Body body: RequestBody = mapOf("" to "").toRequestBody()
): BaseResultBean<SimpleResultBean>
// 具体业务接口示例
// 列表查询(返回分页数据)
@POST("DomExpCheckIn/search")
suspend fun getGncShouYunList(@Body data: RequestBody): BaseListBean<GncShouYunBean>
// 详情查询(返回单个对象)
@POST("DomExpCheckIn/queryWbByNo")
suspend fun getGncShouYunDetails(@Query("wbNo") wbNo: String): BaseResultBean<GncShouYunBean>
// 新增/编辑/删除(返回成功标志)
@POST("DomExpCheckIn/save")
suspend fun saveGncShouYun(@Body data: RequestBody): BaseResultBean<SimpleResultBean>
// 文件上传
@Multipart
@POST("api/upload")
suspend fun uploadFile(@Part file: MultipartBody.Part): BaseResultBean<FileBean>
}
```
### RequestKtx - 请求扩展函数
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ktx/RequestKtx.kt`
#### (1) launchCollect - 无Loading请求
适用场景:后台刷新、非关键操作、不需要阻塞用户操作的请求
```kotlin
fun <T> Any.launchCollect(
block: suspend () -> T,
resultBuilder: ResultBuilder<T>.() -> Unit
)
```
**使用示例**:
```kotlin
launchCollect({
NetApply.api.getXxxList(params.toRequestBody())
}) {
onSuccess = { result ->
// 成功处理
dataList.value = result.data
}
onFailed = { code, message ->
// 失败处理默认已showToast
Log.e("TAG", "请求失败: $message")
}
onComplete = {
// 请求完成(无论成功失败)
isRefreshing.value = false
}
}
```
#### (2) launchLoadingCollect - 带Loading请求
适用场景:关键操作、提交表单、需要用户等待的请求
```kotlin
fun <T> ILoading.launchLoadingCollect(
block: suspend () -> T,
resultBuilder: ResultBuilder<T>.() -> Unit
)
```
**使用示例**:
```kotlin
launchLoadingCollect({
NetApply.api.saveXxx(params.toRequestBody())
}) {
onSuccess = { result ->
showToast("保存成功")
finish() // 关闭当前页面
}
onFailed = { code, message ->
// 失败处理已自动显示Toast和关闭Loading
}
}
```
#### (3) toRequestBody - 数据转换
```kotlin
fun Any.toRequestBody(removeEmptyOrNull: Boolean = false): RequestBody
```
**功能**:
- 将Map或Bean自动转换为JSON格式的RequestBody
- `removeEmptyOrNull = true`: 移除空字符串和null值字段
**使用示例**:
```kotlin
val params = mapOf(
"page" to 1,
"limit" to 20,
"waybillNo" to "", // 空字符串
"status" to null // null值
)
// 不移除空值
params.toRequestBody()
// 生成: {"page":1,"limit":20,"waybillNo":"","status":null}
// 移除空值
params.toRequestBody(removeEmptyOrNull = true)
// 生成: {"page":1,"limit":20}
```
### BaseResultBean - 统一返回格式
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/bean/BaseResultBean.kt`
```kotlin
open class BaseResultBean<T> {
var msg: String? = null // 消息提示
var status: String = "" // 状态码("1"=成功,其他=失败)
var data: T? = null // 实际数据
fun verifySuccess(): Boolean {
return status == "1" // 判断是否成功
}
}
```
**BaseListBean - 分页返回**:
```kotlin
class BaseListBean<T> {
var pages = 1 // 总页数
var total = 0 // 总数量
var list: ArrayList<T>? = null // 数据列表
}
```
## 统一UI组件规范
### 自定义控件
#### 1. PadSearchLayout - 搜索输入框组合控件
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt`
**功能**: 集成多种输入类型的搜索框组件
**支持的类型**:
- `SearchLayoutType.INPUT` - 文本输入
- `SearchLayoutType.INTEGER` - 数字输入
- `SearchLayoutType.SPINNER` - 下拉选择
- `SearchLayoutType.DATE` - 日期选择
**核心属性**:
```kotlin
type: SearchLayoutType // 控件类型
value: String // 绑定的值(支持双向绑定@={}
hint: String // 提示文字
list: List<KeyValue> // 下拉选项列表
required: Boolean // 是否必填(显示*号)
icon: Int // 右侧图标资源ID
enable: Boolean // 是否可编辑
refreshCallBack: () -> Unit // 刷新回调
onIconClickListener: (View) -> Unit // 图标点击回调
```
**使用示例**:
```xml
<!-- 文本输入+扫码 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.waybillNo}"
hint="@{`请输入运单号`}"
required="@{true}"
icon="@{@mipmap/scan_code}"
setOnIconClickListener="@{(v)-> viewModel.scanWaybill()}" />
<!-- 日期选择 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
type="@{SearchLayoutType.DATE}"
value="@={viewModel.date}"
hint="@{`选择日期`}"
icon="@{@mipmap/calendar}" />
<!-- 下拉选择 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
type="@{SearchLayoutType.SPINNER}"
list="@{viewModel.statusList}"
value="@={viewModel.status}"
hint="@{`选择状态`}" />
```
#### 2. PadDataLayout - 数据展示/编辑组合控件
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/data/layout/PadDataLayout.kt`
**功能**: 带标题的数据展示/编辑控件(标题+输入框/下拉/日期)
**支持的类型**:
- `DataLayoutType.INPUT` - 文本输入
- `DataLayoutType.SPINNER` - 下拉选择
- `DataLayoutType.DATE` - 日期选择
**核心属性**:
```kotlin
type: DataLayoutType // 控件类型
title: String // 左侧标题
titleLength: Int // 标题长度(用于对齐,单位:汉字个数)
value: String // 绑定的值(支持双向绑定@={}
hint: String // 提示文字
list: List<KeyValue> // 下拉选项列表
required: Boolean // 是否必填(显示*号)
icon: Int // 右侧图标
enable: Boolean // 是否可编辑
inputHeight: Int // 多行输入高度
maxLength: Int // 最大长度
```
**使用示例**:
```xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 文本输入 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
required="@{true}"
title='@{"运单号:"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.bean.waybillNo}'
maxLength="@{11}" />
<!-- 下拉选择 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
list="@{viewModel.statusList}"
required="@{true}"
title='@{"状态:"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.bean.status}' />
<!-- 日期选择 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
title='@{"日期:"}'
titleLength="@{5}"
type="@{DataLayoutType.DATE}"
value='@={viewModel.bean.date}' />
</LinearLayout>
<!-- 多行文本输入 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
title='@{"备注:"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.bean.remark}'
inputHeight="@{100}" />
```
#### 3. StatusView - 状态栏占位View
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/StatusView.kt`
**功能**: 自动适配状态栏高度的占位View用于沉浸式状态栏
**使用方式**:
```xml
<com.lukouguoji.module_base.ui.weight.StatusView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary" />
```
### 通用样式定义
#### 文本样式 (module_base/res/values/styles.xml)
```xml
<!-- 列表项标签(灰色) -->
<style name="tv_item_label">
<item name="android:textColor">@color/weak_grey</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
</style>
<!-- 列表项值(蓝色) -->
<style name="tv_item_value">
<item name="android:textColor">@color/colorPrimary</item>
<item name="android:textSize">15sp</item>
<item name="android:layout_marginLeft">10dp</item>
<item name="android:singleLine">true</item>
</style>
<!-- 列表项操作按钮(白色文字) -->
<style name="tv_item_action">
<item name="android:textColor">@color/white</item>
<item name="android:textSize">16sp</item>
<item name="android:gravity">center</item>
<item name="android:paddingStart">10dp</item>
<item name="android:paddingEnd">10dp</item>
</style>
<!-- 详情页标签 -->
<style name="tv_manifest_details_label">
<item name="android:layout_width">90dp</item>
<item name="android:textColor">#999999</item>
<item name="android:textSize">16sp</item>
</style>
<!-- 详情页值 -->
<style name="tv_manifest_details_value">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">40dp</item>
<item name="android:background">@color/white</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textColor">#333333</item>
<item name="android:textSize">16sp</item>
</style>
```
#### 按钮样式
```xml
<!-- 底部按钮(标准大小) -->
<style name="tv_bottom_btn">
<item name="android:layout_width">100dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:background">@drawable/bg_btn_bottom</item>
<item name="android:textColor">@color/white</item>
<item name="android:textSize">18sp</item>
<item name="android:gravity">center</item>
</style>
<!-- 底部按钮(大尺寸) -->
<style name="tv_bottom_btn_lg">
<item name="android:layout_width">150dp</item>
<item name="android:layout_height">50dp</item>
<item name="android:background">@drawable/bg_btn_bottom</item>
<item name="android:textColor">@color/white</item>
<item name="android:textSize">18sp</item>
</style>
<!-- 信息项按钮 -->
<style name="info_item_button">
<item name="android:layout_width">120dp</item>
<item name="android:layout_height">60dp</item>
<item name="android:background">@drawable/submit_shape</item>
<item name="android:textColor">@color/white</item>
<item name="android:gravity">center</item>
</style>
```
#### 布局样式
```xml
<!-- 搜索行布局 -->
<style name="ll_search">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_weight">1</item>
<item name="android:background">@drawable/bg_search_row</item>
<item name="android:orientation">horizontal</item>
</style>
<!-- 信息项容器 -->
<style name="info_item_content_parent">
<item name="android:layout_width">0dp</item>
<item name="android:layout_weight">12</item>
<item name="android:background">@drawable/bg_data_layout</item>
</style>
<!-- 必填标记(红色*号) -->
<style name="info_item_must">
<item name="android:layout_width">30dp</item>
<item name="android:textColor">@color/red</item>
<item name="android:gravity">center|start</item>
</style>
```
### 颜色规范 (module_base/res/values/colors.xml)
```xml
<!-- 主色调 -->
<color name="colorPrimary">#FF1C8CF5</color> <!-- 蓝色主色 -->
<color name="app_them">#0A80FC</color>
<!-- 文本颜色 -->
<color name="textValue">#1E1E1E</color> <!-- 深黑 -->
<color name="text_normal">#333333</color> <!-- 正常文本 -->
<color name="text_gray">#666666</color> <!-- 灰色文本 -->
<color name="text_gray_l">#999999</color> <!-- 浅灰文本 -->
<color name="weak_grey">#FF999999</color> <!-- 弱灰色 -->
<color name="text_blue">#3CB5F3</color> <!-- 蓝色文本 -->
<!-- 基础颜色 -->
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="red">#d9001b</color>
<!-- 背景颜色 -->
<color name="list_bg">#FFEDEDED</color> <!-- 列表背景 -->
<color name="backgroud_gray">#FFEDEDED</color> <!-- 页面背景灰色 -->
<color name="home_area_bg">#FFF6FBFF</color> <!-- 首页区域背景 -->
<color name="disable_grey">#FFEDEDED</color> <!-- 禁用灰色 -->
<color name="data_layout_disable_grey">#F1F1F1</color> <!-- 数据布局禁用色 -->
<color name="color_bottom_layout">#5c6890</color> <!-- 底部布局颜色 -->
```
### Drawable背景规范
#### 输入框背景
```xml
<!-- bg_search_layout.xml - 搜索框背景(选择器) -->
<selector>
<item android:drawable="@drawable/bg_search_layout_s" android:state_enabled="true" />
<item android:drawable="@drawable/bg_search_layout_n" android:state_enabled="false" />
</selector>
<!-- bg_data_layout.xml - 数据布局背景(选择器) -->
<selector>
<item android:drawable="@drawable/bg_data_layout_s" android:state_enabled="true" />
<item android:drawable="@drawable/bg_data_layout_n" android:state_enabled="false" />
</selector>
<!-- 启用状态:白色 + 8dp圆角 -->
<!-- 禁用状态:#E0E0E0灰色 + 8dp圆角 -->
```
**使用说明**:
- `bg_search_layout`: 搜索区域输入框专用8dp圆角
- `bg_data_layout`: 数据展示区域输入框专用4dp圆角
- `bg_input`: 通用输入框背景(白色+灰色边框+8dp圆角
#### 按钮背景
```xml
<!-- bg_btn_bottom.xml - 底部按钮背景(选择器) -->
<selector>
<item android:drawable="@drawable/bg_primary_radius_4" android:state_enabled="true" />
<item android:drawable="@drawable/bg_gray_radius_4" android:state_enabled="false" />
</selector>
<!-- 启用:蓝色主色 + 4dp圆角 -->
<!-- 禁用:灰色 + 4dp圆角 -->
```
**常用按钮背景**:
- `@drawable/bg_btn_bottom` - 底部按钮(蓝色/灰色选择器)
- `@drawable/submit_shape` - 提交按钮(蓝色+5dp圆角
- `@drawable/bg_red_radius_5` - 红色按钮5dp圆角
- `@drawable/bg_primary_radius_4` - 主色按钮4dp圆角
#### 通用形状背景
- `@drawable/bg_white_radius_8` - 白色圆角背景8dp
- `@drawable/bg_white_circle` - 白色圆形背景
- `@drawable/bg_dialog_round` - 对话框圆角背景
- `@drawable/bg_confirm_dialog` - 确认对话框背景
### 通用布局组件
#### 标题栏 (title_tool_bar.xml)
**使用方式**:
```xml
<include layout="@layout/title_tool_bar" />
```
在Activity中设置标题
```kotlin
setBackArrow("页面标题")
```
## DataBinding适配器
项目中提供了丰富的BindingAdapter简化DataBinding使用。
### 图片加载 (BindingAdapter.kt)
```xml
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
loadImage="@{imageUrl}"
loadError="@{@mipmap/default_image}"
loadPlaceholder="@{@mipmap/loading}"
loadCircle="@{true}"
loadRadius="@{8}" />
```
**支持的属性**:
- `loadImage`: 图片地址URL或本地路径
- `loadError`: 错误图片
- `loadPlaceholder`: 占位图
- `loadCircle`: 是否圆形true/false
- `loadRadius`: 圆角dp
- `loadWidth`/`loadHeight`: 指定宽高
### View可见性 (ViewAdapter.kt)
```xml
<!-- 支持Boolean -->
<TextView
visible="@{viewModel.showText}"
android:text="文本内容" />
<!-- 支持Int0隐藏非0显示 -->
<TextView
visible="@{viewModel.count}"
android:text="@{String.valueOf(viewModel.count)}" />
<!-- 支持String空隐藏非空显示 -->
<TextView
visible="@{viewModel.message}"
android:text="@{viewModel.message}" />
```
### TextView文本对齐 (TextViewAdapter.kt)
```xml
<!-- 自动对齐标题 -->
<LinearLayout android:orientation="vertical">
<TextView
android:text="运单号"
completeSpace="@{5}" /> <!-- 按5个汉字宽度对齐 -->
<TextView
android:text="状态"
completeSpace="@{5}" />
<TextView
android:text="日期"
completeSpace="@{5}" />
</LinearLayout>
```
### Spinner下拉框 (SpinnerAdapter.kt)
```xml
<Spinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
items="@{viewModel.statusList}"
hint="@{`请选择状态`}"
onSelected="@{(position)-> viewModel.onStatusSelected(position)}" />
```
### Shape动态背景 (ShapeBindingAdapter.kt)
```xml
<!-- 纯色背景 + 圆角 + 边框 -->
<View
android:layout_width="match_parent"
android:layout_height="50dp"
shape_radius="@{8}"
shape_bg_color="@{`#FF0000`}"
shape_border_width="@{1}"
shape_border_color="@{`#000000`}" />
<!-- 渐变背景 -->
<View
android:layout_width="match_parent"
android:layout_height="50dp"
shape_gradient_start_color="@{`#FF0000`}"
shape_gradient_end_color="@{`#00FF00`}"
shape_gradient_angle="@{0}" />
<!-- 虚线边框 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
shape_border_width="@{1}"
shape_border_color="@{`#999999`}"
shape_dash_width="@{4}"
shape_dash_gap="@{2}" />
```
### EditText扩展 (EditTextKtx.kt)
```xml
<!-- 自动转大写 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
setTextAllCaps="@{true}"
android:hint="输入大写字母" />
<!-- 运单号输入模式限制11位数字 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
setInputWaybill="@{true}"
android:hint="请输入11位运单号" />
```
## Kotlin扩展函数
### Toast扩展 (ToastKtx.kt)
```kotlin
// 显示Toast默认长时间
showToast("操作成功")
// 短时间Toast
showToast("提示信息", isShort = true)
```
### Dialog扩展 (DialogKtx.kt)
```kotlin
// 确认对话框
showConfirmDialog(
message = "确定要删除吗?",
title = "提示",
confirmText = "确定",
cancelText = "取消"
) {
// 确认操作
deleteItem()
}
```
### 字符串处理 (CommonKtx.kt)
```kotlin
// 空处理
val text = nullableString.noNull("默认值")
val text2 = nullableString.noNull() // 空字符串
// 验证非空
if (waybillNo.verifyNullOrEmpty("请输入运单号")) {
return // 验证失败会自动Toast
}
```
### 布尔转换
```kotlin
// Boolean -> Int
val intValue = booleanValue.toInt() // true=1, false=0
// String -> Boolean
val bool = "1".toBoolean() // "1"=true, 其他=false
// Int -> Boolean
val bool = 1.toBoolean() // 1=true, 其他=false
```
### 日期格式化
```kotlin
// 格式化日期
val dateStr = Date().formatDate() // "2025-11-12"
val dateStr2 = Date().formatDate("yyyy/MM/dd") // "2025/11/12"
// 格式化日期时间
val dateTimeStr = Date().formatDateTime() // "2025-11-12 14:30:00"
```
### JSON处理
```kotlin
// 对象转JSON
val json = userBean.toJson()
val jsonFormatted = userBean.toJson(format = true) // 格式化输出
// 对象转Map
val map = userBean.toMap()
// Map去除空值
val cleanMap = map.trim(deep = true)
```
### 权限申请
```kotlin
// 申请单个权限
permission(Manifest.permission.CAMERA) {
// 获取权限后操作
openCamera()
}
// 申请多个权限
permission(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) {
// 所有权限获取后操作
takePicture()
}
// 处理拒绝情况
permission(
Manifest.permission.CAMERA,
onDenied = {
showToast("需要相机权限才能扫码")
},
onGranted = {
openCamera()
}
)
```
## 路由系统
### ARouter路由注册
**路由常量定义**: `module_base/src/main/java/com/lukouguoji/module_base/router/ARouterConstants.kt`
```kotlin
object ARouterConstants {
// 国内出港
const val ACTIVITY_URL_GNC_SHOUYUN_LIST = "/gnc/GncShouYunListActivity"
const val ACTIVITY_URL_GNC_STASH_DETAILS = "/gnc/GncStashDetailsActivity"
// 国际进港
const val ACTIVITY_URL_GJJ_TALLY_LIST = "/gjj/GjjTallyListActivity"
const val ACTIVITY_URL_GJJ_GOODS_LIST = "/gjj/GjjGoodsListActivity"
// 通用功能
const val ACTIVITY_URL_SCAN = "/common/ScanActivity"
}
```
### 使用方式
```kotlin
// 1. 在Activity上添加注解
@Route(path = ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
class GncShouYunListActivity : BaseBindingActivity<...>() {
// ...
}
// 2. 跳转页面
ARouter.getInstance()
.build(ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
.withString("waybillNo", "12345678901")
.withInt("type", 1)
.navigation()
// 3. 接收参数
@Autowired(name = "waybillNo")
@JvmField
var waybillNo: String? = null
@Autowired(name = "type")
@JvmField
var type: Int = 0
override fun initOnCreate(savedInstanceState: Bundle?) {
ARouter.getInstance().inject(this) // 注入参数
// 使用参数
Log.d("TAG", "waybillNo: $waybillNo, type: $type")
}
```
## 事件通信
### FlowBus事件总线
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/impl/FlowBus.kt`
```kotlin
// 定义事件常量在ConstantEvent.kt中
object ConstantEvent {
const val EVENT_REFRESH_LIST = "event_refresh_list"
const val EVENT_UPDATE_STATUS = "event_update_status"
}
// 发送事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh")
// 接收事件在ViewModel中
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).observe(this) { data ->
// 处理事件
loadData()
}
// 粘性事件(先发送后接收也能收到)
FlowBus.withSticky<UserBean>("user_info").emit(userBean)
FlowBus.withSticky<UserBean>("user_info").observe(this) { user ->
// 处理用户信息
}
```
## 开发规范
### 代码组织规范
#### 目录结构标准
```
module_xxx/
└── src/main/java/com/lukouguoji/xxx/
├── page/ # 页面目录(推荐新模式)
│ └── feature/ # 功能模块
│ ├── list/ # 列表页
│ │ ├── XxxListActivity.kt
│ │ ├── XxxListViewModel.kt
│ │ └── XxxListViewHolder.kt
│ ├── details/ # 详情页
│ │ ├── XxxDetailsActivity.kt
│ │ └── XxxDetailsViewModel.kt
│ └── add/ # 新增/编辑页
│ ├── XxxAddActivity.kt
│ └── XxxAddViewModel.kt
├── activity/ # Activity目录旧模式
├── viewModel/ # ViewModel目录旧模式
└── holder/ # ViewHolder目录
```
#### 命名规范
**Activity命名**:
- 列表页: `XxxListActivity`
- 详情页: `XxxDetailsActivity`
- 新增页: `XxxAddActivity`
- 编辑页: `XxxEditActivity`
**ViewModel命名**:
- 列表: `XxxListViewModel`
- 详情: `XxxDetailsViewModel`
- 新增: `XxxAddViewModel`
**ViewHolder命名**:
- 格式: `XxxViewHolder``XxxListViewHolder`
**Layout命名**:
- Activity: `activity_xxx_list.xml`
- Fragment: `fragment_xxx.xml`
- Item: `item_xxx.xml`
### Bean定义规范
**文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/bean/`
```kotlin
// 使用Kotlin data class
data class XxxBean(
var id: String = "",
var name: String = "",
var date: String = "",
var status: String = ""
)
// 如果需要DataBinding双向绑定使用BaseObservable
class XxxBean : BaseObservable() {
@Bindable
var name: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
@Bindable
var weight: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.weight)
}
// 使用ObservableBoolean/ObservableInt等
val checked = ObservableBoolean(false)
val count = ObservableInt(0)
// 计算属性
val displayName: String
get() = "【$name】"
}
```
## 实际业务开发示例
### 列表页完整示例
#### Activity层
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_GNC_SHOUYUN_LIST)
class GncShouYunListActivity :
BaseBindingActivity<ActivityGncShouyunListBinding, GncShouYunListViewModel>() {
override fun layoutId() = R.layout.activity_gnc_shouyun_list
override fun viewModelClass() = GncShouYunListViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("国内出港收运记录")
// 绑定ViewModel
binding.viewModel = viewModel
// 绑定分页逻辑
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl,
binding.recyclerView,
viewModel,
this
)
// 绑定列表点击
binding.recyclerView.addOnItemClickListener(viewModel)
// 初始加载
viewModel.refresh()
}
}
```
#### ViewModel层
```kotlin
class GncShouYunListViewModel : BasePageViewModel() {
// LiveData定义
val date = MutableLiveData(DateUtils.getCurrentTime().formatDate())
val dest = MutableLiveData("")
val waybillNo = MutableLiveData("")
val count = MutableLiveData("0")
// 适配器配置(在布局中使用)
val itemLayoutId = R.layout.item_gnc_shouyun
val itemViewHolder = GncShouYunListViewHolder::class.java
// 数据加载
override fun getData() {
val requestBody = mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"opDate" to date.value,
"dest" to dest.value,
"wbNo" to waybillNo.value
).toRequestBody()
launchLoadingCollect({
NetApply.api.getGncShouYunList(requestBody)
}) {
onSuccess = {
pageModel.handleListBean(it)
count.value = it.total.toString()
}
}
}
// 条目点击
override fun onItemClick(position: Int, type: Int) {
val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as GncShouYunBean
GncStashDetailsActivity.start(getTopActivity(), bean.whId)
}
// 搜索
fun search() {
refresh()
}
// 扫码
fun scanWaybill() {
ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL)
}
// 处理扫码结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && data != null) {
when (requestCode) {
Constant.RequestCode.WAYBILL -> {
waybillNo.value = data.getStringExtra(Constant.Result.CODED_CONTENT)
search()
}
}
}
}
}
```
#### ViewHolder层
```kotlin
class GncShouYunListViewHolder(view: View) :
BaseViewHolder<GncShouYunBean, ItemGncShouyunBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val bean = getItemBean(item) ?: return
// DataBinding绑定数据
binding.bean = bean
// 整个条目可点击
notifyItemClick(position, binding.shouyunItem)
// 特定控件点击
binding.ivIcon.setOnClickListener {
bean.checked.set(!bean.checked.get())
}
}
}
```
#### Layout层
```xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.lukouguoji.module_base.ui.weight.search.layout.SearchLayoutType" />
<variable name="viewModel" type="com.lukouguoji.gnc.page.shouyun.list.GncShouYunListViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroud_gray"
android:orientation="vertical">
<!-- 标题栏 -->
<include layout="@layout/title_tool_bar" />
<!-- 搜索区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/colorPrimary"
android:orientation="horizontal"
android:padding="10dp">
<!-- 日期 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
hint="@{`选择日期`}"
icon="@{@mipmap/calendar}"
type="@{SearchLayoutType.DATE}"
value="@={viewModel.date}" />
<Space
android:layout_width="10dp"
android:layout_height="match_parent" />
<!-- 运单号 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
hint="@{`请输入运单号`}"
icon="@{@mipmap/scan_code}"
setOnIconClickListener="@{(v)-> viewModel.scanWaybill()}"
type="@{SearchLayoutType.INPUT}"
value="@={viewModel.waybillNo}" />
<Space
android:layout_width="10dp"
android:layout_height="match_parent" />
<!-- 搜索按钮 -->
<TextView
android:layout_width="80dp"
android:layout_height="match_parent"
android:background="@drawable/submit_shape"
android:gravity="center"
android:onClick="@{()-> viewModel.search()}"
android:text="搜索"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<!-- 统计信息 -->
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingStart="15dp"
android:text="@{`共 ` + viewModel.count + ` 条记录`}"
android:textColor="@color/text_gray"
android:textSize="14sp" />
<!-- 列表 -->
<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/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</layout>
```
### 详情页完整示例
#### Activity层
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_GNC_STASH_DETAILS)
class GncStashDetailsActivity :
BaseBindingActivity<ActivityGncStashDetailsBinding, GncStashDetailsViewModel>() {
override fun layoutId() = R.layout.activity_gnc_stash_details
override fun viewModelClass() = GncStashDetailsViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("国内出港运单详情")
// 初始化ViewModel传递参数
viewModel.initOnCreated(intent)
// 绑定ViewModel
binding.viewModel = viewModel
}
companion object {
/**
* 静态启动方法(推荐方式)
*/
@JvmStatic
fun start(context: Context, whId: String) {
val starter = Intent(context, GncStashDetailsActivity::class.java)
.putExtra(Constant.Key.ID, whId)
context.startActivity(starter)
}
}
}
```
#### ViewModel层
```kotlin
class GncStashDetailsViewModel : BaseViewModel() {
var id = ""
val dataBean = MutableLiveData<GncStashBean>()
fun initOnCreated(intent: Intent) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
getData()
}
private fun getData() {
launchLoadingCollect({
NetApply.api.getGncStashDetails(id)
}) {
onSuccess = {
dataBean.value = it.data ?: GncStashBean()
}
}
}
// 打印标签
fun printLabel() {
dataBean.value?.let { bean ->
PrinterModel.printWaybillLabel(bean)
}
}
}
```
#### Layout层
```xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable name="viewModel" type="com.lukouguoji.gnc.page.stash.details.GncStashDetailsViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroud_gray"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
<!-- 基本信息 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="基本信息"
android:textColor="@color/text_normal"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/bg_white_radius_8"
android:orientation="vertical"
android:padding="15dp">
<!-- 运单号 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/tv_manifest_details_label"
android:text="运单号:" />
<TextView
style="@style/tv_manifest_details_value"
android:text="@{viewModel.dataBean.waybillNo}" />
</LinearLayout>
<!-- 分割线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/list_bg" />
<!-- 件数 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/tv_manifest_details_label"
android:text="件数:" />
<TextView
style="@style/tv_manifest_details_value"
android:text="@{viewModel.dataBean.pieces}" />
</LinearLayout>
<!-- 重量 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/tv_manifest_details_label"
android:text="重量:" />
<TextView
style="@style/tv_manifest_details_value"
android:text="@{viewModel.dataBean.weight + `kg`}" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 底部按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/white"
android:gravity="center"
android:orientation="horizontal">
<TextView
style="@style/tv_bottom_btn_lg"
android:onClick="@{()-> viewModel.printLabel()}"
android:text="打印标签" />
</LinearLayout>
</LinearLayout>
</layout>
```
### 新增/编辑页完整示例
#### Activity层
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_CAR_ADD)
class CarAddActivity :
BaseBindingActivity<ActivityCarAddBinding, CarAddViewModel>() {
override fun layoutId() = R.layout.activity_car_add
override fun viewModelClass() = CarAddViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
viewModel.initOnCreated(intent)
// 根据页面类型设置标题
when (viewModel.pageType) {
DetailsPageType.Add -> setBackArrow("新增车辆")
DetailsPageType.Edit -> setBackArrow("编辑车辆")
DetailsPageType.Details -> setBackArrow("车辆详情")
}
binding.viewModel = viewModel
}
companion object {
@JvmStatic
fun startForAdd(context: Context) {
val starter = Intent(context, CarAddActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
context.startActivity(starter)
}
@JvmStatic
fun startForEdit(context: Context, carId: String) {
val starter = Intent(context, CarAddActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Edit.name)
.putExtra(Constant.Key.ID, carId)
context.startActivity(starter)
}
@JvmStatic
fun startForDetails(context: Context, carId: String) {
val starter = Intent(context, CarAddActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Details.name)
.putExtra(Constant.Key.ID, carId)
context.startActivity(starter)
}
}
}
```
#### ViewModel层
```kotlin
class CarAddViewModel : BaseViewModel() {
var pageType: DetailsPageType = DetailsPageType.Add
var carId = ""
val carBean = MutableLiveData(CarBean())
val statusList = MutableLiveData<List<KeyValue>>()
fun initOnCreated(intent: Intent) {
// 获取页面类型
pageType = DetailsPageType.valueOf(
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
)
// 加载下拉列表
loadStatusList()
// 如果是编辑或详情,加载数据
if (pageType != DetailsPageType.Add) {
carId = intent.getStringExtra(Constant.Key.ID) ?: ""
loadData()
}
}
private fun loadStatusList() {
statusList.value = listOf(
KeyValue("1", "正常"),
KeyValue("2", "维修中"),
KeyValue("3", "停用")
)
}
private fun loadData() {
launchLoadingCollect({
NetApply.api.getCarDetails(carId)
}) {
onSuccess = {
carBean.value = it.data ?: CarBean()
}
}
}
fun submit() {
val bean = carBean.value ?: return
// 验证
if (bean.carId.verifyNullOrEmpty("请输入车辆编号")) return
if (bean.status.verifyNullOrEmpty("请选择状态")) return
// 提交
launchLoadingCollect({
val params = mapOf(
"id" to carId,
"carId" to bean.carId,
"status" to bean.status,
"remark" to bean.remark
).toRequestBody(removeEmptyOrNull = true)
NetApply.api.saveCar(params)
}) {
onSuccess = {
showToast(if (pageType == DetailsPageType.Add) "新增成功" else "编辑成功")
// 发送刷新事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_CAR_LIST).emit("refresh")
// 关闭页面
getTopActivity().finish()
}
}
}
}
```
#### Layout层
```xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType" />
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
<variable name="viewModel" type="com.lukouguoji.car.page.add.CarAddViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroud_gray"
android:orientation="vertical">
<include layout="@layout/title_tool_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp">
<!-- 第一行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 车辆编号 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
required="@{viewModel.pageType != DetailsPageType.Details}"
title='@{"车辆编号:"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.carBean.carId}' />
<Space
android:layout_width="10dp"
android:layout_height="match_parent" />
<!-- 状态 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
enable="@{viewModel.pageType != DetailsPageType.Details}"
list="@{viewModel.statusList}"
required="@{viewModel.pageType != DetailsPageType.Details}"
title='@{"状态:"}'
titleLength="@{5}"
type="@{DataLayoutType.SPINNER}"
value='@={viewModel.carBean.status}' />
</LinearLayout>
<!-- 备注 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
enable="@{viewModel.pageType != DetailsPageType.Details}"
inputHeight="@{100}"
title='@{"备注:"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.carBean.remark}' />
</LinearLayout>
</ScrollView>
<!-- 底部按钮(详情页不显示) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/white"
android:gravity="center"
android:orientation="horizontal"
visible="@{viewModel.pageType != DetailsPageType.Details}">
<TextView
style="@style/tv_bottom_btn_lg"
android:onClick="@{()-> viewModel.submit()}"
android:text="@{viewModel.pageType == DetailsPageType.Add ? `提交` : `保存`}" />
</LinearLayout>
</LinearLayout>
</layout>
```
## 开发检查清单
### 列表页开发清单7步
**步骤1: 创建Bean**
```kotlin
// 位置: module_base/src/main/java/com/lukouguoji/module_base/bean/
data class XxxBean(
var id: String = "",
var name: String = "",
var date: String = "",
var status: String = ""
)
```
**步骤2: 在Api中添加接口**
```kotlin
// 位置: module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt
@POST("api/xxx/list")
suspend fun getXxxList(@Body data: RequestBody): BaseListBean<XxxBean>
```
**步骤3: 创建ViewHolder**
```kotlin
// 位置: module_xxx/src/main/java/.../holder/
class XxxViewHolder(view: View) :
BaseViewHolder<XxxBean, ItemXxxBinding>(view) {
override fun onBind(item: Any?, position: Int) {
val bean = getItemBean(item) ?: return
binding.bean = bean
notifyItemClick(position, binding.root)
}
}
```
**步骤4: 创建ViewModel**
```kotlin
// 继承BasePageViewModel
class XxxListViewModel : BasePageViewModel() {
val searchText = MutableLiveData<String>()
val itemLayoutId = R.layout.item_xxx
val itemViewHolder = XxxViewHolder::class.java
override fun getData() {
val requestBody = mapOf(
"page" to pageModel.page,
"limit" to pageModel.limit,
"searchText" to searchText.value
).toRequestBody()
launchLoadingCollect({
NetApply.api.getXxxList(requestBody)
}) {
onSuccess = {
pageModel.handleListBean(it)
}
}
}
override fun onItemClick(position: Int, type: Int) {
val bean = pageModel.rv!!.commonAdapter()!!.getItem(position) as XxxBean
// 跳转详情页
}
}
```
**步骤5: 创建Activity**
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_XXX_LIST)
class XxxListActivity :
BaseBindingActivity<ActivityXxxListBinding, XxxListViewModel>() {
override fun layoutId() = R.layout.activity_xxx_list
override fun viewModelClass() = XxxListViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("列表页面")
binding.viewModel = viewModel
viewModel.pageModel.bindSmartRefreshLayout(
binding.srl,
binding.recyclerView,
viewModel,
this
)
binding.recyclerView.addOnItemClickListener(viewModel)
}
}
```
**步骤6: 创建Layout**
```xml
<!-- activity_xxx_list.xml -->
<layout>
<data>
<variable name="viewModel" type="...XxxListViewModel" />
</data>
<LinearLayout>
<include layout="@layout/title_tool_bar" />
<!-- 搜索区域 -->
<com.lukouguoji.module_base.ui.weight.search.layout.PadSearchLayout ... />
<!-- 列表 -->
<com.scwang.smart.refresh.layout.SmartRefreshLayout android:id="@+id/srl">
<androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
</layout>
<!-- item_xxx.xml -->
<layout>
<data>
<variable name="bean" type="...XxxBean" />
</data>
<LinearLayout>
<!-- 列表项内容 -->
</LinearLayout>
</layout>
```
**步骤7: 注册路由**
```kotlin
// 位置: module_base/.../router/ARouterConstants.kt
const val ACTIVITY_URL_XXX_LIST = "/xxx/XxxListActivity"
```
### 详情页开发清单4步
**步骤1: 在Api中添加接口**
```kotlin
@POST("api/xxx/details")
suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean<XxxBean>
```
**步骤2: 创建ViewModel**
```kotlin
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()
}
}
}
}
```
**步骤3: 创建Activity含静态start方法**
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_XXX_DETAILS)
class XxxDetailsActivity :
BaseBindingActivity<ActivityXxxDetailsBinding, XxxDetailsViewModel>() {
override fun layoutId() = R.layout.activity_xxx_details
override fun viewModelClass() = XxxDetailsViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("详情页面")
viewModel.initOnCreated(intent)
binding.viewModel = viewModel
}
companion object {
@JvmStatic
fun start(context: Context, id: String) {
val starter = Intent(context, XxxDetailsActivity::class.java)
.putExtra(Constant.Key.ID, id)
context.startActivity(starter)
}
}
}
```
**步骤4: 创建Layout**
```xml
<layout>
<data>
<variable name="viewModel" type="...XxxDetailsViewModel" />
</data>
<LinearLayout>
<include layout="@layout/title_tool_bar" />
<ScrollView>
<!-- 详情内容 -->
<LinearLayout>
<TextView android:text="@{viewModel.dataBean.name}" />
<!-- 更多字段... -->
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>
```
### 新增/编辑页开发清单5步
**步骤1: 在Api中添加接口**
```kotlin
@POST("api/xxx/save")
suspend fun saveXxx(@Body data: RequestBody): BaseResultBean<SimpleResultBean>
@POST("api/xxx/details")
suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean<XxxBean>
```
**步骤2: 创建ViewModel**
```kotlin
class XxxAddViewModel : BaseViewModel() {
var pageType: DetailsPageType = DetailsPageType.Add
var id = ""
val dataBean = MutableLiveData(XxxBean())
val optionList = MutableLiveData<List<KeyValue>>()
fun initOnCreated(intent: Intent) {
pageType = DetailsPageType.valueOf(
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
)
loadOptions()
if (pageType != DetailsPageType.Add) {
id = intent.getStringExtra(Constant.Key.ID) ?: ""
loadData()
}
}
private fun loadData() {
launchLoadingCollect({
NetApply.api.getXxxDetails(id)
}) {
onSuccess = {
dataBean.value = it.data ?: XxxBean()
}
}
}
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(if (pageType == DetailsPageType.Add) "新增成功" else "保存成功")
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh")
getTopActivity().finish()
}
}
}
}
```
**步骤3: 创建Activity含多个静态start方法**
```kotlin
@Route(path = ARouterConstants.ACTIVITY_URL_XXX_ADD)
class XxxAddActivity :
BaseBindingActivity<ActivityXxxAddBinding, XxxAddViewModel>() {
override fun layoutId() = R.layout.activity_xxx_add
override fun viewModelClass() = XxxAddViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
viewModel.initOnCreated(intent)
when (viewModel.pageType) {
DetailsPageType.Add -> setBackArrow("新增")
DetailsPageType.Edit -> setBackArrow("编辑")
DetailsPageType.Details -> setBackArrow("详情")
}
binding.viewModel = viewModel
}
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.Edit.name)
.putExtra(Constant.Key.ID, id)
context.startActivity(starter)
}
@JvmStatic
fun startForDetails(context: Context, id: String) {
val starter = Intent(context, XxxAddActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Details.name)
.putExtra(Constant.Key.ID, id)
context.startActivity(starter)
}
}
}
```
**步骤4: 创建Layout**
```xml
<layout>
<data>
<import type="com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType" />
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
<variable name="viewModel" type="...XxxAddViewModel" />
</data>
<LinearLayout>
<include layout="@layout/title_tool_bar" />
<ScrollView>
<LinearLayout>
<!-- 表单字段 -->
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
enable="@{viewModel.pageType != DetailsPageType.Details}"
required="@{true}"
title='@{"名称:"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@={viewModel.dataBean.name}' />
<!-- 更多字段... -->
</LinearLayout>
</ScrollView>
<!-- 底部按钮(详情页不显示) -->
<LinearLayout visible="@{viewModel.pageType != DetailsPageType.Details}">
<TextView
style="@style/tv_bottom_btn_lg"
android:onClick="@{()-> viewModel.submit()}"
android:text="@{viewModel.pageType == DetailsPageType.Add ? `提交` : `保存`}" />
</LinearLayout>
</LinearLayout>
</layout>
```
**步骤5: 注册路由并发送刷新事件**
```kotlin
// 注册路由
const val ACTIVITY_URL_XXX_ADD = "/xxx/XxxAddActivity"
// 在列表页接收刷新事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).observe(this) {
viewModel.refresh()
}
```
## 常见业务场景
### 扫码后查询
```kotlin
// 在ViewModel中
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) {
val code = data?.getStringExtra(Constant.Result.CODED_CONTENT)
waybillNo.value = code
search() // 自动搜索
}
}
```
### 打印标签
```kotlin
// 绑定打印服务
bindService(Intent(this, PrinterService::class.java), serviceConnection, BIND_AUTO_CREATE)
// 打印
val printData = DataForSendToPrinter()
printData.addText("运单号: ${waybillNo}")
printData.addText("重量: ${weight}kg")
printData.addBarcode(waybillNo, 2, 100)
printerService?.sendPrintData(printData)
```
### 图片上传
```kotlin
// 选择图片
PictureSelector.create(this)
.openGallery(SelectMimeType.ofImage())
.setMaxSelectNum(1)
.setImageEngine(GlideEngine.createGlideEngine())
.forResult { result ->
val path = result[0].realPath
uploadImage(path)
}
// 上传接口
private fun uploadImage(path: String) {
val file = File(path)
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
val part = MultipartBody.Part.createFormData("file", file.name, requestFile)
launchLoadingCollect({
NetApply.api.uploadFile(part)
}) {
onSuccess = { result ->
imageUrl.value = result.data?.url
showToast("上传成功")
}
}
}
```
## 关键技术点
### 屏幕适配
- **横屏设计尺寸**: 1152dp × 720dp主要场景
- **竖屏设计尺寸**: 720dp × 1280dp
- **强制横屏**: 所有Activity在AndroidManifest中配置 `android:screenOrientation="userLandscape"`
- **适配库**: AutoSize 1.2.+
### 蓝牙打印
**核心类**:
- `Printer` 模块:独立的打印服务
- `PrinterService`: 后台打印服务
- `PrinterConfig`: 打印机配置管理
- 使用佳博SDK 2.0.4
**打印流程**:
1. 启动PrinterService
2. 扫描/绑定蓝牙打印机
3. 构建打印数据(DataForSendToPrinter)
4. 发送打印任务
### 扫码功能
- 使用 ZXing 2.2.9 库
- 扫描条形码/二维码
- 主要用于运单号、ULD编号、车辆编号、板箱编号
### 权限管理
使用 AndPermission 2.0.2 或扩展函数:
```kotlin
// 方式1: 使用扩展函数(推荐)
permission(Manifest.permission.CAMERA) {
openCamera()
}
// 方式2: 使用AndPermission
AndPermission.with(this)
.runtime()
.permission(Permission.CAMERA)
.onGranted { }
.onDenied { }
.start()
```
### 图片选择
使用 PictureSelector v3.11.2 + Glide 4.15.1
```kotlin
PictureSelector.create(this)
.openGallery(SelectMimeType.ofImage())
.setMaxSelectNum(9)
.setImageEngine(GlideEngine.createGlideEngine())
.forResult { result ->
// 处理选择结果
}
```
## 重要配置文件
### 签名配置
- **KeyStore**: `key.jks` (项目根目录)
- **密码**: storePassword/keyPassword均为 `123321`
- **别名**: `key`
### 网络配置
- **超时时间**: 30秒连接/读取/写入)
- **认证方式**: Bearer Token通过拦截器自动添加
- **Token存储**: SharedPreferences (key: Constant.Share.token)
- **网络安全配置**: `res/xml/network_security_config.xml` (支持HTTP)
### 数据持久化
- **SharedPreferences**: IP地址、Token、用户信息、角色
- **关键常量**: 定义在 `Constant.kt``ConstantEvent.kt`
## Git分支管理
- **当前开发分支**: feature/hefei
- **主分支**: develop用于PR
- **提交前**: 确保代码通过编译,无明显错误
## 技术栈速查
- **协程**: kotlinx-coroutines 1.6.0
- **网络**: Retrofit 2.6.1 + OkHttp 3.12.12
- **JSON**: FastJSON 1.2.73 + Gson 2.10.1
- **路由**: ARouter 1.5.2
- **下拉刷新**: SmartRefreshLayout 2.0.3
- **图表**: MPAndroidChart (定制版)
- **弹窗**: XPopup 2.9.19
- **图片**: Glide 4.15.1 + PictureSelector v3.11.2
- **扫码**: ZXing 2.2.9
- **权限**: AndPermission 2.0.2
- **打印**: 佳博SDK 2.0.4
- **日志**: Timber 5.0.1
- **事件**: EventBus 3.1.1 + FlowBus
---
## 总结
本开发指南涵盖了Android航空物流App的完整开发流程包括
1. **基类架构**: 统一的MVVM架构减少样板代码
2. **网络请求**: 协程+Flow的现代化异步方案
3. **UI组件**: 高度封装的输入、展示控件,保证界面一致性
4. **DataBinding**: 全面的适配器支持,简化视图绑定
5. **扩展函数**: 丰富的Kotlin扩展提高开发效率
6. **开发清单**: 详细的步骤指引,确保不遗漏关键环节
**开发原则**:
- ✅ 优先使用项目现有的基类和封装
- ✅ 充分利用PadDataLayout和PadSearchLayout组件
- ✅ 遵循统一的命名和目录组织规范
- ✅ 使用DataBinding简化代码
- ✅ 利用扩展函数处理通用逻辑
- ✅ 不重复造轮子,保持架构一致性
---
## 常见编译错误及解决方案
### 1. DataBinding错误Cannot resolve type 'DetailsPageType'
**错误信息**:
```
ERROR: Cannot resolve type 'DetailsPageType' file://app/src/main/res/layout/activity_xxx.xml Line:XX
```
**错误原因**:
在XML布局文件中import的包名错误。
**错误示例**:
```xml
<import type="com.lukouguoji.module_base.constant.DetailsPageType" />
```
**正确写法**:
```xml
<import type="com.lukouguoji.module_base.common.DetailsPageType" />
```
**注意**: `DetailsPageType`位于`common`包,不是`constant`包!
---
### 2. DataBinding错误Could not find accessor DataLayoutType.INTEGER
**错误信息**:
```
ERROR: Could not find accessor com.lukouguoji.module_base.ui.weight.data.layout.DataLayoutType.INTEGER
```
**错误原因**:
`DataLayoutType`枚举中不存在`INTEGER`类型。
**错误示例**:
```xml
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
type="@{DataLayoutType.INTEGER}"
value='@={viewModel.bean.count}' />
```
**正确写法**:
```xml
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
type="@{DataLayoutType.INPUT}"
value='@={viewModel.bean.count}' />
```
**可用类型**:
- `DataLayoutType.INPUT` - 文本输入(可输入数字)
- `DataLayoutType.SPINNER` - 下拉选择
- `DataLayoutType.DATE` - 日期选择
---
### 3. Kotlin编译错误Unresolved reference: PAGE_TYPE
**错误信息**:
```
e: Unresolved reference: PAGE_TYPE
```
**错误原因**:
`Constant.Key`对象中缺少`PAGE_TYPE`常量。
**解决方案**:
`module_base/src/main/java/com/lukouguoji/module_base/common/Constant.kt`中添加:
```kotlin
object Key {
// ... 其他常量
// ID
const val ID = "id"
// 页面类型
const val PAGE_TYPE = "pageType"
// ... 其他常量
}
```
**使用示例**:
```kotlin
val starter = Intent(context, XxxActivity::class.java)
.putExtra(Constant.Key.PAGE_TYPE, DetailsPageType.Add.name)
.putExtra(Constant.Key.ID, id)
```
---
### 4. Kotlin编译错误Unresolved reference: Edit
**错误信息**:
```
e: Unresolved reference: Edit
```
**错误原因**:
`DetailsPageType`枚举中不存在`Edit`值。
**错误示例**:
```kotlin
when (viewModel.pageType) {
DetailsPageType.Add -> setBackArrow("新增")
DetailsPageType.Edit -> setBackArrow("编辑") // ❌ 错误
DetailsPageType.Details -> setBackArrow("详情")
}
```
**正确写法**:
```kotlin
when (viewModel.pageType) {
DetailsPageType.Add -> setBackArrow("新增")
DetailsPageType.Modify -> setBackArrow("编辑") // ✅ 正确
DetailsPageType.Details -> setBackArrow("详情")
}
```
**DetailsPageType枚举值**:
```kotlin
enum class DetailsPageType(val title: String) {
Add("新增"), // 新增页面
Modify("编辑"), // 编辑页面注意不是Edit
Details("详情") // 详情页面
}
```
---
### 5. Kotlin编译错误Unresolved reference: IOnItemClickListener
**错误信息**:
```
e: Unresolved reference: IOnItemClickListener
```
**错误原因**:
import的包名错误`IOnItemClickListener``interfaces`包,不是`impl`包。
**错误示例**:
```kotlin
import com.lukouguoji.module_base.impl.IOnItemClickListener // ❌ 错误
```
**正确写法**:
```kotlin
import com.lukouguoji.module_base.interfaces.IOnItemClickListener // ✅ 正确
```
**使用场景**:
```kotlin
class XxxViewModel : BaseViewModel(), IOnItemClickListener {
override fun onItemClick(position: Int, type: Int) {
// 处理点击事件
}
}
```
---
### 6. FlowBus使用错误
#### 错误AUnresolved reference: observe
**错误信息**:
```
e: Unresolved reference: observe
```
**错误原因**:
缺少`observe`扩展函数的import。
**解决方案**:
```kotlin
import com.lukouguoji.module_base.impl.FlowBus
import com.lukouguoji.module_base.impl.observe // ✅ 添加这一行
// 在Activity中使用
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).observe(this) {
viewModel.refresh()
}
```
#### 错误BSuspend function 'emit' should be called only from a coroutine
**错误信息**:
```
e: Suspend function 'emit' should be called only from a coroutine or another suspend function
```
**错误原因**:
`emit()`是suspend函数需要在协程中调用。
**错误示例**:
```kotlin
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") // ❌ 错误
```
**正确写法**:
```kotlin
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
// 在ViewModel中
viewModelScope.launch {
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") // ✅ 正确
}
```
---
### 7. 图片上传字段错误
**错误信息**:
```
e: Unresolved reference: url
```
**错误原因**:
`UploadBean`返回的字段名是`newName`,不是`url`
**UploadBean结构**:
```kotlin
class UploadBean {
var newName: String = "" // ✅ 正确字段名
var zipFileName: String = ""
}
```
**错误示例**:
```kotlin
val result = UploadUtil.upload(filePath)
if (result.verifySuccess()) {
val imageUrl = result.data?.url ?: "" // ❌ 错误没有url字段
}
```
**正确写法**:
```kotlin
val result = UploadUtil.upload(filePath)
if (result.verifySuccess()) {
val imageUrl = result.data?.newName ?: "" // ✅ 正确使用newName
}
```
**完整上传示例**:
```kotlin
launchLoadingCollect({
val uploadedUrls = mutableListOf<String>()
imageList.forEach { fileBean ->
if (fileBean.path.startsWith("http")) {
// 已上传的图片直接使用URL
uploadedUrls.add(fileBean.path)
} else {
// 本地图片,需要上传
val result = UploadUtil.upload(fileBean.path)
if (result.verifySuccess()) {
uploadedUrls.add(result.data?.newName ?: "") // 使用newName
}
}
}
// 提交时将图片URL列表用逗号拼接
val params = mapOf(
"images" to uploadedUrls.joinToString(",")
).toRequestBody()
NetApply.api.saveData(params)
}) {
onSuccess = {
showToast("保存成功")
}
}
```
---
### 8. RecyclerView DataBinding items属性问题
**问题现象**:
在DataBinding中使用`items`属性绑定数据会导致编译错误。
**错误示例**:
```xml
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvImages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
items="@{viewModel.imageList}" 这个属性会导致编译错误
itemLayoutId="@{viewModel.imageItemLayoutId}"
viewHolder="@{viewModel.imageItemViewHolder}" />
```
**正确做法**:
移除`items`属性在Activity中手动更新adapter。
**XML布局**:
```xml
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvImages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
itemLayoutId="@{viewModel.imageItemLayoutId}"
viewHolder="@{viewModel.imageItemViewHolder}"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3" />
```
**Activity代码**:
```kotlin
import com.lukouguoji.module_base.ktx.commonAdapter
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("页面标题")
binding.viewModel = viewModel
// 监听数据变化并手动更新adapter
viewModel.imageList.observe(this) { images ->
binding.rvImages.commonAdapter()?.refresh(images)
}
}
```
---
### 9. pageType在DataBinding中的正确使用
**问题**:
如果`pageType`声明为普通变量DataBinding无法正确绑定。
**错误示例**:
```kotlin
class XxxViewModel : BaseViewModel() {
var pageType: DetailsPageType = DetailsPageType.Add // ❌ 普通变量
}
```
**正确写法**:
```kotlin
class XxxViewModel : BaseViewModel() {
val pageType = MutableLiveData(DetailsPageType.Add) // ✅ 使用LiveData
}
```
**ViewModel中访问**:
```kotlin
fun initOnCreated(intent: Intent) {
pageType.value = DetailsPageType.valueOf(
intent.getStringExtra(Constant.Key.PAGE_TYPE) ?: DetailsPageType.Add.name
)
if (pageType.value != DetailsPageType.Add) {
loadData()
}
}
```
**Activity中访问**:
```kotlin
override fun initOnCreate(savedInstanceState: Bundle?) {
viewModel.initOnCreated(intent)
when (viewModel.pageType.value) {
DetailsPageType.Add -> setBackArrow("新增")
DetailsPageType.Modify -> setBackArrow("编辑")
DetailsPageType.Details -> setBackArrow("详情")
}
}
```
**XML中使用**:
```xml
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayout
enable="@{viewModel.pageType != DetailsPageType.Details}"
required="@{viewModel.pageType != DetailsPageType.Details}"
title='@{"运单号:"}'
value='@={viewModel.bean.waybillNo}' />
```
**注意**: 在XML的DataBinding表达式中直接使用`viewModel.pageType`即可,不需要`.value`
---
## 错误排查流程
当遇到编译错误时,按以下顺序排查:
### 第1步检查DataBinding错误
如果看到`android.databinding.tool.util.LoggedErrorException`
1. **检查import语句的包名**
-`com.lukouguoji.module_base.common.DetailsPageType`
-`com.lukouguoji.module_base.constant.DetailsPageType`
2. **检查枚举值是否正确**
-`DataLayoutType.INPUT``SPINNER``DATE`
-`DataLayoutType.INTEGER`(不存在)
-`DetailsPageType.Modify`(编辑)
-`DetailsPageType.Edit`(不存在)
3. **移除不支持的属性**
- 移除RecyclerView的`items`属性
- 改为在Activity中手动更新adapter
### 第2步检查Kotlin编译错误
如果看到`Unresolved reference`
1. **检查import语句**
- `IOnItemClickListener``com.lukouguoji.module_base.interfaces.IOnItemClickListener`
- `observe``com.lukouguoji.module_base.impl.observe`
2. **检查常量是否存在**
- 确认`Constant.Key.PAGE_TYPE`已定义
- 确认`Constant.Key.ID`已定义
3. **检查字段名称**
- `UploadBean`使用`newName`,不是`url`
### 第3步检查协程相关错误
如果看到suspend function相关错误
1. **添加必要的import**
```kotlin
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
```
2. **在协程中调用emit**
```kotlin
viewModelScope.launch {
FlowBus.with<String>(event).emit(data)
}
```
### 第4步清理并重新构建
如果上述都检查过,仍有问题:
```bash
# 清理项目
./gradlew clean
# 重新构建
./gradlew assembleDebug
```
---
## 最佳实践建议
### 1. 开发前检查清单
- [ ] 确认`DetailsPageType`在`common`包
- [ ] 确认`IOnItemClickListener`在`interfaces`包
- [ ] 确认`Constant.Key.PAGE_TYPE`常量已定义
- [ ] 熟悉`DataLayoutType`和`DetailsPageType`的枚举值
### 2. 代码编写规范
- ✅ 使用`MutableLiveData`声明`pageType`,不用普通变量
- ✅ RecyclerView不使用`items`属性改用手动更新adapter
- ✅ FlowBus的`emit()`必须在`viewModelScope.launch`中调用
- ✅ `observe`扩展函数需要单独import
- ✅ 图片上传使用`UploadBean.newName`字段
### 3. 参考已有代码
遇到问题时,优先参考项目中已有的类似实现:
- 查看`AccidentVisaDetailsViewModel`了解`pageType`的LiveData用法
- 查看`GncShouYunListActivity`了解FlowBus的正确使用
- 查看现有的编辑页面了解图片上传的完整流程
---
## 快速修复命令
```bash
# 查找DetailsPageType的正确包名
grep -r "enum class DetailsPageType" module_base/src --include="*.kt"
# 查找IOnItemClickListener的正确包名
find module_base/src -name "IOnItemClickListener.kt"
# 查找DataLayoutType的枚举值
grep -A 5 "enum class DataLayoutType" module_base/src --include="*.kt"
# 查找UploadBean的字段定义
grep -A 10 "class UploadBean" module_base/src --include="*.kt"
```
通过遵循这些规范和检查清单,可以避免大部分常见的编译错误。