注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比

Android 中加载 UI 数据不是一件轻松的事,开发者经常需要处理各种边界情况。如各种生命周期和因为「配置更改」导致的 Activity 的销毁与重建。因此普遍处理方式是使用分层的架构。这样开发者就可以编写独立于 UI 的代码,而无需过多考虑生命周期,配...
继续阅读 »

Android 常用的分层架构

Android 中加载 UI 数据不是一件轻松的事,开发者经常需要处理各种边界情况。如各种生命周期和因为「配置更改」导致的 Activity 的销毁与重建。

「配置更改」的场景有很多:屏幕旋转,切换至多窗口模式,调整窗口大小,浅色模式与暗黑模式的切换,更改默认语言,更改字体大小等等

因此普遍处理方式是使用分层的架构。这样开发者就可以编写独立于 UI 的代码,而无需过多考虑生命周期,配置更改等场景。 例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(Domain Layer) 来保存业务逻辑,使用数据层(Data Layer)对上层屏蔽数据来源(数据可能来自远程服务,可能是本地数据库)。

img

表现层可以分成具有不同职责的组件:

  • View:处理生命周期回调,用户事件和页面跳转,Android 中主要是 Activity 和 Fragment

  • Presenter 或 ViewModel:向 View 提供数据,并不了解 View 所处的生命周期,通常生命周期比 View 长

Presenter 和 ViewModel 向 View 提供数据的机制是不同的,简单来说:

  • Presenter 通过持有 View 的引用并直接调用操作 View,以此向 View 提供数据

  • ViewModel 通过将可观察的数据暴露给观察者来向 View 提供数据

官方提供的可观察的数据 组件是 LiveData。Kotlin 1.4.0 正式版发布之后,开发者有了新的选择:StateFlowSharedFlow

最近网上流传出「LiveData 被弃用,应该使用 Flow 替代 LiveData」的声音。

LiveData 真的有那么不堪吗?Flow 真的适合你使用吗?

不人云亦云,只求接近真相。我们今天来讨论一下这两种组件。

ViewModel + LiveData

为了实现高效地加载 UI 数据,获得最佳的用户体验,应实现以下目标:

  • 目标1:已经加载的数据无需在「配置更改」的场景下再次加载

  • 目标2:避免在非活跃状态(不是 STARTEDRESUMED)下加载数据和刷新 UI

  • 目标3:「配置更改」时不会中断的工作

Google 官方在 2017 年发布了架构组件库:使用 ViewModel + LiveData 帮助开发者实现上述目标。

img

相信很多人在官方文档中见过这个图,ViewModelActivity/Fragment 的生命周期更长,不受「配置更改」导致 Activity/Fragment 重建的影响。刚好满足了目标 1 和目标 3。

LiveData 是可生命周期感知的。 新值仅在生命周期处于 STARTEDRESUMED 状态时才会分配给观察者,并且观察者会自动取消注册,避免了内存泄漏。 LiveData 对实现目标 1 和 目标 2 很有用:它缓存其持有的数据的最新值,并将该值自动分派给新的观察者。

LiveData 的特性

既然有声音说「LiveData 要被弃用了」,那么我们先对 LiveData 进行一个全面的了解。聊聊它能做什么,不能做什么,以及使用过程中有哪些要注意的地方。

LiveData 是 Android Jetpack Lifecycle 组件中的内容。属于官方库的一部分,Kotlin/Java 均可使用。

一句话概括 LiveDataLiveData 是可感知生命周期的,可观察的,数据持有者

它的能力和作用很简单:更新 UI

它有一些可以被认为是优点的特性:

  • 观察者的回调永远发生在主线程

  • 仅持有单个且最新的数据

  • 自动取消订阅

  • 提供「可读可写」和「仅可读」两个版本收缩权限

  • 配合 DataBinding 实现「双向绑定」

观察者的回调永远发生在主线程

这个很好理解,LiveData 被用来更新 UI,因此 ObserveronChanged() 方法在主线程回调。

img

背后的原理也很简单,LiveDatasetValue() 发生在主线程(非主线程调用会抛异常,postValue() 内部会切换到主线程调用 setValue())。之后遍历所有观察者的 onChanged() 方法。

仅持有单个且最新的数据

作为数据持有者(data holder),LiveData 仅持有 单个最新 的数据。

单个且最新,意味着 LiveData 每次持有一个数据,并且新数据会覆盖上一个。

这个设计很好理解,数据决定了 UI 的展示,绘制 UI 时肯定要使用最新的数据,「过时的数据」应该被忽略。

配合 Lifecycle,观察者只会在活跃状态下(STARTEDRESUMED)接收到 LiveData 持有的最新的数据。在非活跃状态下绘制 UI 没有意义,是一种资源的浪费。

自动取消订阅

这是 LiveData 可感知生命周期的重要表现,自动取消订阅意味着开发者无需手动写那些取消订阅的模板代码,降低了内存泄漏的可能性。

背后原理是在生命周期处于 DESTROYED 时,移除观察者。

img

提供「可读可写」和「仅可读」两个版本

img

public abstract class LiveData<T> {
@MainThread
protected void setValue(T value) {
// ...
}

protected void postValue(T value) {
// ...
}

@Nullable
public T getValue() {
// ...
}
}

public class MutableLiveData<T> extends LiveData<T> {
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) {
super.setValue(value);
}
}

抽象类 LiveDatasetValue()postValue() 是 protected,而其实现类 MutableLiveData 均为 public。

LiveData 提供了 mutable(MutableLiveData) 和 immutable(LiveData) 两个类,前者「可读可写」,后者「仅可读」。通过权限的细化,让使用者各取所需,避免由于权限泛滥导致的数据异常。

img

class SharedViewModel : ViewModel() {
private val _user : MutableLiveData<User> = MutableLiveData()

val user : LiveData<User> = _user

fun setUser(user: User) {
_user.posetValue(user)
}
}

配合 DataBinding 实现「双向绑定」

LiveData 配合 DataBinding 可以实现 更新数据自动驱动 UI 变化,如果使用「双向绑定」还能实现 UI 变化影响数据的变化。


以下也是 LiveData 的特性,但我不会将其归类为「设计缺陷」或「LiveData 的缺点」。作为开发者应了解这些特性并在使用过程中正确处理它们。

  • value 是 nullable 的

  • 在 fragment 订阅时需要传入正确的 lifecycleOwner

  • LiveData 持有的数据是「事件」时,可能会遇到「粘性事件

  • LiveData 是不防抖的

  • LiveDatatransformation 工作在主线程

value 是 nullable 的

img

@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}

LiveData#getValue() 是可空的,使用时应该注意判空。

使用正确的 lifecycleOwner

fragment 调用 LiveData#observe() 方法时传入 thisviewLifecycleOwner 是不一样的。

原因之前写过,此处不再赘述。感兴趣的小伙伴可以移步查看

AS 在 lint 检查时会避免开发者犯此类错误。

img

粘性事件

官方在 [] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例) 一文中描述了一种「数据只会消费一次」的场景。如展示 Snackbar,页面跳转事件或弹出 Dialog。

由于 LiveData 会在观察者活跃时将最新的数据通知给观察者,则会产生「粘性事件」的情况。

如点击 button 弹出一个 Snackbar,在屏幕旋转时,lifecycleOwner 重建,新的观察者会再次调用 Livedata#observe(),因此 Snackbar 会再次弹出。

解决办法是:将事件作为状态的一部分,在事件被消费后,不再通知观察者。这里推荐两种解决方案:

默认不防抖

setValue()/postValue() 传入相同的值多次调用,观察者的 onChanged() 会被多次调用。

严格讲这不算一个问题,看具体的业务场景,处理也很容易,官方在 Transformations 中提供了 distinctUntilChanged() 方法,配合官方提供的扩展函数,如下使用即可:

img

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewModel.headerText.distinctUntilChanged().observe(viewLifecycleOwner) {
header.text = it
}
}

transformation 工作在主线程

有些时候我们从 repository 层拿到的数据需要进行处理,例如从数据库获得 User List,我们想根据 id 获取某个 User。

此时我们可以借助 MediatorLiveDataTransformatoins 来实现:

img

class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}

mapswitchMap 内部均是使用 MediatorLiveData#addSource() 方法实现的,而该方法会在主线程调用,使用不当会有性能问题。

img

@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}

我们可以借助 Kotlin 协程和 RxJava 实现异步任务,最后在主线程上返回 LiveData。如 androidx.lifecycle:lifecycle-livedata-ktx 提供了这样的写法

img

val result: LiveData<Result> = liveData {
val data = someSuspendingFunction() // 协程中处理
emit(data)
}

LiveData 小结

  • LiveData 作为一个 可感知生命周期的,可观察的,数据持有者,被设计用来更新 UI

  • LiveData 很轻,功能十分克制,克制到需要配合 ViewModel 使用才能显示其价值

  • 由于 LiveData 专注单一功能,因此它的一些方法使用上是有局限性的,即通过设计来强制开发者按正确的方式编码(如观察者仅在主线程回调,避免了开发者在子线程更新 UI 的错误操作)

  • 由于 LiveData 专注单一功能,如果想在表现层之外使用它,MediatorLiveData 的操作数据的能力有限,仅有的 mapswitchMap 发生在主线程。可以在 switchMap 中使用协程或 RxJava 处理异步任务,最后在主线程返回 LiveData。如果项目中使用了 RxJavaAutoDispose,甚至可以不使用 LiveData,关于 Kotlin 协程的 Flow,我们后文介绍。

  • 笔者不喜欢将 LiveData 改造成 bus 使用,让组件做其分内的事(此条属于个人观点)

Flow

Flow 是 Kotlin 语言提供的功能,属于 Kotlin 协程的一部分,仅 Kotlin 使用。

Kotlin 协程被用来处理异步任务,而 Flow 则是处理异步数据流。

那么 suspend 方法和 Flow 的区别是什么?各自的使用场景是哪些?

一次性调用(One-shot Call)与数据流(data stream)

img

假如我们的 app 的某一屏里显示以下元素,其中红框部分实时性不高,不必很频繁的刷新,转发和点赞属于实时性很高的数据,需要定时刷新。

img

对于实时性不高的数据,我们可以使用 Kotlin 协程处理(此处数据的请求是异步任务):

suspend fun loadData(): Data

uiScope.launch {
 val data = loadData()
 updateUI(data)
}

而对于实时性较高的数据,挂起函数就无能为力了。有的小伙伴可能会说:「返回个 List 不就行了嘛」。其实无论返回什么类型,这种操作都是 One-shot Call,一次性的请求,有了结果就结束。

示例中的点赞和转发,需要一个 数据是异步计算的,能够 按顺序 提供 多个值 的结构,在 Kotlin 协程中我们有 Flow。

fun dataStream(): Flow<Data>

uiScope.launch {
 dataStream().collect { data ->
    updateUI(data)
}
}

当点赞或转发数发生变化时,updateUI() 会被执行,UI 根据最新的数据更新

Flow 的三驾马车

FLow 中有三个重要的概念:

  • 生产者(Producer)

  • 消费者(Consumer)

  • 中介(Intermediaries)

生产者提供数据流中的数据,得益于 Kotlin 协程,Flow 可以 异步地生产数据

消费者消费数据流内的数据,上面的示例中,updateUI() 方法是消费者。

中介可以对数据流中的数据进行更改,甚至可以更改数据流本身,我们可以借助官方视频中的动画来理解:

img

在 Android 中,数据层的 DataSource/Repository 是 UI 数据的生产者;而 view/ViewModel 是消费者;换一个角度,在表现层中,view 是用户输入事件的生产者(例如按钮的点击),其它层是消费者。

「冷流」与「热流」

你可能见过这样的描述:「流是冷的」

img

简单来说,冷流指数据流只有在有消费者消费时才会生产数据。

val dataFlow = flow {
   // 代码块只有在有消费者 collect 后才会被调用
   val data = dataSource.fetchData()
   emit(data)
}

...

dataFlow.collect { ... }

有一种特殊的 Flow,如 StateFlow/SharedFlow ,它们是热流。这些流可以在没有活跃消费者的情况下存活,换句话说,数据在流之外生成然后传递到流。

BroadcastChannel` 未来会在 Kotlin 1.6.0 中弃用,在 Kotlin 1.7.0 中删除。它的替代者是 `StateFlow`  `SharedFlow

StateFlow

StateFlow 也提供「可读可写」和「仅可读」两个版本。

SateFlow` 实现了 `SharedFlow`,`MutableStateFlow` 实现 `MutableSharedFlow

img

StateFlowLiveData 十分像,或者说它们的定位类似。

StateFlowLiveData 有一些相同点:

  • 提供「可读可写」和「仅可读」两个版本(StateFlowMutableStateFlow

  • 它的值是唯一的

  • 它允许被多个观察者共用 (因此是共享的数据流)

  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的

  • 支持 DataBinding

它们也有些不同点:

  • 必须配置初始值

  • value 空安全

  • 防抖

MutableStateFlow 构造方法强制赋值一个非空的数据,而且 value 也是非空的。这意味着 StateFlow 永远有值

img

StateFlow 的 emit()tryEmit() 方法内部实现是一样的,都是调用 setValue()

StateFlow 默认是防抖的,在更新数据时,会判断当前值与新值是否相同,如果相同则不更新数据。

img

SharedFlow

SateFlow 一样,SharedFlow 也有两个版本:SharedFlowMutableSharedFlow

img

那么它们有什么不同?

  • MutableSharedFlow 没有起始值

  • SharedFlow 可以保留历史数据

  • MutableSharedFlow 发射值需要调用 emit()/tryEmit() 方法,没有 setValue() 方法

img

MutableStateFlow 不同,MutableSharedFlow 构造器中是不能传入默认值的,这意味着 MutableSharedFlow 没有默认值。

val mySharedFlow = MutableSharedFlow<Int>()
val myStateFlow = MutableStateFlow<Int>(0)
...
mySharedFlow.emit(1)
myStateFlow.emit(1)

SateFlowSharedFlow 还有一个区别是 SateFlow 只保留最新值,即新的订阅者只会获得最新的和之后的数据。

SharedFlow 根据配置可以保留历史数据,新的订阅者可以获取之前发射过的一系列数据。

后文会介绍背后的原理

它们被用来应对不同的场景:UI 数据是状态还是事件

状态(State)与事件(Event)

状态可以是的 UI 组件的可见性,它始终具有一个值(显示/隐藏)

而事件只有在满足一个或多个前提条件时才会触发,不需要也不应该有默认值

为了更好地理解 SateFlowSharedFlow 的使用场景,我们来看下面的示例:

  1. 用户点击登录按钮

  2. 调用服务端验证登录合法性

  3. 登录成功后跳转首页

我们先将步骤 3 视为 状态 来处理:

img

使用状态管理还有与 LiveData 一样的「粘性事件」问题,如果在 ViewNavigationState 中我们的操作是弹出 snackbar,而且已经弹出一次。在旋转屏幕后,snackbar 会再次弹出。

img

如果我们将步骤 3 作为 事件 处理:

img

使用 SharedFlow 不会有「粘性事件」的问题,MutableSharedFlow 构造函数里有一个 replay 的参数,它代表着可以对新订阅者重新发送多个之前已发出的值,默认值为 0。

img

SharedFlow 在其 replayCache 中保留特定数量的最新值。每个新订阅者首先从 replayCache 中取值,然后获取新发射的值。replayCache 的最大容量是在创建 SharedFlow 时通过 replay 参数指定的。replayCache 可以使用 MutableSharedFlow.resetReplayCache 方法重置。

replay 为 0 时,replayCache size 为 0,新的订阅者获取不到之前的数据,因此不存在「粘性事件」的问题。

StateFlowreplayCache 始终有当前最新的数据:

img

至此, StateFlowSharedFlow 的使用场景就很清晰了:

状态(State)用 StateFlow ;事件(Event)用 SharedFlow

StateFlow,SharedFlow 与 LiveData 的使用对比

LiveData StateFlow SharedFlow 在 ViewModel 中的使用

上图分别展示了 LiveDataStateFlowSharedFlowViewModel 中的使用。

其中 LiveDataViewModel 中使用 EventLiveData 处理「粘性事件

FlowViewModel 中使用 SharedFlow 处理「粘性事件

emit()` 方法是挂起函数,也可以使用 `tryEmit()

LiveData StateFlow SharedFlow 在 Fragment 中的使用

注意:Flow 的 collect 方法不能写在同一个 lifecycleScope

flowWithLifecyclelifecycle-runtime-ktx:2.4.0-alpha01 后提供的扩展方法

Flow 在 fragment 中的使用要比 LiveData 繁琐很多,我们可以封装一个扩展方法来简化:

img

关于 repeatOnLifecycle 的设计问题,可以移步 设计 repeatOnLifecycle API 背后的故事

使用 collect 方法时要注意一个问题。

img

这种写法是错误的!

viewModel.headerText.collect 在协程被取消前会一直挂起,这样后面的代码便不会执行。

Flow 与 RxJava

FlowRxJava 的定位很接近,限于篇幅原因,此处不展开讲,本节只罗列一下它们的对应关系:

  • Flow = (cold) Flowable / Observable / Single

  • Channel = Subjects

  • StateFlow = BehaviorSubjects (永远有值)

  • SharedFlow = PublishSubjects (无初始值)

  • suspend function = Single / Maybe / Completable

参考文档与推荐资源

总结

  • LiveData 的主要职责是更新 UI,要充分了解其特性,合理使用

  • Flow 可分为生产者,消费者,中介三个角色

  • 冷流和热流最大的区别是前者依赖消费者 collect 存在,而热流一直存在,直到被取消

  • StateFlowLiveData 定位相似,前者必须配置初始值,value 空安全并且默认防抖

  • StateFlowSharedFlow 的使用场景不同,前者适用于「状态」,后者适用于「事件」

回到文章开头的话题,LiveData 并没有那么不堪,由于其作用单一,功能简单,简单便意味着不易出错。所以在表现层中ViewModel 向 view 暴露 LiveData 是一个不错的选择。而在 RepositoryDataSource 中,我们可以利用 LiveData + 协程来处理数据的转换。当然,我们也可以使用功能更强大的 Flow

LiveDataStateFLowSharedFlow,它们都有着各自的使用场景。并且如果使用不当,都会或多或少地遇到一些所谓的「坑」。因此在使用某个组件时,要充分了解其设计缘由以及相关特性,否则就会掉进陷阱,收到不符合预期的行为。

关于我

人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~

我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。


作者:Flywith24
来源:https://juejin.cn/post/7007602776502960165

收起阅读 »

基于react/vue开发一个专属于程序员的朋友圈应用

前言今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微...
继续阅读 »

前言

今天本来想开源自己写的CMS应用的,但是由于五一期间笔者的mac电脑突然崩溃了,所有数据无法恢复,导致部分代码丢失,但庆幸的是cms的打包文件已上传服务器,感兴趣的朋友可以在文末链接中访问查看。

今天要写的H5朋友圈也是基于笔者开发的cms搭建的,我将仿照微信朋友圈,带大家一起开发一个能发布动态(包括图片上传)的朋友圈应用。有关服务端部分笔者在本文中不会细讲,后续会在cms2.0中详细介绍。

你将收获

  • 使用umi快速创建一个H5移动端应用

  • 基于react-lazy-load实现图片/内容懒加载

  • 使用css3基于图片数量动态改变布局

  • 利用FP创建一个朋友圈form

  • 使用rc-viewer查看/旋转/缩放朋友圈图片

  • 基于axios + formdata实现文件上传功能

  • ZXCMS介绍

应用效果预览

朋友圈列表


查看朋友圈图片


发布动态


正文

在开始文章之前,笔者想先粗略总结一下开发H5移动端应用需要考虑的点。对于任何移动端应用来说,我们都要考虑如下问题:

  • 首屏加载时间

  • 适配问题

  • 页面流畅度

  • 动画性能

  • 交互友好

  • 提供用户反馈 这些不仅仅是前端工程师需要考虑的问题,也是产品经理和交互设计师考虑的范畴。当然还有很多实际的考虑点需要根据自身需求去优化,以上几点大致解决方案如下:

  1. 提高首屏加载时间 可以采用资源懒加载+gzip+静态资源CDN来优化,并且提供加载动画来降低用户焦虑。

  2. 适配问题 移动端适配问题可以通过js动态设置视口宽度/比率或者采用css媒介查询来处理,这块市面上已经有非常成熟的方案

  3. 页面流畅度 我们可以在body上设置-webkit-overflow-scrolling:touch;来提高滚动流畅度,并且可以在a/img标签上使用 -webkit-touch-callout: none来禁止长按产生菜单栏。

  4. 动画性能 为了提高动画性能, 我们可以将需要变化的属性采用transform或者使用absolute定位代替,transform不会导致页面重绘。

  5. 提供用户反馈 提供友好的用户反馈我们可以通过合理设置toastmodal等来控制

以上介绍的只是移动端优化的凤毛麟角,有关前端页面性能优化的方案还有很多,笔者在之前的文章中也详细介绍过,下面我们进入正文。

1. 使用umi快速创建一个应用

笔者将采用umi作为项目的前端集成解决方案,其提供了非常多了功能,使用起来也非常方便,并且对于antd和antd-mobile自动做了按需导入,所以熟悉react的朋友可以尝试一下,本文的方案对于vue选手来说也是适用的,因为任何场景下,方法和思维模式都是跨语言跨框架的。

目前umi已经升级到3.0,本文所使用的是2.0,不过差异不是很大,大家可以放心使用3.0. 具体使用步骤如下

// umi2.0
// 新建项目目录
mkdir friendcircle
// 创建umi项目
cd friendcircle
yarn create umi
// 安装依赖
yarn
yarn add antd-moblie

这样一个umi项目就创建好了。

2. 基于react-lazy-load实现图片/内容懒加载

在项目创建好之后,我们先分析我们需要用到那些技术点:


笔者在设计时研究了很多懒加载实现方式,目前采用react-lazy-load来实现,好处是支持加载事件通知,比如我们需要做埋点或者广告上报等功能时非常方便。当然大家也可以自己通过observer API去实现,具体实现方案笔者在几个非常有意思的javascript知识点总结文章中有所介绍。 具体使用方式:

<LazyLoad key={item.uid} overflow height={280} onContentVisible={onContentVisible}>
  // 需要懒加载的组件
  <ComponentA />
</LazyLoad>

react-lazy-load使用方式非常简单,大家不懂的可以在官网学习了解。

3. 使用css3基于图片数量动态改变布局

目前在朋友圈列表页有个核心的需求就是我们需要在用户传入不同数量的图片时,要有不同的布局,就像微信朋友圈一样,主要作用就是为了让用户尽可能多的看到图片,提高用户体验,如下图所示例子:


我们用js实现起来很方便,但是对性能及其不友好,而且对于用户发布的每一条动态的图片都需要用js重新计算一遍,作为一个有追求的程序员是不可能让这种情况发生的,所以我们用css3来实现,其实有关这种实现方式笔者在之前的css3高级技巧的文章中有详细介绍,我们这里用到了子节点选择器,具体实现如下:

.imgItem {
  margin-right: 6px;
  margin-bottom: 10px;
  &:nth-last-child(1):first-child {
    margin-right: 0;
    width: 100%;
  }
  &:nth-last-child(2):first-child,
  &:nth-last-child(3):first-child,
  &:nth-last-child(4):first-child,
  &:first-child:nth-last-child(n+2) ~ div {
    width:calc(50% - 6px);
    height: 200px;
    overflow: hidden;
  }
  &:first-child:nth-last-child(n+5),
  &:first-child:nth-last-child(n+5) ~ div {
    width: calc(33.33333% - 6px);
    height: 150px;
    overflow: hidden;
  }
}

以上代码中我们对于一张图片,2-4张图片,5张以上的图片分别设置了不同的尺寸,这样就可以实现我们的需求了,还有一个要注意的是,当用户上传不同尺寸的图片时,有可能出现高低不一致的情况,这个时候为了显示一致,我们可以使用img样式中的object-fit属性,有点类似于background-size,我们可以把img便签看作一个容器,里面的内容如何填充这个容器,完全用object-fit来设置,具体属性如下:

  • fill 被替换的内容正好填充元素的内容框。整个对象将完全填充此框。如果对象的宽高比与内容框不相匹配,那么该对象将被拉伸以适应内容框

  • contain 被替换的内容将被缩放,以在填充元素的内容框时保持其宽高比。 整个对象在填充盒子的同时保留其长宽比,因此如果宽高比与框的宽高比不匹配,该对象将被添加“黑边”

  • cover 被替换的内容在保持其宽高比的同时填充元素的整个内容框。如果对象的宽高比与内容框不相匹配,该对象将被剪裁以适应内容框

  • scale-down 内容的尺寸与 none 或 contain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些

  • none 被替换的内容将保持其原有的尺寸

所以为了让图片保持一致,我们这么设置img标签的样式:

img {
width: 100%;
height: 100%;
object-fit: cover;
}

4. 利用FP创建一个朋友圈form

FP是笔者开源的一个表单配置平台,主要用来定制和分析各种表单模型,界面如下:



通过该平台可以定制各种表单模版并分析表单数据。这里朋友圈功能我们只需要配置一个简单的朋友圈发布功能即可,如下:


由于笔者电脑数据丢失导致代码部分损失,感兴趣可以了解一下。

5. 使用rc-viewer查看/旋转/缩放朋友圈图片

对于朋友圈另一个重要的功能就是能查看每一条动态的图片,类似于微信朋友圈的图片查看器,这里笔者采用第三方开源库rc-viewer来实现,具体代码如下:

<RcViewer options={{title: 0, navbar: 0, toolbar: 0}} ref={imgViewRef}>
<div className={styles.imgBox}>
  {
    item.imgUrls.map((item, i) => {
      return <div className={styles.imgItem} key={i}>
        <img src={item} alt=""/>
      </div>
    })
  }
</div>  
</RcViewer>

由上代码可知我们只需要在RcViewer组件里写我们需要的查看的图片结构就行了,其提供了很多配置选项可是使用,这里笔者在option中配置了title,navbar,toolbar均为0,意思是不显示这些功能,因为移动端只需要有基本的查看,缩放,切换图片功能即可,尽可能轻量化。效果如下:


当我们点击动态中的某一张图片时,我们可以看到它的大图,并通过手势进行切换。

6. 基于axios + formdata实现文件上传功能

实现文件上传,除了采用antd的upload组件,我们也可以结合http请求库和formdata来实现,为了支持多图上传并保证时机,我们采用async await函数,具体代码如下:

const onSubmit = async () => {
  // ... something code
  const formData = new FormData()
  for(let i=0; i< files.length; i++) {
    formData.delete('file')
    formData.append('file', files[i].file)
    try{
      const res = await req({
        method: 'post',
        url: '/files/upload/tx',
        data: formData,
        headers: {
            'Content-Type': 'multipart/form-data'
        }
      });
      // ... something co
    }catch(err) {
      Toast.fail('上传失败', 2);
    }
  }

其中req是笔者基于axios封装的http请求库,支持简单的请求/响应拦截,感兴趣的朋友可以参考笔者源码。

7. ZXCMS介绍

ZXCMS是笔者开发的一个商业版CMS,可以快速搭建自己的社区,博客等,并且集成了表单定制平台,配置中心,数据分发中心等功能,后期会扩展H5可视化搭建平台和PC端建站平台,成为一个更加只能强大的开源系统。设计架构如下:


具体界面如下:

一个笔者配置的社区平台:


文章详情页:



社区支持评论,搜索文章等功能。以下介绍后台管理系统:





简单介绍一下,后期笔者会专门出文章介绍具体实现方式和源码设计。

8. 源码地址

由于笔者电脑数据丢失,只能找到部分源码,所以大家可以参考以下地址:

开源不易,欢迎支持~


作者:徐小夕
来源:https://juejin.cn/post/6844904150417801224

收起阅读 »

如何利用performance进行性能优化

可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。各种配置及说明如图所示: 观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、...
继续阅读 »

Performance 可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。

配置 Performance

各种配置及说明如图所示:20210430104643828.png


Performance 不仅可以录制加载阶段的性能数据,还可以录制交互阶段,不过交互阶段的录制需要手动停止录制过程。

观察下图的报告页,我们可以将它分为三个主要的部分,分别为概览面板、性能指标面板和详情面板。image.png


在概览面板中,Performance 就会将几个关键指标,诸如页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存) 等,按照时间顺序做成图表的形式展现出来,可以参看上图。

  • 如果 FPS 图表上出现了红色块,那么就表示红色块附近渲染出一帧所需时间过久,帧的渲染时间过久,就有可能导致页面卡顿。

  • 如果 CPU 图形占用面积太大,表示 CPU 使用率就越高,那么就有可能因为某个 JavaScript 占用太多的主线程时间,从而影响其他任务的执行。

除了以上指标以外,概览面板还展示加载过程中的几个关键时间节点,如 FP、LCP、DOMContentLoaded、Onload 等事件产生的时间点。

Main 指标

在性能面板中,记录了非常多的性能指标项,比如 Main 指标记录渲染主线程的任务执行过程,Compositor 指标记录了合成线程的任务执行过程,GPU 指标记录了 GPU 进程主线程的任务执行过程。有了这些详细的性能数据,就可以帮助我们轻松地定位到页面的性能问题。

简而言之,我们通过概览面板来定位问题的时间节点,然后再使用性能面板分析该时间节点内的性能数据。具体地讲,比如概览面板中的 FPS 图表中出现了红色块,那么我们点击该红色块,性能面板就定位到该红色块的时间节点内了。

因为浏览器的渲染机制过于复杂,所以渲染模块在执行渲染的过程中会被划分为很多子阶段,输入的 HTML 数据经过这些子阶段,最后输出屏幕上的像素,我们把这样的一个处理流程叫做渲染流水线。一条完整的渲染流水线包括了解析 HTML 文件生成 DOM、解析 CSS 生成 CSSOM、执行 JavaScript、样式计算、构造布局树、准备绘制列表、光栅化、合成、显示等一系列操作。

渲染流水线主要是在渲染进程中执行的,在执行渲染流水线的过程中,渲染进程又需要网络进程、浏览器进程、GPU 等进程配合,才能完成如此复杂的任务。另外在渲染进程内部,又有很多线程来相互配合。具体的工作方式你可以参考下图:image.pngimage.png


观察上图,一段段横条代表执行一个个任务,长度越长,花费的时间越多;竖向代表该任务的执行记录。我们知道主线程上跑了特别多的任务,诸如渲染流水线的大部分流程,JavaScript 执行、V8 的垃圾回收、定时器设置的回调任务等等,因此 Main 指标的内容非常多,而且非常重要,所以我们在使用 Perofrmance 的时候,大部分时间都是在分析 Main 指标。

任务 vs 过程

渲染进程中维护了消息队列,如果通过 SetTimeout 设置的回调函数,通过鼠标点击的消息事件,都会以任务的形式添加消息队列中,然后任务调度器会按照一定规则从消息队列中取出合适的任务,并让其在渲染主线程上执行。

Main 指标就记录渲染主线上所执行的全部任务,以及每个任务的详细执行过程image.png


观察上图,图上方有很多一段一段灰色横条,每个灰色横条就对应了一个任务,灰色长条的长度对应了任务的执行时长。通常,渲染主线程上的任务都是比较复杂的,如果只单纯记录任务执行的时长,那么依然很难定位问题,因此,还需要将任务执行过程中的一些关键的细节记录下来,这些细节就是任务的过程,灰线下面的横条就是一个个过程,同样这些横条的长度就代表这些过程执行的时长。

直观地理解,你可以把任务看成是一个 Task 函数,在执行 Task 函数的过程中,它会调用一系列的子函数,这些子函数就是我们所提到的过程。为了让你更好地理解,我们来分析下面这个任务的图形:image.png


观察上面这个任务记录的图形,你可以把该图形看成是下面 Task 函数的执行过程:  

function A(){
A1()
A2()
}
function Task(){
A()
B()
}
Task()  

分析页面加载过程

结合 Main 指标来分析页面的加载过程。先来分析一个简单的页面,代码如下所示:

<html>
<head>
<title>Main</title>
<style>
area {
border: 2px ridge;
}
box {
background-color: rgba(106, 24, 238, 0.26);
height: 5em;
margin: 1em;
width: 5em;
}
</style>
</head>

<body>
<div class="area">
<div class="box rAF"></div>
</div>
<br>
<script>
function setNewArea() {
let el = document.createElement('div')
el.setAttribute('class', 'area')
el.innerHTML = '<div class="box rAF"></div>'
document.body.append(el)
}
setNewArea()
</script>
</body>
</html>

可以看出,它只是包含了一段 CSS 样式和一段 JavaScript 内嵌代码,其中在 JavaScript 中还执行了 DOM 操作了,我们就结合这段代码来分析页面的加载流程。

首先生成报告页,再观察报告页中的 Main 指标,由于阅读实际指标比较费劲,所以先手动绘制了一些关键的任务和其执行过程,如下图所示:image.png


通过上面的图形我们可以看出,加载过程主要分为三个阶段,它们分别是:

  • 导航阶段,该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。

  • 解析 HTML 数据阶段,该阶段主要是将接收到的 HTML 数据转换为 DOM 和 CSSOM。

  • 生成可显示的位图阶段,该阶段主要是利用 DOM 和 CSSOM,经过计算布局、生成层树 (LayerTree)、生成绘制列表 (Paint)、完成合成等操作,生成最终的图片。

那么接下来,我就按照这三个步骤来介绍如何解读 Main 指标上的数据。

导航阶段

当你点击了 Performance 上的重新录制按钮之后,浏览器进程会通知网络进程去请求对应的 URL 资源;一旦网络进程从服务器接收到 URL 的响应头,便立即判断该响应头中的 content-type 字段是否属于 text/html 类型;如果是,那么浏览器进程会让当前的页面执行退出前的清理操作,比如执行 JavaScript 中的 beforunload 事件,清理操作执行结束之后就准备显示新页面了,这包括了解析、布局、合成、显示等一系列操作。image.png


当你点击重新加载按钮后,当前的页面会执行上图中的这个任务:

  • 该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。

  • 接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。

  • 接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。

  • 这些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。

  • 等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。


解析 HTML 数据阶段

这个阶段的主要任务就是通过解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM。那么继续来分析这个阶段的图形,看看它到底是怎么执行的?可以观看下图:image.png


观察上图这个图形,可以看出,其中一个主要的过程是 HTMLParser,顾名思义,这个过程是用来解析 HTML 文件,解析的就是上个阶段接收到的 HTML 数据。

  1. 在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。

  2. 要执行一段脚本我们需要首先编译该脚本,于是在 Evalute Script 过程中,先进入了脚本编译过程,也就是图中的 Complie Script。脚本编译好之后,就进入程序执行过程,执行全局代码时,V8 会先构造一个 anonymous 过程,在执行 anonymous 过程中,会调用 setNewArea 过程,setNewArea 过程中又调用了 createElement,由于之后调用了 document.append 方法,该方法会触发 DOM 内容的修改,所以又强制执行了 ParserHTML 过程生成的新的 DOM。

  3. DOM 生成完成之后,会触发相关的 DOM 事件,比如典型的 DOMContentLoaded,还有 readyStateChanged。

生成可显示位图阶段

生成了 DOM 和 CSSOM 之后,就进入了第三个阶段:生成页面上的位图。通常这需要经历布局 (Layout)、分层、绘制、合成等一系列操作,同样,将第三个阶段的流程也放大了,如下图所示:


image.png


结合上图,我们可以发现,在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、load、pageshow。具体地讲,如果你使用 JavaScript 监听了这些事件,那么这些监听的函数会被渲染主线程依次调用。

接下来就正式进入显示流程了,大致过程如下所示。

  1. 首先执行布局,这个过程对应图中的 Layout。

  2. 然后更新层树 (LayerTree),这个过程对应图中的 Update LayerTree。

  3. 有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程就称为 Paint。

  4. 准备每层的绘制列表之后,就需要利用绘制列表来生成相应图层的位图了,这个过程对应图中的 Composite Layers。

走到了 Composite Layers 这步,主线程的任务就完成了,接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,你也可以对照着 Composite、Raster 和 GPU 这三个指标来分析,参考下图:


image.png

  1. 首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。

  2. 合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。

  3. 当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。

  4. 然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。

本文解答了个人一个长期困扰的问题:在某些情况下,比如网速比较慢或者页面内容很多的时候,页面是一点一点的显示出来的,原本以为是网络数据是加载一点就渲染一点,其实不是的,数据在导航阶段就已经全部获取回来了。之所以会慢慢渲染出来,是因为浏览器的显示频率是60hz,也就是16.67ms就刷新下浏览器,但是在16.67ms内,渲染流水线可能只进行到一半,但是这个时候也要把渲染一半的画面显示出来,所以就会看到页面是一点一点的绘制出来的。


作者:小p
来源:juejin.cn/post/7095647383488299044
收起阅读 »

“𠈌”计划4月优秀环友表彰及5月获选标准

环信4月发起“𠈌”计划,以传递“人人为我,我为人人”的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,送上优秀环友墙并发放环信大礼包,下面我们康康首批优秀环友们吧~社区/社群优...
继续阅读 »

环信4月发起“𠈌”计划,以传递“人人为我,我为人人”的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,送上优秀环友墙并发放环信大礼包,下面我们康康首批优秀环友们吧~

社区/社群优秀环友

环信官方群聚集了数千名开发者,这里有2名活跃的老朋友 @麦田稻草人 @孤狼☞小九 ,陪伴是最长情的告白,他们多年坚守在环信群里帮群友解答问题,是环信编外员工?很多群友傻傻分不清。他们与环信的故事从那一年开始。。

麦田稻草人

从第一次修改环信的头像问题,到现在已经接触环信六年多了,见识过环信的贴心服务,也见证过超级大群的热闹,也算是见证了环信论坛的搭建历程,见证过环信从国内到国外再到国内的过程。一点点熟悉环信,从简单修改到后面修改sdk,感慨良多

孤狼☞小九
不知不觉间,已经接触环信七年多了,见识过环信的贴心服务,也见识过环信QQ群友的互帮互助,也算是参与到环信论坛的搭建与完善.见证过环信功能的一步步完善,也见证过环信的从国内出圈到国外再回归到国内的过程.一点点一步步的熟悉了环信,从刚开始集成时的茫然不知,到后面的各种自定义消息.所遇帮助颇多,感触良多


开源项目贡献者

环信开源项目频道(https://www.imgeek.org/code/)主要展示开发者们基于环信IM的奇思妙想,做出的众多优秀开源项目。为广大开发者提供便捷易用的开源作品,帮助赶工期的煎熬党们开发效率起飞。

感谢以下5名开发者,对环信开源项目卓越的贡献。希望更多开发者加入到开源项目贡献天团,环信有数百名产品经理天团帮你出谋划策哦。
@GraysonGao@Friday@魏头儿@我爱捏猫肚@穿裤衩闯天下(lzan13)



环信内容共建者

IMGeek社区不仅承载帮助使用环信SDK的开发者解决问题,也希望通过精选优质的技术文章帮助更多技术从业者积累知识不断进步。你可能经常看到这些人的身影,

@柳天明@zuyu@费城@马师傅@王二蛋和他的张大花@雨淋湿了天空@上帝之眼
他们推荐了数百条精选文章,连载了一系列个人原创文章,感谢他们的贡献~希望更多开发者5月份加入到内容共建队伍😀


恭喜以上14名开发者首批入选优秀环友墙,请联系微信环信冬冬(vx:huanxin-hh)领取4月礼包及纪念徽章。




“𠈌”计划5月礼包发放标准:

1、社区/社群活跃用户 --2-5人
2、IMGeek发文章2篇原创或5篇转载 不限人数
3、反馈IM SDK bug并技术确认 不限人数
符合5月领取礼包条件的开发者本帖回复,然后坐等福利小助手联系你~


收起阅读 »

关于Kotlin的一些小事

一、碎碎念 说实话,原本是没有这个系列的,或者说是没想过去建立这个系列。 虽然,但是,所以就有了(别问为什么?) val var 声明变量 被 val 修饰的变量:被 final 修饰,且只会为其提供 getter() 而不会提供 setter() 方法。 ...
继续阅读 »

一、碎碎念


说实话,原本是没有这个系列的,或者说是没想过去建立这个系列。


虽然,但是,所以就有了(别问为什么?)


val var 声明变量



  • 被 val 修饰的变量:被 final 修饰,且只会为其提供 getter() 而不会提供 setter() 方法。

    • 因为被 final 修饰的值,只能被赋值一次;所以不会有 setter()。

    • 是否添加了"?":声明变量的时候会根据是否有"?",将变量添加 NotNull 或者 Nullable 注解。



  • 被 var 修饰的变量:普通定义变量的方式,且会同时提供 setter()、getter() 方法。

    • 是否添加了"?":如果没有?,则setter()方法的入参会被标记位NotNull;如果有?,则setter()方法的入参会被标记为Nullable。





?. 操作符



  • 对于声明为 var 的变量,在调用方法时会需要加上 ?. 操作符来进行判空处理,避免空指针。实现空安全。

  • 实现原理:通过在方法内部构造一个局部变量,然后赋值为该数据,紧接着通过判断局部变量是否为空?如果为空,则进行预设的处理;如果不为空,则直接进行方法调用。



声明变量的方式,能否全部声明为可空来避免空指针?为什么?




  • 猜测:这里涉及到一个 java 和 Kt 互调的问题。

    • 假设1:【Java 调 Kotlin 方法,在于调用】java 用一个可能为空的数据作为方法参数去调用 kt 方法,如果此时入参为空,但 kt 方法将方法参数配置为不可空的数据类型,那么此时就会直接报空指针异常。

      • 因为 kt 会对那些入参不可空的对象先进行空指针判断再执行方法操作。



    • 假设2:【Kotlin 调 Java 方法,在于接收】kt 用一个不可空的变量来接收 java 方法调用得到的返回值,如果此时 java 方法返回一个空,那么此时就会直接报空指针异常。





单例的实现方式



  • 后面新建文章再说


data class



  • data class,编译之后变成 public final class;声明的所有参数会作为构造函数的入参。

  • ① 声明为 val 的参数,只会被提供 getter() 方法;而声明为 var 的参数,会被同时提供 setter()/gettter() 方法。

  • ② 带了 ? 标记的参数,即标明为可空的参数,在构造函数中会被检测是否为空并抛出异常。



by lazy 和 lateinit var



  • 【作用对象不同】

    • lateinit 只能用在 var 声明变量且数据类型不能为空。

    • by lazy {} 只能用在 val 声明变量。



  • 【初始化数据的时机不同】

    • 使用 lateinit 标记的变量,认定了开发者自己在使用该变量之前一定会先为其赋值,所以在访问的时候,会先进行判空处理。如果为空则直接crash。

      • 这也证实了 lateinit 只能对数据类型不为空的变量进行修饰。



    • 通过 by lazy 声明的变量,会为该变量提供私有的 getter() 方法并通过该方法来访问变量,而真正保存数据的位置,是类中一个声明为 final 的数据类型为 Lazy 的私有代理对象,将其作为访问入口,通过 Lazy 的 value 属性来获取数据。Lazy.getValue() 会通过执行初始化函数 initializer 来进行初始化。

      • 详见:链接,下面会接着说。





  • 其他方面:看一下对比



by lazy - val



by lazy{} 的使用




  • by lazy{} 入参:需要传入一个初始化数据的函数 initializer: () -> T。

  • by lazy{} 返回值:会通过 initializer 函数作为方法参数,构造并返回一个 SynchronizedLazyImpl:Lazy 对象。



如何获取数据?




  • 可见,访问 by lazy 的变量,会通过其 getter() 方法来获取数据。

  • 而此时可以看到 getter() 方法是通过访问数据类型为 Lazy 的代理对象的 getValue() 方法获取数据;由上述可知,此时得到的代理对象是一个 SynchronizedLazyImpl:Lazy 对象。

  • 立下一个 Flag:后续再对所有 Lazy 实现类新建文章看看?



SynchronizedLazyImpl:Lazy




  • 【关于数据 _value:Any? 】

    • 初始值为一个单例对象 internal object UNINITIALIZED_VALUE,表示当前未初始化。

    • 因此 _value 的数据类型为 Any。



  • 【getVaule() 方法】

    • ① 首先会判断当前保存的数据 _value是否为这个单例对象 UNINITIALIZED_VALUE?如果不是,则直接通过 as 强装为返回值类型并返回。如果数据未初始化,那么

    • ② 进入一个同步块 synchronized(lock),在同步块中,再次判断 _value是否为这个单例对象 UNINITIALIZED_VALUE?这里的流程就类似于 double check lock。如果已经初始化,则同样通过 as 强装为返回值类型并返回。如果数据未初始化,那么

    • ③ 执行初始化函数 initializer 获取数据并赋值给 _value,从而保证下次获取数据时直接返回该数据。此时,还会将初始化函数 initializer 置空。然后返回数据。



  • 【关于 initializer 函数】

    • 上述可见,我们传递给 lazy 的 Lambda ,会被编译成为一个静态内部类。

    • 静态内部类:继承了 FunctionN,且是一个单例类。invoke() 方法的方法体就是我们在 lambda 中的操作,并且返回值为最后一句。

    • 因此可以知道,在执行初始化函数的时候,实际上就是执行我们传递给 lazy{} 的 lambda 中的执行指令。



  • 【关于线程安全】

    • SynchronizedLazyImpl 接受一个锁对象 lock:Any?=null ,这个锁对象可以是任意类型的对象,当然也可以为空,那么默认使用的就是当前实例对象作为锁对象来进行加锁。

    • 在执行初始化函数 initializer 为数据赋值的时候,正是通过加锁来保证线程安全。





lateinit var 对比 by lazy



  • 关于线程安全

    • by lazy {} 的初始化默认是线程安全的,默认是 SynchronizedLazyImpl:Lazy 实现。并且能保证初始化函数 initializer 只会被调用一次,在数据未初始化时进行调用 且 调用完毕后会置空。

    • lateint 默认是不保证线程安全的。



  • 关于内存泄漏

    • 上述可知,传递给 lazy 的 Lambda ,会被编译成为一个静态内部类。

    • 在使用 by lazy{} 的时候,如果在 lambda 里面使用了类中的成员变量,那么这个引用会一直被持有,直到该初始化函数执行,即该变量被初始化了才会释放(因为初始化函数执行完毕之后会被置空,断开引用链)。

    • 而这里就很可能会导致内存泄漏。






二、各种函数?



  • Flag 立下来:

    • T.let

    • T.run

    • T.also

    • T.apply

    • with

    • run

    • 扩展函数

    • 高阶函数

    • inline noinline crossinline

作者:冰美式上瘾患者
链接:https://juejin.cn/post/7085965272510627877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Dart中的extends, with, implements, on关键字详解

Dart中类的类型 Dart是支持基于mixin继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null以外的所有的类都继承自Object类。 基于mixin的继承意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代...
继续阅读 »

Dart中类的类型


Dart是支持基于mixin继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null以外的所有的类都继承自Object类。 基于mixin的继承意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。



以上这段是官方文档的说明,在实际使用中,由于mixin的加入,使得Dart中类的使用和其它语言有所不同。Dart中类的类型有三种,分别是:



  • class:声明一个类,提供具体的成员变量和方法实现。

  • abstract class:声明一个抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。

  • mixin:声明一个Mixin类,与抽象类一样无法被实例化,是一种在多重继承中复用某个类中代码的方法模式,可以声明接口方法或有具体的方法实现。




  1. 每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的成员变量以及这个类所实现的其它接口。

  2. 如果想让抽象类同时可被实例化,可以为其定义工厂构造函数。具体内容可以参考:抽象类的实例化

  3. mixin关键字在Dart 2.1中才被引用支持。早期版本中的代码通常使用 abstract class代替



从上述内容可以看出,mixin是后面才被引入的,与abstract class有些通用的地方,可以理解为abstract class的升级版。它相对于abstract class说,可以同时引入多个Mixin,并且可以通过on关键字来限制使用范围。


类相关关键字的使用


而对上述这些类型的使用,又有extends, with, implements, on这几个关键字:



  • extends:继承,和其它语言的继承没什么区别。

  • with:使用Mixin模式混入一个或者多个Mixin类

  • implements:实现一个或多个接口并实现每个接口定义的API。

  • on:限制Mixin的使用范围。


针对这几个关键字的使用,我做了一张表进行总结:


样例说明


针对上面的内容,我举几个例子,可以复制代码到DartPad中进行验证:


类混入类或者抽象类(class with class)


class Animal {
String name = "Animal";
}
abstract class Flyer {
String name = "Flyer";
void fly() => print('$name can fly!');
}
abstract class Eater extends Animal {
void eat() => print('I can Eat!');
}

// 同时混入class和abstract class
abstract class Bird with Animal, Flyer {}
class Bird1 with Animal, Flyer {}

// 只支持无任何继承和混入的类,Eater继承自Animal,所以它不支持被混入。
// 报错:The class 'Eater' can't be used as a mixin because it extends a class other than 'Object'.
// class Bird with Eater {
// }

main() {
Bird1().fly(); // Flyer can fly!
}

类继承抽象类并混入Mixin


class Animal {
String name = "Animal";
}

mixin Flyer {
String name = "Flyer";
void fly() => print('$name can fly!');
}

abstract class Eater extends Animal {
@override
String get name => "Eater";
void eat() => print('$name can Eat!');
}

// 类继承抽象类并混入Mixin
class Bird extends Eater with Flyer { }

main() {
// 因为with(混入)的优先级比extends(继承)更高,所以打印出来的是Flyer而不是Eater
Bird().fly(); // Flyer can fly!
Bird().eat(); // Flyer can Eat!
}

类继承抽象类并混入Mixin的同时实现接口


class Biology {
void breathe() => print('I can breathe');
}

class Animal {
String name = "Animal";
}

// 这里设置实现了Biology接口,但是mixin与abstract class一样并不要求实现接口,声明与实现均可。
// on关键字限制混入Flyer的类必须继承自Animal或它的子类
mixin Flyer on Animal implements Biology {
@override
String get name => "Flyer";
void fly() => print('$name can fly!');
}

abstract class Eater extends Animal {
@override
String get name => "Eater";
void eat() => print('$name can Eat!');
}

// 类继承抽象类并混入Mixin的同时实现接口
// 注意关键字的使用顺序,依次是extends -> with -> implements
class Bird extends Eater with Flyer implements Biology {
// 后面使用了`implements Biology`,所以子类必须要实现这个类的接口
@override
void breathe() => print('Bird can breathe!');
}

main() {
// 因为with(混入)的优先级比extends(继承)更高,所以打印出来的是Flyer而不是Eater
Bird().fly(); // Flyer can fly!
Bird().eat(); // Flyer can Eat!
Bird().breathe(); // Bird can breathe!
}

混入mixin的顺序问题


abstract class Biology {
void breathe() => print('I can breathe');
}

mixin Animal on Biology {
String name = "Animal";
@override
void breathe() {
print('$name can breathe!');
super.breathe();
}
}

mixin Flyer on Animal {
@override
String get name => "Flyer";
void fly() => print('$name can fly!');
}

/// mixin的顺序问题:
/// with后面的Flyer必须在Animal后面,否则会报错:
/// 'Flyer' can't be mixed onto 'Biology' because 'Biology' doesn't implement 'Animal'.
class Bird extends Biology with Animal, Flyer {
@override
void breathe() {
print('Bird can breathe!');
super.breathe();
}
}

main() {
Bird().breathe();
/*
* 上述代码执行,依次输出:
* Bird can breathe!
* Flyer can breathe!
* I can breathe
* */
}

这里的顺序问题和运行出来的结果会让人有点费解,但是可以这样理解:Mixin语法糖, 本质还是类继承. 继承可以复用代码, 但多继承会导致代码混乱。 Java为了解决多继承的问题, 用了interface, 只有函数声明而没有实现(后面加的default也算语法糖了)。以A with B, C, D为例,实际是A extends D extends C extends B, 所以上面的Animal必须在Flyer前面,否则就变成了Animal extends Flyer,会出现儿子给爹当爹的混乱问题。



mixin的底层本质只是猜测,并没有查看语言底层源码进行验证.



总结


从上述样例可以看出,三种类结构可以同时存在,关键字的使用有前后顺序:extends -> mixins -> implements
另外需要注意的是相同方法的优先级问题,这个有两种情况:



  1. 同时被extendswithimplements时,混入(with)的优先级比继承(extends)要高,而implements只提供接口,不会被调用。

  2. with多个Mixin时,则会调用距离with关键字最远Mixin中的方法。


当然,如果当前使用类重写了该方法,就会优先调用当前类中的方法。


参考资料



作者:星的天空
链接:https://juejin.cn/post/7094642592880525320
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter Modular使用教程

什么是Flutter Modular? 随着应用项目发展和变得越来越复杂,保持代码和项目结构可维护和可复用越来越难。Modular提供了一堆适配Flutter的解决方案来解决这些问题,比如依赖注入,路由系统和“一次性单例”系统(也就是说,当注入模块超出范围时,...
继续阅读 »

什么是Flutter Modular?


随着应用项目发展和变得越来越复杂,保持代码和项目结构可维护和可复用越来越难。Modular提供了一堆适配Flutter的解决方案来解决这些问题,比如依赖注入,路由系统和“一次性单例”系统(也就是说,当注入模块超出范围时,模块化自动配置注入模块)。


Modular的依赖注入为任何状态管理系统提供了开箱即用的支持,管理你应用的内存。


Modular也支持动态路由和相对路由,像在Web一样。


Modular结构


Modular结构由分离和独立的模块组成,这些模块将代表应用程序的特性。
每个模块都位于自己的目录中,并控制自己的依赖关系、路由、页面、小部件和业务逻辑。因此,您可以很容易地从项目中分离出一个模块,并在任何需要的地方使用它。


Modular支柱


这是Modular关注的几个方面:



  • 自动内存管理

  • 依赖注入

  • 动态和相对路由

  • 代码模块化


在项目中使用Modular


安装


打开你项目的pubspec.yaml并且添加flutter_modular作为依赖:


dependencies:
flutter_modular: any

在一个新项目中使用


为了在新项目中使用Modular,你必须做一些初始化步骤:




  1. MaterialApp创建你的main widget并且调用MaterialApp().modular()方法。


    // app_widget.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_modular/flutter_modular.dart';

    class AppWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    initialRoute: "/",
    ).modular();
    }
    }



  2. 创建继承自Module的你项目的main module文件:


    // app_module.dart
    class AppModule extends Module {

    // Provide a list of dependencies to inject into your project
    @override
    final List binds = [];

    // Provide all the routes for your module
    @override
    final List routes = [];

    }



  3. main.dart文件中,将main module包裹在ModularApp中以使Modular初始化它:


    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_modular/flutter_modular.dart';

    import 'app/app_module.dart';

    void main() => runApp(ModularApp(module: AppModule(), child: AppWidget()));



  4. 完成!你的应用已经设置完成并且准备好和Modular一起工作!




创建child modules


你可以在你的项目中创建任意多module:


class HomeModule extends Module {
@override
final List binds = [
Bind.singleton((i) => HomeBloc()),
];

@override
final List routes = [
ChildRoute('/', child: (_, args) => HomeWidget()),
ChildRoute('/list', child: (_, args) => ListWidget()),
];

}

你可以通过module参数将子模块传递给你main module中的一个Route


class AppModule extends Module {

@override
final List routes = [
ModuleRoute('/home', module: HomeModule()),
];
}

我们建议你讲代码分散到不同模块中,例如一个AuthModule,并将与此模块相关的所有路由放入其中。通过这样做,维护和与其他项目分享你的代码将变得更加容易。



**注意:**使用ModuleRoute对象创建复杂的路由。



添加路由


模块路由是通过覆盖routes属性来提供的。


// app_module.dart
class AppModule extends Module {

// Provide a list of dependencies to inject into your project
@override
final List binds = [];

// Provide all the routes for your module
@override
final List routes = [
// Simple route using the ChildRoute
ChildRoute('/', child: (_, __) => HomePage()),
ChildRoute('/login', child: (_, __) => LoginPage()),
];
}


**注意:**使用ChildRoute对象来创建简单路由。



动态路由


你可以使用动态路由系统来提供参数给你的Route


// 使用 :参数名 语法来为你的路由提供参数。
// 路由参数可以通过' args '获得,也可以在' params '属性中访问,
// 使用方括号符号 (['参数名']).

@override
final List routes = [
ChildRoute(
'/product/:id',
child: (_, args) => Product(id: args.params['id']),
),
];

当调用给定路由时,参数将是模式匹配的。例如:


// In this case, `args.params['id']` will have the value `1`.
Modular.to.pushNamed('/product/1');

你也可以在多个界面中使用它。例如:


@override
final List routes = [
// We are sending an ID to the DetailPage
ChildRoute(
'/product/:id/detail',
child: (_, args) => DetailPage(id: args.params['id']),
),
// We are sending an ID to the RatingPage
ChildRoute(
'/product/:id/rating',
child: (_, args) => RatingPage(id: args.params['id']),
),
];

与第一个实例相同,我们只需要调用这个路由。例如:


// In this case, modular will open the page DetailPage with the id of the product equals 1
Modular.to.navigate('/product/1/detail');
// We can use the pushNamed too

// The same here, but with RatingPage
Modular.to.navigate('/product/1/rating');

然而,这种表示法只对简单的文字有效。


发送对象


如果你想传递一个复杂对象给你的路由,通过arguments参数传递给它::


Modular.to.navigate('/product', arguments: ProductModel());

并且,它将通过args.data属性提供而不是args.params


@override
final List routes = [
ChildRoute(
'/product',
child: (_, args) => Product(model: args.data),
),
];

你可以直接通过binds来找回这些参数:



@override
final List binds = [
Bind.singleton((i) => MyController(data: i.args.data)),
];

路由泛型类型


你可以从导航返回一个值,就像.pop。为了实现这个,将你期望返回的参数作为类型参数传递给Route:


@override
final List routes = [
// This router expects to receive a `String` when popped.
ChildRoute('/event', child: (_, __) => EventPage()),
]

现在,使用.pop就像你使用Navigator.pop


// Push route
String name = await Modular.to.pushNamed('/event');

// And pass the value when popping
Modular.to.pop('banana');

路由守卫


路由守卫是一种类似中间件的对象,允许你从其它路由控制给定路由的访问权限。你通过让一个类implements RouteGuard可以实现一个路由守卫.


例如,下面的类只允许来自/admin的路由的重定向:


class MyGuard implements RouteGuard {
@override
Future canActivate(String url, ModularRoute route) {
if (url != '/admin'){
// Return `true` to allow access
return Future.value(true);
} else {
// Return `false` to disallow access
return Future.value(false);
}
}
}

要在路由中使用你的RouteGuard,通过guards参数传递:


@override
final List routes = [
final ModuleRoute('/', module: HomeModule()),
final ModuleRoute(
'/admin',
module: AdminModule()
,
guards: [MyGuard()],
),
]
;

如果你设置到module route上,RouteGuard将全局生效。


如果RouteGuard验证失败,添加guardedRoute属性来添加路由选择路由:


@override
final List routes = [
ChildRoute(
'/home',
child: (context, args) => HomePage(),
guards: [AuthGuard()],
guardedRoute: '/login',
),
ChildRoute(
'/login',
child: (context, args) => LoginPage(),
),
];

什么时候和如何使用navigate或pushNamed


你可以在你的应用中使用任何一个,但是需要理解每一个。


pushNamed


无论何时使用,这个方法都将想要的路由放在当前路由的上面,并且您可以使用AppBar上的后退按钮返回到上一个页面。 它就像一个模态,它更适合移动应用程序。


假设你需要深入你的路线,例如:


// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');

最后,您可以看到返回到前一页的back按钮,这加强了模态页面在前一页上面的想法。


navigate


它删除堆栈中先前的所有路由,并将新路由放到堆栈中。因此,在本例中,您不会在AppBar中看到后退按钮。这更适合于Web应用程序


假设您需要为移动应用程序创建一个注销功能。这样,您需要从堆栈中清除所有路由。


// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');

// Then you need to go again to the Login page, only use the navigation to clean all the stack.
Modular.to.navigate('/login');

Relative Navigation


要在页面之间导航,请使用Modular.to.navigate


Modular.to.navigate('/login');

你可以使用相对导航来导航,就像在web程序一样:


// Modules Home → Product
Modular.to.navigate('/home/product/list');
Modular.to.navigate('/home/product/detail/3');

// Relative Navigation inside /home/product/list
Modular.to.navigate('detail/3'); // it's the same as /home/product/detail/3
Modular.to.navigate('../config'); // it's the same as /home/config

您仍然可以使用旧的Navigator API来堆叠页面。


Navigator.pushNamed(context, '/login');

或者,您可以使用Modular.to.pushhnamed,你不需要提供BuildContext:


Modular.to.pushNamed('/login');

Flutter Web URL routes (Deeplink-like)


路由系统可以识别URL中的内容,并导航到应用程序的特定部分。动态路由也适用于此。例如,下面的URL将打开带有参数的Product视图。args.params['id']设置为1。


https://flutter-website.com/#/product/1

它也可以处理查询参数或片段:


https://flutter-website.com/#/product?id=1

路由过渡动画


通过设置Route的转换参数,提供一个TransitionType,您可以选择在页面转换中使用的动画类型。


ModuleRoute('/product',
module: AdminModule(),
transition: TransitionType.fadeIn,
), //use for change transition

如果你在一个Module中指定了一个过渡动画,那么该Module中的所有路由都将继承这个过渡动画。


自定义过渡动画路由


你也可以通过将路由器的transitioncustomTransition参数分别设置为TransitionType.custom和你的CustomTransition来使用自定义的过渡动画:


import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

CustomTransition get myCustomTransition => CustomTransition(
transitionDuration: Duration(milliseconds: 500),
transitionBuilder: (context, animation, secondaryAnimation, child){
return RotationTransition(turns: animation,
child: SlideTransition(
position: Tween(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: ScaleTransition(
scale: Tween(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: animation,
curve: Interval(
0.00,
0.50,
curve: Curves.linear,
),
),
),
child: child,
),
),
)
;
},
);

依赖注入


可以通过重写Modulebinds的getter将任何类注入到Module中。典型的注入例子有BLoCs、ChangeNotifier实例或(MobX)。


一个Bind对象负责配置对象注入。我们有4个Bind工厂类型和一个AsyncBind


class AppModule extends Module {

// Provide a list of dependencies to inject into your project
@override
List get binds => [
Bind((i) => AppBloc()),
Bind.factory((i) => AppBloc()),
Bind.instance(myObject),
Bind.singleton((i) => AppBloc()),
Bind.lazySingleton((i) => AppBloc()),
AsyncBind((i) => SharedPreferences.getInstance())
];
...
}

Factory


每当调用类时实例化它。


@override
List get binds => [
Bind.factory((i) => AppBloc()),
];

Instance


使用已经实例化的对象。


@override
List get binds => [
Bind.instance((i) => AppBloc()),
];

Singleton


创建一个类的全局实例。


@override
List get binds => [
Bind.singleton((i) => AppBloc()),
];

LazySingleton


只在第一次调用类时创建一个全局实例。


@override
List get binds => [
Bind.lazySingleton((i) => AppBloc()),
];

AsyncBind


若干类的一些方法返回一个Future。要注入那些特定方法返回的实例,你应该使用AsyncBind而不是普通的同步绑定。使用Modular.isModuleReady()等待所有AsyncBinds解析,以便放开Module供使用。



重要:如果有其他异步绑定的相互依赖,那么AsyncBind的顺序很重要。例如,如果有两个AsyncBind,其中A依赖于B, AsyncBind B必须在A之前声明。注意这种类型的顺序!



import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
final controller = StreamController();

@override
void dispose() {
controller.close();
}
}

isModuleReady


如果你想确保所有的AsyncBinds都在Module加载到内存之前被解析,isModuleReady是一个方法。使用它的一种方法是使用RouteGuard,将一个AsyncBind添加到你的AppModule中,并将一个RouteGuard添加到你的ModuleRoute中。


class AppModule extends Module {
@override
List get binds => [
AsyncBind((i)=> SharedPreferences.getInstance()),
];

@override
List get routes => [
ModuleRoute(Modular.initialRoute, module: HomeModule(), guards: [HomeGuard()]),
];
}

然后,像下面这样创建一个RouteGuard。这样,在进入HomeModule之前,模块化会评估你所有的异步依赖项。


import 'package:flutter_modular/flutter_modular.dart';

class HomeGuard extends RouteGuard {
@override
Future canActivate(String path, ModularRoute router) async {
await Modular.isModuleReady();
return true;
}
}

在视图中检索注入的依赖项


让我们假设下面的BLoC已经定义并注入到我们的模块中(就像前面的例子一样):


import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
final controller = StreamController();

@override
void dispose() {
controller.close();
}
}


注意:Modular自动调用这些Binds类型的销毁方法:Sink/Stream, ChangeNotifier和[Store/Triple]



有几种方法可以检索注入的AppBloc


class HomePage extends StatelessWidget {

@override
Widget build(BuildContext context) {

// You can use the object Inject to retrieve..

final appBloc = Modular.get();
//or for no-ready AsyncBinds
final share = Modular.getAsync();
}
}

使用Modular小部件检索实例


ModularState


在本例中,我们将使用下面的MyWidget作为页面,因为这个页面需要是StatefulWidget


让我们来了解一下ModularState的用法。当我们定义类_MyWidgetState扩展ModularState时,我们正在为这个小部件(在本例中是HomeStore)将Modular与我们的Store链接起来。当我们进入这个页面时,HomeStore将被创建,store/controller变量将被提供给我们,以便在MyWidget中使用。


在此之后,我们可以使用存储/控制器而没有任何问题。在我们关闭页面后,模块化将自动处理HomeStore


class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends ModularState {
store.myVariableInsideStore = 'Hello!';
controller.myVariableInsideStore = 'Hello!';

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Modular"),
),
body: Center(child: Text("${store.counter}"),),
);
}
}

WidgetModule


WidgetModule具有与Module相同的结构。如果你想要一个带有Modular页面的TabBar,这是非常有用的。


class TabModule extends WidgetModule {

@override
List binds => [
Bind((i) => TabBloc(repository: i())),
Bind((i) => TabRepository()),
];

final Widget view = TabPage();

}

Mock导航系统


我们认为,在使用Modular.toModular.link时,提供一种native方式来mock导航系统会很有趣。要做到这一点,您只需实现IModularNavigator并将您的实现传递给Modular.navigatorDelegate


使用 Mockito示例:


main() {
var navigatorMock = MyNavigatorMock();

// Modular.to and Modular.link will be called MyNavigatorMock implements!
Modular.navigatorDelegate = navigatorMock;

test('test navigator mock', () async {
when(navigatorMock.pushNamed('/test')).thenAnswer((_) async => {});

Modular.to.pushNamed('/test');
verify(navigatorMock.pushNamed('/test')).called(1);
});
}

class MyNavigatorMock extends Mock implements IModularNavigator {
@override
Future pushNamed(String? routeName, {Object? arguments, bool? forRoot = false}) =>
(super.noSuchMethod(Invocation.method(#pushNamed, [routeName], {#arguments: arguments, #forRoot: forRoot}), returnValue: Future.value(null)) as Future);
}

本例使用手动实现,但您也可以使用 代码生成器来创建模拟。


RouterOutlet


每个ModularRoute都可以有一个ModularRoute列表,这样它就可以显示在父ModularRoute中。反映这些内部路由的小部件叫做RouterOutlet。每个页面只能有一个RouterOutlet,而且它只能浏览该页面的子页面。



class StartModule extends Module {
@override
List get binds => [];

@override
List get routes => [
ChildRoute(
'/start',
child: (context, args) => StartPage(),
children: [
ChildRoute('/home', child: (_, __) => HomePage()),
ChildRoute('/product', child: (_, __) => ProductPage()),
ChildRoute('/config', child: (_, __) => ConfigPage()),
],
),
];
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: RouterOutlet(),
bottomNavigationBar: BottomNavigationBar(
onTap: (id) {
if (id == 0) {
Modular.to.navigate('/start/home');
} else if (id == 1) {
Modular.to.navigate('/start/product');
} else if (id == 2) {
Modular.to.navigate('/start/config');
}
},
currentIndex: currentIndex,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.control_camera),
label: 'product',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Config',
),
],
),
);
}

作者:牛奶燕麦
链接:https://juejin.cn/post/6998910339882418189
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

jetpack compose实战——基本框架搭建

前言 项目地址:github.com/Peakmain/Co… 网上现在有不少jetpack compose的文章和教程,但是实战项目不多。 项目接口基于玩Android,这里也非常感谢大佬提供的免费接口 建议 先学习kotlin语言,最好有Android...
继续阅读 »

前言



  • 项目地址:github.com/Peakmain/Co…

  • 网上现在有不少jetpack compose的文章和教程,但是实战项目不多。

  • 项目接口基于玩Android,这里也非常感谢大佬提供的免费接口


建议


先学习kotlin语言,最好有Android App开发经验


项目结构


新建项目New Project->选择 Empty Compose Activity
image.png


image.png


项目结构


新建项目New Project->选择 Empty Compose Activity


image.png
填写必要信息,完成项目创建


image.png


Compose和Android View的区别


















































Android Viewcompose
ButtonButton
TextViewText
EditTextTextField
ImageViewImage
LinearLayout(horizontally)Row
LinearLayout(vertically)Column
FrameLayoutBox
RecyclerViewLazyColumn
RecyclerView(horizontally)LazyRow
SnackbarSnackbar

一些基础知识


Scaffold

@Composable
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
)

Scaffold主要用于快速搭建一个项目的结构,包含:



  • topBar:通常是TopAppBar

  • bottomBar 通常是一个 BottomNavigation,里面每个item是BottomNavigationItem

  • floatingActionButton 悬浮按钮

  • floatingActionButtonPosition 悬浮按钮位置

  • isFloatingActionButtonDocked 悬浮按钮是否贴到 bottomBar 上

  • drawerContent 侧滑菜单

  • content:内容区域


状态


状态和组合

由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新


@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}


  • OutlinedTextField与 TextField 只是样式不同

  • 如果运行此代码,您将不会看到任何反应。这是因为,TextField 不会自行更新,但会在其 value 参数更改时更新。


Compose中的状态


  • Composable中可以使用remember来记住单个对象。

  • 系统会在初始化由 remember计算的值存储在Composable中,并在重组的时候返回存储的值

  • remember既可以存储可变对象,也可以存储不可变对象。


注意:remember 会将对象存储在组合中,当调用 remember 的可组合项从组合中移除后,它会忘记该对象。

mutableStateOf 会创建可观察的 MutableState,后者是与 Compose 运行时集成的可观察类型。


interface MutableState<T> : State<T> {
override var value: T
}

value 如有任何更改,系统会安排重组读取 value 的所有可组合函数。


在可组合项中声明 MutableState 对象的方法有三种:



  • val mutableState = remember { mutableStateOf(default) }

  • var value by remember { mutableStateOf(default) }

  • val (value, setValue) = remember { mutableStateOf(default) }


这些声明是等效的,以语法糖的形式针对状态的不同用法提供。您选择的声明应该能够在您编写的可组合项中生成可读性最高的代码。


所以上面代码的解决办法


@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }//👈🏻定义状态
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,//👈🏻要显示的当前值
onValueChange = { name = it },//👈🏻请求更改值的事件,其中 T 是建议的新值
label = { Text("Name") }
)
}
}

小技巧:Compose的代码模板


在搭建基本框架之前,我们先来定义一个模板,方便大家开发(我的是Mac电脑)



  • 1、Android Studio-> Preferences->Editor->File and Code Templates


image.png



  • 2、点击➕号


image.png



  • 3、使用,右击选择New->kotlin compose


image.png


image.png


基本框架搭建


效果图


12.gif


13.png



  • 1、新建项目,修改MainActivity


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeProjectTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
MainFrame()
}
}
}
}
}


  • 2、MainFrame


@Composable
fun MainFrame() {
val navigationItems = listOf(
NavigationItem("首页", Icons.Default.Home),
NavigationItem("项目", Icons.Default.Article),
NavigationItem("分类", Icons.Default.Category),
NavigationItem("我的", Icons.Default.Person)
)
var currentNavigationIndex by remember {
mutableStateOf(0)
}
Scaffold(
bottomBar = {
BottomNavigation(backgroundColor = MaterialTheme.colors.surface) {
navigationItems.forEachIndexed { index, navigationItem ->
BottomNavigationItem(
selected = currentNavigationIndex == index,
onClick = { currentNavigationIndex = index },
icon = {
Icon(imageVector = navigationItem.icon, contentDescription = null)
},
label = {
Text(text = navigationItem.title)
},
selectedContentColor = Color_149EE7,
unselectedContentColor = Color_999999
)
}
}
},
) {
when (currentNavigationIndex) {
0 -> HomeFragment()
1 -> ProjectFragment()
2 -> TypeFragment()
else -> MineFragment()
}
}
}

代码其实很简单,主要通过Scaffold来搭建一个项目结构,用remember+ mutableStateOf来记住状态。内容区域通过选中的index来展示不同的Fragment



implementation "androidx.compose.material:material-icons-extended:$compose_version"

总结


到这里呢,基本框架已经搭完了,其实还是比较简单的。有不动的呢,可以多看看Google官方文档:developer.android.google.cn/jetpack/com…


作者:peakmain
链接:https://juejin.cn/post/7093341380549804045
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

重复setContentView后fitsSystemWindows失效

项目中有个沉浸式的activity,在调用setContentView切换布局的时候fitsSystemWindows失效了,效果如图: Activity代码: class MainActivity : AppCompatActivity() { ...
继续阅读 »

项目中有个沉浸式的activity,在调用setContentView切换布局的时候fitsSystemWindows失效了,效果如图:


demo.gif


Activity代码:



class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
immerse()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

private fun immerse() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val decorView = window.decorView
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.statusBarColor = Color.TRANSPARENT
}
}

fun reload(view: View) {
setContentView(R.layout.activity_main)
}
}

布局代码:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:gravity="center_horizontal"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:scaleType="centerCrop"
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="400dp"
android:src="@drawable/avatar"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_marginTop="20dp"
android:onClick="reload"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="reload"
android:textAllCaps="false"/>
</LinearLayout>

首先看下fitsSystemWindows起到的作用


        <!-- Boolean internal attribute to adjust view layout based on
system windows such as the status bar.
If true, adjusts the padding of this view to leave space for the system windows.
Will only take effect if this view is in a non-embedded activity. -->

<attr name="fitsSystemWindows" format="boolean" />

这个属性用于根据系统窗口(如状态栏)来调整视图的布局。如果为true,则调整此视图的padding来为系统窗口留出空间,也就是说视图布局的内容不会扩展到任务栏中


正常情况下,什么时候会触发fitsSystemWindows的padding调整?


ViewRootImpl首次绘制的时候会调用dispatchApplyInsets方法,将WindowInset(窗口内容的插入,包括状态栏,导航栏,键盘等,可以理解为这些它们所占窗口的大小)分发给decorView,最终会分发到到上述布局中的根布局LinearLayout的fitSystemWindowsInt方法完成padding的设置,LinearLayout没有重写此方法,最终调用的还是View的fitSystemWindowsInt


ViewRootImpl

private void performTraversals() {
......
//首次绘制判断,host为decorView
if (mFirst) {
......
dispatchApplyInsets(host);
}
......
//其他条件触发,这个标记位在下文会用到
if (...... || mApplyInsetsRequested){
dispatchApplyInsets(host)
}
......
}

public void dispatchApplyInsets(View host) {
......
WindowInsets insets = getWindowInsets(true);
host.dispatchApplyWindowInsets(insets);
......
}

View

private boolean fitSystemWindowsInt(Rect insets) {
//判断fitSystemWindows是否为true
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
Rect localInsets = sThreadLocal.get();
boolean res = computeFitSystemWindows(insets, localInsets);
applyInsets(localInsets);
return res;
}
return false;
}

private void applyInsets(Rect insets) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
mUserPaddingLeftInitial = insets.left;
mUserPaddingRightInitial = insets.right;
internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
}

protected void internalSetPadding(int left, int top, int right, int bottom) {
......
//设置padding
......

//如果padding改变了,重新布局
if (changed) {
requestLayout();
invalidateOutline();
}
}

为什么重新setContentView之后没有为新的视图设置padding?


当我们调用setContentView重新设置布局时,activity对应的window已经被添加到WindowManager中了,ViewRootImpl不会重新创建,但是布局是重新加载并实例化视图了。此时ViewRootImpl的首次绘制判断不成立,不会将WindowInset分发给新加载的布局,因此新的视图没有设置顶部的padding,绘制的时候也就跑到了状态栏中去了


ActivityThread

public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
......
if (r.window == null && !a.mFinished && willBeVisible) {
......
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//ViewRootImpl创建的起点
wm.addView(decor, l);
}
......
}
......
}

应该怎样让ViewRootImpl重新分发WindowInset


从上文中ViewRootImpl调用dispatchApplyInsets的地方可以看到,mApplyInsetsRequested也能影响是否调用该方法,可以从这个标志位入手。分析代码发现,调用ViewrequestFitSystemWindowsrequestApplyInsets方法可以向上调用到ViewRootImpl的同名方法中,在这个方法中会将mApplyInsetsRequested设为true,并调用scheduleTraversals触发界面绘制。


View

@Deprecated
public void requestFitSystemWindows() {
//最终会调用到ViewRootImpl中去
if (mParent != null) {
mParent.requestFitSystemWindows();
}
}

public void requestApplyInsets() {
requestFitSystemWindows();
}

ViewRootImpl

public void requestFitSystemWindows() {
checkThread();
mApplyInsetsRequested = true;
scheduleTraversals();
}

demo中的reload方法修改为如下可以解决此问题


    fun reload(view: View) {
val root = layoutInflater.inflate(R.layout.activity_main, null)
setContentView(root)
//使用此方法做版本兼容,最终还是会调用到 View.requestFitSystemWindows()
ViewCompat.requestApplyInsets(root)
}

作者:今天想吃什么
链接:https://juejin.cn/post/7091260989504815112
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter启动页白屏处理

前言 在上篇实现了一个Nike的加载页,但有一些遗留问题,其中之一就是启动时的白屏处理。如下: 启动页 几乎所有App都会设计一个启动页,Flutter项目如果不做处理的话,在点开时都会有这个白色的闪屏。其实这个启动页在项目文件就可以进行更改。 安卓 1.打...
继续阅读 »

前言


在上篇实现了一个Nike的加载页,但有一些遗留问题,其中之一就是启动时的白屏处理。如下:

pm.gif


启动页


几乎所有App都会设计一个启动页,Flutter项目如果不做处理的话,在点开时都会有这个白色的闪屏。其实这个启动页在项目文件就可以进行更改。


安卓


1.打开AndroidManifes.xml文件,可以看到启动屏数据源指向了drawable中的launch_background

image.png


2.打开drawable/launch_background文件,就会发现现在的启动页背景是白色。

image.png


3.若要设置图片样式的启动页,则需要将下面注释的内容放开。


image.png


4. 默认情况下是没有launch_image的,将启动页图片的名字设置为launch_image,然后放到drawable文件下,启动页就设置好啦。


image.png


iOS


打开下面文件,将LaunchImag.pngLaunchImag@2x.pngLaunchImag@3x.png替换为我们自己的图片即可。

image.png


虚假の示例


这里以之前完成的启动图为例来试一下效果。


1.首先随便掏出个画图软件做一张启动页图片:

image.png


2.然后将上面所说项目中的图片替换为我们自己的图片看下效果:

pml.gif


。。。

image.png


这是什么鬼,难道图片尺寸必须跟屏幕保持一致才可以吗... 非也,其实用这种方式设置启动图并非上策,因为不同尺寸的屏幕间很难做适配,特别是示例中需要启动页中的logo与启动页消失后的logo大小保持一致的情况,所以需要尝试其他方法:

真正の示例


1.以iOS为例,使用Xcode打开项目,在Asset中我们看到了刚才拖入的图片。

image.png


2.点击LaunchScreen,这是iOSApp启动时展示的屏幕窗口,可以看到我们拖入的图片展示在一个imageView中。

image.png


3.那如果把LaunchImage的约束重新设置一下呢


image.png
image.png
image.png


4.再来看一下效果,这次似乎像那么回事儿了,但还是能发现logo大小不一样的情况(虽然这是我随手做的一张启动页图片,但既然我们的需求是根据代码,让启动页在所有屏幕上的显示效果都一样的话就不该止步于此)。

ppm.gif


5.终极解决方案:设置背景底色,为盛放logo的imageView设置约束(在上一篇文章中,我们设置logo的初始大小为屏幕宽度的1/3,位置为屏幕的中心),那么我们为imageView设置同样的约束,然后就有了⬇️

image.png


6.最后看一下效果:

ppp.gif


完美 🎉🎉🎉


image.png


结语


最后说一些题外话吧,其实看过苹果的App设计规范就会了解到,其实启动页被设计的初衷就是起一个过渡的作用,让用户在使用感受上不回觉得太过突兀,比如iOS系统自带的天气app,启动页只是一张简单的渐变图片,是不建议添加产品Logo或者其他一些花里胡哨的广告的,否则审核有可能会因此被拒,大家随手打开几个App感受下就不难发现,会这样做的产品少之又少,在不知不觉中就被消费了体验,也许是已经习惯了。不过毕竟咱也不是产品,在下随口说说,诸君随意听听就好 ~

作者:晚夜
链接:https://juejin.cn/post/7092371526111985694
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Today,我们不聊技术,聊聊前端发展

今天是2022年04月26日,一年已经过去三分之一。 掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如...
继续阅读 »

今天是2022年04月26日,一年已经过去三分之一。


掘金里面有很多的技术文章,每一位前端工程师都在这里展现自己的技术水平。有很多时候,我看见很多的技术文章,里面大致上的内容其实都是差不多的,总的来说,其实普通的前端工程师是用不到去学习这么多的技术点的。就比如Node.js 。 一般的公司也不会用JavaScript语言来写后端,所以大部分的前端甚至都不需要去了解它,反而更应该了解多一点Ajax与网络请求协议。数据的问题交给后端去处理就好了,前端有自己要做的活。


我个人认为,技术框架的源码这种东西,如果能不学习,就不要去深入的学习了。很多人其实是没有达到进大厂的门槛的,大部分的前端其实都达不到,而一些中小型的公司,一般也不会去问一个技术架构的源码及核心问题(绝大部分),因为中小型公司需要的是能干活的人,而大部分的项目业务,其实还没有说你不懂源码就做不了的程度。总的来说就是只要你能干活,你懂什么是你自己的事儿,我就给这么多钱,这些项目你能干就来,你做不了我就辞退你。


其实大部分的前端,只要有请求到后端的接口,然后能把后端接口的数据处理好,并渲染到页面上就可以了。然后一些不懂的问题,一些复杂的功能模块,其实你一百度,基本上都能解决问题,如果你百度都不能解决的问题,那不是百度解决不了,而是你的项目本身就是有问题的。这里面说的是绝大部分的情况,当然也有一些奇怪的例子,这种只是占少部分。


其实我们前端的活总体来说都不难,就好比开车,其实绝大部分人都会开车,但是要想要把车技提升上去,那就需要去学习了,如果说你只是为了通勤,那么很多时候,你都不需要去提升你的车技。你只需要懂得怎么启动,怎么刹车等一些基本的操作就行了(实在不行就百度)。


前端往后的生态


其实前端往后也不会有什么太大的变化,基本上就定型了。像网上说的什么新技术啊,新方向什么的,其实很多都会不了了之,因为在没有发生技术变革的年代,我们想要去改变一些东西是很难的。我们很多人其实都是需要去等待,等待那个奇点的到来。没有很大的改变,其实都只能这样子。就好比我知道的,在网络请求中,其实有很大部分资源都浪费在了一些协议上,而这些协议的束缚,导致了我们的网络传输会消耗掉三分之一的性能,这种问题是历史遗留问题,虽然现在已经有很多方法能够解决掉这个性能消耗问题,但是解决这个问题需要互联网的企业把旧机器换成新机器,而新机器的成本又高于网络传输消耗的成本,所以我们普通人只能这样去无端的消耗掉这些资源,又或者等待那个奇点的到来。


说到设备又不得不提现如今的大部分互联网用户,在现在的互联网,其实绝大部分用户的设备性能已经是非常高了,而我们缺还有的人说在项目做一些性能优化问题,其实有时候,这种优化是无意义的,还不如不去做这种优化。当然这种场景也是区分项目的体验人的年龄段,如果项目主要服务于年轻人,其实年轻人的设备性能说不定比我们自己的设备都好,你的优化起不到太大的作用。如果项目主要服务于老年人,其实这个时候需要思考的不是设备性能优化的问题,反而更需要注重项目体验上的问题,就是怎么简单怎么来,别让老人觉得用你的东西太麻烦。


我所期待的前端世界


随着电子产品的更新换代,设备的性能越来越好,用户的CUP跑得越来越快,我们可以在我们的前端项目中放更多的新颖东西,比如把项目变革为3D场景,让用户在体验产品时,如同进入一个真实的虚拟世界(希望这一天不会超过50年)web3D值得期待。还有就是网页端游戏,现在绝大部分游戏都是部署在用户的设备中,而每个人的设备存放1个G,那一百个人, 就会有100个G的文件是存在重复,如果一款游戏,能把他部署在服务器上面,而用户只需要进入到网页中就可以体验,那真的是非常令人期待。


END


其实这些都是我瞎写的,没有什么值得看的地方,各位看官就当做是一个笑话,如果觉得有意思,麻烦点个赞。谢谢


作者:无我上青云
链接:https://juejin.cn/post/7090725867441258503
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为了看Flutter到底有没有人用我竟然

首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。github.com/zhaobozhen/…github.com/sugood/apka…App列...
继续阅读 »

Flutter这个东西出来这么久了,到底市场占有率怎么样呢?为了让大家了解这一真实数据,也为了让大家了解当前Flutter在各大App中的使用情况,我今天下载了几百个App,占了手机将近80G空间,就为了得出一个结论——Flutter,到底有没有人用。

首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。

检测方法,我使用LibChecker来查看App是否有使用Flutter相关的so。

github.com/zhaobozhen/…

除了使用LibChecker之外,还有其它方案也可以,例如使用shell指令——zipinfo。

github.com/sugood/apka…

Apk本质上也是一种压缩包,所以,通过zipinfo指令并进行grep,就可以很方便的获取了,同时,如果配合一下爬虫来爬取应X宝的Apk下载地址,就可以成为一个全自动化的脚本分析工具,这里没这么强的需求,所以就不详细做了。

App列表

我们来看下,我都下载了多少App。

Screenshot_2022-04-12-09-45-44-13_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-47-46_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-49-64_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-45-51-75_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-53-78_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-55-92_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-45-58-12_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-00-27_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-02-34_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-46-04-34_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-06-60_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-09-14_92b64b2a7aa6eb3771ed6e18d0029815

这些App基本上已经覆盖了应用商店各个排行榜里的Top软件,所以应该还是比较具有代表性和说服力的。

下面我们就用LibChecker来看下,这些App里面到底有多少使用了Flutter。

统计结果

Screenshot_2022-04-12-09-51-25-73_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-34-94_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-51-39-66_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-44-41_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-51-49-75_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-58-19_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-52-04-67_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-52-13-25_708f76cdf2c7449ff16a8486e0e036f6

已经使用Flutter的App共52个,占全体样本的19.2%,作为参考,统计了下RN相关的App,共有45个,占全体样本的16.6%,可以说,Flutter已经超过RN成为跨平台方案的首选。

在52个使用Flutter的App中:

  • 腾讯系:QQ邮箱、微信、QQ同步助手、蓝盾、腾讯课堂、QQ浏览器、微视、企业微信、腾讯会议

  • 百度系:百度网盘、百度输入法

  • 阿里系:优酷视频、哈啰出行、淘特、酷狗直播、阿里1688、学习强国、钉钉、淘宝、闲鱼

  • 其它大厂:链家、转转、智联招聘、拍拍贷、哔哩哔哩漫画、网易有道词典、爱奇艺、考拉海购、携程旅行、微博、Soul、艺龙旅行、唯品会、飞猪旅行

从上面的数据来看,各大厂都对Flutter有使用,头条系未列出的原因是,目前好像只有头条系大规模使用了Flutter的动态化加载方案,所以原始包内找不到Flutter相关的so,所以未检出(猜测是这样,具体可以请头条系的朋友指出,根据上次头条的分享,内部有90+App在使用Flutter)。

不过这里要注意的 ,这里并不是选取的大家常用的一些APP来做测试的,而是直接选取的排行榜,如果直接用常用APP来测试,那比例可能更高,大概统计了下,估计在60%左右。

不过大厂里面,京东没有使用Flutter我还是比较意外的,看了下京东的几个App,目前还是以RN为主作为跨平台的方案。这跟其它很多大厂一样,它们不仅使用了Flutter,RN也还可以检出,这也从侧面说明了,各个厂商,对跨平台的方案探索,从未停止。

所以,总结一下,目前使用Flutter的团队的几个特定:

  • 创业公司:快速试错、快速开发,像Blued、夸克这也的

  • 大厂:大厂的话题永远是效率,如何利用跨平台技术来提高开发效率,是它们引入Flutter的根本原因

  • 创新型业务:例如B漫、淘特、Soul这类没有太多历史包袱的新业务App,可以利用Flutter进行极为高效的开发

所以,整体在知乎上吵「Flutter被抛弃了」、「Flutter要崛起了」,有什么意义呢?所有的争论都抵不过数据来的真实。

嘴上说着不要,身体倒是很诚实。

希望这份数据能给你一些帮助。


作者:xuyisheng
来源:juejin.cn/post/7088864824284676110


收起阅读 »

前端单点登录实现

通过token校验登录信息前端单点存储方式共享本地存储数据token值,token存储方式用的是localStorage 或 sessionStorage,由于这两种都会受到同源策略限制。跨域存储想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe...
继续阅读 »

通过token校验登录信息

前端单点存储方式

共享本地存储数据token值,token存储方式用的是localStoragesessionStorage,由于这两种都会受到同源策略限制。

跨域存储

想要实现跨域存储,先找到一种可跨域通信的机制,就是 iframe postMessage,它可以安全的实现跨域通信,不受同源策略限制(后端要修改配置允许iframe打开其他域的地址)。

cross-storage.js(开源库)

原理是用 postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据


前端后端通讯

多平台入口页=》某平台中转页=》平台首页

平台中转页

主要将其他平台的token 转成当前平台的信任token值

/** 单点登录获取票据 */
export async function getTicket(token) {
return request('/getTicket', {
  method: 'GET',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token },
});
}
/**免登录 */
export async function singleLogin(data) {
return request('/singleLogin', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'x-access-token': token   },
  data
});
}

export default (props: any) => {
const _singleLogin = async () => {
  try {
    //根据本地token 获取票据 getTicket 通过localStorage.getItem("token")
    const { code, data } = await getTicket(token);
    //免登录成功后跳转页面
    const link = '/home';
    if (code !== 200 || !data) {
      window.location.href = link;
      return;
    }
    //免登接口 获取登录token值
    const res: any = await singleLogin({
      ticket: data,
      source: '',//平台来源
    });
    if (res?.code === 200) {
      localStorage.setItem('tokneKey', res?.data.tokneKey);
      localStorage.setItem('tokenValue', res?.data.tokenValue);
    } else {
      console.log(res?.msg);
      localStorage.removeItem('tokneKey');
      localStorage.removeItem('tokenValue');
    }
    window.location.href = link;
  } catch (e) {
    window.location.href = link;
  }
};
useEffect(() => {
  _singleLogin();
});

return (
  <div style={{ width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', display: 'flex' }}>
    <Spin spinning={loading}></Spin>
  </div>
);
};


作者:NeverSettle_
来源:https://juejin.cn/post/7021407926837313544

收起阅读 »

关于防抖函数的思考

防抖概念本质:是优化高频率执行代码的一种手段。防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。一个经典的比喻:想象每天上班大厦底下的电梯。把电梯完成一次运送,类...
继续阅读 »

防抖概念

本质:是优化高频率执行代码的一种手段。

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

好处:能够保证用户在频繁触发某些事件的时候,不会频繁的执行回调,只会被执行一次。

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这就是防抖策略(debounce)。

用于测试的HTML结构

实现效果:鼠标在盒子上移动时,盒子中央打印出数字。



      //未实现防抖时的测试代码
     const container = document.querySelector('#container')
     let count = 0
     function move(e) {
       container.innerHTML = count++
       console.log(this)
       console.log(e)
    }
     container.addEventListener('mousemove', move)

未实现防抖时对应的页面效果如下:


     //实现防抖后的测试代码
     const container = document.querySelector('#container')
     let count = 0
     function move(e) {
       container.innerHTML = count++
       console.log(this)
       console.log(e)
    }
     const test = debounce(move, 500, true)
     container.addEventListener('mousemove', test)
     const btn = document.querySelector('button')
     btn.onclick = function () {
       test.cancel()
    }

实现防抖后对应的页面效果如下:


接下来记录我一步步思考完善的过程。

v1.0 简单实现一个防抖(非立即执行版本)

function debounce(func, delay) {
 let timeout
 return function () {
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(func, delay)
}
}

问题探讨:发现打印出来的this是window,打印出来的e是undefined。实际想要得到的是div#container和mouseEvent。出现这种情况的原因:在container的鼠标移动事件调用debounce函数时,在传递给形参func的实参move里打印了this与e。注意move是在定时器setTimeout里,定时器里的this在非严格模式下指向的是window对象,而window对象里的e自然是undefined。解决办法是在return的function里保存this与arguments,通过apply改变func的this指向同时把保存的参数传递给func。

v2.0 解决了this指向和event对象的问题。

function debounce(func, delay) {
 let timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(function () {
     func.apply(context, args)
  }, delay)
}
}

问题探讨:发现第一次不能立即执行,需要等到delay秒以后才会执行第一次。

v3.0 立即执行版本

function debounce(func, delay) {
 let timeout
 return function () {
   const context = this,
     args = arguments,
     callNow = !timeout
   if (timeout) clearTimeout(timeout)
   timeout = setTimeout(function () {
     timeout = null
  }, delay)
   if (callNow) func.apply(context, args)
}
}

Q:为什么利用callNow = !timeout来判断?而不是用callNow = true,然后在定时器内将callNow设置为false?

首先解答为什么不能用布尔值来判断。因为定时器是异步任务,在delay时间段内,callNow始终为true,这就会导致func在delay时间段内会一直触发,直到时间到达delay,callNow变成false才会停止执行func。

再回到为什么可以利用callNow = !timeout来判断的问题上。在首次触发mousemove事件时,'let timeout'执行,此时timeout为undefined;callNow对timeout取反为true;因为此时timeout为undefined,跳过清除定时器操作;把定时器赋值给timeout,注意此时timeout保存的值是1(第一个定时器的id),但是定时器是异步任务,里面的'timeout = null'尚未执行;接下来判断callNow为true,执行func函数,达到了立即执行的效果。在delay秒内第二次移动鼠标,此时timeout保存的值为1,callNow取反为false;清除上一个id为1的定时器;timeout保存值2(id为2的定时器),判断callNow为false,不执行func;反之如果等到delay秒后第二次移动鼠标,此时异步任务已执行,timeout变为null,callNow取反为true,就会执行func。注意点:这里利用了闭包,timeout是可以被访问的。

问题探讨:可以通过传入一个参数来判断实际业务需求是要立即执行还是非立即执行。

v4.0 立即执行与非立即执行结合版本(immediate为true时立即执行,反之非立即执行)

function debounce(func, delay, immediate) {
 let timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
}
}

问题探讨:继续完善,如果需要获得func函数的返回值该怎么办呢?那就需要把func的执行结果保存为一个result变量return出来。由此又引出了一个问题,setTimeout是一个异步任务,return时获得的是undefined,只有在立即执行的情况下会获得返回值(immediate为true时)。

v5.0 包含返回值的版本

function debounce(func, delay, immediate) {
 let result, timeout
 return function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
   return result
}
}

问题探讨:当delay设置时间过长时(比如30秒甚至更长),我只有等到delay时间过后才能再次触发,如果可以把取消防抖绑定在一个按钮上,点击之后可以立即执行代码。需要考虑的问题是:可以把这个功能做成是debounce的一个cancel方法,因为函数也是一个对象。具体实现思路应该是把原先return出来的函数用一个变量debounced保存,然后再定义debounced.cancel,赋值为一个函数。

v6.0 包含取消功能的版本

function debounce(func, delay, immediate) {
 let timeout, result
 const debounced = function () {
   const context = this,
     args = arguments
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(function () {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(context, args)
  } else {
     timeout = setTimeout(function () {
       func.apply(context, args)
    }, delay)
  }
   return result
}
 debounced.cancel = function () {
   if (timeout) clearTimeout(timeout)
   //需要注意,这里的目的并不是为了避免内存泄漏!而是为了让取消后鼠标再次移入盒子能立即执行代码。如果不置空,取消过后再移入,是不会立即执行打印数字的操作的。
   timeout = null
}
 return debounced
}


v7.0 ES6箭头函数版本(省略了this指向与参数对象的版本)

function debounce(func, delay, immediate) {
 let timeout, result
 //注意下面的函数声明不能改成箭头函数,否则this会指向window
 const debounced = function () {
   if (timeout) clearTimeout(timeout)
   if (immediate) {
     const callNow = !timeout
     timeout = setTimeout(() => {
       timeout = null
    }, delay)
     if (callNow) result = func.apply(this, arguments)
  } else {
     timeout = setTimeout(() => {
       func.apply(this, arguments)
    }, delay)
  }
   return result
}
 debounced.cancel = () => {
   if (timeout) clearTimeout(timeout)
   timeout = null
}
 return debounced
}

作者:GreyJiangy
来源:https://juejin.cn/post/7093466427805401118

收起阅读 »

跟我学flutter:细细品Widget(一)Widget&Element初识

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Everything's a widget!

Widget

Flutter 中 Widget是一个“描述一个UI元素的配置信息”,Widget就是接受元素,而不是真是绘制的显示元素。 类比原生的Android开发,Widget更像是负责UI配置的xml文件,而非负责绘制组件的View。 当一个Widget状态发生变化时,Widget就会重新调用build()函数来返回控件的描述,过程中Flutter框架会与之前的Widget进行比较,确保实现渲染树中最小的变动来保证性能和稳定性。换句话说,当Widget发生改变时,渲染树只会更新其中的一小部分而非全部重新渲染。

源码

@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });

final Key? key;

@protected
@factory
Element createElement();

@override
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}

@override
@nonVirtual
bool operator ==(Object other) => super == other;

@override
@nonVirtual
int get hashCode => super.hashCode;

static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}

图: 在这里插入图片描述

@immutable

@immutable widget中的属性时不可变的,如果有可变的你需要放在state中。

如果属性发生变更flutter则会重新构建Widget树,一旦 Widget 自己的属性变了自己就会被替换。

如你在开发过程中会有如下提示:This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final:

key

主要用于控制当 Widget 更新时,对应的 Element 如何处理 (是更新还是新建)。若某 Widget 是其「Parent Widget」唯一的子节点时,一般不用设置 key

LocalKey

LocalKey是diff算法的核心所在,用做Element和Widget的比较。常用子类有以下几个:

ValueKey:以一个数据为作为key,比如数字、字符等。 ObjectKey:以Object对象作为Key。 UniqueKey:可以保证key的唯一性,如果使用这个类型的key,那么Element对象将不会被复用。 PageStorageKey:用于存储页面滚动位置的key。

GlobalKey

每个globalkey都是一个在整个应用内唯一的key。globalkey相对而言是比较昂贵的,如果你并不需要globalkey的某些特性,那么可以考虑使用Key、ValueKey、ObjectKey或UniqueKey。 他有两个用途:

  1. 允许widget在应用程序中的任何位置更改其parent而不丢失其状态。应用场景:在两个不同的屏幕上显示相同的widget,并保持状态相同。
  2. 可以获取对应Widget的state对象:

createElement

一个 widget 可以对应多个Element

canUpdate

控制一个widget如何替换树中另一个widget。如果两个widget的runtimeType与key相同,则表示新的widget将替换旧的widget,并调用Element.update更新Element;否则旧的element将从树中移出,新的element插入树中。

Widget在重新build的时候,是增量更新的,而不是全部更新 runtimeType就是这个widget的类型

Widget类大家族

在这里插入图片描述

简述(后面文章将展开讲解):

  • StatelessWidget:无状态Widget
  • StatefulWidget:有状态Widget,值得注意的是StatefulWidget是不可变的,变化的状态在。
  • ProxyWidget:其有2个比较重要的子类, ParentDataWidget和InheritedWidget
  • RenderObjectWidget:持有RenderObject对象的Widget,RenderObject是完成界面的布局、测量与绘制,像Padding,Table,Align都是它的子类

Widget的创建可以做到复用,通过const修饰,否则setState后,Widget重新被创建了(Element不会重建)

Element

通过Widget Tree,会生成一系列Element Tree,其主要功能如下:

  1. 维护这棵Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等
  2. Element 是 Widget 和 RenderObject 的粘合剂,根据 Element 树生成 Render 树(渲染树)

Element类大家族

在这里插入图片描述

两大类:

简述(后面文章将展开讲解):

ComponentElement

组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidget和StatefulWidget相对应的Element:StatelessElement和StatefulElement,即属于ComponentElement。

RenderObjectElement

渲染类Element,对应Renderer Widget,是框架最核心的Element。RenderObjectElement主要包括LeafRenderObjectElement,SingleChildRenderObjectElement,和MultiChildRenderObjectElement。其中,LeafRenderObjectElement对应的Widget是LeafRenderObjectWidget,没有子节点;SingleChildRenderObjectElement对应的Widget是SingleChildRenderObjectWidget,有一个子节点;MultiChildRenderObjectElement对应的Widget是MultiChildRenderObjecWidget,有多个子节点。

Element生命周期

Element有4种状态:initial,active,inactive,defunct。其对应的意义如下:

  • initial:初始状态,Element刚创建时就是该状态。
  • active:激活状态。此时Element的Parent已经通过mount将该Element插入Element Tree的指定的插槽处(Slot),Element此时随时可能显示在屏幕上。
  • inactive:未激活状态。当Widget Tree发生变化,Element对应的Widget发生变化,同时由于新旧Widget的Key或者的RunTimeType不匹配等原因导致该Element也被移除,因此该Element的状态变为未激活状态,被从屏幕上移除。并将该Element从Element Tree中移除,如果该Element有对应的RenderObject,还会将对应的RenderObject从Render Tree移除。但是,此Element还是有被复用的机会,例如通过GlobalKey进行复用。
  • defunct:失效状态。如果一个处于未激活状态的Element在当前帧动画结束时还是未被复用,此时会调用该Element的unmount函数,将Element的状态改为defunct,并对其中的资源进行清理。

Element4种状态间的转换关系如下图所示:

在这里插入图片描述


收起阅读 »

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

本节主要讲如何使用kg_charts中的雷达图表,来绘制一个雷达图,下一章节则会对如何绘制一个可点击雷达图表进行详细说明。 最近我在开发有关雷达图表的的业务,但的确在线上找不到可以快速集成的雷达图表,找到一篇文章(Flutter雷达图package)但不是很好定制化我们的业务,但其中的代码有比较好的借鉴。然后我借鉴了部分代码,进行了kg_charts的开发。

集成方式

dependencies:
kg_charts: ^0.0.1

源码地址:github.com/smartbackme…

展示效果

1、圆形雷达图表

在这里插入图片描述

2、方形雷达图表

在这里插入图片描述

3、方形可点击雷达图表(点击效果为气泡)

在这里插入图片描述

4、方形多绘制区域图表(自定义展示文字)

在这里插入图片描述

4、方形多绘制区域图表(无自定义展示文字)

在这里插入图片描述

参数说明

参数类型是否必要说明
radarMapRadarMapModel包含 图例,雷达点,雷达数据,半径 ,雷达种类(圆形,方形),文字最大宽度,内部画几条线(LineModel中包含绘制线颜色,文字大小等)
textStylestyle外部绘制文字颜色与大小
isNeedDrawLegendbool默认为true
lineTextfun内部线上画的文字,根据数据动态生成,如果为空则不展示
dilogTextfun点击出现的dialog,根据数据动态生成,如果为空则不展示
outLineTextfun外部线上画的文字,根据数据动态生成,如果为空则不展示

详细使用说明

图片说明 在这里插入图片描述

代码使用说明

1、图例

legend: [
LegendModel('10/10',const Color(0XFF0EBD8D)),
LegendModel('10/11',const Color(0XFFEAA035)),
]

2、维度数据 如上代码所示,假设目前有两个日期维度,(业务假设是两天的考试)制定两个维度。

data: [
MapDataModel([100,90,90,90,10,20]),
MapDataModel([90,90,90,90,10,20]),
],

两个维度需要配置两套数据

维度和数据必须对应,两个维度必须是两套数据

3、数据组

indicator: [
IndicatorModel("English",100),
IndicatorModel("Physics",100),
IndicatorModel("Chemistry",100),
IndicatorModel("Biology",100),
IndicatorModel("Politics",100),
IndicatorModel("History",100),
]

数据的长短必须与数据的参数一致,比如说是六个科目,那么每套数据必须是6个数据,这个数据设置一个最大数据值,而且数据组中的值不能比该数据大。

4、RadarMapModel中其他基本参数

radius: 130,
shape: Shape.square,
maxWidth: 70,
line: LineModel(4),

radius 半径 shape 圆形的图还是方形的图 maxWidth 展示外环文字最大宽度 line 内环有几个环(还可配置内环文字大小和颜色)

5、其他基本配置

textStyle: const TextStyle(color: Colors.black,fontSize: 14),
isNeedDrawLegend: true,
lineText: (p,length) => "${(p*100~/length)}%",
dilogText: (IndicatorModel indicatorModel,List legendModels,List mapDataModels) {
StringBuffer text = StringBuffer("");
for(int i=0;i "${data*100~/max}%",

textStyle : 外环文字颜色,大小 isNeedDrawLegend:是否需要图例 lineText : 线上标注的文字(动态) 如上代码所示是转换为% dilogText:点击后弹出的浮动框(动态) 如上代码所示把日期都输出 outLineText:区域外环是否展示文字(动态) 如上代码所示是转换为%

整体代码展示

RadarWidget(
radarMap: RadarMapModel(
legend: [
LegendModel('10/10',const Color(0XFF0EBD8D)),
LegendModel('10/11',const Color(0XFFEAA035)),
],
indicator: [
IndicatorModel("English",100),
IndicatorModel("Physics",100),
IndicatorModel("Chemistry",100),
IndicatorModel("Biology",100),
IndicatorModel("Politics",100),
IndicatorModel("History",100),
],
data: [
// MapDataModel([48,32.04,1.00,94.5,19,60,50,30,19,60,50]),
// MapDataModel([42.59,34.04,1.10,68,99,30,19,60,50,19,30]),
MapDataModel([100,90,90,90,10,20]),
MapDataModel([90,90,90,90,10,20]),
],
radius: 130,
duration: 2000,
shape: Shape.square,
maxWidth: 70,
line: LineModel(4),
),
textStyle: const TextStyle(color: Colors.black,fontSize: 14),
isNeedDrawLegend: true,
lineText: (p,length) => "${(p*100~/length)}%",
dilogText: (IndicatorModel indicatorModel,List legendModels,List mapDataModels) {
StringBuffer text = StringBuffer("");
for(int i=0;i "${data*100~/max}%",
),
收起阅读 »

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

平时在做flutter Plugin或者 Package的时候,如果觉得自己做的还不错,想要分享到PUB库上如何操作?虽然官方已经告诉我们如何操作,但是呢由于一些特殊的原因,采用官方的方式并不能上传到PUB库上,今天就跟着我学习一下如何上传pub库吧。

准备开始

开始前需要你已经有一个已经开发好的库来进行提交了。 比如我的这个

1650433668(1).png

如图红色箭头表示的是必须要存在的两个文件,如果没有的话,需要添加你的开源协议。编写你的README文档。

开源协议和README我就不做介绍了,咱们来看看yaml文件需要什么内容呢?

1650433806(1).png

红色的箭头分别说明了需要的内容

  • name 库名
  • description 描述
  • version 版本号
  • homepage 开源项目地址

注意:你必须先拥有google 账户

按照官方尝试

第一步: flutter packages pub publish --dry-run

1650433977(1).png

Package has 0 warnings 没得问题:(如果有问题的话,会在输出的最后几行提示你缺什么)

第二步: flutter packages pub publish

90e1871133e703c73e6f103e8351d90.png

输入完命令后会先检查项目结构,然后会问题是否准备好要发布了么?当然你需要输入Y

之后经过漫长的等待他会告诉你链接超时

第一次上传的话,必须登录谷歌账号。界面上会展示一个url,这时候你需要去复制URL,到你的浏览器。1e47eadba7744a6c0e67b6303dd7de4.png

哈哈哈好了,到此结束。你的电脑访问不了。就戛然而止了。当然作为一个开发者需要具备一定的访问外网能力。这里我给大家介绍我用的这款外网能力软件。

开始我们的外网之路

首先页面如图所示:

e79239a0caab1ee8a8733aa90984496.png 我需要记录配置中的一个关键参数:

HTTP(S)代理服务器

关键步骤

1、在你的CMD命令模式下输入如下命令

set http_proxy=http://127.0.0.1:49256
set https_proxy=https://127.0.0.1:49256

配置完成后执行**flutter packages pub publish** 与官方的步骤一致

第一次上传的话,必须登录谷歌账号。界面上会展示一个url,这时候你需要去复制URL,到你的浏览器。1e47eadba7744a6c0e67b6303dd7de4.png

之后会提示你succeed

这样你的库就会被上传到pub库里(当然你需要等待一段时间)

当然我建议你上 pub.flutter-io.cn/ (国内网站访问更快) 查看自己的库

看发文章的过程我的库已经上线了 pub.flutter-io.cn/packages/kg…

d229ea6531594b656bb402108cf42c1.png


收起阅读 »

“鬼才”论文致谢刷屏!感谢我导“似导非导”的指导...

有人在论文最后写道:“感谢我导似导非导的指导”。网友表示:简直说出了我的心声,但我不敢这么写。图源:豆瓣从开题到送审,导师一次没看过回想当年研究生毕业前,小编最怕的就是导师和任何导师所在的群里发出的消息,因为一不小心就要迎来“论文大改”的噩耗。但不是所有人都这...
继续阅读 »

5ed9000ffb2415b2c60c35770788dfe5.png

有人在论文最后写道:“感谢我导似导非导的指导”。网友表示:简直说出了我的心声,但我不敢这么写。

22ca8060b294d6d998bf85b6e02723ed.png

图源:豆瓣

从开题到送审,导师一次没看过

回想当年研究生毕业前,小编最怕的就是导师和任何导师所在的群里发出的消息,因为一不小心就要迎来“论文大改”的噩耗。

但不是所有人都这般“幸运”!

我们前段时间发的文章《导师半夜给学生发信息:你睡了吗?我改你的论文气得睡不着》的评论区,很多准毕业研究生对于这种经历十分羡慕,有人表示,(硕士)从开题到送审,导师一次都没看过,只有干活才能想到我。

424df4ce2a8d6ac1f4f0601fb85185f5.png

还有网友表示,“从初稿到盲审,我发给导师的文件,导师都没打开过。”看来指导是不可能指导的,只能自己求神祈祷盲审通过……

还有同学表达相对含蓄:感谢我导在我读博期间对我的帮助,虽然不多。

还有同学感谢了自己的导师,但是导师表示:不需要感谢我,我也没做什么。。。

这里不排除导师是一个淡泊名利、不以物喜的存在,但也没准儿是一个悲伤的故事,在学生的毕业论文及日常指导中,导师确实没有出太多力ce2a555eafe4d01d5afc0c686cabd411.png

“鬼才”毕业致谢文案来了

你的致谢写完了吗?


现在的你,可能论文还没开始写,选题还在修修改改,但是致谢已经想好了。

给大家分享一些 “鬼才”毕业致谢文案,你的致谢,写完了吗?

01

谁也不感谢了

我最牛逼

37d07ce9581f92bafbf1576c7d14d993.png

02

读博占三年,疫情也三年

一博士在论文最后写道:人生有几年?读博占三年,疫情也三年。

不少研究生在疫情刚爆发时入学,如今都要毕业了,疫情还没结束。

4568cf661ac8751335dbea7e1154ff73.png

图源:豆瓣

03

感谢全国人民在抗击COVID-19中所作的贡献。

b98851f55483964f3fe9c886a3b888eb.png

图源:豆瓣

04

为国为民、清新脱俗

纵有千言万语,也只汇作一句:感谢国家。或许这就是格局吧!

e26d8e1c79f14a4f5d890b1b87384778.png

图源:豆瓣

05

《超大格局》

53f899df536894a541647355e16286aa.png

06

caae6b30183cb6122b2cd262f0589dae.png

这里也提醒你一下

马上要毕业咯

07

即使生产了科研垃圾 我也感谢我自己

b7f188bbe093c8ea1a5e151070932707.png

08

感谢东拉西扯的网友

667ce6dee84fbf024803ff2dcf93cfaa.png

09

感谢同学在英雄联盟里的蹩脚操作

让我无法沉迷游戏7004ae094f5e0868ae1a9b4f42229166.png

fba0c1217ad55f75205fd87170e0f301.png

10

感谢还没出现的女朋友

46fb0efa07ef912ee67d2fd5d965a445.png

11

感谢偷了我那台存有初稿文件

和所有论文资料的电脑小偷

是你让我明白人生的反复

是你让我懂得有时候我们认为的不可能

只是缺少重来一次的勇气

396e60af7125bf366f00b2f19bec4015.png

12

感谢可爱猫咪

从我的键盘上移开的屁股

7ecf8ef7efa39fb6434c2761a09c4fad.png

13

只愿你们来世不要做一只科研鼠

要做就要做一只野老鼠

在大千世界自由徜徉

24130a08d5aff8a295c7be01b2e73360.png

14

感谢我的指导老师

对我的毕业设计提出了许多宝贵意见

如果不是他

这篇论文可能早就写完了49390cd3d9f93525fe31ed8a5b95d0f5.png

9780af69576cbe5a2131f35c4e7b7335.png

15

感谢我的师兄师姐们

0c868f484aac62a63c8a4414de2ccc7a.png

本科到博士,致谢风格大变

有不少同学写论文写到指导老师时,才发现,“我导的头衔居然变了。”

除了上述或幽默、或个性的同学,还是有很多同学,在毕业时,穷尽各种词汇,感谢自己的导师和同学。

有人总结道:

本科论文致谢,可以看到一个年轻的自己,对未来充满无限憧憬,又是感谢学校、又是感谢老师、再者感谢父母,像是在发表诺奖感言;

硕士论文致谢,写下了一些平静的感悟,但还是在感谢导师和父母时用了感叹号;

博士论文致谢,全文没有一个感叹号。暮气沉沉,稳如老狗。波澜不惊地写下了自己的感谢,和将来想做的事。

山高水长,一段终章的结束,只是下一段旅程的开始。只有一个小建议,大家如能当上导师,千万别再“似导非导”了a38c3a6939a244f351226ef8affd2922.png~

来源丨青塔学术、4A广告圈(ID:newggm)

收起阅读 »

奇葩公司按代码行数算工资,员工一个月提成2.6万遭开除

之前,有这么一个帖子吸引了很多网友的注意,是什么呢?该网友表示自己以前碰到过一个按代码行数算工资的公司,还不同的代码有不同的换算系数,考核部门没日没夜的在那数代码,各种争吵,后来有个同事利用规则刷到一个月提成2.6万,然后领导找他商量让他能不能少报点,结果第二...
继续阅读 »

之前,有这么一个帖子吸引了很多网友的注意,是什么呢?

该网友表示自己以前碰到过一个按代码行数算工资的公司,还不同的代码有不同的换算系数,考核部门没日没夜的在那数代码,各种争吵,后来有个同事利用规则刷到一个月提成2.6万,然后领导找他商量让他能不能少报点,结果第二天就离职了。


对此,有网友表示:不是应该代码越少越好的吗?

作为程序员,大家应该都清楚,我们编程的时候最注重的就是代码的精简,力求少编码多思考,因为很多时候代码越多,问题越多。

而程序员们常常所说的高内聚,低耦合,也是力求代码简洁的一种方式,所以程序员都会被要求尽量简化代码。

估计程序员都有过这样的体会,当审查一个功能模块的代码时,如果代码很多很乱,第一印象肯定不好,相反,如果该模块代码简洁明了,你会非常愉悦。

更通俗点讲就是代码越多,管理起来也就越困难:搜索代码库的时间会变长、查看文件导航也需要较长的时间、跟踪执行也会变的困难等。


有的网友则表示:这样会让代码一团乱,不然就会像外包公司一样。

相信很多人都看过外包公司的代码,他们的代码基本都是复制粘贴,特别乱,所以,很多程序员只要看到外包公司的代码都会很崩溃。

首先是,他们只会考虑到如何去完成任务,而不会去考虑整个项目中会出现的外在问题,比如,占用资源,项目大小等。


还有的网友搞笑的表示:这样的方式,可以把公司刷破产,虽然夸张,但是也不夸张。

一个程序员如果去追求代码量,随随便便写个循环什么的,这都是很简单的事。

对于这样的公司可以说是很奇葩的了,其一:按代码行数来算工资,就是不合理,从中就可以知道,该公司肯定是一家小公司,做不大的。

其二:既然制定了游戏规则,却不能按照规则来执行,被员工逮到漏洞,最后不执行,也是很难看的。

虽然说每个公司的标准都不一样,但这样的方式,真的很不赞同的。

大家怎么看的呢?

来源:java那些事

收起阅读 »

Flutter 必知必会系列 —— mixin 和 BindingBase 的巧妙配合

前面我们已经介绍了 Flutter 的入口方法 —— main,入口方法做了初始化、根节点生成并绑定等工作。这一节我们就详细介绍 Flutter 的初始化。 混入 mixin 混入是一个很实用的语法特性,可以让一个类在不成为某一个目标类的父类的情况下,目标类可...
继续阅读 »

前面我们已经介绍了 Flutter 的入口方法 —— main,入口方法做了初始化、根节点生成并绑定等工作。这一节我们就详细介绍 Flutter 的初始化。


混入 mixin


混入是一个很实用的语法特性,可以让一个类在不成为某一个目标类的父类的情况下,目标类可以使用混入类的方法和属性。混入的关键字是 withmixinonmixin 用来声明混入类,with 用来使用混入类,on 用来限制混入的层级。


最简单的使用如下:


首先: 声明混入类


mixin CustomerBinding {
String name = 'CustomerBinding';

void printName() {
print(name);
}
}

然后:目标类添加混入类


class TestClass with CustomerBinding {

}

我们使用 with 关键字为 TestClass 添加了混入类,那么 TestClass 中就有了 name 字段printName 方法


最后:使用目标类


void main(List<String> args) { 
TestClass().printName();
}

即使 TestClass 没有明确的声明 printName,也可以被调用到,原因就是 TestClass 的混入类中有该方法。


上面的过程就是混入的基本使用,大家可能会问到的问题是:



  • 直接继承一个类不就行了么,为啥还有搞一个混入啊?



首先 看混入类和普通类的区别,混入类是不可以直接构造的,这意味着它的这一方面的功能要弱化一点点 🤏🏻。


其次 Dart 也是单继承的,就是一个类只能有一个直接的父类,而混入是可以多混入的,所以可以把不同的功能模块线性的混入到目标类中。


这就是为啥搞出来一个混入。




  • 既然一个类既可以混入又可以继承,那么继承和混入的优先级谁高呢?



结论是混入高于继承,我们先看例子。



void main(List<String> args) {
var testClass = TestClass();
// 第三处
testClass.printName();
}

class TestClass extends Parent with CustomerBinding {}

class Parent {
// 第二处
void printName() {
print('Parent');
}
}

mixin CustomerBinding {
// 第一处
void printName() {
print('CustomerBinding');
}
}

第一处 和 第二处分别在混入类和父类中定义了 同名方法


第三处是使用该方法,控制台打印的是 CustomerBinding


出现这种现象的原因是:混入的实现是依靠生成中间类的方式。上面的继承关系如下:


每混入一个类都会生成一个中间类,比如上面的例子,就根据 CustomerBinding 生成一个中间类,这个类继承自 Parent,而 TestClass继承自中间类


所以 testClass 调用的就是中间类的方法,而中间类的方法就是 CustomerBinding 中的方法,所以打印了 CustomerBinding



  • 既然可以多混入,那么混入的执行顺序是什么呢?



结论:混入是线性的,后面的会覆盖前面的同名方法



看这个例子:


void main(List<String> args) {
var testClass = TestClass();
testClass.printName();
}

class TestClass extends Parent with CustomerBinding, CustomerBinding2 {}

class Parent {
void printName() {
print('Parent');
}
}

mixin CustomerBinding {
void printName() {
print('CustomerBinding');
}
}

mixin CustomerBinding2 {
void printName() {
print('CustomerBinding2');
}
}

上面的代码会打印 CustomerBinding2 ,因为 CustomerBinding2 在混入的最后面。上面形成的体系图如下:


image.png


TextClass 直接调用的就是距离它最近的父类,也就是 CustomerBinding2 中的方法,所以打印了 CustomerBinding2



  • 既然可以多混入,那么混入可以有层级吗?就是同名不方法不覆盖,在原有逻辑的基础上实现自己的逻辑。



结论是可以的,实现的方式就是混入限定 on



既然要调到前排混入类的逻辑,首先要知道有前排的存在。 比如子类调用父类的方法,可以用 super,前提是子类要 extends 父类。


而混入类是不知道是否有混入类存在的,这个时候就需要 on 来限定了。


看下面的例子:


void main(List<String> args) {
var testClass = TestClass();
testClass.printName();
}

class TestClass extends Parent with CustomerBinding, CustomerBinding2 {}

class Parent {
void printName() {
print('Parent');
}
}

mixin CustomerBinding on Parent{ //第一处
void printName() {
super.printName();
print('CustomerBinding');
}
}

mixin CustomerBinding2 on Parent{ //第二处
void printName() {
super.printName();
print('CustomerBinding2');
}
}

和前面的例子相比,第一处和第二处多了 on Parent,表示 CustomerBindingCustomerBinding 只能用在 Parent 的子类上,所以它俩内部的 printName 就可以调用到 super


截图3.png


而且根据上面的线性规则,每次调用 super 都是向前一个混入的类调用,所以最后把三个打印语句都执行了。


小结


上面介绍了混入类、混入类的规则、大家可能会问到的混入类的问题,混入在 Flutter 中经常遇到,比如我们写动画的 TickerProviderStateMixin、初始化的 Binding 等等,大家也可以在自己的项目用混入来封装公有逻辑,比如 Loading 等。


混入类的规则如下:



  • 混入高于继承

  • 混入是线性的,后面的会覆盖前面的同名方法

  • super 会保证混入的执行顺序为从前往后


知道了混入,下面我们来看 Flutter 是怎么用混入来实现初始化的。


Binding 初始化


前面我们讲了混入,下面我们就看看初始化中怎么使用混入的。


class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();//第一处
return WidgetsBinding.instance!;
}
}

这是初始化的代码,这个地方可以看 Flutter 必知必会系列 —— runApp 做了啥 这一篇的介绍。


我们这一节的任务就是看看 WidgetsFlutterBinding() 构造方法干了啥。


WidgetsFlutterBinding 继承自 BindingBase,并混入了 7 个类。


WidgetsFlutterBinding 没有构造方法,第一处直接调用到了父类 BindingBase 的构造方法中。如下:


BindingBase() {
//...省略代码
initInstances();//第一处
initServiceExtensions();//第二处

}

省略一些无关的代码,就剩下了第一处和第二处的代码。从名字就可以,看出来这俩方法是用来初始化的。


initInstances 用来初始化实例对象,initServiceExtensions 用来注册服务。


这里介绍一下 注册服务 是咋回事。


注册服务


Flutter 是运行在 Dart VM 上的,Flutter 应用和 Dart VM 是可以互相调用的,比如 Flutter 可以调用 Dart VM 的各种服务来获取,内存信息、类信息、调用方法等等,Dart VM 同样可以调用到 Flutter 层注册好的方法。


Flutter 和 Dart VM 的调用需要遵循 JSON 协议,详细的可以看这里 Json 协议


上面列出的方法,都是 Flutter 对 Dart VM 的调用。


Dart VM 对 Flutter 的调用也是一样的,只要注册过,名字可以匹对上就可以调用。


Flutter 的注册是 registerServiceExtension 方法。


void registerServiceExtension({
required String name,
required ServiceExtensionCallback callback,
}) {
final String methodName = 'ext.flutter.$name';
developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
// 代码省略
late Map<String, dynamic> result;
try {
result = await callback(parameters);
} catch (exception, stack) {

}
result['type'] = '_extensionType';
result['method'] = method;
return developer.ServiceExtensionResponse.result(json.encode(result));
});
}

registerServiceExtension 就是注册方法,接受的入参就是服务名字回调


服务名字:就是 FlutterDart Vm 能够认识的服务标示,方法名字就是 VM 可以调用到的名字。


回调:就是 VM 调用服务名字时,Flutter 做出的反应


这里注意一点,我们传递的名字会被 包装成 ext.flutter.$名字 的形式。


注册会调用 developerregisterExtension 方法。developer 是一个开发者包,里面有一个比较基础的 API


最后这个 registerExtension 会将名字和回调注册到 VM 中,这是一个 native 的方法。


external _registerExtension(String method, ServiceExtensionHandler handler);

大家感兴趣,可以从 native 看看。这里我们只需要知道 flutter 调用注册,就是为 VM 注册了一个执行 Flutter 方法的回调。


下面我们以注册的退出应用服务来验证注册过程。


registerSignalServiceExtension(
name: 'exit',
callback: _exitApplication,
);
Future<void> _exitApplication() async {
exit(0);
}

这个服务的效果是:只要 VM 调用 exit 方法,应用就退出去。


Dart VMFlutter 的通信遵循 socket 的协议,只要连接上虚拟机运行的 URL 就可以了。


首先 Flutterpubspec.yaml 文件中添加 vm_service 依赖


其次 Flutter 应用主动连接 vm 虚拟机


// 连接虚拟机的服务
Service.getInfo().then((value) {
String url = value.serverUri.toString();
Uri uri = Uri.parse(url);
Uri socketUri = convertToWebSocketUrl(serviceProtocolUrl: uri);
vmServiceConnectUri(socketUri.toString()).then((value) {
});
});

Service.getInfo 是获取虚拟机服务的 url,这是 Flutter 提供的 API ,这种方式更加方便。FlutterEngine 也提供了获取 url 的方法,但是需要通过插件来传递,使用不方便。


convertToWebSocketUrl 就是对 url 进行了转换,结果就是 WebSocket 可以识别的 url


vmServiceConnectUri 就是 FluttervmService 进行了连接


最后 我们调用一下:


Service.getInfo().then((value) {
String url = value.serverUri.toString();
Uri uri = Uri.parse(url);
Uri socketUri = convertToWebSocketUrl(serviceProtocolUrl: uri);
vmServiceConnectUri(socketUri.toString()).then((service) {

service.callServiceExtension('ext.flutter.exit',
isolateId: Service.getIsolateID(Is.Isolate.current),
args: {'enabled': true}); //第一处

});
});

第一处的代码执行之后 应用就退出去了,可以看一下效果。


Flutter DevTools 就是调用 Flutter 注册的服务来实现调试效果的,大家可以看这里:Flutter DevTools 的调试工具


上面就是 注册服务的过程和作用,下面我们来看 BaseBiding 注册了哪些服务:


void initServiceExtensions() {
if (!kReleaseMode) {
if (!kIsWeb) {
registerSignalServiceExtension(
name: 'exit',
callback: _exitApplication,
);
}
// These service extensions are used in profile mode applications.
registerStringServiceExtension(
name: 'connectedVmServiceUri',
getter: () async => connectedVmServiceUri ?? '',
setter: (String uri) async {
connectedVmServiceUri = uri;
},
);
registerStringServiceExtension(
name: 'activeDevToolsServerAddress',
getter: () async => activeDevToolsServerAddress ?? '',
setter: (String serverAddress) async {
activeDevToolsServerAddress = serverAddress;
},
);
}
}

exit 是退出应用,上面我们已经看过了。


connectedVmServiceUri 是设置虚拟机的URL


activeDevToolsServerAddress 设置是否可以连接 DevTools


小结


Binding 的构造方法会调用 initInstancesinitServiceExtensions 两个方法,其中 initInstances 用于初始化实例,initServiceExtensions 用于注册虚拟机可以调用的方法。


所以 Binding 的构造方法 起到了模版方法的功能,定义好了初始化的流程。


根据上面介绍到的规则,大家知道 WidgetsFlutterBinding 初始化执行的顺序吗?


就是从前向后的执行,因为每一个 Binding 都调用了 super


BaseBinding 的构造方法起到了模版方法的功能,定义好了初始化的流程。下面我们看各个 Binding 初始化了啥。


GestureBinding 初始化


initInstances 初始化实例



@override
void initInstances() {
super.initInstances();
_instance = this; //第一处
window.onPointerDataPacket = _handlePointerDataPacket;//第二处
}
static GestureBinding? get instance => _instance;
static GestureBinding? _instance;//第一处

第一处就是对 \_instance 进行了赋值,因为 initInstances 是在构造方法中调用的,并且构造方法值调用一次,所以 \_instance 只会初始化一次,这也是 Flutter 中另外一种单例的实现方式。


第二处就是对 windowonPointerDataPacket 进行赋值。onPointerDataPacket 是一个方法回调,就是屏幕的手势会调用到这里。


所以 GestureBinding_handlePointerDataPacketFlutter 手势系统的起点。


如果我们自己对 onPointerDataPacket 进行重新复制,那么就会走到我们自定义的手势流程。


比如:


@override
void initState() {
super.initState();

ui.window.onPointerDataPacket = (PointerDataPacket packet) {

};
}

这样不管怎么点击、滑动屏幕,都是没有任何反应的。


这个有什么用呢?拦截手势增加自定义操作。


比如 屏幕上有一个浮窗,点击浮窗以外的其他区域,关闭浮窗,就可以在这个里面做。定义的点击埋点也可以在这里做。


_handlePointerDataPacket 的具体流程,我们后面在详细介绍。


各个子 Binding 初始化


SchedulerBinding 初始化


initInstances 初始化实例


@override
void initInstances() {
super.initInstances();
_instance = this; //第一处

if (!kReleaseMode) { //第二处
addTimingsCallback((List<FrameTiming> timings) {
timings.forEach(_profileFramePostEvent);
});
}
}

第一处的代码是不是很熟悉,同样实例化单例对象。


第二处的代码就是增加了一个回调,这个回调就是一个帧绘制的监听,类似于我们的性能监控,只不过监控的是帧的信息,包含了以下信息:


postEvent('Flutter.Frame', <String, dynamic>{
'number': frameTiming.frameNumber,
'startTime': frameTiming.timestampInMicroseconds(FramePhase.buildStart),
'elapsed': frameTiming.totalSpan.inMicroseconds,
'build': frameTiming.buildDuration.inMicroseconds,
'raster': frameTiming.rasterDuration.inMicroseconds,
'vsyncOverhead': frameTiming.vsyncOverhead.inMicroseconds,
});

initServiceExtensions 注册服务



@override
void initServiceExtensions() {
super.initServiceExtensions();

if (!kReleaseMode) {
registerNumericServiceExtension(
name: 'timeDilation',
getter: () async => timeDilation,
setter: (double value) async {
timeDilation = value;
},
);
}
}

注册了 timeDilation 服务,timeDilation 就是来设置动画慢放倍数的。Android StudioDevTools 都有这个调试功能。


ServicesBinding 初始化


initInstances 初始化实例


@override
void initInstances() {
super.initInstances();
//第一处
_instance = this;

//第二处
_defaultBinaryMessenger = createBinaryMessenger();
_restorationManager = createRestorationManager();

//第三处
_initKeyboard();
initLicenses();

//第四处
SystemChannels.system.setMessageHandler((dynamic message) => handleSystemMessage(message as Object));
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
SystemChannels.platform.setMethodCallHandler(_handlePlatformMessage);
readInitialLifecycleStateFromNativeWindow();
}

第一处 就是实例化单例对象,和之前的一样


第二处 就是处理 channel 通信数据恢复,可以在这一层做 channel 调用的拦截


第三处 就是初始化了键盘之类的内容


第四处 就是做了系统自带的 channel 的回调,system 是内存紧张的回调,lifecycle 是生命周期的回调,platform 是剪切板、系统声音等的回调


initServiceExtensions 初始化注册服务


@override
void initServiceExtensions() {
super.initServiceExtensions();
registerStringServiceExtension(
name: 'evict',
getter: () async => '',
setter: (String value) async {
evict(value);
},
);
}

void evict(String asset) {
rootBundle.evict(asset);
}

调试工具调用 ext.flutter.evict 就会从缓存中清除指定路径的资源。


PaintingBinding


initInstances 初始化实例


@override
void initInstances() {
super.initInstances();
_instance = this; //第一处
_imageCache = createImageCache(); //第二处
shaderWarmUp?.execute();//第三处
}

第一处 依然是初始化实例


第二处 声明了一个图片缓存,Flutter 自带了图片缓存,缓存的算法是 LRU ,缓存的大小是 100 MB,图片张数是 1000张。


第三处 是让 Skia 着色器执行一下,随便画了一个小图片,避免发起绘制任务的时候 Skia 初始化等待的时间。


RendererBinding


initInstances 初始化实例


@override
void initInstances() {
super.initInstances();
_instance = this; //第一处
_pipelineOwner = PipelineOwner( //第二处
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction; //第三处
initRenderView(); //第四处
_handleSemanticsEnabledChanged();
addPersistentFrameCallback(_handlePersistentFrameCallback); //第五处
}

第一处 依然是初始化实例


第二处 初始化了渲染绘制的 PipelineOwnerPipelineOwner 会管理绘制过程,比如布局、合成涂层、绘制等等


第三处 为 window 中与绘制相关的属性赋值,onMetricsChanged 是窗口尺寸变化的回调,onTextScaleFactorChanged 是系统文字变化的回调,onPlatformBrightnessChanged 是深色模式与否变化的回调


第四处 是根节点 RenderObject 的初始化


第五处 是添加帧阶段的回调,发起布局任务


initServiceExtensions 初始化注册服务


initServiceExtensions 中注册的服务都是和绘制、RenderObject相关的,代码较多,就不一一列举了。


debugPaint 就是 RenderObject 的边框


debugDumpRenderTree 就是打印出 RenderObject 的树信息等等


WidgetsBinding


initInstances 初始化实例


@override
void initInstances() {
super.initInstances();
_instance = this;//第一处

_buildOwner = BuildOwner(); //第二处
buildOwner!.onBuildScheduled = _handleBuildScheduled;

window.onLocaleChanged = handleLocaleChanged;
window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; //第三处

SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); //第四处
}

第一处 依然是初始化实例


第二处 是初始化 BuildOwnerBuildOwner 用于管理 Element,维护了 '脏' Element 的列表


第三处 是为 window 的属性赋值


第四处 是系统的物理返回键添加 channel 回调


initServiceExtensions 初始化注册服务


initServiceExtensions 中注册的服务都是和 Widget 相关的,代码较多,就不一一列举了。


debugDumpApp 就是打印 Widget 树的信息


showPerformanceOverlay 就是页面中添加帧性能的浮窗等等


小结


不知道到大家注意到一点没有,从 GestureBinding 开始到 WidgetsBinding 结束,它们的 initInstancesinitServiceExtensions 都调用了 super


所以按着我们之前介绍的混入规则,虽然 WidgetsBinding 在最后面,但是调用的顺序也是在最后面,这样保证了初始化的正确性


总结


这一篇介绍了混入的使用和规则,并借此延伸到了 Flutter 的初始化。WidgetsFlutterBinding 的继承体系看着唬人,其实就是从前向后的依次调用,后面我们就从第一个 GestureBinding 开始看起。


作者:一条上岸小咸鱼
链接:https://juejin.cn/post/7088962808385110053
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter事件之GestureBinding

Flutter在启动时(runApp)会进行一些浇水类的"粘合",WidgetsFlutterBinding作为主类,需要粘合一系列的Binding,其中GestureBinding就是事件处理类; GestureBinding是Flutter中管理手势事件的...
继续阅读 »

Flutter在启动时(runApp)会进行一些浇水类的"粘合",WidgetsFlutterBinding作为主类,需要粘合一系列的Binding,其中GestureBinding就是事件处理类;


GestureBinding是Flutter中管理手势事件的Binding,是Flutter Framework层处理事件的最起点;


GestureBinding实现了HitTestable, HitTestDispatcher, HitTestTarget,分别具有以下功能



  • hitTest命中测试

  • dispatchEvent事件分发

  • handleEvent处理事件()


成员变量:


//触点路由,由手势识别器注册,会把手势识别器的pointer和handleEvent存入
//以便在GestureBinding.handleEvent调用
final PointerRouter pointerRouter = PointerRouter();

//手势竞技场管理者,管理竞技场们的相关操作
final GestureArenaManager gestureArena = GestureArenaManager();

//hitTest列表,里面存储了被命中测试成员
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};

GestureBinding在_handlePointerDataPacket方法接收有Engine层传递过来的触点数据,经过数据包装转换为Framework层可处理的数据:PointerAddedEvent、PointerCancelEvent、PointerDownEvent、PointerMoveEvent、PointerUpEvent等等,随后在_handlePointerEventImmediately方法中进行命中测试和事件分发;


手指按下


当手指按下时,接收到的事件类型是PointerDownEvent


首先是命中测试


当事件类型是event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent会进行新的命中测试,命中测试相关请看,得到命中测试列表后,开始调用dispatchEvent进行事件分发。


void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
//当前事件是按下状态,重用hitTest结果
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}

事件分发


事件分发的目的是调用命中对象的handleEvent方法以处理相关逻辑,比如我们熟知的Listener组件,它做的事就是回调相关方法,比如按下时Listener会回调onPointerDown


## GestureBinding ##
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
...
}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
...
}
}
}

## Listener ##
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent)
return onPointerDown?.call(event);
if (event is PointerMoveEvent)
return onPointerMove?.call(event);
if (event is PointerUpEvent)
return onPointerUp?.call(event);
if (event is PointerHoverEvent)
return onPointerHover?.call(event);
if (event is PointerCancelEvent)
return onPointerCancel?.call(event);
if (event is PointerSignalEvent)
return onPointerSignal?.call(event);
}

我们知道命中测试最后会把GestrueBinding本身加入到列表中,所以最后也会执行GestrueBinding的handleEvent方法


handleEvent


GestrueBinding.handleEvent是处理手势识别器相关的逻辑,pointerRouter.route(event)调用了识别器的handleEvent方法(需要提前进行触点注册),随后的是竞技场的相关处理;可以看这里了解手势识别器;


## GestrueBinding ##
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);

if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}

手指抬起,


手指抬起会重用之前hitTest结果,并不会重新hitTest,如果是Listener组件,则会回调PointerUpEvent


作者:palpitation97
链接:https://juejin.cn/post/7087873740658180133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter使用source_gen快速提升开发效率

认识APT APT(Annotation Process Tool),注解处理器,可以在编译期或运行时获取到注解信息,进行生成代码源文件、其他文件或逻辑处理的功能。 Java中按注解保留的范围可以分为三类,功能也各不相同,分别是: SOURCE:编译期间丢...
继续阅读 »

认识APT


APT(Annotation Process Tool),注解处理器,可以在编译期或运行时获取到注解信息,进行生成代码源文件、其他文件或逻辑处理的功能。


Java中按注解保留的范围可以分为三类,功能也各不相同,分别是:




  • SOURCE:编译期间丢弃,编译完成后这些注解没有任何意义,可提供IDE语法检查,静态模版代码


    例 :@Override, @SuppressWarningsLombok




  • CLASS: 保留在class文件中,类加载期间被丢弃,运行时不可见,可以用于字节码操作、可获取到加载类信息的动态代码生成


    例:AspectJButterKnifeRoomEventBus3.0之后ARouter




  • RUNTIME:注解保留至运行期,结合反射技术使用


    例:RetrofitEventBus3.0之前




在应用程序构建的阶段分布如图:


img

第一阶段为编译期,由外部构建工具将源代码翻译成目标可执行文件,如exe。类似嵌入式c语言开发的构建工具make、cmake,java中为javac。对应SOURCE


第二阶段为执行期,生成的字节码.class文件是JVM可执行文件,由JVM加载.class文件、验证、执行的过程,在JVM内部完成,把.class翻译成平台相关的本地机器码。对应CLASS


第三阶段为运行时,硬件执行机器码过程,程序运行期间。对应RUNTIME



Flutter出于安全性考虑,不支持反射,所以本文讨论范围不包含运行时部分功能



为什么使用代码生成


在特定的场景下,代码自动生成有很多好处,如下几个场景:



  • 数据类(Data classes):这些类型的类相当简单,而且通常需要创建很多。因此,最好的方法是生成它们而不是手动编写每一个

  • 架构样板(Architecture boilerplate):几乎每个架构解决方案都会带有一定数量的样板代码。每次重复编写就会让人很头疼,所以,通过代码生成可以很大程度上避免这种情况。 MobX就是一个很好的这样的例子

  • 公共特性/方法(Common features/functions):几乎所有model类使用确定的方法,比如fromMap,toMap,和copyWith。通过代码可以一键生成所有这些方法


代码生成不仅节省时间和精力,提高效率,更能提升代码质量,减少手动编写的bug数量。你可以随便打开任何生成的文件,并保证它能正常运行


项目现状


使用领域驱动(DDD)架构设计,核心业务逻辑层在domain层,数据获取在service层,这两层包含了稳定数据获取架构,提供了稳定性的同时,也造成了项目架构的弊病,包含大量的模版代码。


经过多次激烈讨论,如果单纯的将servce层删掉,将势必导致domain层耦合了数据层获取的逻辑或是service层耦合底层数据池获取的逻辑,对domain层只关心核心业务和将来数据池的扩展和迁移都造成不利影响,总之,每一层都有意义。所以,最终决定保留


不删除又会导致,实现一个功能,要编写很多代码、类。为此需要一个开发中提升效率的折中方案


Dart运行时注解处理及代码生成库build刚好可以完成这个功能


确定范围


确定好Flutter支持代码生成的功能后,需要分析代码结构特点,确定使用范围


分析代码结构


主要业务逻辑实现分为两部分:


1、调用接口实现的获取数据流程


2、调用物模型实现的属性服务


两部分都在代码中有较高的书写频率,同时也是架构样板代码的重灾区,需要重点优化


期望效果




  • 定义好repo层,自动生成中间层代码




  • 文件名、类名遵循架构规范




  • 移动文件到指定位置




img

困难与挑战




  • source_gen代码生成配置流程、API熟悉、调试




  • 根据注解类信息,拿到类中方法,包括方法名、返回类型、必选参数、可选参数




  • 物模型设置时,set/get方法调用不同API,返回参数为对象时,要添加convert方法自动转换




  • 接口生成类文件移动到指定目录,物模型生成文件需要拼接




Build相关库


类似java中的Java-APT,dart中也提供一系列注解生成代码的工具,核心库有如下几个:



  • build:提供代码生成的底层基础依赖库,定义一些创建Builder的接口

  • build_config:提供解析build.yaml文件的支持库,由build_runner使用

  • build_runner:提供了一些用于生成文件的通用命令,触发builders执行

  • source_gen:提供build库的上层封装,方便开发者使用


生成器package配置


快速开始:


1、创建生成器package


创建注解解析器的package,配置依赖


dependency_overrides:
build: ^2.0.0
build_runner: ^2.0.0
source_gen: ^0.9.1

2、创建注解


创建一个类,添加const 构造函数,可选择有参或无参:


class Multiplier {
final num value;

const Multiplier(this.value);
}

3、创建Generator


负责拦截解析创建的注解,创建类继承GeneratorForAnnotation<T>,实现generate方法。和Java中的Processor类似


泛型参数是要拦截的注解,例:


class MultiplierGenerator extends GeneratorForAnnotation<Multiplier> {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
final numValue = annotation.read('value').literalValue as num;

return 'num ${element.name}Multiplied() => ${element.name} * $numValue;';
}
}

返回值是String,内容就是生成的代码,可以直接返回文本,例:


class PropertyProductGenerator extends Generator {
@override
String generate(LibraryReader library, BuildStep buildStep) {
final productNames = topLevelNumVariables(library)
.map((element) => element.name)
.join(' * ');

return '''
num allProduct() => $productNames;
''';
}
}

4、创建Builder


Generator是通过Builder触发的,创建Builder


Builder metadataLibraryBuilder(BuilderOptions options) => LibraryBuilder(
MemberCountLibraryGenerator(),
generatedExtension: '.info.dart',
);
Builder multiplyBuilder(BuilderOptions options) =>
SharedPartBuilder([MultiplierGenerator()], 'multiply');

Builder 是build 库中的抽象类



/// The basic builder class, used to build new files from existing ones.
abstract class Builder {
/// Generates the outputs for a given [BuildStep].
FutureOr<void> build(BuildStep buildStep);

Map<String, List<String>> get buildExtensions;
}

实现类在source_gen中,对Builder进行了封装,提供更友好的API。执行Builder要依赖build_runner ,允许通过dart 代码生成文件,是编译期依赖dev_dependency;只在开发环境使用


各个Builder作用:



  • PartBuilder:生成属于文件的part of代码。官方不推荐使用,更推荐SharedPartBuilder

  • SharedPartBuilder:生成共享的可和其他Builder合并的part of文件。比PartBuilder优势是可合并多个部分文件到最终的一个.g.dart文件输出

  • LibraryBuilder:生成单独的Dart 库文件

  • CombiningBuilder:合并其他SharedPartBuilder生产的文件。收集所有.*.g.part文件



需要注意的是SharedPartBuilder 会生成.g.dart后缀文件输出,并且,执行命令前,要在源文件引入part '*.g.dart'才会生成文件


LibraryBuilder,比较灵活,可以扩展任意后缀



5、配置build.yaml


创建的Builder要在build.yaml文件配置,build期间,会读取该文件配置,拿到自定义的Builder


# Read about `build.yaml` at https://pub.dev/packages/build_config
builders:
# name of the builder
member_count:
# library URI containing the builder - maps to `lib/member_count_library_generator.dart`
import: "package:source_gen_example/builder.dart"
# Name of the function in the above library to call.
builder_factories: ["metadataLibraryBuilder"]
# The mapping from the source extension to the generated file extension
build_extensions: {".dart": [".info.dart"]}
# Will automatically run on any package that depends on it
auto_apply: dependents
# Generate the output directly into the package, not to a hidden cache dir
build_to: source

property_multiply:
import: "package:source_gen_example/builder.dart"
builder_factories: ["multiplyBuilder"]
build_extensions: {".dart": ["multiply.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]

使用package配置


1、添加依赖


pubspec.yaml文件添加生成器package依赖。可添加到dev_dependencies


dev_dependencies:
source_gen_builder:
path: ../source_gen_builder

2、添加注解


在要生成文件类名添加注解,这里用官方例子


part 'library_source.g.dart';

@Multiplier(2)
const answer = 42;

const tau = pi * 2;

3、配置build.yaml


使用的package也需要配置build.yaml,用来定制化build行为。例如,配置注解扫描范围,详情见build_config


# Read about `build.yaml` at https://pub.dev/packages/build_config
targets:
$default:
builders:
# Configure the builder `pkg_name|builder_name`
# In this case, the member_count builder defined in `../example`
source_gen_builder|property_impl:
generate_for:

source_gen_builder|retrofit:
generate_for:
- lib/*/retrofit.dart

# The end-user of a builder which applies "source_gen|combining_builder"
# may configure the builder to ignore specific lints for their project
source_gen|combining_builder:
options:
ignore_for_file:
- lint_a
- lint_b

4、执行命令


在使用的package根目录下执行:


flutter packages pub run build_runner build 

结果展示:


生成*.g.dart文件


// GENERATED CODE - DO NOT MODIFY BY HAND

// ignore_for_file: lint_a, lint_b

part of 'library_source.dart';

// **************************************************************************
// MultiplierGenerator
// **************************************************************************

num answerMultiplied() => answer * 2;

5、debug调试


复制该目录下文件到使用package根目录下



Android Studio下配置



点击debug按钮,打断点调试即可



注意,debug需要生成器package和使用package在统一工程下才可以



配合脚本使用


上述生成文件都是带.g.dart或其他后缀文件,并且目录和源文件同级。如果想生成架构中的模版源文件,并生成到其他目录,可以配合脚本实现,可以帮你完成:后缀名修改、移动文件目录、文件代码拼接的功能


这部分代码根据个人情况实现,大体框架如下


#!/bin/bash
# cd到执行目录
cd ../packages/domain
# 执行build命令
flutter packages pub run build_runner build --delete-conflicting-outputs
# 循环遍历目录下文件,
function listFiles()
{
#1st param, the dir name
#2nd param, the aligning space
for file in `ls $1`;
do
if [ -d "$1/$file" ]; then
listFiles "$1/$file" "$2"
else
if [[ $2$file =~ "repository.usecase.dart" ]]
then
# 找到生成对应后缀文件,执行具体操作
# dosmothing
fi

if [[ $2$file =~ "repository.impl.dart" ]]
then
# dosmothing
fi

fi
done
}
listFiles $1 "."

总结


以上,就是利用Dart-APT编译期生成代码的步骤和调试过程


最后实现的效果可以做到只声明业务层接口声明,然后脚本一键生成service中间层实现。后面再有需求过来,再也不用费力梳理架构实现逻辑和敲代码敲的手指疼了


截止到目前,项目现在已有接口统计:GET 79、POST 97,并随着业务持续增长。从统计编码字符的维度来看,单个repo,一只接口,一个参数的情况下需手动编写222个,自动生成1725个,效率提升88.6%



底层的数据获取使用的retrofit,同样是自动生成的代码所以不计入统计字符范围,这里的效率提升并不是指一个接口开发完成的整体效率,而是只涵盖从领域到数据获取中间层的代码编写效率



字符和行数优化前后对比:


img

达到了既保证不破坏项目架构,又提升开发效率的目标


作者:QiShare
链接:https://juejin.cn/post/7081165404113993736
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

仅用了81行代码,实现一个简易打包器

最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器1. 3个js文件index.js -> 依赖 subtraction.js => 依赖 sum.js2. 5个npm依赖包代码const path = require(...
继续阅读 »

前言

最近打算跳槽到大厂,webpack打包流程必须了解,于是尝试一下手写一个打包器

准备工作

1. 3个js文件

index.js -> 依赖 subtraction.js => 依赖 sum.js


2. 5个npm依赖包


代码

const path = require("path")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const fs = require("fs")
const { transformFromAst } = require("babel-core")
const config = {
   entry: "./src/index.js",
   output: {
       path: "./src/",
       filename: "build.js",
  },
}
const { output } = config
let id = 0
const createAsset = (entryFile) => {
   // 读取文件
   const source = fs.readFileSync(entryFile, "utf-8")
   // 代码转为ast,为了转换成ES5
   const ast = parser.parse(source, {
       sourceType: "module",
  })
   const dependents = {}
   // 借用traverse提取文件import的依赖
   traverse(ast, {
       ImportDeclaration({ node }) {
           dependents[node.source.value] = node.source.value
      },
  })
   // es6语法转es5
   const { code } = transformFromAst(ast, null, {
       presets: ["env"],
  })
   return {
       entryFile,
       dependents,
       code,
       id: id++,
       mapping: {},
  }
}
const createGraph = (rootPath) => {
   // 从根路径出发,获取所有与根路径相关依赖存放到modules中
   const mainAsset = createAsset(rootPath)
   const modules = [mainAsset]
   const dirname = path.dirname(rootPath)
   for (let asset of modules) {
       const { dependents } = asset
       for (let dep in dependents) {
           const childPath = path.join(dirname, dependents[dep])
           const childAsset = createAsset(childPath)
           asset.mapping[dependents[dep]] = childAsset.id
           modules.push(childAsset)
      }
  }
   return modules
}
// 转换一下数据结构
const createModules = (graph) => {
   const obj = {}
   graph.forEach((item) => {
       obj[item.id] = [item.code, item.mapping]
  })
   return obj
}
// 生成文件
const writeFiles = (modules) => {
   // 编译模板,modules是不固定的,其他都一样
   const bundle = `
   ;(function (modules) {
       const require = (id) => {
           const [code, mapping] = modules[id]
           const exports = {}
           ;(function (_require, exports, code, mapping) {
               const require = (path) => {
                   return _require(mapping[path])
               }
               eval(code)
           })(require, exports, code, mapping)
           return exports
       }
       require(0)
   })(${JSON.stringify(modules)})
   `
   // 生成文件
   const filePath = path.join(output.path, output.filename)
   fs.writeFileSync(filePath, bundle, "utf-8")
}
const graph = createGraph(config.entry)
const modules = createModules(graph)
writeFiles(modules)


作者:SYX
来源:juejin.cn/post/7091225169120722952

收起阅读 »

【 Flutter 极限测试】连续 1000000 次 setState 会怎么样

测试描述可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?本篇文章将做一个极限测试,看一下连续触发 1000000 次 setState 会发生...
继续阅读 »
测试描述

可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?

本篇文章将做一个极限测试,看一下连续触发 1000000setState 会发生什么?是连续触发 1000000 次屏幕更新,导致界面卡死,还是无事发生?用你的眼睛来见证吧!


1、测试代码说明

如下所示,在默认案例基础上添加了两个蓝色文字,点击时分别触发如下的 _increment1_setState1000000 。其中 _setState1000000 是遍历执行 1000000setState


void _increment1() {
 setState(() {
   _counter++;
});
}

void _setState1000000() {
 for (int i = 0; i < 1000000; i++) {
   setState(() {
     _counter++;
  });
}
}

2、运行结果

如下是在 profile 模式下,网页调试工具中的测试结果。可以看出即使连续触发了 1000000 次的 steState ,也不会有 1000000 次的帧触发来更新界面。也就是说,并非每次的 steState 方法触发时,都会进行重新构建,所以,你真的懂 State#steState 吗?



3. 源码调试分析

如下,在 State#setState 源码中可以看出,它只做了两件事:

  • 触发入参回调 fn 。

  • 执行持有元素的 markNeedsBuild 方法。


这里 1121 行的 fn() 做了什么,不用多说了吧。就是 setState 入参的那个自加方法。



此时该 State 中持有的 _element 对象类型是 StatefulEmement ,也就是 MyHomePage 组件创建的元素。



Elememt#markNeedsBuild 方法中没有一个非常重要的判断,那就是下面 4440 行 中,如果 dirty 已经是 true 时,则直接返回,不会执行接下来的方法。如果 dirtyfalse ,那接下来会置为 true

另外,owner.scheduleBuildFor 用于收集脏元素,以及申请新帧的触发。这就是为什么连续执行 1000000stateState 时,该元素不会加入脏表 1000000 次,不会触发 1000000 帧的原因。


总的来说, State#setState 的核心作用就是把持有的元素标脏申请新帧调度。而只有新帧到来,执行完构建之后,元素的 dirty 才会置为 false 。也就是说,两帧之间,无论调用多少次 setState ,都只会触发一次, 元素标脏申请新帧调度 。这就是为什么连续触发 1000000 次,并无大事发生的原因。


作者:张风捷特烈
来源:https://juejin.cn/post/7091471603774521352

收起阅读 »

一次关于架构的“嘴炮”

文章标题很随意,些微有一些骗点击的“贼意”;但内容却是充满了诚意,想必你已经感受到了。这是一次源于头条 Android 客户端软件架构问题的探讨,之所以冠上“嘴炮”之名,是因为它有一些务虚;同时又夹杂了一些方法论,不仅适用于客户端软件架构,也适用于其他工作场景...
继续阅读 »

文章标题很随意,些微有一些骗点击的“贼意”;但内容却是充满了诚意,想必你已经感受到了。

这是一次源于头条 Android 客户端软件架构问题的探讨,之所以冠上“嘴炮”之名,是因为它有一些务虚;同时又夹杂了一些方法论,不仅适用于客户端软件架构,也适用于其他工作场景,希望对大家有所帮助。

为了拉满读者的带入感,且以“我们”为主语,来看架构的挑战、判断和打法。

我们的挑战

期望高

优秀的公司对架构都有着很高的期许,都希望有一个良好的顶层设计,从上到下有统一的认知,遵循共同的规范,写出让人舒适的代码,甚至有那么一丢偷懒,有没有“一劳永逸”的架构设计可保基业长青?

然而高期望意味着高落差,面对落差,我们容易焦虑:

  • 代码什么时候能写的看上去本就应该是那个样子;而现在怎么就像是在攀登“屎山”呢?

  • 文档什么时候能写的既简明又详细;而现在怎么就简明的看不懂,详细的很多余呢?

  • 工具什么时候能更好用更强大一点;而现在怎么就动不动掉链子,没有想要的功能常年等排期呢?

  • “我”什么时候能从架构工作中找到成就感,而不是搞一搞就想着跑路呢?

责任大

大量问题的最终归因都是代码问题:设计不合理、使用不规范、逻辑太晦涩、编码“坑”太多。

没有一个单一的团队能承担这些问题的责任,我们收到过很多“吐槽”:

  • 这尼玛谁写的,简直不堪入目,看小爷我推倒重来展现一把真正的实力

  • XX 在这里埋了颗雷,但 XX 已经不管了,事到如今,我也只能兜底搞一把

  • 这压根就不应该这么用,本来的设计又不是为了这个场景,乱搞怪我咯?

  • 卧槽,这特么是隐藏技能啊,编译时悄悄改了老子的代码,找瞎了都没找到在哪过环节渗透进来的

一方面,口嗨一时爽,我们“吐槽”历史代码得到了一时的舒缓;另一方面,也意味着责任也传递到了我们:处理得好,我们的产出可能还是一样会被当作糟粕,但如果处理不好,我们就断送了业务发展的前程。

事情难

架构面临的从来不是单一的业务问题,而是多个业务多人协作的交叉问题,负重前行是常态。

  • 业务历久弥新,历史包袱叠加新的场景,随便动动刀子就拔出萝卜带出泥。譬如:头条 2021 年 10 月的版本有 XXXX 组件,相比一年前已经翻倍;类个数 XXXXX;插件 XX 个;仓库数量 XX 个;ttmain 仓库权限 XXX 人。(XX 代表数量级,隐去了具体数字,_)

  • 技术栈层出不穷,一方面要保持成熟稳定,一方面要积极探索落地。架构的同学要熟悉多种技术栈,譬如:跨端技术在客户端业务中通常都是多种共存(H5/Hybrid/小程序/Lynx/Flutter),一个业务到底选用哪种技术栈进行承载,需要耗费多少成本?选定技术栈后存在什么局限,是否存在不可逾越的障碍?

疗效慢

我们经常说代码复杂度高,并把降复杂度作为架构方向的重点工作之一;但影响复杂度的因子众多,从外部来看,有主观感受、客观指标、行业对标三个角度;从内部来看,有工程组织、代码实现和技术栈三个角度。即便我们很好的优化了工程结构这个因子,短时间内也很难感受到复杂度有一个明显的下降。


我们常说治理,其实是设计一种机制,在这种机制下运转直到治愈。

就像老中医开方子,开的不是特效药,而是应对病症的方法,是不是有用的方子,终究还是需要通过实践和时间的检验。希望我们不要成为庸医,瞎抓几把药一炖,就吹嘘药到病除。

我们的判断

架构问题老生常谈

谁来复盘架构问题,都免不了炒一炒“冷饭”;谁来规划架构方向,都逃不出了“减负”、“重构”、“复用”、“规范”这些关键词。难点在于把冷饭炒热,把方向落实。


架构方向一直存在

架构并不只局限于一个产品的初始阶段,而是伴随着产品的整个生命周期。架构也不是一成不变的,它只适合于特定的场景,过去的架构不一定适合现在,当下的架构不一定能预测未来,架构是随着业务不断演进的,不会出现架构方向做到头了、没有事情可搞了的情况,架构永远生机勃勃。

  • 强制遵循规范: 通常会要求业务公共的组件逐渐下沉到基础组件层,但随着时间的推移,这个规范很容易被打破

  • 需要成熟的团队: 领域专家(对业务细节非常熟悉的角色)和开发团队需紧密协作,构建出核心领域模型是关键。但盲目尝试 DDD 往往容易低估领域驱动设计这套方法论的实践成本,譬如将简单问题复杂化、陷入过分强调技术模型的陷阱

迄今为止,用于商业应用程序的最流行的软件架构设计模式是大泥球(Big Ball of Mud, BBoM),BBoM 是“…一片随意构造、杂乱无章、凌乱、任意拼贴、毫无头绪的代码丛林。”

泥球模式将扼杀开发,即便重构令人担忧,但也被认为是理所应当。然而,如果还是缺乏对领域知识应有的关注和考量,新项目最终也会走向泥球。没有开发人员愿意处理大泥球,对于企业而言,陷入大泥球就会丧失快速实现商业价值的能力。

——《领域驱动设计模式、原理与实践》Scott Millett & Nick Tune

复杂系统熵增不断

只要业务继续发展,越来越复杂就是必然趋势,这贴合热力学的熵增定律。

可以从两个维度来看复杂度熵增的过程:理解成本变高和预测难度变大。


理解成本:规模和结构是影响理解成本的两个因素

  • 宏大的规模是不好理解的,譬如:在城市路网中容易迷路,但在乡村中就那么几条道

  • 复杂的结构是不好理解的,譬如:一个钟表要比一条内裤难以理解

当需求增多时,软件系统的规模也会增大,且这种增长趋势并非线性增长,会更加陡峭。倘若需求还产生了事先未曾预料到的变化,我们又没有足够的风险应对措施,在时间紧迫的情况下,难免会对设计做出妥协,头疼医头、脚疼医脚,在系统的各个地方打上补丁,从而欠下技术债(Technical Debt)。当技术债务越欠越多,累积到某个临界点时,就会由量变引起质变,整个软件系统的复杂度达到巅峰,步入衰亡的老年期,成为“可怕”的遗留系统。

正如饲养场的“奶牛规则”:奶牛逐渐衰老,最终无奶可挤;然而与此同时,饲养成本却在上升。

——《实现领域驱动设计 - 深入理解软件的复杂度》张逸

预测难度:当下的筹码不足以应对未来的变化

  • 业务变化不可预测,譬如:头条一开始只是一个单端的咨询流产品,5 年前谁也不会预先设计 Lite 版、抖音、懂车帝等,多端以及新的业务场景带来的变化是无法预测的。很多时候,我们只需要在当下做到“恰当”的架构设计,但需要尽可能保持“有序”,一旦脱离了“有序”,那必将走向混乱,变得愈加不可预测

  • 技术变化不可预测,譬如:作为一个 Java 开发人员,Lambda 表达式的简洁、函数式编程的快感、声明式/响应式 UI 的体验,都是“真香”的技术变化,而陈旧的 Java 版本以及配套的依赖都需要升级,一旦升级,伴随着的就是多版本共存、依赖地狱(传递升级)等令人胆颤的问题。很多时候,我们不需要也没办法做出未来技术的架构设计,但需要让架构保持“清晰”,这样我们能更快的拥抱技术的变化

既然注定是逆风局,那跑到最后就算赢。

过多的流程规范反倒会让大家觉得是自己是牵线木偶,牵线木偶注定会随风而逝。

我们应该更多“强调”一些原则,譬如:分而治之、控制规模、保持结构的清晰与一致,而不是要求大家一定要按照某一份指南进行架构设计,那既降低不了复杂度,又跟不上变化。“强调”并不直接解决问题,而是把重要的问题凸显出来,让大家在一定的原则下自己找到问题的解决办法。

我们的打法

我们的套路是:定义问题 → 确定架构 → 方案落地 → 结果复盘。越是前面的步骤,就越是重要和抽象,也越是困难,越能体现架构师的功力。所以,我们打法的第一步就是要认清问题所在。

认清问题

问题分类

架构的问题是盘根错节的,将所有问题放在一起,就有轻重缓急之分,就有类别之分。区分问题的类别,就能在一定的边界内,匹配上对应的人来解决问题。

工程架构:


业务架构:


基础能力:


标准化:


问题分级

挑战、问题、手段这些经常混为一谈,哪些是挑战?哪些是问题?那些是手段?其实这些都是一回事,就是矛盾,只是不同场景下,矛盾所在的层级不同,举一个例子:


我们判断当前的研发体验不能满足业务日渐延伸的需要,这是一个矛盾,既是当下的挑战,也是当下的一级问题。要处理好这个矛盾,我们得拆解它,于是就有了二级问题:我们的代码逻辑是否已经足够优化?研发流程是否已经足够便捷?文档工具是否已经足够完备?二级问题也是矛盾,解决好二级问题就是我们处理一级矛盾的手段。这样层层递进下去,我们就能把握住当前我们要重点优化和建设的一些基础能力:插件化、热更新、跨端能力。

在具体实践过程中,基础技术能力还需要继续拆解,譬如:热更新能力有很多(Java 层的 Robust/Qzone 超级补丁/Tinker 等,Native 层的 Sophix/ByteFix 等),不同热更方案各有优劣,适用场景也不尽相同。我们要结合现状做出判断,从众多方案汲取长处,要么做出更大的技术突破创新,要么整合已有的技术方案进行组合创新。

勤于思考问题背后的问题

亨利福特说,如果我问客户需要什么,他们会告诉我,他们需要一匹更快的马。从亨利福特的这句话,我们可以提炼出一个最直接的问题:客户需要一匹更快的马。立足这个问题本身去找解决方案,可能永远交不出满意的答卷:寻找更好的品种,更科学的训马方式。

思考问题背后的问题,为什么客户需要一匹更快的马?可能客户想要更快的日常交通方式,上升了一个层次后,我们立刻找到了更好的解决方案:造车。

我们不能只局限于问题本身,还需要看到问题背后的问题,然后才能更容易找到更多的解决方案。

认知金字塔

引用认知金字塔这个模型,谨以此共勉,让我们能从最原始数据中,提炼出解决问题的智慧。


DATA: 金字塔的最底层是数据。数据代表各种事件和现象。数据本身没有组织和结构,也没有意义。数据只能告诉你发生了什么,并不能让你理解为什么会发生。

INFORMATION: 数据的上一层是信息。信息是结构化的数据。信息是很有用的,可以用来做分析和解读。

KNOWLEDGE: 信息再往上一层是知识。知识能把信息组织起来,告诉我们事件之间的逻辑联系。有云导致下雨,因为下雨所以天气变得凉快,这都是知识。成语典故和思维套路都是知识。模型,则可以说是一种高级知识,能解释一些事情,还能做预测。

WISDOM: 认知金字塔的最上一层,是智慧。智慧是识别和选择相关知识的能力。你可能掌握很多模型,但是具体到这个问题到底该用哪个模型,敢不敢用这个模型,就是智慧。

这就是“DIKW 模型”。

循序渐进

架构的问题不能等,也不能急。一个大型应用软件,并非要求所有部分都是完美设计,针对一部分低复杂性的区域或者不太可能花精力投入的区域,满足可用的条件即可,不需要投入高质量的构建成本。

以治理头条复杂度为例:

  • 长期的架构目标:更广(多端复用)、更快(单端开发速度)、更好(问题清理和前置拦截)

  • 当下的突出问题:业务之间耦合太重、缺少标准规范、代码冗余晦涩


细节已打码,请读者不要在意。重点在于厘清问题之后,螺旋式上升,做到长期有方向,短期有反馈。

最后

一顿输出之后,千万不能忘却了人文关怀,毕竟谋事在人。架构狮得供起来,他们高瞻远瞩,运筹帷幄;但架构人,却是更需要被点亮的,他们可能常年在“铲屎”,他们期望得到认可,他们有的还没有对象…干着干着,架构的故事还在,但人却仿佛早已翻篇。

来源:字节跳动技术团队 blog.csdn.net/ByteDanceTech/article/details/123700599

收起阅读 »

新的图形框架可以带来什么? 揭秘OpenHarmony新图形框架

3月30日,OpenHarmony v3.1 Release版本正式发布了。此版本为大家带来了全新的图形框架,实现了UI框架显示、多窗口、流畅动画等基础能力,夯实了OpenHarmony系统能力基座。下面就带大家详细了解新图形框架。一、完整能力视图新图形框架的...
继续阅读 »

3月30日,OpenHarmony v3.1 Release版本正式发布了。此版本为大家带来了全新的图形框架,实现了UI框架显示、多窗口、流畅动画等基础能力,夯实了OpenHarmony系统能力基座。下面就带大家详细了解新图形框架。

一、完整能力视图

新图形框架的能力在持续构建中,图1展示了新图形框架当前及未来提供的完整能力视图。

图1 OpenHarmony图形完整能力视图

按照分层抽象和轻模块化的架构设计原则,新图形框架分为接口层、架构层和引擎层。各层级说明如下:

● 接口层:提供图形NDK(native development kit,原生开发包)能力,包括OpenGL ES、Native Drawing等绘制接口能力。

● 框架层:由Render Service、Animation、Effect、Drawing、显示与内存管理等核心模块组成。框架层各模块说明如下:

● 引擎层:包括2D图形库和3D图形引擎两个模块。2D图形库提供2D图形绘制底层API,支持图形绘制与文本绘制底层能力。3D图形引擎能力尚在构建中。

二、新图形框架的亮点

经过上一节介绍,我们对新图形框架的完整能力有了基本的了解。那么,新图形框架有什么亮点呢?

新图形框架在渲染、动画流畅性、接口方面重点发力:

(1)渲染方面

通常来讲,UI界面显示分为两个部分:一是描述的UI元素在应用内部显示,二是多个应用的界面在屏幕上同时显示。对此,新图形框架从功能上做了相应的设计:控件级渲染窗口级渲染。“控件级渲染”重点考虑如何跟UI框架前端进行对接,需要将ArkUI框架的控件描述转换成绘制指令,并提供对应的节点管理以及渲染能力。而“窗口级渲染”重点考虑如何将多个应用合成显示到同一个屏幕上。

(2)动画流畅性方面

我们深挖动画处理流程中的各个环节,对新图形框架进行了新的动画实现设计,提升动画的流畅性体验。

(3)接口方面

新图形框架在接口层提供了更丰富的接口能力。

下面为大家一一详细介绍新图形框架的亮点特性。

1. 控件级渲染

新图形框架实现了基于RenderService(简称RS)的控件级渲染功能,如图2所示。

图2 控件级渲染

控件级渲染功能具有以下特点:

● 支持GPU渲染,提升渲染性能。

● 动画逻辑从主线程中剥离,提供独立的步进驱动机制。

● 将渲染节点属性化,属性与内容分离。

2. 窗口级渲染

新图形框架实现了基于RenderService的窗口级渲染功能,如图3所示。

图3 窗口级渲染

窗口级渲染功能具有以下特点:

● 取代Weston合成框架,实现RS新合成框架。

● 支持硬件VSync/软件Vsync。

● 支持基于NativeWindow接入EGL/GLES的能力。

● 更灵活的合成方式,支持硬件在线合成/CPU合成/混合合成(GPU合成即将上线)。

● 支持多媒体图层在线overlay。

3. 更流畅的动画体验

动画流畅性是一项很基本、也很关键的特性,直接影响用户体验。为了提升动画的流畅性体验,我们深挖动画处理流程中的各个环节,对新图形框架进行了新的动画实现设计。

如图4所示,传统动画的实现流程如下:

(1) 应用创建动画,设置动画参数。

(2) 每帧回调,修改控件参数,重新测量、布局、绘制。

(3) 内容渲染。

图4 传统动画实现

经过深入分析,我们发现传统动画实现存在以下缺点:

(1)UI与动画一起执行,UI的业务阻塞会影响动画的执行,导致动画卡顿。

(2)每帧回调修改控件属性,会触发测量布局录制,导致耗时增加。

针对以上两点缺陷,我们对新图形框架进行了新的动画实现设计,如图5所示。

图5 新框架的动画实现

(1)动画与UI分离。

动画在渲染线程步进,与UI业务线程分离。

(2)动画仅测量、布局、绘制一次,降低动画负载。

通过计算最终界面属性值,对有改变的控件添加动画,动画过程中不测量、布局、绘制,提升性能。

4. 对外提供的接口

新图形框架提供了丰富的接口:

(1)SDK:支持WebGL 1.0、WebGL 2.0,满足JS开发者的3D开发的需求。

WebGL开发指导:

https://docs.openharmony.cn/pages/zh-cn/app/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3/%E5%BC%80%E5%8F%91/%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD%E5%BC%80%E5%8F%91/WebGL/WebGL%E5%BC%80%E5%8F%91%E6%8C%87%E5%AF%BC/#:~:text=%23-,%E7%9D%80%E8%89%B2%E5%99%A8%E7%BB%98%E5%88%B6%E5%BD%A9%E8%89%B2%E4%B8%89%E8%A7%92%E5%BD%A2,-%E6%AD%A4%E5%9C%BA%E6%99%AF%E4%B8%BA

(2)NDK:支持OpenGL ES3.X,可以通过XComponent提供的nativewindow创建EGL/OPENGL绘制环境,满足游戏引擎等开发者对3D绘图能力的需求。

图6 OpenGL ES使用示例

新图形框架还处于不断完善过程中,我们将基于新框架提供更多的能力,相信以后会给大家带来更多的惊喜,敬请期待~

收起阅读 »

Flutter实现掘金App点赞效果

前言 点赞这个动作不得不说在社交、短视频等App中实在是太常见了,当用户手指按下去的那一刻,给用户一个好的反馈效果也是非常重要的,这样用户点起赞来才会有一种强烈的我点了赞的效果,那么今天我们就用Flutter实现一个掘金App上的点赞效果。 首先我们看下掘金...
继续阅读 »

前言


点赞这个动作不得不说在社交、短视频等App中实在是太常见了,当用户手指按下去的那一刻,给用户一个好的反馈效果也是非常重要的,这样用户点起赞来才会有一种强烈的我点了赞的效果,那么今天我们就用Flutter实现一个掘金App上的点赞效果。



  • 首先我们看下掘金App的点赞组成部分,有一个小手,点赞数字、点赞气泡效果,还有一个震动反馈,接下来我们一步一步实现。


知识点:绘制、动画、震动反馈


1、绘制小手


这里我们使用Flutter的Icon图标中的点赞小手,Icons图标库为我们提供了很多App常见的小图标,如果使用苹果苹果风格的小图标可以使用cupertino_icons: ^1.0.2插件,图标并不是图片,本质上和emoji图标一样,可以添加到文本中使用,所以图标才可以设置不同的颜色属性,对比使用png格式图标可以节省不少的内存。
image.png
接下来我们就将这两个图标绘制出来,首先我们从上图可以看到真正的图标数据其实是IconData类,里面有一个codePoint属性可以获取到Unicode统一码,通过String.fromCharCode(int charCode)可以返回一个代码单元,在Text文本中支持显示。


class IconData{
/// The Unicode code point at which this icon is stored in the icon font.
/// 获取此图标的Unicode代码点
final int codePoint;
}

class String{
/// 如果[charCode]可以用一个UTF-16编码单元表示,则新的字符串包含一个代码单元
external factory String.fromCharCode(int charCode);
}

接下来我们就可以把图标以绘制文本的形式绘制出来了

关键代码:


  // 赞图标
final icon = Icons.thumb_up_alt_outlined;
// 通过TextPainter可以获取图标的尺寸
TextPainter textPainter = TextPainter(
text: TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: 30,
fontFamily: icon.fontFamily,// 字体形象家族,这个字段一定要设置,不然显示不出来
color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
Size size2 = textPainter.size; // 尺寸必须在布局后获取
//将图标偏移到画布中央
textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2));

通过上方代码我们就实现了将图标绘制到画板当中

image.png

接下来继续绘制点赞数量,

代码:


TextPainter textPainter2 = TextPainter(
text: TextSpan(
text: "点赞",// 点赞数量
style: TextStyle(
fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter2.layout(); // 进行布局
// 向右上进行偏移在小手上面
textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5));

然后图标就变成了这样样子,

image.png

我们看到,掘金App点赞的过程中,周围还有一些小气泡的效果,这里提供一个思路,将这些气泡的坐标点放到一个圆的外环上面,通过动画改变圆的半径达到小圆点由内向外发散,发散的同时改变小圆点的大小,从而达到气泡的效果,
关键代码:


var r = size.width / 2 - 15; // 半径
var d = 4; // 偏移量 气泡的移动距离

// 绘制小圆点 一共4个 掘金也是4个 角度可以自由发挥 这里根据掘金App的发散角度定义的
canvas.drawPoints(
ui.PointMode.points,
[
Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2),
(r + d * animation2.value) * sin(pi - pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2),
(r + d * animation2.value) * sin(pi + pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18),
(r + d * animation2.value) * sin(pi * 1.5 - pi / 18)),
Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5),
(r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)),
],

_paint
..strokeWidth = 5
..color = Colors.blue
..strokeCap = StrokeCap.round);

得到现在的图形,
发散前

image.png

发散后

image.png

接下来继续我们来添加交互效果,添加动画,如果有看上一篇吃豆人,相信这里就很so easy了,首先创建两个动画类,控制小手和气泡,再创建两个变量,是否点赞和点赞数量,代码:


late Animation<double> animation; // 赞
late Animation<double> animation2; // 小圆点
ValueNotifier<bool> isZan = ValueNotifier(false); // 记录点赞状态 默认没点赞
ValueNotifier<int> zanNum = ValueNotifier(0); // 记录点赞数量 默认0点赞

这里我们需要使用动画曲线CurvedAnimation这个类,这个类可以实现不同的0-1的运动曲线,根据掘金的点赞效果,比较符合这个曲线规则,快速放大,然后回归正常大小,这个类帮我们实现了很多好玩的运动曲线,有兴趣的小伙伴可以尝试下其他运动曲线。

小手运动曲线

f299df8cd653f6f24de8553bebf055d2.gif

气泡运动曲线:

2222.gif

有了运动曲线之后,接下来我们只需将属性赋值给小手手和小圆点就好了,

完整源码: 封装一下,对外暴露大小,就是一个点赞组件了。


class ZanDemo extends StatefulWidget {
const ZanDemo({Key? key}) : super(key: key);

@override
_ZanDemoState createState() => _ZanDemoState();
}

class _ZanDemoState extends State<ZanDemo> with TickerProviderStateMixin {
late Animation<double> animation; // 赞
late Animation<double> animation2; // 小圆点
ValueNotifier<bool> isZan = ValueNotifier(false); // 记录点赞状态 默认没点赞
ValueNotifier<int> zanNum = ValueNotifier(0); // 记录点赞数量 默认0点赞

late AnimationController _controller; // 控制器
late AnimationController _controller2; // 小圆点控制器
late CurvedAnimation cure; // 动画运行的速度轨迹 速度的变化
late CurvedAnimation cure2; // 动画运行的速度轨迹 速度的变化

int time = 0;// 防止快速点两次赞导致取消赞

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500)); //500ms
_controller2 = AnimationController(
vsync: this, duration: const Duration(milliseconds: 800)); //500ms

cure = CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack);
cure2 = CurvedAnimation(parent: _controller2, curve: Curves.easeOutQuint);
animation = Tween(begin: 0.0, end: 1.0).animate(cure);
animation2 = Tween(begin: 0.0, end: 1.0).animate(_controller2);
}

@override
Widget build(BuildContext context) {
return InkWell(
child: Center(
child: CustomPaint(
size: Size(50, 50),
painter: _ZanPainter(animation, animation2, isZan, zanNum,
Listenable.merge([animation, animation2, isZan, zanNum])),
),
),
onTap: () {
if (!isZan.value && !_isDoubleClick()) {
_controller.forward(from: 0);
// 延迟300ms弹窗气泡
Timer(Duration(milliseconds: 300), () {
isZan.value = true;
_controller2.forward(from: 0);
});
Vibrate.feedback(FeedbackType.success);
zanNum.value++;
} else if (isZan.value) {
Vibrate.feedback(FeedbackType.success);
isZan.value = false;
zanNum.value--;
}
},
);
}

bool _isDoubleClick() {
if (time == 0) {
time = DateTime.now().microsecondsSinceEpoch;
return false;
} else {
if (DateTime.now().microsecondsSinceEpoch - time < 800 * 1000) {
return true;
} else {
time = DateTime.now().microsecondsSinceEpoch;
return false;
}
}
}
}

class _ZanPainter extends CustomPainter {
Animation<double> animation;
Animation<double> animation2;
ValueNotifier<bool> isZan;
ValueNotifier<int> zanNum;
Listenable listenable;

_ZanPainter(
this.animation, this.animation2, this.isZan, this.zanNum, this.listenable)
: super(repaint: listenable);

Paint _paint = Paint()..color = Colors.blue;
List<Offset> points = [];

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.translate(size.width / 2, size.height / 2);
// 赞
final icon =
isZan.value ? Icons.thumb_up_alt_rounded : Icons.thumb_up_alt_outlined;
// 通过TextPainter可以获取图标的尺寸
TextPainter textPainter = TextPainter(
text: TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: animation.value < 0 ? 0 : animation.value * 30,
fontFamily: icon.fontFamily,
color: isZan.value ? Colors.blue : Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
Size size2 = textPainter.size; // 尺寸必须在布局后获取
//将图标偏移到画布中央
textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2));

var r = size.width / 2 - 15; // 半径
var d = 4; // 偏移量

canvas.drawPoints(
ui.PointMode.points,
[
Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2),
(r + d * animation2.value) * sin(pi - pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2),
(r + d * animation2.value) * sin(pi + pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18 * 1),
(r + d * animation2.value) * sin(pi * 1.5 - pi / 18 * 1)),
Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5),
(r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)),
],
_paint
..strokeWidth = animation2.value <= 0.5 ? (5 * animation2.value) / 0.5
: 5 * (1 - animation2.value) / 0.5
..color = Colors.blue
..strokeCap = StrokeCap.round);
TextPainter textPainter2 = TextPainter(
text: TextSpan(
text: zanNum.value == 0 ? "点赞" : zanNum.value.toString(),
style: TextStyle(
fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter2.layout(); // 进行布局
// 向右上进行偏移在小手上面
textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5));
}

@override
bool shouldRepaint(covariant _ZanPainter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

到这里发现是不是少了点什么,不错,还少了震动的效果,这里我们引入flutter_vibrate: ^1.3.0这个插件,这个插件是用来管理设备震动效果的,Andoroid端记得加入震动权限,

<uses-permission android:name="android.permission.VIBRATE"/>

使用方法也很简单,这个插件封装了一些常见的提示震动,比如操作成功、操作警告、操作失败等,其实就是震动时间的长短,这里我们就在点赞时候调用Vibrate.feedback(FeedbackType.success);有一个点击成功的震动就好了。



作者:老李code
链接:https://juejin.cn/post/7088960905429385253
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

kotlin协程简介

技术是永无止境的,需要不断地学习总结。 什么是协程? 协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一...
继续阅读 »

技术是永无止境的,需要不断地学习总结。


什么是协程?


协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。


1. GlobalScope 的使用(不推荐),绑定的为应用的整个生命周期,GlobalScope是生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。所以需要绑定生命周期。


添加依赖如下:


implementation"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"

implementation"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

kotlin 中 GlobalScope 类提供了几个创建协程的构造函数:


launch: 创建协程


async : 创建带返回值的协程,返回的是 Deferred 类


withContext:不创建新的协程,指定协程上运行代码块


runBlocking:不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会


image.png


2、lifecycleScope (推荐使用) lifecycleScope只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期


**lifecycleScope会绑定调用者的生命周期,因此通常情况下不需要手动去停止

**


添加依赖如下:


implementation'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'//lifecycleScope

implementation'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'//viewModelScope

image.png


1.在不使用回调的前提下完成来线程的切换,代码看上亲也是干净整洁很多。


2.因为线程没有上下文,不能控制线程执行完成后应该回到哪里,但是协程完全帮我们实现自动化,执行完毕自动回到上下文线程中,一般情况下是主线程,可以通过设置来决定要回到哪个线程中。


3.协程可以通过suspend关键字来标志耗时操作,通过编译器来帮助我们避免一些性能上的问题。


作者:张嘉美
链接:https://juejin.cn/post/7087175117976895502
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

穿越到2030年,我找到了窝边草华丽转身的方案

大家好我是外卖小哥,就在昨天晚上大家都在睡觉的时候,我一个人骑车我的小摩托驰骋在安静的夜色中,一不小心竟然穿越到了2030年。我很激动,感觉我的外卖生涯明天就要结束。首先想到的就是去看采飘号码,但是发现搜不到8年前的数据,我还要多送好多年外卖才能迎来命运的转折...
继续阅读 »

大家好我是外卖小哥,就在昨天晚上大家都在睡觉的时候,我一个人骑车我的小摩托驰骋在安静的夜色中,一不小心竟然穿越到了2030年。

我很激动,感觉我的外卖生涯明天就要结束。

首先想到的就是去看采飘号码,但是发现搜不到8年前的数据,我还要多送好多年外卖才能迎来命运的转折点,于是放弃了。

我很快冷静下来,我要以最短的时间最有效的方法把握这次通往财务自由的机会。

我想2030年全球最大的互联网公司不就是FaceChat吗?而2022年FaceChat还没诞生,我要找到它的创始人,并加入他们。

查阅资料发现,FaceChat前身竟是窝边草,我怎么也想不通那么烂的软件怎么在短短几年崛起为IM霸主,继续查阅,我发现了这份方案(下文“我”是FaceChat副总裁):

2022年夏天,我以产品经理的身份发起一款对标窝边草2.0的APP开发工作,并取名FaceChat,采用主流技术现代化审美,预计周期一个月,并预留10天完善。

可是我没有钱也没有人怎么办?走球长时薪招一群摸鱼佬肯定不行。我要高薪聘请三名技术人员,并带他们一起致富。

于是我发出面向996的招聘信息,月薪3万、业绩7万,次月中旬发上月工资。三个全栈按模块分工均参与rn/uniapp/taro跨端开发与服务端开发,我负责产品设计、系统集成和运营,当然也参与开发。

一个月后FaceChat诞生,也急需30万给他们发工资,于是我找到球长,告诉他我有一个FaceChat是比窝边草好一百倍的APP,窝边草有的它都有,当然bugs除外。如果球长愿意收购我的产品和团队只需要300w即可。如果不收购的话窝边草的颜值加内伤是竞争不过FaceChat的。

一个月后,窝边草焕然一新,并以FaceChat的新名字面世。我和几位开发人员也顺利成为窝边草高管,从此,窝边草正式步入快速发展阶段,从国内到国际,从23个用户到23亿用户不过短短3年时间,由于业务发展需要,这期间还收购了环信等一大批优秀企业。

好了各位,剧本都给你们找来了,都对号入座吧。产品经理是谁我就不透露了,三个开发你们可以积极去争取,还有通知下球长准备300w。

有人问我,HX被收购后,阿花呢?小爬呢?他们后来嫁给谁了?他们2030年分别是FaceChat亚太地区产品总监和市场总监,至于嫁给谁了等我送完外卖给你们讲下一个故事。

收起阅读 »

【Flutter】Dart语法之List & Map

【Flutter】学习笔记——Dart中的List & Map的使用 list列表,相当于 OC 中的 NSArray 数组,分为可变和不可变两种。 map键值对,相当于 OC 中的 NSDicti...
继续阅读 »
【Flutter】学习笔记——Dart中的List & Map的使用



  • list列表,相当于 OC 中的 NSArray 数组,分为可变不可变两种。

  • map键值对,相当于 OC 中的 NSDictionary 字典,也分为可变不可变两种。



1. list数组



list默认都是可变的,列表中可以添加不同数据类型的数据。



1.1 可变list

void main() { 
// 直接 list创建
List a = ["1", 2, "3.0", 4.0];
print(a);

// var 创建
var list = [1, 2, "zjp", 3.0];
print(list);
}

运行结果如下:


image.png


1.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(list[3]);
// 直接修改指定下标数据
list[3] = "reno";

插入数据:


list.insert(1, "hellow"); // list.insert(index, element)
print(list);

删除数据:


list.remove(1); // list.remove(element)
print(list);

清空所有数据:


list.clear();
print(list);

运行结果如下:


image.png


1.3 排序和截取

void main() {
List b = [3, 4, 5, 8, 6, 7];
// 排序
b.sort();
print(b);
// 截取
print(b.sublist(1, 3));
}

运行结果如下:


image.png


1.4 不可变list


不可变的 list 需要使用const修饰。



void main() {
List b = const [3, 4, 5, 8, 6, 7];
b[3] = 10; // 报错
}

不可变list不能修改其元素值,否则会报错


image.png


2. map键值对



map默认也是可变的。



2.1 可变map

void main() {
Map a = {"a": 1, "b": 2};
print(a);

var a1 = {"a1": 1, "a2": 2};
print(a1);
}

运行结果如下:


image.png


2.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(a["a"]);
// 直接修改指定下标数据
a["a"] = "aa";
print(a["a"]);

获取map长度


print(a.length);

获取map所有的key


print(a.keys);

获取map所有的value


print(a.values);

运行结果如下:


image.png


2.3 不可变map


不可变的 map 也是使用const修饰。



void main() {
Map a = const {"a": 1, "b": 2};
a["a"] = 10; // 报错
}

不可变map不能修改其元素值,否则也会报错


image.png


3. list转map


void main() {
List b = ["zjp", "reno"];
print(b.asMap());
}

运行结果如下:



作者:忻凯同学
链接:https://juejin.cn/post/7085642932761395237
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter popUntil 黑屏

在flutter 路由跳转中,我们想要回到特定的一个页面 比如:从 A -> B-> C ->D,我们向从 D页面 pop至 B 页面。我们可以使用 popUtil方法回到 B 页面。Navigator.popUnitil(cont...
继续阅读 »

在flutter 路由跳转中,我们想要回到特定的一个页面 比如:从 A -> B-> C ->D,我们向从 D页面 pop至 B 页面。我们可以使用 popUtil方法回到 B 页面。

Navigator.popUnitil(context, ModalRoute.withName('/B'))

或者使用

   Navigator.popUntil(ctx.context, (route){
if (route.settings.name == "/B"){
return true;
}else {
return false;
}
});

但是 运行结果是 : 黑屏。

我们对 route.setting 进行打印后,发现 route.setting == null只有最后 一个A页面的route.setting有值,其name == '/'

所以,我们在跳转至B页面的时候,需要给B页面的routeSetting进行赋值,

  Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>BPage(),
settings: RouteSettings(name: '/B'),
));

这样就可以回到B页面了


作者:Bel李玉
链接:https://juejin.cn/post/7051160759992057869
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

flutter倒计时控件

使用方式1 默认为倒计时 CountdownWidget( 5, ///倒计时的时间 onClick: () { /// 点击事件的回调 _skip2main(); }, onFinish: () { ///倒计...
继续阅读 »

使用方式1 默认为倒计时


CountdownWidget(
5, ///倒计时的时间
onClick: () { /// 点击事件的回调
_skip2main();
},
onFinish: () { ///倒计时完成的回调
_skip2main();
},
)

使用方式2修改圆角和文案



CountdownWidget(
total: 10,
content: "已发送",
textColor: Colors.blue,
borderRadius: 2,
onClick: () {
_skip2main();
},
onFinish: () {
_skip2main();
},
)

倒计时实现


import 'dart:async';

import 'package:bilibili_flutter/common/base/base_state.dart';
import 'package:bilibili_flutter/common/base/base_widget.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';

///用于splash界面中倒计时按钮

class CountdownWidget extends BiliWidget {
///构造中传入函数
final VoidCallback? onClick;

final VoidCallback? onFinish;
final int total;
final double borderRadius;
final String content;
final double? height;

final Color? focusColor;
final Color? hoverColor;
final Color? highlightColor;

CountdownWidget(
{this.total = 5,
Key? key,
this.height = 40,
this.onClick,
this.onFinish,
this.borderRadius = 20,
this.content = "倒计时",
this.focusColor,
this.hoverColor,
this.highlightColor})
: super(key: key);

@override
State<CountdownWidget> createState() => _CountdownWidgetState();
}

class _CountdownWidgetState extends BiliState<CountdownWidget> {
var _count = 0;

late Timer _timer;

///注册倒计时
@override
void initState() {
super.initState();
var duration = const Duration(seconds: 1);
_timer = Timer.periodic(duration, (timer) {
if (_count < widget.total) {
setState(() {
_count++;
});
} else {
widget.onFinish?.call();
_timer.cancel();
}
});
}

@override
Widget build(BuildContext context) {
return InkWell(
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
highlightColor: widget.highlightColor,
onTap: () {
widget.onClick?.call();
},
child: SizedBox(
height: widget.height,
child: Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(widget.borderRadius)),
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Text("${widget.content}${widget.total - _count}s"),
),
),
),
),
);
}
}

依赖的两个基类


import 'package:flutter/material.dart';

@immutable
abstract class BiliWidget extends StatefulWidget {
BiliWidget({
Key? key,
}) : super(key: key);

String param = "";

void setParam(String param) {
this.param = param;
}
}

import 'package:flutter/material.dart';

import 'base_state.dart';

abstract class BiliState<T extends BiliWidget> extends State<T> {}

项目源码


github.com/HaiYangCode…


作者:yanghai
链接:https://juejin.cn/post/7022318876968878111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter实现心碎的感觉

前言 继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。 效果图先上: 1、绘制一个心 首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径pat...
继续阅读 »

前言


继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。


效果图先上:


1、绘制一个心


首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径path进行绘制出来,效果:


image.png

绘制代码:


canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.close();
Path path2 = Path();
canvas.save();
canvas.drawPath(
path,
paint
..color = Colors.red
..style = PaintingStyle.stroke);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.close();
canvas.drawPath(
path2,
paint..color = Colors.black87);

2、绘制心的裂痕


我们看到心确实分成两半了,但是中间还缺少裂痕,接下来我们就绘制心碎的裂痕,也很简单,在两段路径path闭合前进行绘制线,效果:


image.png


绘制代码:


path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);

path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);

OK,我们已经看到心已经有了裂痕,如何心碎,只需将画布进行翻转一定角度即可,这里我们将画布翻转45°,看下效果:

左边:
image.png

右边:
image.png


3、加入动画


已经有心碎的感觉了,接下来加入动画元素让心碎的过程动起来。

思路: 我们可以想一下,心碎的过程是什么样子,心的颜色慢慢变灰,心然后慢慢裂开,下方的动画运动曲线看起来更符合心碎的过程,里面有不舍,不甘,但最后心还是慢慢的碎了。
xinsui.gif


我们把画笔进行填充将这个动画加入进来看下最终效果。

df5dbcbb-f36b-4f05-9613-0e94149d888f.gif
是不是心碎了一地。


知识点: 这里我们需要找到红色和灰色的RGB色值,通过Color.fromRGBO(r, g, b, opacity)方法赋值颜色的色值。然后通过动画值改变RGB的值即可。
这里我使用的色值是:

红色:Color.fromRGBO(255, 0, 0, 1)

灰色:Color.fromRGBO(169, 169, 169, 1)


最终代码:


class XinSui extends StatefulWidget {
const XinSui({Key? key}) : super(key: key);

@override
_XinSuiState createState() => _XinSuiState();
}

class _XinSuiState extends State<XinSui> with SingleTickerProviderStateMixin {
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 4000))
..repeat();
late CurvedAnimation cure =
CurvedAnimation(parent: _controller, curve: Curves.bounceInOut);

late Animation<double> animation =
Tween<double>(begin: 0.0, end: 1.0).animate(cure);

@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
size: Size(double.infinity, double.infinity),
painter: _XinSuiPainter(animation),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

class _XinSuiPainter extends CustomPainter {
Animation<double> animation;

_XinSuiPainter(this.animation) : super(repaint: animation);

@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);
path.close();
Path path2 = Path();
canvas.save();
canvas.rotate(-pi / 4 * animation.value);
canvas.drawPath(
path,
paint
..color = Colors.red
..color = Color.fromRGBO(
255 - (86 * animation.value).toInt(),
(animation.value * 169).toInt(),
(animation.value * 169).toInt(),
1)

..style = PaintingStyle.fill);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);
path2.close();
canvas.rotate(pi / 4 * animation.value);
canvas.drawPath(
path2,paint);
}
@override
bool shouldRepaint(covariant _XinSuiPainter oldDelegate) {
return oldDelegate.animation != animation;
}
}

小结


动画曲线Curves配合绘制可以实现很多好玩的东西,这个需要勤加练习方能掌握,仅将此心碎献给今天受伤的股民朋友们(ಥ﹏ಥ)


作者:老李code
来源:https://juejin.cn/post/7090457954415017991
收起阅读 »

指纹解锁分析

systemServer进程会在ZygoteInit中进行创建,而ZygoteInit是Zygote进程启动的。 在systemServer进程的run方法中会启动重要服务其中就包括指纹解锁相对应的服务。 指纹解锁需要和Hal层进行交互,并对上层framewr...
继续阅读 »

systemServer进程会在ZygoteInit中进行创建,而ZygoteInit是Zygote进程启动的。


在systemServer进程的run方法中会启动重要服务其中就包括指纹解锁相对应的服务。
指纹解锁需要和Hal层进行交互,并对上层framewrok提供接口以实现解锁功能


整体流程可以大致分为:


1.SystemServer中调用startOtherService方法根据设备支持的功能启动对应的服务
该例中如果设备支持指纹解锁就执行接下来的方法:
启动指纹解锁对应的Service,也就是FingerprintService这个类


startOtherService方法:
image.png


startService:
image.png


2.可以看到会反射创建这个类的构造方法并把它添加到services中,接着执行这个类的onStart方法


image.png


FingerprintService这个类的onStart方法
image.png
3.FingerprintService这个类的onStart方法中可以看到创建了一个 FingerprintServiceWrapper()这个类。


发布服务保存在SystemServer中,可以看到这个服务对应的接口是
IFingerprintService.Stub


image.png


image.png


可以看到是在用了个线程池在调用这个run方法,接下来去看看这个Runnable接口做了什么操作
image.png


getFingerprintDaemon函数首先调用getService函数不断尝试链接HAL层的进程(IBiometricsFingerprint这个服务是在HAL层初始化的之后讲解),链接成功之后调用setNotify设置回调函数,最后加载用户相关数据。至此,Framework层已经启动完成。


image.png


BiometricsFingerprint


上面讲到FrameWork中会获取BiometricsFingerprint这个服务,这个服务是在哪个地方初始化的呢?


首先需要讲下Android.bp文件:



Android.bp的出现就是为了替换Android.mk文件,随着Android越来越庞大,module越来越多,编译时间也越来越长,而使用ninja在编译的并发处理上较make有很大的提升。Ninja的配置文件就是Android.bp,Android系统使用Blueprint和Soong工具来解析Android.bp转换生成ninja文件



详细内容及自定义文件可参考这篇博客 Android.bp文件详解


这里首先看下一些配置信息
这是一些注释信息:



cc_library_shared :编译成动态库,类似于Android.mk中的BUILD_SHARED_LIBRARY
cc_binary:编译成可执行文件,类似于Android.mk中的BUILD_EXECUTABLE
name :编译出的模块的名称,类似于Android.mk中的LOCAL_MODULE
srcs:源文件,类似于Android.mk中的LOCAL_SRC_FILES
local_include_dirs:指定路径查找头文件,类似于Android.mk中的LOCAL_C_INCLUDES
shared_libs:编译所依赖的动态库,类似于Android.mk中的LOCAL_SHARED_LIBRARIES
static_libs:编译所依赖的静态库,类似于Android.mk中的LOCAL_STATIC_LIBRARIES
cflags:编译flag,类似于Android.mk中的LOCAL_CFLAGS



image.png


Service.cpp是HAL层启动的入口文件。


1.首先通过BiometricsFingerprint::getInstance()实例化一个bio服务,不同厂商的指纹识别算法和逻辑也都在这个bibo服务中体现出来。这个方法里面会进行初始化HAL层关于指纹的一些初始化动作最后讲


2.接着设置用于RPC通信的线程数


3.接着把自己添加到线程池中,用于之后framework获取进行返回bibo服务


image.png


BiometricsFingerprint::getInstance()


该函数单利创建出来一个BiometricsFingerprint对象,接着看他的构造方法


image.png


BiometricsFingerprint构造方法,可以看到调用了openHal方法。
image.png
1.openHal方法第一步首先打开指纹HW模块,也就是获取厂商指纹模组的so



hw_get_module(FINGERPRINT_HARDWARE_MODULE_ID, &hw_mdl)



image.png


2.接着调用open方法


image.png


image.png


3.这个open方法主要是将厂商指纹模组模块的算法识别逻辑结果和HAL层进行绑定,设置回调通知。


image.png


大致流程:


首先将framework中的指纹解锁Service启动接着去获取HAL层的指纹解锁服务Service。
framework层的Service主要用于和HAL层进行通信(获取HAL层的Service)
HAL层的Service收到后会使用厂商自定义的指纹模组so模块对应的逻辑去判断是否是本人
最后结果在给到framework层响应

作者:北洋
来源:https://juejin.cn/post/7090362782767546398
收起阅读 »

一种emoji表情判断方法

Emoji表情输入 常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则...
继续阅读 »

image.png


Emoji表情输入


常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则插入时会报错。


在某些场景下,我们并不希望文本中出现emoji表情等非常用字符,那么如何过滤呢?

对于字符过滤,一般我们第一个想到的大多是正则表达式。然而,实际使用中,由于emoji表情的不断增加或正则表达式本身的缺陷,往往达不到过滤的效果。


image.png


发现问题



欢迎来到王者荣耀😊😊



字符数量10,字符串长度12


一次开发中,使用了el-input的字符数统计属性show-word-limit,发现输入emoji表情统计到的字符数量和实际看到的字符数量不一致。

然后,尝试通过字符串分割成数组,再比较长度,发现str.split('')得到的数组长度和统计到的字符数是一样的,但是和肉眼看到的字符数量还是不一致。


var str = '欢迎来到王者荣耀😊😊'
var arr = str.split('')
console.log(str.length) // 12
console.log(arr.length) // 12

解决问题


那么,是否可以通过字符串的字符数量和字符串长度来判断是否输入了emoji表情呢?

要验证这个问题,关键的是获取到字符串中字符的数量。


那么如何获取字符串中字符的数量呢,通过研究(百度)发现,分割utf8字符串的正确方法是使用 Array.from(str) 而不是str.split('')。


Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。


var str = '欢迎来到王者荣耀😊😊'
var arr2 = Array.from(str)
console.log(str.length) // 12
console.log(arr2.length) // 10

一个大胆的猜想


emoji表情判断,可以通过字符串长度和字符数量的比较判断是否存在emoji表情,当长度和数量不一致的时候,有emoji表情。


isEmojiStr(str) { 
if (typeof (str) === 'string') {
const arr = Array.from(str);
if (str.length !== arr.length) {
return true;
}
}
return false;
}

image.png


参考


# Emoji Unicode Tables

# 深入理解Emoji(一) —— 字符集,字符集编码

# 深入理解Emoji(二) —— 字节序和BOM

# 深入理解Emoji(三) —— Emoji详解


作者:前端老兵
来源:https://juejin.cn/post/7090182766158938120
收起阅读 »

Flutter实现一个牛顿摆

前言牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。 - 知识点:绘制、动画曲线、多动画状态更新 效果图: 1、绘制静态效果 首先我们需要把线和小圆球绘制出来,...
继续阅读 »

前言

  • 牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。

- 知识点:绘制、动画曲线、多动画状态更新


效果图:


638bdf30-7b2a-4c3e-ad14-94da128b68f1.gif


1、绘制静态效果


首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:

 
image.png

关键代码:


// 小圆球半径
double radius = 6;

/// 小球圆心和直线终点一致
//左边小球圆心
Offset offset = Offset(20, 60);
//右边小球圆心
Offset offset2 = Offset(20 * 6 * 8, 60);

Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制小圆球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);

2、加入动画


思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。


两个关键点


小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。


运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。

下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。

 
ndb.gif

完整源码:


class OvalLoading extends StatefulWidget {
const OvalLoading({Key? key}) : super(key: key);

@override
_OvalLoadingState createState() => _OvalLoadingState();
}

class _OvalLoadingState extends State
with TickerProviderStateMixin
{
// 左边小球
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
_controller2.forward();
}
})
..forward();
// 右边小球
late AnimationController _controller2 =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller2.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
// 反向执行完毕左边小球执行
_controller.forward();
}
});
late var cure =
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
late var cure2 =
CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic);

late Animation animation = Tween(begin: 0.0, end: 1.0).animate(cure);

late Animation animation2 =
Tween(begin: 0.0, end: 1.0).animate(cure2);

@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsetsDirectional.only(top: 300, start: 150),
child: CustomPaint(
size: Size(100, 100),
painter: _OvalLoadingPainter(
animation, animation2, Listenable.merge([animation, animation2])),
),
);
}

@override
void dispose() {
_controller.dispose();
_controller2.dispose();
super.dispose();
}
}

class _OvalLoadingPainter extends CustomPainter {
double radius = 6;
final Animation animation;
final Animation animation2;
final Listenable listenable;

late Offset offset; // 左边小球圆心
late Offset offset2; // 右边小球圆心

final double lineLength = 60; // 线长

_OvalLoadingPainter(this.animation, this.animation2, this.listenable)
: super(repaint: listenable) {
offset = Offset(20, lineLength);
offset2 = Offset(20 * radius * 8, lineLength);
}

// 摆动角度
double angle = pi / 180 * 30; // 30°

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

// 左边小球 默认坐标 下方是90度 需要+pi/2
var dx = 20 + 60 * cos(pi / 2 + angle * animation.value);
var dy = 60 * sin(pi / 2 + angle * animation.value);
// 右边小球
var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value);
var dy2 = 60 * sin(pi / 2 + angle * animation2.value);

offset = Offset(dx, dy);
offset2 = Offset(dx2, dy2);

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(
Offset(20 + radius * 2, 60),
radius,
paint);

canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);
}
@override
bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

去掉线的效果:

b6a23e8a-9c4a-4aa8-9518-46a53b756a88.gif


总结


本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点,本篇只展示了实现的原理,希望对大家有一些帮助~


作者:老李code
来源:https://juejin.cn/post/7090123854135164935 收起阅读 »

Android自动生成代码,可视化脚手架之环境搭建

系列文章Github开源地址(源码及各项资料不间断进行更新):github.com/AbnerMing88…Hello,各位老铁,系列文章上一篇,简单大概熟悉了一下基本的功能,当然了这只是其中的一部分,随着需求的增加,各种方便我们日常开发的功能都会研发出来,那...
继续阅读 »

系列文章Github开源地址(源码及各项资料不间断进行更新):

github.com/AbnerMing88…

Hello,各位老铁,系列文章上一篇,简单大概熟悉了一下基本的功能,当然了这只是其中的一部分,随着需求的增加,各种方便我们日常开发的功能都会研发出来,那么对于这样的一个可视化工具,我们该如何开发出来呢?又需要掌握什么技术呢?环境如何搭建呢?这篇,咱们就简单的聊一聊。

可能很多老铁有疑问,为什么不直接以插件的形式在Android Studio中使用呢,这样直接IDE中就可以操作了,也不用再打开其他工具了,岂不是更方便,哎!小老弟,一开始我就是整的插件,还写了好几个功能,但有一个致命的问题是,视图的绘制,贼麻烦,大家感兴趣的可以试试,多个控件的摆放,还有,拖拽View的实现,亲自操刀试试就知道了,正因为各个视图的绘制比较麻烦,最终才选择了可视化工具的开发。

目前可视化工具采用的是Electron进行开发的,Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台的桌面应用程序,它基于 Node.js 和 Chromium,被 Atom 编辑器和许多其他应用程序使用,也就是说使用Electron,您必须有一定的web开发经验,如果没有也没关系,后续您可以直接在我的模板中进行对应的修改即可,当然了,为了能够自己灵活的可视化,建议还是掌握一些Web的经验,编程语言之间的语法,基本互通,学起来也比较容易。

对于Electron,网上流传着一些风言风语,说微软要放弃Electron了,这里简单辟谣一下,微软自始至终,就没有放弃Electron,也不会放弃Electron,只是旗下的Teams产品打算把Electron框架换成WebView2而已,况且微软内部有很多软件都是基于Electron开发的,比如VSCode和GitHubDesktop,不仅仅是只有Teams这么一个产品在用它,非但微软内部,包括Facebook、MongoDB、twitch、Slack、迅雷、字节跳动、阿里、拼多多、京东等大企业都在用这个框架,这么一个好东西,微软怎么会放弃它呢?所以,各位老铁,不要在听信网上的谣言了,桌面开发工具Electron,兼容 Mac、Windows 和 Linux,可以构建出三个平台的应用程序,学起来,指定没错!

Electron官网:http://www.electronjs.org/

关于Electron的教程,网上一搜一大堆,咱们言简意赅,直奔主题,老铁们,跟好脚步,我们发车!

1、安装 Node.js

别问为什么,问就是,Electron开发依赖Node.js,因为Node.js中允许使用 JavaScript 开发服务端以及命令行程序,我们可以去官网nodejs.org下载最新版本的安装程序,也可以下载我给大家准备好的安装包,都在上面github开源地址中。

下载后,怎么安装,就不用我来教了吧,一路一路下一步,中间会有个选择安装路径,这个尽量自己选一个,不要用默认的,安装完成后会自动配置环境变量,如果没有配置,那就需要自己去环境变量下配置一下:

自己配置的话,首先找到你的安装路径,复制一下:


然后配置到环境变量里,以windows为例子


一切搞定之后,打开命令窗口,输入node -v,检验下是否安装成功,回显当前版本,证明安装成功!


2、安装 Electron

打开命令窗口,输入下面命令:

npm install -g electron

下载慢的话,可以先执行下面的命令,electron安装包指向淘宝的镜像

npm config set electron_mirror "https://npm.taobao.org/mirrors/electron/"

等待安装完成之后,在命令行输入electron -v能够显示版本号代表安装成功。


如果想删除 Electron,可以使用下面的命令。

npm uninstall electron

如果想升级 Electron,则可以使用这个命令。

npm update electron -g

大家也可以指定版本进行安装,有一些版本升高之后,会有一些兼容性问题,目前,我的版本是15.0.0,大家可以和我保持一致。

cnpm install electron@^15.0.0 -g

以上两步执行完毕之后,环境就搭建完毕,剩下的就是愉快的敲代码时刻。

搞一个Hello,World!

随便找一个空的文件夹,进入到目录下,执行下面的命令,或者在命令窗口找到你的目录,都行

npm init 
npm install --save-dev electron 或者安装制定版本 npm install --save-dev electron@^15.0.0

如下图,我新建的一个code目录:


进入到当前目录命令下,执行上面的命令:


当执行npm init时,会按照步骤,让输入很多东西,如果你不想一步一步输入,每次直接回车即可,反正也是可以修改的。

如果想进行一步一步输入,具体流程如下,中间不想输入,可以回车略过:

package name 包名,也就是工程名,默认是括号中的内容 
version:版本号,默认是括号中的内容
description:描述信息
entry point:入口文件名,默认是括号中的内容
test command:测试命令
git repository:git仓库地址
keywords: 密码
author: 作者名字
license: (ISC)许可证

我自己执行的程序如下:


执行完成之后,就会在你刚才选中的目录下,生成一个,package.json文件:


我们打开看一下,其实就是我们一步一步输入的内容:


接着我们在去执行第二个命令,我是选择指定版本进行安装的:


命令执行完毕后,会生成如下图所示:


node_modules,是安装node后,用来存放下载安装的包文件夹。

执行完命令之后,我们就可以书写主入口了,之前执行npm init命令时,有个主入口的输入,还记得吗,就是下面这个:


新建index.js文件


内容如下:

const { app, BrowserWindow } = require('electron')

function createWindow () {  
 // 创建浏览器窗口
 let win = new BrowserWindow({
   width: 800,
   height: 600,
   webPreferences: {
     nodeIntegration: true
  }
})

 // 加载index.html文件
 win.loadFile('index.html')
}

// 应用程序准备就绪后打开一个窗口
app.whenReady().then(createWindow)

紧接着新建一个index.js中对应的index.html文件:


内容如下:

<!DOCTYPE html>
<html>
 <head>
   <meta charset="UTF-8">
   <title>Android可视化工具</title>
 </head>
 <body>
   <h1>Hello,World!</h1>
 </body>
</html>

最后修改package.json,添加Electron运行时


回到目录下,打开命令窗口,执行npm start命令,如下图


执行命令之后,随之就会,弹出来一个可视化窗口,如下图:


ok,一个简单的Demo就完成了,是不是贼简单。

老铁们,第二章的内容,虽然有点多,但基本上都是些操作的步骤,环境的安装以及简单的项目运行,还是希望大家从头到尾的执行一遍,都是一些流程化的操作,并不是很难,下一章,我们讲讲述可视化工具的一些配置项,敬请期待!

作者:二流小码农
来源:https://juejin.cn/post/7090322746260848671

收起阅读 »

春节钱包大流量奖励系统入账及展示的设计与实现

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。1. 背景&挑...
继续阅读 »

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。

1. 背景&挑战&目标

1.1 业务背景

(1)支持八端:2022 年字节系产品春节活动需要支持八端 APP 产品(包含抖音/抖音火山/抖音极速版/西瓜/头条/头条极速版/番茄小说/番茄畅听)的奖励互通。用户在上述任意一端都可以参与活动,得到的奖励在其他端都可以提现与使用。

(2)玩法多变:主要有集卡、朋友页红包雨、红包雨、集卡开奖与烟火大会等。

(3)多种奖励:奖励类型包含现金红包、补贴视频红包、商业化广告券、电商券、支付券、消费金融券、保险券、信用卡优惠券、喜茶券、电影票券、dou+券、抖音文创券、头像挂件等。

1.2 核心挑战

(1)设计&实现八端奖励入账与展示互通的大流量的方案,最高预估有 360w QPS 发奖。

(2)多种发奖励的场景,玩法多变;奖励类型多,共 10 余种奖励。对接多个下游系统。

(3)从奖励系统稳定性、用户体验、资金安全与运营基础能力全方位保障,确保活动顺利进行 。

1.3 最终目标

(1)奖励入账:设计与实现八端奖励互通的奖励入账系统,对接多个奖励下游系统,抹平不同奖励下游的差异,对上游屏蔽底层奖励入账细节,设计统一的接口协议提供给业务上游。提供统一的错误处理机制,入账幂等能力和奖励预算控制。

(2)奖励展示/使用:设计与实现活动钱包页,支持在八端展示用户所获得的奖励,支持用户查看、提现(现金),使用卡券/挂件等能力。

(3)基础能力:

  • 【基础 sdk】提供查询红包余额、累计收入、用户在春节活动是否获得过奖励等基础 sdk,供业务方查询使用。

  • 【预算控制】与上游奖励发放端算法策略打通,实现大流量卡券入账的库存控制能力,防止超发。

  • 【提现控制】在除夕当天多轮奖励发放后,提供用户提现的灰度放量能力、提现时尚未入账的处理能力。

  • 【运营干预】活动页面灵活的运营配置能力,支持快速发布公告,及时触达用户。为应对黑天鹅事件,支持批量卡券和红包补发能力。

(4)稳定性保障:在大流量的入账场景下,保证钱包核心路径稳定性与完善,通过常用稳定性保障手段如资源扩容、限流、熔断、降级、兜底、资源隔离等方式保证用户奖励方向的核心体验。

(5)资金安全:在大流量的入账场景下,通过幂等、对账、监控与报警等机制,保证资金安全,保证用户资产应发尽发,不少发。

(6)活动隔离:实现内部测试活动、灰度放量活动和正式春节活动三个阶段的奖励入账与展示的数据隔离,不互相影响。

2. 产品需求介绍

用户可以在任意一端参与字节的春节活动获取奖励,以抖音红包雨现金红包入账场景为例,具体的业务流程如下:

登录抖音 → 参与活动 → 活动钱包页 → 点击提现按钮 → 进入提现页面 → 进行提现 → 提现结果页,另外从钱包页也可以进入活动钱包页。

img

奖励发放核心场景:

  1. 集卡:集卡抽卡时发放各类卡券,集卡锦鲤还会发放大额现金红包,集卡开奖时发放瓜分奖金和优惠券;

  2. 红包雨:发红包、卡券以及视频补贴红包,其中红包和卡券最高分别 180w QPS;

  3. 烟火大会:发红包、卡券以及头像挂件。

dd6af98d51f5db710872248ae51a30e4.png

3. 钱包资产中台设计与实现

在 2022 年春节活动中,UG 主要负责活动的玩法实现,包含集卡、红包雨以及烟火大会等具体的活动相关业务逻辑和稳定性保障。而钱包方向定位是大流量场景下实现奖励入账、奖励展示、奖励使用与资金安全保障的相关任务。其中资产中台负责奖励发放与奖励展示部分。

3.1 春节资产资产中台总体架构图如下:

768d79e164975d1794d875abaf74b627.png

钱包资产中台核心系统划分如下:

  1. 资产订单层:收敛八端奖励入账链路,提供统一的接口协议对接上游活动业务方如 UG、激励中台、视频红包等的奖励发放功能,同时对上游屏蔽对接奖励业务下游的逻辑处理,支持预算控制、补偿、订单号幂等。

  2. 活动钱包 api 层:收敛八端奖励展示链路,同时支持大流量场景

3.2 资产订单中心设计

核心发放模型:

37c250c4a4b5e91b0dc4ce1e236ae00f.png

说明:

  1. 活动 ID 唯一区分一个活动,本次春节分配了一个单独的母活动 ID

  2. 场景 ID 和具体的一种奖励类型一一对应,定义该场景下发奖励的唯一配置,场景 ID 可以配置的能力有:发奖励账单文案;是否需要补偿;限流配置;是否进行库存控制;是否要进行对账。提供可插拔的能力,供业务可选接入。

实现效果:

  1. 实现不同活动之间的配置隔离

  2. 每个活动的配置呈树状结构,实现一个活动发多种奖励,一种奖励发多种奖励 ID

  3. 一种奖励 ID 可以有多种分发场景,支持不同场景的个性化配置

订单号设计:

资产订单层支持订单号维度的发奖幂等,订单号设计逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发,每个场景的奖励用户最多只领一次。

  1. 核心难点问题解决

4.1 难点一:支持八端奖励数据互通

前文背景已经介绍过了,参与 2022 年春节活动一共有八个产品端,其中抖音系和头条系 APP 是不同的账号体系,所以不能通过用户 ID 打通奖励互通。具体解决方案是字节账号中台打通了八端的账号体系给每个用户生成唯一的 actID(手机号优先级最高,如果不同端登录的手机号一样,在不同端的 actID 是一致的)。钱包侧基于字节账号中台提供的唯一 actID 基础上,设计实现了支持八端奖励入账、查看与使用的通用方案,即每个用户的奖励数据是绑定在 actID 上的,入账和查询是通过 actID 维度实现的,即可实现八端奖励互通。

示意图如下:

d5e9bb782bba0c3e5fd950a64664d2c6.png

4.2 难点二:高场景下的奖励入账实现

每年的春节活动,发现金红包都是最关键的一环,今年也不例外。有几个原因如下:

  1. 预估发现金红包最大流量有 180w TPS。

  2. 现金红包本身价值高,需要保证资金安全。

  3. 用户对现金的敏感度很高,在保证用户体验与功能完整性同时也要考虑成本问题。

终上所述,发现金红包面临比较大的技术挑战。

发红包其实是一种交易行为,资金流走向是从公司成本出然后进入个人账户。

(1)从技术方案上是要支持订单号维度的幂等,同一订单号多次请求只入账一次。订单号生成逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发。

(2)支持高并发,有以下 2 个传统方案:

具体方案类型实现思路优点缺点
同步入账申请和预估流量相同的计算和存储资源1.开发简单; 2.不容易出错;浪费存储成本。 拿账户数据库举例,经实际压测结果:支持 30w 发红包需要 152 个数据库实例,如果支持 180w 发红包,至少需要 1152 个数据库实例,还没有算上 tce 和 redis 等其他计算和存储资源。
异步入账申请部分计算和存储资源资源,实际入账能力与预估有一定差值1.开发简单; 2.不容易出错; 3.不浪费资源;用户体验受到很大影响。 入账延迟较大,以今年活动举例会有十几分钟延迟。用户参与玩法得到奖励后在活动钱包页看不到奖励,也无法进行提现,会有大量客诉,影响抖音活动的效果。

以上两种传统意义上的技术方案都有明显的缺点,那么进行思考,既能相对节约资源又能保证用户体验的方案是什么?
最终采用的是红包雨 token 方案,具体方案是使用异步入账加较少量分布式存储和较复杂方案来实现,下面具体介绍一下。

4.2.1 红包雨 token 方案:

本次春节活动在红包雨/集卡开奖/烟火大会的活动下有超大流量发红包的场景,前文介绍过发奖 QPS 最高预估有 180w QPS,按照现有的账户入账设计,需要大量存储和计算资源支撑,根据预估发放红包数/产品最大可接受发放时间,计算得到钱包实际入账最低要支持的 TPS 为 30w,所以实际发放中有压单的过程。

设计目标:

在活动预估给用户发放(180w)与实际入账(30w)有很大 gap 的情况下,保证用户的核心体验。用户在前端页面查看与使用过当中不能感知压单的过程,即查看与使用体验不能受到影响,相关展示的数据包含余额,累计收入与红包流水,使用包含提现等。

具体设计方案:

我们在大流量场景下每次给用户发红包会生成一个加密 token(使用非对称加密,包含发红包的元信息:红包金额,actID,与发放时间等),分别存储在客户端和服务端(容灾互备),每个用户有个 token 列表。每次发红包的时候会在 Redis 里记录该 token 的入账状态,然后用户在活动钱包页看到的现金红包流水、余额等数据,是合并已入账红包列表+token 列表-已入账/入账中 token 列表的结果。同时为保证用户提现体验不感知红包压单流程,在进入提现页或者点击提现时将未入账的 token 列表进行强制入账,保证用户提现时账户的余额为应入账总金额,不 block 用户提现流程。

示意图如下:

fcf21c86fa2ef5ea8c132ca58883cf91.png

token 数据结构:

token 使用的是 pb 格式,经单测验证存储消耗实际比使用 json 少了一倍,节约请求网络的带宽和存储成本;同时序列化与反序列化消耗 cpu 也有降低。

// 红包雨token结构
type RedPacketToken struct {
  AppID     int64 `protobuf: varint,1,opt json: AppID,omitempty ` // 端ID
  ActID     int64 `protobuf: varint,2,opt json: UserID,omitempty ` // ActID
  ActivityID string `protobuf: bytes,3,opt json: ActivityID,omitempty ` // 活动ID
  SceneID   string `protobuf: bytes,4,opt json: SceneID,omitempty ` // 场景ID
  Amount     int64 `protobuf: varint,5,opt json: Amount,omitempty ` // 红包金额
  OutTradeNo string `protobuf: bytes,6,opt json: OutTradeNo,omitempty ` // 订单号
  OpenTime   int64 `protobuf: varint,7,opt json: OpenTime,omitempty ` // 开奖时间
  RainID     int32 `protobuf: varint,8,opt,name=rainID json: rainID,omitempty ` // 红包雨ID
  Status     int64 `protobuf: varint,9,opt,name=status json: status,omitempty ` //入账状态
}

token 状态机流转:

在调用账户真正入账之前会置为处理中(2)状态,调用账户成功为成功(8)状态,发红包没有失败的情况,后续都是可以重试成功的。

token 安全性保障:

采用非对称加密算法来保障存储在的客户端尽可能不被破解,其中加密算法为秘密仓库,限制其他人访问。同时考虑极端情况下如果 token 加密算法被黑产破译,可监控报警发现,可降级。

4.2.2 活动钱包页展示红包流水

需求背景:

活动钱包页展示的红包流水是现金红包入账流水、提现流水、c2c 红包流水三个数据源的合并,按照创建时间倒叙排列,需要支持分页,可降级,保证用户体验不感知发现金红包压单过程。

c5ff3a6471d9230554d6b50395c7c946.png

4.3 难点三:发奖励链路依赖多的稳定性保障

发红包流程降级示意图如下:

8677db831f49ccf481df9fc43f6fcd6e.png

根据历史经验,实现的功能越复杂,依赖会变多,对应的稳定性风险就越高,那么如何保证高依赖的系统稳定性呢?

解决方案:

现金红包入账最基础要保障的功能是将用户得到的红包进行入账,同时支持幂等与预算控制(避免超发),红包账户的幂等设计强依赖数据库保持事务一致性。但是如果极端情况发生,中间的链路可能会出现问题,如果是弱依赖需要支持降级掉,不影响发放主流程。钱包方向发红包最短路径为依赖服务实例计算资源和 MySQL 存储资源实现现金红包入账。

发红包强弱依赖梳理图示:

psm依赖服务是否强依赖降级方案降级后影响
资产中台tcc降级读本地缓存
bytkekv主动降级开关,跳过 bytekv,依赖下游做幂等
资金交易层分布式锁 Redis被动降级,调用失败,直接跳过基本无
token Redis主动降级开关,不调用 Redis用户能感知到入账有延迟,会有很多客诉
MySQL主有问题,联系 dba 切主故障期间发红包不可用

4.4 难点四:大流量发卡券预算控制

需求背景:

春节活动除夕晚上 7 点半会开始烟火大会,是大流量集中发券的一个场景,钱包侧与算法策略配合进行卡券发放库存控制,防止超发。

具体实现:

(1)钱包资产中台维护每个卡券模板 ID 的消耗发放量。

(2)每次卡券发放前算法策略会读取钱包 sdk 获取该卡券模板 ID 的消耗量以及总库存数。同时会设置一个阈值,如果卡券剩余量小于 10%后不发这个券(使用兜底券或者祝福语进行兜底)。

(3) 同时钱包资产中台方向在发券流程累计每个券模板 ID 的消耗量(使用 Redis incr 命令原子累加消耗量),然后与总活动库存进行比对,如果消耗量大于总库存数则拒绝掉,防止超发,也是一个兜底流程。

具体流程图:

9074ea1e5bc8d3a06b7958c380876833.png

优化方向:

(1)大流量下使用 Redis 计数,单 key 会存在热 key 问题,需要拆分 key 来解决。

(2)大流量场景下操作 Redis 会存在超时问题,返回上游处理中,上游继续重试发券会多消耗库存少发,本次春节活动实际活动库存在预估库存基础上加了 5%的量级来缓解超时带来的少发问题。

4.5 难点五:高 QPS 场景下的热 key 的读取和写入稳定性保障

需求背景:

在除夕晚上 7 点半开始会开始烟火大会活动,展示所有红包雨与烟火大会红包的实时累计发放总额,最大流量预估读取有 180wQPS,写入 30wQPS。

这是典型的超大流量,热点 key、更新延迟不敏感,非数据强一致性场景(数字是一直累加),同时要做好容灾降级处理,最后实际活动展示的金额与产品预计发放数值误差小于 1%。

bad435d6650fdb6b002016da05a29b1d.png

4.5.1 方案一

提供 sdk 接入方式,复用了主会场机器实例的资源。高 QPS 下的读取和写入单 key,比较容易想到的是使用 Redis 分布式缓存来进行实现,但是单 key 读取和写入的会打到一个实例上,压测过单实例的瓶颈为 3w QPS。所以做的一个优化是拆分多个 key,然后用本地缓存兜底。

具体写入流程:

设计拆分 100 个 key,每次发红包根据请求的 actID0 使用 incr 命令累加该数字,因为不能保证幂等性,所以超时不重试。

8029924d3b366d73aff105bf7b2874bc.png

读取流程:

与写入流程类似,优先读取本地缓存,如果本地缓存值为为 0,那么去读取各个 Redis 的 key 值累加到一起,进行返回。

73a7b7c3a4dfd9c721119a3a55e511a5.png

问题:

(1)拆分 100 个 key 会出现读扩散的问题,需要申请较多 Redis 资源,存储成本比较高。而且可能存在读取超时问题,不能保证一次读取所有 key 都读取成功,故返回的结果可能会较上一次有减少。

(2)容灾方案方面,如果申请备份 Redis,也需要较多的存储资源,需要的额外存储成本。

4.5.2 方案二

设计思路:

在方案一实现的基础上进行优化,并且要考虑数字不断累加、节约成本与实现容灾方案。在写场景,通过本地缓存进行合并写请求进行原子性累加,读场景返回本地缓存的值,减少额外的存储资源占用。使用 Redis 实现中心化存储,最终大家读到的值都是一样的。

具体设计方案:

每个 docker 实例启动时都会执行定时任务,分为读 Redis 任务和写 Redis 任务。

读取流程:

  1. 本地的定时任务每秒执行一次,读取 Redis 单 key 的值,如果获取到的值大于本地缓存那么更新本地缓存的值。

  2. 对外暴露的 sdk 直接返回本地缓存的值即可。

  3. 有个问题需要注意下,每次实例启动第一秒内是没有数据的,所以会阻塞读,等有数据再返回。

写入流程:

  1. 因为读取都是读取本地缓存(本地缓存不过期),所以处理好并发情况下的写即可。

  2. 本地缓存写变量使用 go 的 atomic.AddInt64 支持原子性累加本地写缓存的值。

  3. 每次执行更新 Redis 的定时任务,先将本地写缓存复制到 amount 变量,然后再将本地写缓存原子性减去 amount 的值,最后将 amount 的值 incr 到 Redis 单 key 上,实现 Redis 的单 key 的值一直累加。

  4. 容灾方案是使用备份 Redis 集群,写入时进行双写,一旦主机群挂掉,设计了一个配置开关支持读取备份 Redis。两个 Redis 集群的数据一致性,通过定时任务兜底实现。

本方案调用 Redis 的流量是跟实例数成正比,经调研读取侧的服务为主会场实例数 2 万个,写入侧服务为资产中台实例数 8 千个,所以实际 Redis 要支持的 QPS 为 2.8 万/定时任务执行间隔(单位为 s),经压测验证 Redis 单实例可以支持单 key2 万 get,8k incr 的操作,所以设置定时任务的执行时间间隔是 1s,如果实例数更多可以考虑延长执行时间间隔。

具体写入流程图如下:

e109f0690f711922dc101c6702e3eaaf.png

4.5.3 方案对比

优点缺点
方案一1. 实现成本简单1. 浪费存储资源; 2. 难以做容灾; 3. 不能做到一直累加;
方案二1. 节约资源; 2. 容灾方案比较简单,同时也节约资源成本;1. 实现稍复杂,需要考虑好并发原子性累加问题

结论:

从实现效果,资源成本和容灾等方面考虑,最终选择了方案二上线。

4.6 难点六:进行母活动与子活动的平滑切换

需求背景:

为了保证本次春节活动的最终上线效果和交付质量,实际上分了三个阶段进行的。

(1)第一阶段是内部人员测试阶段。

(2)第二个阶段是外部演练阶段,圈定部分外部用户进行春节活动功能的验证(灰度放量),也是发现暴露问题以及验证对应解决机制最有效的手段,影响面可控。

(3)第三个阶段是正式春节活动。

而产品的需求是这三个阶段是分别独立的阶段,包含用户获得奖励、展示与使用奖励都是隔离的。

1f1592a1838cc5a1a85b5f411246e972.png

技术挑战:

有多个上游调用钱包发奖励,同时钱包有多个奖励业务下游,所以大家一起改本身沟通成本较高,配置出错的概率就比较大,而且不能同步改,会有较大的技术安全隐患。

设计思路:

作为奖励入账的唯一入口,钱包资产中台收敛了整个活动配置切换的实现。设计出母活动和子活动的分层配置,上游请求参数统一传母活动 ID 代表春节活动,钱包资产中台根据请求时间决定采用哪个子活动配置进行发奖,以此来实现不同时间段不同活动的产品需求。降低了沟通成本,减少了配置出错的概率,并且可以同步切换,较大地提升了研发与测试人效。

示意图:

24ac8b5a51a9db3f3ff016dfd9cfe086.png

4.7 难点七:大流量场景下资金安全保障

钱包方向在本次春节活动期间做了三件事情来保障大流量大预算的现金红包发放的资金安全:

  1. 现金红包发放整体预算控制的拦截

  2. 单笔现金红包发放金额上限的拦截

  3. 大流量发红包场景的资金对账

  • 小时级别对账:支持红包雨/集卡/烟火红包发放 h+1 小时级对账,并针对部分场景设置兜底 h+2 核对。

  • 准实时对账:红包雨已入账的红包数据反查钱包资产中台和活动侧做准实时对账

多维度核对示意图:

ac8168ba12977b430d33d7a5f3c27f3c.png

准实时对账流程图:

2e0ae5cd61906fc5d264ae6f5fa4e411.png

说明:

准实时对账监控和报警可以及时发现是否异常入账情况,如果报警发现会有紧急预案处理。

5. 通用模式抽象

在经历过春节超大流量活动后的设计与实现后,有一些总结和经验与大家一起分享一下。

5.1 容灾降级层面

大流量场景,为了保证活动最终上线效果,容灾是一定要做好的。参考业界通用实现方案,如降级、限流、熔断、资源隔离,根据预估活动参与人数和效果进行使用存储预估等。

5.1.1 限流层面

(1)限流方面应用了 api 层 nginx 入流量限流,分布式入流量限流,分布式出流量限流。这几个限流器都是字节跳动公司层面公共的中间件,经过大流量的验证。

(2)首先进行了实际单实例压测,根据单实例扛住的流量与本次春节活动预估流量打到该服务的流量进行扩容,并结合下游能抗住的情况,在 tlb 入流量、入流量限流以及出流量限流分别做好了详细完整的配置并同。

限流目标:

保证自身服务稳定性,防止外部预期外流量把本身服务打垮,防止造成雪崩效应,保证核心业务和用户核心体验。

简单集群限流是实例维度的限流,每个实例限流的 QPS=总配置限流 QPS/实例数,对于多机器低 QPS 可能会有不准的情况,要经过实际压测并且及时调整配置值。

对于分布式入流量和出流量限流,两种使用方式如下,每种方式都支持高低 QPS,区别只是 SDK 使用方式和功能不同。一般低 QPS 精度要求高,采用 redis 计数方式,使用方提供自己的 redis 集群。高 QPS 精度要求低,退化为总 QPS/tce 实例数的单实例限流。

5.1.2 降级层面

对于高流量场景,每个核心功能都要有对应的降级方案来保证突发情况核心链路的稳定性。

(1)本次春节奖励入账与活动活动钱包页方向做好了充分的操作预案,一共有 26 个降级开关,关键时刻弃车保帅,防止有单点问题影响核心链路。

(2)以发现金红包链路举例,钱包方向最后完全降级的方案是只依赖 docker 和 MySQL,其他依赖都是可以降级掉的,MySQL 主有问题可以紧急联系切主,虽说最后一个都没用上,但是前提要设计好保证活动的万无一失。

5.1.3 资源隔离层面

(1)提升开发效率不重复造轮子。因为钱包资产中台也日常支持抖音资产发放的需求,本次春节活动也复用了现有的接口和代码流程支持发奖。

(2)同时针对本次春节活动,服务层面做了集群隔离,创建专用活动集群,底层存储资源隔离,活动流量和常规流量互不影响。

5.1.4 存储预估

(1)不但要考虑和验证了 Redis 或者 MySQL 存储能抗住对应的流量,同时也要按照实际的获取参与和发放数据等预估存储资源是否足够。

(2)对于字节跳动公司的 Redis 组件来讲,可以进行垂直扩容(每个实例增加存储,最大 10G),也可以进行水平扩容(单机房上限是 500 个实例),因为 Redis 是三机房同步的,所以计算存储时只考虑一个机房的存储上限即可。要留足 buffer,因为水平扩容是很慢的一个过程,突发情况遇到存储资源不足只能通过配置开关提前下掉依赖存储,需要提前设计好。

5.1.5 压测层面

本次春节活动,钱包奖励入账和活动钱包页做了充分的全链路压测验证,下面是一些经验总结。

  1. 在压测前要建立好压测整条链路的监控大盘,在压测过程当中及时和方便的发现问题。

  2. 对于 MySQL 数据库,在红包雨等大流量正式活动开始前,进行小流量压测预热数据库,峰值流量前提前建链,减少正式活动时的大量建链耗时,保证发红包链路数据库层面的稳定性。

  3. 压测过程当中一定要传压测标,支持全链路识别压测流量做特殊逻辑处理,与线上正常业务互不干扰。

  4. 针对压测流量不做特殊处理,压测流量处理流程保持和线上流量一致。

  5. 压测中要验证计算资源与存储资源是否能抗住预估流量

  • 梳理好压测计划,基于历史经验,设置合理初始流量,渐进提升压测流量,实时观察各项压测指标。

  • 存储资源压测数据要与线上数据隔离,对于 MySQL 和 Bytekv 这种来讲是建压测表,对于 Redis 和 Abase 这种来讲是压测 key 在线上 key 基础加一下压测前缀标识 。

  • 压测数据要及时清理,Redis 和 Abase 这种加短时间的过期时间,过期机制处理比较方便,如果忘记设置过期时间,可以根据写脚本识别压测标前缀去删除。

  1. 压测后也要关注存储资源各项指标是否符合预期。

5.2 微服务思考

在日常技术设计中,大家都会遵守微服务设计原则和规范,根据系统职责和核心数据模型拆分不同模块,提升开发迭代效率并不互相影响。但是微服务也有它的弊端,对于超大流量的场景功能也比较复杂,会经过多个链路,这样是极其消耗计算资源的。本次春节活动资产中台提供了 sdk 包代替 rpc 进行微服务链路聚合对外提供基础能力,如查询余额、判断用户是否获取过奖励,强制入账等功能。访问流量最高上千万,与使用微服务架构对比节约了上万核 CPU 的计算资源。

6. 系统的未来演进方向

(1)梳理上下游需求和痛点,优化资产中台设计实现,完善基础能力,优化服务架构,提供一站式服务,让接入活动方可以更专注进行活动业务逻辑的研发工作。

(2)加强实时和离线数据看板能力建设,让奖励发放数据展示的更清晰更准确。

(3)加强配置化和文档建设,对内减少对接活动的对接成本,对外提升活动业务方接入效率。

来源:字节跳动技术团队

收起阅读 »

浙大教授盘和林:未来NFT或元宇宙,会出现万亿市值以上的企业

目前学界或者业界对元宇宙都没有一个非常准确的定义。我个人认为元宇宙是虚拟世界向人类现实世界的一种延伸、一种融合联通, 元宇宙实际上是将虚拟世界的体验改善:让虚拟世界能够做到现实世界中的事情,比如工作、生活,以前我们要开车奔赴目的地,我们现在可以在元宇宙上实现,...
继续阅读 »

目前学界或者业界对元宇宙都没有一个非常准确的定义。我个人认为元宇宙是虚拟世界向人类现实世界的一种延伸、一种融合联通, 元宇宙实际上是将虚拟世界的体验改善:让虚拟世界能够做到现实世界中的事情,比如工作、生活,以前我们要开车奔赴目的地,我们现在可以在元宇宙上实现,以前工厂上班单调乏味,现在我们改造成元宇宙,可能就会不一样。

但元宇宙和以前我们所说的虚拟世界还是有区别的。元宇宙有几个基本概念,比如它有经济系统、用户创作等。简单地说,原来的虚拟世界是一个跟现实世界有很多脱节的地方,元宇宙理想的状态是我们在现实世界当中有一个人,在虚拟世界也有一个人。元宇宙目前最需要改善的就是我们的感官体验。现在的虚拟世界可能只有视觉感受。在真正的元宇宙中,感官、嗅觉这些都要和现实无限接近和逼真。

单一从VR和AR技术限制的角度来看,目前学界和业界的专家大都认为,元宇宙至少要到2040年以后才能达到理想的状态,现在无非就是现实增强(VR)可能比原来的体验更高,甚至加入了一些互动元素而已,和真正严格意义上的元宇宙中的场景还是有很大差距的。不过,我个人是技术乐观派,有可能在各种资本的催化下,或许科技进步的脚步比我们想象的来得更快,我们也可能在2040年之前就能够感受到元宇宙实质性的进步。

从根本上来说,互联网平台不能提供所有的内容,需要所有的用户参与内容的制作,否则这个元宇宙可能就是寡淡的单线游戏。所以元宇宙一个非常重要的特征,就是开放用户参与,内容创作这一块可能是年轻人参与的部分。未来人们不再满足物质消费,而是千金难买我乐意,未来搞怪的包括个性化的艺术作品、道具、虚拟偶像,很可能都是一个比较好的变现渠道。只要有自己生产的内容,且能够满足一些爱好者的偏好,就有可能抓住意想不到的变现机会。

我个人认为NFT或者元宇宙,起码要出几家万亿市值以上的头部企业,这个我坚信一定会出现的,其实我们现在都能够感受到这种变化的雏形,能看到呼之欲出的样子。比如,现在开或还是多高端的论坛,一定要有在线视频支持,以前很少有嘉宾在线发言,现在几乎很多的会议与论坛都有视频嘉宾发言,这种体验效果已经非常好了。未来随着5G或者6G带宽实现高速、低时延,在线未必能够实现连嗅觉都能传递,但一定会很大程度增强用户体验,从而促进线下产业的效率,拓展线下产业的边界,因而,线上线下深度融合带来的效率转换成伟大公司或产业,想象空间还是非常大的,也是值得期待的。

虽然现在有反对的声音,认为元宇宙会让人沉迷在一个虚拟的空间里,逃避现实,甚至有人认为影响到生育。尽管虚拟空间确实会让人沉迷,但我不完全赞同就是洪水猛兽。

沉迷于虚拟世界,可不影响男女接触了吗?但实际上持持观点的人不了解Z世代,1995-2010年出生的这批年轻人,你就正常让他们接触,他们现实世界的社交圈子也是很窄,那时候还是独生子女,又整日电子产品相伴,天天猫在互联网,你让他们怎么找到彼此?所以,才会有很多网络上的婚恋网站,社交网站,去撮合。所以,这完全是一个杞人忧天,互联网也好,元宇宙也好,其核心是连接,而不是断开,连接就包括社交,有了社交接触,婚姻反而更好实现。如今的问题是什么?问题是社交媒体没有为社交增加可信度,存在虚伪包装,如果减少这些虚伪包装,网络红娘效果一定比现实红娘效果要好。元宇宙时代是多元文化时代,这个时代又多年轻人以部落状态在一起社交,比如国潮、国风、二次元。相同爱好的年轻人在一起,当然有更多交集,也更容易走向婚姻,不是吗?

我认为,元宇宙和现实不能说是一种对立关系,而是弥补。即使不考虑疫情的场景,假如像今天为了做一场直播和节目,往返于广州和北京的航班上,我们的时间成本是非常高的。原来,我们之所以没有像今天的这么线上会议或直播,很大程度上是是因为数字技术限制,当然还有用户习惯,而今天5G等数字技术的进步,以及疫情的用户习惯培育,才显得那么理所应当。实事求是地说,我们今天的直播很难有真正坐着面对面聊天的效果,不过已经能够在很大程度上满足了交流需要,其便捷性已经超过了小微不好体验。当然。我们完全可以憧憬未来,随着通信网络传递实现低时延与增强带宽,即网速提升,未来直播等在线视频实际上是能够做到更接近真实的,这完全可以期待的。未来,我们可以在元宇宙中实现更多学习、生活、工作的场景。

技术进步必然带来产业的大发展。为什么我说有万亿级别的企业呢?就拿服装产业来说吧,现在虽然也可以在网购服装,但颜色、尺寸等体验,离现实效果总是有不小的差异,影响了用户体验和消费欲望。如果互联网、VR、AR等数字技术能够接近到95%甚至更高,很可能对服装产业的商业模式产生颠覆性改变,也许我们以后逛服装店,就看不到实体衣服了,而是在元宇宙中试衣服后,工厂就快递过来了。实际上,类似这种个性化制已经发生在我们现实当中,只不过目前的数字技术还影响消费体验而已,未来在元宇宙中无限接近现实世界的体验。我们可以试想,类似服装的很多传统产业,都可能在用户体验、改善供需匹配、产业链资源配置效率等方面得到大幅度提升,其带来的巨大价值或整个产业生产效率提升,那将是以万亿为单位的。

有人问,什么东西是非得在元宇宙里做不可的,就是这个东西我只能在元宇宙里做,在别的地方做就不行?比如我必须在现实世界里买房,但元宇宙毕竟是虚拟空间,在元宇宙买房可有可无,那它能成为一门生意吗?

实际上,生意有好多种,一个是经济价值,一个是情感价值,还有完全个性化的个人价值,这些价值产生支付都可以视为生意,因此,未必就是房子才是刚需。也就是说,可能我们这一代人永远搞不懂那些玩游戏的人,为什么在游戏里面可以大把地花钱去买一些工具。人是有多种复杂需求的,除了我们常规理解的食物、衣着是刚需,慢慢地娱乐、社交实际上也是一些人的刚需。

并不是说必须在元宇宙里实现的,才会有客户,才会有购买力。像用户创作或者参与内容变现,体验价值可能才是元宇宙里的“刚需”。类似现在的抖音,你看到有很多“非常无聊”的视频也有惊人的点击量,那么刷抖音是不是一种“刚需”呢?还有元宇宙中的虚拟人也是一个产业,已经人格化的虚拟人,背后有一些系列的社会及经济属性。也许,我们在元宇宙中有一个我们一模一样的虚拟人也是一种“刚需”呢。补充说明一下前面说到的元宇宙与虚拟世界的区别这个问题,原来的虚拟世界里没有人格化的虚拟人,这也是元宇宙与此前的虚拟世界一个非常重要的区别。比如元宇宙已经出现了性骚扰案,那就是已经把它人格化了。而虚拟世界就没有人格化问题。

也就是说,在元宇宙中,我们不能按常规理解只有吃饭、睡觉这一类的是刚需。娱乐化或者线上会议在有些地方就是刚需。在美国已经出现了元宇宙的毕业典礼,国内也有大型元宇宙会议,如果由于距离无法在线下实现的时候,它某种程度上成为了一种刚需。数字技术改变着我们当下的经济社会,元宇宙就带有“社会性”和“经济性”。

回到今天的主题上来,靠元宇宙发财现实吗?现在元宇宙这么火,这也是大家现在都很关注的问题。我认为短时间内肯定不会,至少绝大多数人无法靠元宇宙发财,而且即使在未来,元宇宙是多数人参与的游戏,但只会是少数人的发财游戏。很简单,现在你参与不进去, NFT的门槛和风险绝对不是普通人能够玩的,打个比方,抖音的视频高流量毕竟是少数。因此,元宇宙可能是我们参与的理想世界和美好家园,那里可以摆脱现实世界诸多的“痛苦”,或许有很多精神财富,但未必每个人都能够赢得货币财富。

到目前为止,绝大多数的专家也没有讲出一个关于元宇宙的所以然,或者成熟的商业模式。元宇宙怎么能够成为绝大多数普通人的创富工具呢?不排除有些人敢于玩NFT的,可能赢得了一笔财富,但这占人群中的比重是非常小的。

假如非得要说创富,如果未来元宇宙普及,年轻人怎么在元宇宙中赚到第一桶金?个人的看法,依然是制造内容,而不是倒卖内容。元宇宙的一大特征是数字资产确权,利用NFT缔造了经济系统,由于元宇宙元素很多,互联网平台不可能提供所有的内容和服务,此时元宇宙需要什么?需要所有用户参与制造内容,否则这个元宇宙就是寡淡的单线游戏,元宇宙时代要开放游戏和用户参与。所以元宇宙要用户来创造内容。此时包括虚拟偶像、道具装备、个性化艺术涂鸦等等,都是一个很好的变现渠道,不需要你多优秀,要你做出来的东西,有足够多的爱好者,所谓千金难买我乐意,千金难买我喜欢。搞怪、鬼畜、赛博朋克、意识流,都没关系,只要符合一部分人的审美喜好就能够实现变现。所以,年轻人要在3D建模、剪辑、设计、区块链NFT、AI上去学习。

而在当下,元宇宙的概念是从华尔街讲故事讲出来的,目前第一波还是资本玩的游戏,无论是从游戏也好、还是社交也好,第一波人可能把握一定的财富机会,但终归是少数人。

我认为,现在元宇宙的样子,只是人类依据过去互联网技术与发展趋势,以及对未来互联网世界的美好想象而折射到今天的一个产物,但未来会长成什么样子,甚至用什么名词更合适,现在都是不确定的。从既往互联网发展的经验来看,我们对未来的判断,很可能是盲人摸象式的,我们今天认为元宇宙的一些特征,未必就是未来真正的样子。但是人类还是要靠想象去谋划。就像人类研究未来学一样,我们对未来数字世界也要有想象力,无论叫第三代互联网也好,还是第四代互联网也好,人类都需要有一个憧憬,或者有一个发展方向,在这个过程当中不断去调试。这绝不仅仅是资本讲故事的需要,人类世界因为想象而变得更加美好。

目前来看,元宇宙很多商业模式是很难实现的。当然,元宇宙即便在当下也是能够实实在在促进产业的,例如VR和AR技术及相关产业,还有社交、游戏应用场景是比较确定的,这些行业已经分享到元宇宙的“想象力红利”。至于炒房和NFT,都是风险泡沫非常大的,例如元宇宙房地产,我们没办法在元宇宙里面住,肉身还得寄放在高房价和拥堵的城市森林之中。

而且现在还有一个很大的误区,我们说数字藏品、NFT,现在只讲一个独特性、稀缺性,不讲其中的价值,可能存在很大问题。我们知道,商品或服务的独特性一定要跟价值关联起来,比如小孩子画了一幅画,它非常有独特性、稀缺性,因为世界上只有他一个人画了这一幅,但是它不一定产生艺术价值。现在,国内外都有这种情况,一种毫无艺术价值的NFT被拍出了天价,这就是泡沫。

目前很多的数字藏品过于追求稀缺性。其实,稀缺性往往是很容易获得的,比如我今天随便乱写几个字、乱画几笔也是有稀缺性,但它不一定代表有价值。当然,应该说泡沫未必都是坏事,就像啤酒的泡沫,产业也相类似,一个新兴产业就是要靠泡沫想象或者实践往前走的。泡沫下面才是甘美的啤酒。

泡沫破裂往往才会沉淀出新的产业。遥想当年互联网撕杀也是异常惨烈,几次波及全世界的互联网泡沫之后才有今天的互联网产业,那真是“一将功成万骨枯”,最后起来那么几个互联网巨头,但背后参与竞争的可能是成千上万个互联网企业,没有一个产业是风轻云淡就起来的,都是异常的市场搏杀。元宇宙赛道也会是这样的,为什么我一再主张,哪怕是投资,哪怕是故事,都交给市场去选择,每一个市场的形成都是残酷的。市场的成熟发展,没有这些 “万骨枯”就没有“一将功成”。当然,这种容忍度也是要设置底线的,就是自愿参与原则,即市场主体按照自己的意愿参与,同时还要考虑参与者的风险识别和承受能力,对一些没有多少风险识别能力、低承受能力的人要进行规劝、规避,甚至设置行政法规的规制,比如NFT就不能让跳广场舞的大妈来参与了,未来需要形成一系列的参与规则。

现在很多元宇宙或者NFT被导入到一些传销的概念里去了,这些人实际上无论从知识结构还是从风险承受能力都是很弱的,市场对这一类参与者应该尽可能地采取一些规则,我们要考虑弱势群体的一些福利和承受能力。其他的还是要鼓励,比如资本的参与,哪怕资本最终“血流成河”也是体现“市场风险与利润并存”的理念的,我认为这就是市场经济的代价,不能因为代价就裹足不前。

主张“破坏性创新”的创新之父熊比特曾经说过,企业及企业家就像旅鼠一样,一只只义无反顾地向着大海奔赴死亡之旅,用“旅鼠”来形容企业及企业家其实十分形象、生动,也让我非常感慨,正是一代又一代的企业及企业家向“死”而生,才让经济社会生生不息!每一个产业中的企业及人,未尝不都是“旅鼠”,我们的互联网产业也是这样走过来的, 元宇宙产业的形成过程中未尝不是如此。

在这个过程当中,谁能够更准确地找准用户的需求,谁就更容易在残酷的市场中胜出。另外就是一定要符合社会的基本价值和规则,否则的话你做了一个非常大的市值,最后会付出非常惨痛的代价。如果你不符合社会的价值、逻辑和伦理的一些商业模式,最终还是会被淘汰的,这样的例子在当前的现实中并不鲜见。

在抓住需求的时候,一定要去考虑多方相关利益者的约束条件。如果一种商业模式一再利用成瘾机制,导致用户人性扭曲,或者是社会价值的扭曲,这个商业模式哪怕再有用户支付率,再多忠实消费者,并不代表它是一个非常伟大的商业模式。

毫无疑问,这些都是值得元宇宙产业值得思考的。

因此,元宇宙发展可能还是要立足于实现价值、满足需求的维度来促进我们的产业效率提升。如果是符合客户及社会价值需求的,我们就可以大胆去尝试。我认为,国内发展元宇宙产业也应该是这样的,把边界约束好,当然产业的边界未必很清晰,但价值的边界不能出偏差,其他的还是要鼓励去发展,发展成什么样子,还是要予以审慎包容监管心态,来对待元宇宙的发展。女大十八变,元宇宙还是刚刚出生的女婴,未来长成什么样子,我们还不知道,但我完全有理由去憧憬是一个曼妙少女。这不是空想,毕竟我们前面说了,元宇宙是人们基于过去互联网发展现实与现状、未来发展趋势判断而形成的,并非异想天开的空想。

接下来,谈谈同样热门的碳中和与个人碳账户话题。

我认为,这个不确定的世界里,未来有两件事情是非常确定的:第一是数字化,其中包括产业数字化,就是现实世界往数字走,AIoT(人工智能物联网),也包括元宇宙,数字世界和现实世界深度融合。我在我即将出版的《从AIoT到元宇宙:关键技术、产业图景与投资机遇》(暂定名)一书中描述了一个场景,期待AIoT与元宇宙相向而行,说的也是现实世界与数字世界相向而行。第二个是碳中和,这事关人类生存空间。关于碳中和,我先说一个观点,环境经济学认为,满足人类基本需求的污染不应该被视为一种需要规制的环境污染。比如举行一场篝火晚会,篝火会带来一定的碳排放,但不能因为影响环境就禁止篝火晚会。

本质上来说,碳中和也好,元宇宙也好,核心还是依赖技术进步,而不是简单的规制。比如,按照以前能源悲观主义者的观点,石油早消耗殆尽了。而后来,石油开发技术进步,页岩气开发,包括现在的风能和光伏的利用,未来的能源是否枯竭依然要依赖于技术进步。元宇宙所需要的算力资源也是同理,我们还是要通过技术去实现。

普通人如何参与开展碳中和呢?我认为,普通人只要按照规矩办事,顺着政策指导的方向,从需求出发,不过度浪费,就是对碳中和最好的支持。比如新能源车,你和老百姓说很多道理,说锂电动车节能环保,碳排放少,这些都可能无法触及灵魂。但近年来锂电池车大卖,原因很简单,锂电池动力车成本比汽油车低,不仅仅是制造成本,还有使用成本,维护成本,保养成本。老百姓一算账就明白了,。当然除了性价比高,当前充电桩设施也普及了,所以消费行为就导向了锂电池汽车。但反过来说,现在氢能汽车就不是很好推,原因很简单,氢能的运输成本太高,因为体积大,密度低,所以加氢站多不起来,成本太高。这个时候个人的观点,就是在氢能运输问题解决之前,我们也不能因为要碳中和而去大面积推广氢能汽车。所以对于普通人来说,碳中和就是根据你的需要,算好经济账,不要浪费。你普通家庭在耗能上实现经济了,省钱了,那么这个世界的碳排放自然就少了。

其实,作为我经济学者,更关注碳中和的产业问题,而不是个人减碳行为。我认为,“双碳”目标是中国作为一个大国的政治承诺,也是履行国际义务的体现。但未来真正的竞争不是在于碳中和,而是在于碳足迹(全生命周期碳排放),在于碳关税。碳关税可能是一个贸易竞争的核心焦点,这一块如何争取,可能比我们关注个人碳账户更有价值,至少更迫切。无论是产业视角的碳足迹,还是个体行为的个人碳账户,我个人觉得最重要的还是要在底层数据以及宏观产业层面去解决“双碳”问题。当然,我不是说个人碳账户不重要,俗话说众人拾柴火焰高,多数人的自觉绿色环保的行为,可能会给生活环境带来很大的变化。不过,我更关注碳足迹、碳关税所带来的产业竞争,这也关系到每个人的就业、收入等切身福利。

对中国实现双碳目标来说,第一个就是对高能耗的产业进行一定的测算。

第二个角度是产业竞争。无论怎么样,未来都不可能实现地球村,总是会有竞争的。现在的竞争,不会是战争的竞争,虽然也有俄乌冲突,更多是产业的竞争,本质上还是要回归到产业和企业效率上竞争,一个国家的产业效率越高,企业的竞争力越强,你在世界上获取的经济效益份额可能就越大。

因此,中国还是要去关注那些外向型即国际贸易高度相关的产业,一定想办法把这些产业的碳足迹核算好,不用说领先于全球,起码要同步欧美可能采取贸易壁垒的产业先扎扎实实做起来,否则欧美做出一整套、有说服力的碳足迹数据来,我们未来就会在产业上吃亏,他们必然高举碳关税来打击我们的产业,设置新的国际贸易壁垒。

产业看起来离我们普通老百姓很远,但事实上这几年的产业转移,包括企业的竞争,事实上已经关系到老百姓的就业和收入。

还是那句话,说一尺不如干一寸。我们对碳足迹也好,或者对元宇宙也好,不能停留在想象中的好处和坏处,我们一定要在实践中实现它,要往前迈一步。国家前段时间已经出台了要进行碳核查,建立县一级的碳核查机制的文件。我觉得,当前最迫切的,除了宏观层面,还是要往产业上靠,尤其是会受到影响的外贸型产业。
来源:
mp.weixin.qq.com/s/4azKexPbPlfqVmsLErp2QA

收起阅读 »

剑指 Offer 10- I. 斐波那契数列

题目描述: 写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下: F(0) = 0,   F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中...
继续阅读 »

题目描述:


写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:


F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。


答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/fe…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


第一个想法使用递归:但是这个题目用递归写会超时。所以直接使用非递归的解法。


    public int fib(int n) {
final int MOD = 1000000007;
if(n == 0){
return 0;
}
if(n<=2){
return 1;
}
//因为已经确定第一个值和第二个值了,所以直接从第三个数开始做循环
int i1 = 1;
int i2 = 1;
int sum = 0;
for (int i=3;i<=n;i++){
sum = (i1+i2) % MOD;

i1 = i2;
i2 = sum;
}
return sum;
}

作者:未来_牛小牛
链接:https://juejin.cn/post/7087419363595714573
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-ViewBinding的内存泄露

场景 在MainActivity中分别加载两个Fragment处理业务。 首先触发加载SecondFragment: //MainActivity触发 supportFragmentManager.commit { add(R.id.content...
继续阅读 »
场景

在MainActivity中分别加载两个Fragment处理业务。
首先触发加载SecondFragment:


//MainActivity触发
supportFragmentManager.commit {
add(R.id.contentLayout, FirstFragment())
addToBackStack(null)//点击返回键可以回到FirstFragment
}

//FirstFragment布局中有一个自定义MyButton且有bitmap属性
class MyButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : Button(
context, attrs, defStyle
) {
private val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.a)
}

然后触发加载SecondFragment;


//MainActivity触发
supportFragmentManager.commit {
replace(R.id.contentLayout, SecondFragment())
addToBackStack(null)
}

Android Profile可以发现有内存泄露

1.png


- MyButton中的bitmap无法释放。


为什么认为bitmap无法释放就是内存泄漏呢?

内存泄漏:简单点说,就是该释放的内存无法得到释放,而且内存不能被使用。
从Fragment的生命周期说起,从FirstFragment切换到SecondFragment,前者生命周期从onPause->onStop->onDestoryView,注意这里只走到onDestoryView,并没有onDetach以及onDestory。其实也很好理解,FirstFragment是加入了回退栈,后续是要被恢复,所以保留了Fragment对象,但为了不占用过多的内存,View会被销毁释放资源。
当FirstFragment从回退栈回到前台,会再次触发onCreateView重建View。既然View会重建,那么之前的View就是不需要的,留着也没用,就应该销毁掉。


该释放的View、Bitmap没有被释放,所以就出现了泄漏。


例子比较简单,只是为了说明问题,如果FirstFragment View持有大量占内存的对象,而且SecondFragment的加载需要耗费比较多的内存且存在跳转的其他页面的可能性,那么FirstFragment View的释放就显得很有必要。


补充引用链:FirstFragment-MyButton-Bitmap


onDestoryView官方注释

注意到这句“The next time the fragment needs
* to be displayed, a new view will be created”,当Fragment恢复时,会创建新的view添加到Fragment,也就是重走onCreateView,那么我理解旧的view就应该可以被销毁。


    /**
* Called when the view previously created by {@link #onCreateView} has
* been detached from the fragment. The next time the fragment needs
* to be displayed, a new view will be created. This is called
* after {@link #onStop()} and before {@link #onDestroy()}. It is called
* <em>regardless</em> of whether {@link #onCreateView} returned a
* non-null view. Internally it is called after the view's state has
* been saved but before it has been removed from its parent.
*/
@MainThread
@CallSuper
public void onDestroyView() {
mCalled = true;
}

LeakCanary日志

建议在onDestroyView要释放掉View


LeakCanary: Watching instance of androidx.constraintlayout.widget.ConstraintLayout (com.yang.myapplication.MyFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 0f101dfe-5e4e-4448-95cc-f5d08bbdf06e

解决方案

将ViewBinding置空就欧了。其实这也是官方的建议,当你新建项目的时候,就能看到这样的案列。


   override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

总结

当出现Fragment没有被销毁(onDestory没有回调),而view需要被销毁时(onDestoryView),要注意把ViewBinding置空,以免出现内存泄露。


作者:杨小妞566
链接:https://juejin.cn/post/7088212218205962276
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

在Flutter上优雅的请求网络数据

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。 解决的问题 通用异常处理 请求资源状态可见(加载成功,加载中,加载失败) 通用重试逻辑 效果展示 为了演示请求失败的处理,...
继续阅读 »

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。


解决的问题



  • 通用异常处理

  • 请求资源状态可见(加载成功,加载中,加载失败)

  • 通用重试逻辑


效果展示


为了演示请求失败的处理,特意在wanApi抛了两次错
LBeZ5Q.gif


正文


搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo


定义数据源


使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库


@RestApi(baseUrl: "https://www.wanandroid.com")
abstract class WanApi {
factory WanApi(Dio dio, {String baseUrl}) = _WanApi;

@GET("/banner/json")
Future<BannerModel> getBanner();

@GET("/article/top/json")
Future<TopArticleModel> getTopArticle();

@GET("/friend/json")
Future<PopularSiteModel> getPopularSite();
}

生成的代码


class _WanApi implements WanApi {
_WanApi(this._dio, {this.baseUrl}) {
baseUrl ??= 'https://www.wanandroid.com';
}

final Dio _dio;

String? baseUrl;

@override
Future<BannerModel> getBanner() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BannerModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/banner/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BannerModel.fromJson(_result.data!);
return value;
}

@override
Future<TopArticleModel> getTopArticle() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<TopArticleModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/article/top/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = TopArticleModel.fromJson(_result.data!);
return value;
}

@override
Future<PopularSiteModel> getPopularSite() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<PopularSiteModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/friend/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = PopularSiteModel.fromJson(_result.data!);
return value;
}

RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
}

repository封装


Resource是封装的资源加载状态类,用于包装资源


enum ResourceState { loading, failed, success }

class Resource<T> {
final T? data;
final ResourceState state;
final dynamic error;
Resource._({required this.state, this.error, this.data});

factory Resource.failed(dynamic error) {
return Resource._(state: ResourceState.failed, error: error);
}

factory Resource.success(T data) {
return Resource._(state: ResourceState.success, data: data);
}

factory Resource.loading() {
return Resource._(state: ResourceState.loading);
}

bool get isLoading => state == ResourceState.loading;
bool get isSuccess => state == ResourceState.success;
bool get isFailed => state == ResourceState.failed;
}

接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream<Resource<HomeDataMapper>> homeData() async* {
//加载中
yield Resource.loading();
try {
var result = await Future.wait<dynamic>([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
} catch (e) {
//加载失败
yield Resource.failed(e);
}
}
}

咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream<Resource<HomeDataMapper>> homeData() async* {
///定义加载函数
loadHomeData()async*{
var result = await Future.wait<dynamic>([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
///将加载函数放在一个包装器里执行
yield* MyWrapper.customStreamWrapper(loadHomeData);
}
}

得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能)
包装器代码:


class MyWrapper {
//流的方式
static Stream<Resource<T>> customStreamWrapper<T>(
Stream<Resource<T>> Function() function,
{bool retry = false}) async* {
yield Resource.loading();
try {
var result = function.call();
await for(var data in result){
yield data;
}
} catch (e) {
//重试代码
if (retry) {
var toRetry = await Get.dialog(const RequestRetryDialog());
if (toRetry == true) {
yield* customStreamWrapper(function,retry: retry);
} else {
yield Resource.failed(e);
}
} else {
yield Resource.failed(e);
}
}
}
}

其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:


@Repo()
abstract class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();

///获取首页所需的所有数据
@ProxyCall()
@Retry()
Stream<Resource<HomeDataMapper>> homeData() async* {
var result = await Future.wait<dynamic>(
[wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
}

是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:


// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'wan_repository.dart';

// **************************************************************************
// RepositoryGenerator
// **************************************************************************

class WanRepositoryImpl = WanRepository with _WanRepository;

mixin _WanRepository on WanRepository {
@override
Stream<Resource<HomeDataMapper>> homeData() {
return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
}
}

结语


感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考


附demo地址:gitee.com/cysir/examp…


flutter版本:2.8


作者:贼不走空
链接:https://juejin.cn/post/7088223867017101343
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

阿里四面,居然栽在一道排序算法上

前言 算法是程序的灵魂,一个优秀的程序是可以在海量的数据中,仍保持高效计算。目前各大厂的面试要求也越来越高,算法肯定会要去。如果你不想去大厂,只想去小公司,获取并不需要要求算法。但是你永远只能当一个代码工人,也就是跟搬砖的没区别。可能一两年后你就会被淘汰。 ...
继续阅读 »

前言



算法是程序的灵魂,一个优秀的程序是可以在海量的数据中,仍保持高效计算。目前各大厂的面试要求也越来越高,算法肯定会要去。如果你不想去大厂,只想去小公司,获取并不需要要求算法。但是你永远只能当一个代码工人,也就是跟搬砖的没区别。可能一两年后你就会被淘汰。
如果不想永远当个代码工人,就在业余时间学学数据结构和算法。



今天就来分享一个朋友阿里四面挂了的排序算法题912. 排序数组,
排序数组这道题本身是没有规定使用什么排序算法的,但面试官指定需要使用归并排序算法来解答,肯定是有他道理的。


我们知道,排序算法有很多,大致有如下几种:


MESA Monitor


其中归并排序应该是使用的最多的几种之一,Java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。归并排序自身的优点有二,首先是因为它的平均时间复杂度低,为O(N*logN);其次它是稳定的排序,即相等元素的顺序不会改变;除了这两点优点之外,其蕴含的分治思想,是可以用来解决我们许多算法问题的,这也是面试官为什么要指定归并排序的原因。好了,废话不多说,我们接下来具体看看归并排序算法是如何实现的吧。


归并排序(递归版)


归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略,即分为两步:分与治。


1. 分:先递归分解数组成子数组


2. 治:将分阶段得到的子数组按顺序合并


我们来具体看看例子,假设我们现在给定一个数组:[6,3,2,7,1,3,5,4],我们需要使用归并算法对其排序,其大致过程如下图所示:


MESA Monitor


阶段可以理解为就是递归拆分子序列的过程,递归的深度为log2n。而治的阶段则是将两个子序列进行排序的过程,我们通过图解看看治阶段最后一步中是如何将[2,3,6,7]和[1,3,4,5]这两个数组合并的。


MESA Monitor


图中左边是复制的临时数组,而右边是原数组,我们将左右指针对应的值进行大小比较,将较小的那个数放入原数组中,然后将相应的指针右移。比如第一步中,我们比较左边指针L指向的2和右指针R指向的1,R指向的1小,则把1放入原数组中的第一个位置中,然后R指针向右移动。后面再继续,直到左边临时数组的元素都按序覆盖了右边的原数组。最后我们通过上图再结合源码来看看吧:


class Solution {
public int[] sortArray(int[] nums) {
sort(0, nums.length - 1, nums);
return nums;
}

// 分:递归二分
private void sort(int l, int r, int[] nums) {
if (l >= r) return;

int mid = (l + r) / 2;
sort(l, mid, nums);
sort(mid + 1, r, nums);
merge(l, mid, r, nums);
}


// 治:将nums[l...mid]和nums[mid+1...r]两部分进行归并
private void merge(int l, int mid, int r, int[] nums) {
int[] aux = Arrays.copyOfRange(nums, l, r + 1);

int lp =l, rp = mid + 1;

for (int i = lp; i <= r; i ++) {
if (lp > mid) { // 如果左半部分元素已经全部处理完毕
nums[i] = aux[rp - l];
rp ++;
} else if (rp > r) { // 如果右半部分元素已经全部处理完毕
nums[i] = aux[lp - l];
lp ++;
} else if (aux[lp-l] > aux[rp - l]) { // 左半部分所指元素 > 右半部分所指元素
nums[i] = aux[rp - l];
rp ++;
} else { // 左半部分所指元素 <= 右半部分所指元素
nums[i] = aux[lp - l];
lp ++;
}
}
}
}

我们可以看到,分阶段的时间复杂度是logN,而合并阶段的时间复杂度是N,所以归并算法的时间复杂度是O(N*logN),因为每次合并都需要对应范围内的数组,所以其空间复杂度是O(N);


归并排序(迭代版)


上面的归并排序是通过递归二分的方法进行数组切分的,其实我们也可以通过迭代的方法来完成分这步,看下图:


MESA Monitor


其因为数组,所以我们直接通过迭代从1开始合并,其中sz就是合并的长度,这种方法也可以称为自底向上的归并,其具体的代码如下


class Solution {
public int[] sortArray(int[] nums) {
int n = nums.length;
// sz= 1,2,4,8 ... 排序
for (int sz = 1; sz < n; sz *= 2) {
// 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
for (int i = 0; i < n - sz; i += 2*sz ) {
merge(i, i + sz - 1, Math.min(i+sz+sz-1, n-1), nums);
}
}
return nums;
}

// 和递归版一样
private void merge(int l, int mid, int r, int[] nums) {
int[] aux = Arrays.copyOfRange(nums, l, r + 1);

int lp =l, rp = mid + 1;

for (int i = lp; i <= r; i ++) {
if (lp > mid) {
nums[i] = aux[rp - l];
rp ++;
} else if (rp > r) {
nums[i] = aux[lp - l];
lp ++;
} else if (aux[lp-l] > aux[rp - l]) {
nums[i] = aux[rp - l];
rp ++;
} else {
nums[i] = aux[lp - l];
lp ++;
}
}
}
}

总结


归并排序是一种十分高效的排序算法,其时间复杂度为O(N*logN)。归并排序的最好,最坏的平均时间复杂度均为O(nlogn),排序后相等的元素的顺序不会改变,所以也是一种稳定的排序算法。归并排序被应用在许多地方,其java中Arrays.sort()采用了一种名为TimSort的排序算法,其就是归并排序的优化版本。


作者:初念初恋
链接:https://juejin.cn/post/6979760710289424421
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Kotlin-apply、also、run、let、区别

apply、also介绍 两者都是T的扩展函数,也就是任何类型对象都调用apply、also; 两者的返回值都是this,也就是函数调用者; apply的闭包使用this来访问函数调用者,also的闭包使用it来访问函数的调用者。 一看看apply、als...
继续阅读 »
apply、also介绍


  • 两者都是T的扩展函数,也就是任何类型对象都调用apply、also;

  • 两者的返回值都是this,也就是函数调用者;

  • apply的闭包使用this来访问函数调用者,also的闭包使用it来访问函数的调用者。


一看看apply、also源码

public inline fun <T> T.apply(block: T.() -> Unit): T {//1
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()//2
return this//返回值为this,也就是apply的调用者
}

public inline fun <T> T.also(block: (T) -> Unit): T {//3
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)//4
return this 返回值为this,也就是also的调用者
}


  • 注释1:apply接受的闭包类型为T.() -> Unit,也就是调用者的扩展函数,例子tv.apply{},闭包{}为tv的扩展函数,所以this可以访问到调用者;

  • 注释2:直接调用闭包,完成apply的逻辑;

  • 注释3:also接受的闭包类型为 (T) -> Unit,也就是任意函数,只要函数入参类型为also调用类型返回为Unit都可以;

  • 注释3:把this作为闭包的参数传入,例子tv.also{},闭包的入参为tv,所以it能访问到tv;

  • apply this可以访问调用者本身,因为闭包是扩展函数,而also用it访问调用者本身,因为调用者是作为参数传入闭包的。


apply、also适用场景

因为返回值为调用者this,所以它们非常适合对同一个对象连续操作的链式调用。
以下代码以apply为例,链式调用对tv进行一系列操作。注意:例子不一定合理,只是想表达相应的意思而已。


    private fun init() {
val tv = TextView(this)
tv.apply {
this.text = "name" //操作1
}.apply {
this.setOnClickListener { //操作2
Log.d("MainActivity", "setOnClickListener")
}
}.apply {
this.gravity = Gravity.CENTER //操作3
}
}

run、let介绍


  • 两者都是T的扩展函数,也就是任何类型对象都调用run、let;

  • 两者的返回值是:最后一行非赋值代码作为闭包的返回值,否则返回Unit;

  • run的闭包使用this来访问函数调用者,let的闭包使用it来访问函数的调用者。


一起看看 run、let源码

public inline fun <T, R> T.run(block: T.() -> R): R {//1
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()//2
}

public inline fun <T, R> T.let(block: (T) -> R): R {//3
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)//4
}


  • 注释1:run接受的闭包类型为T.() -> Unit,也就是调用者的扩展函数,this可以访问到调用者,这点跟apply一样;

  • 注释2:直接调用闭包,将闭包的返回值返回;

  • 注释3:let接受的闭包类型为block: (T) -> Unit,也就是任意函数,只要函数入参类型为also调用者类型返回为Unit都可以;

  • 注释4:直接调用闭包,将this作为参数传入闭包;

  • run this可以访问调用者本身,因为闭包是扩展函数,而let用it访问调用者本身,因为是作为参数传入闭包。


run、let适用场景

它们都可以有返回值,所以非常适合上一个操作返回值作用于下一个操作的链式调用。以下代码以let为例,操作1返回值作用于操作2,操作2返回值作用于操作3。注意:例子不一定合理,只是想表达相应的意思而已。


    private fun init(data: Int): Int {
return data.let {
if (data == 1) it + 1 else it + 2 //操作1
}.let {
if (data == 2) it + 3 else it + 4 //操作2
}.let {
if (data == 3) it + 5 else it + 6 //操作3
}
}

作用函数更重要的作用

确保操作的作用域,以下代码确保tv不为空的情况下执行,保证操作的作用域。


        val tv = TextView(this)
tv?.apply {
text = count.toString()
setOnClickListener {
Log.d("MainActivity", "setOnClickListener")
}
gravity = Gravity.CENTER
}

为什么有的用this访问调用者,有的则用it?

前面分析源码的时候可以看到,



  • apply、run接收的闭包类型为调用者的扩展函数,既然是扩展函数,那么当然是用this来访问调用者;

  • also、let接受的闭包类型为任意类型的函数,只要函数入参类型为调用者类型返回为Unit都可以,既然是参数,那么就能用不能用this来访问,就得用其他字符来访问,定义it来访问也未尝不可;


总结


  • apply、also,闭包的返回值都是this,前者apply接受的闭包类型调用者的扩展函数,后者接受的闭包类型为 入参为调用者类型的函数;

  • also、apply,非常适合对同一个对象连续操作的链式调用;

  • run、let,闭包的返回值为最后一行非赋值代码,前者run接受的闭包类型调用者的扩展函数,后者接受的闭包类型为 入参为调用者类型的函数;

  • run、let,非常适合上一个操作返回值作用于下一个操作的调用;


以上分析有不对的地方,请指出,互相学习,谢谢哦!


作者:杨小妞566
链接:https://juejin.cn/post/7088908213399060488
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

开箱即用,5 个功能强悍的 JSON 神器!

大家好,我是小 G。自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实...
继续阅读 »

大家好,我是小 G。

自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。

身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实现一些小功能,因此便到 GitHub 了解了一下关于 JSON 的开发者工具。

逛了一圈之后,可谓是收获颇丰。

下面,就挑选几个我认为比较不错的,在日常开发场景中,也会时不时用到的 JSON 工具,给大家做下分享。

JSON 数据可视化

JSON Visio,一个开源的 JSON 数据可视化工具,可通过图表节点,完美呈现 JSON 数据间的结构关系与详情。


GitHub:https://github.com/AykutSarac/jsonvisio.com

凭借这款工具,你可以快速捕捉到 JSON 中的错误信息,搜索节点,并且,还能使用不同布局来展开 JSON 数据,让你可以更直观的看出数据间的关系。

链式操作 JSON

Dasel,一个比较实用的 JSON 命令行工具,可通过类似链式语法的方式,对 JSON、YAML、CSV 等文件进行增删改查、转换等操作。

用作者的原话说,就是当你掌握了 dasel 之后,便可以一劳永逸,在多种数据格式中,无缝切换,快速查找、修改数据。


GitHub:https://github.com/TomWright/dasel

该工具支持多种结构化数据文件,如 JSON、YAML、TOML、XML、CSV 等。

数据检索、查询

DataStation,是一款面向开发者的开源数据 IDE。

简单来说,就是可通过 SQL 语句,快速查询 JSON、CSV、Excel、日志记录库等文件中的数据,并为之创建可视化图表。

DataStation:https://github.com/multiprocessio/datastation

这款 IDE 支持 Linux、macOS、Windows 等主流操作系统,以及 18 种 SQL 和 NoSQL 数据库、文件、HTTP 服务器。

此外,作者还提供了命令行工具:DSQ,除了数据查询外,还支持多个文件合并查询,不同格式的数据源文件转化(比如将 CSV 转为 JSON)等功能。

DSQ:https://github.com/multiprocessio/dsq

在线存储 JSON

之前在 GitHub 热榜,火过一个跟 JSON 有关的开源项目,叫 JSONBox。

它能为开发者提供一个特定链接,通过向这个链接发送 HTTP 请求,可以用来存储、读取、修改 JSON 数据。

整个过程无需其他操作,完全免费,开箱即用,非常便捷。


GitHub:https://github.com/vasanthv/jsonbox

不过,我还是建议你在使用这个工具时,最好是基于自己的服务器来托管数据,这样安全性才比较有保障。

快速生成表单

通过上面几个项目,你应该能大概感知出 JSON 的灵活性与可扩展性有多强了。因此,基于这两大特点,国内有位开发者做了一款在线动态表单生成器:Form Create。

用户只需上传 JSON 数据,即可快速生成表单:


GitHub:https://github.com/xaboy/form-create

生成的表单,可具备动态渲染、数据收集、验证和提交功能等功能。另外还内置了 20 种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。

文中所提到的所有开源项目,已收录至 GitHubDaily 的开源项目列表中,有需要的,可访问下方 GitHub 地址或点击「阅读原文」查看:

GitHub:https://github.com/GitHubDaily/GitHubDaily

好了,今天的分享到此结束,感谢大家抽空阅读,我们下期再见,Respect!

来源:blog.csdn.net/sinat_33224091/article/details/124263178

收起阅读 »

复盘前端工程师必知的javascript设计模式

前言 设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是...
继续阅读 »

前言

设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是开发高质量,高可维护性,可扩展性代码的重要手段.

我们所熟知的金典的几大框架,比如jquery, react, vue内部也大量应用了设计模式, 比如观察者模式, 代理模式, 单例模式等.所以作为一个架构师,设计模式是必须掌握的.

在中高级前端工程师的面试的过程中,面试官也会适当考察求职者对设计模式的了解,所以笔者结合多年的工作经验和学习探索, 总结并画出了针对javascript设计模式的思维导图和实际案例,接下来就来让我们一起来探索习吧.

你将收获

  • 单例模式

  • 构造器模式

  • 建造者模式

  • 代理模式

  • 外观模式

  • 观察者模式

  • 策略模式

  • 迭代器模式

正文

我们先来看看总览.设计模式到底可以给我们带来什么呢?


以上笔者主要总结了几点使用设计模式能给工程带来的好处, 如代码可解耦, 可扩展性,可靠性, 条理性, 可复用性. 接下来来看看我们javascript的第一个设计模式.

1. 单例模式


1.1 概念解读

单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.

1.2 作用

  • 模块间通信

  • 保证某个类的对象的唯一性

  • 防止变量污染

1.3 注意事项

  • 正确使用this

  • 闭包容易造成内存泄漏,所以要及时清除不需要的变量

  • 创建一个新对象的成本较高

1.4 实际案例

单例模式广泛应用于不同程序语言中, 在实际软件应用中应用比较多的比如电脑的任务管理器,回收站, 网站的计数器, 多线程的线程池的设计等.

1.5 代码实现

(function(){
// 养鱼游戏
let fish = null
function catchFish() {
  // 如果鱼存在,则直接返回
  if(fish) {
    return fish
  }else {
    // 如果鱼不存在,则获取鱼再返回
    fish = document.querySelector('#cat')
    return {
      fish,
      water: function() {
        let water = this.fish.getAttribute('weight')
        this.fish.setAttribute('weight', ++water)
      }
    }
  }
}

// 每隔3小时喂一次水
setInterval(() => {
  catchFish().water()
}, 3*60*60*1000)
})()

2. 构造器模式


2.1 概念解读

构造器模式: 用于创建特定类型的对象,以便实现业务逻辑和功能的可复用.

2.2 作用

  • 创建特定类型的对象

  • 逻辑和业务的封装

2.3 注意事项

  • 注意划分好业务逻辑的边界

  • 配合单例实现初始化等工作

  • 构造函数命名规范,第一个字母大写

  • new对象的成本,把公用方法放到原型链上

2.4 实际案例

构造器模式我觉得是代码的格局,也是用来考验程序员对业务代码的理解程度.它往往用于实现javascript的工具库,比如lodash等以及javascript框架.

2.5 代码展示

function Tools(){
if(!(this instanceof Tools)){
  return new Tools()
}
this.name = 'js工具库'
// 获取dom的方法
this.getEl = function(elem) {
  return document.querySelector(elem)
}
// 判断是否是数组
this.isArray = function(arr) {
  return Array.isArray(arr)
}
// 其他通用方法...
}

3. 建造者模式


3.1 概念解读

建造者模式: 将一个复杂的逻辑或者功能通过有条理的分工来一步步实现.

3.2 作用

  • 分布创建一个复杂的对象或者实现一个复杂的功能

  • 解耦封装过程, 无需关注具体创建的细节

3.3 注意事项

  • 需要有可靠算法和逻辑的支持

  • 按需暴露一定的接口

3.4 实际案例

建造者模式其实在很多领域也有应用,笔者之前也写过很多js插件,大部分都采用了建造者模式, 可以在笔者github地址徐小夕的github学习参考. 其他案例如下:

  • jquery的ajax的封装

  • jquery插件封装

  • react/vue某一具体组件的设计

3.5 代码展示

笔者就拿之前使用建造者模式实现的一个案例:Canvas入门实战之用javascript面向对象实现一个图形验证码, 那让我们使用建造者模式实现一个非常常见的验证码插件吧!

// canvas绘制图形验证码
(function(){
  function Gcode(el, option) {
      this.el = typeof el === 'string' ? document.querySelector(el) : el;
      this.option = option;
      this.init();
  }
  Gcode.prototype = {
      constructor: Gcode,
      init: function() {
          if(this.el.getContext) {
              isSupportCanvas = true;
              var ctx = this.el.getContext('2d'),
              // 设置画布宽高
              cw = this.el.width = this.option.width || 200,
              ch = this.el.height = this.option.height || 40,
              textLen = this.option.textLen || 4,
              lineNum = this.option.lineNum || 4;
              var text = this.randomText(textLen);
   
              this.onClick(ctx, textLen, lineNum, cw, ch);
              this.drawLine(ctx, lineNum, cw, ch);
              this.drawText(ctx, text, ch);
          }
      },
      onClick: function(ctx, textLen, lineNum, cw, ch) {
          var _ = this;
          this.el.addEventListener('click', function(){
              text = _.randomText(textLen);
              _.drawLine(ctx, lineNum, cw, ch);
              _.drawText(ctx, text, ch);
          }, false)
      },
      // 画干扰线
      drawLine: function(ctx, lineNum, maxW, maxH) {
          ctx.clearRect(0, 0, maxW, maxH);
          for(var i=0; i < lineNum; i++) {
              var dx1 = Math.random()* maxW,
                  dy1 = Math.random()* maxH,
                  dx2 = Math.random()* maxW,
                  dy2 = Math.random()* maxH;
              ctx.strokeStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.beginPath();
              ctx.moveTo(dx1, dy1);
              ctx.lineTo(dx2, dy2);
              ctx.stroke();
          }
      },
      // 画文字
      drawText: function(ctx, text, maxH) {
          var len = text.length;
          for(var i=0; i < len; i++) {
              var dx = 30 * Math.random() + 30* i,
                  dy = Math.random()* 5 + maxH/2;
              ctx.fillStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.font = '30px Helvetica';
              ctx.textBaseline = 'middle';
              ctx.fillText(text[i], dx, dy);
          }
      },
      // 生成指定个数的随机文字
      randomText: function(len) {
          var source = ['a', 'b', 'c', 'd', 'e',
          'f', 'g', 'h', 'i', 'j',
          'k', 'l', 'm', 'o', 'p',
          'q', 'r', 's', 't', 'u',
          'v', 'w', 'x', 'y', 'z'];
          var result = [];
          var sourceLen = source.length;
          for(var i=0; i< len; i++) {
              var text = this.generateUniqueText(source, result, sourceLen);
              result.push(text)
          }
          return result.join('')
      },
      // 生成唯一文字
      generateUniqueText: function(source, hasList, limit) {
          var text = source[Math.floor(Math.random()*limit)];
          if(hasList.indexOf(text) > -1) {
              return this.generateUniqueText(source, hasList, limit)
          }else {
              return text
          }  
      }
  }
  new Gcode('#canvas_code', {
      lineNum: 6
  })
})();
// 调用
new Gcode('#canvas_code', {
lineNum: 6
})

4. 代理模式


4.1 概念解读

代理模式: 一个对象通过某种代理方式来控制对另一个对象的访问.

4.2 作用

  • 远程代理(一个对象对另一个对象的局部代理)

  • 虚拟代理(对于需要创建开销很大的对象如渲染网页大图时可以先用缩略图代替真图)

  • 安全代理(保护真实对象的访问权限)

  • 缓存代理(一些开销比较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前相同,则可以直接返回前面存储的运算结果)

4.3 注意事项

使用代理会增加代码的复杂度,所以应该有选择的使用代理.

实际案例

我们可以使用代理模式实现如下功能:

  • 通过缓存代理来优化计算性能

  • 图片占位符/骨架屏/预加载等

  • 合并请求/资源

4.4 代码展示

接下来我们通过实现一个计算缓存器来说说代理模式的应用.

// 缓存代理
function sum(a, b){
return a + b
}
let proxySum = (function(){
let cache = {}
return function(){
    let args = Array.prototype.join.call(arguments, ',');
    if(args in cache){
        return cache[args];
    }

    cache[args] = sum.apply(this, arguments)
    return cache[args]
}
})()

5. 外观模式


5.1 概念解读

外观模式(facade): 为子系统中的一组接口提供一个一致的表现,使得子系统更容易使用而不需要关注内部复杂而繁琐的细节.

5.2 作用

  • 对接口和调用者进行了一定的解耦

  • 创造经典的三层结构MVC

  • 在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展

  • 为大型复杂系统提供一个清晰的接口

5.3 注意事项

当外观模式被开发者连续调用时会造成一定的性能损耗,这是由于每次调用都会进行可用性检测

5.4 实际案例

我们可以使用外观模式来设计兼容不同浏览器的事件绑定的方法以及其他需要统一实现接口的方法或者抽象类.

5.5 代码展示

接下来我们通过实现一个兼容不同浏览器的事件监听函数来让大家理解外观模式如何使用.

function on(type, fn){
// 对于支持dom2级事件处理程序
if(document.addEventListener){
    dom.addEventListener(type,fn,false);
}else if(dom.attachEvent){
// 对于IE9一下的ie浏览器
    dom.attachEvent('on'+type,fn);
}else {
    dom['on'+ type] = fn;
}
}

6. 观察者模式


6.1 概念解读

观察者模式: 定义了一种一对多的关系, 所有观察对象同时监听某一主题对象,当主题对象状态发生变化时就会通知所有观察者对象,使得他们能够自动更新自己.

6.2 作用

  • 目标对象与观察者存在一种动态关联,增加了灵活性

  • 支持简单的广播通信, 自动通知所有已经订阅过的对象

  • 目标对象和观察者之间的抽象耦合关系能够单独扩展和重用

6.3 注意事项

观察者模式一般都要注意要先监听, 再触发(特殊情况也可以先发布,后订阅,比如QQ的离线模式)

6.4 实际案例

观察者模式是非常经典的设计模式,主要应用如下:

  • 系统消息通知

  • 网站日志记录

  • 内容订阅功能

  • javascript事件机制

  • react/vue等的观察者

6.5 代码展示

接下来我们我们使用原生javascript实现一个观察者模式:

class Subject {
constructor() {
  this.subs = {}
}

addSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    this.subs[key] = []
  }
  this.subs[key].push(fn)
}

trigger(key, message) {
  const subArr = this.subs[key]
  if (!subArr || subArr.length === 0) {
    return false
  }
  for(let i = 0, len = subArr.length; i < len; i++) {
    const fn = subArr[i]
    fn(message)
  }
}

unSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    return false
  }
  if (!fn) {
    this.subs[key] = []
  } else {
    for (let i = 0, len = subArr.length; i < len; i++) {
      const _fn = subArr[i]
      if (_fn === fn) {
        subArr.splice(i, 1)
      }
    }
  }
}
}

// 测试
// 订阅
let subA = new Subject()
let A = (message) => {
console.log('订阅者收到信息: ' + message)
}
subA.addSub('A', A)

// 发布
subA.trigger('A', '我是徐小夕')   // A收到信息: --> 我是徐小夕

7. 策略模式


7.1 概念解读

策略模式: 策略模式将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者.

7.2 作用

  • 实现不同, 作用一致

  • 调用方式相同,降低了使用成本以及不同算法之间的耦合

  • 单独定义算法模型, 方便单元测试

  • 避免大量冗余的代码判断,比如if else等

7.3 实际案例

  • 实现更优雅的表单验证

  • 游戏里的角色计分器

  • 棋牌类游戏的输赢算法

7.4 代码展示

接下来我们实现一个根据不同类型实现求和算法的模式来带大家理解策略模式.

const obj = {
A: (num) => num * 4,
B: (num) => num * 6,
C: (num) => num * 8
}

const getSum =function(type, num) {
return obj[type](num)
}

8. 迭代器模式


8.1 概念解读

迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示.

8.2 作用

  • 为遍历不同集合提供统一接口

  • 保护原集合但又提供外部访问内部元素的方式

8.3 实际案例

迭代器模式模式最常见的案例就是数组的遍历方法如forEach, map, reduce.

8.4 代码展示

接下来笔者使用自己封装的一个遍历函数来让大家更加理解迭代器模式的使用,该方法不仅可以遍历数组和字符串,还能遍历对象.lodash里的.forEach(collection, [iteratee=.identity])方法也是采用策略模式的典型应用.

function _each(el, fn = (v, k, el) => {}) {
// 判断数据类型
function checkType(target){
  return Object.prototype.toString.call(target).slice(8,-1)
}

// 数组或者字符串
if(['Array', 'String'].indexOf(checkType(el)) > -1) {
  for(let i=0, len = el.length; i< len; i++) {
    fn(el[i], i, el)
  }
}else if(checkType(el) === 'Object') {
  for(let key in el) {
    fn(el[key], key, el)
  }
}
}

最后

如果想了解本文完整的思维导图, 更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

来源:https://mp.weixin.qq.com/s/xTp3jY0IvXiOWBZhZ5H9fQ

收起阅读 »

前端-SSO单点登录方案

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。什么是单点登录概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录...
继续阅读 »

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。

什么是单点登录

概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录操作,自动识别登录。

为什么要选择单点登录

  • [🌰] 举个栗子 目前有一个产品,产品下有三个子项目,如果每个子项目都写一遍登录,那么后面维护的时候,开发人员需要打开三处的登录去修改同样的逻辑,这样会发生一种情况就是在改逻辑的时候,如果有另外一个bug着急修改,再回来的时候发现自己不知道改到哪了。(别问,问就是发生在我身上了!)这仅仅是站在前端开发的角度上,维护起来非常累。

  • [ ✔] 使用案例 单点登录在大型网站里使用得非常频繁,例如,阿里旗下有淘宝、天猫等网站,还有背后的成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。

话不多说直接上图

图片有点抽象,不过让我们清晰认知到单点登录要解决的就是,用户只需要登录一次就可以访问所有相互信任的应用系统。

单点登录的实现方式

单点登录的本质就是在多个应用系统中共享登录状态,所以实现单点登录的关键在于,如何让Token在多个域中共享。

1、同域下的单点登录

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。

比如我有个域名:clnct.cn,同时有三个业务系统分别为:

cpc.clnct.cn
cmk.clnct.cn
ckn.clnct.cn

我们要做单点登录(SSO),需要一个登录系统,叫做:cuc.clnct.cn。

我们只要在cuc.clnct.cn登录,cpc.clnct.cn、cmk.clnct.cn、ckn.clnct.cn也登录了。

实现方式:其实这里就是利用了 二级域名 写 一级域名的 Cookie 。cuc.clnct.cn登录以后,可以将Cookie的域设置为顶域,即.clnct,这样所有子域的系统都可以访问到顶域的Cookie。

此种实现方式比较简单,但不支持跨主域名,局限性限于一级域名是一样的。

2、不同域下的单点登录

同域下的单点登录是巧用了Cookie顶域的特性,如果是不同域呢,比如:下面三个是不同域的

cpc.dun.cn
cmk.qun.cn
ckn.nun.cn

实现方式:我们可以部署一个SSO认证中心,认证中心就是一个专门负责处理登录请求。


所有的请求(登录、退出、获取用户信息、当前用户状态)都请求sso系统,sso系统维护用户信息。

此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

逻辑分析

  • 输入用户名密码,登陆成功,接口返回token

有token,调取换code的接口。 1、接口如果获取的code值为空,清除本地的cookies,再登录;2、如果code有值,将url中的redirectUrl后拼接接口拿到的code 重定向到想要去的页面。

  • 判断域名

这里需用用到
document.domain 获取浏览器的域名

获取到浏览器域名后,匹配当前获取的域名是同域还是来自第三方系统。

如果是同域名,直接将redirectUrl返回,无需携带code接口返回的值。
如果是第三方系统,需要处理redirectUrl。因为可能用户会做登录成功再退出,那么带到登录系统的redirectUrl就会携带code值,我们需要通过js的方法去替换原来的code值。
  • 成功返回redirectUrl

一切流程通了之后,通过 location.replace(redirectUrl) 浏览器跳转返回到重定向页面

目标达成: 子系统在未登录的情况下,点击【登录】按钮,跳转到统一用户中心。统一用户中心判断当前cookies是否有token存在,如果不存在--登录;如果存在就去校验token的合法性(调取code接口),调取code接口成功,重定向到原页面。那么同域下所有的子系统,都无需登录。第三方系统进来的时候,因为做了domian的校验,因此登录成功之后,将code码放在redirectUrl,重定向到第三方系统。

总结一下

这虽然并不是最规范的SSO单点登录。但事实上比起一搜一堆概念性的文章,我认为这仅此是我个人的一种做法,至于逻辑对与错,希望大家给出合理的意见和建议,互相学习。


作者:上班摸鱼看日记
来源:https://juejin.cn/post/7088978055737114638

收起阅读 »

仿海报工厂效果的自定义View

之前做了一个自定义View,效果有些类似海报工厂,当做自定义View的入门学习吧~先看下效果图: 就是一个背景图,中间挖了若干个形状不同的“洞”,每个“洞”里放着一张图片,用手可以拖拽、缩放、旋转该图片,并且当前图片备操作时会有红色的高亮边框。点击选中某个图...
继续阅读 »

之前做了一个自定义View,效果有些类似海报工厂,当做自定义View的入门学习吧~先看下效果图:


这里写图片描述


就是一个背景图,中间挖了若干个形状不同的“洞”,每个“洞”里放着一张图片,用手可以拖拽、缩放、旋转该图片,并且当前图片备操作时会有红色的高亮边框。点击选中某个图片的时候,底部会弹出菜单栏,菜单栏有三个按钮,分别是对该图片进行旋转90度、对称翻转图片、和保存整个海报到手机内置sd卡根目录。


这就类似海报工厂效果,选择若干张图片还有底部模板(就是背景图片和挖空部分的位置和形状),然后通过触摸改变选择的图片的大小位置角度,来制作一张自己喜爱的海报。


这里主要是一个自定义View,项目中叫做JigsawView完成的。它的基本结构是最底层绘制可操作的图片,第二层绘制背景图片,第三层绘制镂空的部分,镂空部分通过PorterDuffXfermode来实现,镂空部分的形状由对应手机目录的svg文件确定。


在用Android中的Canvas进行绘图时,可以通过使用PorterDuffXfermode将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果。关于PorterDuffXfermode详细可以参考
Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解


首先这里要关掉硬件加速,因为硬件加速可能会使效果丢失。在View的初始化语句中调用


setLayerType(View.LAYER_TYPE_SOFTWARE, null);

即可。


由于JigsawView的代码不少,所以这里只展示比较重要的部分,完整代码请见文章末尾的GitHub链接。


首先需要两支画笔:


 //绘制图片的画笔
Paint mMaimPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//绘制高亮边框的画笔
Paint mSelectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

这里图片的模型是PictureModel 。PictureModel 主要都是包含了位置和缩放信息以及镂空部分的HollowModel,而图片的具体位置和大小由HollowModel确定,图片时填充镂空部分的,以类似ImageView的CenterCrop方式填充,这个在JigsawView的makePicFillHollow方法中处理。


HollowModel持有解析svg文件得到的path对象集合,该集合可以表示一个svg文件表示的路径。具体的解析工作由自定义的SvgParseUtil类处理,SvgParseUtil从手机的内置sd卡中(当然路径可以灵活配置)读取对应的svg文件,然后解析为可以绘制的Path集合对象。SvgParseUtil本质是解析xml文件(以为svg就是一个xml文件),对于svg路径直接拷贝系统的PathParser处理,其他的圆形矩形多边形就自己处理。这里具体代码这里就不展示了,详细请看GitHub上的源码。


以下是完整的onDraw方法:


 @Override
protected void onDraw(Canvas canvas) {
if (mPictureModels != null && mPictureModels.size() > 0 && mBitmapBackGround != null) {
//循环遍历画要处理的图片
for (PictureModel pictureModel : mPictureModels) {
Bitmap bitmapPicture = pictureModel.getBitmapPicture();
int pictureX = pictureModel.getPictureX();
int pictureY = pictureModel.getPictureY();
float scaleX = pictureModel.getScaleX();
float scaleY = pictureModel.getScaleY();
float rotateDelta = pictureModel.getRotate();

HollowModel hollowModel = pictureModel.getHollowModel();
ArrayList<Path> paths = hollowModel.getPathList();
if (paths != null && paths.size() > 0) {
for (Path tempPath : paths) {
mPath.addPath(tempPath);
}
drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, mPath);
} else {
drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, null);
}
}
//新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。
int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);

drawBackGround(canvas);

//循环遍历画镂空部分
for (PictureModel pictureModel : mPictureModels) {
int hollowX = pictureModel.getHollowModel().getHollowX();
int hollowY = pictureModel.getHollowModel().getHollowY();
int hollowWidth = pictureModel.getHollowModel().getWidth();
int hollowHeight = pictureModel.getHollowModel().getHeight();
ArrayList<Path> paths = pictureModel.getHollowModel().getPathList();
if (paths != null && paths.size() > 0) {
for (Path tempPath : paths) {
mPath.addPath(tempPath);
}
drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, mPath);
mPath.reset();
} else {
drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, null);
}
}

//把这个layer绘制到canvas默认的layer上去
canvas.restoreToCount(layerId);

//绘制选择图片高亮边框
for (PictureModel pictureModel : mPictureModels) {
if (pictureModel.isSelect() && mIsNeedHighlight) {
canvas.drawRect(getSelectRect(pictureModel), mSelectPaint);
}
}
}
}

思路还是比较清晰的。第3行到第22行为绘制可操作图片。第19行的drawPicture就是绘制所有的可操作图片,而当该图片对应的镂空部分没有相应的svg时,就绘制HollowModel的位置尺寸对应的矩形作为镂空部分,即20行的drawPicture。


看下drawPicture方法:


private void drawPicture(Canvas canvas, Bitmap bitmapPicture, int coordinateX, int coordinateY, float scaleX, float scaleY, float rotateDelta
, HollowModel hollowModel, Path path) {
int picCenterWidth = bitmapPicture.getWidth() / 2;
int picCenterHeight = bitmapPicture.getHeight() / 2;
mMatrix.postTranslate(coordinateX, coordinateY);
mMatrix.postScale(scaleX, scaleY, coordinateX + picCenterWidth, coordinateY + picCenterHeight);
mMatrix.postRotate(rotateDelta, coordinateX + picCenterWidth, coordinateY + picCenterHeight);
canvas.save();

//以下是对应镂空部分相交的处理,需要完善
if (path != null) {
Matrix matrix1 = new Matrix();
RectF rect = new RectF();
path.computeBounds(rect, true);

int width = (int) rect.width();
int height = (int) rect.height();

float hollowScaleX = hollowModel.getWidth() / (float) width;
float hollowScaleY = hollowModel.getHeight() / (float) height;

matrix1.postScale(hollowScaleX, hollowScaleY);
path.transform(matrix1);
//平移path
path.offset(hollowModel.getHollowX(), hollowModel.getHollowY());
//让图片只能绘制在镂空内部,防止滑动到另一个拼图的区域中
canvas.clipPath(path);
path.reset();
} else {
int hollowX = hollowModel.getHollowX();
int hollowY = hollowModel.getHollowY();
int hollowWidth = hollowModel.getWidth();
int hollowHeight = hollowModel.getHeight();
//让图片只能绘制在镂空内部,防止滑动到另一个拼图的区域中
canvas.clipRect(hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight);
}
canvas.drawBitmap(bitmapPicture, mMatrix, null);
canvas.restore();
mMatrix.reset();
}

这里主要是运用了Matrix处理图片的各种变化。在onTouchEvent方法中,会根据触摸的事件不同对正在操作的PictureModel对象的位置、缩放、角度进行对应的赋值,所以在drawPicture中将每次触摸后的赋值参数取出来,交给Matrix对象处理,最后通过


canvas.drawBitmap(bitmapPicture, mMatrix, null);

就能将触摸后的变化图片显示出来。
另外第26行的canvas.clipPath(path);是将图片的可绘制区域限定在镂空部分中,防止图片滑动到其他的镂空区域。


注意onDraw的第25行的


int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);

为了正确显示PorterDuffXfermode所带来的的效果,需要新建一个图层,具体可以参见上面链接引用的博文。


onDraw第26行的drawBackGround方法就是绘制背景,这个很简单就不必说了。


第28行到第44行为绘制镂空部分,主要是先把HollowModel中存储的Path集合取出,再通过addPath方法将路径数据交给mPath对象,再由drawHollow方法去真正绘制镂空部分。


private void drawHollow(Canvas canvas, int hollowX, int hollowY, int hollowWidth, int hollowHeight, Path path) {
mMaimPaint.setXfermode(mPorterDuffXfermodeClear);
//画镂空
if (path != null) {
canvas.save();
canvas.translate(hollowX, hollowY);
//缩放镂空部分大小使得镂空部分填充HollowModel对应的矩形区域
scalePathRegion(canvas, hollowWidth, hollowHeight, path);
canvas.drawPath(path, mMaimPaint);
canvas.restore();
mMaimPaint.setXfermode(null);
} else {
Rect rect = new Rect(hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight);
canvas.save();
canvas.drawRect(rect, mMaimPaint);
canvas.restore();
mMaimPaint.setXfermode(null);
}
}

这里首先对设置画笔的PorterDuffXfermode:


mMaimPaint.setXfermode(mPorterDuffXfermodeClear);

这里为了镂空效果,PorterDuffXfermode使用PorterDuff.Mode.CLEAR。


然后对画布进行平移,然后通过scalePathRegion方法让表示镂空路径的Path对象进行缩放,使得镂空的路径填充HollowModel对应的矩形区域。接着使用


canvas.drawRect(rect, mMaimPaint);

将镂空的路径绘制上去。


最后别忘了


canvas.restore();
mMaimPaint.setXfermode(null);

恢复画布和画笔的状态。


然后onDraw的第47行把这个layer绘制到canvas默认的layer上去:


 canvas.restoreToCount(layerId);

onDraw最后的


 //绘制选择图片高亮边框
for (PictureModel pictureModel : mPictureModels) {
if (pictureModel.isSelect() && mIsNeedHighlight) {
canvas.drawRect(getSelectRect(pictureModel), mSelectPaint);
}
}

在onTouchEvent中,将通过触摸事件判断哪个图片当前被选择,然后在onDraw中让当前被选择的图片绘制对应的HollowModel的边框。


onDraw到此结束。


再看下onTouchEvent方法:


@Override
public boolean onTouchEvent(MotionEvent event) {
if (mPictureModels == null || mPictureModels.size() == 0) {
return true;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:

//双指模式
if (event.getPointerCount() == 2) {
//mPicModelTouch为当前触摸到的操作图片模型
mPicModelTouch = getHandlePicModel(event);
if (mPicModelTouch != null) {
// mPicModelTouch.setSelect(true);
//重置图片的选中状态
resetNoTouchPicsState();
mPicModelTouch.setSelect(true);
//两手指的距离
mLastFingerDistance = distanceBetweenFingers(event);
//两手指间的角度
mLastDegree = rotation(event);
mIsDoubleFinger = true;
invalidate();
}
}
break;

//单指模式
case MotionEvent.ACTION_DOWN:
//记录上一次事件的位置
mLastX = event.getX();
mLastY = event.getY();
//记录Down事件的位置
mDownX = event.getX();
mDownY = event.getY();
//获取被点击的图片模型
mPicModelTouch = getHandlePicModel(event);
if (mPicModelTouch != null) {
//每次down重置其他picture选中状态
resetNoTouchPicsState();
mPicModelTouch.setSelect(true);
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
switch (event.getPointerCount()) {
//单指模式
case 1:
if (!mIsDoubleFinger) {
if (mPicModelTouch != null) {
//记录每次事件在x,y方向上移动
int dx = (int) (event.getX() - mLastX);
int dy = (int) (event.getY() - mLastY);
int tempX = mPicModelTouch.getPictureX() + dx;
int tempY = mPicModelTouch.getPictureY() + dy;

if (checkPictureLocation(mPicModelTouch, tempX, tempY)) {
//检查到没有越出镂空部分才真正赋值给mPicModelTouch
mPicModelTouch.setPictureX(tempX);
mPicModelTouch.setPictureY(tempY);
//保存上一次的位置,以便下次事件算出相对位移
mLastX = event.getX();
mLastY = event.getY();
//修改了mPicModelTouch的位置后刷新View
invalidate();
}
}
}
break;

//双指模式
case 2:
if (mPicModelTouch != null) {
//算出两根手指的距离
double fingerDistance = distanceBetweenFingers(event);
//当前的旋转角度
double currentDegree = rotation(event);
//当前手指距离和上一次的手指距离的比即为图片缩放比
float scaleRatioDelta = (float) (fingerDistance / mLastFingerDistance);
float rotateDelta = (float) (currentDegree - mLastDegree);

float tempScaleX = scaleRatioDelta * mPicModelTouch.getScaleX();
float tempScaleY = scaleRatioDelta * mPicModelTouch.getScaleY();
//对缩放比做限制
if (Math.abs(tempScaleX) < 3 && Math.abs(tempScaleX) > 0.3 &&
Math.abs(tempScaleY) < 3 && Math.abs(tempScaleY) > 0.3) {
//没有超出缩放比才真正赋值给模型
mPicModelTouch.setScaleX(tempScaleX);
mPicModelTouch.setScaleY(tempScaleY);
mPicModelTouch.setRotate(mPicModelTouch.getRotate() + rotateDelta);
//修改模型之后,刷新View
invalidate();
//记录上一次的两手指距离以便下次计算出相对的位置以算出缩放系数
mLastFingerDistance = fingerDistance;
}
//记录上次的角度以便下一个事件计算出角度变化值
mLastDegree = currentDegree;
}
break;
}
break;
//两手指都离开屏幕
case MotionEvent.ACTION_UP:
// for (PictureModel pictureModel : mPictureModels) {
// pictureModel.setSelect(false);
// }
mIsDoubleFinger = false;
double distance = getDisBetweenPoints(event);

if (mPicModelTouch != null) {
//是否属于滑动,非滑动则改变选中状态
if (distance < ViewConfiguration.getTouchSlop()) {
if (mPicModelTouch.isLastSelect()) {
mPicModelTouch.setSelect(false);
mPicModelTouch.setLastSelect(false);
if (mPictureCancelSelectListner != null) {
mPictureCancelSelectListner.onPictureCancelSelect();
}

} else {
mPicModelTouch.setSelect(true);
mPicModelTouch.setLastSelect(true);
//选中的回调
if (mPictureSelectListener != null) {
mPictureSelectListener.onPictureSelect(mPicModelTouch);
}
}
invalidate();
} else {
//滑动则取消所有选择的状态
mPicModelTouch.setSelect(false);
mPicModelTouch.setLastSelect(false);
//取消状态之后刷新View
invalidate();
}
} else {
//如果没有图片被选中,则取消所有图片的选中状态
for (PictureModel pictureModel : mPictureModels) {
pictureModel.setLastSelect(false);
}
//没有拼图被选中的回调
if (mPictureNoSelectListener != null) {
mPictureNoSelectListener.onPictureNoSelect();
}
//取消所有图片选中状态后刷新View
invalidate();
}
break;
//双指模式中其中一手指离开屏幕,取消当前被选中图片的选中状态
case MotionEvent.ACTION_POINTER_UP:
if (mPicModelTouch != null) {
mPicModelTouch.setSelect(false);
invalidate();
}
}
return true;
}

虽然比较长,但是并不难理解,基本是比较套路化的东西,看注释应该就能懂。


总的流程就是:
首先在Down事件:
不管单手还是双手模式,都将选择当前点击到的图片模型,这也是为了以后的事件中可以修改选中的图片模型以在onDraw中改变图片的显示。


Move事件中:
单手模式的话,针对每个MOVE事件带来的位移给PictureModel的位置赋值,然后就调用invalidate进行刷新界面。


如果是双手模式,则根据每个MOVE事件带来的角度变化和两个手指间的距离变化分别给PictureModel的角度和缩放比赋值,然后调用invalidate进行刷新界面。


Up事件:
单指模式下,先判断是否已经滑动过(滑动距离小于ViewConfiguration.getTouchSlop()就认为不是滑动而是点击),不是滑动的话就以改变当前的图片选中状态处理,切换选中状态。
是滑动过的话则取消所有图片的选中状态。


双指状态下均取消所有图片的选中状态。


这里为了使得缩放旋转体验更好,所以只要手指DOWN事件落在镂空部分中,在没有Up事件的情况下即使滑出镂空部分仍然可以继续对选中的图片进行操作,避免因为镂空部分小带来的操作不便,这也和海报工厂的效果一致。


源码地址:github.com/yanyinan/Ji…


作者:半岛铁盒里的猫
链接:https://juejin.cn/post/7084555852606242846
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter制作一个吃豆人加载Loading

知识点:绘制、动画、多状态监听 国际惯例,先看效果图: 具体效果就是吃豆人会根据吃不同颜色的豆子改变身体的颜色。 1、绘制静态吃豆人、豆豆、眼睛 首先,我们需要将这个静态的吃豆人绘制出来,我们可以把吃豆人看做是一个实心圆弧,豆豆和眼睛就是一个圆。 关键...
继续阅读 »
  • 知识点:绘制、动画、多状态监听


国际惯例,先看效果图:


fcb99a9b-0c08-43b8-ad80-e6f058cd6d58.gif



  • 具体效果就是吃豆人会根据吃不同颜色的豆子改变身体的颜色。


1、绘制静态吃豆人、豆豆、眼睛


首先,我们需要将这个静态的吃豆人绘制出来,我们可以把吃豆人看做是一个实心圆弧,豆豆和眼睛就是一个圆。

关键代码:


//画头
_paint
..color = color.value
..style = PaintingStyle.fill;
var rect = Rect.fromCenter(
center: Offset(0, 0), width: size.width, height: size.height);
/// 起始角度
var a = 40 / 180 * pi;
// 绘制圆弧
canvas.drawArc(rect, 0, 2 * pi - a * 2, true, _paint);

// 画豆豆
canvas.drawOval(
Rect.fromCenter(
center: Offset(
size.width / 2 +
ddSize -
angle2.value * (size.width / 2 + ddSize),
0),
width: ddSize,
height: ddSize),
_paint..color = color2.value);

//画眼睛
canvas.drawOval(
Rect.fromCenter(
center: Offset(0, -size.height / 3), width: 8, height: 8),
_paint..color = Colors.black87);

动画属性: 嘴巴的张合:通过圆弧的角度不断改变实现,豆豆移动:从头的右侧源源不断的有豆子向左移动,改变豆豆x轴的坐标即可,接下来我们让吃豆人动起来吧。


2、加入动画属性


这里我们需要创建2个动画控制器,一个控制头,一个控制豆豆,我们看到因为头部一开一合属于动画正向执行一次然后再反向执行一次,相当于执行了两次,豆豆的从右边到嘴巴只执行了一次,所以头的执行时间是豆豆执行时间的两倍,嘴巴一张一合才能吃豆子嘛,吃豆完毕,将豆子颜色赋值给头改变颜色,豆子随机获取另一个颜色,不断的吃豆。 这里的绘制状态有多种情况,嘴巴的张合、豆子的平移、颜色的改变都需要进行重新绘制,这里我们可以使用 Listenable.merge方法来进行监听,接受一个Listenable数组,可以将我们需要改变的状态放到这个数组里,返回一个 Listenable赋值给CustomPainter构造函数repaint属性即可,然后在监听只需判断这个Listenable即可。


factory Listenable.merge(List<Listenable?> listenables) = _MergingListenable;
复制代码

关键代码: 动画执行相关。


late Animation<double> animation; // 吃豆人
late Animation<double> animation2; // 豆豆
late AnimationController _controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 500)); //1s
late AnimationController _controller2 = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000)); //2s

//初始化吃豆人、豆豆颜色
ValueNotifier<Color> _color = ValueNotifier<Color>(Colors.yellow.shade800);
ValueNotifier<Color> _color2 =
ValueNotifier<Color>(Colors.redAccent.shade400);

// 动画轨迹
late CurvedAnimation cure = CurvedAnimation(
parent: _controller, curve: Curves.easeIn); // 动画运行的速度轨迹 速度的变化

@override
void initState() {
super.initState();
animation = Tween(begin: 0.2, end: 1.0).animate(_controller)
..addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 100-0
} else if (status == AnimationStatus.dismissed) {
_color.value = _color2.value;
// 获取一个随机彩虹色
_color2.value = getRandomColor();
_controller.forward(); //正向执行 0-100
// 豆子已经被吃了 从新加载豆子动画
_controller2.forward(from: 0); //正向执行 0-100
}
});
animation2 = Tween(begin: 0.2, end: 1.0).animate(_controller2);
// 启动动画 正向执行
_controller.forward();
// 启动动画 0-1循环执行
_controller2.forward();
// 这里这样重复调用会导致两次动画执行时间不一致 时间长了就不对应了
// _controller2.repeat();
}

@override
void dispose() {
_controller.dispose();
_controller2.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
size: Size(50, 50),
painter: Pain2Painter(
_color,
_color2,
animation,
animation2,
Listenable.merge([
animation,
animation2,
_color,
]),
ddSize: 8),
));
}

// 获取一个随机颜色
Color getRandomColor() {
Random random = Random.secure();
int randomInt = random.nextInt(6);
var colors = <Color>[
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.indigo,
Colors.purple,
];
Color color = colors[randomInt];
while (color == _color2.value) {
// 重复再选一个
color = colors[random.nextInt(6)];
}
return color;
}

绘制吃豆人源码:


class Pain2Painter extends CustomPainter {
final ValueNotifier<Color> color; // 吃豆人的颜色
final ValueNotifier<Color> color2; // 豆子的的颜色
final Animation<double> angle; // 吃豆人
final Animation<double> angle2; // 豆
final double ddSize; // 豆豆大小
final Listenable listenable;

Pain2Painter(
this.color, this.color2, this.angle, this.angle2, this.listenable,
{this.ddSize = 6})
: super(repaint: listenable);
Paint _paint = Paint();

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.translate(size.width / 2, size.height / 2);
// 画豆豆
canvas.drawOval(
Rect.fromCenter(
center: Offset(
size.width / 2 +
ddSize -
angle2.value * (size.width / 2 + ddSize),
0),
width: ddSize,
height: ddSize),
_paint..color = color2.value);
//画头
_paint
..color = color.value
..style = PaintingStyle.fill;

var rect = Rect.fromCenter(
center: Offset(0, 0), width: size.width, height: size.height);

/// 起始角度
/// angle.value 动画控制器的值 0.2~1 0是完全闭合就是 起始0~360° 1是完全张开 起始 40°~ 280° 顺时针
var a = angle.value * 40 / 180 * pi;
// 绘制圆弧
canvas.drawArc(rect, a, 2 * pi - a * 2, true, _paint);
//画眼睛
canvas.drawOval(
Rect.fromCenter(
center: Offset(0, -size.height / 3), width: 8, height: 8),
_paint..color = Colors.black87);
canvas.drawOval(
Rect.fromCenter(
center: Offset(-1.5, -size.height / 3 - 1.5), width: 3, height: 3),
_paint..color = Colors.white);
}

@override
bool shouldRepaint(covariant Pain2Painter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

至此,一个简单的吃豆人加载Loading就完成啦。再也不要到处都是菊花转的样式了。。。


总结


通过这个加载Loading动画可以重新复习下Flutter中绘制、动画的使用的联动使用、还有多状态重绘机制,通过动画还可以改变吃豆的速度和吃豆的时间运动轨迹,有兴趣可以试试哦,希望这篇文章对你有所帮助,喜欢的话点个赞再走呗~


作者:老李code
链接:https://juejin.cn/post/7088268036804706318
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

【Flutter 组件集录】Autocomplete 自动填充

简单来说,Autocomplete 意为 自动填充 。其作用就是在输入时,进行 关键字联想。在输入框下方展示列表,如下所示:注意,这是目前 Flutter 框架内部的组件,非三方组件。目前已收录入 FlutterUnit ,下面效果的源码详见之,大家可以更新查...
继续阅读 »

简单来说,Autocomplete 意为 自动填充 。其作用就是在输入时,进行 关键字联想。在输入框下方展示列表,如下所示:注意,这是目前 Flutter 框架内部的组件,非三方组件。目前已收录入 FlutterUnit ,下面效果的源码详见之,大家可以更新查看体验:















FlutterUnit 中输入时联想效果

下面是动态搜索的效果展示:





1. Autocomplete 组件最简代码


我们先一步步来了解 Autocomplete 组件,先实现如下的最简代码:



使用 Autocomplete 时,必须提供的是 optionsBuilder 参数,另外可以通过 onSelected 回调来监听选中的条目。



Autocomplete<String>(
optionsBuilder: buildOptions,
onSelected: onSelected,
)

optionsBuilder 是一个 AutocompleteOptionsBuilder<T> 类型的函数,从下面的定义中可以发现,该函数会回调 TextEditingValue 对象,且返回 FutureOr<Iterable<T>> 。这说明这个函数是一个异步函数,我们可以在此进行网络请求,数据库查询等工作,来返回一个 Iterable<T> 的可迭代对象。


用脚指头想一下也知道,这个可迭代对象,就决定这输入框下面的联想词是哪些。


final AutocompleteOptionsBuilder<T> optionsBuilder;

typedef AutocompleteOptionsBuilder<T extends Object> =
FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);



比如下面通过 searchByArgs 模拟网络请求,通过 args 参数搜索数据,


Future<Iterable<String>> searchByArgs(String args) async{
// 模拟网络请求
await Future.delayed(const Duration(milliseconds: 200));
const List<String> data = [
'toly', 'toly49', 'toly42', 'toly56',
'card', 'ls', 'alex', 'fan sha',
];
return data.where((String name) => name.contains(args));
}



这样,buildOptions 的逻辑如下,这就完成了 输入--> 搜索 --> 展示联想词 的流程。这也是 Autocomplete 组件最简单的使用。


Future<Iterable<String>> buildOptions( TextEditingValue textEditingValue ) async {
if (textEditingValue.text == '') {
return const Iterable<String>.empty();
}
return searchByArgs(textEditingValue.text);
}



2. 自定义 Autocomplete 组件内容


其实上面那样的默认样式很丑,而且没有提供 直接 的属性设置样式。所以了解如何自定义是非常关键的,否则只是一个玩具罢了。如下,我们先来实现搜索高亮显示的自定义,其中也包括对输入框的自定义。





Autocomplete 中提供了 fieldViewBuilderoptionsViewBuilder 分别用于构造输入框浮层面板



如下,代码中通过 _buildOptionsView_buildFieldView 进行相应组件构造:


Autocomplete<String>(
optionsBuilder: buildOptions,
onSelected: onSelected,
optionsViewBuilder: _buildOptionsView,
fieldViewBuilder: _buildFieldView,
);



如下是 _buildOptionsView 方法的实现,其中会回调 onSelected 回调函数,和 options 数据,我们需要做的就是依靠数据,构建组件进行展示即可。另外,默认浮层面板和输入框底部平齐,可以通过 Padding 进行下移。另外,由于是浮层,展示文字时,上面需要嵌套 Material 组件。



至于高亮某个关键字,下面是我封装的一个小方法,拿来即用。


---->[高亮某些文字]----
final TextStyle lightTextStyle = const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
);
InlineSpan formSpan(String src, String pattern) {
List<TextSpan> span = [];
List<String> parts = src.split(pattern);
if (parts.length > 1) {
for (int i = 0; i < parts.length; i++) {
span.add(TextSpan(text: parts[i]));
if (i != parts.length - 1) {
span.add(TextSpan(text: pattern, style: lightTextStyle));
}
}
} else {
span.add(TextSpan(text: src));
}
return TextSpan(children: span);
}



另外,对于输入框的构建,通过如下的 _buildFieldView 实现,其中有 _controller 记录一下 TextEditingController,是因为 optionsViewBuilder 回调中并没有回调输入的 arg 字符,所以想要输入的关键字高亮,只能出此下策。这样,在 TextFormField 构建时,你可以指定自己需要的装饰。



到此,我们就实现了上面,输入过程中,浮层面板内容关键字高亮显示的效果。




3.关于 Autocomplete 中的泛型


泛型的作用非常明显,它最主要的是对浮层面板的构建,如果浮层中的条目不止是 String ,我们就需要使用泛型,来提供某个的数据类型。比如下面的效果,其中浮层面板的条目是可以显示更多的信息:



先定义一个数据类 User ,记录信息:


class User {
final String name;
final bool man;
final String image;

const User(this.name, this.man, this.image);

@override
String toString() {
return 'User{name: $name, man: $man, image: $image}';
}
}



然后在 Autocomplete 的泛型中使用 User 即可。



这样在 _buildOptionsView 中,回调的就是 User 的可迭代对象。如下。封装一个 _UserItem 组件,对条目进行显示。





4、Autocomplete 源码简看


Autocomplete 本质上依赖于 RawAutocomplete 组件进行构建,可见它是一层简单的封装,简化使用。为我们提供了默认的 optionsViewBuilderfieldViewBuilder ,显示一个很丑的界面。也就是说,如果你了解如何定制这两部分内容,你也就会了 RawAutocomplete 组件。





我们先看一下 AutocompleteoptionsViewBuilder 提供的默认显示,其返回的是 _AutocompleteOptions 组件。如下,其实和我们自己实现的也没有太大的区别,只是个默认存在,方便使用的小玩意而已。





另外,对于输入框的构建,使用 _defaultFieldViewBuilder 静态方法完成。



该方法,返回 _AutocompleteField 组件,本质上也就是构建了一个 TextFormField 组件。









Autocomplete 来说,只是 RawAutocomplete 套了个马甲,本质上的功能还是在 RawAutocomplete 的状态类中完成的。如下是 _RawAutocompleteState 的部分代码,可以看出这里的浮层面板,是通过 Overlay 实现的,另外通过 CompositedTransformTargetCompositedTransformFollower 对浮层进行定位。



那本文就这样,如果想要简单地实现搜索联想词,Autocomplete 是一个很不错的选择。


作者:张风捷特烈
链接:https://juejin.cn/post/7087805411377545252
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »