杰理 SDK 文档中心
首页
首页
  • 项目概述

    • 杰理 OTA SDK 项目简介
    • 快速开始与接入指南
    • 工程结构与发布物
  • 核心库与依赖

    • OTA 核心库集成
    • 版本历史与更新说明
  • SDK 工具层

    • OTA 参数配置
    • 蓝牙扫描与连接管理
    • BLE 通道与事件回调
    • OTA 升级流程与状态模型
    • 固件文件管理与监听
  • 演示应用

    • 演示应用架构与主界面
    • 设备发现与连接界面
    • 文件选择与升级界面
    • 多设备 OTA 模型

演示应用架构与主界面

BroadcastBox 演示应用(com.jieli.broadcastbox 包)是 JL OTA Android SDK 的完整示例工程,采用「单 Activity + 多 Fragment + ViewModel」架构,通过底部导航承载连接设备、文件选择、固件升级三大核心页面,并串联 BLE 扫描/连接与多设备 OTA 能力。

目的与范围

本页面向读者完整讲解演示应用(Demo App)的应用层架构与主界面容器的实现:

  • 主界面容器 BroadcastBoxActivity:ViewPager2 + BottomNavigationView + Toolbar 的组合与联动逻辑;
  • 三个主页面(Tab)ConnectFragment、FilesFragment、UpgradeFragment 的组织方式;
  • ViewModel 层 BroadcastBoxViewModel 对 BLE 扫描/连接能力的封装与 LiveData 数据流;
  • 应用生命周期中 MultiOTAProcessor 的挂接与释放;
  • 并发、边界场景、扩展方式与相关资源配置。

以下主题属于 SDK 其他能力页面的范畴,本页只做引用、不展开:BLE 底层连接管理(BleManager)、多设备并行升级引擎(MultiOTAManager / MultiOTAProcessor / ReConnectHelper)、文件浏览与选择(FileOpViewModel)、OTA 升级状态机(UpgradeFragment / UpgradeInfo)。如需这些细节,请参见对应目录页。

概述

演示应用名为 BroadcastBox,其设计目标不是演示单机 OTA,而是演示 SDK 的多设备广播盒场景:一台 Android 设备作为"广播盒",通过 BLE 同时扫描、连接、管理多台耳机/音箱等外设,并批量执行固件升级。

为此,应用层选择了清晰的三层结构:

  1. UI 壳层:BroadcastBoxActivity 持有 ViewPager2,通过 FragmentStateAdapter 管理 3 个 Fragment,底部 BottomNavigationView 负责 Tab 切换,Toolbar 标题与菜单随当前 Tab 联动变化;
  2. 页面层:ConnectFragment(设备连接)、FilesFragment(升级文件选择)、UpgradeFragment(升级执行)三个页面各司其职;
  3. 状态层:BroadcastBoxViewModel 单例化持有 BleManager,把 BLE 扫描、连接事件通过 LiveData 暴露给 UI,保证配置变更(如旋转)时状态不丢失。

整个应用是 SDK 能力的"最小完整调用示范":UI 层只做展示与交互,业务状态全部下沉到 ViewModel 与 SDK 单例中。

架构总览

flowchart TD
    subgraph sg_UI["UI 层 (com.jieli.broadcastbox)"]
        Activity["BroadcastBoxActivity<br/>(extends BaseActivity)"]
        Nav["BottomNavigationView<br/>connect / files / upgrade"]
        Toolbar["Toolbar + Menu<br/>switch_function / choose_way"]
        Pager["ViewPager2<br/>+ FragmentStateAdapter"]
        FragConnect["ConnectFragment"]
        FragFiles["FilesFragment"]
        FragUpgrade["UpgradeFragment"]
    end

    subgraph sg_VM["ViewModel 层"]
        VM["BroadcastBoxViewModel<br/>(持有 BleManager 单例)"]
    end

    subgraph sg_SDK["OTA SDK 能力层"]
        Ble["BleManager (单例)"]
        MultiOTA["MultiOTAProcessor (单例)"]
        FileOp["FileOpViewModel"]
    end

    Activity --> Nav
    Activity --> Toolbar
    Activity --> Pager
    Pager --> FragConnect
    Pager --> FragFiles
    Pager --> FragUpgrade
    FragConnect --> VM
    FragFiles --> VM
    FragUpgrade --> MultiOTA
    VM --> Ble
    Activity -->|"onDestroy() 时 destroy()"| MultiOTA

各组件职责与连接关系:

  • BroadcastBoxActivity 是唯一的 Activity 入口,负责把三个 Fragment 注册进 ViewPager2,并同步 BottomNavigationView 与 Toolbar 状态。它继承自 SDK 的 BaseActivity(com.jieli.otasdk.ui.base.BaseActivity),复用 SDK 提供的基础能力。
  • ConnectFragment 与 FilesFragment 通过 BroadcastBoxViewModel 访问 BleManager:前者做扫描与连接,后者选择升级文件;UpgradeFragment 直接驱动 MultiOTAProcessor 执行多设备升级。
  • BroadcastBoxViewModel 在 init 块中向 BleManager 注册 BleEventCallback,把底层回调翻译成 MutableLiveData(bluetoothStateMLD、scanResultMLD、deviceConnectionMLD),UI 以观察者方式订阅。
  • MultiOTAProcessor 与应用同生命周期:Activity.onDestroy() 中调用其 destroy(),确保多设备升级任务与资源随页面退出而释放。

这种分层把"SDK 能力"与"示例 UI"解耦:替换任何一层(例如把 Fragment 换成 Compose 页面)都不会影响 SDK 侧的连接与升级逻辑。

主界面容器:BroadcastBoxActivity

单 Activity + 多 Fragment 的壳结构

BroadcastBoxActivity 是演示应用的唯一入口 Activity。它不承载具体业务,而是把三个页面以数组形式注册,交由 ViewPager2 + FragmentStateAdapter 统一管理:

Fragment[] fragments = new Fragment[]{new ConnectFragment(), new FilesFragment(), new UpgradeFragment()};

Source: BroadcastBoxActivity.java

onCreate() 中完成三件事:加载布局 R.layout.activity_broadcast_box,获取 BottomNavigationView(R.id.bnv_bottomBar)、Toolbar(R.id.toolbar)、标题 TextView(R.id.toolbar_title)与 ViewPager2(R.id.vp2_container)四个核心视图,然后初始化菜单与监听器:

BottomNavigationView bottomNav = findViewById(R.id.bnv_bottomBar);
bottomNav.setItemIconTintList(null);// 不隐藏显示不出原来的logo

Toolbar toolbar = findViewById(R.id.toolbar);
TextView tvTitle = findViewById(R.id.toolbar_title);
ViewPager2 viewPager2 = findViewById(R.id.vp2_container);
tvTitle.setText(getString(R.string.connect));
toolbar.inflateMenu(R.menu.menu_toolbar);
toolbar.getMenu().findItem(R.id.switch_function).setVisible(true);
toolbar.getMenu().findItem(R.id.choose_way).setVisible(false);

Source: BroadcastBoxActivity.java

注意 bottomNav.setItemIconTintList(null) 的注释:默认的 Material 底部导航会对图标施加着色,传 null 是为了保留原始 logo 颜色,这是演示应用刻意保留品牌图标的设计决策。

底部导航与页面联动

BottomNavigationView.OnItemSelectedListener 是页面切换的唯一驱动源。回调中依据菜单项标题(connect / files / upgrade 字符串资源)同时做三件事:更新 Toolbar 标题、切换 ViewPager2 当前页、动态调整两个菜单项的可见性:

bottomNav.setOnItemSelectedListener(item -> {
    JL_Log.i(tag, "getTitle=" + item.getTitle());
    if (item.getTitle().equals(getString(R.string.connect))) {
        tvTitle.setText(getString(R.string.connect));
        viewPager2.setCurrentItem(0, false);
        toolbar.getMenu().findItem(R.id.switch_function).setVisible(true);
        toolbar.getMenu().findItem(R.id.choose_way).setVisible(false);
    } else if (item.getTitle().equals(getString(R.string.files))) {
        tvTitle.setText(getString(R.string.files));
        toolbar.getMenu().findItem(R.id.switch_function).setVisible(false);
        toolbar.getMenu().findItem(R.id.choose_way).setVisible(true);
        viewPager2.setCurrentItem(1, false);
    } else if (item.getTitle().equals(getString(R.string.upgrade))) {
        tvTitle.setText(getString(R.string.upgrade));
        viewPager2.setCurrentItem(2, false);
        toolbar.getMenu().findItem(R.id.switch_function).setVisible(false);
        toolbar.getMenu().findItem(R.id.choose_way).setVisible(false);
    }
    return true;
});

Source: BroadcastBoxActivity.java

设计要点:

  • 菜单与页面一一绑定:switch_function(切换 SDK 功能入口)只在"连接"页显示;choose_way(选择文件方式)只在"文件"页显示;"升级"页两个菜单都不显示。这样避免菜单项在错误的页面上下文下被误触。
  • setCurrentItem(pos, false):第二个参数 false 表示禁用页面切换动画,配合 setUserInputEnabled(false)(见下文)实现"只能点底部导航、不能手势滑动"的确定性导航模型——因为 Toolbar 菜单的显隐依赖当前页,手势滑动会造成页面与菜单状态不同步。
  • 用 item.getTitle() 而非 position 判断:以资源字符串为契约,语义清晰且不依赖 Tab 顺序。

ViewPager2 的配置与适配器

viewPager2.setUserInputEnabled(false);
viewPager2.setOffscreenPageLimit(3);
viewPager2.setAdapter(new FragmentStateAdapter(this) {
    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return fragments[position];
    }

    @Override
    public int getItemCount() {
        return fragments.length;
    }
});

Source: BroadcastBoxActivity.java

  • setUserInputEnabled(false):禁止手势左右滑动切换页面,导航完全交给底部导航栏,保证页面状态(标题、菜单)始终一致;
  • setOffscreenPageLimit(3):三个页面全部预加载,牺牲少量内存换取切换零延迟——连接页的扫描状态、文件页的列表状态在切换时不会丢失;
  • 内联匿名 FragmentStateAdapter 按 position 返回预置的 fragments 数组元素,getItemCount() 直接返回数组长度,新增 Tab 只需改数组。

Toolbar 菜单回调

菜单项点击通过 Toolbar.OnMenuItemClickListener 分发:switch_function 弹出功能切换弹窗(switchPopupWindow),choose_way 转发给 FilesFragment.chooseFilePopupWindow 弹出来源选择(如本地文件/蓝牙文件):

toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
        Fragment fragment;
        View menuItemView;
        switch (item.getItemId()) {
            case R.id.switch_function:
                menuItemView = findViewById(R.id.switch_function);
                switchPopupWindow(BroadcastBoxActivity.this, menuItemView);
                break;
            case R.id.choose_way:
                fragment = fragments[1]; // FilesFragment
                menuItemView = findViewById(R.id.choose_way);
                if (fragment != null) {
                    ((FilesFragment) fragment).chooseFilePopupWindow(menuItemView);
                }
                break;
        }
        return false;
    }
});

Source: BroadcastBoxActivity.java

这里采用Activity 转发而非 Fragment 自处理的方式:菜单属于 Toolbar(Activity 持有),因此 Activity 作为中转,把"选择文件方式"事件转交给 FilesFragment 的公开方法,保持页面职责单一。

生命周期与资源释放

@Override
protected void onDestroy() {
    super.onDestroy();
    MultiOTAProcessor.Companion.getInstance().destroy();
}

Source: BroadcastBoxActivity.java

onDestroy() 中直接销毁 MultiOTAProcessor(Kotlin 伴生单例,通过 Companion.getInstance() 访问)。这意味着多设备升级引擎的生命周期与应用界面严格绑定:退出应用即停止所有升级队列与重连任务,避免后台残留。配合 BroadcastBoxViewModel.destroy()(反注册 BLE 回调、清空缓存列表),应用退出时不会泄漏 SDK 资源。

此外 Activity 上还有一个公开标志位 isSkipDestroyViewModel = false,供外部(如 MultiOTAProcessor 内部)标记"跳过销毁 ViewModel"的场景,用于进程保活或特殊恢复路径。

相关页面与对话框

围绕主容器,com.jieli.broadcastbox 包还包含一批支撑 UI 组件(本次文档仅列出其存在与职责,细节见各自目录页):

组件类型职责
ConnectFragment / FilesFragment / UpgradeFragmentFragment三个主页面
DeviceAdapter / UpgradeDeviceAdapter / UpgradeFileAdapter / UpgradeFilePickerAdapter / UpgradeProgressAdapterAdapter设备列表、升级设备列表、文件列表与进度展示
DialogUpgradeDevice / DialogUpgradeFilePicker / NotifyDialogDialog升级确认、文件选择、提示弹窗
BroadcastBoxInfo / UpgradeInfoModel设备广播信息与升级信息模型
model.ota.*(MultiOTAStart/Working/Reconnect/End/Stop/State)Model多设备 OTA 状态机事件模型

状态层:BroadcastBoxViewModel

对 BleManager 的封装

BroadcastBoxViewModel 继承自 androidx.lifecycle.ViewModel,是连接页与文件页共享的状态中枢。其核心思路是:单例 BleManager 负责真实蓝牙操作,ViewModel 负责状态翻译与缓存:

class BroadcastBoxViewModel : ViewModel() {
    private val tag = javaClass.simpleName
    private val bleManager: BleManager = BleManager.getInstance()

    val cacheAdvInfo: HashMap<String, BroadcastBoxInfo> = HashMap()
    val connectedBleDevices: MutableList<BroadcastBoxInfo> =
        Collections.synchronizedList(ArrayList())
    val selectedDeviceList: MutableList<BroadcastBoxInfo> =
        Collections.synchronizedList(ArrayList())
    var isFilterDevice = true
    var isAddObserver = false

    val bluetoothStateMLD = MutableLiveData<Boolean>()
    val scanResultMLD = MutableLiveData<ScanResult>()
    lateinit var deviceConnectionMLD: MutableLiveData<DeviceConnection>
    ...
    fun startScan() {
        if (!isScanning()) {
            bleManager.startLeScan(OtaConstant.SCAN_TIMEOUT)
        }
    }
}

Source: BroadcastBoxViewModel.kt

设计意图拆解:

  • BleManager 单例注入:SDK 的 BleManager 全局唯一,ViewModel 通过 getInstance() 获取,保证多个页面看到同一份连接状态;ViewModel 只做薄封装,不复制底层状态。
  • 三个同步容器:cacheAdvInfo(MAC 地址 → 广播信息缓存,用于去重/快速查找)、connectedBleDevices(已连接设备)、selectedDeviceList(用户勾选待升级设备)。后两者使用 Collections.synchronizedList,因为 BLE 回调可能来自不同线程,直接读写 ArrayList 会引发 ConcurrentModificationException。
  • 三个 LiveData:bluetoothStateMLD(蓝牙开关状态)、scanResultMLD(扫描结果事件)、deviceConnectionMLD(连接状态事件)。UI 通过 observe 订阅,自动获得生命周期感知。
  • startScan() 的幂等保护:先查 isScanning() 再启动,避免重复发起扫描;超时时间取自 SDK 常量 OtaConstant.SCAN_TIMEOUT,保证与 SDK 行为一致。

BLE 事件回调 → LiveData 数据流

ViewModel 在 init 块中注册回调、并在 destroy() 中反注册,回调对象把四类底层事件逐一翻译成 LiveData 值:

private val bleEventCallback = object : BleEventCallback() {

    override fun onAdapterChange(bEnabled: Boolean) {
        bluetoothStateMLD.value = bEnabled
    }

    override fun onDiscoveryBleChange(bStart: Boolean) {
        val result = if (bStart) {
            ScanResult(ScanResult.SCAN_STATUS_SCANNING)
        } else {
            ScanResult(ScanResult.SCAN_STATUS_IDLE)
        }
        scanResultMLD.postValue(result)
    }

    override fun onDiscoveryBle(device: BluetoothDevice?, bleScanMessage: BleScanInfo?) {
        val result = ScanResult(
            ScanResult.SCAN_STATUS_FOUND_DEV,
            device?.let {
                val data = bleScanMessage?.rawData ?: ByteArray(0)
                ScanDevice(it, bleScanMessage?.rssi ?: 0, data)
            })
        scanResultMLD.value = result
    }

    override fun onBleConnection(device: BluetoothDevice?, status: Int) {
        deviceConnectionMLD.value = DeviceConnection(device, status)
    }
}

init {
    deviceConnectionMLD = MutableLiveData<DeviceConnection>()
    bleManager.registerBleEventCallback(bleEventCallback)
}

Source: BroadcastBoxViewModel.kt

事件翻译规则(UI 层只认识这些语义):

底层事件翻译结果
onAdapterChange(bEnabled)bluetoothStateMLD.value = bEnabled(蓝牙开关状态)
onDiscoveryBleChange(true)scanResultMLD ← SCAN_STATUS_SCANNING
onDiscoveryBleChange(false)scanResultMLD ← SCAN_STATUS_IDLE
onDiscoveryBle(device, info)scanResultMLD ← SCAN_STATUS_FOUND_DEV + ScanDevice(device, rssi, rawData)
onBleConnection(device, status)deviceConnectionMLD ← DeviceConnection(device, status)

值得注意的细节:onDiscoveryBleChange 使用 postValue(可在后台线程调用),而 onDiscoveryBle、onBleConnection 使用 value(要求主线程)——这反映了 SDK 回调线程模型的差异,也是使用 LiveData 时常见的线程语义陷阱。

destroy() 负责反注册回调并清空三个容器,确保 ViewModel 清理时不会继续收到 BLE 事件造成泄漏。

BLE 扫描数据流

sequenceDiagram
    participant Frag as ConnectFragment
    participant VM as BroadcastBoxViewModel
    participant Ble as BleManager (SDK 单例)
    participant CB as BleEventCallback

    Frag->>VM: startScan()
    VM->>VM: isScanning()? 防重复
    VM->>Ble: startLeScan(OtaConstant.SCAN_TIMEOUT)
    Ble-->>CB: onDiscoveryBleChange(true)
    CB->>VM: scanResultMLD.postValue(SCANNING)
    Ble-->>CB: onDiscoveryBle(device, info)
    CB->>VM: scanResultMLD.value(FOUND_DEV + ScanDevice)
    VM-->>Frag: observe(scanResultMLD) 刷新列表
    Frag->>VM: connectBle(device)
    VM->>Ble: connectBleDevice(device)
    Ble-->>CB: onBleConnection(device, status)
    CB->>VM: deviceConnectionMLD.value(DeviceConnection)
    VM-->>Frag: observe(deviceConnectionMLD) 更新连接态

说明:ConnectFragment 通过 viewModel() 获取同一 ViewModel 实例;扫描与连接事件均以 LiveData 单向流动,UI 不反向调用 SDK,符合 MVVM 单向数据流。

API 参考

BroadcastBoxViewModel(公开成员)

成员签名说明
cacheAdvInfoHashMap<String, BroadcastBoxInfo>MAC 地址 → 广播信息缓存;findCacheAdvMessage 的查找源
connectedBleDevicesMutableList<BroadcastBoxInfo>已连接设备(线程安全同步列表)
selectedDeviceListMutableList<BroadcastBoxInfo>勾选待升级设备(线程安全同步列表)
bluetoothStateMLDMutableLiveData<Boolean>蓝牙开关状态
scanResultMLDMutableLiveData<ScanResult>扫描事件(SCANNING/IDLE/FOUND_DEV)
deviceConnectionMLDMutableLiveData<DeviceConnection>设备连接事件
startScan()Unit非扫描状态下调用 bleManager.startLeScan(SCAN_TIMEOUT)
stopScan()Unit调用 bleManager.stopLeScan()
isConnectedDevice(device)Boolean查询设备是否已连接
getConnectedDevices()MutableList<BluetoothDevice>返回 bleManager.connectedDeviceList
connectBle(device)Unit调用 bleManager.connectBleDevice(device)
disconnectBle(device)Unit调用 bleManager.disconnectBleDevice(device)
findCacheAdvMessage(address)BroadcastBoxInfo?先 BluetoothAdapter.checkBluetoothAddress 校验地址,再从缓存查询
destroy()Unit反注册回调、清空三个容器

BroadcastBoxActivity(公开成员)

成员签名说明
isSkipDestroyViewModelboolean供外部标记是否跳过 ViewModel 销毁(默认 false)
fragmentsFragment[]三个主页面实例数组,ViewPager2 数据源
onCreate(Bundle)void初始化视图、菜单、底部导航与 ViewPager2
onDestroy()void销毁 MultiOTAProcessor 单例

核心交互流程:Tab 切换

sequenceDiagram
    participant User as 用户
    participant Nav as BottomNavigationView
    participant Act as BroadcastBoxActivity
    participant Pager as ViewPager2
    participant Frag as 目标 Fragment

    User->>Nav: 点击底部 Tab(connect/files/upgrade)
    Nav->>Act: onItemSelected(item)
    Act->>Act: JL_Log 记录选中项标题
    Act->>Act: 更新 Toolbar 标题(tvTitle)
    Act->>Act: 更新菜单可见性(switch_function / choose_way)
    Act->>Pager: setCurrentItem(position, false) 无动画切换
    Pager->>Frag: 显示对应 Fragment(已预加载,OffscreenPageLimit=3)
    Act-->>Nav: 返回 true(消费事件)

为什么切换是确定性的: 由于 setUserInputEnabled(false) 禁用了手势滑动,页面永远只能通过底部导航切换;切换瞬间 Activity 同步完成"标题 + 菜单 + 页面"三态更新,不存在手势滑动导致的"页面已变、菜单未变"的中间状态。setOffscreenPageLimit(3) 保证三个 Fragment 常驻内存,连接页的扫描、文件页的选择结果在切换后原样保留。

配置与资源

以下资源 ID 与行为均从 BroadcastBoxActivity 源码中的使用方式直接可见(资源具体内容位于 res/layout、res/menu、res/values 目录,未在本页逐一展开):

资源用途使用点
R.layout.activity_broadcast_box主界面布局setContentView 加载
R.id.bnv_bottomBar底部导航栏Tab 切换事件源
R.id.toolbar / R.id.toolbar_title标题栏与标题文本随 Tab 联动更新
R.id.vp2_containerViewPager2 容器承载三个 Fragment
R.menu.menu_toolbarToolbar 菜单switch_function、choose_way 两项
R.id.switch_function切换 SDK 功能菜单仅"连接"页可见;触发 switchPopupWindow
R.id.choose_way选择文件方式菜单仅"文件"页可见;转发给 FilesFragment.chooseFilePopupWindow
R.string.connect / R.string.files / R.string.upgrade三个 Tab 的标题契约底部导航判断与 Toolbar 标题共用

菜单显隐规则可归纳为一张状态表:

当前 TabToolbar 标题switch_functionchoose_way
connect(连接)connect✅ 可见❌ 隐藏
files(文件)files❌ 隐藏✅ 可见
upgrade(升级)upgrade❌ 隐藏❌ 隐藏

并发、边界与失败模式

  • 多线程列表访问:connectedBleDevices 与 selectedDeviceList 使用 Collections.synchronizedList 包装,抵御 BLE 回调线程与 UI 线程的并发读写。若在 UI 层遍历这些列表,仍应复制或加锁,避免迭代期间被回调线程修改。
  • LiveData 线程语义:onDiscoveryBleChange 用 postValue(后台线程安全),onDiscoveryBle / onBleConnection 用 value(需主线程)。若 SDK 回调线程模型变化导致 value 在非主线程调用,会抛 IllegalStateException——这是该封装最脆弱的边界。
  • 重复扫描防护:startScan() 先判断 isBleScanning,防止并发调用导致多次 startLeScan;stopScan() 无状态保护,重复调用由 SDK 侧容错。
  • 非法地址查询:findCacheAdvMessage 先执行 BluetoothAdapter.checkBluetoothAddress(address),非法地址(如空串、格式错误)直接返回 null,避免 HashMap 无效键查询与后续蓝牙 API 崩溃。
  • 生命周期释放:onDestroy 同步销毁 MultiOTAProcessor;BroadcastBoxViewModel.destroy() 反注册 BleEventCallback 并清空缓存。若 Activity 被系统重建(如旋转),ViewModel 因独立于配置而存活,但 isSkipDestroyViewModel 标志提供了"跳过销毁"的逃生通道,需谨慎使用以免泄漏。
  • 扫描结果的三态模型:ScanResult 用 SCAN_STATUS_SCANNING / SCAN_STATUS_IDLE / SCAN_STATUS_FOUND_DEV 表达扫描生命周期;UI 应同时订阅 scanResultMLD 与 bluetoothStateMLD,在蓝牙关闭时(onAdapterChange(false))提示用户并停止扫描逻辑。

性能与运行注意

  • setOffscreenPageLimit(3) 使三个 Fragment 全部常驻内存:换来切换零延迟与状态保留,代价是内存占用约三份页面实例,适合演示应用;生产应用可按需降低该值。
  • BleManager 与 MultiOTAProcessor 均为 SDK 单例:整个应用只有一份连接与升级状态,避免多页面各自维护导致的连接冲突;代价是必须严格配对 destroy(),否则跨页面残留。
  • 扫描超时由 OtaConstant.SCAN_TIMEOUT 统一控制,应用层不重复定义超时值,保证与 SDK 的扫描窗口一致。
  • 菜单项每次 Tab 切换都会执行 findItem().setVisible(),属于轻量 UI 操作,无性能风险;JL_Log 打印 Tab 标题便于联调时定位切换时序。

扩展点

  1. 新增 Tab:修改 fragments 数组加入第 4 个 Fragment,同步在底部导航与 FragmentStateAdapter.getItemCount 自动生效;再在 onItemSelectedListener 与菜单显隐分支中补充对应分支。
  2. 自定义导航方式:由于页面切换统一收敛到 viewPager2.setCurrentItem(pos, false),替换底部导航(如改为侧边栏)只需修改 onItemSelectedListener 一处。
  3. 替换状态层:BroadcastBoxViewModel 对 BleManager 的封装是唯一入口,若改用其他连接方案(如经典蓝牙、双模),只需替换该 ViewModel 内部实现,Fragment 侧订阅的 LiveData 契约不变。
  4. 菜单行为扩展:switchPopupWindow 与 FilesFragment.chooseFilePopupWindow 是两个独立入口,可在 menu_toolbar.xml 中新增菜单项并按同样模式在 onMenuItemClick 分发。
  5. 多设备升级接入:UpgradeFragment 直接驱动 MultiOTAProcessor 单例(其内部为 MultiOTAManager + ReConnectHelper 组合),业务方可在自己的 Activity 中复用同一模式:onCreate 获取单例、onDestroy 调用 destroy()。

相关链接

  • BroadcastBoxActivity.java(主界面容器)
  • BroadcastBoxViewModel.kt(状态层 / BLE 封装)
  • 相关能力页面(本页不展开):多设备升级引擎(MultiOTAManager / MultiOTAProcessor / ReConnectHelper)、BLE 连接管理(BleManager)、文件浏览(FileOpViewModel / FilesFragment)、升级流程(UpgradeFragment / UpgradeInfo)
  • 相关 UI 组件源码:ConnectFragment.kt、FilesFragment.kt、UpgradeFragment.java、DeviceAdapter.kt、DialogUpgradeDevice.java 等均位于 code/JL_OTA_Android_V1.9.0_SDK_V1.11.0/otasdk/src/main/java/com/jieli/broadcastbox/ 目录
Next
设备发现与连接界面