2512 lines
74 KiB
Markdown
2512 lines
74 KiB
Markdown
# 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="文本内容" />
|
||
|
||
<!-- 支持Int(0隐藏,非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简化代码
|
||
- ✅ 利用扩展函数处理通用逻辑
|
||
- ✅ 不重复造轮子,保持架构一致性
|