首先要对点进来的看官说声sorry,我标题党了。?

虽然抛弃UITableView是不存在的,但是看完这篇文章确实能让90%的列表页抛弃UITableView,让界面易实现易复用。

下面我将以第三人称的叙述方式,通过一个例子比较传统实现和最新实现的手段说明如何让列表页不再难构建。

开始

小明是A公司的iOS程序员,刚入职不久,A公司的产品经理想出来一个新需求,正好安排给小明完成。 产品经理提出要做一个feed流页面,显示用户所关注的其他所有用户的动态。

传统实现

第一个需求:显示用户名和文字内容

产品经理说了用户只能发文字内容,所以列表页也只需要显示用户名和文字内容,就像图片所示,

小明一看这设计图,so easy,UITableView嘛,这cell太简单了,轻车熟路,很快小明就写了大概像这样的代码

class FeedCell: UITableViewCell {var imageView: UIImageViewvar nameLabel: UILabelvar textLabel: UILabelfunc init(frame: CGRect) {///布局代码}func setViewModel(_ viewModel: FeedCellModel) {imageView.image = viewModel.imagenameLabel.text = viewModel.nametextLabel.text = viewModel.content}
}
复制代码

没毛病,小明花了5分钟写完了布局和实现tableview的数据源和代理协议。 产品经理还要求内容默认显示一行,超过省略号表示,点击上去再全部显示,小明想这也容易,在FeedCellModel中加一个表示是否展开的bool量isExpand,然后didSelect代理方法中改变这个值并且reload这一行,在heightForRow代理方法中判断isExpand,返回小明已在FeedCellModel中已经计算的两个高度(初始高度和全部高度)。代码就不展示了哦。 很好,很快,第一版上线了。

第二个需求:点赞

在第二版的计划中,产品经理设计了点赞的功能,如图

于是小明又在FeedCell里加上了这几行代码

var favorBtn: UIButton
var favorLable: UILabelfunc init(frame: CGRect) {///再加几行布局favorBtn和favorLable的代码}func favorClick(_ sender: Any) {///在这里请求点赞,然后重新给favorLable赋值
}
复制代码

然后又到FeedCellModel里面在原有计算高度的地方加一下点赞控件的高度。 很好,目前为止,两个需求都非常快速完美的完成了。

第三个需求:图片展示

只有文字可太单调了,俗话说没图说个jb?,产品经理又设计了图片展示,需求如图

根据设计图,图片是以九宫格展示,并且要放到内容和点赞中间,这时小明感到有点棘手了,觉得要改的代码不少,用UIButton一个个加的话,无论是计算frame还是约束,都很烦,压根就不想写,或者用CollectionView貌似好一点,设置好与上下视图的约束,根据有没有图片设置隐藏,在FeedCellModel里面根据图片数量重新计算一下高度,这样好像也能完成,改动的地方还能接受(可是笔者已经无法接受了,所以此处没有示例代码),于是乎,又愉快的完成的第三版。

class FeedCell: UITableViewCell {var imageCollectionView: UICollectionView
}
复制代码

第四个需求:评论展示

产品经理又设计了一个新需求,要显示所有的评论并且允许发送人删掉某些不合适的评论。看样子是要往社交方面发展了。 小明想了一下,有这几个思路,可以在FeedCell里再嵌套个tableview,预先计算出高度,在commentCell的删除按钮点击事件里重新计算高度然后删除cell;或者封装一下commentView,还是预先计算出高度,根据数据加对应数量的commentView,删除一个再重新计算一下高度。无论哪一种,都有不小的工作量。

class CommentTableView: UIView {var tableView: UITableViewvar comments: [Comment] {didSet {tableView.reloadData()}}func onDeleteClick(_ sender: UIBUtton) {//代理出去处理删除评论事件}
}
class FeedCell: UITableViewCell {var commentTable: CommentTableViewfunc setViewModel(_ viewModel: FeedCellModel) {//调整commentTable的高度约束,把数据传入commentTable渲染评论列表}
}
复制代码

这个需求小明花了两天赶在周末前完成了。不过此时他也下定决心,要在周末花点时间找到一种重构方案,毕竟产品经理的想法很多,后期完全可能再加入视频播放、语音播放,甚至在这个feed流中加入比如广告等其他类型的数据,这个FeedCell和tableview将会越来越难以维护,计算高度也将变难,而且牵一发而动全身。

周末空闲时,小明去github上逛了逛,发现了能够拯救他的救世主--IGListKit。

IGListKit

IGListKit是Instagram出的一个基于UICollectionView的数据驱动UI框架,目前在github上有9k+ star,被充分利用在Instagram App上,可以翻墙的同学可以去体验一下,看看Instagram的体验,想想如果那些页面让小明用传统方式实现,那将是什么样的情况。可以这样说,有了IGListKit,任何类似列表的页面UI构建,都将so easy!

首先,得介绍IGList中的几个基本概念。

ListAdapter

适配器,它将collectionview的dataSource和delegate统一了起来,负责collectionView数据的提供、UI的更新以及各种代理事件的回调。

ListSectionController

一个 section controller是一个抽象UICollectionView的section的controller对象,指定一个数据对象,它负责配置和管理 CollectionView 中的一个 section 中的 cell。这个概念类似于一个用于配置一个 view 的 view-model:数据对象就是 view-model,而 cell 则是 view,section controller 则是二者之间的粘合剂。

具体关系如下图所示

周末两天,小明认真学习了一下IGListKit,得益于IGListKit的易用性,当然还有小明的聪明才智,他决定下周就重构feed页。

周一一上班,小明就开始动手用IGListKit重写上面的需求。

准备工作:布局collectionView和绑定适配器

BaseListViewController.swift

let collectionView: UICollectionView = {let flow = UICollectionViewFlowLayout()let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flow)collectionView.backgroundColor = UIColor.groupTableViewBackgroundreturn collectionView}()
override func viewDidLayoutSubviews() {super.viewDidLayoutSubviews()collectionView.frame = view.bounds}
复制代码

创建adapter,将collectionView和它适配起来

//存放数据的数组,数据模型需要实现ListDiffable协议,主要实现判等,具体是什么后面再说
var objects: [ListDiffable] = [ListDiffable]()
lazy var adapter: ListAdapter = {let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self)return adapter}()
override func viewDidLoad() {super.viewDidLoad()view.addSubview(collectionView)adapter.collectionView = collectionViewadapter.dataSource = self}
复制代码

实现ListAdapterDataSource协议来提供数据

///返回要在collectionView中显示的所有数据
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {return objects}
///返回每个数据对应的sectionController,func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {//ListSectionController是抽象基类,不能直接使用,必须子类化,这里这么写是因为是在基类BaseListViewController里。return ListSectionController()}
///数据为空时显示的占位视图func emptyView(for listAdapter: ListAdapter) -> UIView? {return nil}
复制代码

因为为了清晰的比较每个需求的变更,所以在demo里每个需求都有一个ViewController,搞了个基类来创建collectionView和adapter。

第一个需求:显示用户名和文字内容

准备两个cell

class UserInfoCell: UICollectionViewCell {@IBOutlet weak var avatarView: UIImageView!@IBOutlet weak var nameLabel: UILabel!public var onClickArrow: ((UserInfoCell) -> Void)?override func awakeFromNib() {super.awakeFromNib()self.avatarView.layer.cornerRadius = 12}@IBAction private func onClickArrow(_ sender: Any) {onClickArrow?(self)}func bindViewModel(_ viewModel: Any) {guard let viewModel = viewModel as? UserInfoCellModel else { return }self.avatarView.backgroundColor = UIColor.purpleself.nameLabel.text = viewModel.userName}}class ContentCell: UICollectionViewCell {@IBOutlet weak var label: UILabel!override func awakeFromNib() {super.awakeFromNib()// Initialization code}static func lineHeight() -> CGFloat {return UIFont.systemFont(ofSize: 16).lineHeight}static func height(for text: NSString,limitwidth: CGFloat) -> CGFloat {let font = UIFont.systemFont(ofSize: 16)let size: CGSize = CGSize(width: limitwidth - 20, height: CGFloat.greatestFiniteMagnitude)let rect = text.boundingRect(with: size, options: [.usesFontLeading,.usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font:font], context: nil)return ceil(rect.height)}func bindViewModel(_ viewModel: Any) {guard let vm = viewModel as? String else { return }self.label.text = vm}
}
复制代码

准备sectionController,一个cell对应一个sectionController。这只是一种实现方式,下面还有一种方式(只需要一个sectionController)。

final class UserInfoSectionController: ListSectionController {var object: Feed!lazy var viewModel: UserInfoCellModel = {let model = UserInfoCellModel(avatar: URL(string: object.avatar), userName: object.userName)return model}()override func numberOfItems() -> Int {return 1}override func sizeForItem(at index: Int) -> CGSize {let width: CGFloat! = collectionContext?.containerSize(for: self).widthreturn CGSize(width: width, height: 30)}override func cellForItem(at index: Int) -> UICollectionViewCell {guard let cell = collectionContext?.dequeueReusableCell(withNibName: UserInfoCell.cellIdentifier, bundle: nil, for: self, at: index) as? UserInfoCell else { fatalError() }cell.bindViewModel(viewModel as Any)cell.onClickArrow = {[weak self] cell inguard let self = self else { return }let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)actionSheet.addAction(UIAlertAction(title: "share", style: .default, handler: nil))actionSheet.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))actionSheet.addAction(UIAlertAction(title: "delete", style: .default, handler: { (action) inNotificationCenter.default.post(name: Notification.Name.custom.delete, object: self.object)}))self.viewController?.present(actionSheet, animated: true, completion: nil)}return cell}override func didUpdate(to object: Any) {self.object = object as? Feed}
}
复制代码
class ContentSectionController: ListSectionController {var object: Feed!var expanded: Bool = falseoverride func numberOfItems() -> Int {if object.content?.isEmpty ?? true {return 0}return 1}override func sizeForItem(at index: Int) -> CGSize {guard let content = object.content else { return CGSize.zero }let width: CGFloat! = collectionContext?.containerSize(for: self).widthlet height = expanded ? ContentCell.height(for: content as NSString, limitwidth: width) : ContentCell.lineHeight()return CGSize(width: width, height: height + 5)}override func cellForItem(at index: Int) -> UICollectionViewCell {guard let cell = collectionContext?.dequeueReusableCell(withNibName: ContentCell.cellIdentifier, bundle: nil, for: self, at: index) as? ContentCell else { fatalError() }cell.bindViewModel(object.content as Any)return cell}override func didUpdate(to object: Any) {self.object = object as? Feed}override func didSelectItem(at index: Int) {expanded.toggle()UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: {self.collectionContext?.invalidateLayout(for: self, completion: nil)}, completion: nil)}
}
复制代码

在ViewController里获取数据,实现数据源协议

class FirstListViewController: BaseListViewController {
override func viewDidLoad() {super.viewDidLoad()do {let data = try JsonTool.decode([Feed].self, jsonfileName: "data1")self.objects.append(contentsOf: data)adapter.performUpdates(animated: true, completion: nil)} catch {print("decode failure")}}override func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {let stack = ListStackedSectionController(sectionControllers: [UserInfoSectionController(),ContentSectionController()])stack.inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)return stack}
}
复制代码

这里用到了框架里的一个类ListStackedSectionController,它是来管理子sectionController的。这里我把每个数据对应看做大组,每个cell显示的数据看做小组,ListStackedSectionController即是大组,它会按照sectionControllers数组顺序从上至下排列子sectionController,有点类似于UIStackView。

第一个需求已经实现了,貌似比原来的实现代码更多了啊,哪变简单了,别着急,继续往下看。

第二个需求:点赞

按照原来的思路,我们得修改原来FeedCell,在里面再加上新的控件,然后再在viewModel里重新计算高度,这其实违反了面向对象的设计原则开闭原则。那么现在该如何去做,我们直接新增一个FavorCell,和对应的一个FavorSectionController,根本不需要碰原有运行良好的代码。

class FavorCell: UICollectionViewCell {@IBOutlet weak var favorBtn: UIButton!@IBOutlet weak var nameLabel: UILabel!var favorOperation: ((FavorCell) -> Void)?var viewModel: FavorCellModel?override func awakeFromNib() {super.awakeFromNib()// Initialization code}@IBAction func onClickFavor(_ sender: Any) {self.favorOperation!(self)}func bindViewModel(_ viewModel: Any) {guard let viewModel = viewModel as? FavorCellModel else { return }self.viewModel = viewModelself.favorBtn.isSelected = viewModel.isFavorself.nameLabel.text = viewModel.favorNum}
}
复制代码
class FavorSectionController: ListSectionController {var object: Feed!lazy var viewModel: FavorCellModel = {let vm = FavorCellModel()vm.feed = objectreturn vm}()override func numberOfItems() -> Int {return 1}override func sizeForItem(at index: Int) -> CGSize {let width: CGFloat! = collectionContext?.containerSize(for: self).widthreturn CGSize(width: width, height: 65)}override func cellForItem(at index: Int) -> UICollectionViewCell {guard let cell = collectionContext?.dequeueReusableCell(withNibName: FavorCell.cellIdentifier, bundle: nil, for: self, at: index) as? FavorCell else { fatalError() }cell.bindViewModel(viewModel as Any)cell.favorOperation = {[weak self] cell inguard let self = self else { return }self.object.isFavor.toggle()let origin: UInt! = self.object.favorself.object.favor = self.object.isFavor ? (origin + 1) : (origin - 1)self.viewModel.feed = self.objectself.collectionContext?.performBatch(animated: true, updates: { (batch) inbatch.reload(in: self, at: IndexSet(integer: 0))}, completion: nil)}return cell}override func didUpdate(to object: Any) {self.object = object as? Feed}
}
复制代码

在ViewController里重新实现一下数据源方法就行了

override func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {let stack = ListStackedSectionController(sectionControllers: [UserInfoSectionController(),ContentSectionController(),FavorSectionController()])stack.inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)return stack}
复制代码

看,只需要在ListStackedSectionController里新增一个FavorSectionController,就能完成这个需求了。

第三个:图片展示

九宫格的图片展示,用UICollectionView是最简单的实现方式。

class ImageCollectionCell: UICollectionViewCell {let padding: CGFloat = 10@IBOutlet weak var collectionView: UICollectionView!var viewModel: ImagesCollectionCellModel!override func awakeFromNib() {super.awakeFromNib()collectionView.register(UINib(nibName: ImageCell.cellIdentifier, bundle: nil), forCellWithReuseIdentifier: ImageCell.cellIdentifier)}func bindViewModel(_ viewModel: Any) {guard let viewModel = viewModel as? ImagesCollectionCellModel else { return }self.viewModel = viewModelcollectionView.reloadData()}
}extension ImageCollectionCell: UICollectionViewDataSource {func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {return (self.viewModel?.images.count)!}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCell.cellIdentifier, for: indexPath) as? ImageCell else { fatalError() }cell.image = self.viewModel?.images[indexPath.item]return cell}
}extension ImageCollectionCell: UICollectionViewDelegateFlowLayout {func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {let width: CGFloat = (collectionView.bounds.width - padding * 2) / 3return CGSize(width: width, height: width)}
}复制代码
class ImageSectionController: ListSectionController {let padding: CGFloat = 10var object: Feed!lazy var viewModel: ImagesCollectionCellModel = {let vm = ImagesCollectionCellModel()vm.imageNames = object.imagesreturn vm}()override func numberOfItems() -> Int {if object.images.count == 0 {return 0}return 1}override func sizeForItem(at index: Int) -> CGSize {let width: CGFloat! = collectionContext?.containerSize(for: self).widthlet itemWidth: CGFloat = (width - padding * 2) / 3let row: Int = (object.images.count - 1) / 3 + 1let h: CGFloat = CGFloat(row) * itemWidth + CGFloat(row - 1) * paddingreturn CGSize(width: width, height: h)}override func cellForItem(at index: Int) -> UICollectionViewCell {guard let cell = collectionContext?.dequeueReusableCell(withNibName: ImageCollectionCell.cellIdentifier, bundle: nil, for: self, at: index) as? ImageCollectionCell else { fatalError() }cell.bindViewModel(viewModel)return cell}override func didUpdate(to object: Any) {self.object = object as? Feed}
}
复制代码

同之前同样的操作,在ListStackedSectionController里把ImageSectionController加进去就?了。 哦,慢着,这个图片区域好像是在内容的下面和点赞的上面,那就把ImageSectionController放到ContentSectionController和FavorSectionController之间,就行了。

override func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {let stack = ListStackedSectionController(sectionControllers:[UserInfoSectionController(),ContentSectionController(),ImageSectionController(),FavorSectionController()])stack.inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)return stack}
复制代码

这里已经体现出IGListKit相对于传统实现的绝对优势了,高灵活性和高可扩展性。

假如产品经理要把图片放到内容上面或者点赞下面,只需要挪动ImageSectionController的位置就行了,她想怎么改就怎么改,甚至改回原来的需求,现在都将能从容应对?,按照原来的方式,小明肯定想打死产品经理?。

第四个需求:评论

评论区域看成单独一组,这一组里cell的数量不确定,得根据Feed中的评论数量生成cellModel,然后进行配置。

class CommentSectionController: ListSectionController {var object: Feed!lazy var viewModels: [CommentCellModel] = {let vms: [CommentCellModel]  = object.comments?.map({ (comment) -> CommentCellModel inlet vm = CommentCellModel()vm.comment = commentreturn vm}) ?? []return vms}()override func numberOfItems() -> Int {return viewModels.count}override func sizeForItem(at index: Int) -> CGSize {let width: CGFloat! = collectionContext?.containerSize(for: self).widthreturn CGSize(width: width, height: 44)}override func cellForItem(at index: Int) -> UICollectionViewCell {guard let cell = collectionContext?.dequeueReusableCell(withNibName: CommentCell.cellIdentifier, bundle: nil, for: self, at: index) as? CommentCell else { fatalError() }cell.bindViewModel(viewModels[index])cell.onClickDelete = {[weak self] (deleteCell) inguard let self = self else {return}self.collectionContext?.performBatch(animated: true, updates: { (batch) inlet deleteIndex: Int! = self.collectionContext?.index(for: deleteCell, sectionController: self)self.viewModels.remove(at: deleteIndex)batch.delete(in: self, at: IndexSet(integer: deleteIndex))}, completion: nil)}return cell}override func didUpdate(to object: Any) {self.object = object as? Feed}
}
复制代码

这里把点击commentCell的删除按钮事件代理出来给CommentSectionController处理,在闭包里先对cellModels数组删除,然后调用IGListKit的批量更新操作,在里面删除指定位置的cell。 最后同样的操作,在ListStackedSectionController里面再加一个就又ok了。

小明花了一天就重构完了这个页面,并且再也不怕后面产品经理提出的奇葩需求了。小明决定今天准时下班并且要去吃顿好的。

ListDiffable

ListDiffable协议,这属于IGListKit核心Diff算法的一部分,实现了ListDiffable协议才能使用diff算法,这个算法是计算新老两个数组前后数据变化增删改移关系的一个算法,时间复杂度是O(n),算是IGListKit的特色特点之一。使用的是Paul Heckel 的A technique for isolating differences between files 的算法。

总结

到目前为止,我们用子sectionController+ListStackedSectionController的方式完美实现了四个需求。这是我比较推荐的实现方式,但并不是唯一的,还有两种实现方式ListBindingSectionController(推荐实现)和只需要一个ListSectionController就能实现,已经在demo里实现,这里就不贴出来了,诸位可以去demo里理解。

IGListKit还能非常方便的实现多级列表、带多选功能的多级列表。

当然一样事物不可能只有优点,IGListKit同样拥有缺点,就目前为止我使用的经历来看,主要这几个可能有点坑。

  • 对autolayout支持不好。基本上都是要自己计算cell的size的,不过IGListKit将大cell分成小cell了,计算高度已经变的容易很多了,这个缺点可以忽略了

  • 因为是基于UICollectionView的,所以没有UITableView自带的滑动特性,这一点其实issue里有人提过,但其实这并不属于IGListKit应该考虑的范畴(官方人员这么回复的),目前我想到有两种解决方案,一是自己实现或用第三方库实现UICollectionViewCell的滑动,二是把UITableView嵌套进UICollectionViewCell,这个可能得好好封装一下了。

相信看到这里,诸位看官已经能明显感觉到IGListKit强大的能力,它充分展现了OOP的高内聚低耦合的思想,拥有高易用性、可扩展性、可维护性,体现了化整为零、化繁为简的哲学。

demo:github.com/Bruce-pac/I…, github.com/Bruce-pac/I…

转载于:https://juejin.im/post/5bfa5ad8e51d450cb4187ca0

抛弃UITableView,让所有列表页不再难构建相关推荐

  1. dedecms 栏目列表页链接如何优化

    这篇文章主要介绍了dedecms 栏目列表页链接如何优化,具有一定借鉴价值,需要的朋友可以参考下.希望大家阅读完这篇文章后大有收获.下面让小编带着大家一起了解一下.  dedecms 栏目列表页链接怎 ...

  2. php新闻列表页模块,PHP开发简单新闻发布系统之新闻列表页整体功能实现

    前面的章节我们说到了怎么实现简单的分页效果,这里我们就不再详细讲解了. 首先我们要把数据库中的新闻纪录展示出来 这里我们需要用到select 字段名1 , 字段2 , -- from 数据表名 来获取 ...

  3. ECSHOP 如何删除商品列表页 购买弹出 商品属性框后面的价格

    场景如图: 商品列表页 购买弹出 商品属性框是由js文件控制的. 打开JS文件js/common.js 问题一.弹出后属性多时,看不到[购买][取消]按钮 搜索代码: newDiv.style.hei ...

  4. iOS 仿百度外卖,饿了么-商品列表页

    今天带来的是仿百度外卖的商品列表页 下载地址 先看下效果图 这个主要是通过 - (void)selectRowAtIndexPath:(nullableNSIndexPath *)indexPath ...

  5. 数据结构与算法教程,让数据结构不再难懂,让算法不再难写

    据结构与算法不分家 数据结构包括数据对象集以及它们在计算机中的组织方式,即它们的逻辑结构和物理存储结构,一般我们可以认为数据结构指的是一组数据的存储结构. 算法就是操作数据的方法,即如何操作数据效率更 ...

  6. Django项目实战——14—(列表页热销排行、商品搜索、Haystack建立数据索引、渲染商品搜索结果、商品详情页)

    1.列表页热销排行 根据路径参数category_id查询出该类型商品销量前二的商品. 使用Ajax实现局部刷新的效果. 查询列表页热销排行数据 请求方式 请求参数:路径参数 响应结果:JSON {& ...

  7. 「后台列表页设计原则和技巧」

    摘要: 本文详述了管理后台列表页的设计原则和技巧,对于新手有很大的学习价值. 无论是什么类型的产品,几乎都会出现「列表页」,前台部分的列表页设计技巧已经有很多的介绍了,下面我以「电商系统」为例,谈谈业 ...

  8. 新闻列表页flex_微信小程序新闻网站列表页

    javascript 技术文章 开发 微信小程序新闻网站列表页 在app.json中可以设置所有文件的头部导航颜色 (是window属性的子属性) 在具体页面可以单独设置该页面的导航颜色 (直接写该属 ...

  9. NOW直播Flutter动态搜索列表页实现

    作者:腾讯NOW直播 - narutosun (孙帅) 前言 Flutter是Google使用Dart语言开发的移动应用开发框架,使用一套Dart代码就能构建高性能.高保真的iOS和Android应用 ...

最新文章

  1. 限流降级神器-哨兵(sentinel)原理分析
  2. 数学:拓展中国剩余定理
  3. 贺利坚老师汇编课程61笔记:操作显存数据即在屏幕上显示
  4. RESTORE DATAFILE TO A NEW LOCATION
  5. 踩着七彩祥云来接你的人不一定是意中人,也可能是阿里云
  6. 【光学】基于matlab光栅衍射仿真【含Matlab源码 502期】
  7. 生意参谋指数转化算法(2021最新)
  8. 步进电机c语言驱动原理,连接PC的步进电机简单驱动电路
  9. 软件测试面试题:智力题。
  10. excel 错位插入_Excel中图表插件导入错位的操作方法
  11. Docker-compose编排MySQL+Nacos+Seata
  12. URI中有关@符号的一些猥琐idea
  13. 系列九、vue中css样式字体设置为华文行楷
  14. SpringBoot mybatis多数据源配置,记录下我磕磕碰碰的三个月找工作经历
  15. 电精2 android,安卓街机模拟器|街机电精2(街机模拟器)安卓版 - 系统天堂
  16. Jmeter常用断言之响应断言详解
  17. SEC6 - MySQL 查询语句--------------进阶2:条件查询
  18. php面向对象之tian,php之面向对象
  19. 元器选型攻略之 电感
  20. Java IO篇:什么是零拷贝?

热门文章

  1. ubuntu 设置root用户密码并实现root用户登录
  2. 梯度下降法的三种形式-BGD、SGD、MBGD
  3. Android 5.0状态栏和导航栏
  4. 【洛谷 P5341】 [TJOI2019]甲苯先生和大中锋的字符串(后缀自动机)
  5. 吴恩达Coursera机器学习 - Chapter 4 多变量线性回归
  6. View事件分发机制(源码 API27)
  7. java08 Set
  8. 【LVS】负载均衡集群
  9. 【NetApp】关于Snapmirror强制停止的一点记录
  10. Wix学习整理(7)——在开始菜单中为HelloWorld添加卸载快捷方式