2019就此告别2020新的征程

2019就此告别2020新的征程

[TOC]

一、学习与工作

2019年年初阔别了工作两年的大搜车,满怀新鲜感来到了学智云。至此开启了我的梦幻2019年。

1 梦幻开局·工程结构改造

初到公司接触的第一个工程就是学智云Android主工程。初见之时真是说不出的梦幻感。

  • 1 模块间使用本地相对路径依赖(导致模块间的git节点间毫无联系, 代码难回溯)
  • 2 Git仓库多了一层路径(比如项目名叫xuezhi, 它的根目录是xuezhi/xuezhi,再往里面才是build.gradlesetting.gradle等工程代码文件)
  • 3 项目中文件、代码的命名也相当混乱, 甚至有整个java/xml文件全被注释的,却不删掉,还是留在项目中
  • 4 只能提交主工程的代码, 子模块的代码要到各自的git本地仓库中提交
  • 5 其他工程结构引起的问题

鉴于这些存在的问题,我进入公司的第一件事便是,对其进行工程结构改造。首要解决的问题是代码回溯问题。

① 过渡方案(git submodule)

改造之前稍微解释一下本地相对路径依赖

改造前目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|--xuezhi(学智云Android主工程Git仓库)
| |-- xuezhi(主工程)
| | |-- module.depend(子模块依赖路径配置文件)
| | |-- app(主模块)
| | |-- build.gradle
| | |-- setting.gradle
| | |-- ...(其他工程文件)
|--bizcomp(业务模块)
|-- AndroidLearnCenter(学习中心Git仓库)
| |-- learncenter(学习中心模块-被app依赖)
|-- AndroidTeachCenter(教学中心Git仓库)
| |-- teachcenter(教学中心模块-被app依赖)
|-- Android...Center(其他业务模块Git仓库)
| |-- ...(其他业务模块-被app依赖)

其中module.depend配置文件代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 说明:
# - # 开头为注释
# - 模块必须以module开头
# - 另起一行 模块名称
# - 另起一行 模块文件路径
# - 另起一行 模块git根目录
# - 另起一行 使用的分支
# - 其他均会出错
module
app-old
app-old
../
module
learncenter
../../../bizcomp/AndroidLearnCenter/learncenter
module
teachcenter
../../../bizcomp/AndroidTeachCenter/teachcenter
# 省略部分代码

然后在setting.gradle中遍历module.depend中的文件

1
2
3
4
5
// 省略解析配置文件的代码
for(module in dependModules){
include("${module.name}")
project(":${module.name}").projectDir = file("${module.path}")
}

工程结构的问题很明显了,除了我说的难回溯的问题外,还有配置麻烦的问题。对于多人协作的项目而言, 增加和删除新的业务模块都需要每个人本地的子模块的本地路径保持一致。这个问题不复杂但是麻烦。哪有别人改了代码后我拉去了远程代码还不行, 还跟学他的本地路径配置?! 这工程结构是真的有够反人类的。回归主题,那么这个工程结构为什么会有难回溯的问题呢?我来描述一个场景。主工程和业务模块都在各自提交代码, 但是比如app发布1.0版本,此时主工程知道自己在哪个git节点,也知道各自的业务模块在哪个git节点。但是当我发布2.0版本的时候,还知道各自的业务模块在发布1.0的时候是哪个git节点吗?显然是不能的。因为业务模块的git节点与主工程之间没有任何的git约束,仅仅通过源码依赖而已。甚至于你想回到1.0的发布版本重新打1.0包都有可能运行不起来(要是找不到正确的节点的话)。只有找对了所有业务模块在app发布1.0时的各自的git节点,才能完全还原1.0的包。费劲不?我甚至不知道这样的工程结构怎么根据线上bug日志排查问题…

此时git submodule的工程结构方案应运而生。它保留了原来的工程目录结构,对工程的破坏性小, 同时又加强了主/子工程之间的git节点联系。

什么是git submodule?

子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。

使用了git submodule的工程下会生成一个.gitmodules文件, 文件内容如下:

1
2
3
4
5
6
7
8
9
[submodule "submodules/AndroidLearnCenter"]
path = submodules/AndroidLearnCenter
url = https://git.xxx.com/xxx/AndroidLearnCenter.git
branch = master
[submodule "submodules/AndroidTeachCenter"]
path = submodules/AndroidTeachCenter
url = https://git.xxx.com/xxx/AndroidTeachCenter.git
branch = master
# 省略其他模块的配置

改造后的项目的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|--xuezhi(学智云Android主工程Git仓库)
|-- submodules
| |-- AndroidLearnCenter(学习中心Git仓库)
| | |-- learncenter(学习中心模块-被app依赖)
| |-- AndroidTeachCenter(教学中心Git仓库)
| | |-- teachcenter(教学中心模块-被app依赖)
| |-- Android...Center(其他业务模块Git仓库)
| | |-- ...(其他业务模块-被app依赖)
|-- .gitmodules(子模块仓库配置文件)
|-- learncenter(软引用, 指向./submodules/AndroidLearnCenter/learncenter)
|-- teachcenter(软引用, 指向./submodules/AndroidTeachCenter/teachcenter)
|-- ...center(软引用, 指向./submodules/Android...Center/...)
|-- app(主模块)
|-- build.gradle
|-- setting.gradle
|-- ...(其他工程文件)

setting.gradle中的代码也更改为:

1
2
3
4
5
6
7
8
// 主工程
include ':app'
// 所有submodule依赖, 详情请查看.gitmodule文件
new File('./').eachFile {
if (isSymlink(it)) { // 自定义函数, 用于判断当前文件是否是软引用文件
include(":${it.getName()}")
}
}

完成改造后, 每个子模块的git记录都可以查看,也可以提交到子模块的仓库,再也不需要打开多个AndroidStudio了。

② maven私服+坐标依赖

maven私服+坐标依赖是目前的主流做法。私服其实早有搭建, 但疏于使用。在工程结构的规范化进程推动下,后期我们慢慢地将多个业务稳定的模块拆分, 打包上传到maven私服。经过几个月的努力, 所有的业务模块全都上传到了maven。git submodule结构也已经彻底移除。但在工程结构改造的路上, git submodule功不可没。

2 新技能GET·Kotlin

其实在2018年我已经使用了一段时间的Kotlin, 但并没有用于商业级项目中,只是偶尔耍耍题,小打小闹罢了。顺便给我们的仓库打个广告: RichCodersAndMe/LeetCode-Solution

自从Google在2017年的Google I/O大会上宣布Kotlin成为Android 开发的一级(first-class)编程语言后,Kotlin在社区的影响力和发展稳步上升;而后又在2019年的Google I/O大会上宣布, Android 团队将会优先提供 Kotlin 版本的 Jetpack(Kotlin first)。官方如此尽力地推广, 我们开发者还有什么理由拒绝呢?

但是商业级项目,以稳定为主,团队成员对Kotlin的掌握程度参差不齐,难免出现很多问题。那么如何优雅地过渡这个阶段,以推动Kotlin在自己团队内使用呢?

① 悬浮窗开发调试工具(Genos)

悬浮窗开发调试工具应运而生。现已开源: relish-wang/genos

由于开发调试工具仅用于开发测试环境, 并不会运用于线上, 所以可以放心地使用Kotlin开发各种各样的插件;同时各种各样的功能强大的插件也极大地提高了我们开发和调试的效率。真可谓是一举两得!秒啊!

② 纯Kotlin项目与编程思维转变

随着对Kotlin的熟悉, 越来越感受到Kotlin为我们的工作带来的效率提升以及它优良的语法设计带来的舒适的编码体验,我们开始尝试在实际项目中使用Kotlin。恰好有一个新的XX云App的开发需求, 我迎难而上接下这个开发任务, 并全程使用Kotlin编写这个App。下图是截止到现在的XX云App的Kotlin语言在工程中所占的比例:

在实际开发中其实有很多“水土不服”, 这些“水土不服”都源于我们长期对Java语言的使用而带来的惯性思维。举几个简单的例子:

  • 1 Kotlin的takeIf这个通用拓展函数配合安全调用操作符?.的使用

Java写法:

1
2
3
if(obj != null && obj.someCondition()){ 
obj.doSomeThing();
}

要是你仍然保持着Java的写法, 那这段代码转成Kotlin应该是这样:

1
2
3
if(obj?.someCondition() == true){
obj.doSomeThing()
}

如果用上takeIf函数, 这段代码应该写成:

1
obj?.takeIf{ it.someCondition() }?.doSomeThing()

条件判断语句写成了一行代码的链式的调用形式。代码简短, 逻辑清晰明了。

我们在Java中写一个DTO的话应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
private long id;
private String name;
private int age;
public User(){}
public User(long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public long getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }
}

如果你仍然保持着Java的写法, 那这段代码转成Kotlin应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User {
var id: Long = 0
private set
var name: String? = null
private set
var age = 0
private set
constructor() {}
constructor(id: Long, name: String?, age: Int) {
this.id = id
this.name = name
this.age = age
}
}

但是如果你用上数据类(data), 那应该写成这样:

1
2
3
4
5
data class User(
val id: Long = 0,
val name: String? = null,
val age: Int = 0
)

成员属性声明为val, 则对于Java调用来说只有get方法, 声明为var则有getset方法。

  • 3 自定义扩展函数

相比很多的工程中都有类似这样的工具方法:

1
DisplayUtils.dp2px(Context context, int dp)

每次调用的时候都要传入context和具体数值。Kotlin的扩展函数为这种代码提供了更加舒适的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1 声明扩展函数
fun Int.dp2px(): Int {
val reSize: Float = Utils.getContext().resources.displayMetrics.density
return (this * reSize + 0.5).toInt()
}
// 2 调用扩展函数
button.minimumHeight = 16.dp2px()
layoutParams.height = statusBar.height.dp2px()

// 再举个例子:
// View单击事件(拦截了快速多次点击)
inline fun View.setOnSingleClickListener(
crossinline onClick: (View) -> Unit
) {
this.setOnClickListener {
this.isClickable = false
onClick(it)
this.postDelayed({ this.isClickable = true }, 800L)
}
}
// 调用setOnSingleClickListener
btnLogin.setOnSingleClickListener { /* do sth.(默认入参it是onClick的View) */ }

Kotlin还有很多与Java的不同的语法习惯和编程思想:

  • 字符串内插
  • 单表达式函数
  • 对一个对象调用多个方法(with/apply)
  • 函数的默认参数

更多的Kotlin习惯用法<-点这里

③ Kotlin协程(Coroutines)

在XX云需求完成后,我转而负责学智云App的开发。有了在XX云中使用Kotlin的经验, 这次在学智云中我更加大胆地引入了Kotlin炙手可热的协程库(Coroutines).

为了体现Coroutines的语法优势这里我用Retrofit+RxJavaRetrofit+Coroutines做对比:

  • 1 串行执行两个网络请求:

判断课程是否可用, 可用的情况下拉去课程的讲师列表,并展示

Retrofit+RxJava:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1 根据课程ID判断课程是否可用
CourseApi.isCourseAvaliable(courseId)
.subscribeOn(Schedulers.io())
.flatMap(new Function<StdRespnose<Void>, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(StdRespnose<Void> response) throws Exception {
if(response.isSuccess()){
// 2.1 如果课程可用,则返回讲师列表
return CourseApi.getLecturerList(courseId);
}else{
// 2.2 如果课程不可用,则返回空的讲师列表
return Obseravle.just(Collections.<LecturerVO>emptyList())
}
}
})
.observeOn(AndroidSchedulers.mainThread())
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
.subscribe(new ObserverAdapter<List<LecturerVO>>() {
@Override
public void onNext(List<LecturerVO> lecturerVOS) {
// show lecturers
}
});

Retrofit+Coroutines:

1
2
3
4
5
6
7
8
9
10
11
// Rxjava2不允许传递null对象, 但Kotlin Coroutines可以直接在异常处退出(就像写同步代码一样)。因此两段代码的逻辑稍有不同。
lifecycleScope.launch(Dispatchers.Main) {
// 1 根据课程ID判断课程是否可用
val isAvaliable:Boolean = CourseApi.isCourseAvaliableKt(courseId)
?.body()?.data?.isAvaliable?:return@launch
val lecturerList :List<LecturerVO> = takeIf{ isAvaliable }?.apply{
// 2.1 如果课程可用,则返回讲师列表
CourseApi.getLecturerListKt(courseId)
} ?: emptyList() // 2.2 如果课程不可用,则返回空的讲师列表
//3 show lecturers
}
  • 2 接收DTO转化为VO

拉取讲师列表, 将服务端返回的DTO转化为VO进行展示

Retrofit+RxJava:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
CourseApi.getLecturerList(courseId)
.subscribeOn(Schedulers.io())
// 将网络请求传递过来的源数据转化为直接交给UI层展示的数据
.map(new Function<StdListResponse<LecturerDTO>, List<LecturerVO>>() {
@Override
public List<LecturerVO> apply(StdListResponse<LecturerDTO> listRes) throws Exception {]
try{
final List<LecturerVO> result = new ArrayList<>();
if (!listRes.isSuccess()) return result;
final StdArrayData<LecturerDTO> data = listRes.getData();
if (data == null) return result;
final List<LecturerDTO> array = data.getArray();
if (array == null) return result;
for (LecturerDTO dto : array) {
if (dto == null) continue;
// transform方法实现了“网络传输数据”转化为“UI展示数据”的过程
result.add(dto.transform());
}
return result;
} catch(Exception e){
return Collections.<LecturerVO>emptyList();
}
}
})
.observeOn(AndroidSchedulers.mainThread())
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
.subscribe(new ObserverAdapter<StdResponse<User>>() {
@Override
public void onNext(List<LecturerVO> lecturers){
// 直接接收到用于UI展示的数据
// do sth.
}
});

Retrofit+Coroutines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lifecycleScope.launch(Dispatchers.Main) {
val lecturerList: MutableList<LecturerVO> =
try{
CourseApi.getLecturerListKt(courseId)
?.body().data?.array
// transform方法实现了“网络传输数据”转化为“UI展示数据”的过程
?.map{ it.transform() } // 运用高阶函数`map`进行DTO->VO的转化
?:return emptyList()
}catch(e: Exception){
emptyList()
}
// 直接接收到用于UI展示的数据
// do sth.
}
  • 3 并行执行两个网络请求

拉去课程的课时列表并展示; 拉去课程的详情并展示

Retrofit+RxJava:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1 根据课程ID,获取课程课时目录
Observable<List<CourseCataLog>> catalogObs = CourseApi.getCataLog(courseId);
// 2 根据课程ID,获取课程详情
Observable<CourseProfile> profileObs = CourseApi.getProfile(courseId);
// 3 将二者返回的数据聚合成课程实体
Observable<Course> merge = Observable.merge(
catalogObs.flatMap(new Function<List<CourseCataLog>, ObservableSource<Course>>() {
@Override
public ObservableSource<Course> apply(List<CourseCataLog> courseCataLogs) throws Exception {
return Observable.just(new Course(courseCataLogs));
}
}),
profileObs.flatMap(new Function<CourseProfile, ObservableSource<Course>>() {
@Override
public ObservableSource<Course> apply(CourseProfile courseProfile) throws Exception {
return Observable.just(new Course(courseProfile));
}
})
);
// 4 执行合并后的Obseravle
merge.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this)))
.subscribe(new ObserverAdapter<Course>() {
@Override
public void onNext(Course course) {
// 5.1 更新profile部分UI
final CourseProfile profile = course.getProfile();
if(profile != null){ updateProfileUI(profile); }
// 5.2 更新catalog部分UI
final List<CourseCatalog> catalogs = course.getCatalogs();
if(catalogs != null){ updateCatalogUI(catalogs); }
}
});

Retrofit+Coroutines:

1
2
3
4
5
6
7
8
9
10
lifecycleScope.launch(Dispatchers.Main) {
// 1 根据课程ID,获取课程课时目录
val catalogDeferred = async { CourseApi.getCataLogKt(courseId) }
// 2 根据课程ID,获取课程详情
val profileDeferred = async { CourseApi.getProfileKt(courseId) }
// 3.1 更新profile部分UI
updateProfileUI(profileDeferred.await()?:return@launch);
// 3.2 更新catalog部分UI
updateCatalogUI(profileDeferred.await()?:return@launch);
}

不得不说Kotlin语法的简洁性, 相同的逻辑代码, 使用Kotlin Coroutines减少了一半以上的代码量。而且协程的异步代码的同步写法可读性高, 逻辑也更为清晰,实乃提供工作效率的利器!

3 软技能提升·主人翁意识

​ 工作不是完成日常需求就行,应该更多地考虑工程的稳定性,不同职能之间的协作效率的提升,如何优化开发/测试流程等等。比如,前文提到的Genos系列开发调试工具的设计,不仅方便了我们开发人员定位问题,同时极大地方便了测试人员的测试工作。再比如,需求评审会和设计评审会的功能是否有大量重复的部分,是否可以合二为一,减少重复无意义的会议,给前端/客户端开发人员留更多的时间进行开发?还有,应当自发整理项目工程文档,为工作内容留下记录,哪怕有任何紧急情况不在岗位上,也可以很好的做好交接。

​ 这些都是完成日常工作之余,值得思考的问题。提升主人翁意识,提高工作的效率,创造舒适的工作体验。

4 其他杂事

前文挑了几个值得展开讨论的要点进行了分析,除此之外2019年我还做了些什么事呢?

① 个人github

这一年疏于Github的经营, commit记录也是零散惨淡。惭愧惭愧。

② 聚慧编程(在线评测系统)

我在2018年搭建的新城中学OJ系统现已更名为聚慧编程。目前已有369名用户在此学习和训练编程语言。不过都是俊杰在打理,我就出个服务器的钱。

③ 个人博客

个人域名的博客实在没啥人气。反倒是我的CSDN博客稍稍有些气色。访问量破20w了,积分榜总排名26648。留个记录吧。交给2020年的我来赶超吧。

​ 小婷婷提醒我说:”你去年就18w访问量了, 今年才20w。你这访问量明明就是靠前几年的文章涨的,你就是吃老本。” 对对对,我是吃老本,罪过罪过。明年加油!

二、生活与情感

1 新住所与小惬意

​ 换次工作搬次家,相当折腾。这次兜兜转转搬到了我实习之初住的地方的——隔壁小区。因为我2年多前租的房子拆迁了(目瞪狗呆),小道消息据说每户平均获赔千万级别(再次目瞪狗呆)。

​ 这次虽然搬的还是单间,而且没有独卫,还是一楼,但好在房间大啊,除了房间原来就有的2.2m的大床和又大又长的靠墙衣柜(目测0.8m*2.2m*2.5m)外,还可以放下两张书桌, 一张化妆台, 一个约门高的小书架, 还有个小阳台晾衣服,剩下的面积还可以打个地铺。(咦?我为什么要打地铺?) 虽然是一楼其实并不潮湿, 因为它朝南呀。血——赚——(模仿敬汉卿说话)

​ 房东叔叔和房东阿姨人很好,还经常做菜给我们吃, 还教我们做菜。

​ 小区里有个室外游泳池,仅在暑假开放两个月。¥20/人不计时。虽然池子小,但强在离家近。

​ 把轮滑鞋带来了, 不过好像没玩几次, 就在钱塘江边滑了几次。后来买了肥宅快乐环, 也就不再出去滑了。

2 人际关系与变化

​ 原来在大搜车的不少老朋友们都四散到了各地,阿呆薪资double, 还在北京; 家🍐涨薪60%, 突破20k大关; 耗子也double了,去了蚂蚁金服。大家都有美好的前程。

​ 我也结识了一群新朋友。经常和小剑剑一起搓炉石,周五还搭他的车回家,是个聊天爱发黄图的帅小伙; 和沉默的阿岩一起来到这个新环境,临走时还没好好打招呼; 看着洋洋身上的拼劲,像看到当年的自己,小小的眼睛装着大大的疑惑。哈哈哈哈。

3 牙牙牙牙

​ 单独抽一节说说这个牙的事。你看这个牙,它又丑又歪skr

​ 牙疼不是病, 疼起来真要命。这四颗牙,没一颗好牙。尤其下面那两颗,害的我下排牙都对不齐上排的牙了。每颗牙的拔牙费用2134依次递增, 越歪的越贵,掏空了我医保的历年余额。总共大概花了¥4000+把。血——亏——。拔完牙的一周里, 只能喝粥度日, 了无生趣。以后有了孩子一定好好带他/她保护牙齿,定期检查牙齿,免得受这份罪。

4 新型冠状病毒

2019新型冠状病毒,即“2019-nCoV”, 因2019年武汉病毒性肺炎病例而被发现,2020年1月12日被世界卫生组织命名。冠状病毒是一个大型病毒家族,已知可引起感冒以及中东呼吸综合征(MERS)和严重急性呼吸综合征(SARS)等较严重疾病。新型冠状病毒是以前从未在人体中发现的冠状病毒新毒株。

出门要戴口罩了, 符合标准的口罩还买不到。回武汉的小伙伴很慌, 回家路过武汉的小伙伴也慌。听说武汉的小伙伴出门是这样的:

三、娱乐与消遣

1 动漫

① JoJo的奇幻冒险

在景1的鼓动下, 开始从第一季看《JOJO》。差点被画风劝退, 而后一发不可收拾。就连小婷婷看了之后也开始“木大木大”“平角裤平角裤”了。一口气看完了五季。坐等2020年的第六季——《石之海》。

② 我的英雄学院

要是通行百万当主角就好了,他那么努力还是拼不过主角光环的绿谷

③ 海贼王

听网友说,春节估计看不到路飞凯多, 估计要到清明节。顺便一提《狂热行动》还不错。

④ 齐木楠雄的灾难·始动篇

一口气看完了6集漫画早就完结了居然还能出动漫!听说是网飞给的钱太多了…资本的力量真牛逼…

2 游戏

Nintendo Switch

我曾拥有你,想到就心酸。

上图大部分游戏都卖了回血了。只剩下《塞尔达传说·旷野之息》和《超级马里奥·奥德赛》。

  • 舞力全开2018 ¥130
  • 喷射战士2 ¥240
  • 星之卡比 ¥254
  • 出发吧皮卡丘 ¥190
  • 马里奥赛车8 ¥284
  • 织梦岛 ¥250

回血的钱又入手了《健身环大冒险》。

稍微评价一下2019年玩的几个游戏:

① 《塞尔达传说·旷野之息》

​ 虽然是2018年年初就买了的游戏, 但我一直没玩。很奇怪,大家越是吹的厉害,我越是不想玩。后来慢慢玩下去,沉浸在海拉鲁大陆里之后,我只想说:”真香!”。旷野之息里有很多细节真的做的很棒,不注意的话不会发现。比如,快要下雨了蜻蜓会飞得很低

​ 前期一直是那种见了怪就跑, 不敢干架,到处乱跑,错过很多好装备。后来入了DLC,开始肝剑之试炼。对于我这种手残党来说,剑之试炼真是太难了,被关卡虐得自闭。好几次去网上找盾跳穿墙的教程。练了好几次,明明储存了扭曲就是穿不了墙,我也是服了。最后老老实实地干架。肝了几周总算成功开光三次。剑之试炼出来之后,干架都有了自信。直接去了岛之试炼,那叫一个爽。

​ 再后来神兽任务都做完了,就差救公主了。但我还是喜欢在这片美丽的大陆上旅旅游,采采蘑菇,放放火,炸炸鱼。

​ 在2019年6月11日的任天堂E3展上, 放出重磅消息——《塞尔达传说·旷野之息》续作正在开发中。后来又有知名游戏主播称2020年就能玩上(不知道是不是有内幕)。这个要放入2020年的规划里(嗯, 确信,没错。认真脸。)。

② 《塞尔达传说·织梦岛》

​ 说实话, 织梦岛这游戏真的配不上它的售价。剧情主线太短了,而且总是在固定的路线上走来走去,没多久就没新鲜感了(可能我被旷野之息的开放世界惯坏了吧)。通关后我就卖给Mawh了,听说后来他也没怎么玩就又卖了。哈哈哈哈。果然大家对烂作的认知都是统一的。

③ 《煮糊了2》

​ 买的电子版, 去年跟景1玩过一点点。今年跟小剑剑、景1又分别玩了一次,打到第二章了。说实话这游戏太考验配合了,一不小心就做错了,真是游戏如其名分手厨房

④ 《喷射战士2》

​ 不是喜欢的类型,已出。

⑤ 《人类一败涂地》

​ 本来以为只有跟同样买了《人类一败涂地》的Switch玩家才能一起联机玩。后来才发现可以本机两个手柄就可以双人进行游戏了。跟景1玩了一晚上,相当欢乐。

⑥ 《出发吧!皮卡丘》

​ 其实是去年(2018)年底买的,感觉一般。只能抓野怪,都不能与野怪对战,没有灵魂。打完四大天王,还没抓超梦就卖了。还在超梦的洞窟里捡了好几个大师球。

⑦ 《宝可梦·剑》

​ 比《出发吧!皮卡丘》好多了。极巨化还是挺有意思的。近期抓了只百变怪,沉迷配种中。期待春节回家和小伙伴交换宝可梦。

⑧ 《健身环大冒险》

​ 《健身环大冒险》是任天堂自主研发的,不是外面的野鸡游戏工作室做的小垃圾可以比的。网上又不少专业的健身教练评测,都是正面的评价,也就是说健身环大冒险真的可以健身。而且效果还不错,还会为你纠正姿势。

炉石传说

​ 什么炉石传说?是那个酒馆战旗的启动器吗?

​ 酒馆战旗的火爆程度都快盖过巨龙降临新版本了。反正我是记不得巨龙降临有哪些强势构筑了。每次上线,好友们都在下棋。下棋最大的乐趣就是双排。互通情报,互相py。比如,第一回合遇到小伙伴就商量好都不下怪;中后期遇到小伙伴了,开黑双方有一边鱼人快成形了但血量堪忧了,就让小伙伴站位py一下, 放点水,被小入一点点。一个吃鸡另一人还能恰烂分, 岂不美哉。但也有错误估计小伙伴的实力,饶的太多,导致提前速8的情况…这就是另一个故事了。

​ 战旗初始积分4000分。我目前战旗天梯分数6494,留个记录。来年再战。

3 电子设备

① Magic Keyboard

​ 嫌之前的Cherry入门款键盘太占桌面面积了, 就入了一块Magic Keyboard。

② Apple Watch S5

​ 本来想给小婷婷当做生日礼物的,可惜她的手机版本太低,连接不了Apple Watch S5。只好去老赵那淘了一台iPhone 5SE(这个就不单独记录了), 手表就给我自己用了。后来去Apple 西湖修电脑屏幕(有点花屏)又顺便买了一块蓝色布表带。说实话还是硅胶舒服, 而且耐脏; 布表带比较轻,但是待在手腕上不安分,滑来滑去的,系的紧了又勒手。没几天又换回硅胶表带了。

四、总结与规划

2019就此告别

Bye~

2020新的征程

​ 制定几个目标吧。目标不比总结,有明确方向和内容就行,不需要长篇大论。

① 学习目标

​ 计划为github上的kotlin项目提交代码

② 开支目标

​ 今年的支付宝年账单有够扯的,我花的居然比我挣得还多。还好我记了账不然就被它骗了。2020年计划继续保持今年的开支水平吧。毕竟2020年有更多需要花钱的地方。加油吧!

③ 游戏目标

​ 少买点游戏吧,要买也买实体版,回头转卖还能回血。之前一时脑热买了好几个电子版游戏,结果不是自己喜欢的类型…目标明确一下,除了《塞尔达传说·旷野之息》续作,其他游戏都不买了。要是续作跳票了,那也只能买一部游戏

这次话太多了, 不符合我的风格,我还是喜欢张牧之的发言:

“咳咳, 出发!”

参考资料

Kotlin中文站

本文标题:2019就此告别2020新的征程

文章作者:景三

发布时间:2020年01月19日 - 10:01

最后更新:2020年01月21日 - 17:01

原始链接:https://relish.wang/posts/62750/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
显示 Gitment 评论