Android Jetpack Compose 中的分页与缓存展示

news2024/10/7 15:22:40

Android Jetpack Compose 中的分页与缓存展示

在几乎任何类型的移动项目中,移动开发人员在某个时候都会处理分页数据。如果数据列表太大,无法一次从服务器检索完毕,这就是必需的。因此,我们的后端同事为我们提供了一个端点,返回分页数据列表,并期望我们知道如何在客户端处理它。
在本文中,我们将重点介绍如何使用 Android 在 2023 年 6 月推荐的最新方法来获取、缓存和显示分页数据。我们将经过以下步骤:

  • 从公共 GraphQL API 中按页获取 Pokemon 数据列表
  • 使用 Room 将获取的数据缓存到本地数据库
  • 使用最新的 Paging 库组件来处理分页
  • 使用 LazyColumn 智能地显示页面项(只渲染可见内容)

对于示例项目,我将在文章末尾分享 GitHub 存储库链接,我们将使用 Hilt 作为我们的依赖注入库,并使用干净架构(表示层 → 领域层 ← 数据层)。因此,我将从数据层开始解释事物,然后转向领域层,最后结束在表示层。

数据层

这一层是关于分页和缓存的大部分内容。因此,如果您能够通过这一部分,您将基本完成了它。

远程数据源

作为远程数据源,我们将使用一个公共的 GraphQL Pokemon API。与我们用于与 REST API 交互的 Retrofit 不同,我们使用 Apollo 的 Kotlin 客户端来处理 GraphQL API。它允许我们执行 GraphQL 查询,并根据请求和响应自动生成 Kotlin 模型。
首先,我们需要将以下行添加到我们的模块级别的 build.gradle 文件中:

plugins {
    // ...
    id "com.apollographql.apollo3" version "$apollo_version"
}

apollo {
    service("pokemon") {
        packageName.set("dev.thunderbolt.pokemonpager.data")
    }
}

dependencies {
    // ...
    implementation "com.apollographql.apollo3:apollo-runtime:$apollo_version"
}

在这里,我们在 apollo 块中设置了 Apollo 库的配置。它提供了许多设置,您可以通过其文档查看所有设置。目前,我们只需要将包名设置为 dev.thunderbolt.pokemonpager.data,这样生成的 Kotlin 文件将位于正确的包中,也就是数据层。

然后,我们需要下载服务器的模式,以便库能够生成模型,并且我们可以使用自动完成来编写查询。为了下载模式,我们使用 Apollo 提供的以下命令:

./gradlew :app:downloadApolloSchema --endpoint='https://graphql-pokeapi.graphcdn.app/graphql' --schema=app/src/main/graphql/schema.graphqls

这将在 app/src/main/graphql/schema.graphqls 目录中下载服务器的模式。

现在,是时候在一个名为 pokemon.graphql 的文件中编写我们的查询,该文件与模式文件位于同一文件夹中。

query PokemonList(
    $offset: Int!
    $limit: Int!
) {
    pokemons(
        offset: $offset,
        limit: $limit
    ) {
        nextOffset
        results {
            id
            name
            image
        }
    }
}

当我们构建项目时,Apollo Kotlin 将通过自动运行名为 generateApolloSources 的 Gradle 任务为此查询生成模型。

回到 Kotlin 的世界,我们将定义我们的 PokemonApi 类,以封装与 GraphQL 的所有交互,如下所示:

class PokemonApi {

    private val BASE_URL = "https://graphql-pokeapi.graphcdn.app/graphql"

    private val apolloClient = ApolloClient.Builder()
        .serverUrl(BASE_URL)
        .addHttpInterceptor(LoggingInterceptor())
        .build()

    suspend fun getPokemonList(offset: Int, limit: Int): PokemonListQuery.Pokemons? {
        val response = apolloClient.query(
            PokemonListQuery(
                offset = offset,
                limit = limit,
            )
        ).execute()
        // IF RESPONSE HAS ERRORS OR DATA IS NULL, THROW EXCEPTION
        if (response.hasErrors() || response.data == null) {
            throw ApolloException(response.errors.toString())
        }
        return response.data!!.pokemons
    }
}

在这里,我们使用所需的配置初始化 Apollo Client 实例,并实现了我们执行在 pokemon.graphql 文件中编写的生成的 Kotlin 版本查询的函数。该函数基本上会获取 offsetlimit 参数,执行查询,如果一切顺利,就会返回查询的响应,这也是由 Apollo 自动生成的。

本地数据源/存储

为了在本地存储关系型数据并创建一个离线优先的应用程序,我们将依赖于 Room,这是一个在 SQLite 之上编写的 Android 持久性库。

首先,我们需要将 Room 依赖项添加到我们的 build.gradle 文件中:

dependencies {
    // ...
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-paging:$room_version"
}

然后,我们将定义两个实体类,一个用于在我们的数据库中存储 Pokemon 数据,另一个用于跟踪要获取的下一页的页数。

@Entity("pokemon")
data class PokemonEntity(
    @PrimaryKey val id: Int,
    val name: String,
    val imageUrl: String,
)

@Entity("remote_key")
data class RemoteKeyEntity(
    @PrimaryKey val id: String,
    val nextOffset: Int,
)

在这方面,我们还需要两个 DAO(数据访问对象)类来定义其中的所有数据库交互。

@Dao
interface PokemonDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(items: List<PokemonEntity>)

    @Query("SELECT * FROM pokemon")
    fun pagingSource(): PagingSource<Int, PokemonEntity>

    @Query("DELETE FROM pokemon")
    suspend fun clearAll()
}

@Dao
interface RemoteKeyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(item: RemoteKeyEntity)

    @Query("SELECT * FROM remote_key WHERE id = :id")
    suspend fun getById(id: String): RemoteKeyEntity?

    @Query("DELETE FROM remote_key WHERE id = :id")
    suspend fun deleteById(id: String)
}

在这里,我们需要特别关注的关键函数是 pagingSource()。Room 可以返回数据列表作为 PagingSource,以便我们稍后将创建的 Pager 对象将其用作生成 PagingData 流的单一源。

最后,我们需要一个 RoomDatabase 类,在本地数据库中为这些实体创建表,并提供 DAO 以与这些表进行交互。

@Database(
    entities = [PokemonEntity::class, RemoteKeyEntity::class],
    version = 1,
)
abstract class PokemonDatabase : RoomDatabase() {
    abstract val pokemonDao: PokemonDao
    abstract val remoteKeyDao: RemoteKeyDao
}

这两个类,即 PokemonDatabase 和之前定义的 PokemonApi 类,都由我们数据层的 Hilt 模块实例化并提供为单例对象。

@Module
@InstallIn(SingletonComponent::class)
class DataModule {

    @Provides
    @Singleton
    fun providePokemonDatabase(@ApplicationContext context: Context): PokemonDatabase {
        return Room.databaseBuilder(
            context,
            PokemonDatabase::class.java,
            "pokemon.db",
        ).fallbackToDestructiveMigration().build()
    }

    @Provides
    @Singleton
    fun providePokemonApi(): PokemonApi {
        return PokemonApi()
    }

    // ...
}

远程中介器(Remote Mediator)

现在,我们要实现我们的远程中介器类(RemoteMediator),它将负责在需要时从远程 API 加载分页数据到本地数据库中。需要注意的是,远程中介器并不直接向用户界面提供数据。如果分页数据用尽,分页库会触发远程中介器的 load(…) 方法,以从远程获取并存储更多的数据到本地。因此,我们的本地数据库始终可以保持作为唯一的真实数据源。

load(…) 函数中,我们首先需要检查我们正在处理哪种类型的加载。如果 LoadType 是:

  • REFRESH,这意味着我们要么处于初始加载状态,要么数据已经无效,我们需要从头开始获取数据。因此,如果是这种情况,我们将偏移值设置为 “0”,以获取第一页的数据。
  • PREPEND,我们需要获取当前页面之前的页面数据。在这个示例的范围内,不需要在向上滚动时获取任何内容。因此,我们只需返回 MediatorResult.Success(endOfPaginationReached = true),以指示不应再进行数据加载。
  • APPEND,我们需要获取当前页面之后的页面数据。在这种情况下,我们会获取已经由前一个数据加载存储在本地数据库中的远程键(remote key)对象。如果没有或者其 nextOffset 值为 “0”,则表示没有更多数据可加载和追加。顺便说一下,这就是该 API 的工作方式。你的 API 可能以不同方式指示数据的结束,因此需要相应地编写你的 APPEND 逻辑。

在确定了正确的偏移值之后,现在是时候使用此偏移值和配置中提供的 pageSize 进行 API 调用了。我们将在下一步创建 Pager 对象时设置页面大小。

如果 API 调用成功返回新的页面数据,我们将使用相应的 DAO 函数将项目和下一个偏移值存储在我们的数据库中。在这里,我们需要在事务块中执行所有数据库交互,以便如果任何交互失败,数据库不会发生任何更改。

最后,如果在数据库调用之后一切顺利,我们将返回 MediatorResult.Success,通过将最新加载返回的项目数与我们将在配置中定义的页面大小进行比较,来检查是否已达到分页的末尾。

Pager 对象

现在,我们要再次回到我们数据层的 Hilt 模块,并创建我们的 Pager 对象。这个对象将把我们到目前为止所定义的所有内容整合在一起,作为 PagingData 流的构造函数工作。

@Module
@InstallIn(SingletonComponent::class)
class DataModule {

    // ...

    @Provides
    @Singleton
    fun providePokemonPager(
        pokemonDatabase: PokemonDatabase,
        pokemonApi: PokemonApi,
    ): Pager<Int, PokemonEntity> {
        return Pager(
            config = PagingConfig(pageSize = 20),
            remoteMediator = PokemonRemoteMediator(
                pokemonDatabase = pokemonDatabase,
                pokemonApi = pokemonApi,
            ),
            pagingSourceFactory = {
                pokemonDatabase.pokemonDao.pagingSource()
            },
        )
    }
}

在这里,我们向 Pager 的构造函数提供了三个要素。首先,我们设置了所需的页面大小的 PagingConfig,正如我之前提到的。其次,我们提供了我们的远程中介器实例。第三,我们将由 Room 提供的分页源设置为 Pager 的唯一数据源。

仓库(Repository)

由于我们在远程中介器中完成了大部分工作,所以我们的仓库实现将相当简单。

class PokemonRepositoryImpl @Inject constructor(
    private val pokemonPager: Pager<Int, PokemonEntity>
) : PokemonRepository {

    override fun getPokemonList(): Flow<PagingData<Pokemon>> {
        return pokemonPager.flow.map { pagingData ->
            pagingData.map { it.toPokemon() }
        }
    }
}

使用我们的 Pager 实例,我们只需将其 PagingData 流返回给使用者。但在这之前,我们还需要将 PokemonEntity 映射到领域的 Pokemon 模型。这是因为根据 Clean Architecture 的基础,我们的领域层不了解数据或表示层,因此不应将数据模型传递到领域层。

领域层(Domain Layer)

在这个纯 Kotlin 层中,实际上没有太多事情发生。在这里,我们有我们的 Pokemon 模型、仓库接口以及与该仓库交互的简单用例类。

// REPOSITORY INTERFACE
interface PokemonRepository {
    fun getPokemonList(): Flow<PagingData<Pokemon>>
}

// USE CASE
class GetPokemonList @Inject constructor(
    private val pokemonRepository: PokemonRepository
) {
    operator fun invoke(): Flow<PagingData<Pokemon>> {
        return pokemonRepository.getPokemonList()
            .flowOn(Dispatchers.IO)
    }
}

// MODEL
data class Pokemon(
    val id: Int,
    val name: String,
    val imageUrl: String,
)

在这里,你可能会有一个问题,即如何在纯 Kotlin 层中使用PagingData,而在这里我们没有依赖于任何 Android 组件。实际上很简单:分页库为非 Android 模块提供了特定的依赖项,因此我们可以访问所有简单的 Paging 组件,如 PagingSource、PagingData、Pager,甚至是 RemoteMediator

dependencies {
    // ...
    implementation "androidx.paging:paging-common:$paging_version"
}

表示层(Presentation Layer)
在快速涵盖了领域层之后,让我们直接跳入表示层,其中的关键内容都在这里。但首先,我们需要将以下 Paging 依赖项添加到我们的 build.gradle 文件中:

dependencies {
    // ...
    implementation "androidx.paging:paging-runtime-ktx:$paging_version"
    implementation "androidx.paging:paging-compose:$paging_version"
}

除了 runtime-ktx 依赖项之外,这里还需要 compose 依赖项,因为它在我们的分页数据流和 UI 之间提供了一些中间件。

ViewModel

这又是本文中的一个简单类,在这里我们只需获取由用例提供的流(该流已由仓库提供),并将其存储在一个值中。

@HiltViewModel
class PokemonListViewModel @Inject constructor(
    private val getPokemonList: GetPokemonList
) : ViewModel() {

    val pokemonPagingDataFlow: Flow<PagingData<Pokemon>> = getPokemonList()
        .cachedIn(viewModelScope)
}

我们通过调用cachedIn(viewModelScope)来存储该流,以便在 ViewModel 的生命周期内保持其活动状态。此外,它还可以在屏幕旋转等配置更改时保持存活,这样你就可以获取相同的现有数据,而不必从头开始获取。
这种方法还可以保持我们的冷流状态不变,并且不会像 stateIn(…) 方法一样将其转换为热流(StateFlow)。这意味着如果流未被收集,就不会执行不必要的代码。

屏幕(UI)

现在,我们来到了分页的最后一步,在这一步中,我们将在LazyColumn中显示我们的分页项。在 Jetpack Compose 中,不再有 RecyclerView 或适配器。所有这些都在下面进行处理,而且我们大量的项目仍然可以智能布局,而不会引起任何性能问题。

@Composable
fun PokemonListScreen(
    snackbarHostState: SnackbarHostState
) {
    val viewModel = hiltViewModel<PokemonListViewModel>()
    val pokemonPagingItems = viewModel.pokemonPagingDataFlow.collectAsLazyPagingItems()

    if (pokemonPagingItems.loadState.refresh is LoadState.Error) {
        LaunchedEffect(key1 = snackbarHostState) {
            snackbarHostState.showSnackbar(
                (pokemonPagingItems.loadState.refresh as LoadState.Error).error.message ?: ""
            )
        }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        if (pokemonPagingItems.loadState.refresh is LoadState.Loading) {
            CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
        } else {
            LazyColumn(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                items(
                    count = pokemonPagingItems.itemCount,
                    key = pokemonPagingItems.itemKey { it.id },
                ) { index ->
                    val pokemon = pokemonPagingItems[index]
                    if (pokemon != null) {
                        PokemonItem(
                            pokemon,
                            modifier = Modifier.fillMaxWidth(),
                        )
                    }
                }
                item {
                    if (pokemonPagingItems.loadState.append is LoadState.Loading) {
                        CircularProgressIndicator(modifier = Modifier.padding(16.dp))
                    }
                }
            }
        }
    }
}

在我们的组合屏幕中,首先要做的是创建我们的 ViewModel 实例,并使用辅助函数 collectAsLazyPagingItems() 收集其中存储的分页数据流。这将冷流转换为 LazyPagingItems 实例。通过这个实例,我们可以访问已加载的项目,以及不同的加载状态,以相应地改变 UI。除此之外,我们甚至可以使用此实例触发数据刷新或重新尝试以前失败的加载。

在 Box 布局中,如果 LazyPagingItems 的“refresh”加载状态为 Loading,则我们知道我们正在初始加载,并且尚无项目可显示。因此,我们显示一个进度指示器。否则,我们会显示一个 LazyColumn,以及使用我们的 LazyPagingItems 实例设置的项目列表的数量和键参数。在每个项目中,我们只需使用给定的索引访问相应的 Pokemon 对象,并呈现 PokemonItem 组合,出于简单起见,这里不给出实现细节。

我们还有一种特殊情况,即需要在这些项目下方显示加载指示器。这发生在我们正在获取更多数据的过程中,可以通过 LazyPagingItems 的“append”加载状态来检测到。因此,如果是这种情况,我们将一个进度指示器追加到列表的末尾。

最后,请不要认为我们在开始部分忽略了LaunchedEffect部分。LaunchedEffect 组合用于在组合内部安全地调用挂起函数。在 Jetpack Compose 中,我们需要协程范围来显示 Snackbar,因为 SnackbarHostState.showSnackbar(…) 是一个挂起函数。在这里,我们显示一个 Snackbar 消息,以防刷新错误,基本上对应于我们的情况下的“初始加载”错误。然而,正如我之前提到的,我们在这里构建了一个离线优先的应用,因此如果我们在 Room 中已经缓存了数据,用户将看到该数据,以及错误消息。

希望您在 Android Jetpack Compose 中的分页和缓存的这段具有挑战性的旅程中能够与我同行。我尽力坚持最新和推荐的操作方式。请随时指出错误或可以做得更好的地方。整个项目已经作为 GitHub 存储库共享,以便您可以下载并进行测试。

GitHub

https://github.com/thunderbolt-codes/Pokemon-Pager

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/878314.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分类预测 | MATLAB实现GAPSO-BP遗传算法组合粒子群算法优化BP神经网络多输入分类预测

分类预测 | MATLAB实现GAPSO-BP遗传算法组合粒子群算法优化BP神经网络多输入分类预测 目录 分类预测 | MATLAB实现GAPSO-BP遗传算法组合粒子群算法优化BP神经网络多输入分类预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.分类预测 | MATLAB实现GAPSO-BP遗…

每日一题——对称的二叉树

题目 给定一棵二叉树&#xff0c;判断其是否是自身的镜像&#xff08;即&#xff1a;是否对称&#xff09; 例如&#xff1a; 下面这棵二叉树是对称的 下面这棵二叉树不对称。 数据范围&#xff1a;节点数满足 0≤n≤1000&#xff0c;节点上的值满足 ∣val∣≤1000 要求&…

并发服务器模型,多线程并发

一、多线程并发完整代码 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> #include <…

[高光谱]PyTorch使用CNN对高光谱图像进行分类

项目原地址&#xff1a; Hyperspectral-Classificationhttps://github.com/eecn/Hyperspectral-ClassificationDataLoader讲解&#xff1a; [高光谱]使用PyTorch的dataloader加载高光谱数据https://blog.csdn.net/weixin_37878740/article/details/130929358 一、模型加载 在…

怎样才能免费使用Qt开发闭源商业软件?

Qt 是一个跨平台的应用程序开发框架&#xff0c;其使用遵循 GNU Lesser General Public License&#xff08;LGPL&#xff09;开源许可协议。根据 LGPL 许可协议&#xff0c;您可以将 Qt 用于闭源商业软件&#xff0c;但是您需要满足以下条件&#xff1a; 1. 在您的软件中使用…

Python实现SSA智能麻雀搜索算法优化卷积神经网络分类模型(CNN分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&a…

Synopsys EDA数字设计与仿真

参考如下文章安装Synopsys EDA开发工具 https://blog.csdn.net/tugouxp/article/details/132255002?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132255002%22%2C%22source%22%3A%22tugouxp%22%7D Synopsys EDA工具的结构 下…

【Android Framework系列】第10章 PMS之Hook实现广播的调用

1 前言 前面章节我们学习了【Android Framework系列】第4章 PMS原理我们了解了PMS原理&#xff0c;【Android Framework系列】第9章 AMS之Hook实现登录页跳转我们知道AMS可以Hook拦截下来实现未注册Activity页面的跳转&#xff0c;本章节我们来尝试一下HookPMS实现广播的发送。…

React入门 jsx学习笔记

一、JSX介绍 概念&#xff1a;JSX是 JavaScript XML&#xff08;HTML&#xff09;的缩写&#xff0c;表示在 JS 代码中书写 HTML 结构 作用&#xff1a;在React中创建HTML结构&#xff08;页面UI结构&#xff09; 优势&#xff1a; 采用类似于HTML的语法&#xff0c;降低学…

chatserver服务器开发笔记

chatserver服务器开发笔记 1 chatserver2 开发环境3 编译 1 chatserver 集群聊天服务器和客户端代码&#xff0c;基于muduo、redis、mysql实现。 学习于https://fixbug.ke.qq.com/ 本人已经挂github&#xff1a;https://github.com/ZixinChen-S/chatserver/tree/main 需要该项…

ASR 语音识别接口封装和分析

这个文档主要是介绍一下我自己封装了 6 家厂商的短语音识别和实时流语音识别接口的一个包&#xff0c;以及对这些接口的一个对比。分别是&#xff0c;阿里&#xff0c;快商通&#xff0c;百度&#xff0c;腾讯&#xff0c;科大&#xff0c;字节。 zxmfke/asrfactory (github.c…

神秘的ip地址8.8.8.8,到底是什么类型的DNS服务器?

下午好&#xff0c;我的网工朋友。 DNS&#xff0c;咱们网工配置网络连接或者路由器时&#xff0c;高低得和这玩意儿打交道吧。 它是互联网中用于将人类可读的域名&#xff08;例如http://www.example.com&#xff09;转换为计算机可理解的IP地址&#xff08;例如192.0.2.1&a…

机器学习中基本的数据结构说明

数据维度或数据结构 当我们在机器学习或深度学习的领域内处理数据&#xff0c;我们通常会遇到四种主要的数据结构&#xff1a;标量&#xff0c;向量&#xff0c;矩阵和张量。理解这些基本数据结构是非常重要的&#xff0c;因为它们是机器学习算法和神经网络的核心。下面是对这…

AssetBundle总结

文章目录 目的打包过程打包时的分组策略和压缩方式资源的载入和卸载其它&#xff1a;Manifest、校验、视图工具思维导图 前言&#xff1a; 大佬文章链接&#xff08;据此总结的&#xff09; 目的 避免软件因资源占用空间太大&#xff0c;导致运行缓慢 避免每次更新资源&#…

【STM32】高效开发工具CubeMonitor快速上手

工欲善其事必先利其器。拥有一个辅助测试工具&#xff0c;能极大提高开发项目的效率。STM32CubeMonitor系列工具能够实时读取和呈现其变量&#xff0c;从而在运行时帮助微调和诊断STM32应用&#xff0c;类似于一个简单的示波器。它是一款基于流程的图形化编程工具&#xff0c;类…

Michael.W基于Foundry精读Openzeppelin第26期——ERC1820Implementer.sol

Michael.W基于Foundry精读Openzeppelin第26期——ERC1820Implementer.sol 0. 版本0.1 ERC1820Implementer.sol 1. 目标合约2. 代码精读2.1 _registerInterfaceForAddress(bytes32 interfaceHash, address account) internal2.2 canImplementInterfaceForAddress(bytes32 interf…

Java的反射机制、Lambda表达式和枚举

Java的反射机制、Lambda表达式和枚举 文章目录 Java的反射机制、Lambda表达式和枚举1.反射机制反射的概念、用途、优缺点反射相关的类及使用&#xff08;重要&#xff01;&#xff01;&#xff09;相关类Class类&#xff1a;代表类实体&#xff0c;表示类和接口Field类&#xf…

URLSearchParams:JavaScript中的URL查询参数处理工具

文章目录 导言&#xff1a;一、URLSearchParams的来历二、URLSearchParams的作用三、URLSearchParams的方法和属性四、使用示例五、注意事项六、结论参考资料 导言&#xff1a; 在Web开发中&#xff0c;处理URL查询参数是一项常见的任务。为了简化这一过程&#xff0c;JavaScr…

论文阅读——Adversarial Eigen Attack on Black-Box Models

Adversarial Eigen Attack on Black-Box Models 作者&#xff1a;Linjun Zhou&#xff0c; Linjun Zhou 攻击类别&#xff1a;黑盒&#xff08;基于梯度信息&#xff09;&#xff0c;白盒模型的预训练模型可获得&#xff0c;但训练数据和微调预训练模型的数据不可得&#xff…

龙迅LT86102UXE产品概括,HDMI2.0转一分二HDMI2.0/1.4,支持音频剥离,支持4K60HZ

LT86102UXE 1. 一般说明 龙迅 LT86102UXE HDMI2.0 分路器具有符合 HDMI2.0/1.4 规范的 1&#xff1a;2 分路器、最大 6Gbps 高速数据速率、自适应均衡 RX 输入和预加重 TX 输出&#xff08;用于支持长电缆应用&#xff09;、内部 TX 通道交换以实现灵活的 PCB 布线。 LT86102…