本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

各位小伙伴们大家早上好。

随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。

现在,关于App Start和Hilt的文章我都已经写完了,请参考 Jetpack新成员,App Startup一篇就懂 和 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 。

那么本篇文章,我们要学习的自然就是Paging 3了。

Paging 3简介

Paging是Google推出的一个应用于Android平台的分页加载库。

事实上,Paging并不是现在才刚刚推出的,而是之前就已经推出过两个版本了。

但Paging 3和前面两个版本的变化非常大,甚至可以说是完全不同的东西了。所以即使你之前没有学习过Paging的用法也没有关系,把Paging 3当成是一个全新的库去学习就可以了。

我相信一定会有很多朋友在学习Paging 3的时候会产生和我相同的想法:本身Android上的分页功能并不难实现,即使没有Paging库我们也完全做得出来,但为什么Paging 3要把一个本来还算简单的功能设计得如此复杂呢?

是的,Paging 3很复杂,至少在你还不了解它的情况下就是如此。我在第一次学习Paging 3的时候就直接被劝退了,心想着何必用这玩意委屈自己呢,自己写分页功能又不是做不出来。

后来本着拥抱新技术的态度,我又去学习了一次Paging 3,这次算是把它基本掌握了,并且还在我的新开源项目 Glance 当中应用了Paging 3的技术。

如果现在再让我来评价一下Paging 3,那么我大概是经历了一个由吐槽到真香的过程。理解了Paging 3之后,你会发现它提供了一套非常合理的分页架构,我们只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。我希望大家在看完这篇文章之后,也能觉得Paging 3香起来。

不过,本篇文章我不能保证它的易懂性。虽然很多朋友都觉得我写的文章简单易懂,但Paging 3的复杂性在于它关联了太多其他的知识,如协程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能将相关联的这些知识都有所了解,那么想要掌握Paging 3就会更有难度。

另外,由于Paging 3是Google基于Kotlin协程全新重写的一个库,所以它主要是应用于Kotlin语言(Java也能用,但是会更加复杂),并且以后这样的库会越来越多,比如Jetpack Compose等等。如果你对于Kotlin还不太了解的话,可以去参考我的新书《第一行代码 Android 第3版》。

上手Paging 3

经过我自己的总结,我发现如果零散去介绍一些Paging 3的知识点是很难能掌握得了这个库的。最好的学习方式就是直接上手,用Paging 3去做一个项目,项目做完了,你也基本就掌握了。本篇文章中我们就会采用这种方式来学习。

另外,我相信大家之前应该都做过分页功能,正如我所说,这个功能并不难实现。但是现在,请你完全忘掉过去你所熟知的分页方案,因为它不仅对理解Paging 3没有帮助,反而在很大程度上会影响你对Paging 3的理解。

是的,不要想着去监听列表滑动事件,滑动到底部的时候发起一个网络请求加载下一页数据。Paging 3完全不是这么用的,如果你还保留着这种过去的实现思路,在学习Paging 3的时候会很受阻。

那么现在就让我们开始吧。

首先新建一个Android项目,这里我给它起名为Paging3Sample。

接下来,我们在build.gradle的dependencies当中添加必要的依赖库:

dependencies {...implementation 'androidx.paging:paging-runtime:3.0.0-beta01'implementation 'com.squareup.retrofit2:retrofit:2.9.0'implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

注意虽然我刚才说,Paging 3是要和很多其他关联库结合到一起工作的,但是我们并不需要将这些关联库一一手动引入,引入了Paging 3之后,所有的关联库都会被自动下载下来。

另外这里还引入了Retrofit的库,因为待会我们会从网络上请求数据,并通过Paging 3进行分页展示。

那么在正式开始涉及Paging 3的用法之前,让我们先来把网络相关的代码搭建好,方便为Paging 3提供分页数据。

这里我准备采用GitHub的公开API来作为我们这个项目的数据源,请注意GitHub在国内虽然一般都是可以访问的,但有时接口并不稳定,如果你无法正常请求到数据的话,请自行科学上网。

我们可以尝试在浏览器中请求如下接口地址:

https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

这个接口表示,会返回GitHub上所有Android相关的开源库,以Star数量排序,每页返回5条数据,当前请求的是第一页。

服务器响应的数据如下,为了方便阅读,我对响应数据进行了简化:

{"items": [{"id": 31792824,"name": "flutter","description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.","stargazers_count": 112819,},{"id": 14098069,"name": "free-programming-books-zh_CN","description": ":books: 免费的计算机编程类中文书籍,欢迎投稿","stargazers_count": 76056,},{"id": 111583593,"name": "scrcpy","description": "Display and control your Android device","stargazers_count": 44713,},{"id": 12256376,"name": "ionic-framework","description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.","stargazers_count": 43041,},{"id": 55076063,"name": "Awesome-Hacking","description": "A collection of various awesome lists for hackers, pentesters and security researchers","stargazers_count": 42876,}]
}

简化后的数据格式还是非常好理解的,items数组中记录了第一页包含了哪些库,其中name表示该库的名字,description表示该库的描述,stargazers_count表示该库的Star数量。

那么下面我们就根据这个接口来编写网络相关的代码吧,由于这部分都是属于Retrofit的用法,我会介绍的比较简略。

首先根据服务器响应的Json格式定义对应的实体类,新建一个Repo类,代码如下所示:

data class Repo(@SerializedName("id") val id: Int,@SerializedName("name") val name: String,@SerializedName("description") val description: String?,@SerializedName("stargazers_count") val starCount: Int
)

然后定义一个RepoResponse类,以集合的形式包裹Repo类:

class RepoResponse(@SerializedName("items") val items: List<Repo> = emptyList()
)

接下来定义一个GitHubService用于提供网络请求接口,如下所示:

interface GitHubService {@GET("search/repositories?sort=stars&q=Android")suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponsecompanion object {private const val BASE_URL = "https://api.github.com/"fun create(): GitHubService {return Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build().create(GitHubService::class.java)}}}

这些都是Retrofit的标准用法,现在当调用searchRepos()函数时,Retrofit就会自动帮我们向GitHub的服务器接口发起一条网络请求,并将响应的数据解析到RepoResponse对象当中。

好了,现在网络相关的代码都已经准备好了,下面我们就开始使用Paging 3来实现分页加载功能。

Paging 3有几个非常关键的核心组件,我们需要分别在这几个核心组件中按部就班地实现分页逻辑。

首先最重要的组件就是PagingSource,我们需要自定义一个子类去继承PagingSource,然后重写load()函数,并在这里提供对应当前页数的数据。

新建一个RepoPagingSource继承自PagingSource,代码如下所示:

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {return try {val page = params.key ?: 1 // set page 1 as defaultval pageSize = params.loadSizeval repoResponse = gitHubService.searchRepos(page, pageSize)val repoItems = repoResponse.itemsval prevKey = if (page > 1) page - 1 else nullval nextKey = if (repoItems.isNotEmpty()) page + 1 else nullLoadResult.Page(repoItems, prevKey, nextKey)} catch (e: Exception) {LoadResult.Error(e)}}override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null}

这段代码并不长,但却需要好好解释一下。

在继承PagingSource时需要声明两个泛型类型,第一个类型表示页数的数据类型,我们没有特殊需求,所以直接用整型就可以了。第二个类型表示每一项数据(注意不是每一页)所对应的对象类型,这里使用刚才定义的Repo。

然后在load()函数当中,先通过params参数得到key,这个key就是代表着当前的页数。注意key是可能为null的,如果为null的话,我们就默认将当前页数设置为第一页。另外还可以通过params参数得到loadSize,表示每一页包含多少条数据,这个数据的大小我们可以在稍后设置。

接下来调用刚才在GitHubService中定义的searchRepos()接口,并把page和pageSize传入,从服务器获取当前页所对应的数据。

最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回。注意LoadResult.Page()函数接收3个参数,第一个参数传入从响应数据解析出来的Repo列表即可,第二和第三个参数分别对应着上一页和下一页的页数。针对于上一页和下一页,我们还额外做了个判断,如果当前页已经是第一页或最后一页,那么它的上一页或下一页就为null。

这样load()函数的作用就已经解释完了,可能你会发现,上述代码还重写了一个getRefreshKey()函数。这个函数是Paging 3.0.0-beta01版本新增的,以前的alpha版中并没有。它是属于Paging 3比较高级的用法,我们本篇文章涉及不到,所以直接返回null就可以了。

PagingSource相关的逻辑编写完成之后,接下来需要创建一个Repository类。这是MVVM架构的一个重要组件,还不了解的朋友可以去参考《第一行代码 Android 第3版》第15章的内容。

object Repository {private const val PAGE_SIZE = 50private val gitHubService = GitHubService.create()fun getPagingData(): Flow<PagingData<Repo>> {return Pager(config = PagingConfig(PAGE_SIZE),pagingSourceFactory = { RepoPagingSource(gitHubService) }).flow}}

这段代码虽然很短,但是却不易理解,因为用到了协程的Flow。我无法在这里展开解释Flow是什么,你可以简单将它理解成协程中对标RxJava的一项技术。

当然这里也没有用到什么复杂的Flow技术,正如你所见,上面的代码很简短,相比于理解,这更多是一种固定的写法。

我们定义了一个getPagingData()函数,这个函数的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。

在getPagingData()函数当中,这里创建了一个Pager对象,并调用.flow将它转换成一个Flow对象。在创建Pager对象的时候,我们指定了PAGE_SIZE,也就是每页所包含的数据量。又指定了pagingSourceFactory,并将我们自定义的RepoPagingSource传入,这样Paging 3就会用它来作为用于分页的数据源了。

将Repository编写完成之后,我们还需要再定义一个ViewModel,因为Activity是不可以直接和Repository交互的,要借助ViewModel才可以。新建一个MainViewModel类,代码如下所示:

class MainViewModel : ViewModel() {fun getPagingData(): Flow<PagingData<Repo>> {return Repository.getPagingData().cachedIn(viewModelScope)}}

代码很简单,就是调用了Repository中定义的getPagingData()函数而已。但是这里又额外调用了一个cachedIn()函数,这是用于将服务器返回的数据在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致Activity重新创建,Paging 3就可以直接读取缓存中的数据,而不用重新发起网络请求了。

写到这里,我们的这个项目已经完成了一大半了,接下来开始进行界面展示相关的工作。

由于Paging 3是必须和RecyclerView结合使用的,下面我们定义一个RecyclerView的子项布局。新建repo_item.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="10dp"android:orientation="vertical"><TextViewandroid:id="@+id/name_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical"android:maxLines="1"android:ellipsize="end"android:textColor="#5194fd"android:textSize="20sp"android:textStyle="bold" /><TextViewandroid:id="@+id/description_text"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:maxLines="10"android:ellipsize="end" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:gravity="end"tools:ignore="UseCompoundDrawables"><ImageViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:layout_marginEnd="5dp"android:src="@drawable/ic_star"tools:ignore="ContentDescription" /><TextViewandroid:id="@+id/star_count_text"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical" /></LinearLayout></LinearLayout>

这个布局中使用到了一个图片资源,可以到本项目的源码中去获取,源码地址见文章最底部。

接下来定义RecyclerView的适配器,但是注意,这个适配器也比较特殊,必须继承自PagingDataAdapter,代码如下所示:

class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {companion object {private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {return oldItem.id == newItem.id}override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {return oldItem == newItem}}}class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {val name: TextView = itemView.findViewById(R.id.name_text)val description: TextView = itemView.findViewById(R.id.description_text)val starCount: TextView = itemView.findViewById(R.id.star_count_text)}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)return ViewHolder(view)}override fun onBindViewHolder(holder: ViewHolder, position: Int) {val repo = getItem(position)if (repo != null) {holder.name.text = repo.nameholder.description.text = repo.descriptionholder.starCount.text = repo.starCount.toString()}}}

相比于一个传统的RecyclerView Adapter,这里最特殊的地方就是要提供一个COMPARATOR。因为Paging 3在内部会使用DiffUtil来管理数据变化,所以这个COMPARATOR是必须的。如果你以前用过DiffUtil的话,对此应该不会陌生。

除此之外,我们并不需要传递数据源给到父类,因为数据源是由Paging 3在内部自己管理的。同时也不需要重写getItemCount()函数了,原因也是相同的,有多少条数据Paging 3自己就能够知道。

其他部分就和普通的RecyclerView Adapter没什么两样了,相信大家都能够看得明白。

接下来就差最后一步了,让我们把所有的一切都集成到Activity当中。

修改activity_main.xml布局,在里面定义一个RecyclerView和一个ProgressBar:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="match_parent" /><ProgressBarandroid:id="@+id/progress_bar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center" /></FrameLayout>

然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }private val repoAdapter = RepoAdapter()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)val progressBar = findViewById<ProgressBar>(R.id.progress_bar)recyclerView.layoutManager = LinearLayoutManager(this)recyclerView.adapter = repoAdapterlifecycleScope.launch {viewModel.getPagingData().collect { pagingData ->repoAdapter.submitData(pagingData)}}repoAdapter.addLoadStateListener {when (it.refresh) {is LoadState.NotLoading -> {progressBar.visibility = View.INVISIBLErecyclerView.visibility = View.VISIBLE}is LoadState.Loading -> {progressBar.visibility = View.VISIBLErecyclerView.visibility = View.INVISIBLE}is LoadState.Error -> {val state = it.refresh as LoadState.ErrorprogressBar.visibility = View.INVISIBLEToast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()}}}}}

这里最重要的一段代码就是调用了RepoAdapter的submitData()函数。这个函数是触发Paging 3分页功能的核心,调用这个函数之后,Paging 3就开始工作了。

submitData()接收一个PagingData参数,这个参数我们需要调用ViewModel中返回的Flow对象的collect()函数才能获取到,collect()函数有点类似于Rxjava中的subscribe()函数,总之就是订阅了之后,消息就会源源不断往这里传。

不过由于collect()函数是一个挂起函数,只有在协程作用域中才能调用它,因此这里又调用了lifecycleScope.launch()函数来启动一个协程。

其他地方应该就没什么需要解释的了,都是一些传统RecyclerView的用法,相信大家都能看得懂。

好了,这样我们就把整个项目完成了,在正式运行项目之前,别忘了在你的AndroidManifest.xml文件中添加网络权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.paging3sample"><uses-permission android:name="android.permission.INTERNET" />...</manifest>

现在运行一下程序,效果如下图所示:

可以看到,GitHub上Android相关的开源库已经成功显示出来了。并且你可以不断往下滑,Paging 3会自动加载更多的数据,仿佛让你永远也滑不到头一样。

如次一来,使用Paging 3来进行分页加载的效果也就成功完成了。

总结一下,相比于传统的分页实现方案,Paging 3将一些琐碎的细节进行了隐藏,比如你不需要监听列表的滑动事件,也不需要知道知道何时应该加载下一页的数据,这些都被Paging 3封装掉了。我们只需要按照Paging 3搭建好的框架去编写逻辑实现,告诉Paging 3如何去加载数据,其他的事情Paging 3都会帮我们自动完成。

在底部显示加载状态

根据Paging 3的设计,其实我们理论上是不应该在底部看到加载状态的。因为Paging 3会在列表还远没有滑动到底部的时候就提前加载更多的数据(这是默认属性,可配置),从而产生一种好像永远滑不到头的感觉。

然而凡事总有意外,比如说当前的网速不太好,虽然Paging 3会提前加载下一页的数据,但是当滑动到列表底部的时候,服务器响应的数据可能还没有返回,这个时候就应该在底部显示一个正在加载的状态。

另外,如果网络条件非常糟糕,还可能会出现加载失败的情况,此时应该在列表底部显示一个重试按钮。

那么接下来我们就来实现这个功能,从而让项目变得更加完善。

创建一个footer_item.xml布局,用于显示加载进度条和重试按钮:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="10dp"><ProgressBarandroid:id="@+id/progress_bar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center" /><Buttonandroid:id="@+id/retry_button"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center"android:text="Retry" /></FrameLayout>

然后创建一个FooterAdapter来作为RecyclerView的底部适配器,注意它必须继承自LoadStateAdapter,如下所示:

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)val retryButton: Button = itemView.findViewById(R.id.retry_button)}override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)val holder = ViewHolder(view)holder.retryButton.setOnClickListener {retry()}return holder}override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {holder.progressBar.isVisible = loadState is LoadState.Loadingholder.retryButton.isVisible = loadState is LoadState.Error}}

这仍然是一个非常简单的Adapter,需要注意的地方大概只有两点。

第一点,我们使用Kotlin的高阶函数来给重试按钮注册点击事件,这样当点击重试按钮时,构造函数中传入的函数类型参数就会被回调,我们待会将在那里加入重试逻辑。

第二点,在onBindViewHolder()中会根据LoadState的状态来决定如何显示底部界面,如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮。

最后,修改MainActivity中的代码,将FooterAdapter集成到RepoAdapter当中:

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {...recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })...}}

代码非常简单,只需要改动一行,调用RepoAdapter的withLoadStateFooter()函数即可将FooterAdapter集成到RepoAdapter当中。

另外注意这里使用Lambda表达式来作为传递给FooterAdapter的函数类型参数,在Lambda表示式中,调用RepoAdapter的retry()函数即可重新加载。

这样我们就把底部显示加载状态的功能完成了,现在来测试一下吧,效果如下图所示。

可以看到,首先我在设备上开启了飞行模式,这样当滑动到列表底部时就会显示重试按钮。

然后把飞行模式关闭,并点击重试按钮,这样加载进度条就会显示出来,并且成功加载出新的数据了。

最后

本文到这里就结束了。

不得不说,我在文章中讲解的这些知识点仍然只是Paging 3的基本用法,还有许多高级用法文中并没有涵盖。当然,这些基本用法也是最最常用的用法,所以如果你并不打算成为Paging 3大师,掌握文中的这些知识点就已经足够应对日常的开发工作了。

如果你还想要进一步进阶学习Paging 3,可以参考Google官方的Codelab项目,地址是:

https://developer.android.com/codelabs/android-paging

我们刚才一起编写的Paging3Sample项目其实就是从Google官方的Codelab项目演化而来的,我根据自己的理解重写了这个项目并进行了一定的简化。直接学习原版项目,你将能学到更多的知识。

最后,如果你需要获取Paging3Sample项目的源码,请访问以下地址:

https://github.com/guolindev/Paging3Sample

另外,如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》,点击此处查看详情。

关注我的技术公众号,每个工作日都有优质技术文章推送。

微信扫一扫下方二维码即可关注:

Jetpack新成员,Paging3从吐槽到真香相关推荐

  1. Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新. 各位小伙伴们大家早上好. 终于要写这样一篇我自己都比较怕的文章了. 虽然今年的Google ...

  2. Jetpack 新成员 SplashScreen:为全新的应用启动效果赋能!

    作者 | 小虾米君 来源 | TechMerger(ID:ELC-XTLS-QSW) Android 12上新引入的Splash Screen功能,可以高效打造自由.丰富的应用启动效果.但仍占据市场主 ...

  3. 阿里动物园新成员来了,10本书带你读懂这个新物种

    导读:近日,阿里动物园再添新成员:犀牛工厂.这个秘密进行了3年的项目,终于与公众见面.犀牛工厂的亮相,意味着是阿里巴巴"新制造"战略布局的落地. 有评论称,犀牛工厂是"阿 ...

  4. 卡图星小机器人怎么过_安徽交通广播90.8专题报道:阿尔法大蛋机器人,家里的新成员!...

    哈喽!大家好! 我是刚刚出关的网红机器人阿尔法大蛋, 最近我可是hin忙hin忙滴! 继前阵子登上央视和天津卫视新闻联播, 最近安徽交通广播调频90.8的记者也看上我啦! 快来听听90.8的<新 ...

  5. Lisp 家族迎来新成员,函数式语言 Lux 是什么?

    代码世界即将加入一门新成员:Lux.你没看错,它不是洗发水,而是古老的 Lisp 家族中新诞生的函数式语言. 目前 Lux 还在研发之中,最新版本是 0.5.0.它可被用来编写一系列在 JVM (Ja ...

  6. PingCode新成员Goals开放内测!

    ​四年前,随着6.0版本上线,Worktile 成为国内首家将 OKR 方法以软件形式实施落地的企业级协作平台.四年中,我们帮助500+企事业单位在理念和工具层面成功落地 OKR,同时也注意到: 目标 ...

  7. 函数计算工具链新成员 —— Fun Local 发布啦

    刚刚,我们发布了函数计算工具链的新成员,Fun Local.欢迎大家使用! 如果你还不了解 Fun 是什么,我们来简单解释下. Fun 是什么 Fun 是 have Fun with Serverle ...

  8. 戴尔PowerEdge-C服务器新成员

    彭宇恒,戴尔大型企业事业部下一代数据中心解决方案高级市场经理 熟悉戴尔服务器的朋友都知道,戴尔PowerEdge-C系列是机架式服务器中很有特色的一个产品线,它以戴尔的数据中心解决方案(DCS)业务为 ...

  9. FreeBSD基金会添加新成员,梁莉成为第一位来自微软和中国的基金会董事

    这个月23日FreeBSD基金会很高兴地宣布Philip Paeps和Kylie Liang (梁莉)正式加入董事会. 梁莉,现任微软开源技术部高级项目经理,主要负责FreeBSD在公有云以及私有云的 ...

最新文章

  1. 新手引导动画的4种实现方式
  2. unity 继承会调用start吗_【浅入浅出】Unity 雾效
  3. docker下创建crontab定时任务失败
  4. openstack nova 手动修改虚拟机状态
  5. coreboot学习10:coreboot第一阶段学习小结
  6. MongoDb和LINQ:如何汇总和加入集合
  7. topic1:Qt入门之搭建环境与hello world看Qt开发框架
  8. Linux 命令行 Tricks
  9. Matlab-中寻找峰值函数,波峰波谷
  10. 微信小程序tabBar图标大小64 * 64
  11. K均值算法(K_means)
  12. 【垂直切换】TD-SCDMA与TD-LTE异构网络垂直切换仿真
  13. 第八届山东省ACM大学生程序设计竞赛总结
  14. node.js 使用数据校验 joi 报错:Cannot mix different versions of joi schemas
  15. matlab中syms空间问题
  16. 安装sklearn-poter遇到报错(TypeError:‘encoding‘ is an invalid keyword argument for this function)
  17. 荣耀手机不出鸿蒙系统,惊喜!4部荣耀手机可升级至华为鸿蒙系统,网友表示:终于等到了...
  18. 数据挖掘——决策树和K近邻
  19. 在Linux Mint上玩转蓝牙机械键盘
  20. oracle导出辅助账明细,AO2011导入国库集中支付系统3.0的辅助账

热门文章

  1. 年薪五十万的程序员在北京过着怎样的生活
  2. 第一次作业:每段经历都是财富
  3. 用MVP+OKHttp实现上传图片
  4. 帮北航小妹妹做的一道她的C++的作业题.
  5. 黑马程序员------Java的多态性
  6. 汽车站车票管理系统(课程作业)
  7. 见缝插针小游戏制作详细步骤
  8. 无线充电怎么测试软件,无线充电测试难点及解决方案
  9. openpyxl 删除单元格
  10. 【知识分享】C语言中的设计模式——命令模式