前言
我曾经是Java安卓开发者,进入大学后了解并且转向了Kotlin开发安卓,时至今日已经有了一年时间,Kotlin带给我的体验实在是太惊艳了,我深信这就是我最喜欢的语言了。
抱着这种看法,我发现了Flutter+Dart这种抽象的组合,大量的嵌套好像让我在写ifelse,这导致了我迟迟没有接触Flutter跨平台框架,当然还有一些其他原因。
其实在之前Flutter的跨平台能力已经惊艳到我了,这次寒假正好有机会让我学习它。
当我试着用它完成业务时,我发现好像也不是那么不可接受 ,我甚至还有那么点快感,如果你写过安卓Compose那么你会更加觉得如此,因为在UI和业务的关系上它真的太容易绑定了,我不再考虑像XML监听数据变化,只可惜Dart语法仍然在一些地方让我感觉到不太好用,还是得Kotlin来,等等,那我不就是想要Compose吗?
哈哈,不要着急,为什么这个项目是Flutter而不是KMP随后我们再说。
其实我本身没有很严重的技术癖,面对新的事物和技术,一旦有合适的机会我都是愿意试一试,比起框架的选择,我更加享受开发过程,思想转换为代码的那一刻至少我是享受的。
这次选择Flutter开发不意味着我会一直选择和追捧它,更不会放弃安卓原生和KMP的学习,因此也希望阅读这篇文章读者意识到这点,我作为原生开发者学习Flutter是希望扩展技能而不是代替原生,Flutter本身也不是这么想的,它更像是给大家了一个更低的开发门槛,让更多其他领域的创作者完成他们作品的一种媒介。
如果你希望快速了解Kotlin开发者使用Dart的开发体验,那么直接跳过下面两部分,直接阅读#错怪的Dart。
动机
我觉得主要动机由两部分组成吧,一部分是跨平台开发本身是我感兴趣的方向之一,另一边是未来工作可能需要吧,现在来看国内跨平台趋势还是比较明显的。
不过我更希望这次项目是体验 移动跨平台开发,而不是真正的深入学习 移动跨平台开发。为此,我希望可以找到学习成本和项目质量相平衡的开发方式,很遗憾我没有那么多的精力做到既要还要,这是我必须面临的选择。
面对众多跨平台框架下我还是选择了Flutter,这主要与它的跨桌面端和生态完善有关,毫无疑问,Flutter有许多的成品组件,这让我可以快速轻松的上手跨平台开发。
为什么是Flutter
这个项目的主要功能就是播放器,只不过这个播放器比较特殊,后续文章我们会揭晓它。
单就网络音频播放器 开发任务而言,假设使用KMP可能没有现成封装好的库来给我用,可能许多开发者考虑没有就造一个,很遗憾,我不太具备这样的能力,我们需要同时对接多个平台的媒体播放,无论开发周期,单就这样的任务对我已经是很难了。
好吧,我想必须承认我很菜,但是事实如此,因此我选择了更加成熟的Flutter,避免我写不出来,哈哈哈哈。
不过我们今天先不谈Flutter,我们看看Dart。
错怪的Dart
对Dart的刻板印象是从我第一次见到Flutter的语法时形成的,第一次见到Dart时我还没有接触Kotlin。
看着有点像Java,还有好多_的名字是什么鬼东西、怎么要写这么多return、为什么有个?、总之就是反人类啊!!!
当我真正尝试去编写Flutter程序时,我发现,嗯,错怪Dart了,特别是因为我了解Kotlin后,Kotlin和Dart也有几分相似之处,这体现在一些语法特性上。
空安全
可空类型在Kotlin上可以说相当不错,在Dart上也可以体验到它,虽然它是类型前置,但是写法倒是一样的在类型后加上"?"即可。
dart
class AudioMediaItem {
String title;
String description;
AudioMediaType type;
String? mediaUrl;
String? bvId;
//省略其他代码.....
}
当我们试图使用AudioMediaItem的对象时,我们就可以像Kotlin那样做,注意mediaUrl现在是可空的。
audioMediaItem?.mediaUrl
,如果我们认为这个属性一定有值,那么就可以使用audioMediaItem!.mediaUrl
,需要注意的是,dart中是"!"而不是"!!"。
如果你希望使用Kotlin的Elvis操作符 ?: ,那么你可以这么做
dart
audioMediaItem?.mediaUrl ?? "default";
对应Kotlin的
kotlin
audioMediaItem?.mediaUrl ?: "default"
在这方面,dart和Kotlin是非常相似的,因此,你可以非常平滑的迁移这部分的开发体验和理解。
延迟初始化
在Kotlin中,我们可以使用lateinit var
定义一个非空延迟初始化的变量,通俗的讲就是定义一个非空类型,但是不给初始值。dart也有对应从关键字,那就是late
了。
dart
late String name;
相当于Kotlin的
kotlin
lateinit var String name
我们知道延迟初始化意味着这个值必定有值,只是我们希望这个值在代码运行过程中产生并且初始化,初始化后再使用该值,否则就会空指针了。
如果你已经熟悉了Kotlin的lateinit,那这里也可以平滑迁移了。
但是在Android Studio 2023.1.1我发现个有意思的事情。
dart
late String? name;
ide没有提示这是错误的,我没试着运行,但是我觉得这应该是不合理的。
扩展函数
扩展函数在Kotlin当中可以说相当重要,许多内置函数都是这个特性所带来的。
在Kotlin中,我们通过 被扩展的类名.扩展函数名(){} 这样的写法就实现了一个扩展函数。
kotlin
fun String.toColorInt(): Int = Color.parseColor(this)
Dart中也存在扩展函数的语法糖!
dart
extension StringExtension on String {
/// 将字符串的首字母大写
String capitalize() {
if (isEmpty) {
return this;
}
return '${this[0].toUpperCase()}${substring(1)}';
}
}
其中,StringExtension只是这个扩展的名字,相当于一个标志,可以随便起,on String 则代表扩展String类,那么capitalize 自然就是扩展的方法名了。
将Kotlin的内置函数带入
Kotlin的内置函数实在是太棒了,下面以also和let为例子,模仿了Kotlin的扩展函数,只可惜Dart的lambda不太能像Kotlin那样,还是有一些割裂。
dart
extension AlsoExtension<T> on T {
T also(void Function(T) block) {
block(this);
return this;
}
}
extension LetExtension<T> on T {
R let<R>(R Function(T) block) {
return block(this);
}
}
//用法
String demo = "xada".let((it) => "${it}xadadawdwad");
emm不过因为没办法直接传this,在变量很长或者类型可空时还有点用。
顶层函数
Kotlin中,我们有时候需要在全局使用一些函数,但是不希望写在类里,而是随时随地直接可以调用或者拿到。
注意这些代码不在类里
kotlin
val json = Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
var retrofit = Retrofit.Builder()
.baseUrl("https://api.juejin.cn/")
.addConverterFactory(json.asConverterFactory(MediaType.parse("application/json;charset=utf-8")!!))
.build()
在某个类需要我们就直接写retrofit.xxxx() 就可以了,我们不需要再单独从类中找。
Dart也有这样的功能
dart
final _cookieJar = CookieJar();
final Dio dioClient = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
contentType: Headers.jsonContentType,
persistentConnection: true,
))
..transformer = BackgroundTransformer()
..let((it) {
if (!kIsWeb) {
it.interceptors.add(CookieManager(_cookieJar));
return it;
} else {
return it;
}
});
上面的例子只是写了变量,写函数也是一样的,都可以直接在全局任何的位置调用。
高阶函数
在Kotlin中,高阶函数是特殊的一种函数,这种函数接受了另一个函数作为参数。
我们以Kotlin的forEach函数为例子:
kotlin
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
// 用法
fun main() {
val demoList = listOf("da", "da", "da")
demoList.forEach {
println(it)
}
}
forEach本身扩展了Iterable,但是它的参数非常特殊,我们看看action参数的类型:
(T) -> Unit,这是Kotlin匿名函数的写法,意味着这个函数有一个参数,类型为T泛型,这个参数也没有起名字,所以就只有类型T在。
这种情况,在Java中这种实现一般是接口类,我们需要实例化这个匿名类,假设这个接口只有一个方法,那么就可以转换为lambda的写法。
在Kotlin里我们可以直接写为lambda的形式,要方便很多,由于只有一个参数,那么kotlin默认就叫it了。
OK回顾完Kotlin,我们看看Dart:
dart
void forEach(void action(E element)) {
for (E element in this) action(element);
}
//用法
List<String> demoList = ["da","da","da"];
demoList.forEach((element) {
print(element);
});
其实差别不大,只是我们需要写void当作这个参数的类型,内部写法没有太大差异。
不过,Dart的lambda更加贴近JS,写法基本上是一模一样。
相信如果你已经掌握了Kotlin的高阶函数,那么在Dart尝试也是不错的。
运算符重载
Kotlin当中有个不太常用的东西,叫运算符重载,它在Dart中也有。
kotlin
public operator fun <T> Collection<T>.plus(elements: Iterable<T>): List<T> {
if (elements is Collection) {
val result = ArrayList<T>(this.size + elements.size)
result.addAll(this)
result.addAll(elements)
return result
} else {
val result = ArrayList<T>(this)
result.addAll(elements)
return result
}
}
//用法
val demoList = listOf("da", "da", "da") + listOf<String>("add")
可以看到kotlin通过operator关键字配合扩展函数实现了这个功能,dart也可以模仿这种手段:
dart
// 模仿
extension ListPlusOperatorExtension<T> on List<T> {
List<T> operator +(List<T> elements) {
List<T> result = this;
addAll(elements);
return result;
}
}
// 用法
List<String> demo1 = ["da","da"];
List<String> demo2 = ["da","d1a"] + demo1;
不过这里的加减乘除就是operator + 了。
总结
可以看得出,Dart也有部分我们在Kotlin中喜欢的特性,如果你已经掌握了Kotlin的基本语法,那么相信Dart对你来说也不是太大问题,你可以平滑的迁移一些在Kotlin中的知识到Dart上去。
起初我是很坑距使用Flutter的,现在看见Dart的特性,我似乎又接受了一些,好吧,对于Flutter开发、布局约束和其他感受我在下一篇文章再分享给大家吧。
最后感谢大家看到这里,还有什么好玩的特性欢迎在下面留言,文章内容有错误请指出。