# 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 ↓ 持有 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() { 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() val dataList = MutableLiveData>() // 适配器配置(在布局中使用) 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> ) : RecyclerView.Adapter>() // 数据管理 fun refresh(list: List?) // 刷新数据(清空后重新加载) fun loadMore(list: List?) // 加载更多(追加数据) 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(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 // 具体业务接口示例 // 列表查询(返回分页数据) @POST("DomExpCheckIn/search") suspend fun getGncShouYunList(@Body data: RequestBody): BaseListBean // 详情查询(返回单个对象) @POST("DomExpCheckIn/queryWbByNo") suspend fun getGncShouYunDetails(@Query("wbNo") wbNo: String): BaseResultBean // 新增/编辑/删除(返回成功标志) @POST("DomExpCheckIn/save") suspend fun saveGncShouYun(@Body data: RequestBody): BaseResultBean // 文件上传 @Multipart @POST("api/upload") suspend fun uploadFile(@Part file: MultipartBody.Part): BaseResultBean } ``` ### RequestKtx - 请求扩展函数 **文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ktx/RequestKtx.kt` #### (1) launchCollect - 无Loading请求 适用场景:后台刷新、非关键操作、不需要阻塞用户操作的请求 ```kotlin fun Any.launchCollect( block: suspend () -> T, resultBuilder: ResultBuilder.() -> 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 ILoading.launchLoadingCollect( block: suspend () -> T, resultBuilder: ResultBuilder.() -> 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 { var msg: String? = null // 消息提示 var status: String = "" // 状态码("1"=成功,其他=失败) var data: T? = null // 实际数据 fun verifySuccess(): Boolean { return status == "1" // 判断是否成功 } } ``` **BaseListBean - 分页返回**: ```kotlin class BaseListBean { var pages = 1 // 总页数 var total = 0 // 总数量 var list: ArrayList? = 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 // 下拉选项列表 required: Boolean // 是否必填(显示*号) icon: Int // 右侧图标资源ID enable: Boolean // 是否可编辑 refreshCallBack: () -> Unit // 刷新回调 onIconClickListener: (View) -> Unit // 图标点击回调 ``` **使用示例**: ```xml ``` #### 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 // 下拉选项列表 required: Boolean // 是否必填(显示*号) icon: Int // 右侧图标 enable: Boolean // 是否可编辑 inputHeight: Int // 多行输入高度 maxLength: Int // 最大长度 ``` **使用示例**: ```xml ``` #### 3. StatusView - 状态栏占位View **文件位置**: `module_base/src/main/java/com/lukouguoji/module_base/ui/weight/StatusView.kt` **功能**: 自动适配状态栏高度的占位View(用于沉浸式状态栏) **使用方式**: ```xml ``` ### 通用样式定义 #### 文本样式 (module_base/res/values/styles.xml) ```xml ``` #### 按钮样式 ```xml ``` #### 布局样式 ```xml ``` ### 颜色规范 (module_base/res/values/colors.xml) ```xml #FF1C8CF5 #0A80FC #1E1E1E #333333 #666666 #999999 #FF999999 #3CB5F3 #FFFFFFFF #FF000000 #d9001b #FFEDEDED #FFEDEDED #FFF6FBFF #FFEDEDED #F1F1F1 #5c6890 ``` ### Drawable背景规范 #### 输入框背景 ```xml ``` **使用说明**: - `bg_search_layout`: 搜索区域输入框专用(8dp圆角) - `bg_data_layout`: 数据展示区域输入框专用(4dp圆角) - `bg_input`: 通用输入框背景(白色+灰色边框+8dp圆角) #### 按钮背景 ```xml ``` **常用按钮背景**: - `@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 ``` 在Activity中设置标题: ```kotlin setBackArrow("页面标题") ``` ## DataBinding适配器 项目中提供了丰富的BindingAdapter,简化DataBinding使用。 ### 图片加载 (BindingAdapter.kt) ```xml ``` **支持的属性**: - `loadImage`: 图片地址(URL或本地路径) - `loadError`: 错误图片 - `loadPlaceholder`: 占位图 - `loadCircle`: 是否圆形(true/false) - `loadRadius`: 圆角(dp) - `loadWidth`/`loadHeight`: 指定宽高 ### View可见性 (ViewAdapter.kt) ```xml ``` ### TextView文本对齐 (TextViewAdapter.kt) ```xml ``` ### Spinner下拉框 (SpinnerAdapter.kt) ```xml ``` ### Shape动态背景 (ShapeBindingAdapter.kt) ```xml ``` ### EditText扩展 (EditTextKtx.kt) ```xml ``` ## 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(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") // 接收事件(在ViewModel中) FlowBus.with(ConstantEvent.EVENT_REFRESH_LIST).observe(this) { data -> // 处理事件 loadData() } // 粘性事件(先发送后接收也能收到) FlowBus.withSticky("user_info").emit(userBean) FlowBus.withSticky("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() { 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(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 ``` ### 详情页完整示例 #### Activity层 ```kotlin @Route(path = ARouterConstants.ACTIVITY_URL_GNC_STASH_DETAILS) class GncStashDetailsActivity : BaseBindingActivity() { 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() 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 ``` ### 新增/编辑页完整示例 #### Activity层 ```kotlin @Route(path = ARouterConstants.ACTIVITY_URL_CAR_ADD) class CarAddActivity : BaseBindingActivity() { 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>() 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(ConstantEvent.EVENT_REFRESH_CAR_LIST).emit("refresh") // 关闭页面 getTopActivity().finish() } } } } ``` #### Layout层 ```xml ``` ## 开发检查清单 ### 列表页开发清单(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 ``` **步骤3: 创建ViewHolder** ```kotlin // 位置: module_xxx/src/main/java/.../holder/ class XxxViewHolder(view: View) : BaseViewHolder(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() 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() { 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 ``` **步骤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 ``` **步骤2: 创建ViewModel** ```kotlin class XxxDetailsViewModel : BaseViewModel() { var id = "" val dataBean = MutableLiveData() 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() { 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 ``` ### 新增/编辑页开发清单(5步) **步骤1: 在Api中添加接口** ```kotlin @POST("api/xxx/save") suspend fun saveXxx(@Body data: RequestBody): BaseResultBean @POST("api/xxx/details") suspend fun getXxxDetails(@Query("id") id: String): BaseResultBean ``` **步骤2: 创建ViewModel** ```kotlin class XxxAddViewModel : BaseViewModel() { var pageType: DetailsPageType = DetailsPageType.Add var id = "" val dataBean = MutableLiveData(XxxBean()) val optionList = MutableLiveData>() 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(ConstantEvent.EVENT_REFRESH_LIST).emit("refresh") getTopActivity().finish() } } } } ``` **步骤3: 创建Activity(含多个静态start方法)** ```kotlin @Route(path = ARouterConstants.ACTIVITY_URL_XXX_ADD) class XxxAddActivity : BaseBindingActivity() { 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 ``` **步骤5: 注册路由并发送刷新事件** ```kotlin // 注册路由 const val ACTIVITY_URL_XXX_ADD = "/xxx/XxxAddActivity" // 在列表页接收刷新事件 FlowBus.with(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简化代码 - ✅ 利用扩展函数处理通用逻辑 - ✅ 不重复造轮子,保持架构一致性