使用 Gemini 读代码和写代码

我发现将我电脑上 VPN 的地区切换一下,是可以在 Android Studio 中使用 Gemini 的。这几天体验了一下 Gemini,试用过后的感悟是:代码能力是场较量的话,人类肯定打不过 AI。我们作为普通人,如果打不过,那就加入吧。

读代码

读代码是个比较费脑的过程,尤其是那种有一定规模、有一定年头的项目。即便一开始我们忽略不必要细节、只是从整体上去理解这个项目,比如核心功能、整体架构、关键流程、主要类/接口/数据结构,仍然需要时间和精力。

没有 AI 之前,我习惯找一条关键流程去看代码,边看边画类图和时序图,一来加深理解,二来避免很快就遗忘。但我不敢说这个过程就真的很轻松。我在想,如果有人提前就帮我准确地画出我想要类图和时序图,我是不是就能更轻松地读代码了?当然,这些图最好还能轻松地调整。第一个原因是代码会更新,所以图也必须能同步更新;第二个原因是关注点会变,比如一开始我们并不关注异常处理和次要分支,所以可以忽略它们,但随着对代码理解的加深,这些细节可能会变得不容忽视。

有了 AI,我的想法就变成了现实。以 DeckPicker.onDecksLoaded() 这个方法为例,它对应着这个界面中列表部分的数据加载过程。从界面上看这个过程应该并不复杂,但实际上在 AnkiDroid 项目它涉及到非常多的代码。

如果没有工具的辅助,想一头扎进代码堆中去理清头绪,还真得费点功夫。有了 Gemini,一切变得不可思议地简单。我只需要从选定的这处代码(第12行)向 Gemini 提问即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
private fun onDecksLoaded(
result: DeckNode,
collectionHasNoCards: Boolean,
) {
Timber.i("Updating deck list UI")
hideProgressBar()
// Make sure the fragment is visible
if (fragmented) {
invalidateOptionsMenu()
studyoptionsFrame!!.visibility = if (collectionHasNoCards) View.GONE else View.VISIBLE
}
dueTree = result
}

我的第一个问题是:

generate a sequence diagram to show me how the data dueTree is fetched and displayed

这是 Gemini 生成的答案:用 mermaid 代码表示的时序图。这个代码粘贴到支持 mermaid 的编辑器中会显示成一个时序图。只花了两三秒,完全准确无误!唯一不足的是这个图过于详细,有些我暂时不太关注的细节。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
sequenceDiagram
participant DP as DeckPicker
participant CM as CollectionManager
participant S as Scheduler
participant D as Decks
participant DA as DeckAdapter
participant UI as UserInterface

Note over DP, UI: User interacts with DeckPicker
DP->>CM: isOpenUnsafe()
alt Collection is open
CM-->>DP: true
DP->>DP: performBackupInBackground()
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.deckDueTree()
activate S
S->>D: Access deck data
activate D
D-->>S: Deck data
deactivate D
S-->>DP: DeckNode (dueTree)
deactivate S
DP->>DP: onDecksLoaded(dueTree, collectionHasNoCards)
DP->>UI: hideProgressBar()
alt fragmented
DP->>UI: invalidateOptionsMenu()
UI->>UI: studyoptionsFrame!!.visibility = if (collectionHasNoCards) View.GONE else View.VISIBLE
end
DP->>DP: dueTree = result
DP->>DP: launchCatchingTask { renderPage(collectionHasNoCards) }
DP->>DP: renderPage(collectionHasNoCards)
alt tree is null
DP->>DP: updateDeckList()
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.deckDueTree()
activate S
S->>D: Access deck data
activate D
D-->>S: Deck data
deactivate D
S-->>DP: DeckNode (dueTree)
deactivate S
DP->>DP: onDecksLoaded(dueTree, collectionHasNoCards)
DP->>UI: hideProgressBar()
alt fragmented
DP->>UI: invalidateOptionsMenu()
UI->>UI: studyoptionsFrame!!.visibility = if (collectionHasNoCards) View.GONE else View.VISIBLE
end
DP->>DP: dueTree = result
DP->>DP: launchCatchingTask { renderPage(collectionHasNoCards) }
DP->>DP: renderPage(collectionHasNoCards)
end
alt isEmpty
DP->>UI: deckPickerContent.visibility = View.GONE
UI->>UI: noDecksPlaceholder.visibility = View.VISIBLE
DP->>UI: supportActionBar?.subtitle = null
DP->>DA: submit(emptyList(), false, -1)
DA->>UI: Update UI with empty list
else not isEmpty
DP->>UI: deckPickerContent.visibility = View.VISIBLE
UI->>UI: noDecksPlaceholder.visibility = View.GONE
DP->>DA: submit(tree.filterAndFlatten(currentFilter), tree.children.any { it.children.any() }, withCol { decks.current().getLong("id") })
DA->>UI: Update UI with filtered and flattened data
DP->>UI: supportActionBar?.subtitle = ...
DP->>DP: scrollDecklistToDeck(current)
DP->>DP: viewModel.focusedDeck = current
end
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.studiedToday()
activate S
S-->>DP: studiedToday()
deactivate S
DP->>UI: reviewSummaryTextView.text = ...
UI->>UI: Update review summary
else Collection is not open
CM-->>DP: false
end

我暂时只聚集核心流程,不关注异常处理和空数据处理。所以我的第二个问题是:

Remove some unnecessary details like false condition and exception handling from the diagram to make it more clear

Gemini 马上帮我移除了几个 false 条件分支以及 null 处理分支。这是简化后的时序图:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
sequenceDiagram
participant DP as DeckPicker
participant CM as CollectionManager
participant S as Scheduler
participant D as Decks
participant DA as DeckAdapter
participant UI as UserInterface

Note over DP, UI: User interacts with DeckPicker
DP->>CM: isOpenUnsafe()
alt Collection is open
CM-->>DP: true
DP->>DP: performBackupInBackground()
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.deckDueTree()
activate S
S->>D: Access deck data
activate D
D-->>S: Deck data
deactivate D
S-->>DP: DeckNode (dueTree)
deactivate S
DP->>DP: onDecksLoaded(dueTree, collectionHasNoCards)
DP->>UI: hideProgressBar()
alt fragmented
DP->>UI: invalidateOptionsMenu()
UI->>UI: studyoptionsFrame!!.visibility = if (collectionHasNoCards) View.GONE else View.VISIBLE
end
DP->>DP: dueTree = result
DP->>DP: launchCatchingTask { renderPage(collectionHasNoCards) }
DP->>DP: renderPage(collectionHasNoCards)
alt tree is null
DP->>DP: updateDeckList()
Note over DP: Recursive call to fetch dueTree
else not null
alt isEmpty
DP->>UI: deckPickerContent.visibility = View.GONE
UI->>UI: noDecksPlaceholder.visibility = View.VISIBLE
DP->>DA: submit(emptyList(), false, -1)
DA->>UI: Update UI with empty list
else not isEmpty
DP->>UI: deckPickerContent.visibility = View.VISIBLE
UI->>UI: noDecksPlaceholder.visibility = View.GONE
DP->>DA: submit(tree.filterAndFlatten(currentFilter), tree.children.any { it.children.any() }, withCol { decks.current().getLong("id") })
DA->>UI: Update UI with filtered and flattened data
DP->>UI: supportActionBar?.subtitle = ...
DP->>DP: scrollDecklistToDeck(current)
DP->>DP: viewModel.focusedDeck = current
end
end
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.studiedToday()
activate S
S-->>DP: studiedToday()
deactivate S
DP->>UI: reviewSummaryTextView.text = ...
UI->>UI: Update review summary
end

这个图仍然有些琐碎。我继续让 Gemini 去掉不必要的细节,第三个问题是:

It’s not fragmented and the tree always has data.  Continue to remove these unnecessary details from the diagram.

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
35
36
37
38
39
40
41
42
sequenceDiagram
participant DP as DeckPicker
participant CM as CollectionManager
participant S as Scheduler
participant D as Decks
participant DA as DeckAdapter
participant UI as UserInterface

Note over DP, UI: User interacts with DeckPicker
DP->>CM: isOpenUnsafe()
alt Collection is open
CM-->>DP: true
DP->>DP: performBackupInBackground()
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.deckDueTree()
activate S
S->>D: Access deck data
activate D
D-->>S: Deck data
deactivate D
S-->>DP: DeckNode (dueTree)
deactivate S
DP->>DP: onDecksLoaded(dueTree, collectionHasNoCards)
DP->>UI: hideProgressBar()
DP->>DP: dueTree = result
DP->>DP: launchCatchingTask { renderPage(collectionHasNoCards) }
DP->>DP: renderPage(collectionHasNoCards)
DP->>UI: deckPickerContent.visibility = View.VISIBLE
UI->>UI: noDecksPlaceholder.visibility = View.GONE
DP->>DA: submit(tree.filterAndFlatten(currentFilter), tree.children.any { it.children.any() }, withCol { decks.current().getLong("id") })
DA->>UI: Update UI with filtered and flattened data
DP->>UI: supportActionBar?.subtitle = ...
DP->>DP: scrollDecklistToDeck(current)
DP->>DP: viewModel.focusedDeck = current
DP->>DP: launchCatchingTask { ... }
DP->>S: sched.studiedToday()
activate S
S-->>DP: studiedToday()
deactivate S
DP->>UI: reviewSummaryTextView.text = ...
UI->>UI: Update review summary
end

这个代码对应时序图如下。这个图足够我围绕着数据加载和显示流程来看代码了。

写代码

看 AnkiDroid 代码时发现了 AudioWaveform 这个类。代码并不复杂,看名字也能推测出其功能,但我还是想看看实际运行效果。不过模拟器上不能真正地录音,所以看到的波形图是一条直线。

一种办法使用真机来效果(但我也懒得接上真机,因为开发速度大部分时候不如模拟器快),另一种办法是让 Gemini 给我们加点代码生成模拟数据。当然,我们也可以自己生成模拟数据,但是为了让数据接近真实效果,可能需要相关的数学或物理知识。这可能不是几分钟的事儿。

我向 Gemini 提的问题是:

Generate some random waveform data to display in AudioWaveform and make the data seem like it’s from a human. The value ranges from 42 to 2100

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
35
36
fun generateWaveformFloats(
count: Int,
min: Float = 42f,
max: Float = 2100f,
baseFrequency: Float = 0.1f, // Adjust for overall wave speed
amplitudeVariation: Float = 0.5f, // Adjust for how much the amplitude changes
frequencyVariation: Float = 0.2f // Adjust for how much the frequency changes
): List<Float> {
require(min < max) { "Min value must be less than max value" }
require(count > 0) { "Count must be greater than 0" }

val waveform = mutableListOf<Float>()
val random = Random(System.currentTimeMillis()) // Seed for more varied results

var currentAmplitude = (max - min) / 2f // Start in the middle
var currentFrequency = baseFrequency

for (i in 0 until count) {
// Introduce some randomness to amplitude and frequency
currentAmplitude += (random.nextFloat() - 0.5f) * (max - min) * amplitudeVariation
currentFrequency += (random.nextFloat() - 0.5f) * baseFrequency * frequencyVariation

// Clamp amplitude to stay within bounds
currentAmplitude = currentAmplitude.coerceIn(min, max)

// Generate a sine wave value
val sineValue = sin(i * currentFrequency)

// Scale and shift the sine wave to fit the desired range
val scaledValue = (sineValue + 1) / 2 * (currentAmplitude - min) + min

waveform.add(scaledValue.toFloat())
}

return waveform
}

Gemini 提供了一个基于正弦函数生成随机数的代码。但我发现调用这个函数还有些工作量,所以又要求 Gemini 补充代码以方便我调用。

Generate code to make it easy to get value from the data based on auto increased index

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
object AudioSamples {
private var waveformData: List<Float> = emptyList()
private var currentIndex: Int = 0

fun initialize(
count: Int,
min: Float = 42f,
max: Float = 2100f,
baseFrequency: Float = 0.1f,
amplitudeVariation: Float = 0.5f,
frequencyVariation: Float = 0.2f
) {
waveformData = generateWaveformFloats(count, min, max, baseFrequency, amplitudeVariation, frequencyVariation)
currentIndex = 0
}

fun getNextSample(): Float {
if (waveformData.isEmpty()) {
throw IllegalStateException("AudioSamples not initialized. Call initialize() first.")
}
if (currentIndex >= waveformData.size) {
currentIndex = 0 // Loop back to the beginning
}
return waveformData[currentIndex++]
}

fun reset() {
currentIndex = 0
}
fun isInitialized(): Boolean {
return waveformData.isNotEmpty()
}
}

有些 Gemini 生成的这些代码,我现在只需要简单地调整 AudioRecordingController 类第721行代码就能用模拟数据展示 waveform 波形了。

这是模拟数据的效果。(你看得出这是假数据吗?)

更新


我重新看了代码,发现一个更好的想法。我们可以让 generateWaveformFloats() 返回 Sequence 以方便读取数据(可避免索引方式从列表获取数据这种繁琐做法),代码清爽很多。

在 Android Studio 选中 generateWaveformFloats 方法,右键点击然后选择菜单中的 “Transform Selected Code…” (快捷键是 Command + ),输入提示词 “Make it to a Sequence”,Gemini 更新原有代码,等我确认。我提示 Gemini 可以进一步优化,去掉 count 参数。

优化后的调用代码(第5行)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override fun onAudioTick() {
try {
if (isRecording) {
val maxAmplitude = waveformData.first()//audioRecorder.maxAmplitude() / 10
audioWaveform.addAmplitude(maxAmplitude.toFloat())
}
} catch (e: IllegalStateException) {
Timber.d(e, "Audio recorder interrupted")
}
}

companion object {
var isRecording = false
// 生成的模拟数据, 一个 Sequence
var waveformData = generateWaveformFloats()
}
  • 第5行直接从 waveformData Sequence 中取数据,无需索引
  • 第15行使用新的 generateWaveformFloats() 函数来初始化 waveformData

以上是个既有些无聊的例子却又能展示 AI 强大代码能力的例子。

总结

最后是 Gemini 的一个小细节。它直接告诉我代码中 Color.rgb(244, 81, 30) 是橙色,Color.rgb(33, 150, 243) 是蓝色,Color.argb(20, 229, 228, 226) 是半透明的灰色。

不得不感叹:代码能力是场较量的话,人类肯定打不过 AI。我们作为普通人,如果打不过,那就加入吧。还好,从谷歌推出 Gemini 预览版到现在,我最多只比先行者晚了十几个月而已。

参考