31 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
项目概况
项目名称: AirLogistics - 航空物流信息管理系统 架构模式: MVVM + 组件化 | 语言: Kotlin 1.6.21 + Java | 版本: 1.8.4 (versionCode 84) SDK: minSdk 24 / targetSdk 30 / compileSdk 31 | Gradle: 7.3.3 | JDK: 1.8
核心架构
- MVVM 基类:
BaseActivity→BaseBindingActivity(DataBinding)、BaseViewModel→BasePageViewModel(分页列表) - 适配器:
CommonAdapter+BaseViewHolder统一列表封装 - 路由: ARouter 1.5.2 | 事件: FlowBus(Flow)+ EventBus 3.1.1
- 网络: Retrofit 2.6.1 + OkHttp 3.12.12 + Coroutines
launchCollect:无 Loading 后台请求launchLoadingCollect:带 Loading 请求toRequestBody:Map/Bean 转 JSON
组件化模块
| 模块 | 说明 | 模块 | 说明 |
|---|---|---|---|
app/ |
应用壳层 | module_base/ |
核心基础库 |
module_gnc/ |
国内出港 | module_gnj/ |
国内进港 |
module_gjc/ |
国际出港 | module_gjj/ |
国际进港 |
module_hangban/ |
航班管理 | module_cargo/ |
货物追踪 |
module_mit/ |
监装监卸 | module_p/ |
PDA 功能 |
Printer/ |
蓝牙打印 | MPChartLib/ |
图表库 |
关键目录结构
aerologic-app/
├── app/src/main/java/com/lukouguoji/aerologic/
│ ├── ui/viewModel/ # ViewModel
│ ├── ui/fragment/ # Fragment
│ └── page/ # 业务页面
├── module_base/src/main/java/com/lukouguoji/module_base/
│ ├── base/ # 基类 (BaseActivity, BaseViewModel, BaseViewHolder, BaseDialogModel)
│ ├── bean/ # 数据模型
│ ├── common/ # 常量 (Constant, DetailsPageType, ConstantEvent)
│ ├── http/net/ # 网络 (NetApply, Api)
│ ├── ktx/ # 扩展函数
│ ├── impl/ # FlowBus, observe
│ ├── interfaces/ # IOnItemClickListener
│ ├── router/ # ARouterConstants
│ └── ui/weight/ # UI 组件 (PadSearchLayout, PadDataLayout, PadDataLayoutNew)
├── module_gjc/src/main/ # 国际出港(典型参考模块)
└── 其他业务模块...
6 种典型页面类型
基于 module_gjc(国际出港)模块归纳,覆盖项目中所有常见页面模式。
类型 1:列表查询页
代表: GjcBoxWeighingActivity / GjcInspectionActivity
结构: 搜索条件区 + SmartRefreshLayout 分页列表 + 底部统计/操作栏
Activity 骨架:
@Route(path = ARouterConstants.ACTIVITY_URL_XXX)
class XxxActivity : BaseBindingActivity<ActivityXxxBinding, XxxViewModel>() {
override fun layoutId() = R.layout.activity_xxx
override fun viewModelClass() = XxxViewModel::class.java
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("页面标题")
binding.viewModel = viewModel
// 绑定分页
viewModel.pageModel.bindSmartRefreshLayout(binding.srl, binding.rv, viewModel, this)
// 监听刷新事件
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) { viewModel.refresh() }
viewModel.refresh()
}
}
ViewModel 骨架:
class XxxViewModel : BasePageViewModel() {
// 搜索条件
val flightDate = MutableLiveData(DateUtils.getCurrentTime().formatDate())
val flightNo = MutableLiveData("")
// 适配器配置
val itemViewHolder = XxxViewHolder::class.java
val itemLayoutId = R.layout.item_xxx
// 统计数据
val totalCount = MutableLiveData("0")
fun searchClick() { refresh() }
override fun getData() {
val params = mapOf(
"pageNum" to pageModel.page, "pageSize" to pageModel.limit,
"fdate" to flightDate.value?.ifEmpty { null },
"fno" to flightNo.value?.ifEmpty { null }
).toRequestBody()
launchLoadingCollect({ NetApply.api.getXxxList(params) }) {
onSuccess = { pageModel.handleListBean(it) }
}
// 统计(无 Loading,不阻塞列表)
launchCollect({ NetApply.api.getXxxTotal(totalParams) }) {
onSuccess = { totalCount.value = (it.data?.count ?: 0).toString() }
}
}
}
布局结构:
<LinearLayout orientation="vertical">
<include layout="@layout/title_tool_bar" />
<!-- 搜索区:PadSearchLayout 横排 + 操作按钮(如有) -->
<LinearLayout orientation="horizontal">
<PadSearchLayout type="@{SearchLayoutType.DATE}" value="@={viewModel.flightDate}" />
<PadSearchLayout type="@{SearchLayoutType.INPUT}" value="@={viewModel.flightNo}" />
<ImageView style="@style/iv_search_action" android:onClick="@{()-> viewModel.searchClick()}" />
<!-- 如需新增/删除按钮,尺寸规范见「开发原则」工具栏图标尺寸规范 -->
</LinearLayout>
<!-- 分页列表 -->
<SmartRefreshLayout android:id="@+id/srl" layout_weight="1">
<RecyclerView android:id="@+id/rv"
itemLayoutId="@{viewModel.itemLayoutId}" viewHolder="@{viewModel.itemViewHolder}" />
</SmartRefreshLayout>
<!-- 底部统计栏 -->
<LinearLayout background="@color/color_bottom_layout" height="50dp">
<TextView text='@{"合计:" + viewModel.totalCount + "票"}' />
</LinearLayout>
</LinearLayout>
参考文件: module_gjc/.../GjcBoxWeighingActivity.kt、GjcBoxWeighingViewModel.kt
列表项布局规范 (item_xxx.xml)
整体结构: 水平 LinearLayout → 左侧图标 + 中间内容区(多行 KV)+ 右侧箭头
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginVertical="5dp"
android:background="@drawable/bg_item"
android:orientation="horizontal"
android:padding="10dp">
<!-- 左侧图标(普通列表用 img_plane,多选列表用 img_plane/img_plane_s 切换) -->
<ImageView
android:id="@+id/iv_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:src="@drawable/img_plane" />
<!-- 中间内容区 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<!-- 第一行 KV -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- KV 组件(见下方单个 KV 模板) -->
</androidx.appcompat.widget.LinearLayoutCompat>
<!-- 第二行 KV(marginTop=10dp) -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<!-- KV 组件(weight 和 completeSpace 必须与第一行对应位置相同) -->
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
<!-- 右侧箭头(固定写法) -->
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:src="@drawable/img_pda_right" />
</androidx.appcompat.widget.LinearLayoutCompat>
单个 KV 组件模板:
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="center_vertical">
<TextView
completeSpace="@{5}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签名:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.fieldName}" />
</androidx.appcompat.widget.LinearLayoutCompat>
关键对齐规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| 列 weight 一致 | 第一行和第二行相同位置的 KV 组件必须使用相同的 layout_weight |
第一行第 1 列 weight=1.0,第二行第 1 列也必须 weight=1.0 |
| 列 completeSpace 取最大值 | 同一列位置的 completeSpace 取两行中较大的那个值(1 个汉字 = 1 宽度,1 个标点 = 1 宽度) | 第一行"运单号:"=4,第二行"特码:"=3 → 两行都用 completeSpace=4 |
| 右侧箭头统一 | 固定使用 @drawable/img_pda_right,尺寸 30x30dp,layout_gravity="center",marginLeft="10dp" |
— |
completeSpace 计算方法: 统计 Label 字符数(含冒号),每个汉字算 1,每个标点(:)算 1。示例:
- "运单号:" = 3字 + 1标点 = 4
- "运单类型:" = 4字 + 1标点 = 5
- "状态:" = 2字 + 1标点 = 3
- "始发站:" = 3字 + 1标点 = 4(注:此处"始发站:"虽然有4个字符位,但":"也要占1个宽度)
同列两行的 completeSpace 统一取 max(row1, row2)。例如第 1 列:row1"运单号:"=4,row2"特码:"=3 → 两行都用 4。
常用字段 weight 参考表(基于国际出港模块统计):
| 字段类型 | 典型 weight | 常见范围 | 典型 completeSpace |
|---|---|---|---|
| 运单号 | 1.0 | 0.9~1.2 | 4 |
| 件数 | 1.2 | 0.8~1.2 | 3~5 |
| 重量 | 0.8 | 0.7~1.0 | 3~5 |
| 状态 | 0.8 | 0.7~0.8 | 3~4 |
| 代理 | 0.8 | 0.7~0.8 | 3~4 |
| 特码 | 1.0 | 0.9~1.0 | 3~4 |
| 始发站/目的站 | 0.8 | 0.7~0.8 | 4 |
| 运单类型/业务类型 | 1.2 | 1.0~1.2 | 5 |
| 分单数 | 0.8 | 0.6~0.8 | 4 |
| 航班号/航班 | 1.0~1.2 | 1.0~1.2 | 4~5 |
| 时间类(入库/离港/过磅) | 1.0~1.2 | 1.0~1.2 | 5 |
原则: 相同字段在不同页面应使用相近的 weight,优先参照同模块已有布局。若新页面与已有页面字段完全相同,应直接复用其 weight 和 completeSpace 配置。
典型 weight 分布示例(5列,运单号/状态/代理/件数/重量 + 特码/始发站/目的站/运单类型/分单数):
位置: 第1列 第2列 第3列 第4列 第5列
weight: 1.0 0.8 0.8 1.2 0.8 ← 第一行
weight: 1.0 0.8 0.8 1.2 0.8 ← 第二行(必须相同)
cSpace: 4 4 4 5 4 ← 第一行
cSpace: 4 4 4 5 4 ← 第二行(必须相同,取 max)
参考文件: module_gjc/.../item_int_exp_tally.xml(典型)、item_gjc_query.xml、item_gjc_box_weighing.xml
类型 2:多选列表 + 批量操作页
代表: IntExpOutHandoverActivity / GjcAssembleAllocateActivity
结构: 类型 1 基础上 + 全选按钮 + ObservableBoolean 选中状态 + 批量操作
与类型 1 的区别:
- Bean 增加 ObservableBoolean:
class XxxBean {
val checked: ObservableBoolean = ObservableBoolean(false)
var isSelected: Boolean
get() = checked.get()
set(value) = checked.set(value)
}
- ViewModel 增加全选逻辑:
val isAllChecked = MutableLiveData(false)
init {
isAllChecked.observeForever { checked ->
val list = pageModel.rv?.commonAdapter()?.items as? List<XxxBean> ?: return@observeForever
list.forEach { it.checked.set(checked) }
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
}
fun checkAllClick() {
val list = pageModel.rv?.commonAdapter()?.items as? List<XxxBean> ?: return
val shouldCheckAll = !isAllChecked.value!!
list.forEach { it.checked.set(shouldCheckAll) }
isAllChecked.value = shouldCheckAll
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
fun batchAction() {
val selected = (pageModel.rv?.commonAdapter()?.items as? List<XxxBean>)
?.filter { it.isSelected } ?: return
if (selected.isEmpty()) { showToast("请选择数据"); return }
launchLoadingCollect({ NetApply.api.batchXxx(selected.toRequestBody()) }) {
onSuccess = { showToast("操作成功"); refresh() }
}
}
- Activity 观察全选图标:
viewModel.isAllChecked.observe(this) { binding.checkIcon.alpha = if (it) 1.0f else 0.5f }
- Item 布局图片切换:
<ImageView android:id="@+id/iv_icon"
loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}" />
- ViewHolder 中处理点击:
binding.ivIcon.setOnClickListener {
bean.checked.set(!bean.checked.get())
binding.executePendingBindings()
}
参考文件: module_gjc/.../IntExpOutHandoverActivity.kt、IntExpOutHandoverViewModel.kt
类型 3:嵌套多选列表页
代表: IntExpStorageUseActivity
结构: 主列表含子列表(展开/收起)+ 主子联动全选 + Dialog 操作
与类型 2 的区别:
- Activity 暴露给布局(用于调用 Dialog 方法):
binding.activity = this // XML 中可调用 activity.showXxxDialog()
- 联动全选(主+子列表):
isAllChecked.observeForever { checked ->
val list = pageModel.rv?.commonAdapter()?.items as? List<GjcMaWb> ?: return@observeForever
list.forEach {
it.checked.set(checked)
it.storageUseList?.forEach { sub -> sub.checked.set(checked) }
}
}
- 全局展开/收起:
val isAllExpanded = MutableLiveData(false)
fun toggleAllExpand() {
val shouldExpand = !isAllExpanded.value!!
isAllExpanded.value = shouldExpand
(pageModel.rv?.commonAdapter()?.items as? List<GjcMaWb>)?.forEach {
if (!it.storageUseList.isNullOrEmpty()) it.showMore.set(shouldExpand)
}
pageModel.rv?.commonAdapter()?.notifyDataSetChanged()
}
参考文件: module_gjc/.../IntExpStorageUseActivity.kt、IntExpStorageUseViewModel.kt
类型 4:Tab 详情页
代表: GjcQueryDetailsActivity
结构: 自定义 Tab 栏 + ViewPager2 + 多 Fragment
Activity 骨架:
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("查询详情")
binding.viewModel = viewModel
viewModel.initOnCreated(intent)
// ViewPager2 配置
binding.vp.adapter = CustomVP2Adapter(viewModel.fragmentList, supportFragmentManager, lifecycle)
binding.vp.isUserInputEnabled = false // 禁用滑动
binding.vp.offscreenPageLimit = 3
// Tab 切换
viewModel.currentTab.observe(this) { binding.vp.setCurrentItem(it, false) }
viewModel.loadDetails()
}
companion object {
@JvmStatic
fun start(context: Context, id: Long?) {
context.startActivity(Intent(context, XxxDetailsActivity::class.java)
.putExtra(Constant.Key.ID, id?.toString() ?: ""))
}
}
ViewModel 骨架:
class XxxDetailsViewModel : BaseViewModel() {
val currentTab = MutableLiveData(0)
val fragmentList by lazy {
listOf(
FragmentA.newInstance(this),
FragmentB.newInstance(this),
FragmentC.newInstance(this)
)
}
fun onTabClick(index: Int) { currentTab.value = index }
}
Tab 布局模式:
<!-- 自定义 Tab(非 TabLayout) -->
<LinearLayout height="40dp" orientation="horizontal">
<LinearLayout width="100dp" gravity="center" onClick="@{()->viewModel.onTabClick(0)}" orientation="vertical">
<TextView text="运单信息"
textColor="@{viewModel.currentTab == 0 ? @color/colorPrimary : @color/text_gray}" />
<View height="3dp"
background="@{viewModel.currentTab == 0 ? @color/colorPrimary : @color/transparent}"
visibility="@{viewModel.currentTab == 0 ? View.VISIBLE : View.INVISIBLE}" />
</LinearLayout>
<!-- 更多 Tab... -->
</LinearLayout>
<ViewPager2 android:id="@+id/vp" layout_weight="1" />
参考文件: module_gjc/.../GjcQueryDetailsActivity.kt、GjcQueryDetailsViewModel.kt
类型 5:编辑表单页
代表: GjcQueryEditActivity
结构: ScrollView + PadDataLayoutNew 表单(只读+可编辑混合)+ 保存/取消
Activity 骨架:
override fun initOnCreate(savedInstanceState: Bundle?) {
setBackArrow("运单修改")
binding.viewModel = viewModel
viewModel.initOnCreated(intent)
}
companion object {
@JvmStatic
fun start(context: Context, bean: XxxBean) {
context.startActivity(Intent(context, XxxEditActivity::class.java)
.putExtra(Constant.Key.DATA, Gson().toJson(bean)))
}
}
ViewModel 骨架:
class XxxEditViewModel : BaseViewModel() {
val dataBean = MutableLiveData(XxxBean())
val packageTypeList = MutableLiveData<List<KeyValue>>(emptyList())
fun initOnCreated(intent: Intent) {
val json = intent.getStringExtra(Constant.Key.DATA) ?: ""
if (json.isNotEmpty()) {
val bean = Gson().fromJson(json, XxxBean::class.java)
loadDropdownLists()
loadDetails(bean.id)
}
}
fun submit() {
val bean = dataBean.value ?: return
if (bean.wbNo.verifyNullOrEmpty("运单号不能为空")) return
launchLoadingCollect({ NetApply.api.updateXxx(bean.toRequestBody()) }) {
onSuccess = {
showToast("修改成功")
viewModelScope.launch { FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh") }
getTopActivity().finish()
}
}
}
fun cancel() { getTopActivity().finish() }
}
表单布局模式:
<ScrollView layout_weight="1" fillViewport="true">
<LinearLayout padding="15dp" orientation="vertical">
<LinearLayout background="@drawable/bg_white_radius_8" padding="15dp" orientation="vertical">
<!-- 三列表单行 -->
<LinearLayout orientation="horizontal">
<PadDataLayoutNew layout_weight="1" enable="@{false}" title='@{"运单号"}'
type="@{DataLayoutType.INPUT}" value='@{viewModel.dataBean.wbNo}' />
<PadDataLayoutNew layout_weight="1" enable="@{true}" title='@{"特码"}'
type="@{DataLayoutType.INPUT}" value='@={viewModel.dataBean.spCode}' />
<PadDataLayoutNew layout_weight="1" title='@{"包装类型"}'
type="@{DataLayoutType.SPINNER}" list="@{viewModel.packageTypeList}"
value='@={viewModel.dataBean.packageType}' />
</LinearLayout>
<!-- 备注(多行) -->
<PadDataLayoutNew inputHeight="@{80}" title='@{"备注"}'
type="@{DataLayoutType.INPUT}" value='@={viewModel.dataBean.remark}' />
</LinearLayout>
<!-- 底部按钮 -->
<LinearLayout gravity="center" marginTop="24dp">
<TextView style="@style/tv_bottom_btn" width="120dp" onClick="@{()-> viewModel.cancel()}" text="取消" />
<TextView style="@style/tv_bottom_btn" width="120dp" onClick="@{()-> viewModel.submit()}" text="保存" />
</LinearLayout>
</LinearLayout>
</ScrollView>
参考文件: module_gjc/.../GjcQueryEditActivity.kt、GjcQueryEditViewModel.kt
类型 6:添加表单页(含输入回调)
代表: GjcBoxWeighingAddActivity
结构: 类型 5 基础上 + setRefreshCallBack 输入完成回调 + 实时计算 + 扫码自动填充
与类型 5 的区别:
- 输入完成回调(关键特性):
<!-- 使用方法引用,不能用 Lambda -->
<PadDataLayoutNew
setRefreshCallBack="@{viewModel::onCarIdInputComplete}"
title='@{"架子车号"}' type="@{DataLayoutType.INPUT}" value='@={viewModel.carId}' />
private var lastQueriedCarId = ""
fun onCarIdInputComplete() {
val id = carId.value
if (!id.isNullOrEmpty() && id != lastQueriedCarId) {
lastQueriedCarId = id
queryFlatcarInfo(id) // 输入完成后自动查询
}
}
- 级联查询(航班日期+航班号同时有值时查询):
fun onFlightNoInputComplete() { queryFlightIfReady() }
fun onFlightDateInputComplete() { lastQueriedFlight = ""; queryFlightIfReady() }
private fun queryFlightIfReady() {
val fdate = flightDate.value; val fno = flightNo.value
if (!fdate.isNullOrEmpty() && !fno.isNullOrEmpty()) {
val key = "$fdate-$fno"
if (key != lastQueriedFlight) { lastQueriedFlight = key; queryFlightInfo(fdate, fno) }
}
}
- 实时计算:
fun initOnCreated(activity: Activity) {
totalWeight.observe(activity as LifecycleOwner) {
val total = it?.toDoubleOrNull() ?: 0.0
val net = total - (dataBean.value?.carWeight ?: 0.0)
val cargo = net - (dataBean.value?.uldWeight ?: 0.0)
netWeight.value = if (net > 0) net.toString() else "0"
cargoWeight.value = if (cargo > 0) cargo.toString() else "0"
}
}
- 输入限制:
// Activity 中设置
binding.carIdInput.et.setUpperCaseAlphanumericFilter()
- 重置功能:
fun resetClick() {
dataBean.value = XxxBean()
carId.value = ""; uldNo.value = ""; flightNo.value = ""
lastQueriedCarId = ""; lastQueriedUld = ""; lastQueriedFlight = ""
}
参考文件: module_gjc/.../GjcBoxWeighingAddActivity.kt、GjcBoxWeighingAddViewModel.kt
自定义 Dialog 开发模式
基于 BaseDialogModel(XPopup 封装),支持 5 种弹窗类型。
基础模板
class XxxDialogModel(
private val onConfirm: () -> Unit
) : BaseDialogModel<DialogXxxBinding>(DIALOG_TYPE_CENTER) { // CENTER/BOTTOM/DRAWER/FULL
override fun layoutId() = R.layout.dialog_xxx
override fun onDialogCreated(context: Context) {
binding.model = this
}
fun onConfirmClick() { onConfirm(); dismiss() }
}
// 使用
XxxDialogModel(onConfirm = { refresh() }).show()
弹窗类型
| 类型 | 常量 | 场景 | 示例 |
|---|---|---|---|
| 底部 | DIALOG_TYPE_BOTTOM |
操作选项 | 默认类型 |
| 中间 | DIALOG_TYPE_CENTER |
确认框、表单输入 | ConfirmDialogModel |
| 抽屉 | DIALOG_TYPE_DRAWER |
筛选条件 | GjcQueryFilterDialogModel |
| 全屏 | DIALOG_TYPE_FULL |
列表展示 | NoticeMessageDialogModel |
抽屉弹窗(筛选场景)
class XxxFilterDialogModel(
val filterField: MutableLiveData<String>,
private val onConfirm: () -> Unit
) : BaseDialogModel<DialogXxxFilterBinding>(DIALOG_TYPE_DRAWER) {
override fun onBuild(builder: XPopup.Builder) {
builder.popupPosition(PopupPosition.Right)
val width = DevUtils.getTopActivity().window.decorView.width / 3
builder.maxWidth(width).popupWidth(width)
}
override fun onDialogCreated(context: Context) {
binding.model = this
binding.lifecycleOwner = context as? LifecycleOwner
}
fun onResetClick() { filterField.value = "" }
fun onConfirmClick() { dismiss(); onConfirm() }
}
参考文件: module_base/.../BaseDialogModel.kt、module_gjc/.../dialog/
命名与文件组织
| 类型 | 命名 | 目录 |
|---|---|---|
| Activity | XxxActivity |
模块/page/ 或 模块/activity/ |
| ViewModel | XxxViewModel |
模块/viewModel/ |
| ViewHolder | XxxViewHolder |
模块/holder/ |
| Adapter | XxxAdapter |
模块/adapter/ |
| Bean | XxxBean |
module_base/bean/ |
| Dialog | XxxDialogModel |
模块/dialog/ |
| 布局 | activity_xxx.xml / item_xxx.xml / dialog_xxx.xml |
res/layout/ |
构建命令
./gradlew clean # 清理
./gradlew assembleDebug # Debug APK
./gradlew assembleRelease # Release APK(已签名)
./gradlew installDebug # 安装到设备
adb devices -l # 查看设备
adb logcat | grep "com.lukouguoji.aerologic" # 日志
DataBinding 关键规则
- lifecycleOwner 必须设置(
BaseBindingActivity已自动设置,手动使用时binding.lifecycleOwner = this) - 字符串拼接用反引号:
@{+`+姓名:+`++ viewModel.name} - LiveData 自动解包: XML 中直接
viewModel.dataBean.name,不写.value - 修改对象属性需重新赋值:
dataBean.value = dataBean.value?.copy(name = "新值") - 双向绑定用
@={}:value="@={viewModel.searchText}" - 点击事件用 Lambda:
onClick="@{() -> viewModel.submit()}" - setRefreshCallBack 用方法引用:
setRefreshCallBack="@{viewModel::methodName}"(不能用 Lambda) - 使用 View.VISIBLE/GONE 必须导入:
<import type="android.view.View" /> - textStyle 不支持 DataBinding: 只能用固定值如
android:textStyle="bold"
UI 组件速查
PadSearchLayout(搜索区)
| type | 用途 | 示例 |
|---|---|---|
SearchLayoutType.INPUT |
文本输入 | value="@={viewModel.flightNo}" |
SearchLayoutType.DATE |
日期选择 | value="@={viewModel.flightDate}" |
SearchLayoutType.SPINNER |
下拉选择 | list="@{viewModel.statusList}" value="@={viewModel.status}" |
支持扫码图标: icon="@{@mipmap/scan_code}" setOnIconClickListener="@{(v)-> viewModel.scan()}"
PadDataLayoutNew(表单区)
| type | 用途 | 关键属性 |
|---|---|---|
DataLayoutType.INPUT |
文本/多行输入 | enable, required, maxLength, inputHeight, hint |
DataLayoutType.SPINNER |
下拉选择 | list, hint |
DataLayoutType.DATE |
日期选择 | hint |
通用属性: title='@{"标题"}'、titleLength="@{5}"、value='@={viewModel.field}'
回调属性: setRefreshCallBack="@{viewModel::onInputComplete}"
completeSpace 对齐
completeSpace="@{5}" 设置 Key 文本宽度(以"一"字宽度为单位),用于 Key-Value 布局对齐。
Import 路径速查
基类与常用类
| 类 | 正确路径 |
|---|---|
BaseActivity |
com.lukouguoji.module_base.base.BaseActivity |
BaseBindingActivity |
com.lukouguoji.module_base.base.BaseBindingActivity |
BaseViewModel |
com.lukouguoji.module_base.base.BaseViewModel |
BasePageViewModel |
com.lukouguoji.module_base.base.BasePageViewModel |
BaseViewHolder |
com.lukouguoji.module_base.base.BaseViewHolder |
BaseDialogModel |
com.lukouguoji.module_base.base.BaseDialogModel |
CustomVP2Adapter |
com.lukouguoji.module_base.base.CustomVP2Adapter |
Constant |
com.lukouguoji.module_base.common.Constant |
DetailsPageType |
com.lukouguoji.module_base.common.DetailsPageType |
ConstantEvent |
com.lukouguoji.module_base.common.ConstantEvent |
NetApply |
com.lukouguoji.module_base.http.net.NetApply |
FlowBus |
com.lukouguoji.module_base.impl.FlowBus |
observe(FlowBus 扩展) |
com.lukouguoji.module_base.impl.observe |
IOnItemClickListener |
com.lukouguoji.module_base.interfaces.IOnItemClickListener |
ARouterConstants |
com.lukouguoji.module_base.router.ARouterConstants |
扩展函数(均在 com.lukouguoji.module_base.ktx 包下)
launchCollect、launchLoadingCollect、showToast、toRequestBody、verifyNullOrEmpty、noNull、formatDate
常见编译错误速查
| 错误 | 原因 | 修复 |
|---|---|---|
DetailsPageType 找不到 |
包名错误 | common.DetailsPageType,非 constant. |
DataLayoutType.INTEGER |
不存在 | 用 DataLayoutType.INPUT |
DetailsPageType.Edit |
不存在 | 用 DetailsPageType.Modify |
IOnItemClickListener 找不到 |
包名错误 | interfaces.,非 impl. |
FlowBus.observe 无法调用 |
未导入扩展 | 单独导入 com.lukouguoji.module_base.impl.observe |
FlowBus.emit() 报错 |
需在协程中 | viewModelScope.launch { FlowBus.with<String>(...).emit(...) } |
| 图片上传字段错误 | 无 url 字段 |
用 result.data?.newName |
pageType 绑定失效 |
非 LiveData | 用 MutableLiveData(DetailsPageType.Add) |
RecyclerView items 报错 |
不支持该属性 | Activity 中手动 rv.commonAdapter()?.refresh(data) |
| 资源引用不存在 | drawable/color/string 缺失 | 先检查资源是否存在,不存在则创建或用已有资源 |
View.VISIBLE 报错 |
未导入 | XML <data> 中加 <import type="android.view.View" /> |
textStyle DataBinding 报错 |
不支持 | 用固定值 android:textStyle="bold" |
开发检查清单
新页面开发必做
- 创建 Bean(如需)→ API 接口 → ViewHolder(列表页)→ ViewModel → Activity → 布局
- 在
app/src/main/AndroidManifest.xml注册 Activity(最易遗忘):
<activity android:name="com.lukouguoji.gjc.activity.XxxActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" android:screenOrientation="userLandscape" />
- 在
ARouterConstants注册路由(如需) - 标题栏统一用
<include layout="@layout/title_tool_bar" />,Activity 中setBackArrow("标题")
常见业务操作
扫码:
fun scanWaybill() { ScanModel.startScan(getTopActivity(), Constant.RequestCode.WAYBILL) }
// Activity.onActivityResult 中: waybillNo.value = data?.getStringExtra(Constant.Result.CODED_CONTENT)
图片上传: UploadUtil.upload(filePath) → result.data?.newName(注意是 newName 非 url)
刷新事件:
// 发送(ViewModel 中,必须在协程中)
viewModelScope.launch { FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh") }
// 接收(Activity 中,必须导入 observe)
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).observe(this) { viewModel.refresh() }
静态启动方法:
companion object {
@JvmStatic
fun start(context: Context, id: String) {
context.startActivity(Intent(context, XxxActivity::class.java).putExtra(Constant.Key.ID, id))
}
}
开发原则
- 资源引用必须存在 — 创建布局前确认 drawable/color/string 资源真实存在或主动创建
- 标题栏统一用
title_tool_bar— 禁止手动编写 Toolbar - 优先使用 PadDataLayoutNew 和 PadSearchLayout 组件
- 在每个页面布局时,如有截图,务必尽可能还原图片上的页面设计,而不是推测假想。如有困难一律要询问,禁止自己想象
- 工具栏图标尺寸规范:
img_search36dp + padding 2dp;img_add40dp 无 padding(使用drawable/img_add.xml矢量图,drawable-xhdpi/img_add.png已废弃删除) - 常用资源:
bg_white_radius_8、colorPrimary、text_normal、text_gray、color_bottom_layout
环境配置
- 服务器:
module_base/.../res/values/strings.xml中system_url_inner/weight_url - 签名:
key.jks(根目录),密码123321,别名key - 模块独立运行:
gradle.properties中isBuildModule=true
错误排查流程
- Import 错误 → 查上方 Import 速查表
- 资源引用错误 → 检查 drawable/color/string 是否存在
- DataBinding 错误 → 检查 import、枚举值、View 类导入
- suspend function 错误 → 在
viewModelScope.launch中调用 - 仍有问题 →
./gradlew clean后重新构建