Files
aerologic-app/CLAUDE.md
2026-03-18 20:54:13 +08:00

31 KiB
Raw Blame History

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 基类: BaseActivityBaseBindingActivityDataBindingBaseViewModelBasePageViewModel(分页列表)
  • 适配器: CommonAdapter + BaseViewHolder 统一列表封装
  • 路由: ARouter 1.5.2 | 事件: FlowBusFlow+ EventBus 3.1.1
  • 网络: Retrofit 2.6.1 + OkHttp 3.12.12 + Coroutines
    • launchCollect:无 Loading 后台请求
    • launchLoadingCollect:带 Loading 请求
    • toRequestBodyMap/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.ktGjcBoxWeighingViewModel.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>

        <!-- 第二行 KVmarginTop=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,尺寸 30x30dplayout_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"运单号:"=4row2"特码:"=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.xmlitem_gjc_box_weighing.xml


类型 2多选列表 + 批量操作页

代表: IntExpOutHandoverActivity / GjcAssembleAllocateActivity 结构: 类型 1 基础上 + 全选按钮 + ObservableBoolean 选中状态 + 批量操作

与类型 1 的区别:

  1. Bean 增加 ObservableBoolean:
class XxxBean {
    val checked: ObservableBoolean = ObservableBoolean(false)
    var isSelected: Boolean
        get() = checked.get()
        set(value) = checked.set(value)
}
  1. 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() }
    }
}
  1. Activity 观察全选图标:
viewModel.isAllChecked.observe(this) { binding.checkIcon.alpha = if (it) 1.0f else 0.5f }
  1. Item 布局图片切换:
<ImageView android:id="@+id/iv_icon"
    loadImage="@{bean.checked.get() ? @drawable/img_plane_s : @drawable/img_plane}" />
  1. ViewHolder 中处理点击:
binding.ivIcon.setOnClickListener {
    bean.checked.set(!bean.checked.get())
    binding.executePendingBindings()
}

参考文件: module_gjc/.../IntExpOutHandoverActivity.ktIntExpOutHandoverViewModel.kt


类型 3嵌套多选列表页

代表: IntExpStorageUseActivity 结构: 主列表含子列表(展开/收起)+ 主子联动全选 + Dialog 操作

与类型 2 的区别:

  1. Activity 暴露给布局(用于调用 Dialog 方法):
binding.activity = this  // XML 中可调用 activity.showXxxDialog()
  1. 联动全选(主+子列表):
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) }
    }
}
  1. 全局展开/收起:
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.ktIntExpStorageUseViewModel.kt


类型 4Tab 详情页

代表: 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.ktGjcQueryDetailsViewModel.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.ktGjcQueryEditViewModel.kt


类型 6添加表单页含输入回调

代表: GjcBoxWeighingAddActivity 结构: 类型 5 基础上 + setRefreshCallBack 输入完成回调 + 实时计算 + 扫码自动填充

与类型 5 的区别:

  1. 输入完成回调(关键特性):
<!-- 使用方法引用,不能用 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)  // 输入完成后自动查询
    }
}
  1. 级联查询(航班日期+航班号同时有值时查询):
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) }
    }
}
  1. 实时计算:
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"
    }
}
  1. 输入限制:
// Activity 中设置
binding.carIdInput.et.setUpperCaseAlphanumericFilter()
  1. 重置功能:
fun resetClick() {
    dataBean.value = XxxBean()
    carId.value = ""; uldNo.value = ""; flightNo.value = ""
    lastQueriedCarId = ""; lastQueriedUld = ""; lastQueriedFlight = ""
}

参考文件: module_gjc/.../GjcBoxWeighingAddActivity.ktGjcBoxWeighingAddViewModel.kt


自定义 Dialog 开发模式

基于 BaseDialogModelXPopup 封装),支持 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.ktmodule_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 关键规则

  1. lifecycleOwner 必须设置BaseBindingActivity 已自动设置,手动使用时 binding.lifecycleOwner = this
  2. 字符串拼接用反引号: @{ + ` + 姓名: + ` + + viewModel.name}
  3. LiveData 自动解包: XML 中直接 viewModel.dataBean.name,不写 .value
  4. 修改对象属性需重新赋值: dataBean.value = dataBean.value?.copy(name = "新值")
  5. 双向绑定用 @={}: value="@={viewModel.searchText}"
  6. 点击事件用 Lambda: onClick="@{() -> viewModel.submit()}"
  7. setRefreshCallBack 用方法引用: setRefreshCallBack="@{viewModel::methodName}"(不能用 Lambda
  8. 使用 View.VISIBLE/GONE 必须导入: <import type="android.view.View" />
  9. 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
observeFlowBus 扩展) 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 包下)

launchCollectlaunchLoadingCollectshowToasttoRequestBodyverifyNullOrEmptynoNullformatDate


常见编译错误速查

错误 原因 修复
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"

开发检查清单

新页面开发必做

  1. 创建 Bean如需→ API 接口 → ViewHolder列表页→ ViewModel → Activity → 布局
  2. app/src/main/AndroidManifest.xml 注册 Activity(最易遗忘):
<activity android:name="com.lukouguoji.gjc.activity.XxxActivity"
    android:configChanges="orientation|keyboardHidden"
    android:exported="false" android:screenOrientation="userLandscape" />
  1. ARouterConstants 注册路由(如需)
  2. 标题栏统一用 <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(注意是 newNameurl

刷新事件:

// 发送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_search 36dp + padding 2dpimg_add 40dp 无 padding使用 drawable/img_add.xml 矢量图,drawable-xhdpi/img_add.png 已废弃删除)
  • 常用资源: bg_white_radius_8colorPrimarytext_normaltext_graycolor_bottom_layout

环境配置

  • 服务器: module_base/.../res/values/strings.xmlsystem_url_inner / weight_url
  • 签名: key.jks(根目录),密码 123321,别名 key
  • 模块独立运行: gradle.propertiesisBuildModule=true

错误排查流程

  1. Import 错误 → 查上方 Import 速查表
  2. 资源引用错误 → 检查 drawable/color/string 是否存在
  3. DataBinding 错误 → 检查 import、枚举值、View 类导入
  4. suspend function 错误 → 在 viewModelScope.launch 中调用
  5. 仍有问题 → ./gradlew clean 后重新构建