-
Notifications
You must be signed in to change notification settings - Fork 1
/
atom.xml
562 lines (326 loc) · 574 KB
/
atom.xml
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>柘个角落</title>
<icon>https://www.gravatar.com/avatar/bd80dd1283cd0ce8323c050b40cb6f38</icon>
<subtitle>IF YOU WANT SOMETHING, GO GET IT. PERIOD.</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://lilei.pro/"/>
<updated>2020-06-10T00:36:32.642Z</updated>
<id>https://lilei.pro/</id>
<author>
<name>Li Lei</name>
<email>[email protected]</email>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>AndroidWeekly#416 学习笔记</title>
<link href="https://lilei.pro/2020/06/10/AndroidWeekly-416/"/>
<id>https://lilei.pro/2020/06/10/AndroidWeekly-416/</id>
<published>2020-06-10T00:32:22.000Z</published>
<updated>2020-06-10T00:36:32.642Z</updated>
<content type="html"><![CDATA[<p><a href="https://androidweekly.net/issues/issue-416" target="_blank" rel="noopener">https://androidweekly.net/issues/issue-416</a> </p><h2 id="Designing-and-Working-with-Single-View-States-on-Android"><a href="#Designing-and-Working-with-Single-View-States-on-Android" class="headerlink" title="Designing and Working with Single View States on Android"></a>Designing and Working with Single View States on Android</h2><p><a href="https://zsmb.co/designing-and-working-with-single-view-states-on-android/" target="_blank" rel="noopener">https://zsmb.co/designing-and-working-with-single-view-states-on-android/</a></p><p><strong>ViewState</strong>是用于标识View状态的对象,通过ViewState,可以将View的显示逻辑与控制逻辑抽离。在MVI、MVVM等设计模式中都可以看到它的身影。</p><h3 id="用例:加载联系人信息"><a href="#用例:加载联系人信息" class="headerlink" title="用例:加载联系人信息"></a>用例:加载联系人信息</h3><p>这个页面有三个状态:Loading、Loaded、Error。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200610_andoridweekly416/loading_loaded_error.png" alt="loading_loaded_error.png" title=""> </div> <div class="image-caption">loading_loaded_error.png</div> </figure><h3 id="共用data类"><a href="#共用data类" class="headerlink" title="共用data类"></a>共用data类</h3><ul><li>优点:类数目最少</li><li>缺点:会出现诸如<code>errored=true, loading=true</code>的异常状态</li></ul><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">ProfileViewState</span></span>(</span><br><span class="line"> <span class="keyword">val</span> errored: <span class="built_in">Boolean</span> = <span class="literal">false</span>,</span><br><span class="line"> <span class="keyword">val</span> loading: <span class="built_in">Boolean</span> = <span class="literal">false</span>,</span><br><span class="line"> <span class="keyword">val</span> name: String? = <span class="literal">null</span>,</span><br><span class="line"> <span class="keyword">val</span> email: String? = <span class="literal">null</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="Sealed-classes(密封类)"><a href="#Sealed-classes(密封类)" class="headerlink" title="Sealed classes(密封类)"></a>Sealed classes(密封类)</h3><ul><li>优点:防止进入错误状态</li><li>缺点:无法共用数据</li></ul><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">sealed</span> <span class="class"><span class="keyword">class</span> <span class="title">ProfileViewState</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">object</span> Loading : ProfileViewState()</span><br><span class="line"></span><br><span class="line"><span class="keyword">object</span> Error : ProfileViewState()</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">ProfileLoaded</span></span>(</span><br><span class="line"> <span class="keyword">val</span> name: String,</span><br><span class="line"> <span class="keyword">val</span> email: String</span><br><span class="line">) : ProfileViewState()</span><br></pre></td></tr></table></figure><p><code>View</code>类里面的绘制方法如下,注意<code>exhaustive</code>扩展函数能够在我们的<code>when</code>语句漏写某个分支时,在编译过程就抛出异常。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">render</span><span class="params">(viewState: <span class="type">ProfileViewState</span>)</span></span> {</span><br><span class="line"> <span class="keyword">when</span> (viewState) {</span><br><span class="line"> Loading -> {</span><br><span class="line"> viewFlipper.displayedChild = Flipper.LOADING</span><br><span class="line"> }</span><br><span class="line"> Error -> {</span><br><span class="line"> viewFlipper.displayedChild = Flipper.ERROR</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">is</span> ProfileLoaded -> {</span><br><span class="line"> viewFlipper.displayedChild = Flipper.CONTENT</span><br><span class="line"> profileNameText.text = viewState.name</span><br><span class="line"> profileEmailText.text = viewState.email</span><br><span class="line"> }</span><br><span class="line"> }.exhaustive</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>ViewFlipper</code>可以用来展示多个Children中的一个:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ProfileFragment</span> : <span class="type">Fragment</span></span>() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">object</span> Flipper {</span><br><span class="line"> const <span class="keyword">val</span> LOADING = <span class="number">0</span></span><br><span class="line"> const <span class="keyword">val</span> CONTENT = <span class="number">1</span></span><br><span class="line"> const <span class="keyword">val</span> ERROR = <span class="number">2</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ... </span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>布局文件:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">ViewFlipper</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/viewFlipper"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">layout</span>=<span class="string">"@layout/profile_loading"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">layout</span>=<span class="string">"@layout/profile_content"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">layout</span>=<span class="string">"@layout/profile_error"</span> /></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">ViewFlipper</span>></span></span><br></pre></td></tr></table></figure><h3 id="共享数据,独立状态"><a href="#共享数据,独立状态" class="headerlink" title="共享数据,独立状态"></a>共享数据,独立状态</h3><p>如列表下滑加载更多,已经获取到的item仍然显示在界面上。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200610_andoridweekly416/load_more.png" alt="load_more.png" title=""> </div> <div class="image-caption">load_more.png</div> </figure><p>使用<code>data class</code>来做ViewState。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// data class</span></span><br><span class="line"><span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">ListViewState</span></span>(</span><br><span class="line"> <span class="keyword">val</span> isLoading: <span class="built_in">Boolean</span>,</span><br><span class="line"> <span class="keyword">val</span> items: List<String></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// View.java</span></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">render</span><span class="params">(viewState: <span class="type">ListViewState</span>)</span></span> {</span><br><span class="line"> progressBar.isVisible = viewState.isLoading</span><br><span class="line"> wordAdapter.submitList(viewState.items)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 增加Error状态</span></span><br><span class="line"><span class="keyword">sealed</span> <span class="class"><span class="keyword">class</span> <span class="title">ListViewState</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">object</span> Error: ListViewState()</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">ListReady</span></span>(</span><br><span class="line"> <span class="keyword">val</span> isLoading: <span class="built_in">Boolean</span>,</span><br><span class="line"> <span class="keyword">val</span> items: List<String></span><br><span class="line">): ListViewState()</span><br></pre></td></tr></table></figure><h3 id="更加复杂的UI"><a href="#更加复杂的UI" class="headerlink" title="更加复杂的UI"></a>更加复杂的UI</h3><p>对于包含更多元素的UI,在<code>when</code>语句之前要将共用逻辑抽出,以简化<code>when</code>代码块。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200610_andoridweekly416/upload_sample.png" alt="upload_sample.png" title=""> </div> <div class="image-caption">upload_sample.png</div> </figure><p>复杂写法,每个分支里处理所有UI元素,容易遗漏,维护困难:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">render</span><span class="params">(viewState: <span class="type">UploadViewState</span>)</span></span> {</span><br><span class="line"> <span class="keyword">when</span> (viewState) {</span><br><span class="line"> Initial -> {</span><br><span class="line"> uploadProgressText.isVisible = <span class="literal">false</span></span><br><span class="line"> progressBar.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadDoneIcon.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadStatusText.isVisible = <span class="literal">false</span></span><br><span class="line"> retryUploadButton.isVisible = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">is</span> UploadInProgress -> {</span><br><span class="line"> uploadProgressText.isVisible = <span class="literal">true</span></span><br><span class="line"> progressBar.isVisible = <span class="literal">true</span></span><br><span class="line"> uploadDoneIcon.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadStatusText.isVisible = <span class="literal">false</span></span><br><span class="line"> retryUploadButton.isVisible = <span class="literal">false</span></span><br><span class="line"> progressBar.setProgressWithAnimation(viewState.percentage.toFloat())</span><br><span class="line"> uploadProgressText.text = <span class="string">"<span class="subst">${viewState.percentage}</span>%"</span></span><br><span class="line"> }</span><br><span class="line"> UploadFailed -> {</span><br><span class="line"> uploadProgressText.isVisible = <span class="literal">false</span></span><br><span class="line"> progressBar.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadDoneIcon.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadStatusText.isVisible = <span class="literal">true</span></span><br><span class="line"> uploadStatusText.text = <span class="string">"Sorry, something went wrong."</span></span><br><span class="line"> retryUploadButton.isVisible = <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line"> UploadSuccess -> {</span><br><span class="line"> uploadProgressText.isVisible = <span class="literal">false</span></span><br><span class="line"> progressBar.isVisible = <span class="literal">false</span></span><br><span class="line"> uploadDoneIcon.isVisible = <span class="literal">true</span></span><br><span class="line"> uploadStatusText.isVisible = <span class="literal">true</span></span><br><span class="line"> uploadStatusText.text = <span class="string">"Upload complete!"</span></span><br><span class="line"> retryUploadButton.isVisible = <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> }.exhaustive</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>简化写法,提取各个状态专有UI元素,优先处理;<code>when</code>语句只处理共享UI元素:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">render</span><span class="params">(viewState: <span class="type">UploadViewState</span>)</span></span> {</span><br><span class="line"> uploadProgressText.isVisible = viewState <span class="keyword">is</span> UploadInProgress</span><br><span class="line"> progressBar.isVisible = viewState <span class="keyword">is</span> UploadInProgress</span><br><span class="line"> retryUploadButton.isVisible = viewState <span class="keyword">is</span> UploadFailed</span><br><span class="line"> uploadDoneIcon.isVisible = viewState <span class="keyword">is</span> UploadSuccess</span><br><span class="line"> uploadStatusText.isVisible = </span><br><span class="line"> viewState <span class="keyword">is</span> UploadFailed || viewState <span class="keyword">is</span> UploadSuccess</span><br><span class="line"></span><br><span class="line"> <span class="keyword">when</span> (viewState) {</span><br><span class="line"> Initial -> {</span><br><span class="line"> <span class="comment">// Empty</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">is</span> UploadInProgress -> {</span><br><span class="line"> progressBar.setProgressWithAnimation(viewState.percentage.toFloat())</span><br><span class="line"> uploadProgressText.text = <span class="string">"<span class="subst">${viewState.percentage.toInt()}</span>%"</span></span><br><span class="line"> }</span><br><span class="line"> UploadFailed -> {</span><br><span class="line"> uploadStatusText.text = <span class="string">"Sorry, something went wrong."</span></span><br><span class="line"> }</span><br><span class="line"> UploadSuccess -> {</span><br><span class="line"> uploadStatusText.text = <span class="string">"Upload complete!"</span></span><br><span class="line"> }</span><br><span class="line"> }.exhaustive</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="使用-ConstraintLayout-Groups-简化"><a href="#使用-ConstraintLayout-Groups-简化" class="headerlink" title="使用 ConstraintLayout Groups 简化"></a>使用 ConstraintLayout Groups 简化</h3><p>一个<code>Group</code>可以同时控制多个View的可见性。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">androidx.constraintlayout.widget.Group</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/errorViews"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:constraint_referenced_ids</span>=<span class="string">"errorTitle, errorIcon, errorMessage"</span> /></span></span><br></pre></td></tr></table></figure><h2 id="Easy-Android-Scopes"><a href="#Easy-Android-Scopes" class="headerlink" title="Easy Android Scopes"></a>Easy Android Scopes</h2><p><a href="https://ryanharter.com/blog/2020/03/easy-android-scopes/" target="_blank" rel="noopener">https://ryanharter.com/blog/2020/03/easy-android-scopes/</a></p><p>介绍Activity、Fragment发生Configuration Changed事件时的数据保存方法,类似AndroidX中的ViewModel。</p><p>作者实现了自己的一套<code>Scoped Delegate</code>方案。</p><h2 id="Jetpack-ViewModel-and-string-resources"><a href="#Jetpack-ViewModel-and-string-resources" class="headerlink" title="Jetpack ViewModel and string resources"></a>Jetpack ViewModel and string resources</h2><p><a href="https://www.rockandnull.com/android-viewmodel-resources/" target="_blank" rel="noopener">https://www.rockandnull.com/android-viewmodel-resources/</a></p><p>在使用ViewModel时,作者试图将ViewModel实现与Android解耦,以便ViewModel可以无缝迁移到其它平台(如iOS、Web)。文中提供了两种思路。</p><h3 id="方案一:使用ResourceId"><a href="#方案一:使用ResourceId" class="headerlink" title="方案一:使用ResourceId"></a>方案一:使用ResourceId</h3><p>将资源id作为LiveData进行发送,而非字符串本身。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ViewModel.java</span></span><br><span class="line"><span class="keyword">val</span> errorMessageLiveData = MutableLiveData<<span class="built_in">Int</span>>()</span><br><span class="line">[...]</span><br><span class="line">errorMessageLiveData.value = R.string.error_message_1</span><br><span class="line"></span><br><span class="line"><span class="comment">// Activity.java</span></span><br><span class="line">errorMessageLiveData.observe(<span class="keyword">this</span>, Observer {</span><br><span class="line"> Toast.makeText(context, getString(it), Toast.LENGTH_LONG).show() </span><br><span class="line"> }</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h3 id="方案二:使用资源工厂类(Resources-Helper)"><a href="#方案二:使用资源工厂类(Resources-Helper)" class="headerlink" title="方案二:使用资源工厂类(Resources Helper)"></a>方案二:使用资源工厂类(Resources Helper)</h3><p>该类用于抽象不同平台的资源获取方式。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ResourcesHelper</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> applicationContext: Context) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> errorMessage</span><br><span class="line"> <span class="keyword">get</span>() = applicationContext.getString(R.string.error_message)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>将该类注入(inject)到<code>ViewModel</code>中,可以使其独立于不同平台实现,并且能够进行mock/fake。</p><h3 id="在ViewModel中使用Context"><a href="#在ViewModel中使用Context" class="headerlink" title="在ViewModel中使用Context"></a>在ViewModel中使用Context</h3><p>使用<code>AndroidViewModel</code>代替<code>ViewModel</code>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyViewModel</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> application: Application) : AndroidViewModel(application) {</span><br><span class="line"> <span class="keyword">val</span> errorMessage</span><br><span class="line"> <span class="keyword">get</span>() = application.resources.getString(R.string.some_string)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种写法有一个问题是,<a href="https://medium.com/androiddevelopers/locale-changes-and-the-androidviewmodel-antipattern-84eb677660d9" target="_blank" rel="noopener">用户切换语言无法生效</a>。以及在测试时必须人工提供一个Context对象。</p><p>## </p>]]></content>
<summary type="html">
<p><a href="https://androidweekly.net/issues/issue-416" target="_blank" rel="noopener">https://androidweekly.net/issues/issue-416</a> </p>
<
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Android Weekly" scheme="https://lilei.pro/tags/Android-Weekly/"/>
</entry>
<entry>
<title>麻雀虽小,五脏俱全——下载库PRDownloader源码解析</title>
<link href="https://lilei.pro/2020/06/01/prdownloader/"/>
<id>https://lilei.pro/2020/06/01/prdownloader/</id>
<published>2020-06-01T00:09:02.000Z</published>
<updated>2020-06-01T00:11:27.258Z</updated>
<content type="html"><![CDATA[<blockquote><ul><li>You don’t even know me!</li><li>I have the rest of my life to find out.</li></ul></blockquote><h2 id="前言,为什么选择研究下载框架"><a href="#前言,为什么选择研究下载框架" class="headerlink" title="前言,为什么选择研究下载框架"></a>前言,为什么选择研究下载框架</h2><p>从2020年4月份,自己开始做外销游戏中心模块后,就一直在关注Android平台下载功能的实现思路与方法。优秀的开源下载框架并不多,在GitHub上可以找到以下几个:</p><ul><li><a href="https://github.com/lingochamp/FileDownloader" target="_blank" rel="noopener">FileDownloader</a>,是流利说团队开源的一款下载框架,支持多线程、分块、断点续传等功能。已进化为新项目<a href="https://github.com/lingochamp/okdownload" target="_blank" rel="noopener">OKDownload</a>,旧的FileDownloader不再维护。</li><li><a href="https://github.com/lingochamp/okdownload" target="_blank" rel="noopener">OKDownload</a>,同样是流利说团队出品,是FileDownloader的进化版,在原有基础上进行全方位优化,并增加了大量的自动化测试。</li><li><a href="https://github.com/MindorksOpenSource/PRDownloader" target="_blank" rel="noopener">PRDownloader</a>,是印度的一个开源项目团队<a href="https://github.com/MindorksOpenSource" target="_blank" rel="noopener">MindOrks</a>出品的下载框架。支持多任务、断点续传,不支持分块下载。相比于前面两个流利说的框架,逻辑更为简单清晰,适合进行学习研究。</li></ul><h2 id="表面,接入和使用PRDownloader"><a href="#表面,接入和使用PRDownloader" class="headerlink" title="表面,接入和使用PRDownloader"></a>表面,接入和使用PRDownloader</h2><p>把自吹自擂的东西抛到一边,一个框架做的好不好,很重要一点是接入它以及使用起来是否方便。接入这点没什么好说的,通过gradle引入即可,目前最新的版本号为<code>0.6.0</code>,尚不是稳定版本。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">implementation 'com.mindorks.android:prdownloader:0.6.0'</span><br></pre></td></tr></table></figure><p>同时在manifest文件里声明访问网络的权限:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.INTERNET"</span>/></span></span><br></pre></td></tr></table></figure><p>接下来是使用部分,第一步是在<code>Application.onCreate()</code>中进行初始化,使用默认构造参数即可:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PRDownloader.initialize(getApplicationContext());</span><br></pre></td></tr></table></figure><p>也可进行定制,定制项为是否使用数据库(支持进程重启后恢复下载)、读取超时、连接超时等:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Enabling database for resume support even after the application is killed:</span></span><br><span class="line">PRDownloaderConfig config = PRDownloaderConfig.newBuilder()</span><br><span class="line"> .setDatabaseEnabled(<span class="keyword">true</span>)</span><br><span class="line"> .build();</span><br><span class="line">PRDownloader.initialize(getApplicationContext(), config);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Setting timeout globally for the download network requests:</span></span><br><span class="line">PRDownloaderConfig config = PRDownloaderConfig.newBuilder()</span><br><span class="line"> .setReadTimeout(<span class="number">30_000</span>)</span><br><span class="line"> .setConnectTimeout(<span class="number">30_000</span>)</span><br><span class="line"> .build();</span><br><span class="line">PRDownloader.initialize(getApplicationContext(), config);</span><br></pre></td></tr></table></figure><p>进行完初始化后,就可以在Activity里面调用下载功能了。</p><h3 id="启动下载"><a href="#启动下载" class="headerlink" title="启动下载"></a>启动下载</h3><p>使用静态方法<code>PRDownloader.download(url, dirPath, fileName)</code>进行下载,会返回一个<code>downloadId</code>,该id用于后续的暂停、恢复、获取状态等操作。在启动下载时,也可设置下载进度、成功、失败监听。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> downloadId = PRDownloader.download(url, dirPath, fileName)</span><br><span class="line"> .build()</span><br><span class="line"> .setOnStartOrResumeListener(<span class="keyword">new</span> OnStartOrResumeListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onStartOrResume</span><span class="params">()</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .setOnPauseListener(<span class="keyword">new</span> OnPauseListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPause</span><span class="params">()</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .setOnCancelListener(<span class="keyword">new</span> OnCancelListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onCancel</span><span class="params">()</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .setOnProgressListener(<span class="keyword">new</span> OnProgressListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onProgress</span><span class="params">(Progress progress)</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .start(<span class="keyword">new</span> OnDownloadListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onDownloadComplete</span><span class="params">()</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onError</span><span class="params">(Error error)</span> </span>{</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><h3 id="暂停和恢复下载"><a href="#暂停和恢复下载" class="headerlink" title="暂停和恢复下载"></a>暂停和恢复下载</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 暂停</span></span><br><span class="line">PRDownloader.pause(downloadId);</span><br><span class="line"><span class="comment">// 恢复</span></span><br><span class="line">PRDownloader.resume(downloadId);</span><br></pre></td></tr></table></figure><h3 id="取消下载"><a href="#取消下载" class="headerlink" title="取消下载"></a>取消下载</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Cancel with the download id</span></span><br><span class="line">PRDownloader.cancel(downloadId);</span><br><span class="line"><span class="comment">// The tag can be set to any request and then can be used to cancel the request</span></span><br><span class="line">PRDownloader.cancel(TAG);</span><br><span class="line"><span class="comment">// Cancel all the requests</span></span><br><span class="line">PRDownloader.cancelAll();</span><br></pre></td></tr></table></figure><h3 id="获取下载状态"><a href="#获取下载状态" class="headerlink" title="获取下载状态"></a>获取下载状态</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Status status = PRDownloader.getStatus(downloadId);</span><br></pre></td></tr></table></figure><h3 id="清空临时文件"><a href="#清空临时文件" class="headerlink" title="清空临时文件"></a>清空临时文件</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Method to clean up temporary resumed files which is older than the given day</span></span><br><span class="line">PRDownloader.cleanUp(days);</span><br></pre></td></tr></table></figure><h2 id="原理部分"><a href="#原理部分" class="headerlink" title="原理部分"></a>原理部分</h2><h3 id="初始化部分,PRDownloader-initialize"><a href="#初始化部分,PRDownloader-initialize" class="headerlink" title="初始化部分,PRDownloader.initialize()"></a>初始化部分,PRDownloader.initialize()</h3><p>PRDownloader维护了一个全局的下载器,在开始下载前需要进行初始化,代码位于<code>PRDownloader.java</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">(Context context, PRDownloaderConfig config)</span> </span>{</span><br><span class="line"> ComponentHolder.getInstance().init(context, config);</span><br><span class="line"> DownloadRequestQueue.initialize();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>ComponentHolder</code>明显是一个单例,其中维护了以下变量:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> readTimeout; <span class="comment">// 网络读取超时</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> connectTimeout; <span class="comment">// 网络连接超时</span></span><br><span class="line"><span class="keyword">private</span> String userAgent; <span class="comment">// UA</span></span><br><span class="line"><span class="keyword">private</span> HttpClient httpClient;</span><br><span class="line"><span class="keyword">private</span> DbHelper dbHelper;</span><br></pre></td></tr></table></figure><p>我们可以对这些变量自由配置,或者使用<code>PRDownloaderConfig.newBuilder().build()</code>的默认实现,配置对象位于类<code>PRDownloaderConfig.java</code>当中,基于构建器模式。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Builder</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> readTimeout = Constants.DEFAULT_READ_TIMEOUT_IN_MILLS;</span><br><span class="line"> <span class="keyword">int</span> connectTimeout = Constants.DEFAULT_CONNECT_TIMEOUT_IN_MILLS;</span><br><span class="line"> String userAgent = Constants.DEFAULT_USER_AGENT;</span><br><span class="line"> HttpClient httpClient = <span class="keyword">new</span> DefaultHttpClient();</span><br><span class="line"> <span class="keyword">boolean</span> databaseEnabled = <span class="keyword">false</span>;</span><br><span class="line"> <span class="comment">// 以下省略无关代码</span></span><br></pre></td></tr></table></figure><p>复杂一些的对象是网络<code>HttpClient</code>和数据库<code>DBHelper</code>,下面单独进行解读。</p><h4 id="HttpClient"><a href="#HttpClient" class="headerlink" title="HttpClient"></a>HttpClient</h4><p>封装了网络请求的具体实现,定义接口<code>HttpClient</code>,并提供了默认实现<code>DefaultHttpClient</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// HttpClient.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">HttpClient</span> <span class="keyword">extends</span> <span class="title">Cloneable</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function">HttpClient <span class="title">clone</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">connect</span><span class="params">(DownloadRequest request)</span> <span class="keyword">throws</span> IOException</span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">getResponseCode</span><span class="params">()</span> <span class="keyword">throws</span> IOException</span>;</span><br><span class="line"></span><br><span class="line"> <span class="function">InputStream <span class="title">getInputStream</span><span class="params">()</span> <span class="keyword">throws</span> IOException</span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">long</span> <span class="title">getContentLength</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function">String <span class="title">getResponseHeader</span><span class="params">(String name)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">close</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"> Map<String, List<String>> getHeaderFields();</span><br><span class="line"></span><br><span class="line"> <span class="function">InputStream <span class="title">getErrorStream</span><span class="params">()</span> <span class="keyword">throws</span> IOException</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>默认的实现采用了<code>URLConnection</code>方案,基于这种封装设计,可以无缝切换到<code>OkHttp</code>等实现,出于篇幅考虑,这里不再贴出<a href="https://github.com/MindorksOpenSource/PRDownloader/blob/master/prdownloader/src/main/java/com/downloader/httpclient/DefaultHttpClient.java" target="_blank" rel="noopener">DefaultHttpClient</a>具体实现。</p><h4 id="DBHelper"><a href="#DBHelper" class="headerlink" title="DBHelper"></a>DBHelper</h4><p>同样采用了接口设计,主要操作对象为<code>DownloadModel</code>,有CRUD功能,此外还有<code>updateProgress</code>方法,用以更新下载进度。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DBHelper.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">DbHelper</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function">DownloadModel <span class="title">find</span><span class="params">(<span class="keyword">int</span> id)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">insert</span><span class="params">(DownloadModel model)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">update</span><span class="params">(DownloadModel model)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">updateProgress</span><span class="params">(<span class="keyword">int</span> id, <span class="keyword">long</span> downloadedBytes, <span class="keyword">long</span> lastModifiedAt)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">remove</span><span class="params">(<span class="keyword">int</span> id)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function">List<DownloadModel> <span class="title">getUnwantedModels</span><span class="params">(<span class="keyword">int</span> days)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">clear</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// DownloadModel.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DownloadModel</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> id; <span class="comment">// 唯一id</span></span><br><span class="line"> <span class="keyword">private</span> String url; <span class="comment">// 下载地址</span></span><br><span class="line"> <span class="keyword">private</span> String eTag; <span class="comment">// Response中的ETag字段</span></span><br><span class="line"> <span class="keyword">private</span> String dirPath; <span class="comment">// 下载目录</span></span><br><span class="line"> <span class="keyword">private</span> String fileName; <span class="comment">// 保存的文件名</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> totalBytes; <span class="comment">// 总大小</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> downloadedBytes; <span class="comment">// 已下载大小,每次同步下载进度时会更新该字段</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">long</span> lastModifiedAt; <span class="comment">// 更新时间,每次同步下载进度时会更新该字段</span></span><br><span class="line"> <span class="comment">// 以下无关getter/setter代码省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以上是<code>PRDownloader</code>默认的全局配置,可以看到除了定义一些网络请求常量外,比较重要的是维护了两个单例:HttpClient和DBHelper。在<code>PRDownloader.initialize()</code>方法里,它还对<code>DownloadRequestQueue</code>进行了初始化,我们继续分析这部分代码。</p><h4 id="DownloadRequestQueue"><a href="#DownloadRequestQueue" class="headerlink" title="DownloadRequestQueue"></a>DownloadRequestQueue</h4><p>它也是单例实现,是一个全局下载任务队列,其初始化函数调用了构造方法,初始化内部的请求Map、序列号发生器。请求Map用以在全局维护下载任务,Key为下载任务id,Value为下载任务。序列号发生器用来给每个任务生成唯一的序列id,作用是提供任务排序依据。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DownloadRequestQueue.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DownloadRequestQueue</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> DownloadRequestQueue instance;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Map<Integer, DownloadRequest> currentRequestMap;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> AtomicInteger sequenceGenerator;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="title">DownloadRequestQueue</span><span class="params">()</span> </span>{</span><br><span class="line"> currentRequestMap = <span class="keyword">new</span> ConcurrentHashMap<>();</span><br><span class="line"> sequenceGenerator = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">initialize</span><span class="params">()</span> </span>{</span><br><span class="line"> getInstance();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> DownloadRequestQueue <span class="title">getInstance</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">synchronized</span> (DownloadRequestQueue.class) {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="keyword">null</span>) {</span><br><span class="line"> instance = <span class="keyword">new</span> DownloadRequestQueue();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> instance;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h3 id="提交下载任务,PRDownloader-download-url-dirPath-fileName"><a href="#提交下载任务,PRDownloader-download-url-dirPath-fileName" class="headerlink" title="提交下载任务,PRDownloader.download(url, dirPath, fileName)"></a>提交下载任务,PRDownloader.download(url, dirPath, fileName)</h3><p>完成初始化以后,就可以使用PRDownloader的各项功能,以下载为例探究其内部的实现。</p><p>启动下载的入口是<code>PRDownloader.download(url, dirPath, fileName)</code>,启动流程为 构建下载任务->设置下载回调->启动下载任务。</p><h4 id="构建下载任务"><a href="#构建下载任务" class="headerlink" title="构建下载任务"></a>构建下载任务</h4><p>DownloadRequest的构建同样采用构建器模式,入参为下载链接、存储的路径和文件名。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// PRDownloader.java</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> DownloadRequestBuilder <span class="title">download</span><span class="params">(String url, String dirPath, String fileName)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> DownloadRequestBuilder(url, dirPath, fileName);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// DownloadRequestBuilder.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DownloadRequestBuilder</span> <span class="keyword">implements</span> <span class="title">RequestBuilder</span> </span>{</span><br><span class="line"></span><br><span class="line"> String url;</span><br><span class="line"> String dirPath;</span><br><span class="line"> String fileName;</span><br><span class="line"> Priority priority = Priority.MEDIUM;</span><br><span class="line"> Object tag;</span><br><span class="line"> <span class="keyword">int</span> readTimeout;</span><br><span class="line"> <span class="keyword">int</span> connectTimeout;</span><br><span class="line"> String userAgent;</span><br><span class="line"> HashMap<String, List<String>> headerMap;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DownloadRequestBuilder</span><span class="params">(String url, String dirPath, String fileName)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.url = url;</span><br><span class="line"> <span class="keyword">this</span>.dirPath = dirPath;</span><br><span class="line"> <span class="keyword">this</span>.fileName = fileName;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 以下省略无关代码</span></span><br></pre></td></tr></table></figure><p><code>DownloadRequestBuilder</code>是<code>DownloadRequest</code>的构建器,主要起作用的属性只有3个:url、dirPath、fileName,其它属性与PRDownloaderConfig类里面的重复了,并没有使用到。</p><p><code>DownloadRequest</code>是一个下载任务的呈现,是一个动态的东西,它跟<code>DownloadModel</code>的区别在于:</p><ul><li>DownloadModel:是静态的,记录任务在某个时间点的状态(id,地址链接,总大小,已下载大小,更新时间),用于持久化,并且能从持久化数据源里恢复</li><li>DownloadRequestBuilder:是动态的,不仅包含<code>DownloadModel</code>,还有下载时的各种回调(OnProgressListener、OnDownloadListener、OnStartListener等),此外还有重要的一个字段<code>downloadId</code>,是下载任务的唯一标识符</li></ul><h4 id="设置下载回调"><a href="#设置下载回调" class="headerlink" title="设置下载回调"></a>设置下载回调</h4><p>下载过程中有多种回调:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 开始下载回调</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnStartOrResumeListener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onStartOrResume</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 暂停回调</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnPauseListener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onPause</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 下载取消回调</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnCancelListener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onCancel</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 下载完成/失败回调,叫onFinishListener更好</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnDownloadListener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onDownloadComplete</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onError</span><span class="params">(Error error)</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 下载进度回调</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnProgressListener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onProgress</span><span class="params">(Progress progress)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以在<code>DownloadRequest</code>对象上调用<code>setXXXListener()</code>方法,用以设置下载过程中的回调。设置完成之后,回调是如何发生作用的呢?这就到了【开始下载】的逻辑里。</p><h4 id="开始下载"><a href="#开始下载" class="headerlink" title="开始下载"></a>开始下载</h4><p>调用<code>DownloadRequest.start</code>方法开始下载任务。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DownloadRequest.java</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">start</span><span class="params">(OnDownloadListener onDownloadListener)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.onDownloadListener = onDownloadListener;</span><br><span class="line"> downloadId = Utils.getUniqueId(url, dirPath, fileName);</span><br><span class="line"> DownloadRequestQueue.getInstance().addRequest(<span class="keyword">this</span>);</span><br><span class="line"> <span class="keyword">return</span> downloadId;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>关键的一句命令在<code>DownloadRequestQueue.getInstance().addRequest(this)</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DownloadRequestQueue.java</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addRequest</span><span class="params">(DownloadRequest request)</span> </span>{</span><br><span class="line"> currentRequestMap.put(request.getDownloadId(), request);</span><br><span class="line"> request.setStatus(Status.QUEUED);</span><br><span class="line"> request.setSequenceNumber(getSequenceNumber());</span><br><span class="line"> request.setFuture(Core.getInstance()</span><br><span class="line"> .getExecutorSupplier()</span><br><span class="line"> .forDownloadTasks()</span><br><span class="line"> .submit(<span class="keyword">new</span> DownloadRunnable(request)));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>addRequest</code>的过程,是提交下载任务到执行队列的过程。<code>Core.getInstance().getExecutorSupplier().forDownloadTasks()</code>获取到了一个线程池,该线程池用以执行下载任务。</p><p><code>Core.java</code>是单例,持有一个<code>ExecutorSupplier</code>的实现,后者提供三种Executor,分别用于下载、后台任务、UI任务。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Core.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Core</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> Core instance = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ExecutorSupplier executorSupplier;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="title">Core</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.executorSupplier = <span class="keyword">new</span> DefaultExecutorSupplier();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Core <span class="title">getInstance</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">synchronized</span> (Core.class) {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="keyword">null</span>) {</span><br><span class="line"> instance = <span class="keyword">new</span> Core();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> instance;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> ExecutorSupplier <span class="title">getExecutorSupplier</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> executorSupplier;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">shutDown</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (instance != <span class="keyword">null</span>) {</span><br><span class="line"> instance = <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ExecutorSupplier.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ExecutorSupplier</span> </span>{</span><br><span class="line"> <span class="function">DownloadExecutor <span class="title">forDownloadTasks</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function">Executor <span class="title">forBackgroundTasks</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function">Executor <span class="title">forMainThreadTasks</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// DefaultExecutorSupplier.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultExecutorSupplier</span> <span class="keyword">implements</span> <span class="title">ExecutorSupplier</span> </span>{</span><br><span class="line"> <span class="comment">// 下载线程池大小,内核数*2+1</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> DEFAULT_MAX_NUM_THREADS = <span class="number">2</span> * Runtime.getRuntime().availableProcessors() + <span class="number">1</span>;</span><br><span class="line"> <span class="comment">// 下载Executor</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> DownloadExecutor networkExecutor;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Executor backgroundExecutor;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Executor mainThreadExecutor;</span><br><span class="line"></span><br><span class="line"> DefaultExecutorSupplier() {</span><br><span class="line"> ThreadFactory backgroundPriorityThreadFactory = <span class="keyword">new</span> PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND);</span><br><span class="line"> networkExecutor = <span class="keyword">new</span> DownloadExecutor(DEFAULT_MAX_NUM_THREADS, backgroundPriorityThreadFactory);</span><br><span class="line"> <span class="comment">// 后台任务单线程Executor</span></span><br><span class="line"> backgroundExecutor = Executors.newSingleThreadExecutor();</span><br><span class="line"> <span class="comment">// 主线程Executor</span></span><br><span class="line"> mainThreadExecutor = <span class="keyword">new</span> MainThreadExecutor();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> DownloadExecutor <span class="title">forDownloadTasks</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> networkExecutor;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Executor <span class="title">forBackgroundTasks</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> backgroundExecutor;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Executor <span class="title">forMainThreadTasks</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> mainThreadExecutor;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>Executor.submit()</code>返回一个<code>Future</code>对象,将其保存在<code>DownloadRequest</code>中,用来进行取消任务。</p><p>具体的下载逻辑,在<code>DownloadRunnable</code>中。</p><h3 id="DownloadRunnable,下载具体实现"><a href="#DownloadRunnable,下载具体实现" class="headerlink" title="DownloadRunnable,下载具体实现"></a>DownloadRunnable,下载具体实现</h3><p>从名字上就可以看出它是一个用来提交给Executor的Runnable,关键方法<code>run()</code>是耗时操作,阻塞子线程,直至执行完成后通知request中的各个监听者。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// DownloadRunnable.java</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"> request.setStatus(Status.RUNNING);</span><br><span class="line"> DownloadTask downloadTask = DownloadTask.create(request);</span><br><span class="line"> Response response = downloadTask.run();</span><br><span class="line"> <span class="keyword">if</span> (response.isSuccessful()) {</span><br><span class="line"> request.deliverSuccess();</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (response.isPaused()) {</span><br><span class="line"> request.deliverPauseEvent();</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (response.getError() != <span class="keyword">null</span>) {</span><br><span class="line"> request.deliverError(response.getError());</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (!response.isCancelled()) {</span><br><span class="line"> request.deliverError(<span class="keyword">new</span> Error());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Response.java,包装执行结果字段</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Response</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> Error error;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">boolean</span> isSuccessful;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">boolean</span> isPaused;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">boolean</span> isCancelled;</span><br><span class="line"> <span class="comment">// 以下getter/setter代码省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>提交任务的逻辑被转交给<code>DownloadTask</code>,它的<code>create</code>是构建方法,新建自身的实例并返回,主要关注它的<code>run()</code>方法,其中执行了下载的真正逻辑,在下面的代码里,我把关键处都加上注释。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 最好加上@WorkerThread注释</span></span><br><span class="line"><span class="function">Response <span class="title">run</span><span class="params">()</span> </span>{</span><br><span class="line"></span><br><span class="line"> Response response = <span class="keyword">new</span> Response();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (request.getStatus() == Status.CANCELLED) {</span><br><span class="line"> response.setCancelled(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (request.getStatus() == Status.PAUSED) {</span><br><span class="line"> response.setPaused(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 进度回调handler,通知UI进度情况</span></span><br><span class="line"> <span class="keyword">if</span> (request.getOnProgressListener() != <span class="keyword">null</span>) {</span><br><span class="line"> progressHandler = <span class="keyword">new</span> ProgressHandler(request.getOnProgressListener());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> tempPath = Utils.getTempPath(request.getDirPath(), request.getFileName());</span><br><span class="line"></span><br><span class="line"> File file = <span class="keyword">new</span> File(tempPath);</span><br><span class="line"> <span class="comment">// 通过downloadId查询历史任务</span></span><br><span class="line"> DownloadModel model = getDownloadModelIfAlreadyPresentInDatabase();</span><br><span class="line"> <span class="comment">// 如果有历史任务,且之前下载的文件仍然存在,则继续之前的下载</span></span><br><span class="line"> <span class="keyword">if</span> (model != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (file.exists()) {</span><br><span class="line"> request.setTotalBytes(model.getTotalBytes());</span><br><span class="line"> request.setDownloadedBytes(model.getDownloadedBytes());</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 历史文件被清除了,则删除对应的历史任务</span></span><br><span class="line"> removeNoMoreNeededModelFromDatabase();</span><br><span class="line"> request.setDownloadedBytes(<span class="number">0</span>);</span><br><span class="line"> request.setTotalBytes(<span class="number">0</span>);</span><br><span class="line"> model = <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// HttpClient用以连接网络</span></span><br><span class="line"> httpClient = ComponentHolder.getInstance().getHttpClient();</span><br><span class="line"></span><br><span class="line"> httpClient.connect(request);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (request.getStatus() == Status.CANCELLED) {</span><br><span class="line"> response.setCancelled(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (request.getStatus() == Status.PAUSED) {</span><br><span class="line"> response.setPaused(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 重定向(上限10次),读取header中的Location字段作为新的URL</span></span><br><span class="line"> httpClient = Utils.getRedirectedConnectionIfAny(httpClient, request);</span><br><span class="line"></span><br><span class="line"> responseCode = httpClient.getResponseCode();</span><br><span class="line"></span><br><span class="line"> eTag = httpClient.getResponseHeader(Constants.ETAG);</span><br><span class="line"> <span class="comment">// 是历史任务,则先比对etag是否发生变化,若变化则重新下载</span></span><br><span class="line"> <span class="keyword">if</span> (checkIfFreshStartRequiredAndStart(model)) {</span><br><span class="line"> model = <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Response Code 落在[200,300) 为Successful</span></span><br><span class="line"> <span class="keyword">if</span> (!isSuccessful()) {</span><br><span class="line"> Error error = <span class="keyword">new</span> Error();</span><br><span class="line"> error.setServerError(<span class="keyword">true</span>);</span><br><span class="line"> error.setServerErrorMessage(convertStreamToString(httpClient.getErrorStream()));</span><br><span class="line"> error.setHeaderFields(httpClient.getHeaderFields());</span><br><span class="line"> error.setResponseCode(responseCode);</span><br><span class="line"> response.setError(error);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 设置任务支持断点续传与否</span></span><br><span class="line"> setResumeSupportedOrNot();</span><br><span class="line"> <span class="comment">// 若是历史任务,则读取记录过的总大小</span></span><br><span class="line"> totalBytes = request.getTotalBytes();</span><br><span class="line"> <span class="comment">// 若不支持断点续传则删除历史文件</span></span><br><span class="line"> <span class="keyword">if</span> (!isResumeSupported) {</span><br><span class="line"> deleteTempFile();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (totalBytes == <span class="number">0</span>) {</span><br><span class="line"> totalBytes = httpClient.getContentLength(); <span class="comment">// 从Header里读取大小</span></span><br><span class="line"> request.setTotalBytes(totalBytes);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 不支持断点续传的任务,插入数据库也没有,索性不插入</span></span><br><span class="line"> <span class="keyword">if</span> (isResumeSupported && model == <span class="keyword">null</span>) {</span><br><span class="line"> createAndInsertNewModel();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (request.getStatus() == Status.CANCELLED) {</span><br><span class="line"> response.setCancelled(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (request.getStatus() == Status.PAUSED) {</span><br><span class="line"> response.setPaused(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 通知外部监听者:我开始下载了</span></span><br><span class="line"> request.deliverStartEvent();</span><br><span class="line"> <span class="comment">// 接下来开始写文件</span></span><br><span class="line"> inputStream = httpClient.getInputStream();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">byte</span>[] buff = <span class="keyword">new</span> <span class="keyword">byte</span>[BUFFER_SIZE];</span><br><span class="line"> <span class="comment">// 文件不存在则创建文件</span></span><br><span class="line"> <span class="keyword">if</span> (!file.exists()) {</span><br><span class="line"> <span class="keyword">if</span> (file.getParentFile() != <span class="keyword">null</span> && !file.getParentFile().exists()) {</span><br><span class="line"> <span class="keyword">if</span> (file.getParentFile().mkdirs()) {</span><br><span class="line"> <span class="comment">//noinspection ResultOfMethodCallIgnored</span></span><br><span class="line"> file.createNewFile();</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">//noinspection ResultOfMethodCallIgnored</span></span><br><span class="line"> file.createNewFile();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 具体实现为RAF</span></span><br><span class="line"> <span class="keyword">this</span>.outputStream = FileDownloadRandomAccessFile.create(file);</span><br><span class="line"> <span class="comment">// 因为是RAF,支持随机写</span></span><br><span class="line"> <span class="keyword">if</span> (isResumeSupported && request.getDownloadedBytes() != <span class="number">0</span>) {</span><br><span class="line"> outputStream.seek(request.getDownloadedBytes());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 先检查任务状态,若暂停或取消,则直接返回</span></span><br><span class="line"> <span class="comment">// 在do-while循环里也有此检查</span></span><br><span class="line"> <span class="keyword">if</span> (request.getStatus() == Status.CANCELLED) {</span><br><span class="line"> response.setCancelled(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (request.getStatus() == Status.PAUSED) {</span><br><span class="line"> response.setPaused(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// do-while循环下载文件</span></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> byteCount = inputStream.read(buff, <span class="number">0</span>, BUFFER_SIZE); <span class="comment">// BUFFER_SIZE=4Kb</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (byteCount == -<span class="number">1</span>) {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> outputStream.write(buff, <span class="number">0</span>, byteCount);</span><br><span class="line"></span><br><span class="line"> request.setDownloadedBytes(request.getDownloadedBytes() + byteCount);</span><br><span class="line"> <span class="comment">// ProgressHandler发送下载进度通知给监听者</span></span><br><span class="line"> sendProgress();</span><br><span class="line"> <span class="comment">// 同时满足两个条件时,更新数据库:1.数据量>65536,2.时间差>2s</span></span><br><span class="line"> syncIfRequired(outputStream);</span><br><span class="line"> <span class="comment">// 实施检查任务状态,是否取消或暂停</span></span><br><span class="line"> <span class="keyword">if</span> (request.getStatus() == Status.CANCELLED) {</span><br><span class="line"> response.setCancelled(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (request.getStatus() == Status.PAUSED) {</span><br><span class="line"> sync(outputStream);</span><br><span class="line"> response.setPaused(<span class="keyword">true</span>);</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> } <span class="keyword">while</span> (<span class="keyword">true</span>);</span><br><span class="line"> <span class="comment">// 下载完成,重命名文件</span></span><br><span class="line"> <span class="keyword">final</span> String path = Utils.getPath(request.getDirPath(), request.getFileName());</span><br><span class="line"></span><br><span class="line"> Utils.renameFileName(tempPath, path);</span><br><span class="line"></span><br><span class="line"> response.setSuccessful(<span class="keyword">true</span>);</span><br><span class="line"> <span class="comment">// 删除数据库多余记录</span></span><br><span class="line"> <span class="keyword">if</span> (isResumeSupported) {</span><br><span class="line"> removeNoMoreNeededModelFromDatabase();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> } <span class="keyword">catch</span> (IOException | IllegalAccessException e) {</span><br><span class="line"> <span class="keyword">if</span> (!isResumeSupported) {</span><br><span class="line"> deleteTempFile();</span><br><span class="line"> }</span><br><span class="line"> Error error = <span class="keyword">new</span> Error();</span><br><span class="line"> error.setConnectionError(<span class="keyword">true</span>);</span><br><span class="line"> error.setConnectionException(e);</span><br><span class="line"> response.setError(error);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> closeAllSafely(outputStream);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="总结,如何做一个极简版本的下载框架"><a href="#总结,如何做一个极简版本的下载框架" class="headerlink" title="总结,如何做一个极简版本的下载框架"></a>总结,如何做一个极简版本的下载框架</h2><p>在大众点评的第一年,从事预订模块开发时,第一次从王旭刚口中听到了“MVP”的概念——Minimum Viable Version,最小可行版本。参考PRDownloader,对于一个下载框架而言,兼顾可维护性、可扩展性和概念的独立性,至少应当具备以下特点。</p><p>自下而上地看</p><ul><li>抽象的下载对象,可持久化,支持从数据库中恢复——对应DownloadModel</li><li>执行下载的对象,职责是进行网络连接,并写入文件——对应DownloadTask</li><li>下载任务,包含下载进度、状态回调——对应DownloadRequest</li><li>全局下载管理器,维护正在下载的任务列表,通常为单例——对应DownloadRequestQueue</li><li>线程池,进行前后台任务分发——对应ExecutorSupplier</li></ul><p>以上这些部分,对外都要隐藏起来,并统一包装到PRDownloader对象中,以便抽象和解耦。</p><p>最后的最后,献上一张PRDownloader的类图(哈哈哈哈,偏不放在文章开头)</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_prdownloader/prdownloader.png" alt="PRDownloader" title=""> </div> <div class="image-caption">PRDownloader</div> </figure>]]></content>
<summary type="html">
<blockquote>
<ul>
<li>You don’t even know me!</li>
<li>I have the rest of my life to find out.</li>
</ul>
</blockquote>
<h2 id="前言,为什么选择研究下载
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="源码解析" scheme="https://lilei.pro/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
</entry>
<entry>
<title>AndroidWeekly#415 学习笔记</title>
<link href="https://lilei.pro/2020/06/01/AndroidWeekly-415/"/>
<id>https://lilei.pro/2020/06/01/AndroidWeekly-415/</id>
<published>2020-06-01T00:04:44.000Z</published>
<updated>2020-06-01T00:07:18.370Z</updated>
<content type="html"><![CDATA[<p><a href="https://androidweekly.net/issues/issue-415" target="_blank" rel="noopener">Android Weekly #415</a></p><h2 id="Android-Unidirectional-Data-Flow-—-Kotlin-Flow-vs-RxJava"><a href="#Android-Unidirectional-Data-Flow-—-Kotlin-Flow-vs-RxJava" class="headerlink" title="Android Unidirectional Data Flow — Kotlin Flow vs. RxJava"></a>Android Unidirectional Data Flow — Kotlin Flow vs. RxJava</h2><p><a href="https://proandroiddev.com/udf-flowvsrx-a792b946d75c" target="_blank" rel="noopener">https://proandroiddev.com/udf-flowvsrx-a792b946d75c</a></p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_androidweekly415/flow_vs_rxjava.png" alt="flow_vs_rxjava.png" title=""> </div> <div class="image-caption">flow_vs_rxjava.png</div> </figure><p>基于以上业务场景,对比<code>Flow</code>和<code>RxJava</code>两种实现方式的代码异同,值得一读(前提是了解这两种技术的基础知识)。</p><h2 id="StateFlow-End-of-LiveData"><a href="#StateFlow-End-of-LiveData" class="headerlink" title="StateFlow, End of LiveData?"></a>StateFlow, End of LiveData?</h2><p><a href="https://medium.com/scalereal/stateflow-end-of-livedata-a473094229b3" target="_blank" rel="noopener">https://medium.com/scalereal/stateflow-end-of-livedata-a473094229b3</a></p><p>Kotlin 协程库在<strong>1.3.6</strong>版本推出StateFlow的release版,本文通过<strong>Activity - ViewModel - StateFlow</strong>的代码样例,简单展示了<code>StateFlow</code>的用法。与<code>LiveData</code>非常相似,不过目前,考虑到它刚刚推出,不建议直接替代成熟的<code>LiveData</code>。</p><h2 id="Reification-of-the-Erased"><a href="#Reification-of-the-Erased" class="headerlink" title="Reification of the Erased"></a>Reification of the Erased</h2><p><a href="https://medium.com/androiddevelopers/reification-of-the-erased-41e246725d2c" target="_blank" rel="noopener">https://medium.com/androiddevelopers/reification-of-the-erased-41e246725d2c</a></p><p>Kotlin提供了<code>refied</code>关键字,结合<code>inline</code>函数,可以扩展泛型的功能,实现在Java中无法实现的效果。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">inline</span> <span class="function"><span class="keyword">fun</span> <span class="type"><<span class="keyword">reified</span> T></span> <span class="title">printType</span><span class="params">()</span></span> {</span><br><span class="line"> print(T::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>)</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">printStringType</span><span class="params">()</span></span>{</span><br><span class="line"> <span class="comment">//calling the reified generic function with String type</span></span><br><span class="line"> printType<String>() </span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="The-Result-Monad"><a href="#The-Result-Monad" class="headerlink" title="The Result Monad"></a>The Result Monad</h2><p><a href="https://adambennett.dev/2020/05/the-result-monad/" target="_blank" rel="noopener">https://adambennett.dev/2020/05/the-result-monad/</a></p><p>文章质量不错。</p><p>在处理Kotlin中一系列操作时,借助<a href="https://github.com/michaelbull/kotlin-result" target="_blank" rel="noopener">kotlin-result</a>库,可以简化异常处理流程。</p>]]></content>
<summary type="html">
<p><a href="https://androidweekly.net/issues/issue-415" target="_blank" rel="noopener">Android Weekly #415</a></p>
<h2 id="Android-Unidirect
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Android Weekly" scheme="https://lilei.pro/tags/Android-Weekly/"/>
</entry>
<entry>
<title>AndroidWeekly#414 学习笔记</title>
<link href="https://lilei.pro/2020/06/01/AndroidWeekly-414/"/>
<id>https://lilei.pro/2020/06/01/AndroidWeekly-414/</id>
<published>2020-05-31T23:56:45.000Z</published>
<updated>2020-06-10T00:31:01.626Z</updated>
<content type="html"><![CDATA[<blockquote><p>5月26日,星期二。今天是<a href="https://androidweekly.net/issues/issue-414" target="_blank" rel="noopener">第#414期Android Weekly</a>的学习笔记。</p></blockquote><h2 id="ViewModel-and-SavedStatehandle-always-retain-state"><a href="#ViewModel-and-SavedStatehandle-always-retain-state" class="headerlink" title="ViewModel and SavedStatehandle: always retain state"></a>ViewModel and SavedStatehandle: always retain state</h2><p><a href="https://www.rockandnull.com/viewmodel-savedstate/" target="_blank" rel="noopener">https://www.rockandnull.com/viewmodel-savedstate/</a></p><blockquote><p>一篇关于ViewModel数据恢复的小文。</p></blockquote><h3 id="进程清理和数据恢复"><a href="#进程清理和数据恢复" class="headerlink" title="进程清理和数据恢复"></a>进程清理和数据恢复</h3><p>我们知道ViewModel可以解决诸如Orientation切换时,Activity/Fragment销毁重建的数据保存问题。然而,有时甚至连ViewModel自身也会被销毁,这种情况发生在应用位于后台且内存不足的情况下,系统会<a href="https://developer.android.com/topic/performance/memory-overview" target="_blank" rel="noopener">杀死进程</a>。杀死进程的顺序是<strong>Least Recently Used (LRU)</strong>。</p><p>如果对此什么都不做的话,默认行为是重启App,如果想要在这个过程中重新恢复到App被清理前的页面,就必须通过类似<code>onSaveInstanceState</code>的方法来保存数据。</p><p>对此,Jetpack提供了<a href="https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate" target="_blank" rel="noopener">ViewModel’s Saved State module</a>组件,通过它,可以方便地恢复ViewModel状态。</p><h3 id="ViewModel-SaveState-的使用"><a href="#ViewModel-SaveState-的使用" class="headerlink" title="ViewModel-SaveState 的使用"></a>ViewModel-SaveState 的使用</h3><p>通过gradle接入,在<a href="https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies" target="_blank" rel="noopener">这里</a>查看最新版本。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"</span><br></pre></td></tr></table></figure><h3 id="不包含构造参数的ViewModel"><a href="#不包含构造参数的ViewModel" class="headerlink" title="不包含构造参数的ViewModel"></a>不包含构造参数的ViewModel</h3><p>在构造器里增加一个<code>SavedStateHandle</code>类型的参数。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyViewModel</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> state: SavedStateHandle) : ViewModel() {</span><br><span class="line"> <span class="comment">// some other code</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后通过代理方式重写<code>model</code>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="keyword">val</span> model <span class="keyword">by</span> viewModels<MyVioewModel></span><br></pre></td></tr></table></figure><h3 id="包含构造参数的ViewModel"><a href="#包含构造参数的ViewModel" class="headerlink" title="包含构造参数的ViewModel"></a>包含构造参数的ViewModel</h3><p>对于包含参数的ViewModel(通过ViewModelFactory初始化),需要继承<code>AbstractSavedStateViweModelFactory</code>类以实现SaveState功能。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyViewModelFactory</span></span>(owner: SavedStateRegistryOwner,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> myId: <span class="built_in">Int</span>,</span><br><span class="line"> defaultArgs: Bundle? = <span class="literal">null</span></span><br><span class="line">) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="type"><T : ViewModel?></span> <span class="title">create</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> key: <span class="type">String</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> modelClass: <span class="type">Class</span><<span class="type">T</span>>,</span></span></span><br><span class="line"><span class="function"><span class="params"> handle: <span class="type">SavedStateHandle</span></span></span></span><br><span class="line"><span class="function"><span class="params"> )</span></span>: T = MyViewModel(handle, myId) <span class="keyword">as</span> T</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后在ViewModel里进行代理:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="keyword">val</span> model <span class="keyword">by</span> viewModels<MyViewModel> {</span><br><span class="line"> MyViewModelFactory(<span class="keyword">this</span>, args.myId)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="结合LiveData使用"><a href="#结合LiveData使用" class="headerlink" title="结合LiveData使用"></a>结合LiveData使用</h3><p>通过Key-Value方式获取LiveData对象,而非直接构建。同一个Key往往会返回同一个Value,因为是持久化保存数据,<code>Item</code>类必须实现<code>Parcellable</code>接口,或者更简便地,通过<code>@Parcelize</code>注解,关于<code>Parcelize</code>可以阅读<a href="https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-parcelable.md" target="_blank" rel="noopener">KEEP的这篇文章</a>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyViewModel</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> savedStateHandle: SavedStateHandle) : ViewModel() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> itemsLiveData = savedStateHandle.getLiveData<Item>(<span class="string">"itemsKey"</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// some other code</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="模拟低内存场景"><a href="#模拟低内存场景" class="headerlink" title="模拟低内存场景"></a>模拟低内存场景</h3><ol><li>启动你的APP</li><li>切换到后台</li><li>执行<code>adb shell am kill your.package.name</code></li><li>再次启动APP</li></ol><p>注意<code>SavedSateHandle</code>只能处理ViewModel中的数据,对于全局的静态单例,仍然需要通过人工的方式处理。</p><h2 id="The-Android-Lifecycle-cheat-sheet-—-part-IV-ViewModels-Translucent-Activities-and-Launch-Modes"><a href="#The-Android-Lifecycle-cheat-sheet-—-part-IV-ViewModels-Translucent-Activities-and-Launch-Modes" class="headerlink" title="The Android Lifecycle cheat sheet — part IV : ViewModels, Translucent Activities and Launch Modes"></a>The Android Lifecycle cheat sheet — part IV : ViewModels, Translucent Activities and Launch Modes</h2><p><a href="https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iv-49946659b094" target="_blank" rel="noopener">https://medium.com/androiddevelopers/the-android-lifecycle-cheat-sheet-part-iv-49946659b094</a></p><p><strong>Android Lifecycle cheat sheet</strong> 是Jose Alcerreca写的一系列生命周期总结文章,通过简单直观的图片表达。</p><p>第4期主要分析ViewModel、透明Activity和不同启动模式下的生命周期。</p><h3 id="ViewModels"><a href="#ViewModels" class="headerlink" title="ViewModels"></a>ViewModels</h3><p>应用于Activity和Fragment,在<code>onCreate</code>结束时初始化,在其销毁时销毁自身。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/lifecycle_viewmodel.png" alt="lifecycle_viewmodel.png" title=""> </div> <div class="image-caption">lifecycle_viewmodel.png</div> </figure><h3 id="透明Activity"><a href="#透明Activity" class="headerlink" title="透明Activity"></a>透明Activity</h3><p>通过<code>android:windowIsTranslucent</code>属性将Activity设定为透明,当在一个透明Activity上启动新的Activity时,原透明Activity只会执行<code>onPause</code>,并不会<code>onStop</code>,并且在他Pause的时候,还可以接受UI事件。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/lifecycle_translucent.png" alt="lifecycle_translucent.png" title=""> </div> <div class="image-caption">lifecycle_translucent.png</div> </figure><p>当按下Home键时,位于底层的透明Activity会进入<code>onStop</code>状态,并且当用户切换回应用后,依次执行<code>onRestart</code>和<code>onStart</code>。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/lifecycle_translucent_home.png" alt="lifecycle_translucent_home.png" title=""> </div> <div class="image-caption">lifecycle_translucent_home.png</div> </figure><h3 id="Launch-Modes"><a href="#Launch-Modes" class="headerlink" title="Launch Modes"></a>Launch Modes</h3><p>关于启动模式的建议是——只使用默认的<strong>standard</strong>启动模式。更多细节可以参阅这篇文章:<a href="https://medium.com/androiddevelopers/tasks-and-the-back-stack-dbb7c3b0f6d4" target="_blank" rel="noopener">Tasks and Back Stack</a>。</p><p>这是<code>SINGLE_TOP</code>模式下的生命周期&栈情况。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/lifecycle_single_top.png" alt="lifecycle_single_top.png" title=""> </div> <div class="image-caption">lifecycle_single_top.png</div> </figure><p>然后是<code>SINGLE_TASK</code>,再次强调,强烈不建议使用这种启动模式。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/lifecycle_single_task.png" alt="lifecycle_single_task.png" title=""> </div> <div class="image-caption">lifecycle_single_task.png</div> </figure><h2 id="Understanding-Kotlin-Coroutines-with-this-mental-model"><a href="#Understanding-Kotlin-Coroutines-with-this-mental-model" class="headerlink" title="Understanding Kotlin Coroutines with this mental model"></a>Understanding Kotlin Coroutines with this mental model</h2><p><a href="https://www.lukaslechner.com/understanding-kotlin-coroutines-with-this-mental-model/" target="_blank" rel="noopener">https://www.lukaslechner.com/understanding-kotlin-coroutines-with-this-mental-model/</a></p><p>什么是思维模型(Mental Model)</p><ul><li>思维模型是我们理解世界的方式</li><li>我们通过思维模型将负责的事情简化</li><li>思维模型藐视事物如何运行</li></ul><p>简单说,思维模型让我们知道How it works。</p><h3 id="Routines"><a href="#Routines" class="headerlink" title="Routines"></a>Routines</h3><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/term_coroutines.png" alt="routine.png" title=""> </div> <div class="image-caption">routine.png</div> </figure><p><strong>Coroutine</strong>由<strong>CO</strong>和<strong>ROUTINE</strong>两部分构成,<strong>Routine</strong>的概念是,一旦启动,就会执行完。而<strong>CO</strong>则为其赋予了并行的含义。</p><p>routine的运行方式如下,1和2顺序执行。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/routine.png" alt="routine.png" title=""> </div> <div class="image-caption">routine.png</div> </figure><h3 id="Coroutines"><a href="#Coroutines" class="headerlink" title="Coroutines"></a>Coroutines</h3><p>在调用后会立即返回,1和2的完成先后顺序不固定。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/coroutines.png" alt="coroutines.png" title=""> </div> <div class="image-caption">coroutines.png</div> </figure><p>Coroutine内部调用<code>suspend</code>函数的地方被称为“suspension point”,即中断点。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200601_andoridweekly414/suspension_point.png" alt="suspension_point.png" title=""> </div> <div class="image-caption">suspension_point.png</div> </figure><h3 id="Coroutine思维模型要点"><a href="#Coroutine思维模型要点" class="headerlink" title="Coroutine思维模型要点"></a>Coroutine思维模型要点</h3><ol><li>Coroutine可以在调用后立即返回,在中断点中断后,过一段时间恢复</li><li>你可以通过Coroutine在不切换线程的情况下,执行并发任务,从而达到更高的运行效率</li><li>Coroutine是线程之上的抽象,一个Coroutine内部的代码也可以运行在不同线程上</li><li>编译器将<code>suspend</code>函数转换为常规函数,同时增加一个<code>Cotinuation</code>类型的参数,这是一种状态机</li><li><code>delay()</code>函数是非阻塞的,它使用类似<code>handler.postDelayed()</code>的方式实现</li></ol><h2 id="Kotlin-withContext-vs-Async-await"><a href="#Kotlin-withContext-vs-Async-await" class="headerlink" title="Kotlin withContext vs Async-await"></a>Kotlin withContext vs Async-await</h2><p><a href="https://blog.mindorks.com/kotlin-withcontext-vs-async-await" target="_blank" rel="noopener">https://blog.mindorks.com/kotlin-withcontext-vs-async-await</a></p><p>最后一篇依然是关于Coroutine,作者介绍了两种启动协程的方式<code>withContext</code>与<code>async-await</code>的应用场景,守则如下。</p><ul><li>它们都可以用来获取结果</li><li>不需要并行执行时,用<code>withContext</code></li><li>当且仅当需要并行时,用<code>async</code></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>5月26日,星期二。今天是<a href="https://androidweekly.net/issues/issue-414" target="_blank" rel="noopener">第#414期Android Weekly</a>的学习
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Android Weekly" scheme="https://lilei.pro/tags/Android-Weekly/"/>
</entry>
<entry>
<title>AndroidWeekly#413 学习笔记</title>
<link href="https://lilei.pro/2020/05/20/AndroidWeekly-413/"/>
<id>https://lilei.pro/2020/05/20/AndroidWeekly-413/</id>
<published>2020-05-20T15:35:01.000Z</published>
<updated>2020-05-20T15:41:20.647Z</updated>
<content type="html"><![CDATA[<blockquote><p>5月17日,星期日。今天是第<a href="https://androidweekly.net/issues/issue-413" target="_blank" rel="noopener">#413</a>期Android Weekly的学习笔记。</p></blockquote><h2 id="Concurrency-Frameworks-in-Android-are-Overrated"><a href="#Concurrency-Frameworks-in-Android-are-Overrated" class="headerlink" title="Concurrency Frameworks in Android are Overrated"></a>Concurrency Frameworks in Android are Overrated</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200520_androidweekly413/concurrency_frameworks_overrated.jpeg" alt="Why Frameworks?" title=""> </div> <div class="image-caption">Why Frameworks?</div> </figure><p><a href="https://www.techyourchance.com/concurrency-frameworks-overrated-android/" target="_blank" rel="noopener">https://www.techyourchance.com/concurrency-frameworks-overrated-android/</a></p><p>今天的首发文章质量不错,Vasiliy抛出一个观点“Android下的并发框架都是过度设计的,实际上你可以不借助任何框架写出干净的并发代码”,并通过样例代码佐证自己的这一观点。</p><h3 id="需求简述"><a href="#需求简述" class="headerlink" title="需求简述"></a>需求简述</h3><p>模拟一个合并文件并上传的过程,文件操作和网络操作必须在工作线程进行,并在主线程通知UI操作结果。</p><ol><li>把两组文件分别进行合并</li><li>把合并结果进行压缩</li><li>上传压缩后的zip包到服务器</li><li>通知操作结果</li></ol><h3 id="只用原生的并发原语实现"><a href="#只用原生的并发原语实现" class="headerlink" title="只用原生的并发原语实现"></a>只用原生的并发原语实现</h3><p>第一个版本没有考虑太多性能,在工作线程顺序执行耗时操作,并通过Handler通知UI线程。在最终一步网络操作的回调里,通知执行结果。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UploadFilesUseCase</span> <span class="keyword">extends</span> <span class="title">BaseObservable</span><<span class="title">UploadFilesUseCase</span>.<span class="title">Listener</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Listener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onFilesUploaded</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onFilesUploadFailed</span><span class="params">()</span></span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Handler uiHandler = <span class="keyword">new</span> Handler(Looper.getMainLooper());</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">uploadFiles</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">new</span> Thread(() -> uploadFilesSync()).start();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">uploadFilesSync</span><span class="params">()</span> </span>{</span><br><span class="line"> File mergedA = processAndMergeFilesOfTypeA();</span><br><span class="line"> File mergedB = processAndMergeFilesOfTypeB();</span><br><span class="line"> File archive = compressMergedFiles(mergedA, mergedB);</span><br><span class="line"></span><br><span class="line"> HttpManager.getInstance.uploadFiles(</span><br><span class="line"> archive,</span><br><span class="line"> <span class="keyword">new</span> HttpRequestListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onDone</span><span class="params">(<span class="keyword">int</span> code, <span class="keyword">byte</span>[] body)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (code / <span class="number">100</span> == <span class="number">2</span>) {</span><br><span class="line"> notifySuccess();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> notifyFailure();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onFailure</span><span class="params">()</span> </span>{</span><br><span class="line"> notifyFailure();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeA</span><span class="params">()</span> </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeB</span><span class="params">()</span> </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">compressMergedFiles</span><span class="params">(File fileA, File fileB)</span> </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">notifySuccess</span><span class="params">()</span> </span>{</span><br><span class="line"> uiHandler.post(() -> {</span><br><span class="line"> <span class="keyword">for</span> (Listener listener : getListeners()) {</span><br><span class="line"> listener.onFilesUploaded();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">notifyFailure</span><span class="params">()</span> </span>{</span><br><span class="line"> uiHandler.post(() -> {</span><br><span class="line"> <span class="keyword">for</span> (Listener listener : getListeners()) {</span><br><span class="line"> listener.onFilesUploadFailed();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="增加异常和重试处理"><a href="#增加异常和重试处理" class="headerlink" title="增加异常和重试处理"></a>增加异常和重试处理</h3><p>如果合并文件、压缩文件过程中可能抛出异常,如下函数签名所示:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeA</span><span class="params">()</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br><span class="line"></span><br><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeB</span><span class="params">()</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br><span class="line"></span><br><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> File <span class="title">compressMergedFiles</span><span class="params">(File fileA, File fileB)</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br></pre></td></tr></table></figure><p>则通过<code>try...catch</code>进行捕获,并通知失败。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> File mergedA = processAndMergeFilesOfTypeA();</span><br><span class="line"> File mergedB = processAndMergeFilesOfTypeB();</span><br><span class="line"> archive = compressMergedFiles(mergedA, mergedB);</span><br><span class="line">} <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> notifyFailure();</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后我们加上重试机制,当前已经重试的次数作为参数传入<code>uploadFilesSync</code>函数,并把原来<code>notifyFailuer</code>处均替换为<code>retryOrFail(retryCount)</code>调用,其实现如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">retryOrFail</span><span class="params">(<span class="keyword">int</span> currentRetryCount)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (currentRetryCount >= MAX_RETRIES - <span class="number">1</span>) {</span><br><span class="line"> notifyFailure();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> uploadFilesSync(currentRetryCount + <span class="number">1</span>); <span class="comment">// 重试次数+1</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="并发运行优化效率"><a href="#并发运行优化效率" class="headerlink" title="并发运行优化效率"></a>并发运行优化效率</h3><p>注意到两处merge文件的操作是可以并行处理的,因此在工作线程之外分起两个线程,并通过<code>CountDownLatch(2)</code>来等待两个线程处理完成。由于涉及并发赋值操作,必须使用<code>AtomicReference</code>保证赋值操作的原子性!</p><blockquote><p>这段代码实现太优美了!</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">uploadFilesSync</span><span class="params">(<span class="keyword">int</span> retryCount)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> AtomicReference<File> mergedA = <span class="keyword">new</span> AtomicReference<>(<span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">final</span> AtomicReference<File> mergedB = <span class="keyword">new</span> AtomicReference<>(<span class="keyword">null</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> CountDownLatch countDownLatch = <span class="keyword">new</span> CountDownLatch(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> Thread(() -> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> mergedA.set(processAndMergeFilesOfTypeA());</span><br><span class="line"> } <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> <span class="comment">// log the exception</span></span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> Thread(() -> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> mergedB.set(processAndMergeFilesOfTypeB());</span><br><span class="line"> } <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> <span class="comment">// log the exception</span></span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> countDownLatch.await();</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"unexpected interrupt"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mergedA.get() == <span class="keyword">null</span> || mergedB.get() == <span class="keyword">null</span>) {</span><br><span class="line"> retryOrFail(retryCount);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 后续是合并&上传的代码,省略</span></span><br></pre></td></tr></table></figure><h3 id="将异步的网络操作同步化"><a href="#将异步的网络操作同步化" class="headerlink" title="将异步的网络操作同步化"></a>将异步的网络操作同步化</h3><p>作者在文中提到,由于在接手开发时,已经有了基于回调的网络框架,因此在以上代码里均使用了回调方式处理网络结果。如果回调过多的话,会产生“回调地狱”,因此,进一步优化,将异步通过<code>CountDownLatch(1)</code>转化为同步方法。返回值为<code>int</code>类型,并且会抛出运行时异常。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WorkerThread</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">uploadFileToServer</span><span class="params">(File archive)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> AtomicInteger responseCode = <span class="keyword">new</span> AtomicInteger(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">final</span> CountDownLatch countDownLatch = <span class="keyword">new</span> CountDownLatch(<span class="number">1</span>);</span><br><span class="line"> HttpManager.getInstance.uploadFiles(</span><br><span class="line"> archive,</span><br><span class="line"> <span class="keyword">new</span> HttpRequestListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onDone</span><span class="params">(<span class="keyword">int</span> code, <span class="keyword">byte</span>[] body)</span> </span>{</span><br><span class="line"> responseCode.set(code);</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onFailure</span><span class="params">()</span> </span>{</span><br><span class="line"> responseCode.set(<span class="number">0</span>);</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> countDownLatch.await();</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"unexpected interrupt"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> responseCode.get();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="防止并发调用,避免进入异常状态"><a href="#防止并发调用,避免进入异常状态" class="headerlink" title="防止并发调用,避免进入异常状态"></a>防止并发调用,避免进入异常状态</h3><p>尽管上述的业务需求都已经实现,对于这种场景,还有一种隐形的技术考虑:不可以在同一时间将同一份数据上传多次,这会导致意料之外的bug,严重的话甚至会损坏服务器的数据。</p><p>通过<a href="https://gist.github.com/techyourchance/44670734917d4ce085224a62cb9edf81" target="_blank" rel="noopener">BaseBusyObservable</a>,提供了一个<code>AtomicBoolean</code>类型的标志位,说明当前任务的执行状态,并且在开始上传文件时申请进入<code>busy</code>,在上传结束时释放。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UploadFilesUseCase</span> <span class="keyword">extends</span> <span class="title">BaseBusyObservable</span><<span class="title">UploadFilesUseCase</span>.<span class="title">Listener</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Listener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onFilesUploaded</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onFilesUploadFailed</span><span class="params">()</span></span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> <span class="keyword">int</span> MAX_RETRIES = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Handler uiHandler = <span class="keyword">new</span> Handler(Looper.getMainLooper());</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">uploadFiles</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!isFreeAndBecomeBusy()) { <span class="comment">// 申请进入busy</span></span><br><span class="line"> <span class="comment">// log concurrent invocation attempt</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">new</span> Thread(() -> uploadFilesSync(<span class="number">0</span>)).start();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">uploadFilesSync</span><span class="params">(<span class="keyword">int</span> retryCount)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> AtomicReference<File> mergedA = <span class="keyword">new</span> AtomicReference<>(<span class="keyword">null</span>);</span><br><span class="line"> <span class="keyword">final</span> AtomicReference<File> mergedB = <span class="keyword">new</span> AtomicReference<>(<span class="keyword">null</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> CountDownLatch countDownLatch = <span class="keyword">new</span> CountDownLatch(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> Thread(() -> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> mergedA.set(processAndMergeFilesOfTypeA());</span><br><span class="line"> } <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> <span class="comment">// log the exception</span></span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> Thread(() -> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> mergedB.set(processAndMergeFilesOfTypeB());</span><br><span class="line"> } <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> <span class="comment">// log the exception</span></span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> countDownLatch.await();</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"unexpected interrupt"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mergedA.get() == <span class="keyword">null</span> || mergedB.get() == <span class="keyword">null</span>) {</span><br><span class="line"> retryOrFail(retryCount);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> File archive;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> archive = compressMergedFiles(mergedA.get(), mergedB.get());</span><br><span class="line"> } <span class="keyword">catch</span> (OperationFailedException e) {</span><br><span class="line"> retryOrFail(retryCount);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> responseCode = uploadFileToServer(archive);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (responseCode / <span class="number">100</span> == <span class="number">2</span>) {</span><br><span class="line"> deleteTempDir();</span><br><span class="line"> notifySuccess();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> retryOrFail(retryCount);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">uploadFileToServer</span><span class="params">(File archive)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> AtomicInteger responseCode = <span class="keyword">new</span> AtomicInteger(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">final</span> CountDownLatch countDownLatch = <span class="keyword">new</span> CountDownLatch(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> HttpManager.getInstance.uploadFiles(</span><br><span class="line"> archive,</span><br><span class="line"> <span class="keyword">new</span> HttpRequestListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onDone</span><span class="params">(<span class="keyword">int</span> code, <span class="keyword">byte</span>[] body)</span> </span>{</span><br><span class="line"> responseCode.set(code);</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onFailure</span><span class="params">()</span> </span>{</span><br><span class="line"> responseCode.set(<span class="number">0</span>);</span><br><span class="line"> countDownLatch.countDown();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> countDownLatch.await();</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"unexpected interrupt"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> responseCode.get();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">retryOrFail</span><span class="params">(<span class="keyword">int</span> currentRetryCount)</span> </span>{</span><br><span class="line"> deleteTempDir();</span><br><span class="line"> <span class="keyword">if</span> (currentRetryCount >= MAX_RETRIES - <span class="number">1</span>) {</span><br><span class="line"> notifyFailure();</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> uploadFilesSync(currentRetryCount + <span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeA</span><span class="params">()</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">processAndMergeFilesOfTypeB</span><span class="params">()</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> File <span class="title">compressMergedFiles</span><span class="params">(File fileA, File fileB)</span> <span class="keyword">throws</span> OperationFailedException </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@WorkerThread</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">deleteTempDir</span><span class="params">()</span> </span>{ ... }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">notifySuccess</span><span class="params">()</span> </span>{</span><br><span class="line"> uiHandler.post(() -> {</span><br><span class="line"> <span class="keyword">for</span> (Listener listener : getListeners()) {</span><br><span class="line"> listener.onFilesUploaded();</span><br><span class="line"> }</span><br><span class="line"> becomeNotBusy(); <span class="comment">// 申请离开busy</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">notifyFailure</span><span class="params">()</span> </span>{</span><br><span class="line"> uiHandler.post(() -> {</span><br><span class="line"> <span class="keyword">for</span> (Listener listener : getListeners()) {</span><br><span class="line"> listener.onFilesUploadFailed();</span><br><span class="line"> }</span><br><span class="line"> becomeNotBusy(); <span class="comment">// 申请离开busy</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以下分别是协程实现、Rxjava实现,很难说它们孰优孰劣,但毫无疑问的是,这两个框架都需要一些学习成本,并且代码在理解起来不如并发原语看起来清晰。在作者Vasiliy看来,他会避免把自己的项目与任何第三方框架进行耦合。以及,RxJava已经度过了它的巅峰期。</p><h3 id="Coroutines-实现"><a href="#Coroutines-实现" class="headerlink" title="Coroutines 实现"></a>Coroutines 实现</h3><p><a href="https://gist.github.com/ATizik/0431c0313d3d0596de3ce9a0fc82b29f" target="_blank" rel="noopener">https://gist.github.com/ATizik/0431c0313d3d0596de3ce9a0fc82b29f</a></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UploadFilesUsecase</span></span>() : BaseObservable<UploadFilesUsecase.Listener>() {</span><br><span class="line"> <span class="class"><span class="keyword">interface</span> <span class="title">Listener</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onFilesUploaded</span><span class="params">()</span></span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onFilesUploadFailed</span><span class="params">()</span></span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> MAX_RETRIES = <span class="number">3</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> mutex = Mutex()</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">uploadFiles</span><span class="params">()</span></span>:<span class="built_in">Boolean</span> = mutex.tryWithLock {</span><br><span class="line"> scope.launch {</span><br><span class="line"> repeat(MAX_RETRIES) { retryCount -></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> files = listOf(</span><br><span class="line"> async { processAndMergeFilesOfTypeA() },</span><br><span class="line"> async { processAndMergeFilesOfTypeB() })</span><br><span class="line"> .map { it.await() }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> archive = compressMergedFiles(files)</span><br><span class="line"></span><br><span class="line"> uploadFileToServer(archive)</span><br><span class="line"></span><br><span class="line"> notifySuccess()</span><br><span class="line"> } <span class="keyword">catch</span> (t: Throwable) {</span><br><span class="line"> <span class="comment">//log exception</span></span><br><span class="line"> <span class="keyword">if</span> (retryCount == MAX_RETRIES - <span class="number">1</span>) {</span><br><span class="line"> notifyFaillure()</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> deleteTempDir()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> suspend <span class="function"><span class="keyword">fun</span> <span class="title">uploadFileToServer</span><span class="params">(archive: <span class="type">File</span>)</span></span> = suspendCoroutine<<span class="built_in">Int</span>> { cont -></span><br><span class="line"> HttpManager.uploadFiles(archive,</span><br><span class="line"> onDone = { code: <span class="built_in">Int</span>, body: ByteArray -></span><br><span class="line"> <span class="keyword">if</span> (code / <span class="number">100</span> == <span class="number">2</span>) {</span><br><span class="line"> cont.resume(code)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> cont.resumeWithException(Throwable())</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> },</span><br><span class="line"> onFailure = {</span><br><span class="line"> cont.resumeWithException(Throwable())</span><br><span class="line"> }</span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> suspend <span class="function"><span class="keyword">fun</span> <span class="title">processAndMergeFilesOfTypeA</span><span class="params">()</span></span>: File = TODO()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> suspend <span class="function"><span class="keyword">fun</span> <span class="title">processAndMergeFilesOfTypeB</span><span class="params">()</span></span>: File = TODO()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> suspend <span class="function"><span class="keyword">fun</span> <span class="title">compressMergedFiles</span><span class="params">(files: <span class="type">List</span><<span class="type">File</span>>)</span></span>: File = TODO()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">deleteTempDir</span><span class="params">()</span></span>: <span class="built_in">Unit</span> = TODO()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">notifySuccess</span><span class="params">()</span></span> {</span><br><span class="line"> MainScope().launch { listeners.forEach { it.onFilesUploaded() } }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">notifyFaillure</span><span class="params">()</span></span> {</span><br><span class="line"> MainScope().launch { listeners.forEach { it.onFilesUploadFailed() } }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="RxJava-实现"><a href="#RxJava-实现" class="headerlink" title="RxJava 实现"></a>RxJava 实现</h3><p><a href="https://gist.github.com/kakai248/d3ac349cf2aa54da7a935fc1ab23024b" target="_blank" rel="noopener">https://gist.github.com/kakai248/d3ac349cf2aa54da7a935fc1ab23024b</a></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UploadFilesUseCase</span></span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> schedulerProvider: SchedulerProvider,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> httpManager: HttpManager</span><br><span class="line">) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">var</span> operation: Completable? = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">uploadFiles</span><span class="params">()</span></span>: Completable = synchronized(<span class="keyword">this</span>) {</span><br><span class="line"> operation</span><br><span class="line"> ?: (doUploadFiles()</span><br><span class="line"> .doFinally { operation = <span class="literal">null</span> }</span><br><span class="line"> .cache()</span><br><span class="line"> .also { operation = it })</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">doUploadFiles</span><span class="params">()</span></span>: Completable =</span><br><span class="line"> Singles</span><br><span class="line"> .zip(</span><br><span class="line"> processAndMergeFilesOfTypeA().subscribeOn(schedulerProvider.io),</span><br><span class="line"> processAndMergeFilesOfTypeB().subscribeOn(schedulerProvider.io)</span><br><span class="line"> )</span><br><span class="line"> .flatMap { (fileA, fileB) -> compressMergedFiles(fileA, fileB) }</span><br><span class="line"> .flatMap(::uploadFileToServer)</span><br><span class="line"> .ignoreElement()</span><br><span class="line"> .doOnComplete(::deleteTempDir)</span><br><span class="line"> .doOnError { deleteTempDir() }</span><br><span class="line"> .retry(MAX_RETRIES)</span><br><span class="line"> .observeOn(schedulerProvider.ui)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">uploadFileToServer</span><span class="params">(archive: <span class="type">File</span>)</span></span> =</span><br><span class="line"> httpManager.uploadFiles(archive)</span><br><span class="line"> .map { response -></span><br><span class="line"> <span class="keyword">if</span> (response.code / <span class="number">100</span> != <span class="number">2</span>) {</span><br><span class="line"> <span class="keyword">throw</span> OperationFailedException()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">processAndMergeFilesOfTypeA</span><span class="params">()</span></span>: Single<File> = Single.just(File(<span class="string">""</span>))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">processAndMergeFilesOfTypeB</span><span class="params">()</span></span>: Single<File> = Single.just(File(<span class="string">""</span>))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">compressMergedFiles</span><span class="params">(fileA: <span class="type">File</span>, fileB: <span class="type">File</span>)</span></span>: Single<File> = Single.just(File(<span class="string">""</span>))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">deleteTempDir</span><span class="params">()</span></span> {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"> <span class="keyword">private</span> const <span class="keyword">val</span> MAX_RETRIES = <span class="number">3</span>L</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HttpManager</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">uploadFiles</span><span class="params">(archive: <span class="type">File</span>)</span></span>: Single<Response> = Single.just(Response(<span class="number">200</span>, byteArrayOf()))</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Response</span></span>(</span><br><span class="line"> <span class="keyword">val</span> code: <span class="built_in">Int</span>,</span><br><span class="line"> <span class="keyword">val</span> body: ByteArray</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">OperationFailedException</span> : <span class="type">Throwable</span></span>()</span><br></pre></td></tr></table></figure><h2 id="Restore-RecyclerView-scroll-position"><a href="#Restore-RecyclerView-scroll-position" class="headerlink" title="Restore RecyclerView scroll position"></a>Restore RecyclerView scroll position</h2><p><a href="https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334" target="_blank" rel="noopener">https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334</a></p><h3 id="丢失的位置"><a href="#丢失的位置" class="headerlink" title="丢失的位置"></a>丢失的位置</h3><p>我们知道当Activity/Fragment重建后,Adapter的数据会重新加载(往往是异步的),在RecyclerView进行layout之前,数据没有加载完成的话,会导致RecyclerView失去之前的状态,最直观表现是滑动位置归零。</p><p>Google在<a href="https://developer.android.com/jetpack/androidx/releases/recyclerview" target="_blank" rel="noopener">RecyclerView的1.2.0-alpha02</a>版本提供了一个新的API,可以让Adapter<strong>阻塞layout过程直至它数据恢复完毕</strong>,用以解决RecyclerView状态不一致的问题。</p><h3 id="恢复位置"><a href="#恢复位置" class="headerlink" title="恢复位置"></a>恢复位置</h3><p>有多种方式可以恢复之前丢失的滑动位置,最好的方法时确保在第一次layout之前,已经设置好了<code>Adapter</code>的状态,若要达成这种效果,数据断然不可存储在Activity/Fragment中,而是应担使用ViewModel或者独立的Repository。如果不使用这个方法,其它的手段要么过于复杂,要么容易出错(比如误用<code>LayoutManager.onRestoreInstanceState</code>)。</p><p><code>recyclerview:1.2.0-alpha02</code>提供的解决方案是,在<code>Adapter</code>类中新增了一个接口,设置状态恢复策略(restoration policy),对应的枚举是<a href="https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.Adapter.StateRestorationPolicy" target="_blank" rel="noopener">StateRestorationPolicy</a>。共有3种枚举值。</p><ul><li>ALLOW —— 默认值,直接恢复RecyclerView的状态</li><li>PREVENT_WHEN_EMPTY —— 仅当adapter非空(<code>adapter.getItemCount() > 0</code>)时才去恢复RecyclerView的状态。如果你的数据是异步加载的,RecyclerView会一直等到数据加载完毕才去恢复自身状态。一个特殊场景是,如果你的Adapter包含Header或者Footer时,你应当适用下一种策略<code>PREVENT</code>,除非你使用了同样在1.2.0-alpha2版本新增的<code>MergeAdapter</code>——它会等待自己所有的adapter进入就绪状态。</li><li>PREVENT —— 推迟所有的状态恢复,直至你设置了<code>ALLOW</code>或者<code>PREVENT_WHEN_EMPTY</code></li></ul><p>有了这个接口,可以很直接地通过以下设置,达成RecyclerView恢复状态的目的:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adapter.restorationPolicy = PREVENT_WHEN_EMPTY</span><br></pre></td></tr></table></figure><h3 id="MergeAdapter-RecyclerView的另一新特性"><a href="#MergeAdapter-RecyclerView的另一新特性" class="headerlink" title="MergeAdapter: RecyclerView的另一新特性"></a>MergeAdapter: RecyclerView的另一新特性</h3><p>同样在<code>1.2.0-alpha2</code>版本中也提出了另一个重磅功能——<a href="https://developer.android.com/reference/androidx/recyclerview/widget/MergeAdapter" target="_blank" rel="noopener">MergeAdapter</a>,它类似于开源框架<a href="https://github.com/drakeet/MultiType" target="_blank" rel="noopener">MultiType</a>,可以在同一个RecyclerView中组合多个Adapter,从而显示多种样式。可以阅读这篇文章<a href="https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a" target="_blank" rel="noopener">Merge adapters sequentially with MergeAdapter</a>进一步了解。</p><h2 id="Clean-Dagger"><a href="#Clean-Dagger" class="headerlink" title="Clean Dagger"></a>Clean Dagger</h2><p><a href="https://proandroiddev.com/clean-dagger-f248eda5790b" target="_blank" rel="noopener">https://proandroiddev.com/clean-dagger-f248eda5790b</a></p><p>关于在Android平台使用DI的一些建议。作者的总结具有借鉴意义:</p><p>选择何种框架,这属于实现细节的问题,这些决定应当尽可能晚地做出。一个良好的体系结构不应当依赖于框架的选择。一些项目被描述为“Dagger驱动的架构”,这实际上是错误的。 </p><p><strong> DI是你应用组件之间的胶水,而非骨架。</strong></p><h2 id="Creating-the-Twitter-splash-screen-in-the-simplest-way-possible"><a href="#Creating-the-Twitter-splash-screen-in-the-simplest-way-possible" class="headerlink" title="Creating the Twitter splash screen in the simplest way possible"></a>Creating the Twitter splash screen in the simplest way possible</h2><p><a href="https://proandroiddev.com/android-motionlayout-twitter-splash-screen-b5755ed56ee8" target="_blank" rel="noopener">https://proandroiddev.com/android-motionlayout-twitter-splash-screen-b5755ed56ee8</a></p><p>仿照Twitter实现的开屏扩张动画。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200520_androidweekly413/twitter_splash.gif" alt="Twitter Splash" title=""> </div> <div class="image-caption">Twitter Splash</div> </figure><h2 id="视频:How-To-Stay-Up-To-Date-As-A-Mobile-Developer"><a href="#视频:How-To-Stay-Up-To-Date-As-A-Mobile-Developer" class="headerlink" title="视频:How To Stay Up To Date As A Mobile Developer?"></a>视频:How To Stay Up To Date As A Mobile Developer?</h2><p><a href="https://www.youtube.com/watch?v=BvOn4fAIS34" target="_blank" rel="noopener">https://www.youtube.com/watch?v=BvOn4fAIS34</a></p><p>主要是宏观方面给Android开发者提出的一些建议:</p><ol><li>没有必要掌握Android开发的方方面面,而且也不现实</li><li>在掌握APP基本开发知识的基础上,找一个自己感兴趣的方向进行深入学习</li><li>接上一条,最好是可以在工作中应用该技术,但是应当注意不要引入尚测试阶段不稳定的特性</li><li>初级工程师需要掌握页面、网络请求、构建等基础知识,资深工程师应当在框架方面有相当程度的见解</li></ol>]]></content>
<summary type="html">
<blockquote>
<p>5月17日,星期日。今天是第<a href="https://androidweekly.net/issues/issue-413" target="_blank" rel="noopener">#413</a>期Android Weekly的学习
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Android Weekly" scheme="https://lilei.pro/tags/Android-Weekly/"/>
</entry>
<entry>
<title>通关Jetpack之 Data Binding:第2课 接口纵览</title>
<link href="https://lilei.pro/2020/03/16/Go-through-jetpack-data-binding-L2/"/>
<id>https://lilei.pro/2020/03/16/Go-through-jetpack-data-binding-L2/</id>
<published>2020-03-16T15:18:48.000Z</published>
<updated>2020-03-16T15:13:20.424Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是《通关Jetpack》系列的第2篇文章</p></blockquote><blockquote><p>我没什么能耐,不能给你们更好的生活,唯一能做的,是挡在你们前边。——《误杀》</p></blockquote><p>在上一篇文章中,通过实例介绍了Data Binding的概念和用法,本篇文章则从更丰富的细节上介绍Data Binding的种种用法。</p><p>##创建Data Binding的两种方式</p><p>在代码里,如果要获取到Data Binding对象,有2种方式</p><h3 id="方式一:一步到位的DataBindingUtil。"><a href="#方式一:一步到位的DataBindingUtil。" class="headerlink" title="方式一:一步到位的DataBindingUtil。"></a>方式一:一步到位的<code>DataBindingUtil</code>。</h3><p>在<code>onCreate</code>中,可以同时完成设置布局文件+创建Binding对象两个动作。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> <span class="keyword">val</span> binding: ActivityMainBinding = DataBindingUtil.setContentView(</span><br><span class="line"> <span class="keyword">this</span>, R.layout.activity_main)</span><br><span class="line"> binding.user = User(<span class="string">"Test"</span>, <span class="string">"User"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>或者在<code>Fragment</code>、<code>ListView</code>或者<code>RecyclerView</code>的初始方法中。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, <span class="literal">false</span>)</span><br></pre></td></tr></table></figure><h3 id="方式二:单独使用LayoutInflater"><a href="#方式二:单独使用LayoutInflater" class="headerlink" title="方式二:单独使用LayoutInflater"></a>方式二:单独使用<code>LayoutInflater</code></h3><p>如果已经调用了<code>setContentView()</code>,则可以在之后单独使用<code>inflate</code>方法来获取Binding对象。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())</span><br></pre></td></tr></table></figure><p>对于<code>Fragment</code>、<code>ListView</code>或者<code>RecyclerView</code>也同理。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, <span class="literal">false</span>)</span><br></pre></td></tr></table></figure><h2 id="表达式用法"><a href="#表达式用法" class="headerlink" title="表达式用法"></a>表达式用法</h2><h3 id="可以在布局文件中使用的表达式"><a href="#可以在布局文件中使用的表达式" class="headerlink" title="可以在布局文件中使用的表达式"></a>可以在布局文件中使用的表达式</h3><p>可以在布局文件中使用丰富的表达式,列举如下,暂不举例。</p><ul><li>数学运算符 <code>+ - / * %</code></li><li>字符串拼接 <code>+</code></li><li>逻辑算式 <code>&& ||</code></li><li>二元运算符 <code>& | ^</code></li><li>一元运算符 <code>+ - ! ~</code></li><li>三目运算符 <code>?:</code></li><li>移位运算 <code>>> >>> <<</code></li><li>比较运算 <code>== > < >= <=</code>(注意在xml中需要将<code><</code>转义写为<code>&lt;</code>)</li><li><code>instanceof</code></li><li>小括号 <code>()</code></li><li>字面量 字符、字符串、数字以及<code>null</code></li><li>类型转换</li><li>方法调用</li><li>属性读取</li><li>数组读取 <code>[]</code></li></ul><h3 id="NULL则替换"><a href="#NULL则替换" class="headerlink" title="NULL则替换"></a>NULL则替换</h3><p>NULL则替换表达式(<code>??</code>)很好用,可以避免很多NPE的场景。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">// NULL则替换</span><br><span class="line">android:text="@{user.displayName ?? user.lastName}"</span><br><span class="line">// 等价于</span><br><span class="line">android:text="@{user.displayName != null ? user.displayName : user.lastName}"</span><br></pre></td></tr></table></figure><h3 id="读取属性值"><a href="#读取属性值" class="headerlink" title="读取属性值"></a>读取属性值</h3><p>其实对于 a. public属性 b. 带有getter的属性 c. 可观测的属性<code>ObservableField</code>,它们在xml里的读取写法都是相同的,都是<code>对象.属性</code>。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">android:text="@{user.name}"</span><br></pre></td></tr></table></figure><h3 id="读取同一布局文件里其它的View"><a href="#读取同一布局文件里其它的View" class="headerlink" title="读取同一布局文件里其它的View"></a>读取同一布局文件里其它的View</h3><blockquote><p>尽管很少用到,但还是介绍一下。</p></blockquote><p>比如我有两个TextView,id无分别是<code>text_view_name</code>和<code>text_view_nickname</code>,在第二个TextView里可以访问第一个TextView的文本,则可以这么写,需要留意的就是会自动将id转为驼峰命名。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">...</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{textViewName.text}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">...</span></span></span><br><span class="line"><span class="tag"> /></span></span><br></pre></td></tr></table></figure><h3 id="集合的写法"><a href="#集合的写法" class="headerlink" title="集合的写法"></a>集合的写法</h3><p>在xml里同样可以使用集合Data,并通过<code>[]</code>来获取集合中特定的元素。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">import</span> <span class="attr">type</span>=<span class="string">"android.util.SparseArray"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">import</span> <span class="attr">type</span>=<span class="string">"java.util.Map"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">import</span> <span class="attr">type</span>=<span class="string">"java.util.List"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"list"</span> <span class="attr">type</span>=<span class="string">"List&lt;String>"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"sparse"</span> <span class="attr">type</span>=<span class="string">"SparseArray&lt;String>"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"map"</span> <span class="attr">type</span>=<span class="string">"Map&lt;String, String>"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"index"</span> <span class="attr">type</span>=<span class="string">"int"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"key"</span> <span class="attr">type</span>=<span class="string">"String"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">data</span>></span></span><br><span class="line">…</span><br><span class="line">android:text="@{list[index]}"</span><br><span class="line">…</span><br><span class="line">android:text="@{sparse[index]}"</span><br><span class="line">…</span><br><span class="line">android:text="@{map[key]}"</span><br></pre></td></tr></table></figure><h3 id="String字面量"><a href="#String字面量" class="headerlink" title="String字面量"></a>String字面量</h3><p>在xml文件中,如果要使用双引号<code>"</code>,则可以将外部的双引号替换为单引号<code>'</code>。或者在应当使用双引号的地方使用反引号。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 单引号替代双引号 --></span></span><br><span class="line">android:text='@{map["firstName"]}'</span><br><span class="line"><span class="comment"><!-- 反引号替代双引号 --></span></span><br><span class="line">android:text="@{map[`firstName`]}"</span><br></pre></td></tr></table></figure><h3 id="引用静态资源"><a href="#引用静态资源" class="headerlink" title="引用静态资源"></a>引用静态资源</h3><p>引用多个静态资源的语法如下。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">android:padding="@{larege ? @dimen/largePadding : @dimen/smallPadding}"</span><br><span class="line">android:text="@{@string/nameFormat(firstName, lastName)}"</span><br><span class="line">android:text="@{@plurals/banana(bananaCount)}"</span><br></pre></td></tr></table></figure><h2 id="事件处理"><a href="#事件处理" class="headerlink" title="事件处理"></a>事件处理</h2><p>Data binding允许你为View绑定各种各样的事件处理函数,作为布局文件里的一项属性,大部分命名格式为<code>android:onXXXX</code>,对应的View接口为<code>View.OnXXXXListener</code>。有两种方式处理事件。</p><ul><li>方法引用:在表达式中使用方法签名。Data Binding会将方法与对象包装成一个Listener并设置给View。</li><li>监听绑定:Lambda表达式,Data Binding对此同样生成一个Listener,供事件触发时调用。</li></ul><h3 id="方法引用"><a href="#方法引用" class="headerlink" title="方法引用"></a>方法引用</h3><p>就像你可以为<code>onClick</code>指明绑定的方法一样,也可以为View的各种事件绑定ViewModel中的方法。在编译时会对此进行检查,如果方法不存在,或是签名错误,则直接报错。“方法引用”会在编译时创建一个Listener,相应的,“监听绑定”则在事件触发时才创建Listener。一个方法引用绑定的例子如下。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyHandlers</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onClickFriend</span><span class="params">(view: <span class="type">View</span>)</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们希望在<code>onClick</code>时触发<code>onClickFriend</code>方法,则写法如下。注意:表达式中的签名与类文件里面方法签名必须完全一致。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">layout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"handlers"</span> <span class="attr">type</span>=<span class="string">"com.example.MyHandlers"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"user"</span> <span class="attr">type</span>=<span class="string">"com.example.User"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">LinearLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:orientation</span>=<span class="string">"vertical"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">TextView</span> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{user.firstName}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:onClick</span>=<span class="string">"@{handlers::onClickFriend}"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">LinearLayout</span>></span></span><br><span class="line"><span class="tag"></<span class="name">layout</span>></span></span><br></pre></td></tr></table></figure><h3 id="监听绑定"><a href="#监听绑定" class="headerlink" title="监听绑定"></a>监听绑定</h3><p>监听绑定的自由度更大,它允许你运行任意的代码。限制之处则在于,方法的返回值必须与表达式期望的值相匹配。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Presneter</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onSaveClick</span><span class="params">(task: <span class="type">Task</span>)</span></span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们可以将以上方法绑定在View的<code>onClick</code>上面。因为这里多了<code>task</code>参数,故无法使用方法引用来写。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">layout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"task"</span> <span class="attr">type</span>=<span class="string">"com.android.example.Task"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"presenter"</span> <span class="attr">type</span>=<span class="string">"com.android.example.Presenter"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">LinearLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">Button</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:onClick</span>=<span class="string">"@{() -> presenter.onSaveClick(task)}"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">LinearLayout</span>></span></span><br><span class="line"><span class="tag"></<span class="name">layout</span>></span></span><br></pre></td></tr></table></figure><p>如果需要用到View本身,则可以使用<code>android:onClick="@{(view) -> presenter.onSaveClick(task)}"</code>。如果处理函数里也要用到View,则函数签名写作。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Presenter</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onSaveClick</span><span class="params">(view: <span class="type">View</span>, task: <span class="type">Task</span>)</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>还可以为方法增加更多的参数,比如对于<code>CheckBox</code>的<code>isChecked</code>属性。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Presenter</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onCompletedChanged</span><span class="params">(task: <span class="type">Task</span>, completed: <span class="type">Boolean</span>)</span></span>{}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">CheckBox</span> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:onCheckedChanged</span>=<span class="string">"@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}"</span> /></span></span><br></pre></td></tr></table></figure><p>对于返回值非void的情况,函数签名必须匹配。比如<code>onLongClick</code>方法,要返回<code>boolean</code>类型的值,表示事件是否被消费。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Presenter</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onLongClick</span><span class="params">(view: <span class="type">View</span>, task: <span class="type">Task</span>)</span></span>: <span class="built_in">Boolean</span> { }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"</span><br></pre></td></tr></table></figure><p>也可以在表达式里使用三目运算符,用<code>void</code>表示不响应事件。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"</span><br></pre></td></tr></table></figure><p>忠告:把复杂的业务逻辑放在Kotlin/Java代码中处理,而非xml表达式。</p><h2 id="导入、变量与引入"><a href="#导入、变量与引入" class="headerlink" title="导入、变量与引入"></a>导入、变量与引入</h2><ul><li>导入(Imports):在xml中引入外部类</li><li>变量(Variables):声明在xml中使用的变量</li><li>引入(Incluces):帮助我们构建更加复杂的UI</li></ul><h3 id="导入"><a href="#导入" class="headerlink" title="导入"></a>导入</h3><p>在<code><data></code>块进行导入,导入后就可以在xml表达式里使用相应的类。如下例的<code>View</code>类。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">import</span> <span class="attr">type</span>=<span class="string">"android.view.View"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">data</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{user.lastName}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:visibility</span>=<span class="string">"@{user.isAdult ? View.VISIBLE : View.GONE}"</span>/></span></span><br></pre></td></tr></table></figure><p>可以在表达式中进行强制类型转换,如把<code>user.connection</code>强转为<code>User</code>。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{((User)(user.connection)).lastName}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span>/></span></span><br></pre></td></tr></table></figure><p>在import后,可以在表达式中使用静态方法。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">import</span> <span class="attr">type</span>=<span class="string">"com.example.MyStringUtils"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"user"</span> <span class="attr">type</span>=<span class="string">"com.example.User"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">data</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{MyStringUtils.capitalize(user.lastName)}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span>/></span></span><br></pre></td></tr></table></figure><p>作为Data Binding类库的默认行为,<code>java.lang.*</code>已经自动引入。</p><h3 id="变量"><a href="#变量" class="headerlink" title="变量"></a>变量</h3><p>声明变量,从而在表达式中引用这些变量,变量的赋值来自于Binding对象。对于同名的<code>portrait</code>和<code>landscape</code>布局文件,它们的变量声明会进行合并,因此注意在处理这种场景时,不要发生命名冲突。对于未赋值的变量,在运行时会取它们的默认值:<code>0</code>、<code>false</code>、<code>null</code>等等。</p><p>Data Binding类库同样内置了<code>context</code>变量,供调用者在表达式中使用,它等价于<code>View.getContext()</code>。</p><h3 id="引入"><a href="#引入" class="headerlink" title="引入"></a>引入</h3><p>在进行复杂UI构件时,如果用到了<code>include</code>标签,可以将变量从父布局传递给子布局。如下例,<code>user</code>对象被传递给了子布局。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">layout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">xmlns:bind</span>=<span class="string">"http://schemas.android.com/apk/res-auto"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"user"</span> <span class="attr">type</span>=<span class="string">"com.example.User"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">LinearLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:orientation</span>=<span class="string">"vertical"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">layout</span>=<span class="string">"@layout/name"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">bind:user</span>=<span class="string">"@{user}"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">layout</span>=<span class="string">"@layout/contact"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">bind:user</span>=<span class="string">"@{user}"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">LinearLayout</span>></span></span><br><span class="line"><span class="tag"></<span class="name">layout</span>></span></span><br></pre></td></tr></table></figure><p>但是,Data Binding并不支持<code>merge</code>标签,试图在<code>merge</code>过程中传递变量是无效的。</p><p>【未完待续】</p>]]></content>
<summary type="html">
<blockquote>
<p>这是《通关Jetpack》系列的第2篇文章</p>
</blockquote>
<blockquote>
<p>我没什么能耐,不能给你们更好的生活,唯一能做的,是挡在你们前边。——《误杀》</p>
</blockquote>
<p>在上一篇文章中,
</summary>
<category term="Jetpack" scheme="https://lilei.pro/tags/Jetpack/"/>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>Kotlin 协程基础课 04.协程原理小探——从包装异步请求为同步说起</title>
<link href="https://lilei.pro/2020/03/16/kotlin-coroutines-04/"/>
<id>https://lilei.pro/2020/03/16/kotlin-coroutines-04/</id>
<published>2020-03-16T15:14:41.000Z</published>
<updated>2020-03-16T15:15:56.907Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是《Kotlin 协程基础课》的第4篇,也是最后一篇文章。</p></blockquote><blockquote><p>我不在意十年后的自己是什么样子,我在意的是,十年后的我怎么看现在的自己。</p></blockquote><p>协程是强大的工具,通过前面三篇文章我们介绍了什么是协程、如何使用协程、协程里的CoroutineContext到底是个什么玩意儿。本文是《Kotlin协程基础课》系列的最后一篇文章,将从原理上进行简要的介绍。</p><h1 id="从混合开发说起"><a href="#从混合开发说起" class="headerlink" title="从混合开发说起"></a>从混合开发说起</h1><p>对Java开发者而言,“回调”是再常见不过的概念了。从各种SDK到我们自己开发的代码,处处充满了回调。某个任务需要长时间执行,同时我们希望能在任务完成时得到通知,在函数参数里加上一个回调对象,用以收取结果,是十分常见的解决方案。</p><h2 id="一个回调Demo"><a href="#一个回调Demo" class="headerlink" title="一个回调Demo"></a>一个回调Demo</h2><p>我们假设有个耗时计算任务,sleep指定时间然后返回数值,代码如下。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 耗时函数,将运算结果在回调中返回</span></span><br><span class="line"><span class="comment">// 只能在工作线程里调用</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">calcSlowly</span><span class="params">(inp: <span class="type">Int</span>, callback: <span class="type">CalcTaskCallback</span><<span class="type">Int</span>>)</span></span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> println(<span class="string">"calcSlowly inp=<span class="variable">$inp</span> in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> <span class="keyword">val</span> result = inp * <span class="number">1000</span>L</span><br><span class="line"> Thread.sleep(result)</span><br><span class="line"> callback.onSuccess(result.toInt())</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> println(<span class="string">"Fail to calc"</span>)</span><br><span class="line"> callback.onFailure(<span class="number">-1</span>, <span class="string">"Fail"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了获取计算的结果,我们传入了<code>callback</code>参数,用以接收计算结果,或者异常信息。</p><p>调用它的代码样例如下,用一个匿名内部对象接收回调:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">execCalcTaskAsync</span><span class="params">()</span></span> {</span><br><span class="line"> Thread(Runnable {</span><br><span class="line"> calcSlowly(<span class="number">3</span>, <span class="keyword">object</span> : CalcTaskCallback<<span class="built_in">Int</span>> {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onSuccess</span><span class="params">(result: <span class="type">Int</span>)</span></span> {</span><br><span class="line"> println(<span class="string">"onSuccess, result=<span class="variable">$result</span>, in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onFailure</span><span class="params">(code: <span class="type">Int</span>, msg: <span class="type">String</span>)</span></span> {</span><br><span class="line"> println(<span class="string">"onFailure, code=<span class="variable">$code</span>, msg=<span class="variable">$msg</span>, in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }).start()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种回调写法在Java中再常见不过了。但是它却有着不小的隐患:</p><ol><li>嵌套太多,成为“回调地狱”;</li><li>传入的callback如果是Activity会引起泄露;</li><li>代码阅读起来不直观。</li></ol><p>这几个隐患不详述了,接下来看看在Kotlin中如何将异步回调转换为同步请求。</p><h2 id="将异步转化为同步"><a href="#将异步转化为同步" class="headerlink" title="将异步转化为同步"></a>将异步转化为同步</h2><p>通过协程的<code>suspendCoroutine</code>关键字,可以将异步回调转换为同步调用,上例改写方法如下,</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 包装异步为同步</span></span><br><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">calcSlowlySync</span><span class="params">(inp: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span> =</span><br><span class="line"> suspendCoroutine { cont -></span><br><span class="line"> calcSlowly(inp, <span class="keyword">object</span>: CalcTaskCallback<<span class="built_in">Int</span>> {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onSuccess</span><span class="params">(result: <span class="type">Int</span>)</span></span> {</span><br><span class="line"> cont.resume(result)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onFailure</span><span class="params">(code: <span class="type">Int</span>, msg: <span class="type">String</span>)</span></span> {</span><br><span class="line"> cont.resumeWithException(Exception(<span class="string">"code=<span class="variable">$code</span>, msg=<span class="variable">$msg</span>"</span>))</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>改写之后,就可以在协程内部愉快地使用这个同步方法进行耗时计算了。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">launch(Dispatchers.DEFAULT) {</span><br><span class="line"> <span class="keyword">val</span> result = calcSlowlySync(<span class="number">100</span>)</span><br><span class="line"> println(<span class="string">"result=<span class="variable">$result</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="suspendCoroutine是如何工作的"><a href="#suspendCoroutine是如何工作的" class="headerlink" title="suspendCoroutine是如何工作的"></a>suspendCoroutine是如何工作的</h1><p>todo</p><hr><p>原始代码<code>MainCoroutine.kt</code></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">(args: <span class="type">Array</span><<span class="type">String</span>>)</span></span> {</span><br><span class="line"> runBlocking {</span><br><span class="line"> <span class="keyword">val</span> job = GlobalScope.launch {</span><br><span class="line"> <span class="keyword">val</span> result = calcSlowly(<span class="number">123</span>)</span><br><span class="line"> println(<span class="string">"result = <span class="variable">$result</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> job.join()</span><br><span class="line"> }</span><br><span class="line"> print(<span class="string">"FINISH"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 延时计算</span></span><br><span class="line"><span class="keyword">private</span> suspend <span class="function"><span class="keyword">fun</span> <span class="title">calcSlowly</span><span class="params">(inp: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span> = withContext(Dispatchers.Default) {</span><br><span class="line"> delay(<span class="number">1000</span>L)</span><br><span class="line"> inp * <span class="number">10</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>反编译后生成4个文件</p><ol><li>MainCoroutineKt.class,</li><li>MainCoroutineKt$main$1.class</li><li>MainCoroutineKt$main$1$job$1.class</li><li>MainCoroutineKt$calcSlowly$2.class</li></ol><h1 id="大纲"><a href="#大纲" class="headerlink" title="大纲"></a>大纲</h1><h1 id="混合开发,从把异步请求转换为同步说起"><a href="#混合开发,从把异步请求转换为同步说起" class="headerlink" title="混合开发,从把异步请求转换为同步说起"></a>混合开发,从把异步请求转换为同步说起</h1><h2 id="异步接口"><a href="#异步接口" class="headerlink" title="异步接口"></a>异步接口</h2><h2 id="转换同步"><a href="#转换同步" class="headerlink" title="转换同步"></a>转换同步</h2><h1 id="协程内部如何将异步转换为同步"><a href="#协程内部如何将异步转换为同步" class="headerlink" title="协程内部如何将异步转换为同步"></a>协程内部如何将异步转换为同步</h1><h2 id="suspendCoroutine"><a href="#suspendCoroutine" class="headerlink" title="suspendCoroutine"></a>suspendCoroutine</h2><h2 id="todo"><a href="#todo" class="headerlink" title="todo"></a>todo</h2>]]></content>
<summary type="html">
<blockquote>
<p>这是《Kotlin 协程基础课》的第4篇,也是最后一篇文章。</p>
</blockquote>
<blockquote>
<p>我不在意十年后的自己是什么样子,我在意的是,十年后的我怎么看现在的自己。</p>
</blockquote>
<p>协
</summary>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>通关Jetpack之 Data Binding:第1课 需求实战</title>
<link href="https://lilei.pro/2020/03/16/Go-through-jetpack-data-binding-L1/"/>
<id>https://lilei.pro/2020/03/16/Go-through-jetpack-data-binding-L1/</id>
<published>2020-03-16T15:08:48.000Z</published>
<updated>2020-03-16T15:10:43.566Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是《通关Jetpack》系列的第1篇文章</p></blockquote><blockquote><p>有些人沦为平庸,有的人金玉其外、败絮其中。可未来某一天,不经意间你会遇到一个彩虹般绚丽的人,从此以后,其他人就不过是匆匆浮云。</p></blockquote><h2 id="Data-Binding是什么"><a href="#Data-Binding是什么" class="headerlink" title="Data Binding是什么"></a>Data Binding是什么</h2><p>在学习一项新知识、新技能之前,必须问的问题是这门技术是什么、将为我们带来什么有益的改变。我们来看一下Data Binding。</p><h3 id="Data-Binding是什么-1"><a href="#Data-Binding是什么-1" class="headerlink" title="Data Binding是什么"></a>Data Binding是什么</h3><p>Data Binding是由Google推出的一个库(Library),用来解决数据变化与UI显示同步的问题。</p><p>Data Binding常常与MVVM一同出现,这并不意味着它们是同一类东西。MVVM是设计模式/编程思想,ViewModel是其中一个组成部分。而Data Binding则是实现这门思想的一种工具。</p><h3 id="Data-Binding将为我们带来怎样的改变"><a href="#Data-Binding将为我们带来怎样的改变" class="headerlink" title="Data Binding将为我们带来怎样的改变"></a>Data Binding将为我们带来怎样的改变</h3><p>做应用开发,很大一部分精力都用于处理UI和数据的同步问题。在传统实现方式里,数据从服务器返回了,此时要手动调用<code>updateView</code>一类的方法更新UI;同样,当用户操作UI上的元素时,会通过<code>Listener</code>等监听,更新到内存的数据中——这是逻辑式写法,而Data Binding则是通过声明式的写法,将数据-UI解耦,从而可以简化控制流,让我们把注意力集中在复杂的业务逻辑上,而非UI同步。</p><p>我们首先从<a href="https://codelabs.developers.google.com/codelabs/android-databinding" target="_blank" rel="noopener">codelab</a>的实例入手,学习如何使用Data Binding。然后会深入介绍Data Binding的API,最后则是从原理角度阐述它是如何工作的。预计分成3篇文章。</p><h2 id="假想一种需求场景"><a href="#假想一种需求场景" class="headerlink" title="假想一种需求场景"></a>假想一种需求场景</h2><p>以这样一个App为例,它需要显示一段静态的文本(左侧),以及可点击的<strong>LIKE</strong>按钮(右侧),点击该按钮时会提升欢迎度数值并实时显示在UI上,最后,欢迎度数值每达到一个阶段,会在屏幕右侧显示不同阶段的图片。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="https://codelabs.developers.google.com/codelabs/android-databinding/img/94376396329952d2.png" alt="sample app" title=""> </div> <div class="image-caption">sample app</div> </figure><h3 id="没有Data-Binding时的写法"><a href="#没有Data-Binding时的写法" class="headerlink" title="没有Data Binding时的写法"></a>没有Data Binding时的写法</h3><p>首先是没有使用Data Binding时,传统的MVVM写法,已省略无关代码。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SimpleViewModel.kt</span></span><br><span class="line"><span class="comment">// ViewModel,对应页面上的静态部分、动态部分以及点击监听函数</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SimpleViewModel</span> : <span class="type">ViewModel</span></span>() {</span><br><span class="line"> <span class="keyword">val</span> name = <span class="string">"Grace"</span></span><br><span class="line"> <span class="keyword">val</span> lastName = <span class="string">"Hopper"</span></span><br><span class="line"> <span class="keyword">var</span> likes = <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 点击监听函数</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onLike</span><span class="params">()</span></span> {</span><br><span class="line"> likes++</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 不同阶段的欢迎度,基于like数计算</span></span><br><span class="line"> <span class="keyword">val</span> popularity: Popularity</span><br><span class="line"> <span class="keyword">get</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">when</span> {</span><br><span class="line"> likes > <span class="number">9</span> -> Popularity.STAR</span><br><span class="line"> likes > <span class="number">4</span> -> Popularity.POPULAR</span><br><span class="line"> <span class="keyword">else</span> -> Popularity.NORMAL</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// PlainOldActivity.kt,UI层</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PlainOldActivity</span> : <span class="type">AppCompatActivity</span></span>() {</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Activity当中维护一个成员变量</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> viewModel <span class="keyword">by</span> lazy { ViewModelProviders.of(<span class="keyword">this</span>).<span class="keyword">get</span>(SimpleViewModel::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>) }</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"></span><br><span class="line"> setContentView(R.layout.plain_activity)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 缺点1:当数据发生变化时,需要手动更新UI</span></span><br><span class="line"> updateName()</span><br><span class="line"> updateLikes()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 缺点2:在UI类中包含业务逻辑</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onLike</span><span class="params">(view: <span class="type">View</span>)</span></span> {</span><br><span class="line"> viewModel.onLike()</span><br><span class="line"> updateLikes()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 缺点3:太多的findViewById</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">updateName</span><span class="params">()</span></span> {</span><br><span class="line"> findViewById<TextView>(R.id.plain_name).text = viewModel.name</span><br><span class="line"> findViewById<TextView>(R.id.plain_lastname).text = viewModel.lastName</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 缺点4:多次调用findViewById</span></span><br><span class="line"> <span class="comment">// 缺点5:包含未经测试的逻辑</span></span><br><span class="line"> <span class="comment">// 缺点6:即使数据未发生变化,也会更新View</span></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">updateLikes</span><span class="params">()</span></span> {</span><br><span class="line"> findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()</span><br><span class="line"> findViewById<ProgressBar>(R.id.progressBar).progress =</span><br><span class="line"> (viewModel.likes * <span class="number">100</span> / <span class="number">5</span>).coerceAtMost(<span class="number">100</span>)</span><br><span class="line"> <span class="keyword">val</span> image = findViewById<ImageView>(R.id.imageView)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> color = getAssociatedColor(viewModel.popularity, <span class="keyword">this</span>)</span><br><span class="line"></span><br><span class="line"> ImageViewCompat.setImageTintList(image, ColorStateList.valueOf(color))</span><br><span class="line"></span><br><span class="line"> image.setImageDrawable(getDrawablePopularity(viewModel.popularity, <span class="keyword">this</span>))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getAssociatedColor</span><span class="params">(popularity: <span class="type">Popularity</span>, context: <span class="type">Context</span>)</span></span>: <span class="built_in">Int</span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getDrawablePopularity</span><span class="params">(popularity: <span class="type">Popularity</span>, context: <span class="type">Context</span>)</span></span>: Drawable? {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这么简单一个页面,光是列出来的缺点就有6条之多,无法想象当业务逻辑复杂起来后,代码将会增长到何种程度。接下来,让我们看看Data Binding如何解决上述问题的。</p><h3 id="静态数据绑定UI"><a href="#静态数据绑定UI" class="headerlink" title="静态数据绑定UI"></a>静态数据绑定UI</h3><p>gradle从1.5.0版本开始支持data binding首先需要在project的<code>build.gradle</code>文件里打开开关,</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">android {</span><br><span class="line">...</span><br><span class="line"> dataBinding {</span><br><span class="line"> enabled <span class="literal">true</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相比于普通布局,data binding布局文件在最外层增加了<code><layout></code>标签,<code><layout></code>标签内部则由<code><data></code>标签和原布局组成。对于需要重构成data binding的布局文件,将鼠标选中最外层布局,会自动提示 <strong>Convert to data binding layout</strong>。一个data binding布局如下所示:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">layout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">xmlns:app</span>=<span class="string">"http://schemas.android.com/apk/res-auto"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">xmlns:tools</span>=<span class="string">"http://schemas.android.com/tools"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"></<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">androidx.constraintlayout.widget.ConstraintLayout</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"><span class="attr">...</span></span></span><br></pre></td></tr></table></figure><p><code><data></code>标签中的变量,可以在布局文件里进行调用,也支持简单的表达式,比如<code>if...else...</code>、类型转换、字符串拼接。表达式的格式为<code>@{...}</code>。虽然表达式很强大,但是不要滥用,否则会使布局文件过于复杂,难以维护。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">android:text="@{String.valueOf(index + 1)}"</span><br><span class="line">android:visibility="@{age <span class="tag">< <span class="attr">13</span> ? <span class="attr">View.GONE</span> <span class="attr">:</span> <span class="attr">View.VISIBLE</span>}"</span></span><br><span class="line"><span class="tag"><span class="attr">android:transitionName</span>=<span class="string">'@{"image_" + id}'</span></span></span><br></pre></td></tr></table></figure><p>基于上述知识,我们为属性name和last name声明变量。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"name"</span> <span class="attr">type</span>=<span class="string">"String"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span> <span class="attr">name</span>=<span class="string">"lastName"</span> <span class="attr">type</span>=<span class="string">"String"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">data</span>></span></span><br></pre></td></tr></table></figure><p>从而可以在布局文件中直接使用它们。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/plain_name"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{name}"</span> /></span></span><br></pre></td></tr></table></figure><p>目前为止已经完成了布局文件的编写,需要一个时机,将数据塞给布局文件,可以在<code>onCreate</code>里做这件事。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// onCreate</span></span><br><span class="line">setConetntView(R.layout.plain_activity)</span><br><span class="line"><span class="keyword">val</span> binding: PlainActivityBinding = DataBindingUtil.setContgentView(<span class="keyword">this</span>, R.layout.plain_activity)</span><br><span class="line">binding.name = <span class="string">"Lei"</span></span><br><span class="line">binding.lastName = <span class="string">"Li"</span></span><br></pre></td></tr></table></figure><p>如此即可,不需要<code>findViewById</code>、<code>setText</code>,甚至不关心UI里到底是TextView还是EditText甚至自定义控件,要做的事情只是给成员变量赋值。处理完了静态文本,接下来看看如何响应UI的点击LIKE事件。要知道,这里我们同样也不用<code>findViewById</code>以及<code>setOnClickListener</code>的。</p><p>首先回忆一下最早的点击事件处理方式——在布局文件中,<code>onClick=onLike</code>,这样自动关联起来Activity中的<code>onLike</code>方法,虽然直观,但带来的后果就是绑死了布局文件与Activity,根本无法复用。其实Data Binding的处理方式与此类似,只不过它增加了一个<strong>ViewModel</strong>层。</p><blockquote><p>在计算机软件领域,没有什么问题是增加一个中间层不能解决的,如果就,那就加两层。</p></blockquote><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span></span></span><br><span class="line"><span class="tag"> <span class="attr">name</span>=<span class="string">"viewmodel"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">type</span>=<span class="string">"com.example.android.databinding.basicsample.data.SimpleViewModel"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">data</span>></span></span><br></pre></td></tr></table></figure><p><code><data></code>中不再是简单的String,而是一个<strong>SimpleViewModel</strong>类型对象,该类在上文中可以看到,它包含<code>name</code>、<code>lastName</code>字段,以及一个处理LIKE++事件的<code>onLike</code>方法。有了这个<code>viewmodel</code>对象,就可以在接下来的布局里面这样使用了。且比上种写法更好的地方在于,编译器会检查<code>onLike</code>方法是否存在,如果不存在则编译时就会异常。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/plain_name"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{viewmodel.name}"</span></span></span><br><span class="line"><span class="tag"><span class="attr">...</span> /></span></span><br><span class="line"><span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/plain_lastname"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{viewmodel.lastName}"</span></span></span><br><span class="line"><span class="tag"><span class="attr">...</span> /></span></span><br><span class="line"><span class="tag"><<span class="name">Button</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:onClick</span>=<span class="string">"@{() -> viewmodel.onLike()}"</span></span></span><br><span class="line"><span class="tag"><span class="attr">...</span> /></span></span><br></pre></td></tr></table></figure><p>对于<code>viewmodel</code>的赋值,则在<strong>onCreate</strong>里面直接<code>binding.viewmodel = viewModel</code>就可以。但是如果我们把<code>likes</code>属性绑定到布局文件的某个TextView上(注意这里不要直接让<code>android:text="@{viewmodel.lines}"</code>,因为这会导致运行时读取id=0的字符串资源,从而异常),会发现即使通过<code>onLike()</code>增长了LIKE,但在UI上并没有体现,LIKE数始终显示为0。这是因为目前为止我们进行的绑定都是<strong>静态</strong>且<strong>单向</strong>绑定,下一节我们将学习双向绑定,从而让LIKES的数目实时显示在UI上。</p><h3 id="双向Data-Binding的写法"><a href="#双向Data-Binding的写法" class="headerlink" title="双向Data Binding的写法"></a>双向Data Binding的写法</h3><p>所谓“双向”Data Binding,是指对于View而言,在初始化时可以自动从data里获取数据,对于data而言,当它们的值发生变化时,能自动通知UI显示相应数值。对于这种发生变化时能通知UI的数据,称之为具有“可观察性”,即<strong>observable</strong>。有多重途径生成一个可观察的对象,比如<code>observable classes</code>、<code>observable fields</code>,以及最通用也最好用的<code>LiveData</code>。声明可观察对象的方式如下。带有前置下划线<code>_</code>的变量为私有变量,是可变的,不对外进行暴露。每个私有变量都拥有一个<code>LiveData<T></code>类型的接口变量,用于提供给布局文件读取。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _name = MutableLiveData(<span class="string">"Ada"</span>)</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _lastName = MutableLiveData(<span class="string">"Lovelace"</span>)</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _likes = MutableLiveData(<span class="number">0</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> name: LiveData<String> = _name</span><br><span class="line"><span class="keyword">val</span> lastName: LiveData<String> = _lastName</span><br><span class="line"><span class="keyword">val</span> likes: LiveData<<span class="built_in">Int</span>> = _likes</span><br></pre></td></tr></table></figure><p>同时需要在Activity的<code>onCreate()</code>方法中,为<code>binding</code>对象的<code>lifecycleOwner</code>赋值(为Activity)。如果不做这一步,数据的变化就无法被观测到。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(SavedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> ...</span><br><span class="line"> binding.lifecycleOwner = <span class="keyword">this</span></span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于<code>popularity</code>,同样将其声明为<code>LiveData</code>类型。这里注意,并没有私有的<code>_popularity</code>,因为它的值实际上是根据<code>likes</code>计算得到的。因此通过<code>Transformations.map()</code>操作,从一个<code>MutableLiveData</code>类型的<code>_likes</code>对象计算出<code>LiveData</code>类型的<code>popularity</code>。关于<code>Transformation</code>的使用,可以看<a href="https://developer.android.com/reference/android/arch/lifecycle/Transformations" target="_blank" rel="noopener">这篇文章</a>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// popularity is exposed as LiveData using a Transformation instead of a @Bindable property.</span></span><br><span class="line"><span class="keyword">val</span> popularity: LiveData<Popularity> = Transformations.map(_likes) {</span><br><span class="line"> <span class="keyword">when</span> {</span><br><span class="line"> it > <span class="number">9</span> -> Popularity.STAR</span><br><span class="line"> it > <span class="number">4</span> -> Popularity.POPULAR</span><br><span class="line"> <span class="keyword">else</span> -> Popularity.NORMAL</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时,在<code>onLike()</code>中我们直接修改<code>_likes</code>的值,就会 通知到UI自动发生调整了。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">onLike</span><span class="params">()</span></span> {</span><br><span class="line"> _likes.value = (_likes.value ?: <span class="number">0</span>) + <span class="number">1</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此处简单介绍下其实现原理,Data Binding库里面包含众多Adapters(适配器),见<a href="https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters/" target="_blank" rel="noopener">源码</a>。对于xml布局文件里的UI组件,需要设置的属性均声明了相应的<strong>静态</strong>设值方法,比如对于<code>TextView</code>的<code>android:text</code>属性,就有如下<code>setText(TextView, CharSequence)</code>方法。当text值发生变更时,就会调用到这个方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@BindingAdapter</span>(<span class="string">"android:text"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">setText</span><span class="params">(TextView view, CharSequence text)</span> </span>{</span><br><span class="line"> <span class="comment">// Some checks removed for clarity</span></span><br><span class="line"> view.setText(text);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时还剩下两点功能没有完成,分别是LIKE按钮下方的进度条没有实时增加,以及LIKE图片没有更新。先看进度条相关的事项。对于进度条有3点需求,分别是:</p><ol><li>当LIKES=0时,进度条隐藏</li><li>当LIKES增加时,进度条实时增长,且每5个LIKE一个循环,进度条归零</li><li>当进度条拉满时,颜色变深</li></ol><p>这些需求都可以通过自定义<code>BindingAdapter</code>来实现,它可以为xml元素创建任意的自定义属性,并通过代码读取该属性,进而对xml元素原生的属性进行修改。在任意包下创建一个<code>BindingAdapters.kt</code>文件(不要担心路径,因为编译时会自动识别注解),借助于Kotlin的顶层函数,可以不声明<code>BindingAdapter</code>类而直接写函数。我们为所有的<code>View</code>创建一个<code>app:hideIfZero</code>属性,控制当该属性为0时隐藏UI元素。</p><p><em>当LIKES=0时,进度条隐藏</em></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@BindingAdapter(<span class="meta-string">"app:hideIfZero"</span>)</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">hideIfZero</span><span class="params">(view: <span class="type">View</span>, number: <span class="type">Int</span>)</span></span> {</span><br><span class="line"> view.visibility = <span class="keyword">if</span> (number == <span class="number">0</span>) View.GONE <span class="keyword">else</span> View.VISIBLE</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>随后在xml布局文件中将该属性与<code>viewmodel.likes</code>进行绑定,这样当likes=0时,进度条就会自动隐藏。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">ProgressBar</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/progressBar"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:hideIfZero</span>=<span class="string">"@{viewmodel.likes}"</span></span></span><br><span class="line"><span class="tag"><span class="attr">...</span></span></span><br></pre></td></tr></table></figure><p><em>当LIKES增加时,进度条实时增长,且每5个LIKE一个循环,进度条归零</em></p><p>参考上面的<code>hideIfZero</code>属性,我们这次同样用自定义<code>BindingAdapter</code>来实现。不同之处在于,这次我们需要同时读取两个属性,在<code>@BindingAdapter</code>注解里面,通过字符串数组<code>["app:progressScaled", "android:max"]</code>来声明,同时<code>requireAll = true</code>表示必须两个属性都在xml得到声明时,才会调用该方法进行处理。如果有任一个没有赋值,则不进行方法。与之相对应的<code>requiredAll = false</code>则对至少一个属性进行相应,对于没有声明的属性会使用其默认值。</p><p><a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/coerce-at-most.html" target="_blank" rel="noopener">coerceAtMost</a>是Kotlin提供的扩展函数,表示“最大不超过<code>max</code>”。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@BindingAdapter(value = [<span class="meta-string">"app:progressScaled"</span>, <span class="meta-string">"android:max"</span>], requireAll = true)</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">setProgress</span><span class="params">(progressBar: <span class="type">ProgressBar</span>, likes: <span class="type">Int</span>, max: <span class="type">Int</span>)</span></span> {</span><br><span class="line"> progressBar.progress = (likes * max / <span class="number">5</span>).coerceAtMost(max)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相应地,在布局文件中声明<code>app:progressScaled</code>和<code>android:max</code>两个属性。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">ProgressBar</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/progressBar"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:hideIfZero</span>=<span class="string">"@{viewmodel.likes}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:progressScaled</span>=<span class="string">"@{viewmodel.likes}"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:max</span>=<span class="string">"@{100}"</span></span></span><br><span class="line"><span class="tag"> /></span></span><br></pre></td></tr></table></figure><p><em>当进度条拉满时,颜色变深</em></p><p>如果前面的知识都已经掌握,这里也就不难了。我们声明一个自定义属性<code>app:progressTint</code>,表示会影响到progressBar的外显颜色,输入为popularity,方法内部先根据popularity计算出color(注意这里使用到的Context为View的Context),然后再将其设置到progressBar的<code>progressTintList</code>属性上。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@BindingAdapter(<span class="meta-string">"app:progressTint"</span>)</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">tintPopularity</span><span class="params">(view: <span class="type">ProgressBar</span>, popularity: <span class="type">Popularity</span>)</span></span> {</span><br><span class="line"> <span class="keyword">val</span> color = getAssociatedColor(popularity, view.context)</span><br><span class="line"> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {</span><br><span class="line"> view.progressTintList = ColorStateList.valueOf(color)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>随后还有一个功能,是根据不同的popularity显示不同的图片,可以把它当做课后作业,参考答案<a href="https://codelabs.developers.google.com/codelabs/android-databinding/#9" target="_blank" rel="noopener">在这里</a>。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="https://codelabs.developers.google.com/codelabs/android-databinding" target="_blank" rel="noopener">https://codelabs.developers.google.com/codelabs/android-databinding</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>这是《通关Jetpack》系列的第1篇文章</p>
</blockquote>
<blockquote>
<p>有些人沦为平庸,有的人金玉其外、败絮其中。可未来某一天,不经意间你会遇到一个彩虹般绚丽的人,从此以后,其他人就不过是匆匆浮云。</p>
</summary>
<category term="Jetpack" scheme="https://lilei.pro/tags/Jetpack/"/>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>一文带你读懂compileSdkVersion、minSdkVersion与targetSdkVersion</title>
<link href="https://lilei.pro/2020/03/16/Android-SDK-versions/"/>
<id>https://lilei.pro/2020/03/16/Android-SDK-versions/</id>
<published>2020-03-16T14:42:30.000Z</published>
<updated>2020-03-16T14:48:38.039Z</updated>
<content type="html"><![CDATA[<blockquote><ul><li>Thanks for saving me. - Don’t waster it. Don’t waste your life.</li></ul></blockquote><h1 id="全文思维导图"><a href="#全文思维导图" class="headerlink" title="全文思维导图"></a>全文思维导图</h1><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200306_android_sdk_versions/几种SdkVersion.png" alt="结构脉络" title=""> </div> <div class="image-caption">结构脉络</div> </figure><h1 id="问题产生的背景"><a href="#问题产生的背景" class="headerlink" title="问题产生的背景"></a>问题产生的背景</h1><p>Android 是一个不断更新演进的系统,犹记得自己最初一部手机是在百脑汇购买的Samsung I9001,Android2.3系统 —— GINGERBREAD,现在几乎听不到这个词了。</p><p>在版本迭代的过程中,Google会不断引入新的接口、并且为App增加新的限制。一方面是由于硬件和软件技术更新,为用户&开发者提供了更强大的能力,比如陀螺仪、人脸识别等;另一方面是不断演进和强化的安全隐私需求。</p><p>对于开发者而言,自然希望自己的App的受众范围越大越好,不仅是当前市面上已有的系统版本,甚至对于未来将要推出的版本也一并可以兼容。由此便引出了以下三个问题,这三个问题是开发者必须解决的,如何解决呢?借助Google提供的能力来完成。</p><ol><li>开发者写的代码是基于哪个版本的API —— 对应的是compileSdkVersion</li><li>开发者提供的apk最低可以运行在什么版本的系统 —— 对应的是minSdkVersion</li><li>开发者提供的apk是以哪个系统版本作为目标来开发的 —— 对应的是targetSdkVersion</li></ol><h1 id="解决系统版本适配和限制的问题"><a href="#解决系统版本适配和限制的问题" class="headerlink" title="解决系统版本适配和限制的问题"></a>解决系统版本适配和限制的问题</h1><p>接下来我们逐个分析以上三个需求。</p><h2 id="开发者写的代码是基于哪个版本的API:compileSdkVersion"><a href="#开发者写的代码是基于哪个版本的API:compileSdkVersion" class="headerlink" title="开发者写的代码是基于哪个版本的API:compileSdkVersion"></a>开发者写的代码是基于哪个版本的API:compileSdkVersion</h2><p>首先作为开发者,在编写代码时一定会调用系统API,而不同系统版本提供的API是有区别的,比如有的接口在某一版本之前一直是<code>private</code>的,直到某一版本时被开发成<code>public</code>。开发者如何告诉编译器自己写的代码是基于哪个版本呢?这时就要用到<code>compileSdkVersion</code>了。</p><p>使用compileSdkVersion,最直接的目的是在编译过程中使用指定版本的SDK进行编译,在此之前,在编写代码的时候,如果调用了高于compileSdkVersion的API,编译器会给出出错提示。需要注意的是compileSdkVersion只存在于编译前的阶段,在编译的生成物apk里是看不到任何与其相关信息的。</p><p>另一个限制是,compileSdkVersion需要与Support Library Version首位匹配。比如你用的Support Library是<code>23.1.1</code>版本的,那么compileSdkVersion一定要是<code>23</code>。</p><p>对compileSdkVersion的使用建议是,总使用当前最新的compileSdkVersion,一方面可以调用最新的系统接口,防止未来某一时刻老接口被废弃后不得不修改代码,变被动为主动。另一方面能够享受到最新SDK带来的编译速度提升。</p><h2 id="开发者提供的apk最低可以运行在什么版本的系统:minSdkVersion"><a href="#开发者提供的apk最低可以运行在什么版本的系统:minSdkVersion" class="headerlink" title="开发者提供的apk最低可以运行在什么版本的系统:minSdkVersion"></a>开发者提供的apk最低可以运行在什么版本的系统:minSdkVersion</h2><p>“能够运行在市面上所有的系统版本上”是开发者一个美好的愿望,但实际上,随着Google不断推陈出新,老版本在市场上的占有量越来越小。可以参考Google的<a href="https://developer.android.com/about/dashboards/index.html" target="_blank" rel="noopener">Dashboard</a>,它统计的规则是一周内访问Google Play的系统版本分布,可以看到4.4及更早版本加起来已经不到10%。</p><p>当市面上老版本占有量趋近于无的时候,我们就不必为适配老版本写专门的代码了,这样能够减少适配工作,从而把开发者宝贵的精力投入到更有价值的新功能实现上。</p><p>在项目中我们用minSdkVersion来说明自己的app最低可以运行在什么版本的系统上。在Google Play、vivo应用商店、游戏中心等应用分发系统中会检测本机版本,若低于应用声明的minSdkVersion就会提示用户,甚至直接拒绝安装,因为即使装上也无法使用。</p><p>在边写代码过程中,你可以使用任何不低于compileSdkVersion的API,但是当你使用的API高于minSdkVersion时,就必须在代码里显示地进行版本判断,否则编译器/Lint工具会提示你。</p><p>另一个限制是,主工程的minSdkVerison不低于其所有依赖库的版本。举例说明:如果项目里同时使用了如下依赖</p><ol><li>support(min=4)</li><li>glide(min=7)</li><li>google play(min=9)</li></ol><p>那么主工程的minSdkVersion就应当为9。该限制可以违背,需要人为保证安全,设置方法为在AndroidManifest里声明tools:overrideLibrary。这样做是有风险的,还是以上例说明,你不能在4的系统版本上调用glide接口,否则一定会报错。必须在代码逻辑里面保证这一点。</p><p>对minSdkVersion的使用建议是,开发过程中若使用到了高于minSdkVersion的API,一定要进设备版本判断;其次在声明这个属性时,参考你应用的目标用户群体系统版本分布,这是一个权衡的过程,你希望应用的覆盖面更广,还是应用的特性更新。</p><h2 id="开发者提供的apk是以哪个系统版本作为目标来开发的:targetSdkVersion"><a href="#开发者提供的apk是以哪个系统版本作为目标来开发的:targetSdkVersion" class="headerlink" title="开发者提供的apk是以哪个系统版本作为目标来开发的:targetSdkVersion"></a>开发者提供的apk是以哪个系统版本作为目标来开发的:targetSdkVersion</h2><p>到了本文要介绍的三个<code>SdkVersion</code>中最有趣的部分——targetSdkVersion。考虑这样一种需求,开发者基于5.0的API开发(即compileSdkVersion=5.0),当app发布时候,Google已经发布了Android 6.0,此时当然希望原App不需要任何改动就可以运行在6.0系统的手机上,通常是Google在6.0的SDK里做了这个兼容。</p><p>拿6.0(API=23)的权限限制举例,Google希望在6.0开始控制App滥用权限的情况,列出了一些必须动态申请的敏感权限。这对手机用户而言是百利而无一害的事,而对开发者而言,如果不能及时修改代码,就会面临App不可用甚至无法上架Google Play的情况下。假如你作为一名应用开发者,日常有1000w的活跃用户,而由于手机系统升级,这1000w用户无法使用之前的App了;而你又恰好在度假中,无法及时修改更新,这将是一个灾难。</p><p>针对上面的具体例子,可以通过在gradle文件里声明<code>targetSdkVersion=21</code>来告诉操作系统,App的代码是面向Android 5.0系统编写的,没有考虑动态权限的问题。这样的APK即使被安装在系统版本为6.0的手机上,也会依照之前的行为来运行,即在安装时申请所有必须权限,而非运行时动态申请。</p><blockquote><p>AOSP里有很多<code>getApplicationInfo().targetSdkVersion < Buid.XXXX</code>样式的代码,就是用来判断<code>targetSdkVersion</code>的</p></blockquote><p>使用targetSdkVersion还有一个好处,你可以使用更高版本的API,且不需要关注行为变更。</p><p>也许你会觉得,作为一名顽固恋旧的开发者,我就不要升级targetSdkVersion,Google好像也拿我没有办法。事实上并非如此,如果targetSdkVersion过低的话,Google、手机厂商都有权利拒绝上架你的App。</p><p>对开发者选择targetSdkVersion的建议是,尽量保持最新的版本,以防应用市场把你的应用下架。升级targetSdkVersion后一定要进行针对性的测试。</p><h1 id="学以致用"><a href="#学以致用" class="headerlink" title="学以致用"></a>学以致用</h1><p>了解这几个版本的含义之后,就要看看如何应用了。通常我们的项目是用gradle管理依赖的,我们需要在根project中声明这几个版本。</p><h2 id="Gradle配置"><a href="#Gradle配置" class="headerlink" title="Gradle配置"></a>Gradle配置</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">android {</span><br><span class="line"> compileSdkVersion 23</span><br><span class="line"> buildToolsVersion 23.1.1</span><br><span class="line"> // ... other config</span><br><span class="line"> defaultConfig {</span><br><span class="line"> minSdkVersion 20</span><br><span class="line"> targetSdkVersion 28</span><br><span class="line"> // ... other config</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>android</code>标签下的配置只会在编译期间生效,<code>defaultConfig</code>标签下的设置会打入最终的Apk中。</p><h2 id="版本高低关系"><a href="#版本高低关系" class="headerlink" title="版本高低关系"></a>版本高低关系</h2><p>通常的版本关系是<strong>min<=target<=compile</strong>,最理想的状态则是<strong>min<=target==compile</strong>,其中compile时刻保持最新。</p><h1 id="targetSdkVersion适配指南"><a href="#targetSdkVersion适配指南" class="headerlink" title="targetSdkVersion适配指南"></a>targetSdkVersion适配指南</h1><p>从Android 6.0开始,每一个版本升级,<a href="https://developer.android.com/distribute/best-practices/develop/target-sdk" target="_blank" rel="noopener">Google都会发布适配指南</a>,将其整理成思维导图。</p><h2 id="适配思维导图大纲"><a href="#适配思维导图大纲" class="headerlink" title="适配思维导图大纲"></a>适配思维导图大纲</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200306_android_sdk_versions/targetAPI适配.png" alt="targetSdkVersion适配" title=""> </div> <div class="image-caption">targetSdkVersion适配</div> </figure><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://medium.com/androiddevelopers/picking-your-compilesdkversion-minsdkversion-targetsdkversion-a098a0341ebd" target="_blank" rel="noopener">Picking your compileSdkVersion, minSdkVersion, targetSdkVersion</a></li><li><a href="https://www.race604.com/android-targetsdkversion/" target="_blank" rel="noopener">targetSdkVersion原理</a></li><li><a href="https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels" target="_blank" rel="noopener">API与英文名、系统版本对照表</a></li><li><a href="https://developer.android.com/about/dashboards" target="_blank" rel="noopener">dashboards</a></li><li><a href="https://developer.android.com/distribute/best-practices/develop/target-sdk" target="_blank" rel="noopener">Google target API 版本升级说明</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<ul>
<li>Thanks for saving me. - Don’t waster it. Don’t waste your life.</li>
</ul>
</blockquote>
<h1 id="全文思维导图"><a href="#全文思
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
</entry>
<entry>
<title>Kotlin让接口写法如此简单</title>
<link href="https://lilei.pro/2020/03/16/Kotlin-made-Interface-so-much-better/"/>
<id>https://lilei.pro/2020/03/16/Kotlin-made-Interface-so-much-better/</id>
<published>2020-03-16T14:38:49.000Z</published>
<updated>2020-03-16T14:40:06.628Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是对Medium上<a href="https://proandroiddev.com/kotlin-made-interface-so-much-better-bbeaa59abdd7" target="_blank" rel="noopener">Kotlin made Interface so much better</a>一文的翻译,内容偏基础。</p></blockquote><p>在Java中,“接口”一开始是作为一项新的编程特性被提出的。它描述了一种“可以是”关系,而非“一定是”。这也使得它可以用于多重继承(例如,某物可以是许多特性的集合,但只能是某个事物。译者注:猫可以同时具有喵喵叫和会爬树两种属性,但猫只能是猫,不可能是狗)。</p><p>然而,正如我们所见到的,直到Java7(Java7是Android工程师主要的开发语言。译者注:原文写于2018年10月),接口仍然有很多缺点,这些缺点使得接口变得很难用,以至于一些人宁愿用回抽象类。</p><p>接着我们迎来了Kotlin,我将在本文为大家展示Kotlin在继承关系中的强大能力。</p><h1 id="Kotlin扩展接口能力"><a href="#Kotlin扩展接口能力" class="headerlink" title="Kotlin扩展接口能力"></a>Kotlin扩展接口能力</h1><h2 id="在Java中"><a href="#在Java中" class="headerlink" title="在Java中"></a>在Java中</h2><p>在Java7中,接口自己不能包含方法实现,因此对于接口实现类而言,必须实现接口中的所有方法。</p><p>这是一个麻烦,它削弱了接口的可扩展能力。</p><p>假设我们有如下的<code>Movable</code>接口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">legsCount</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> <span class="keyword">implements</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">legsCount</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">4</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>突然我们发现除了统计腿的数量,还需要统计翅膀的数量。所以我们增加了<code>wingsCount()</code>方法。</p><p>对于所有实现了<code>Movable</code>接口的类,这是一个坏消息,因为它们必须修改代码以适配接口变更。例如,<code>Horse</code>必须修改如下。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">legsCount</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">int</span> <span class="title">wingsCount</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> <span class="keyword">implements</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">legsCount</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">4</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">wingsCount</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="在Kotlin中"><a href="#在Kotlin中" class="headerlink" title="在Kotlin中"></a>在Kotlin中</h2><p>一开始我们有</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> : <span class="built_in">Int</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> = <span class="number">4</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>随后我们可以轻松地扩展它</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> : <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">wingsCount</span><span class="params">()</span></span> : <span class="built_in">Int</span> { <span class="keyword">return</span> <span class="number">0</span> } <span class="comment">// 注意这里可以为方法提供默认实现</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> = <span class="number">4</span> <span class="comment">// 实现类不需进行任何变动</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>甚至可以做的更加复杂,而<code>Horse</code>类不需要因此进行任何改动!</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span>: <span class="built_in">Int</span> { <span class="keyword">return</span> <span class="number">0</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">wingsCount</span><span class="params">()</span></span>: <span class="built_in">Int</span> { <span class="keyword">return</span> <span class="number">0</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canFly</span><span class="params">()</span></span>: <span class="built_in">Boolean</span> { <span class="keyword">return</span> wingsCount() > <span class="number">1</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span>: <span class="built_in">Boolean</span> { <span class="keyword">return</span> legsCount() > <span class="number">1</span> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> = <span class="number">4</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="Kotlin使接口真正地“可覆写”"><a href="#Kotlin使接口真正地“可覆写”" class="headerlink" title="Kotlin使接口真正地“可覆写”"></a>Kotlin使接口真正地“可覆写”</h1><p>剑桥大辞典里对于“override”的定义是</p><blockquote><p>to decide against or refuse to accept a previous decision, an order, a person, etc.</p></blockquote><p>在Java世界中,接口没有覆写任何东西。</p><p>但是在Kotlin世界里,见如下例子</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> : <span class="built_in">Int</span> { <span class="keyword">return</span> <span class="number">0</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">wingsCount</span><span class="params">()</span></span> : <span class="built_in">Int</span> { <span class="keyword">return</span> <span class="number">0</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canFly</span><span class="params">()</span></span> : <span class="built_in">Boolean</span> { <span class="keyword">return</span> wingsCount() > <span class="number">1</span> }</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> : <span class="built_in">Boolean</span> { <span class="keyword">return</span> legsCount() > <span class="number">1</span> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">var</span> isSick = <span class="literal">false</span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> = <span class="number">4</span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> : <span class="built_in">Boolean</span> {</span><br><span class="line"> <span class="keyword">if</span> (isSick) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.canWalk()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果我们让<code>isSick = true</code>,那么无论这个生物有多少条腿,<code>canWalk()</code>方法都会返回<code>false</code>。嗯,真正意义上的“覆写”。</p><h1 id="Kotlin使接口更像一个对象"><a href="#Kotlin使接口更像一个对象" class="headerlink" title="Kotlin使接口更像一个对象"></a>Kotlin使接口更像一个对象</h1><p>在Java世界(我认为包括Java8和9),接口并不允许包含常量之外的任何属性。</p><p>充其量我们可以声明一个变量的存取方法,例如<code>legsCount()</code>。</p><h2 id="在Kotlin中-1"><a href="#在Kotlin中-1" class="headerlink" title="在Kotlin中"></a>在Kotlin中</h2><p>Kotlin允许接口中包含属性。</p><p>比起这种写法</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span>: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount() > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">legsCount</span><span class="params">()</span></span> = <span class="number">4</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>你可以将其简化成</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount : <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span>: <span class="built_in">Boolean</span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>在接口中使用属性有一些局限,它不可以有backfield属性,意味着它不可变。所以它仍然是无状态的。</p></blockquote><blockquote><p>此外,不允许在接口中为属性赋初值。</p></blockquote><h1 id="Kotlin使接口更好地为组合服务"><a href="#Kotlin使接口更好地为组合服务" class="headerlink" title="Kotlin使接口更好地为组合服务"></a>Kotlin使接口更好地为组合服务</h1><p>你也许听说过“组合胜过继承”原则。Kotlin让它变得更加简单。</p><p>假设你有<code>Horse</code>和<code>Dog</code>。它们都是有4条腿的动物。</p><p>一种写法如下</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount : <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Dog</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种写法太繁琐,具体表现在如下地方</p><ul><li>在每一个实现类里都重复<code>override val legsCount = 4</code>的代码段</li><li>如果我们有更多要覆写的方法,或者更多4条腿的类,我们必须重复做一样的事</li><li>未来某一天,如果我们必须把<code>4</code>改成<code>four</code>,或者增加更多功能……</li></ul><p>这将是一个灾难。太难扩展了。</p><h2 id="也许我们可以利用类的继承关系?"><a href="#也许我们可以利用类的继承关系?" class="headerlink" title="也许我们可以利用类的继承关系?"></a>也许我们可以利用类的继承关系?</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">open</span> <span class="class"><span class="keyword">class</span> <span class="title">FourLegged</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">FourLegged</span></span>()</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Dog</span> : <span class="type">FourLegged</span></span>()</span><br></pre></td></tr></table></figure><p>但是这种写法违背了“组合胜于继承”原则。<code>Horse</code>和<code>Dog</code>不仅仅是<code>FourLegged</code>,也可以是其他什么东西,这种写法让他们不可扩展成为其他类型(例如:<code>Pet</code>)。</p><p>这是不可扩展的☹️</p><h2 id="让我们用组合代替继承(传统写法)"><a href="#让我们用组合代替继承(传统写法)" class="headerlink" title="让我们用组合代替继承(传统写法)"></a>让我们用组合代替继承(传统写法)</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">object</span> FourLegged : Movable {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> movable = FourLegged</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount</span><br><span class="line"> <span class="keyword">get</span>() = movable.legsCount</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Dog</span> : <span class="type">Movable {</span></span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> movable = FourLegged</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount</span><br><span class="line"> <span class="keyword">get</span>() = movable.legsCount</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>不知道你怎么想,我并不喜欢这种写法,所以让我们稍事改动…</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">object</span> FourLegged : Movable {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">open</span> <span class="class"><span class="keyword">class</span> <span class="title">MovableImpl</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> movable: Movable) : Movable {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount</span><br><span class="line"> <span class="keyword">get</span>() = movable.legsCount</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">MovableImpl</span></span>(FourLegged)</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Dog</span> : <span class="type">MovableImpl</span></span>(FourLegged)</span><br></pre></td></tr></table></figure><p>现在这种写法要好一些,因为它的扩展性更好,假设未来我们同时有<code>FourLegged</code>和<code>TwoLegged</code>,我们可以很方便地替换它们。</p><p>但是我仍然不喜欢这种写法,因为我不得不继承自类<code>MovableImpl</code>。所幸我们有Kotlin,我们看下Kotlin如何处理此类问题…</p><h2 id="Kotlin的方式:使用By代理简化组合写法"><a href="#Kotlin的方式:使用By代理简化组合写法" class="headerlink" title="Kotlin的方式:使用By代理简化组合写法"></a>Kotlin的方式:使用By代理简化组合写法</h2><p>在Kotlin的接口写法中,我们可以使用<code>By</code>关键字轻松实现代理模式。来看看</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">object</span> FourLegged : Movable {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Horse</span> : <span class="type">Movable by FourLegged</span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Dog</span> : <span class="type">Movable by FourLegged</span></span></span><br></pre></td></tr></table></figure><p>太棒了!🤩。相信你也看得出这种写法的好处。</p><p>如果你想对“组合胜于继承”有更深入的了解,可以阅读以下这篇文章:</p><p><a href="https://proandroiddev.com/composition-over-inheritance-in-kotlin-way-fe341159bf1c" target="_blank" rel="noopener">Composition over inheritance in Kotlin way</a></p><h1 id="Kotlin使接口更好地实现多重继承"><a href="#Kotlin使接口更好地实现多重继承" class="headerlink" title="Kotlin使接口更好地实现多重继承"></a>Kotlin使接口更好地实现多重继承</h1><p>在Java7中使用多重继承是一件痛苦的事,因为我们不得不实现所有的接口,并覆写其中全部方法。</p><blockquote><p>译者注:原文这里用的是<strong>inherit from all interfaces</strong>,可见原作者对“继承”和“实现”并不是严格区分。实际上在Java中不可以“多重继承”(对抽象类而言),而应当是“多重实现”(对接口而言)。</p></blockquote><p>即使这样做,也无法从父类中继承任何属性。(当然了,Java的接口里是不允许有非常量属性的)</p><h2 id="在Kotlin中-2"><a href="#在Kotlin中-2" class="headerlink" title="在Kotlin中"></a>在Kotlin中</h2><p>借助于By代理,我们来看看下面这个例子</p><p>假设我们的动物接口有两个属性</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Movable</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">canWalk</span><span class="params">()</span></span> = legsCount > <span class="number">1</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Pet</span> </span>{</span><br><span class="line"> <span class="keyword">val</span> name: String</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">liveInhouse</span><span class="params">()</span></span> = <span class="literal">false</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>让我们提供一些更具体的内容(也就是接口实现),以便于我们可以使新创建的类使用这些具体内容(代理)。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> FourLegged : Movable {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount = <span class="number">4</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">InHousePet</span></span>(<span class="keyword">override</span> <span class="keyword">val</span> name: String) : Pet {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">liveInHouse</span><span class="params">()</span></span> = <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后我们可以创建<code>Cat</code>类了,它是一个<code>FourLegged</code>动物,并且也是<code>InHousePet</code>。所以为了同时继承这两点,我们采用如下写法</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CatBy</span></span>(name: String) :</span><br><span class="line"> Pet <span class="keyword">by</span> InHousePet(name), Movable <span class="keyword">by</span> FourLegged</span><br></pre></td></tr></table></figure><p>只要你愿意,可以增加更多的属性</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">classCatBy(name: String, trainer: Trainer):</span><br><span class="line"> Pet <span class="keyword">by</span> InHousePet(name),</span><br><span class="line"> Movable <span class="keyword">by</span> FourLegged,</span><br><span class="line"> Trainable <span class="keyword">by</span> Professional(trainer)</span><br></pre></td></tr></table></figure><p>或者,你也可以覆写其中的值,只要你愿意!</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">classCatBy(name: String, trainer: Trainer):</span><br><span class="line"> Pet <span class="keyword">by</span> InHousePet(name),</span><br><span class="line"> Movable <span class="keyword">by</span> FourLegged,</span><br><span class="line"> Trainable <span class="keyword">by</span> Professional(trainer) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> legsCount: <span class="built_in">Int</span></span><br><span class="line"> <span class="keyword">get</span>() = <span class="number">100</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">liveInHouse</span><span class="params">()</span></span> = <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>感觉自己像是在写C++🤪</p><hr><h1 id="译后感"><a href="#译后感" class="headerlink" title="译后感"></a>译后感</h1><p>在Kotlin的接口中可以声明只读属性,其实就相当于Java的<code>getXXX</code>方法,只不过通过Kotlin一贯的<code>get/set</code>省略写法进行了简化。</p><p><code>by</code>关键字用于代理,是Kotlin里面中阶的知识,我们可以为一个接口声明一个代理对象,从而分散目标类的职责。</p><p>在其它方面,Kotlin中的接口写法相比于Java并没有本质上的改动。</p>]]></content>
<summary type="html">
<blockquote>
<p>这是对Medium上<a href="https://proandroiddev.com/kotlin-made-interface-so-much-better-bbeaa59abdd7" target="_blank" rel="noopene
</summary>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>本周知识积累[2020/03] ViewPager使用指南;Kotlin单例写法;MediaPlayer状态机</title>
<link href="https://lilei.pro/2020/03/03/weekly-2020-03-01/"/>
<id>https://lilei.pro/2020/03/03/weekly-2020-03-01/</id>
<published>2020-03-03T05:08:02.000Z</published>
<updated>2020-03-04T12:03:01.482Z</updated>
<content type="html"><![CDATA[<blockquote><p>我们一路奋斗,不是为了改变世界,而是为了不被世界改变。</p></blockquote><h2 id="ViewPager使用指南"><a href="#ViewPager使用指南" class="headerlink" title="ViewPager使用指南"></a>ViewPager使用指南</h2><p><a href="https://abhiandroid.com/materialdesign/viewpager" target="_blank" rel="noopener">ViewPager</a> 是Android SDK提供的用于实现左右滑动切换页面效果的控件,接入非常简单,可以实现如下图的效果。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200303_weekly/ViewPager-In-Android.gif" alt="ViewPager" title=""> </div> <div class="image-caption">ViewPager</div> </figure><p>自顶向下地看,一个完整的包含ViewPager的页面由以下几个对象构成。</p><ul><li>Host:容器页面,可以是Activity,或者Fragment</li><li>ViewPager:关联到页面上的一个View,可以左右滑动切换子页面</li><li>Adapter:ViewPager内部用以获取每个子页面的适配器,参考RecyclerView/ListView的Adapter</li><li>SubFragment:ViewPager内嵌的子页面</li></ul><p>让我们逐个分析(Host就是一个普通页面,略过不提,SubFragment也一样,与常见写法没有区别,同样略过)</p><h3 id="ViewPager"><a href="#ViewPager" class="headerlink" title="ViewPager"></a>ViewPager</h3><p>相当于一个ViewGroup容器,使用的时候,首先在xml布局里声明<code>android.support.v4.view.ViewPager</code>,接着在代码里通过<code>findViewById</code>获取到这个ViewPager,并为其设置Adapter。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mViewPager = findViewById(R.id.view_pager)</span><br><span class="line"><span class="comment">// 这里需要传入一个FragmentManager,可见ViewPager内部是以Fragment作为每个子页面呈现方式的</span></span><br><span class="line">mViewPager.adapter = YourAdapterClass(supportFragmentManager)</span><br></pre></td></tr></table></figure><p>在使用ViewPager时,往往需要对当前选中页面的行为进行监听,比如当用户左右滑动切换页面时,对应地改变标题栏的文字,对应的是<code>addOnPageChangeListener</code>接口,注意这里是<code>add</code>并非<code>set</code>,意味着不要对同一个对象多次调用,否则会多次触发。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ViewPager.java</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addOnPageChangeListener</span><span class="params">(@NonNull OnPageChangeListener listener)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (mOnPageChangeListeners == <span class="keyword">null</span>) {</span><br><span class="line"> mOnPageChangeListeners = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> }</span><br><span class="line"> mOnPageChangeListeners.add(listener);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">OnPageChangeListener</span> </span>{</span><br><span class="line"> <span class="comment">// 页面发生位移时调用,既包含用户手指拖动,也包含页面自身的动画移动。参数是位移的百分比和像素值,可以用来进行一些计算</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onPageScrolled</span><span class="params">(<span class="keyword">int</span> position, <span class="keyword">float</span> positionOffset, <span class="keyword">int</span> positionOffsetPixels)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 页面被选中时调用,动画也许并没有结束,参数是被选中页面的index</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onPageSelected</span><span class="params">(<span class="keyword">int</span> position)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 页面状态发生变化时调用,不特指哪一个页面,而是所有页面。</span></span><br><span class="line"> <span class="comment">// 有三种状态:IDLE(页面静止,无动作)、DRAGGING(用户拖动中)、SETTING(用户已放手,页面归位中)</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onPageScrollStateChanged</span><span class="params">(<span class="keyword">int</span> state)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接口还是比较简单的,同时,如果我们只关注三个回调中的一个(往往是<code>onPageSelected</code>),可以用另一个内部类来创建监听对象,以减少样板代码,<code>SimpleOnPageChangeListener</code>同样位于<code>ViewPager.java</code>中。</p><p>这是很好的一种编程思想,对于包含多个回调函数的监听接口,增加一个内部类,为每个回调函数创建一个空函数,在使用时只覆写业务需要的接口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">SimpleOnPageChangeListener</span> <span class="keyword">implements</span> <span class="title">OnPageChangeListener</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPageScrolled</span><span class="params">(<span class="keyword">int</span> position, <span class="keyword">float</span> positionOffset, <span class="keyword">int</span> positionOffsetPixels)</span> </span>{</span><br><span class="line"> <span class="comment">// This space for rent</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPageSelected</span><span class="params">(<span class="keyword">int</span> position)</span> </span>{</span><br><span class="line"> <span class="comment">// This space for rent</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onPageScrollStateChanged</span><span class="params">(<span class="keyword">int</span> state)</span> </span>{</span><br><span class="line"> <span class="comment">// This space for rent</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>Hint</strong>:如果要在<code>onPageSelected</code>回调里获取相应的SubFragment,不要使用<code>Adapter.getItem</code>,它会返回一个新创建的Fragment。应当调用的方法是<code>Adapter.instantiateItem</code>,这会返回已创建的Fragment,参考Stack Overflow上面的<a href="https://stackoverflow.com/questions/17685787/access-a-method-of-a-fragment-from-the-viewpager-activity" target="_blank" rel="noopener">这个问题</a>。</p><h3 id="Adapter"><a href="#Adapter" class="headerlink" title="Adapter"></a>Adapter</h3><p>有两种Adapter,FragmentPagerAdapter和FragmentStatePagerAdapter,简单地说,如果你的ViewPager只包含3到4个固定的页面,则使用FragmentPagerAdapter;如果有很多个页面,则使用FragmentStatePagerAdapter。</p><p>这里以FragmentStatePagerAdapter为例,介绍Adapter的写法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// FragmentStatePagerAdapter.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">FragmentStatePagerAdapter</span> <span class="keyword">extends</span> <span class="title">PagerAdapter</span> </span>{</span><br><span class="line"> <span class="comment">// ...some code...</span></span><br><span class="line"> <span class="comment">// 这里创建Fragment并返回</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> Fragment <span class="title">getItem</span><span class="params">(<span class="keyword">int</span> position)</span></span>;</span><br><span class="line"> <span class="comment">// ...some code...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// PagerAdapter.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">PagerAdapter</span> </span>{</span><br><span class="line"> <span class="comment">// ...some code...</span></span><br><span class="line"> <span class="comment">// 返回Fragment总个数</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">int</span> <span class="title">getCount</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="comment">// ...some code...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可见最简单的FragmentStatePagerAdapter只需要实现<code>getItem</code>和<code>getCount</code>两个方法。值得一提的是,如果需要在创建SubFragment时传递一些参数,用以下写法。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建Fragment时传入Arg_0</span></span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">getItem</span><span class="params">(position: <span class="type">Int</span>)</span></span>: Fragment {</span><br><span class="line"> <span class="keyword">val</span> frag = YourFragmentClass()</span><br><span class="line"> frag.arguments = Bundle().apply {</span><br><span class="line"> putString(ARG_0, some_value)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ... some code ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在Fragment的onViewCreated里读取参数</span></span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onViewCreated</span><span class="params">(view: <span class="type">View</span>, savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> arguments?.takeIf { it.containsKey(ARG_0) }?.apply {</span><br><span class="line"> <span class="keyword">val</span> someValue = getString(ARG_0)</span><br><span class="line"> <span class="comment">// ... some code ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Activity-传递大数据"><a href="#Activity-传递大数据" class="headerlink" title="- Activity 传递大数据"></a>- Activity 传递大数据</h2><p>在使用Intent进行Activity之间的跳转时,系统提供了<code>putExtra</code>用于参数传递,如下例。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// caller activity</span></span><br><span class="line"><span class="keyword">val</span> intent = Intent(<span class="keyword">this</span>, TheNextActivity::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>)</span></span><br><span class="line">i.putExtra(ARG_0, <span class="number">100</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// called activity</span></span><br><span class="line"><span class="keyword">val</span> param = intent.extras?.getInt(ARG_0)</span><br></pre></td></tr></table></figure><p>如果传递的参数不是基础类型,而是列表,则使用<code>putExtra(String, Parcelable)</code>和<code>getParcelableExtra(String)</code>做相应的存取。</p><p>然而,实际上很多人并不知道,通过Intent传递的参数,是有大小限制的。当我们传递占内存非常大的数据,如1000个元素的列表、Bitmap等等时,稍不注意,就会出现<code>TransactionTooLargeException</code>,从异常名就可以看出,这是由于参数过大引起的。究其原因,是因为ActivityManagerService内部使用了Binder通信机制,其事务缓冲区限制了传输数据的大小。Binder事务缓冲区的大小为1MB,而且,这1MB还不是独享的,意味着有时尽管传递的数据没有超出1MB,也会触发异常。</p><p>那么,对于需要传递大量数据的场景,有哪些方案?</p><h3 id="单例"><a href="#单例" class="headerlink" title="单例"></a>单例</h3><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> Singleton {</span><br><span class="line"> <span class="keyword">var</span> items: List<Foo>? = <span class="literal">null</span></span><br><span class="line"> <span class="keyword">var</span> largeImg: Bitmap? = <span class="literal">null</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>不需要过多解释,注意不要出现内存泄漏,以及单例无法在进程之间共享。</p><h3 id="持久化"><a href="#持久化" class="headerlink" title="持久化"></a>持久化</h3><p>利用网络、数据库、文件、SharedPreference等方式,将数据持久化保存,随后在新页面读取。优点是保存后可以跨进程甚至跨应用、跨平台使用,缺点则是效率低下,读写时没有控制好事务会发生异常。</p><h3 id="使用EventBus"><a href="#使用EventBus" class="headerlink" title="使用EventBus"></a>使用EventBus</h3><p>在《阿里巴巴Android开发手册》中写到:“Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable 的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。”</p><p>由于EventBus滥用会导致代码结构混乱,因此个人不推荐。</p><p><strong>参考资料</strong>:<a href="https://juejin.im/post/5d8de547e51d45781f73bacc" target="_blank" rel="noopener">https://juejin.im/post/5d8de547e51d45781f73bacc</a></p><h2 id="Kotlin单例写法"><a href="#Kotlin单例写法" class="headerlink" title="- Kotlin单例写法"></a>- Kotlin单例写法</h2><p>单例模式是日常开发中最常使用到的设计模式,一个良好的单例模式实现应当兼顾代码性能与调用简便两个方面。在Java中我们通过“双锁”或者“静态内部类”来实现单例模式,相比之下我更喜欢静态内部类的写法,《Effective Java》一书的作者也是这样认为的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 样例代码,来自 wiki:https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Something</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="title">Something</span><span class="params">()</span> </span>{}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">LazyHolder</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">final</span> Something INSTANCE = <span class="keyword">new</span> Something();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Something <span class="title">getInstance</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> LazyHolder.INSTANCE;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="无参数写法"><a href="#无参数写法" class="headerlink" title="无参数写法"></a>无参数写法</h3><p>今天主要讨论Kotlin的单例写法,在Kotlin中,单例被上升到了语言层面,关键字<code>object</code>可以用来声明一个不需要参数的单例对象。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> SomeSingleton {</span><br><span class="line"> init {</span><br><span class="line"> <span class="comment">// 在这里添加初始化代码</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>借助于JVM加载类的过程,它编译后的等效Java代码也是线程安全的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 上述Kotlin代码的Java等价版本</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">SomeSingleton</span> </span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> SomeSingleton INSTANCE;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="title">SomeSingleton</span><span class="params">()</span> </span>{</span><br><span class="line"> INSTANCE = (SomeSingleton)<span class="keyword">this</span>;</span><br><span class="line"> System.out.println(<span class="string">"init complete"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> {</span><br><span class="line"> <span class="keyword">new</span> SomeSingleton();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="有参数写法"><a href="#有参数写法" class="headerlink" title="有参数写法"></a>有参数写法</h3><p>有时我们需要在单例初始化时传入一些参数,比如<code>Glide.with(Context)</code>,此时<code>object</code>关键字就捉襟见肘了。在<a href="https://stackoverflow.com/questions/40398072/singleton-with-parameter-in-kotlin" target="_blank" rel="noopener">Stack Overflow这个问题</a>下面可以学习到,借助伴生对象的“伪静态方法”,能达到传入初始化参数的目的。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UsersDatabase</span> : <span class="type">RoomDatabase</span></span>() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Volatile</span> <span class="keyword">private</span> <span class="keyword">var</span> INSTANCE: UsersDatabase? = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">getInstance</span><span class="params">(context: <span class="type">Context</span>)</span></span>: UsersDatabase =</span><br><span class="line"> INSTANCE ?: synchronized(<span class="keyword">this</span>) {</span><br><span class="line"> INSTANCE ?: buildDatabase(context).also { INSTANCE = it }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">buildDatabase</span><span class="params">(context: <span class="type">Context</span>)</span></span> =</span><br><span class="line"> Room.databaseBuilder(context.applicationContext,</span><br><span class="line"> UsersDatabase::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>, <span class="type">"Sample.db")</span></span></span><br><span class="line"> .build()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以上这种写法,需要关注以下几点。</p><ol><li>单例成员<code>INSTANCE</code>需要有<code>@Volatile</code>声明,以保证对象唯一</li><li><code>synchronized</code>加锁防止重复初始化</li><li>借助<code>also</code>返回原对象</li></ol><p>如果代码里只有一个单例类要实现,上面这种写法就足够了。但是,若有很多个单例类,这种写法产生的样板代码可不少。是不是可以把样板代码逻辑抽出,一次书写,多处调用?答案是肯定的。</p><h3 id="有参数写法,Write-Once,Use-Many"><a href="#有参数写法,Write-Once,Use-Many" class="headerlink" title="有参数写法,Write Once,Use Many"></a>有参数写法,Write Once,Use Many</h3><p>首先区分上述实现方式里,可变的部分与不变的部分,思路是把不变的部分抽象成流程,把可变的部分提取作为参数。</p><p>不变的部分是检查、维护、调用构建函数,将其抽出一个类,这个类一定是用于被继承,因此我们将其声明为<code>open</code>,通过lambda表达式参数<code>constructor</code>,开放出构建对象的能力</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SingletonHolder.kt</span></span><br><span class="line"><span class="keyword">open</span> <span class="class"><span class="keyword">class</span> <span class="title">SingletonHolder</span><<span class="type">out T, in A</span>></span>(<span class="keyword">private</span> <span class="keyword">val</span> <span class="keyword">constructor</span>: (A) -> T) {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Volatile</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">var</span> instance: T? = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">getInstance</span><span class="params">(arg: <span class="type">A</span>)</span></span>: T {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">when</span> {</span><br><span class="line"> instance != <span class="literal">null</span> -> instance!!</span><br><span class="line"> <span class="keyword">else</span> -> synchronized(<span class="keyword">this</span>) {</span><br><span class="line"> <span class="keyword">if</span> (instance == <span class="literal">null</span>) instance = <span class="keyword">constructor</span>(arg)</span><br><span class="line"> instance!!</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时,有一个类需要增加单例实现,并且其构造函数需要一个<code>Context</code>类型的参数,我们只需要在其内部声明一个伴生对象,继承自<code>SingletonHolder<MyManager, Context></code></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MyManager.kt</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyManager</span> <span class="keyword">private</span> <span class="keyword">constructor</span></span>(context: Context) {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">doSomething</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> : SingletonHolder<MyManager, Context>(::MyManager)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对单例的调用者而言,写法与Java无异。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MyManager.getInstance(context).doSomething()</span><br></pre></td></tr></table></figure><p>怎么样,是不是与Glide的<code>Glide.with(context).load(img_url)</code>完全一致?Bravo!</p><p><strong>参考资料</strong></p><ul><li><a href="https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e" target="_blank" rel="noopener">https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e</a></li><li><a href="https://stackoverflow.com/questions/40398072/singleton-with-parameter-in-kotlin" target="_blank" rel="noopener">https://stackoverflow.com/questions/40398072/singleton-with-parameter-in-kotlin</a></li></ul><h2 id="Vimium的页面检索技巧"><a href="#Vimium的页面检索技巧" class="headerlink" title="Vimium的页面检索技巧"></a>Vimium的页面检索技巧</h2><p>在使用浏览器时,有时我们会打开很多个页面,此时如果想要在打开的页面里找到特定页面,往往需要从头翻到尾,十分之麻烦。Vimium考虑到了这一点,并为我们提供快捷键<code>T</code>解决。这个功能属于<strong>Vomnibar</strong>功能集,是Vimium提供的一组页面新建、搜索快捷键,一共有5个。</p><ul><li><code>o</code>,在当前Tab打开URL、书签或浏览历史</li><li><code>O</code>,新建Tab打开URL、书签或浏览历史</li><li><code>b</code>,在当前Tab打开书签</li><li><code>B</code>,新建Tab打开书签</li><li><code>T</code>,也就是刚刚介绍过的,在已打开的Tab中进行搜索</li></ul><p>(顺带提一下,Sublime的copy line快捷键是<code>Ctrl+Shift+D</code>,在写着一段时用到的。)</p><h2 id="使用默认参数简化自定义View的构造函数"><a href="#使用默认参数简化自定义View的构造函数" class="headerlink" title="使用默认参数简化自定义View的构造函数"></a>使用默认参数简化自定义View的构造函数</h2><p>在编写自定义View的类时,如果自定义View继承自<code>android.view.View</code>,通常需要覆写多个构造函数,以支持View的多种构建方式。这种处理不仅麻烦,还带来大量样板代码,稀释了我们的代码质量。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// View.java</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">View</span><span class="params">(Context context)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">View</span><span class="params">(Context context, @Nullable AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>(context, attrs, <span class="number">0</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">View</span><span class="params">(Context context, @Nullable AttributeSet attrs, <span class="keyword">int</span> defStyleAttr)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>(context, attrs, defStyleAttr, <span class="number">0</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">View</span><span class="params">(Context context, @Nullable AttributeSet attrs, <span class="keyword">int</span> defStyleAttr, <span class="keyword">int</span> defStyleRes)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>联想到Kotlin函数的默认参数功能,是不是可以将其应用在这种场景中呢?答案当然是可以。结合<code>@JvmOverloads</code>注解和默认参数,写法如下。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomView</span> <span class="meta">@JvmOverloads</span> <span class="keyword">constructor</span></span>(</span><br><span class="line"> context: Context,</span><br><span class="line"> attrs: AttributSet? = <span class="literal">null</span>,</span><br><span class="line"> defStyle: <span class="built_in">Int</span> = <span class="number">0</span>,</span><br><span class="line"> defStyleRes: <span class="built_in">Int</span> = <span class="number">0</span></span><br><span class="line"> ) : View(context, attrs, defStyle, defStyleRes) {</span><br><span class="line"> <span class="comment">// class body</span></span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>在此基础上,借助<code>init{ ... }</code>代码块,可以执行自定义的初始化代码。</p><p><strong>参考</strong>:<a href="https://stackoverflow.com/questions/20670828/how-to-create-constructor-of-custom-view-with-kotlin" target="_blank" rel="noopener">https://stackoverflow.com/questions/20670828/how-to-create-constructor-of-custom-view-with-kotlin</a></p><h2 id="MediaPlayer状态机"><a href="#MediaPlayer状态机" class="headerlink" title="MediaPlayer状态机"></a>MediaPlayer状态机</h2><p>MediaPlayer是Android SDK提供的音视频播放组件,尽管目前有更优秀的IJKPlayer、EXOPlayer等开源项目,MediaPlayer作为功能单一、接口清晰的播放器,有其值得学习的意义。一切故事,从一张状态机图片开始。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200303_weekly/mediaplayer_state_diagram.gif" alt="状态机" title=""> </div> <div class="image-caption">状态机</div> </figure><p><strong>图例说明</strong>:<code>单箭头</code>表示同步调用,<code>双箭头</code>表示异步调用,双层椭圆(仅End)表示终结态。</p><p>这张图乍一看像是一团乱麻,其实可以按照播放前、播放中、播放后的阶段进行区分。</p><h3 id="播放前"><a href="#播放前" class="headerlink" title="播放前"></a>播放前</h3><ul><li>以<code>Prepared</code>为界,之前的状态都可以认为是“播放前”</li><li>通过<code>new</code>创建一个播放器,或者对已有播放器调用<code>reset</code>,均可以得到一个处于<code>Idle</code>状态的播放器。不过这两种方式有一个显著区别,即对<code>Idle</code>态播放器调用<code>getCurrentPosition()</code>, <code>getDuration()</code>, <code>getVideoHeight()</code>, <code>getVideoWidth()</code>, <code>setAudioAttributes()</code>, <code>setLooping()</code>, <code>setVolume()</code>, <code>pause()</code>, <code>start()</code>, <code>stop()</code>, <code>seekTo()</code>, <code>prepare()</code>, <code>prepareAsync()</code>方法时,如果是新构建的播放器,不会抛出任何一场,而如果是通过<code>reset</code>得到的<code>Idle</code>播放器,则会进入<code>OnErrorListener.onError()</code>回调</li><li>播放器在开始播放前,必须进入<code>Prepared</code>态。有两种方法,分别是同步的<code>prepare()</code>和异步的<code>prepareAsync()</code>。同步方法的返回是很快的,几乎是瞬间。对于异步调用,可以通过<code>setOnPreparedListener()</code>设置监听</li><li>当播放器处于<code>Prepared</code>态时,可以设置音量、屏幕常亮、循环播放等属性</li></ul><h3 id="播放中"><a href="#播放中" class="headerlink" title="播放中"></a>播放中</h3><ul><li>播放过程可能因为各种原因发生异常,诸如不支持的音视频格式、受损的文件、分辨率过高、解码超市等等原因,或者是对于播放器调用了不属于其状态的方法。在这些错误发生时,会走到<code>OnErrorListener.onError()</code>回调中,因此在播放前设置监听<code>setOnErrorListener()</code>是非常重要的</li><li>设置<code>onError</code>监听并不能避免播放器进入<code>Error</code>态,只是在进入时发出程序可以观测到的监听事件</li><li>如果在错误的状态调用<code>prepare()</code>, <code>prepareAsync()</code>, <code>setDatasource()</code>,会导致<code>IllegalStateException</code></li><li>基于上一条,在调用<code>setDatasource</code>以及它的众多重载方法时,必须捕获<code>IllegalArgumentException</code>和<code>IOException</code></li><li>通过<code>start()</code>启动播放,通过<code>isPlaying()</code>判断当前是否处于播放中,可以在<code>start()</code>后继续调用<code>start()</code>,但这不会产生任何影响</li><li>在开始播放后,可以通过<code>setOnBufferingUpdateListener()</code>监听视频缓冲进度</li><li>对于播放中的视频,调用<code>pause()</code>进入<code>Paused</code>态,这是一个略微有延迟(seconds)的调用,意味着<code>isPlaying()</code>可能不会立即反映当前状态,反之亦然</li><li>对于<code>Started</code>, <code>Paused</code>, <code>Prepared</code>, <code>PlaybackCompleted</code>态的播放器调用<code>stop()</code>,使其进入<code>Stopped</code>态;对于<code>Stopped</code>态的播放器,必须使其再次进入<code>Prepared</code>态后,方可用于播放</li><li>与<code>start()</code>一样,多次调用<code>stop()</code>不会产生任何影响</li><li>用<code>seekTo()</code>设置播放进度,这是一个异步方法,<code>OnSeekComplete.onSeekComplete()</code>用于监听;可以在<code>Prepared</code>, <code>Paused</code>, <code>PlaybackCompleted</code>多个态调用,且调用<code>seekTo()</code>后播放器仍保持原状态,同时改变当前帧;相应的,<code>getCurrentPosition()</code>可以返回当前的播放进度</li></ul><h3 id="播放后"><a href="#播放后" class="headerlink" title="播放后"></a>播放后</h3><ul><li>一旦播放器不再使用,建议立即调用<code>release()</code>释放资源,此后播放器进入<code>End</code>态,且再也无法通过任何方法使其恢复</li><li>如果设置了Looping,播放完成后会保持<code>Started</code>态,否则会进入<code>OnCompletionListener</code>回调,并进入<code>PlaybackCompleted</code>态</li></ul><h3 id="播放器的权限要求"><a href="#播放器的权限要求" class="headerlink" title="播放器的权限要求"></a>播放器的权限要求</h3><p>视需求而定,可能需要<code>WAKE_LOCK</code>以及<code>Internet</code>权限。</p><h3 id="线程限制"><a href="#线程限制" class="headerlink" title="线程限制"></a>线程限制</h3><p>必须在UI线程创建播放器,只有这样才能正常收到为播放器设置的各种回调。</p><p><strong>参考</strong>:<a href="https://developer.android.com/reference/android/media/MediaPlayer" target="_blank" rel="noopener">https://developer.android.com/reference/android/media/MediaPlayer</a></p>]]></content>
<summary type="html">
<blockquote>
<p>我们一路奋斗,不是为了改变世界,而是为了不被世界改变。</p>
</blockquote>
<h2 id="ViewPager使用指南"><a href="#ViewPager使用指南" class="headerlink" title="View
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="每周积累" scheme="https://lilei.pro/tags/%E6%AF%8F%E5%91%A8%E7%A7%AF%E7%B4%AF/"/>
</entry>
<entry>
<title>本周知识积累[2020/02] RecyclerView布局预览;Uri对象常用操作;轻松创建MainDispatcher与Deferred</title>
<link href="https://lilei.pro/2020/02/28/weekly-2020-02-01/"/>
<id>https://lilei.pro/2020/02/28/weekly-2020-02-01/</id>
<published>2020-02-28T01:00:37.000Z</published>
<updated>2020-03-04T12:03:34.320Z</updated>
<content type="html"><![CDATA[<blockquote><p>In case of I don’t see you. Good afternoon, good evening and good night.</p></blockquote><h2 id="在xml预览中查看RecyclerView布局"><a href="#在xml预览中查看RecyclerView布局" class="headerlink" title="在xml预览中查看RecyclerView布局"></a>在xml预览中查看RecyclerView布局</h2><p>在功能开发的前期,我们做好布局后,希望可以在IDE的xml预览里查看效果。在使用到RecyclerView时,看到的往往是item 0、item 1、item 2这样的占位文字,如下左图所示。如果我们想查看设计好的item布局(如下右),应当如何做呢?</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20200228_weekly/RecyclerView预览.jpg" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>可以使用<strong>tools</strong>的命名空间。</p><blockquote><p><code>tools</code> namespace enables design-time features (such as which layout to show in a fragment) or compile-time behaviors (such as which shrinking mode to apply to your XML resources) It is really powerful feature that is developing and allows you not compile code every time to see changes.</p></blockquote><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 样例代码 --></span></span><br><span class="line"><span class="comment"><!-- AndroidX --></span></span><br><span class="line"><span class="tag"><<span class="name">androidx.recyclerview.widget.RecyclerView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">tools:layoutManager</span>=<span class="string">"androidx.recyclerview.widget.GridLayoutManager"</span></span></span><br><span class="line"><span class="tag"></span></span><br><span class="line"><!-- support --></span><br><span class="line"><span class="tag"><<span class="name">android.support.v7.widget.RecyclerView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">tools:layoutManager</span>=<span class="string">"android.support.v7.widget.GridLayoutManager"</span></span></span><br><span class="line"><span class="tag"></span></span><br><span class="line"> <!-- common --></span><br><span class="line"> xmlns:android="http://schemas.android.com/apk/res/android"</span><br><span class="line"> xmlns:tools="http://schemas.android.com/tools"</span><br><span class="line"> android:layout_width="match_parent"</span><br><span class="line"> android:layout_height="match_parent"</span><br><span class="line"> tools:itemCount="5"</span><br><span class="line"> tools:listitem="@layout/item_video"</span><br><span class="line"> tools:orientation="horizontal"</span><br><span class="line"> tools:scrollbars="horizontal"</span><br><span class="line"> tools:spanCount="2" /></span><br></pre></td></tr></table></figure><p><strong>参考</strong> <a href="https://stackoverflow.com/questions/29929963/is-there-a-way-to-show-a-preview-of-a-recyclerviews-contents-in-the-android-stu" target="_blank" rel="noopener">https://stackoverflow.com/questions/29929963/is-there-a-way-to-show-a-preview-of-a-recyclerviews-contents-in-the-android-stu</a></p><h2 id="Uri对象的基本操作"><a href="#Uri对象的基本操作" class="headerlink" title="Uri对象的基本操作"></a>Uri对象的基本操作</h2><p>不属于新知识,做网络请求开发时经常会遇到,以前每次碰到时都是现查的,不知道是不是年纪大了记忆力下降的缘故 :-( 这里整理作为备忘。</p><p>Uri是不可变对象,构建出来后即不可修改。</p><p>Uri采用构建器模式进行构建。</p><p>初始化一个Uri.Builder对象:<code>Uri.parse(SAMPLE_URL).buildUpon()</code>,如下例</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> uriBuilder = Uri.parse(<span class="string">"https://www.google.com"</span>).buildUpon()</span><br></pre></td></tr></table></figure><p>进行UTF8编码:<code>encode(String s)</code></p><p>增加参数:<code>appendQueryParameter("key", "value")</code></p><p>分开设置协议和主机:<code>scheme("https")</code>,<code>authority("www.google.com")</code></p><p>举例说明,如果想要构建如下的URL地址</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://www.myawesomesite.com/turtles/types?type=1&sort=relevance#section-name</span><br></pre></td></tr></table></figure><p>则使用以下代码实现</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Uri.Builder builder = <span class="keyword">new</span> Uri.Builder();</span><br><span class="line">builder.scheme(<span class="string">"https"</span>)</span><br><span class="line"> .authority(<span class="string">"www.myawesomesite.com"</span>)</span><br><span class="line"> .appendPath(<span class="string">"turtles"</span>)</span><br><span class="line"> .appendPath(<span class="string">"types"</span>)</span><br><span class="line"> .appendQueryParameter(<span class="string">"type"</span>, <span class="string">"1"</span>)</span><br><span class="line"> .appendQueryParameter(<span class="string">"sort"</span>, <span class="string">"relevance"</span>)</span><br><span class="line"> .fragment(<span class="string">"section-name"</span>);</span><br><span class="line">String myUrl = builder.build().toString();</span><br></pre></td></tr></table></figure><p><strong>参考</strong></p><ul><li><a href="https://developer.android.com/reference/android/net/Uri" target="_blank" rel="noopener">https://developer.android.com/reference/android/net/Uri</a></li><li><a href="https://developer.android.com/reference/android/net/Uri.Builder" target="_blank" rel="noopener">https://developer.android.com/reference/android/net/Uri.Builder</a></li></ul><h2 id="轻松创建MainDispatcher与Deferred"><a href="#轻松创建MainDispatcher与Deferred" class="headerlink" title="轻松创建MainDispatcher与Deferred"></a>轻松创建MainDispatcher与Deferred</h2><h3 id="构建主线程调度器"><a href="#构建主线程调度器" class="headerlink" title="构建主线程调度器"></a>构建主线程调度器</h3><p>Dispatcher/调度器是协程上下文里很重要的一个元素,它决定了代码运行在哪个线程中。尤其是做Android开发时,经常需要把UI操作发送给主线程进行,像ViewModel就为我们提供了MainDispatcher。</p><p>如果当前没有MainDispatcher,需要手动创建时,要怎么方便地进行构建呢?Kotlin在<code>Handler</code>类上新增了一个扩展函数<code>asCoroutineDispatcher()</code>,用法如下</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> mainDispatcher = Handler(Looper.getMainLooper()).asCoroutineDispatcher()</span><br></pre></td></tr></table></figure><p>只要通过MainLooper就可以构建出主线程调度器了,是不是很方便。</p><h3 id="构建即时Deferred对象"><a href="#构建即时Deferred对象" class="headerlink" title="构建即时Deferred对象"></a>构建即时Deferred对象</h3><p>Deferred是一种泛型,在协程中用于表示耗时操作结果,当我们需要取值时,通过<code>Deferred.await()</code>即可。它有点类似Java中的<code>Future</code>,表示一种“进行中的操作,即将返回结果”。</p><p>构建Deferred对象有两种方法,一种是通过<code>async</code>的携程构建器,另一种是今天要介绍的方法,对于已知的值(比如 10),可以直接用<code>CompletableDeferred(10)</code>来创建一个Deferred对象,你可以直接把它用在任何需要Deferred对象的地方,取值时则会直接返回10。</p><p><strong>参考</strong> <a href="https://stackoverflow.com/questions/53273361/how-to-return-deferred-with-the-instant-results" target="_blank" rel="noopener">https://stackoverflow.com/questions/53273361/how-to-return-deferred-with-the-instant-results</a></p>]]></content>
<summary type="html">
<blockquote>
<p>In case of I don’t see you. Good afternoon, good evening and good night.</p>
</blockquote>
<h2 id="在xml预览中查看RecyclerView布局">
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="每周积累" scheme="https://lilei.pro/tags/%E6%AF%8F%E5%91%A8%E7%A7%AF%E7%B4%AF/"/>
</entry>
<entry>
<title>Kotlin 协程基础课 03.揭开协程上下文CoroutineContext的神秘面纱</title>
<link href="https://lilei.pro/2019/12/13/kotlin-coroutines-03/"/>
<id>https://lilei.pro/2019/12/13/kotlin-coroutines-03/</id>
<published>2019-12-13T14:40:28.000Z</published>
<updated>2019-12-13T14:41:37.280Z</updated>
<content type="html"><![CDATA[<blockquote><p>限定目的,能使人生变得简洁。</p></blockquote><h1 id="协程上下文是个啥?"><a href="#协程上下文是个啥?" class="headerlink" title="协程上下文是个啥?"></a>协程上下文是个啥?</h1><p>CoroutineContext,译作“协程上下文”,在协程中是非常重要的概念。你可能会比较好奇,为什么之前都没有注意到它的存在呢?因为协程框架已经为我们包装得非常好了。让我们来看一下<code>launch</code>和<code>async</code>两个函数的签名:</p><h2 id="launch"><a href="#launch" class="headerlink" title="launch"></a>launch</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> CoroutineScope.<span class="title">launch</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> context: <span class="type">CoroutineContext</span> = EmptyCoroutineContext,</span></span></span><br><span class="line"><span class="function"><span class="params"> start: <span class="type">CoroutineStart</span> = CoroutineStart.DEFAULT,</span></span></span><br><span class="line"><span class="function"><span class="params"> onCompletion: <span class="type">CompletionHandler</span>? = <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> block: <span class="type">suspend</span> <span class="type">CoroutineScope</span>.()</span></span> -> <span class="built_in">Unit</span></span><br><span class="line">): Job</span><br></pre></td></tr></table></figure><h2 id="async"><a href="#async" class="headerlink" title="async"></a>async</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type"><T></span> CoroutineScope.<span class="title">async</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> context: <span class="type">CoroutineContext</span> = EmptyCoroutineContext,</span></span></span><br><span class="line"><span class="function"><span class="params"> start: <span class="type">CoroutineStart</span> = CoroutineStart.DEFAULT,</span></span></span><br><span class="line"><span class="function"><span class="params"> onCompletion: <span class="type">CompletionHandler</span>? = <span class="literal">null</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> block: <span class="type">suspend</span> <span class="type">CoroutineScope</span>.()</span></span> -> T</span><br><span class="line">): Deferred<T></span><br></pre></td></tr></table></figure><p>可以看到这两个函数的第一个参数都是<code>CoroutineContext</code>类型的。所有协程构建函数(如<code>launch</code>和<code>async</code>)都是以<code>CoroutineScope</code>的扩展函数的形式被定义的,而<code>CoroutineScope</code>接口唯一的成员就是<code>CoroutineContext</code>类型。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">CoroutineScope</span> </span>{</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * The context of this scope.</span></span><br><span class="line"><span class="comment"> * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.</span></span><br><span class="line"><span class="comment"> * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">val</span> coroutineContext: CoroutineContext</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>协程上下文是协程必备的组成部分,它管理了协程的线程绑定、生命周期、异常处理和调试功能,接下来我们分析上下文具体的结构组成。</p><h1 id="协程上下文的结构"><a href="#协程上下文的结构" class="headerlink" title="协程上下文的结构"></a>协程上下文的结构</h1><blockquote><p>It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key. Keys are compared by reference.</p></blockquote><p>CoroutineContext接口跟Map很类似,具有如下特点:</p><ul><li>有序Map</li><li>Key唯一</li><li>类型安全</li></ul><p>##跟Map类似,为什么不直接用Map</p><p>那么我们为什么不直接用Map来实现呢?参考下面一段代码,它用Map实现了类似CoroutineContext的功能</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">typealias CoroutineContext = Map<CoroutineContext.Key<*>, CoroutineContext.Element></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">get</span><span class="params">(key: <span class="type">CoroutineContext</span>.<span class="type">Key</span><*>)</span></span>: CoroutineContext.Element?</span><br></pre></td></tr></table></figure><p>如果使用这种实现,我们每次调用<code>get</code>之后,必须用显式的类型转换,才能得到想要的<code>Element</code>类型。而CoroutineContext则通过泛型(CoroutineContext的Key即带有类型信息)为我们解决了类型转换的痛点:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type"><E : Element></span> <span class="title">get</span><span class="params">(key: <span class="type">Key</span><<span class="type">E</span>>)</span></span>: E?</span><br></pre></td></tr></table></figure><h1 id="在CoroutineContext上可进行的操作"><a href="#在CoroutineContext上可进行的操作" class="headerlink" title="在CoroutineContext上可进行的操作"></a>在CoroutineContext上可进行的操作</h1><p>CoroutineContext并未实现标准的集合接口,因此无法使用<code>iterator()</code>等集合标准操作。它有独特的一套操作。</p><h2 id="“拼装”上下文对象"><a href="#“拼装”上下文对象" class="headerlink" title="“拼装”上下文对象"></a>“拼装”上下文对象</h2><p>对CoroutineContext来说,最重要的操作是<code>plus</code>,<code>plus</code>操作用于把两个CoroutineContext对象合并成一个。合并时有一个优先级规则:<code>plus</code>右侧对象的属性会覆盖左侧对象中的同名属性。</p><blockquote><p>[The plus operator] returns a context containing elements from this context and elements from other context. The elements from this context with the same key as in the other one are dropped.</p></blockquote><h2 id="Element即是CoroutineContext"><a href="#Element即是CoroutineContext" class="headerlink" title="Element即是CoroutineContext"></a>Element即是CoroutineContext</h2><p>我们知道CoroutineContext中的Value是Element,其实Element本身也是继承于CoroutineContext。这样做的好处是,当我们只有一个CoroutineContext.Element对象时,也可以把它作为一个CoroutineContext来使用,这种一般称之为<strong>singleton context</strong>。</p><h2 id="空对象"><a href="#空对象" class="headerlink" title="空对象"></a>空对象</h2><p>除了<strong>singleton context</strong>,还有一种特殊的上下文对象,<code>EmptyCoroutineContext</code>。它不含有任何Element,因此,当使用<code>plus</code>连接符连接一个<code>EmptyCoroutineContext</code>和另一个上下文对象时,总是得到与另一个上下文对象相同的对象。</p><h1 id="认识一下那些Elements"><a href="#认识一下那些Elements" class="headerlink" title="认识一下那些Elements"></a>认识一下那些Elements</h1><p>如果我们想要查看CoroutineContext里都可以包含哪些Elements,可以搜索CoroutineContext.Key接口的实现,因为CoroutineContext是一个保存类型确定元素的Map。经过搜索后,我们发现以下几个典型Element:</p><h2 id="指定执行线程:ContinuationInterceptor"><a href="#指定执行线程:ContinuationInterceptor" class="headerlink" title="指定执行线程:ContinuationInterceptor"></a>指定执行线程:ContinuationInterceptor</h2><p>用于处理协程挂载在线程上的逻辑,抽象类<strong>CoroutineDispatcher</strong>实现了该接口,一般常用的Dispatcher都会继承于<strong>CoroutineDispatcher</strong>。</p><h2 id="层级关系管理:Job"><a href="#层级关系管理:Job" class="headerlink" title="层级关系管理:Job"></a>层级关系管理:Job</h2><p>用于管理任务层级,处理任务父子关系。</p><ul><li>手动终止父Job时,其中的子Job也被终止</li><li>当所有子Job运行结束时,父Job才可以运行结束</li></ul><h2 id="处理异常:CoroutineExceptionHandler"><a href="#处理异常:CoroutineExceptionHandler" class="headerlink" title="处理异常:CoroutineExceptionHandler"></a>处理异常:CoroutineExceptionHandler</h2><p>如果你在构建协程时使用了无法传递异常的构建器,如<code>launch</code>和<code>actor</code>,当异常发生时,需要有一个异常处理器来处理它。<code>CoroutineExceptionHandler</code>就是充当这样的异常处理器。</p><h2 id="名字:CoroutineName"><a href="#名字:CoroutineName" class="headerlink" title="名字:CoroutineName"></a>名字:CoroutineName</h2><p>协程的别名,一般是用于调试,以区分多个协程。</p><p>上述Element内部都以伴生对象的形式定义了相应的Key,可以通过<code>coroutineContext[element type name]</code>的形式方便地获取到Element对象。比如<code>coroutineContext[Job]</code>会返回Job或者null(如果没有Job)。</p><h1 id="协程构建过程中的CoroutineContext"><a href="#协程构建过程中的CoroutineContext" class="headerlink" title="协程构建过程中的CoroutineContext"></a>协程构建过程中的CoroutineContext</h1><p>前面讲过<code>CoroutineScope</code>实际上是一个<code>CoroutineContext</code>的封装,当我们需要启动一个协程时,会在<code>CoroutineScope</code>的实例上调用构建函数,如<code>async</code>和<code>launch</code>。在构建函数中,一共出现了3个CoroutineContext:</p><ul><li><strong>inherited context</strong>:从<code>CoroutineScope</code>中继承得到的上下文对象</li><li><strong>context argument</strong>:构建函数中传入的第一个参数,默认为<code>EmptyCoroutineContext</code></li><li><strong>coroutine context</strong>:挂起代码块(lambda函数)运行时的上下文对象</li></ul><p>如果我们查看协程构建函数<code>async</code>和<code>launch</code>的源码,会发现它们第一行都是如下代码:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> newContext = newCoroutineContext(context)</span><br></pre></td></tr></table></figure><p>再进一步查看:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CoroutineContext.kt</span></span><br><span class="line"><span class="meta">@ExperimentalCoroutinesApi</span></span><br><span class="line"><span class="keyword">public</span> actual <span class="function"><span class="keyword">fun</span> CoroutineScope.<span class="title">newCoroutineContext</span><span class="params">(context: <span class="type">CoroutineContext</span>)</span></span>: CoroutineContext {</span><br><span class="line"> <span class="keyword">val</span> combined = coroutineContext + context</span><br><span class="line"> <span class="keyword">val</span> debug = <span class="keyword">if</span> (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) <span class="keyword">else</span> combined</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">if</span> (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == <span class="literal">null</span>)</span><br><span class="line"> debug + Dispatchers.Default <span class="keyword">else</span> debug</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里就比较清晰了:构建器函数内部进行了一个CoroutineContext拼接操作,plus的左值是<code>CoroutineScope</code>内部的<code>CoroutineContext</code>,右值是作为构建函数参数的<code>CoroutineContext</code>。根据我们前面讲到的拼接操作,左值具有更高的优先级。</p><p>此外,抽象类<code>AbstractCoroutineScope</code>实现了<code>CoroutineScope</code>和<code>Job</code>接口,大部分CoroutineScope的实现都继承自<code>AbstractCoroutineScope</code>,意味着他们同时也是一个<code>Job</code>。可以得到:<strong>coroutine context = parent context + coroutine job</strong>。</p><h1 id="Elements默认值"><a href="#Elements默认值" class="headerlink" title="Elements默认值"></a>Elements默认值</h1><p>对于上述4个Elements,如果既没有显示指明,则会取相应的默认值:</p><ul><li>ContinuationInterceptor:默认值为<code>Dispatchers.Default</code>,基于线程池实现,线程数目=CPU数目,且最少为2支</li><li>Job:默认值为null,在这种情况下,协程是孤儿(没有父协程,无法被父协程取消,例如<code>GlobalScope</code>)</li><li>CoroutineExceptionHandler:它的情况比较复杂,当异常发生时,若没有指定<code>CoroutineExceptionHandler</code>,会使用全局的异常处理器,在全局异常处理器中调用当前线程的<code>uncaughtExceptionHandler</code>。代码如下:</li></ul><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CoroutineExceptionHandlerImpl.kt</span></span><br><span class="line"><span class="keyword">internal</span> actual <span class="function"><span class="keyword">fun</span> <span class="title">handleCoroutineExceptionImpl</span><span class="params">(context: <span class="type">CoroutineContext</span>, exception: <span class="type">Throwable</span>)</span></span> {</span><br><span class="line"> <span class="comment">// use additional extension handlers</span></span><br><span class="line"> <span class="keyword">for</span> (handler <span class="keyword">in</span> handlers) {</span><br><span class="line"> handler.handleException(context, exception)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// use thread's handler</span></span><br><span class="line"> <span class="keyword">val</span> currentThread = Thread.currentThread()</span><br><span class="line"> currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>Name:默认为“coroutine”</li></ul><p>对于上述默认值,用代码实现起来也并不复杂:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> defaultExceptionHandler = CoroutineExceptionHandler { ctx, t -></span><br><span class="line"> ServiceLoader.load(</span><br><span class="line"> serviceClass, </span><br><span class="line"> serviceClass.classLoader</span><br><span class="line"> ).forEach{</span><br><span class="line"> it.handleException(ctx, t)</span><br><span class="line"> }</span><br><span class="line"> Thread.currentThread().let { </span><br><span class="line"> it.uncaughtExceptionHandler.uncaughtException(it, exception)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CoroutineContext</span></span>(</span><br><span class="line"> <span class="keyword">val</span> continuationInterceptor: ContinuationInterceptor = </span><br><span class="line"> Dispatchers.Default,</span><br><span class="line"> <span class="keyword">val</span> parentJob: Job? = </span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="keyword">val</span> coroutineExceptionHandler: CoroutineExceptionHandler = </span><br><span class="line"> defaultExceptionHandler,</span><br><span class="line"> <span class="keyword">val</span> name: CoroutineName = </span><br><span class="line"> CoroutineName(<span class="string">"coroutine"</span>)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><h1 id="用例浅析"><a href="#用例浅析" class="headerlink" title="用例浅析"></a>用例浅析</h1><p>上述4个Elements中,最重要的是Dispatcher和Job两个,我们来看一些例子。</p><h2 id="Global-Scope-Context"><a href="#Global-Scope-Context" class="headerlink" title="Global Scope Context"></a>Global Scope Context</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">GlobalScope.launch {</span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>全局Scope使用全默认的4个Elements,意味着它使用<code>Dispatchers.Default</code>和为空的<code>Job</code>(无法通过父Job取消)。</p><h2 id="Fully-Qualified-Context"><a href="#Fully-Qualified-Context" class="headerlink" title="Fully Qualified Context"></a>Fully Qualified Context</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">launch(</span><br><span class="line"> Dispatchers.Main + </span><br><span class="line"> Job() + </span><br><span class="line"> CoroutineName(<span class="string">"HelloCoroutine"</span>) + </span><br><span class="line"> CoroutineExceptionHandler { _, _ -> <span class="comment">/* ... */</span> }</span><br><span class="line">) {</span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>全限定Context,即全部显式指定具体值的Elements。不论你用哪一个<code>CoroutineScope</code>构建该协程,它都具有一致的表现,不会受到<code>CoroutineScoipe</code>任何影响。</p><h2 id="CoroutineScope-Context"><a href="#CoroutineScope-Context" class="headerlink" title="CoroutineScope Context"></a>CoroutineScope Context</h2><p>这里我们基于Activity生命周期实现一个CoroutineScope:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">ScopedAppActivity</span>:<span class="type"></span></span></span><br><span class="line"> AppCompatActivity(),</span><br><span class="line"> CoroutineScope</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> job: Job</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> coroutineContext: CoroutineContext</span><br><span class="line"> <span class="keyword">get</span>() = job + Dispatchers.Main <span class="comment">// 注意这里使用+拼接CoroutineContext</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> job = Job()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onDestroy</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onDestroy()</span><br><span class="line"> job.cancel()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>Dispatcher:使用<code>Dispatcher.Main</code>,以在UI线程进行绘制</li><li>Job:在<code>onCreate</code>时构建,在<code>onDestroy</code>时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题</li></ul><h2 id="临时指定参数"><a href="#临时指定参数" class="headerlink" title="临时指定参数"></a>临时指定参数</h2><p>前面数次提到过,CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用<code>withContext</code>便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">withContext(NonCancellable) {</span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="读取协程上下文参数"><a href="#读取协程上下文参数" class="headerlink" title="读取协程上下文参数"></a>读取协程上下文参数</h2><p>可以通过顶级挂起只读属性<code>coroutineContext</code>获取协程上下文参数,它位于 <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/coroutine-context.html" target="_blank" rel="noopener">kotlin-stdlib / kotlin.coroutines / coroutineContext</a>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">println(<span class="string">"Running in <span class="subst">${coroutineContext[CoroutineName]}</span>"</span>)</span><br></pre></td></tr></table></figure><h2 id="Nested-Context"><a href="#Nested-Context" class="headerlink" title="Nested Context"></a>Nested Context</h2><p>内嵌上下文切换是指:在协程A内部构建协程B时,B会自动继承A的Dispatcher,如果没有注意这一点,很容易发生诸如“主线程执行耗时操作”的错误。</p><p>我们可以在调用<code>async</code>时加入Dispatcher参数,以切换到工作线程。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误的做法,在主线程中直接调用async,若耗时过长则阻塞UI</span></span><br><span class="line">GlobalScope.launch(Dispatchers.Main) {</span><br><span class="line"> <span class="keyword">val</span> deferred = async {</span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line"> } </span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确的做法,在工作线程执行协程任务</span></span><br><span class="line">GlobalScope.launch(Dispatchers.Main) {</span><br><span class="line"> <span class="keyword">val</span> deferred = async(Dispatchers.Default) {</span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line"> } </span><br><span class="line"> <span class="comment">/* ... */</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><ul><li>协程上下文环境参数可以用加号<code>+</code>拼接,左值优先</li><li>上下文环境可以继承</li><li>上下文环境可以单独制定参数</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>本文的70%都翻译总结自Medium上的这篇文章,写的非常棒,建议有英文阅读能力的同学直接阅读原文</p><ul><li><a href="https://proandroiddev.com/demystifying-coroutinecontext-1ce5b68407ad" target="_blank" rel="noopener">Demystifying CoroutineContext</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>限定目的,能使人生变得简洁。</p>
</blockquote>
<h1 id="协程上下文是个啥?"><a href="#协程上下文是个啥?" class="headerlink" title="协程上下文是个啥?"></a>协程上下文是个啥?<
</summary>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>Kotlin 协程基础课 02.suspend函数</title>
<link href="https://lilei.pro/2019/12/10/kotlin-coroutines-02/"/>
<id>https://lilei.pro/2019/12/10/kotlin-coroutines-02/</id>
<published>2019-12-10T14:41:39.000Z</published>
<updated>2019-12-10T14:42:46.058Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是《Kotlin 协程基础课》的第2篇文章。</p></blockquote><blockquote><p>Everyone must choose one of two pains: The pain of discipline or the pain of regret. Choose WISELY.</p></blockquote><p>在上篇文章里我们学习了如何通过协程简化耗时操作的写法,其中有一个关键字<code>suspend</code>,用于在定义函数时进行声明。本篇文章将对suspend进行进一步介绍,旨在学会它的含义和用法。</p><h1 id="suspend限定词的含义"><a href="#suspend限定词的含义" class="headerlink" title="suspend限定词的含义"></a>suspend限定词的含义</h1><p><strong>suspend</strong>,翻译过来就是<strong>中断,挂起</strong>,跟<strong>public、static</strong>等关键字相同,用在函数声明前,表示这是一个“挂起函数”。</p><p>挂起函数只能在协程或另一个挂起函数中被调用,如果你在非协程中使用到了挂起函数,会看到编译器有如下报错:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function</span><br></pre></td></tr></table></figure><p>前面用到的<code>delay</code>就是一个挂起函数。<strong>suspend</strong>关键字表明函数内部进行了耗时操作,可以是计算密集型的CPU任务,也可以是网络、磁盘操作密集型的IO任务。基本上可以看做“但凡用callback实现的回调函数,都能用一个相对应的挂起函数实现”。</p><p>所以我们使用<strong>suspend</strong>关键字的时机就非常明确了:当函数要进行耗时操作时,就把它声明为<code>suspend</code>。</p><h1 id="suspend做了什么事"><a href="#suspend做了什么事" class="headerlink" title="suspend做了什么事"></a>suspend做了什么事</h1><p><em>方便起见,后续用“挂起”指代“suspend”。</em></p><p>作为及物动词,“挂起”应当有一个宾语,这里“挂起”的对象是<strong>协程</strong>。接下来我们对挂起的过程中发生了什么一探究竟,记住下面这句话:</p><p><strong>“挂起”是指协程从它当前线程脱离,切换到另一个线程运行。</strong>当线程运行到<code>suspend</code>函数时,会暂时挂起这个函数及后续代码的执行。这里涉及到两个角色:线程和协程。</p><h2 id="线程的行为"><a href="#线程的行为" class="headerlink" title="线程的行为"></a>线程的行为</h2><p>当线程运行到“挂起”代码块时,会跳出当前的代码块,不再执行后续代码。接下来线程会做什么呢?</p><p>如果它是一个后台线程:</p><ul><li>要么无事可做,被系统回收</li><li>要么被调度执行别的后台任务</li></ul><p>跟Java线程池里的线程在工作结束之后的表现完全一样:<strong>回收或者再利用</strong>。</p><p>如果它是Android主线程:</p><ul><li>继续UI刷新工作</li></ul><h2 id="协程的行为"><a href="#协程的行为" class="headerlink" title="协程的行为"></a>协程的行为</h2><p>上面讲到线程运行到挂起代码块时,会暂时退出当前代码块的执行。那么,剩余的协程代码在哪里得到执行呢?答案就在挂起函数的实现中——即我们为挂起函数指定的线程。</p><p><code>withContext</code>函数可以指定协程代码的运行线程,常见的Dispatcher有Main、IO、Default。协程从挂起的地方开始,切换到这些线程之中的一个继续运行,当运行完毕时,会<strong>自动切换回原线程执行</strong>。</p><p>在协程的源码里,“自动切换回来”是通过<a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/resume.html" target="_blank" rel="noopener">resume</a>实现的。挂起函数之所以必须在协程中调用,就是因为协程框架会自动帮我们处理这个自动切换的过程。</p><h2 id="suspend-不会真正操作挂起"><a href="#suspend-不会真正操作挂起" class="headerlink" title="suspend 不会真正操作挂起"></a>suspend 不会真正操作挂起</h2><p>并不是声明了<code>suspend</code>后,线程运行到该位置,就自动进行挂起切换的,参照下面一个例子,挂起函数仍然运行在主线程中。为什么没有切换线程?因为编译器根本不知道要往哪里切,需要我们在编码时明确告诉它,这个挂起函数要切换到哪一个线程继续运行。我们可以用<code>withContext</code>指明待切换的线程。在实现一个挂起函数时,仅仅加上<code>suspend</code>关键字是不够的,必须在函数内部直接或间接地调用协程框架自带的<code>suspend</code>函数。</p><p><code>suspend</code>只是一个提醒,它只有一个效果,就是限制函数只能在协程里调用,如果在非协程里使用了<code>suspend</code>函数,则编译不通过。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 仍然运行在Main</span></span><br><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">suspendingPrint</span><span class="params">()</span></span> {</span><br><span class="line"> println(<span class="string">"Thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">I/System.<span class="keyword">out</span>: Thread: main</span><br><span class="line"></span><br><span class="line"><span class="comment">// 运行在IO</span></span><br><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">suspendingPrint</span><span class="params">()</span></span> = withContext(Dispatchers.IO) {</span><br><span class="line"> println(<span class="string">"Thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="如何写一个suspend函数"><a href="#如何写一个suspend函数" class="headerlink" title="如何写一个suspend函数"></a>如何写一个suspend函数</h1><p>最简单的方式是:</p><ol><li>声明函数为<code>suspend</code></li><li>使用<code>withContext</code>指定目标线程,或者在函数内部调用另一个<code>suspend</code>函数</li></ol><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>本文介绍了协程中最常见的关键字<code>suspend</code>的含义和用法,阅读完本文后你应当掌握:</p><ul><li>挂起函数运行时的表现</li><li>什么情况下使用挂起函数</li><li>如何写一个简单的挂起函数</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://juejin.im/post/5da81352f265da5b774fc39d" target="_blank" rel="noopener">掘金-扔物线</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>这是《Kotlin 协程基础课》的第2篇文章。</p>
</blockquote>
<blockquote>
<p>Everyone must choose one of two pains: The pain of discipline or t
</summary>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>Kotlin 协程基础课 01.协程的基本概念与用法</title>
<link href="https://lilei.pro/2019/11/17/kotlin-coroutines-01/"/>
<id>https://lilei.pro/2019/11/17/kotlin-coroutines-01/</id>
<published>2019-11-17T00:57:40.000Z</published>
<updated>2019-12-03T14:27:46.210Z</updated>
<content type="html"><![CDATA[<blockquote><p>这是《Kotlin 协程基础课》的第1篇文章。</p></blockquote><blockquote><p>正因为她觉得一切都无所谓,所以生活给她什么,她便接受什么。少年时代,她觉得选择为时过早,而现在已是青年,她又觉得改变为时过晚。</p></blockquote><h1 id="系列基础课前言"><a href="#系列基础课前言" class="headerlink" title="系列基础课前言"></a>系列基础课前言</h1><p>对于Kotlin学习而言,要想从“入门”走到“精通”,协程(Coroutines)是必须迈过去的一道坎。接下来一周时间,我会在之前零散学习的基础上,总结成一系列基础课文章,作为对协程的阶段性学习小结。文章目录如下:</p><ul><li>01.协程的基本概念与用法</li><li>02.非阻塞式挂起(suspend)函数</li><li>03.理解协程的域(Scope)和调度器(Dispatcher)</li><li>04.用AAC&协程优化Android架构设计</li></ul><h1 id="什么是协程"><a href="#什么是协程" class="headerlink" title="什么是协程"></a>什么是协程</h1><p>但凡学一门新知识,总是离不开5w1h。说起“协程”,很多人第一反应是2号线北新泾那家做旅游的互联网公司,不过,此“协程”非彼“携程”,协程(Coroutines)并不是一个新的概念,它的年纪要比Kotlin语言大得多。“协程 Coroutines”源自 Simula 和 Modula-2 语言,这个术语早在 1958 年就被 Melvin Edward Conway 发明并用于构建汇编程序,说明协程是一种编程思想,并不局限于特定的语言。目前很多现代语言都有协程的实现,比如Go、JavaScript、C#等。</p><p>协程作为一种编程思想,目的是简化并行代码编写,可以让我们以同步的方式写异步逻辑。对于Kotlin而言,“协程”一词是指实现了协程思想的一系列API的总称。Kotlin协程的底层实现是线程。</p><h1 id="没有协程的日子里"><a href="#没有协程的日子里" class="headerlink" title="没有协程的日子里"></a>没有协程的日子里</h1><p>由于Android是单一UI线程的框架,势必要进行很多UI线程以外的耗时操作,在协程之前,我们通常用这些技术来实现诸如网络请求、数据库读写等功能:</p><ul><li>AsyncTask:是Android原生的异步任务写法,写过的人就知道它有多难用,业务逻辑被分散在前中后三个方法里,冗长的boilerplate代码。更有甚者,一旦发生嵌套,光是一层层回调就能把人搞疯掉。</li><li>Thread:直接开线程并不是一种好的设计,只有新手才这么干。</li><li>ThreadPool or ExecutorService:这比<code>new Thread</code>好一些,但同样要处理UI、工作线程切换的问题,以及无法避免的回调。</li><li>Handler:一个工作线程的Handler用来处理耗时任务,处理完成后丢给主线程Handler,简单、直接、朴实无华。</li></ul><p>这时协程(Coroutines)来了,为我们推翻回调地狱、样板代码、内存泄漏、线程切换几座大山,Android开发者终于翻身农奴把歌唱,敢叫日月换新天。</p><h1 id="第一个协程Demo"><a href="#第一个协程Demo" class="headerlink" title="第一个协程Demo"></a>第一个协程Demo</h1><h2 id="用Thread-sleep模拟耗时操作"><a href="#用Thread-sleep模拟耗时操作" class="headerlink" title="用Thread.sleep模拟耗时操作"></a>用Thread.sleep模拟耗时操作</h2><p>在本文我们暂且不谈Android环境,在更通用的环境下展现协程的用法。首先我们模拟一个耗时操作与非耗时操作混合的场景,看下面一段代码,它在<code>main</code>函数里依次打印<code>one two three</code>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">(args: <span class="type">Array</span><<span class="type">String</span>>)</span></span> {</span><br><span class="line"> println(<span class="string">"one"</span>)</span><br><span class="line"> println(<span class="string">"two"</span>)</span><br><span class="line"> println(<span class="string">"three"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>同时有另一个函数,它会耗时3s后,打印出参数在控制台。我们用直白的<code>Thread.sleep</code>来进行延时模拟。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">printDelayed</span><span class="params">(msg: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Thread.sleep(<span class="number">3000</span>)</span><br><span class="line"> println(msg)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后将主程序的<code>print(two)</code>改为耗时任务。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">(args: <span class="type">Array</span><<span class="type">String</span>>)</span></span> {</span><br><span class="line"> println(<span class="string">"one"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two"</span>)</span><br><span class="line"> println(<span class="string">"three"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行后,输出如下,非常符合预期。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">one</span><br><span class="line">three</span><br><span class="line">// 这里等待3s</span><br><span class="line">two</span><br></pre></td></tr></table></figure><h2 id="用delay模拟耗时操作"><a href="#用delay模拟耗时操作" class="headerlink" title="用delay模拟耗时操作"></a>用delay模拟耗时操作</h2><p>Kotlin的协程库提供了另一种延时的API,<code>delay</code>,我们把原来的<code>Thread.sleep</code>替换为<code>delay</code>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">printDelayed</span><span class="params">(msg: <span class="type">String</span>)</span></span> {</span><br><span class="line"> delay(<span class="number">3000</span>L)</span><br><span class="line"> println(msg)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此时Android Studio会在<code>delay</code>处提示错误:<em>suspend function delay should be called only from a coroutine or another suspend function</em>。翻译过来就是“delay是一个挂起函数,它只能在协程中、或者另一个挂起函数里面被调用”。我们把程序整体改写一下。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">printDelayed</span><span class="params">(msg: <span class="type">String</span>)</span></span> {</span><br><span class="line"> delay(<span class="number">3000</span>L)</span><br><span class="line"> println(msg)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> println(<span class="string">"one"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two"</span>)</span><br><span class="line"> println(<span class="string">"three"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">(args: <span class="type">Array</span><<span class="type">String</span>>)</span></span> {</span><br><span class="line"> print123Blocking()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行后发现程序的输出为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">one</span><br><span class="line">// 等待3s</span><br><span class="line">two</span><br><span class="line">three</span><br></pre></td></tr></table></figure><p>为什么不是132而是123呢,要从<code>runBlocking</code>的定义说起。它会“新起一个协程运行后续代码,并且在该协程的运行过程中阻塞原线程(在demo中是主线程),直至协程运行结束”。而在协程运行时是按照代码顺序逐行运行的。所以打印出来的是123而非132。</p><h2 id="为runBlocking指定运行线程"><a href="#为runBlocking指定运行线程" class="headerlink" title="为runBlocking指定运行线程"></a>为runBlocking指定运行线程</h2><p>我们在<code>print123blocking</code>方法里打印出当前线程名。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>控制台输出如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">one - in thread: main @coroutine#1</span><br><span class="line">two - in thread: main @coroutine#1</span><br><span class="line">three - in thread: main @coroutine#1</span><br></pre></td></tr></table></figure><p>可见这个方法是在当前(main)线程里运行的,并且同属于<code>@coroutine#1</code>。</p><p>我们可以为<code>runBlocking</code>指定运行的线程,在协程的语言里,使用<code>Dispatcher</code>来表明这一概念。我们改写一下<code>print123Blocking</code>方法,使用<code>Dispatcher.Default</code>打印1和2。样例代码与系统输出如下,可见我们为其指定了Dispatcher的代码段运行在另一个线程里,且打印顺序仍然为123,这就是<strong>Blocking</strong>的厉害之处。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> {</span><br><span class="line"> runBlocking(Dispatchers.Default) {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>控制台输出:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">one - in thread: DefaultDispatcher-worker-1 @coroutine#1</span><br><span class="line">two - in thread: DefaultDispatcher-worker-1 @coroutine#1</span><br><span class="line">// 此处等待3s</span><br><span class="line">three - in thread: main</span><br></pre></td></tr></table></figure><h2 id="如何利用协程打印出132"><a href="#如何利用协程打印出132" class="headerlink" title="如何利用协程打印出132"></a>如何利用协程打印出132</h2><h3 id="全局后台线程:GlobalScope-launch"><a href="#全局后台线程:GlobalScope-launch" class="headerlink" title="全局后台线程:GlobalScope.launch"></a>全局后台线程:GlobalScope.launch</h3><p>可以在blocking域中使用<code>GlobalScope.launch{ ... }</code>来指定后台线程运行任务,我们基于此将原有代码改造一下。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> GlobalScope.launch {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段代码的输出是不可知的,有可能是以下两种情况,只打印3或者打印31,这是什么原因呢?因为我们是在后台全局线程中启动的“打印12任务”,后台线程是不会阻止主线程运行结束的,所以2是肯定打不出来,而1能否打印出来就看运行时线程调度情况了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// case 1</span><br><span class="line">three - in thread: main @coroutine#1</span><br><span class="line">one - in thread: DefaultDispatcher-worker-1 @coroutine#2</span><br><span class="line">// case 2</span><br><span class="line">three - in thread: main @coroutine#1</span><br></pre></td></tr></table></figure><h3 id="等待任务完成"><a href="#等待任务完成" class="headerlink" title="等待任务完成"></a>等待任务完成</h3><p>我们可以简单粗暴地使用<code>delay</code>来等待后台任务完成。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> GlobalScope.launch {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> delay(<span class="number">4000</span>L) <span class="comment">// 因为我们知道printDelayed会延迟3秒,故这里等待4秒</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>控制台输出为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">three - in thread: main @coroutine#1</span><br><span class="line">one - in thread: DefaultDispatcher-worker-1 @coroutine#2</span><br><span class="line">two - in thread: DefaultDispatcher-worker-1 @coroutine#2</span><br></pre></td></tr></table></figure><p>但是这种处理方法非常丑陋,而且这个<code>delay</code>的时长很难设置,设置长了吧,会导致无用的等待浪费时间;设置短了吧,有可能在后台线程输出之前就结束任务,有没有更优雅的写法呢?答案是有的,<code>job.join()</code>为我们提供了等待任务完成的功能,代码如下所示。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> <span class="keyword">val</span> job = GlobalScope.launch {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> job.join()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="自定义Dispatcher"><a href="#自定义Dispatcher" class="headerlink" title="自定义Dispatcher"></a>自定义Dispatcher</h2><p>Dispatcher为协程的运行指定了线程,常见Dispatchers如下:</p><ul><li>Dispatcher.IO:进行IO密集型操作,如数据库读写、文件读写、网络交互</li><li>Dispatcher.Default:进行CPU密集型操作,如列表排序、JSON解析、DiffUtils</li><li>Dispatcher.Main:仅存在于Android框架,调用<code>suspend</code>方法、进行UI操作、更新LiveData</li></ul><p>看到这里你可能已经理解了,Dispatcher其实就是线程的另一种表现形式,我们甚至可以自定义一个Dispatcher:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">print123Blocking</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> println(<span class="string">"one - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> <span class="keyword">val</span> customDispatcher = Executors.newFixedThreadPool(<span class="number">2</span>).asCoroutineDispatcher()</span><br><span class="line"> launch(customDispatcher) {</span><br><span class="line"> printDelayed(<span class="string">"two - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> println(<span class="string">"three - in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> (customDispatcher.executor <span class="keyword">as</span> ExecutorService).shutdown() <span class="comment">// !主动停止,否则线程会一直运行下去</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行结果如下(在play.kotlin上面总是超时不知道为啥),意味着我们完全可以高度定制Dispatcher的实现,虽然大部分时间使用默认的Dispatcher就已足够。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">one - from thread main</span><br><span class="line">three - from thread main</span><br><span class="line">two - from thread pool-1-thread-1</span><br></pre></td></tr></table></figure><h2 id="有返回值的suspend函数"><a href="#有返回值的suspend函数" class="headerlink" title="有返回值的suspend函数"></a>有返回值的suspend函数</h2><p>最后一部分内容是跟Android开发密切相关的,大部分时间我们需要通过网络、数据库进行一些读取数据耗时操作,函数会有返回值,我们模拟一个网络操作,它读取一个<code>startNum</code>参数,等待1s延时后,返回<code>startNum * 10</code>。通过 <code>async { ... }.await()</code> 可以获取耗时函数的返回值,代码如下:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 模拟1s延时网络操作</span></span><br><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">calculateHardThings</span><span class="params">(startNum: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span> {</span><br><span class="line"> delay(<span class="number">1000</span>)</span><br><span class="line"> println(<span class="string">"result: <span class="subst">${result}</span>, in thread: <span class="subst">${Thread.currentThread().name}</span>"</span>)</span><br><span class="line"> <span class="keyword">return</span> startNum * <span class="number">10</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用延时操作,分别await,耗时共3s</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">exampleAsyncAwait</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> <span class="keyword">val</span> startTime = System.currentTimeMillis()</span><br><span class="line"> <span class="keyword">val</span> deferred1 = async { calculateHardThings(<span class="number">10</span>) }.await()</span><br><span class="line"> <span class="keyword">val</span> deferred2 = async { calculateHardThings(<span class="number">20</span>) }.await()</span><br><span class="line"> <span class="keyword">val</span> deferred3 = async { calculateHardThings(<span class="number">30</span>) }.await()</span><br><span class="line"> <span class="keyword">val</span> sum = deferred1 + deferred2 + deferred3</span><br><span class="line"> <span class="keyword">val</span> endTime = System.currentTimeMillis()</span><br><span class="line"> println(<span class="string">"sum = <span class="variable">$sum</span>, time = <span class="subst">${endTime - startTime}</span>"</span>) <span class="comment">// sum = 600, time = 3030</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="并发进行耗时操作"><a href="#并发进行耗时操作" class="headerlink" title="并发进行耗时操作"></a>并发进行耗时操作</h3><p>因为是3个耗时1s操作并发,我们自然而然希望它们同时运行,总耗时1s而不是3s,要如何实现呢?把所有的<code>await()</code>调用写入同一个语句,编译器会优化它们,使其同时运行。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调用延时操作,同时await,耗时共1s</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">exampleAsyncAwait</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> <span class="keyword">val</span> startTime = System.currentTimeMillis()</span><br><span class="line"> <span class="keyword">val</span> deferred1 = async { calculateHardThings(<span class="number">10</span>) }</span><br><span class="line"> <span class="keyword">val</span> deferred2 = async { calculateHardThings(<span class="number">20</span>) }</span><br><span class="line"> <span class="keyword">val</span> deferred3 = async { calculateHardThings(<span class="number">30</span>) }</span><br><span class="line"> <span class="keyword">val</span> sum = deferred1.await() + deferred2.await() + deferred3.await()</span><br><span class="line"> <span class="keyword">val</span> endTime = System.currentTimeMillis()</span><br><span class="line"> println(<span class="string">"sum = <span class="variable">$sum</span>, time = <span class="subst">${endTime - startTime}</span>"</span>) <span class="comment">// sum = 600, time = 1065</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="使用withContext的简化写法"><a href="#使用withContext的简化写法" class="headerlink" title="使用withContext的简化写法"></a>使用withContext的简化写法</h3><p>async/await 会运行在当前线程中,对于网络操作一般的做饭是让其在IO线程运行,对于计算密集型操作则是在CPU(Default)线程运行,使用<code>withContext</code>可以同时完成<code>async/await</code>的操作,但缺点是这三个操作只能相继运行,无法同时运行。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 调用延时操作</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">exampleWithContext</span><span class="params">()</span></span> = runBlocking {</span><br><span class="line"> <span class="keyword">val</span> startTime = System.currentTimeMillis()</span><br><span class="line"> <span class="keyword">val</span> deferred1 = withContext(Dispatchers.Default) { calculateHardThings(<span class="number">10</span>) }</span><br><span class="line"> <span class="keyword">val</span> deferred2 = withContext(Dispatchers.Default) { calculateHardThings(<span class="number">20</span>) }</span><br><span class="line"> <span class="keyword">val</span> deferred3 = withContext(Dispatchers.Default) { calculateHardThings(<span class="number">30</span>) }</span><br><span class="line"> <span class="keyword">val</span> sum = deferred1 + deferred2 + deferred3</span><br><span class="line"> <span class="keyword">val</span> endTime = System.currentTimeMillis()</span><br><span class="line"> println(<span class="string">"sum = <span class="variable">$sum</span>, time = <span class="subst">${endTime - startTime}</span>"</span>) <span class="comment">// sum = 600, time = 3029</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>本文介绍了协程的基本用法,在阅读完本文后,你应当掌握以下知识点:</p><ul><li>声明耗时函数,以及在协程代码块里调用耗时函数</li><li>切换运行环境</li><li>等待任务完成</li><li>并行进行耗时操作,获取操作结果</li></ul><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><ul><li><a href="https://www.youtube.com/watch?v=jYuK1qzFrJg" target="_blank" rel="noopener">https://www.youtube.com/watch?v=jYuK1qzFrJg</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>这是《Kotlin 协程基础课》的第1篇文章。</p>
</blockquote>
<blockquote>
<p>正因为她觉得一切都无所谓,所以生活给她什么,她便接受什么。少年时代,她觉得选择为时过早,而现在已是青年,她又觉得改变为时过晚。</p
</summary>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>【转】Android Studio Debug 的 9 个小技巧</title>
<link href="https://lilei.pro/2019/11/11/android-studio-debug-skills/"/>
<id>https://lilei.pro/2019/11/11/android-studio-debug-skills/</id>
<published>2019-11-11T15:46:08.000Z</published>
<updated>2019-11-11T17:09:55.597Z</updated>
<content type="html"><![CDATA[<blockquote><p>Pain is inevitable. Suffering is optional. 痛楚难以避免,而磨难可以选择。</p></blockquote><p>这是一篇转载的文章。</p><ul><li>作者:wanbo</li><li>链接:<a href="https://juejin.im/post/5dbf8036f265da4d4b5fe7c2" target="_blank" rel="noopener">https://juejin.im/post/5dbf8036f265da4d4b5fe7c2</a></li><li>来源:掘金</li></ul><hr><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/dev_summit_19.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>周末看 Android Dev Summit ‘19 的视频的时候,看到一章关于 Android Studio Debug 的介绍,有很多日常非常有用的小技巧,学习了这些小技巧能很大程度的降低我们 Debug 的成本,快速定位问题的本质,今天就向大家介绍一下 Android Studio Debug 的 9 个小技巧。</p><p>没关注的小伙伴记得关注订阅😝,如果觉得这些文章有点意思,记得分享转发评论点赞😝!</p><h2 id="Log-过滤和折叠"><a href="#Log-过滤和折叠" class="headerlink" title="Log 过滤和折叠"></a>Log 过滤和折叠</h2><p>有时候 Logcat 中 log 的信息很长,同时还有些我们不需要的信息也打印出来,例如下图中的【时间+线程 ID】。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/log_filter_0.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>这个时候我们可以点击 Logcat 窗口上的【设置】按钮,设置一条 Log 需要显示哪些关键信息,可设置的项有:时间、线程ID、包名、Tag name。我们可以根据自己的需要控制显示,并且下面还会有一条 Sample Log 提供设置后的预览效果,就像下图中这样。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/log_filter_1.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>日常开发的时候我们还会遇到另一种情况,比如根据当前 UI 的渲染情况,我们需要时刻打印 UI 的某个值,来帮助我们观察 UI,同时当到达某种条件的时候,输入一条我们得到的【结果 log】。也就是说在获得我们的【结果 log】之前会有很多没用但是又必须打印的 log,这样当我们需要查找【结果 log】的时候就会非常麻烦。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/log_filter_2.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>就像上图中这样,我们的【结果 log】被上下【循环打印 log】包围了,很难一下子找出来,这时候我们可以选关键字【右键】,选择【Fold Lines Like This】,如下图所示。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/log_filter_3.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>这样我们相同关键字的 log 就会被折叠,当然也可以展开查看详细 log。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/log_filter_4.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><h2 id="自定义断点执行条件"><a href="#自定义断点执行条件" class="headerlink" title="自定义断点执行条件"></a>自定义断点执行条件</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/custom_break_0.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>我们来看上面这段代码,通过字面意思我们可以得知:这是一个点击事件执行的方法,点击发生后通过 NavController 从当前 HomeFragment 跳转到 EmailFragment。然后我们在第一行打了一个断点,我们已经得知当 email 的 subject 包含 【Bonjour】关键字的时候,这段代码会发生崩溃,反之不包含则不会发生崩溃,所以我们不需要每次断点都生效。</p><p>这里我们可以右键断点,在 Condition 里输入我们的条件判断语句,当条件允许的时候,断点才生效。条件语句的代码支持 Kotlin 和 Java 两种语言的写法,如下图所示。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/custom_break_1.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>然后如果我们想在跳转到 EmailFragment 之后进一步去追踪问题,于是我在 EmailFragment 的 onCreate 方法打了一个断点(如下图所示),然后这里还会遇到我们之前说的问题:不符合条件的时候断点也会生效,这时候我们该怎么办呢?</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/custom_break_2.png" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>我们可以在这个断点上面,右键、点击更多。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/custom_break_3.png" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>左边选择当前断点之后,在右边点击【Disable until breakpoint hit】,选择我们之前有条件判断的断点,那么这个新的断点会在它所跟随的断点生效之后才会生效。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/custom_break_4.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><h2 id="挂起线程"><a href="#挂起线程" class="headerlink" title="挂起线程"></a>挂起线程</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/suspend.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>当我们右键任意一个断点的时候,会有一个 Suspend 选项【All、Thread】,All 也就意味着当我们在一个多线程的应用中 debug 问题的时候,一旦这个断点生效,所有的线程都会被挂起,Thread 表示只挂起当前线程。所以当我们在某个后台线程中 debug 问题的时候就可以选择 Thread,这样就不会在 debug 的时候阻塞主线程的正常功能。</p><p>还有一个打开关闭断点的快捷键也分享一下:Windows 用户 Alt + Click ,Mac Option + Click 。</p><h2 id="动态打印"><a href="#动态打印" class="headerlink" title="动态打印"></a>动态打印</h2><p>详细很多人包括我之前在 debug 的时候,都会在需要 debug 的地方增加 print 输出一下信息供自己排查错误,这里提供一种快捷方便的方法,可以既不污染我们的代码,又可以随时输出任意信息。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/eval_and_log.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>如上图所示,在需要打印的地方增加断点,然后取消所有线程的挂起,选择【Evaluate and log】,属于我们需要打印的语句,当代码执行到断点的时候,不会暂停,而会根据我们设置的打印信息输出 log,是不是很方便?</p><h2 id="断点分组"><a href="#断点分组" class="headerlink" title="断点分组"></a>断点分组</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/group.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>通常遇到一个问题的时候,我们需要增加很多断点去追踪问题的原因,当问题解决之后,往往会忘记取消这些断点,导致在某次调试的时候,设备会被之前的断点所暂停,会让我们很无语。这里我们可以 debug 的时候在某个断点上:右键、更多,然后选择这个问题所有相关的断点,将它们分到同一个 Group 里面,那么这一个组的断点就可以统一开关、统一删除。</p><h2 id="断点上一步"><a href="#断点上一步" class="headerlink" title="断点上一步"></a>断点上一步</h2><p>说到这个真的很痛心,常常因为自己在 debug 的时候,由于下一步点击的太快了而错过了问题关键行,只能重新运行一次代码,重新 debug 然后自己一次次点击下一步。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/drop_frame.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>在运行 Android 10 的设备上,debug 界面中提供了一个叫【Drop frame】的按钮,可以供我们跳出当前方法栈,返回上一步,这样就会避免我们因为错过断点而不得不重新运行代码。</p><h2 id="观察对象"><a href="#观察对象" class="headerlink" title="观察对象"></a>观察对象</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/mark_obj_0.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>当我们 debug 的时候,可以从 debug 窗口中观察当前作用域中的对象以及对象的属性,有时候我们会观察在不同页面是否是同一个对象,之前我的做法很粗暴…就是找张纸,把这个对象的 ID 记下来,然后在另一个页面 debug 看 ID 是否一致😂</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/mark_obj_1.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>这里我们可以在对象上右键、选择【Mark Object】之后会让你自定义一个 Label,然后在整个 debug 期间,相同的对象会以你设置的 Label 为 name 出现,帮助我们方便的分析是否是统一对象。</p><p>顺便提一下,在任意一段代码上,点击行号,可以从当前断点快速执行到目标行并暂停,这个我真是第一次知道,感觉之前 Android Studio 都白用了😭</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/mark_obj_2.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>而且在 debug 的时候我们可以选择 debug 窗口中的【Evaluate expression】按钮来动态观察对象,点击之后会弹出一个计算框,我们可以输入任意当前作用域中的对象以及属性观察。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/mark_obj_3.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>不得不说这个真的很方便,以前遇到这种情况我只有一种方法就是:print 😂,当然这里不仅仅是观察对象,我们可以写任意代码观察我们想要的值,就像下图这样。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/mark_obj_4.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><h2 id="增量更新"><a href="#增量更新" class="headerlink" title="增量更新"></a>增量更新</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/hot_reload.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>我试了一下,这两个按钮是真的很好用啊,比重新全量运行应用真是快了不少,非常方便。</p><h2 id="错误栈分析"><a href="#错误栈分析" class="headerlink" title="错误栈分析"></a>错误栈分析</h2><p>通常我们 App 中会继承一下线上 bug 反馈的 SDK 比如 bugly,在 bugly 我们会得到崩溃的异常栈信息,类似下图这样。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/analyze_stack_0.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>我们可以全选复制,打开我们的 Android Studio,选择 Analyze → Analyze Stack Trace or Thread Dump,然后把异常栈信息粘贴进去,点击确定。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/analyze_stack_1.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>Android Studio 会在控制台显示这段异常栈信息,并且与现有代码 Link 在一起,我们可以点击跳转到问题所在行。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191111_as_debug_skills/analyze_stack_2.webp" alt="" title=""> </div> <div class="image-caption"></div> </figure><p>好了这就是今天要分享的全部内容,关于更多详细的内容,大家可以点击【阅读原文】在油管上查看,真希望有大佬可以出一个【Android Studio 使用全攻略】,感觉自己对 AS 真的只是会用,但还有很多东西需要去学习和探索。😂</p><p>我是 wanbo 大家加油!</p>]]></content>
<summary type="html">
<blockquote>
<p>Pain is inevitable. Suffering is optional. 痛楚难以避免,而磨难可以选择。</p>
</blockquote>
<p>这是一篇转载的文章。</p>
<ul>
<li>作者:wanbo</li>
<li>链接
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Android Studio" scheme="https://lilei.pro/tags/Android-Studio/"/>
</entry>
<entry>
<title>Kotlin域函数小结</title>
<link href="https://lilei.pro/2019/10/28/kotlin-scope-functions/"/>
<id>https://lilei.pro/2019/10/28/kotlin-scope-functions/</id>
<published>2019-10-28T15:37:31.000Z</published>
<updated>2019-10-28T15:43:35.967Z</updated>
<content type="html"><![CDATA[<blockquote><p>并非意志坚强就可以无所不能,人世不是那么单纯的。老实说,我甚至觉得每天坚持跑步同意志强弱并没有太大关联。我能够坚持跑二十年,恐怕还是因为合乎我的性情,至少“不觉得那么痛苦”。人生来如此,喜欢的事自然可以坚持下去,不喜欢的事怎么也坚持不了。 ——<strong>当我谈跑步时我谈些什么</strong></p></blockquote><p>想要掌握Kotlin,域函数是不得不迈过的一道坎。</p><h1 id="所谓“域函数”"><a href="#所谓“域函数”" class="headerlink" title="所谓“域函数”"></a>所谓“域函数”</h1><p>一句话,域函数(scope functions)是为给定的对象创建一个临时的域,在这个域中执行一些操作。相比于传统的对象-函数调用写法,域函数在减少代码量的同时,还可以让编码在逻辑上看起来更加清晰,便于扩展和维护。</p><p>对比一下域函数写法与普通写法。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 域函数写法</span></span><br><span class="line">Person(<span class="string">"Alice"</span>, <span class="number">20</span>, <span class="string">"Amsterdam"</span>).let {</span><br><span class="line"> println(it)</span><br><span class="line"> it.moveTo(<span class="string">"London"</span>)</span><br><span class="line"> it.incrementAge()</span><br><span class="line"> println(it)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 普通写法</span></span><br><span class="line"><span class="keyword">val</span> alice = Person(<span class="string">"Alice"</span>, <span class="number">20</span>, <span class="string">"Amsterdam"</span>)</span><br><span class="line">println(alice)</span><br><span class="line">alice.moveTo(<span class="string">"London"</span>)</span><br><span class="line">alice.incrementAge()</span><br><span class="line">println(alice)</span><br></pre></td></tr></table></figure><p>域函数要结合lambda表达式使用。Kotlin中一共有五个域函数:<code>let</code>, <code>run</code>, <code>with</code>, <code>apply</code>, <code>also</code>。这些域函数有两个区别点。</p><ul><li>域函数中如何引用上下文对象</li><li>域函数返回值</li></ul><h1 id="区别点:上下文对象"><a href="#区别点:上下文对象" class="headerlink" title="区别点:上下文对象"></a>区别点:上下文对象</h1><p>域函数中用<code>this</code>或者<code>it</code>指代上下文对象。</p><h2 id="this"><a href="#this" class="headerlink" title="this"></a>this</h2><p><code>run</code>, <code>with</code>和<code>apply</code>在lambda表达式中用<code>this</code>指代上下文对象,就好像lambda表达式是在对象内部调用的一样,<code>this</code>是可以省略的。对于调用对象内部方法、属性的代码,应当选择使用<code>this</code>指代的域函数。如果在域内要调用其他对象的函数,不要选择<code>this</code>指代,因为容易弄混。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> adam = Person(<span class="string">"Adam"</span>).apply {</span><br><span class="line"> age = <span class="number">20</span> <span class="comment">// same as this.age = 20</span></span><br><span class="line"> city = <span class="string">"Hangzhou"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="it"><a href="#it" class="headerlink" title="it"></a>it</h2><p><code>let</code>, <code>also</code>在lambda表达式中用<code>it</code>指代上下文对象,与<code>this</code>不同,在访问方法和对象时<code>it</code>是不能省略的。当上下文对象需要在域内充当函数参数时,就选用<code>it</code>类型的域函数。另一个便捷之处在于,可以为<code>it</code>指代别名。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getRandomInt</span><span class="params">()</span></span>: <span class="built_in">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> Random.nextInt(<span class="number">100</span>).also {</span><br><span class="line"> writeToLog(<span class="string">"getRandomInt() generated value <span class="variable">$it</span>"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> i = getRandomInt()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 为it指代别名</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getRandomInt</span><span class="params">()</span></span>: <span class="built_in">Int</span> { value -></span><br><span class="line"> <span class="keyword">return</span> Random.nextInt(<span class="number">100</span>).also {</span><br><span class="line"> writeToLog(<span class="string">"getRandomInt() generated value <span class="variable">$value</span>"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="区别点:返回值"><a href="#区别点:返回值" class="headerlink" title="区别点:返回值"></a>区别点:返回值</h1><h2 id="Lambda表达式结果"><a href="#Lambda表达式结果" class="headerlink" title="Lambda表达式结果"></a>Lambda表达式结果</h2><p><code>let</code>, <code>run</code>和<code>with</code>返回lambda表达式的结果(最后一行),可以用这些域函数来进行赋值,也可以进行链式调用。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 赋值</span></span><br><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>)</span><br><span class="line"><span class="keyword">val</span> countEndsWithE = numbers.run { </span><br><span class="line"> add(<span class="string">"four"</span>)</span><br><span class="line"> add(<span class="string">"five"</span>)</span><br><span class="line"> count { it.endsWith(<span class="string">"e"</span>) } <span class="comment">// 返回count数</span></span><br><span class="line">}</span><br><span class="line">println(<span class="string">"There are <span class="variable">$countEndsWithE</span> elements that end with e."</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 无视返回值,只是执行域函数内部操作</span></span><br><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>)</span><br><span class="line">with(numbers) { <span class="comment">// with是this指代,可省略</span></span><br><span class="line"> <span class="keyword">val</span> firstItem = first()</span><br><span class="line"> <span class="keyword">val</span> lastItem = last() </span><br><span class="line"> println(<span class="string">"First item: <span class="variable">$firstItem</span>, last item: <span class="variable">$lastItem</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="上下文对象"><a href="#上下文对象" class="headerlink" title="上下文对象"></a>上下文对象</h2><p><code>also</code>, <code>apply</code>返回上下文对象,可以继续对此进行链式调用,也可以直接返回上下文对象。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 链式调用</span></span><br><span class="line"><span class="keyword">val</span> numberList = mutableListOf<<span class="built_in">Double</span>>()</span><br><span class="line">numberList.also { println(<span class="string">"Populating the list"</span>) }</span><br><span class="line"> .apply { <span class="comment">// 注意apply使用this指代(省略掉)</span></span><br><span class="line"> add(<span class="number">2.71</span>)</span><br><span class="line"> add(<span class="number">3.14</span>)</span><br><span class="line"> add(<span class="number">1.0</span>)</span><br><span class="line"> }</span><br><span class="line"> .also { println(<span class="string">"Sorting the list"</span>) }</span><br><span class="line"> .sort()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 作为返回值</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getRandomInt</span><span class="params">()</span></span>: <span class="built_in">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> Random.nextInt(<span class="number">100</span>).also {</span><br><span class="line"> writeToLog(<span class="string">"getRandomInt() generated value <span class="variable">$it</span>"</span>) <span class="comment">// it指代上下文对象</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> i = getRandomInt()</span><br></pre></td></tr></table></figure><h1 id="逐个函数讲解"><a href="#逐个函数讲解" class="headerlink" title="逐个函数讲解"></a>逐个函数讲解</h1><h2 id="let"><a href="#let" class="headerlink" title="let"></a>let</h2><p><strong>上下文对象</strong>是<code>it</code>,<strong>返回值</strong>是lambda表达式计算结果(最后一行)。</p><p><code>let</code>可以作为链式调用中的一环来使用。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 链式调用</span></span><br><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>, <span class="string">"four"</span>, <span class="string">"five"</span>)</span><br><span class="line">numbers.map { it.length }.filter { it > <span class="number">3</span> }.let { </span><br><span class="line"> println(it)</span><br><span class="line"> <span class="comment">// and more function calls if needed</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更加精简</span></span><br><span class="line">numbers.map { it.length }.filter { it > <span class="number">3</span> }.let(::println)</span><br></pre></td></tr></table></figure><p><code>let</code>经常用来在非空对象上执行一系列操作。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> str: String? = <span class="string">"Hello"</span> </span><br><span class="line"><span class="comment">//processNonNullString(str) // compilation error: str can be null</span></span><br><span class="line"><span class="keyword">val</span> length = str?.let { </span><br><span class="line"> println(<span class="string">"let() called on <span class="variable">$it</span>"</span>) </span><br><span class="line"> processNonNullString(it) <span class="comment">// OK: 'it' is not null inside '?.let { }'</span></span><br><span class="line"> it.length</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>let</code>另一个用法是为变量<code>it</code>创建别名,以增强代码可阅读性。借助于IDE,通常我们可以看到<code>it</code>指代的是什么对象,对这个用法的需求并非十分强烈。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> numbers = listOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>, <span class="string">"four"</span>)</span><br><span class="line"><span class="keyword">val</span> modifiedFirstItem = numbers.first().let { firstItem -></span><br><span class="line"> println(<span class="string">"The first item of the list is '<span class="variable">$firstItem</span>'"</span>)</span><br><span class="line"> <span class="keyword">if</span> (firstItem.length >= <span class="number">5</span>) firstItem <span class="keyword">else</span> <span class="string">"!"</span> + firstItem + <span class="string">"!"</span></span><br><span class="line">}.toUpperCase()</span><br><span class="line">println(<span class="string">"First item after modifications: '<span class="variable">$modifiedFirstItem</span>'"</span>)</span><br></pre></td></tr></table></figure><h2 id="with"><a href="#with" class="headerlink" title="with"></a>with</h2><p><code>with</code>不是扩展函数,它接收<strong>上下文对象</strong>作为函数参数,在lambda表达式中用<code>this</code>指代,返回结果是lambda表达式的值。</p><p>建议在使用<code>with</code>时不要处理它的返回结果,这样<code>with</code>就可以根据字面含义简单的理解成“在这个对象上进行如下操作”。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>)</span><br><span class="line">with(numbers) {</span><br><span class="line"> println(<span class="string">"'with' is called with argument <span class="variable">$this</span>"</span>)</span><br><span class="line"> println(<span class="string">"It contains <span class="variable">$size</span> elements"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>另一种用法是创建一个辅助对象,它的属性或者方法可以用来计算出某个值。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>)</span><br><span class="line"><span class="keyword">val</span> firstAndLast = with(numbers) { <span class="comment">// 相当于声明了一个局部函数</span></span><br><span class="line"> <span class="string">"The first element is <span class="subst">${first()}</span>,"</span> +</span><br><span class="line"> <span class="string">" the last element is <span class="subst">${last()}</span>"</span></span><br><span class="line">}</span><br><span class="line">println(firstAndLast)</span><br></pre></td></tr></table></figure><h2 id="run"><a href="#run" class="headerlink" title="run"></a>run</h2><p>使用<code>this</code>指代上下文对象,返回结果是lambda表达式值。</p><p><code>run</code>在含义上与<code>with</code>一致,在调用方式上与<code>let</code>一致。当需要进行对象初始化、并同时要返回一个计算结果时,应当使用<code>run</code>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> service = MultiportService(<span class="string">"https://example.kotlinlang.org"</span>, <span class="number">80</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> result = service.run {</span><br><span class="line"> port = <span class="number">8080</span></span><br><span class="line"> query(prepareRequest() + <span class="string">" to port <span class="variable">$port</span>"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// the same code written with let() function:</span></span><br><span class="line"><span class="keyword">val</span> letResult = service.let {</span><br><span class="line"> it.port = <span class="number">8080</span></span><br><span class="line"> it.query(it.prepareRequest() + <span class="string">" to port <span class="subst">${it.port}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>除了在接收者对象上调用以外,<code>run</code>还可以让我们在需要一个表达式的地方传入一个代码块(这种用法略复杂)。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> hexNumberRegex = run {</span><br><span class="line"> <span class="keyword">val</span> digits = <span class="string">"0-9"</span></span><br><span class="line"> <span class="keyword">val</span> hexDigits = <span class="string">"A-Fa-f"</span></span><br><span class="line"> <span class="keyword">val</span> sign = <span class="string">"+-"</span></span><br><span class="line"></span><br><span class="line"> Regex(<span class="string">"[<span class="variable">$sign</span>]?[<span class="variable">$digits</span><span class="variable">$hexDigits</span>]+"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (match <span class="keyword">in</span> hexNumberRegex.findAll(<span class="string">"+1234 -FFFF not-a-number"</span>)) { <span class="comment">// findAll正则匹配</span></span><br><span class="line"> println(match.value)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * output: </span></span><br><span class="line"><span class="comment"> * +1234</span></span><br><span class="line"><span class="comment"> * -FFFF</span></span><br><span class="line"><span class="comment"> * -a</span></span><br><span class="line"><span class="comment"> * be</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><h2 id="apply"><a href="#apply" class="headerlink" title="apply"></a>apply</h2><p>使用<code>this</code>指代上下文对象,返回值是上下文对象本身。</p><p><code>apply</code>适用的场景是不需要返回值,且主要是操作对象成员的过程。常见的就是对象配置,“在对象上进行如下操作”。</p><p><code>apply</code>可以很容易地变成链式操作。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> adam = Person(<span class="string">"Adam"</span>).apply {</span><br><span class="line"> age = <span class="number">32</span></span><br><span class="line"> city = <span class="string">"London"</span> </span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="also"><a href="#also" class="headerlink" title="also"></a>also</h2><p>使用<code>it</code>指代上下文对象,返回值是上下文对象本身。</p><p><code>also</code>的使用场景是将对象作为一系列操作的参数,在这些操作中<strong>不应该对参数产生副作用</strong>,因此你可以在一个链式调用中很方便地加上<code>also</code>,或者从链式调用中把<code>also</code>摘掉,且不影响原有逻辑。</p><p>当你在代码中看到<code>also</code>时,可以将其理解为“还需要做这些事情”。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> numbers = mutableListOf(<span class="string">"one"</span>, <span class="string">"two"</span>, <span class="string">"three"</span>)</span><br><span class="line">numbers</span><br><span class="line"> .also { println(<span class="string">"The list elements before adding new one: <span class="variable">$it</span>"</span>) }</span><br><span class="line"> .add(<span class="string">"four"</span>)</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>用一张表格列出函数的对象引用与返回值。</p><table><thead><tr><th>Function</th><th>Object reference</th><th>Return value</th><th>Is extension function</th></tr></thead><tbody><tr><td>let</td><td>it</td><td>Lambda result</td><td>Yes</td></tr><tr><td>run</td><td>it</td><td>Lambda result</td><td>Yes</td></tr><tr><td>run</td><td>-</td><td>Lambda result</td><td>No: called without the context object</td></tr><tr><td>with</td><td>this</td><td>Lambda result</td><td>No: takes the context object as an argument</td></tr><tr><td>apply</td><td>this</td><td>Context object</td><td>Yes</td></tr><tr><td>also</td><td>it</td><td>Context object</td><td>Yes</td></tr></tbody></table><p>一个简单的函数选用指南如下,它们的应用场景有重叠的部分,使用时应当具体情况具体分析。</p><ul><li>在非空对象上调用lambda表达式:<code>let</code></li><li>在域内将表达式抽象成一个变量:<code>let</code></li><li>对象配置:<code>apply</code></li><li>对象配置并计算结果:<code>run</code></li><li>执行一系列表达式,非扩展函数:<code>run</code></li><li>附加效果:<code>also</code></li><li>将对于某个对象的函数调用组合:<code>with</code></li></ul><p>尽管作用域函数功能强大,在使用时应当谨慎,避免出错,尤其是嵌套的情况应当越少越好;当你在写链式调用时,一定要小心分辨当前的上下文对象。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://kotlinlang.org/docs/reference/scope-functions.html" target="_blank" rel="noopener">官方文档</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>并非意志坚强就可以无所不能,人世不是那么单纯的。老实说,我甚至觉得每天坚持跑步同意志强弱并没有太大关联。我能够坚持跑二十年,恐怕还是因为合乎我的性情,至少“不觉得那么痛苦”。人生来如此,喜欢的事自然可以坚持下去,不喜欢的事怎么也坚持不了。 ——<s
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
<category term="Kotlin" scheme="https://lilei.pro/tags/Kotlin/"/>
</entry>
<entry>
<title>移动设备ID那些事</title>
<link href="https://lilei.pro/2019/10/12/unique-device-id/"/>
<id>https://lilei.pro/2019/10/12/unique-device-id/</id>
<published>2019-10-11T16:09:06.000Z</published>
<updated>2019-10-11T16:22:48.854Z</updated>
<content type="html"><![CDATA[<h2 id="为什么需要设备ID"><a href="#为什么需要设备ID" class="headerlink" title="为什么需要设备ID"></a>为什么需要设备ID</h2><p>“设备ID”即用于标识设备唯一身份的ID,即 Unique Device Identifier。基于以下原因,我们经常需要处理设备ID相关功能:</p><ol><li>统计需求。DAU,MAU,转化率,用户行为等统计。</li><li>业务需求。个性化推荐,日志收集,灰度发布,AB Test等业务侧需求。</li><li>风控需求。防刷单,反作弊等。</li></ol><h2 id="设备ID的特征"><a href="#设备ID的特征" class="headerlink" title="设备ID的特征"></a>设备ID的特征</h2><p>为了满足以上需求,一个良好的设备ID方案应当具有“唯一性”和“稳定性”两个特征。</p><ul><li>唯一性:系统中的任意两台设备,它们的设备ID应当不同。</li><li>稳定性:同一台设备在重启、清空应用数据、卸载应用重装、系统升级、Android 版本升级、刷机等情况下,设备ID应当保持不变。</li></ul><p>遗憾的是,Android 平台并没有稳定的API可以提供具有上面两点特征的ID。</p><h2 id="可选方案及限制"><a href="#可选方案及限制" class="headerlink" title="可选方案及限制"></a>可选方案及限制</h2><p>关于Android设备ID,常见的方案有IMEI、MAC地址、Serial、AndroidID等,下面逐一介绍它们是什么,以及为何无法承担唯一ID的职责。</p><h3 id="IMEI"><a href="#IMEI" class="headerlink" title="IMEI"></a>IMEI</h3><p>是 <strong>国际设备识别码(Imternational Mobile Equipment Identity)</strong> 的缩写,即通常所说的手机串号,用于在移动电话网络中识别每一部独立的手机等移动通信设备,共15~17位数字。在拨号键盘输入<code>*#06#</code>即可查看。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 读取IMEI的样例代码,需要 READ_PHONE_STATE 权限</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getIMEI</span><span class="params">()</span></span>: String {</span><br><span class="line"> <span class="keyword">val</span> tm = getSystemService(Context.TELEPHONY_SERVICE) <span class="keyword">as</span> TelephonyManager</span><br><span class="line"> <span class="keyword">return</span> tm.deviceId</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在早些时候,IMEI是很多应用采取的设备ID方案,因为它读取方便,且同时具备唯一性和稳定性的特征。然而自从Android 6开始,<code>READ_PHONE_STATE</code>被列入<code>dangerous</code>的保护级别,意味着我们不仅要在<code>AndroidManifest.xml</code>文件里申请,还应当在应用到这个权限之前动态申请。尤其在中文的安卓系统上,弹窗里的文字提示是“申请电话设备信息”,很容易让人误以为这是要获取电话号码、短信内容等敏感信息。</p><p>如果说Android 6只是提高了使用IMEI作为设备ID的门槛,Android 10则是完全堵死了这条路。在Android 10的系统里,即使申请了<code>READ_PHONE_STATE</code>权限,也无法获取IMEI,会抛出<code>SecurityException</code>异常或者返回null。</p><h3 id="MAC地址"><a href="#MAC地址" class="headerlink" title="MAC地址"></a>MAC地址</h3><p>MAC地址(Media Access Control Address),用于标识可上网设备的唯一地址,设备有几张网卡,就会有几个MAC地址。在OSI七层模型中,MAC地址位于第二层数据链路层。看到这里你也许以为MAC地址是作为设备ID解决方案的最佳选择,实则不然,首先,MAC地址的获取方法历经多次修改,足见Google正欲收紧MAC地址权限,以后完全堵死也并非不可能。</p><p>在 (?, 6.0),[6.0, 7.0),[7.0, ?) 三种不同的Android版本下,有着不同的获取MAC地址方式,可以参考简书这篇文章 <a href="https://www.jianshu.com/p/16d4ff4c4cbe" target="_blank" rel="noopener">《Android 版本兼容 — Android 6.0 和 7.0后获取Mac地址》</a>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取MAC地址,适用于目前Android全版本,不保证以后继续适用(很大可能不再适用)</span></span><br><span class="line"><span class="comment">// 需要 android.permission.INTERNET 权限,为非dangerous权限,可以在AndroidManifst.xml中直接申请</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getMAC</span><span class="params">()</span></span>: String {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> enumeration = getNetworkInterfaces() ?: <span class="keyword">return</span> <span class="string">""</span></span><br><span class="line"> <span class="keyword">while</span> (enumeration.hasMoreElements()) {</span><br><span class="line"> <span class="keyword">val</span> netInterface = enumeration.nextElement()</span><br><span class="line"> <span class="keyword">if</span> (netInterface.name == <span class="string">"wlan0"</span>) {</span><br><span class="line"> <span class="keyword">val</span> addr = netInterface.hardwareAddress</span><br><span class="line"> <span class="keyword">val</span> result = StringBuilder()</span><br><span class="line"> <span class="keyword">for</span> (b <span class="keyword">in</span> addr) {</span><br><span class="line"> result.append(String.format(<span class="string">"%02X:"</span>, b))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (result.isNotEmpty()) {</span><br><span class="line"> result.deleteCharAt(result.length - <span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result.toString()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> Log.e(<span class="string">"tag"</span>, e.message, e)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="string">""</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="Serial"><a href="#Serial" class="headerlink" title="Serial"></a>Serial</h3><p>何为Serial?Serial即“设备序列号”,是设备厂商提供的设备唯一串号,唯一性由各厂商保证。拿vivo举例,它会保证自己生产的每一台设备序列号都是不同的,但是不是与OPPO的也不一样呢?这就无法保证了。一个方案是用<code>厂商ID_设备型号_序列号</code>拼接起来,作为设备ID,这样可以避免不同厂商设备具有相同Serial的问题。但是,并非所有厂商都会严格按照这个规定来做。我曾经在RK3399的开发板上做过开发,试过很多张板子,它们的Serial都是0123456789,让人哭笑不得。</p><p>更糟糕的是,Android 10又堵死了获取Serial的路,会直接抛出<code>SecurityException</code>,除非应用是系统签名且具备<code>android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE</code>权限。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 读取Serial,需要 READ_PHONE_STATE 权限</span></span><br><span class="line"><span class="comment">// Android 10 异常 </span></span><br><span class="line"><span class="comment">// java.lang.SecurityException: getSerial: The user 10236 does not meet the requirements to access device identifiers.</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getSerial</span><span class="params">()</span></span> = android.os.Build.getSerial()</span><br></pre></td></tr></table></figure><h3 id="AndroidID"><a href="#AndroidID" class="headerlink" title="AndroidID"></a>AndroidID</h3><p>AndroidID是SDK提供的获取ID方法,它不需要申明任何权限,具有64bit的取值范围,且唯一性也还不错。但是,它最大的硬伤在于无法满足稳定性:</p><ul><li>刷机、root、恢复出厂设置后都会变化</li><li>对安装在8.0系统的应用来说,AndroidID取决于应用签名+设备两者的组合</li><li>在8.0之前安装的应用,如果在系统升级到8.0后,卸载重装该应用,读取到的AndroidID会变化</li></ul><p>由于以上原因,在一些要求不严格的场景中,可以采用AndroidID作为设备ID,比如记录激活数、曝光数据等。但是在严格的场景中就不能用AndroidID了,如以设备ID标识用户身份,提供相应服务的场景。</p><p>读取AndroidID的样例代码如下:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 读取AndroidID,不需要额外申请任何权限</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">getAndroidID</span><span class="params">()</span></span> = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)</span><br></pre></td></tr></table></figure><h2 id="可选方案总结"><a href="#可选方案总结" class="headerlink" title="可选方案总结"></a>可选方案总结</h2><p>用一张表格总结上述方案:</p><table><thead><tr><th>方案</th><th>概述</th><th>限制</th></tr></thead><tbody><tr><td>IMEI</td><td>具备唯一性和稳定性,过去的最佳选择</td><td>Android10堵死,无法获取</td></tr><tr><td>MAC</td><td>网卡地址,唯一且稳定</td><td>权限逐渐收紧,未来极有可能关闭</td></tr><tr><td>Serial</td><td>手机厂商提供的设备序列号</td><td>不保证唯一性,Android10堵死</td></tr><tr><td>AndroidID</td><td>Android SDK 提供,不需要申请权限,唯一性较好</td><td>不具备稳定性</td></tr></tbody></table><h2 id="设计一个新方案"><a href="#设计一个新方案" class="headerlink" title="设计一个新方案"></a>设计一个新方案</h2><p>上述四个方案里,没有哪一个是解决设备ID问题的终极武器(银弹),只有各种方法综合运用,才是解决之道。下面提出一种设备ID方案,综合使用多个硬件ID,借助服务器生成唯一虚拟设备ID(VDID),可以最大限度地保证唯一性和稳定性。</p><h3 id="稳定性:拜占庭容错"><a href="#稳定性:拜占庭容错" class="headerlink" title="稳定性:拜占庭容错"></a>稳定性:拜占庭容错</h3><p>稳定性要求当获取不到某种ID,或者某种ID发生变化时,系统能够辨识出这个设备。借助“拜占庭容错”可以解决稳定性的问题。</p><p>拜占庭容错机制源于古老 <strong>拜占庭将军(Byzantine failures)</strong> 问题。用简单的语言解释: <strong>如果系统中有n个故障节点,系统要想正确运行,必须至少要有2n+1个正常节点。</strong> 。但对于Android设备ID,我们采用弱化的拜占庭容错机制,即客户端每次上传4个ID(IMEI、MAC、Serial、AndroidID),服务器根据这4个ID生成一个随机的唯一ID即VDID。后续客户端再请求时,可以使用VDID,或者再次使用4个ID,由服务器拿这4个ID在数据库中进行查找VDID,若找到则返回,若未找到,再使用4个ID中的3个进行查找,3个不行则用2个,以此类推。</p><h3 id="获取VDID的时序图"><a href="#获取VDID的时序图" class="headerlink" title="获取VDID的时序图"></a>获取VDID的时序图</h3><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20191012_unique_device_id/vdid_sequence.png" alt="vdid_sequence" title=""> </div> <div class="image-caption">vdid_sequence</div> </figure><h3 id="唯一性:VDID的生成方式与取值范围"><a href="#唯一性:VDID的生成方式与取值范围" class="headerlink" title="唯一性:VDID的生成方式与取值范围"></a>唯一性:VDID的生成方式与取值范围</h3><p>要实现VDID的唯一性,有两种方案可以考虑:</p><h4 id="方案一:自增主键ID"><a href="#方案一:自增主键ID" class="headerlink" title="方案一:自增主键ID"></a>方案一:自增主键ID</h4><p>自增、分步自增、分段构造、Redis分布式ID等方法,可以保证唯一性。但是在传输此类ID时,应当进行hash操作,且保证hash后的ID不可碰撞。</p><p>自增ID的优点是可作为索引,检索速度快;缺点是生成规则存在被破解的风险。</p><h4 id="方案二:随机生成ID"><a href="#方案二:随机生成ID" class="headerlink" title="方案二:随机生成ID"></a>方案二:随机生成ID</h4><p>这是另一种生成唯一ID的方法,当位数足够多时,可以认为碰撞概率趋近于0。首先看一下这张表,它描述了随机数位数与发生碰撞的概率。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/20191012_unique_device_id/random_collision.png" alt="random_collision" title=""> </div> <div class="image-caption">random_collision</div> </figure><p>假设我们应用的活跃用户数为20,000,000,即两千万。可以看到至少要有128Bits,才能在2*10^7(两千万)的数量级有极小的碰撞概率,符合我们的业务需求。</p><p>随机ID的优点是具有隐蔽性,缺点是检索效率一般。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>本文是笔者阅读郭霖公众号推文<a href="https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650247428&idx=1&sn=9d52f7b3262622f9ad25af8af167fcd8&chksm=8863606bbf14e97deb5ecd030ff02ee76f6d320f1c0f586034e442903d00ff79dd7fc5bb3d91&mpshare=1&scene=1&srcid=&sharer_sharetime=1570535190224&sharer_shareid=50472349a2a49142535b8b535dc7dac9&key=042d6afab53eaf2ecfd1c244abe072a53b13e46580bd8c41a7ed57b6b5f3360221dd5c5a4ea8dc0fcde4b6632ba630de39d070bbd64a7e4f24155cec02abb5dc1e36d0ecbc6e250d6f3f87c039ad1407&ascene=1&uin=OTUwMTI1NjEw&devicetype=Windows+10&version=62060833&lang=zh_CN&pass_ticket=hOoinQFE6HGs3U8dFKy9YsMWbVerXZqDJCY6dO0Rh4aIO4VWSduOo5tRbTZkwcN3" target="_blank" rel="noopener">《漫谈设备唯一ID的那些秘密》</a>后的总结归纳,此处向原作者@呼啸长风致谢。然而作者原文中有一处纰漏,设备序列号(SERIAL)在Android 10上基本是无法获取的,这也会影响原作者提出的解决方案。笔者已经在Github上对此提了<a href="https://github.com/No89757/Udid/issues/1" target="_blank" rel="noopener">issue</a>。</p><p>出于对用户隐私的保护,Google一直试图收紧的设备ID的获取权限。而由于“账户”的概念在国内市场并不普遍,再加上各大OEM碎片化严重,国内的各种业务不得不依赖于设备ID进行展开。这也是开发者近年来不得不面对的问题,希望未来国内的Android生态可以更加统一,也希望Google也对此类需求提供更好的权限方案,能让开发者可以不必为此头痛。</p>]]></content>
<summary type="html">
<h2 id="为什么需要设备ID"><a href="#为什么需要设备ID" class="headerlink" title="为什么需要设备ID"></a>为什么需要设备ID</h2><p>“设备ID”即用于标识设备唯一身份的ID,即 Unique Device Ident
</summary>
<category term="Android" scheme="https://lilei.pro/tags/Android/"/>
</entry>
<entry>
<title>Flash Boys 读书笔记</title>
<link href="https://lilei.pro/2019/10/11/flash-boys/"/>
<id>https://lilei.pro/2019/10/11/flash-boys/</id>
<published>2019-10-11T15:55:18.000Z</published>
<updated>2019-10-11T15:56:49.115Z</updated>
<content type="html"><![CDATA[<h2 id="前言-背景与动机"><a href="#前言-背景与动机" class="headerlink" title="前言-背景与动机"></a>前言-背景与动机</h2><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191011_flashboys/cover.jpg" alt="cover" title=""> </div> <div class="image-caption">cover</div> </figure><p>Flash Boys,中译《高频交易员》,是金融畅销书作家迈克尔刘易斯 2015 年的作品,作者之前出版过《大空头》、《说谎者的扑克牌》等华尔街领域的流行作品。在这本书中,刘易斯借助“胜山”的角色,讲解了一些 HFT 的基础知识,可以给我等小白用来入门 HFT。</p><p>书的前 2/3 值得细读,后 1/3 基本上都是给 IEX 做广告,一扫而过即可。</p><h2 id="定义:何为-HFT(高频交易)"><a href="#定义:何为-HFT(高频交易)" class="headerlink" title="定义:何为 HFT(高频交易)"></a>定义:何为 HFT(高频交易)</h2><p>短线交易是经典力学,研究对象为每个周期的成交价格,最短 6 秒。</p><p>高频交易是量子力学,6秒内可以发生上百次交易,最终成交价只是最后一笔订单的价格。</p><p>交易所保留数据的频度是秒,二市场中高频交易主体的数据频度是纳秒级,1秒=10亿纳秒。</p><p><strong>高频交易(High-Frequency Trading)</strong>是投资银行、对冲基金和专业交易公司等利用高速计算机进行程序化证券交易的投资策略的总称,主要包括以下几种策略:</p><ul><li>流动性回扣交易(Liquidity Rebate Trading)</li><li>猎物算法交易(Predatory Algorithmic Trading)</li><li>自动做市商交易(Automated Market Maker Trading, AMMs)</li><li>闪电订单(Flash Order)</li><li>暗池(Dark Pool)</li></ul><h2 id="理解:高频交易何以赚钱"><a href="#理解:高频交易何以赚钱" class="headerlink" title="理解:高频交易何以赚钱"></a>理解:高频交易何以赚钱</h2><p>假设某支股票价格在400~500元之间波动,股民老王下了一个单:当股票价格低于450元时,买入100股。当这个订单在交易所之间传递时(美国有十几家交易所,纽交所、纳斯达克是其中最耳熟能详的),需要几十毫秒的延迟时间,高频交易员就是在这几十毫秒里做文章。</p><p>当股民老王的订单从交易所A传递给交易所B时,借助于高速网络,高频交易员先于交易所B得到这个信息,即“有一个订单是当股票价格低于450元时买100股”,此时如果交易所B的股票价格低于450,比如说是440,交易员就会买入它,再转手以450元的价格履行刚才得到的订单。在这笔交易中,高频交易员获利1000元,股民老王也以预期的450元价格购入了目标股票。</p><blockquote><p>2013年年初,最大的高频交易公司之一Virtu Financial公开称,在其5年半的交易中心仅有一天没有赚到钱,那一天还是因为出现了“人为误差”。</p></blockquote><p>一个高频交易公司可以在市场上疯狂交易而不带来任何附加价值,而且可以不承担任何风险,当它买入时,它知道有确切的卖家,当它卖出时,它知道肯定有人买。在每个交易日结束时,它在任何市场都完全不持有头寸。</p><p>高频交易公司每天闭市时都是平仓的,他们作为买卖双方桥梁的时间极短,以至于根本没有人知道其存在。</p><h2 id="高频交易的分类"><a href="#高频交易的分类" class="headerlink" title="高频交易的分类"></a>高频交易的分类</h2><h3 id="电子抢先交易(electronic-frontrunning)"><a href="#电子抢先交易(electronic-frontrunning)" class="headerlink" title="电子抢先交易(electronic frontrunning)"></a>电子抢先交易(electronic frontrunning)</h3><p>在一个地方探知投资者的交易信息后,在另一个地方抢在投资者之前通过一系列订单推高或拉低价格,并从中获利</p><h3 id="回扣套利(rebate-arbitrage)"><a href="#回扣套利(rebate-arbitrage)" class="headerlink" title="回扣套利(rebate arbitrage)"></a>回扣套利(rebate arbitrage)</h3><p>交易所通常会为创造流动性的券商提供一定的交易费用回扣,高频交易者利用速度优势创造虚假流动性,骗取交易所的回扣</p><h3 id="慢市场套利(slow-market-arbitrage)"><a href="#慢市场套利(slow-market-arbitrage)" class="headerlink" title="慢市场套利(slow market arbitrage)"></a>慢市场套利(slow market arbitrage)</h3><p>高频交易者在一个交易所探知到股票价格变动之后,再利用速度优势在另一个交易所反应过来之前进行买卖操作。这可能是使用最广的</p><h2 id="疑问:赢家拿走一切"><a href="#疑问:赢家拿走一切" class="headerlink" title="疑问:赢家拿走一切"></a>疑问:赢家拿走一切</h2><p>在高频交易的战场上,是否意味着“赢家拿走一切”?如果甲的算法/线路比乙快10%,最终甲会获取所有的订单并完成交易,乙一分钱都拿不到?</p>]]></content>
<summary type="html">
<h2 id="前言-背景与动机"><a href="#前言-背景与动机" class="headerlink" title="前言-背景与动机"></a>前言-背景与动机</h2><figure class="image-bubble">
<di
</summary>
<category term="高频交易" scheme="https://lilei.pro/tags/%E9%AB%98%E9%A2%91%E4%BA%A4%E6%98%93/"/>
</entry>
<entry>
<title>架构学习之 master-v2</title>
<link href="https://lilei.pro/2019/10/11/architecture-master-v2/"/>
<id>https://lilei.pro/2019/10/11/architecture-master-v2/</id>
<published>2019-10-11T15:50:24.000Z</published>
<updated>2019-10-11T15:54:24.831Z</updated>
<content type="html"><![CDATA[<blockquote><p>Move stones, not mountains.</p></blockquote><p>Google 官方的 <a href="https://github.com/googlesamples/android-architecture" target="_blank" rel="noopener">Android Architecture Blueprints</a> 推出了 v2 版本,相比于之前的 v1 版本,v2 采取了更先进的设计思想与组件:</p><ul><li>采用 Kotlin Coroutines 处理后台操作</li><li>单一 Activity 结构,用 <a href="https://developer.android.com/guide/navigation/navigation-getting-started" target="_blank" rel="noopener">Navigation component</a> 处理Fragment 之间跳转</li><li>由 Fragment(View) 和 ViewModel 组成的 Presentation 层,即 <strong>MVVM</strong> 模式</li><li>基于 LiveData 和 DataBinding 的响应式(Reactive)UI</li><li>data 层使用一个 Repository 和两个 Datasource(本地数据、远端数据),采用直观的调用方式(非回调、非 data stream)</li><li>两个 product flavor,分别是<code>mock</code>和<code>prod</code>,对应着测试与开发环境</li><li>一系列单元测试、集成测试以及端到端测试</li></ul><p>接下来从源码角度解析 master 分支工程,看看 v2 究竟可以为我们带来什么便利。</p><hr><h2 id="程序入口"><a href="#程序入口" class="headerlink" title="程序入口"></a>程序入口</h2><p>入口在<code>TasksActivity.java</code>,注意到这是一个“SPA”,即 Single Page Application,在<code>AndroidManifest.xml</code>中只声明了这一个 Activity。<code>TasksActivity</code>实际上只是一个壳页面,只处理了 Navigation、ActionBar、NavigationDrawer 等基础功能。它在<code>onCreate</code>里进行这些初始化:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> drawerLayout: DrawerLayout</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> appBarConfiguration: AppBarConfiguration</span><br><span class="line"></span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContentView(R.layout.tasks_act)</span><br><span class="line"> setupNavigationDrawer()</span><br><span class="line"> setSupportActionBar(findViewById(R.id.toolbar))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> navController: NavController = findNavController(R.id.nav_host_fragment)</span><br><span class="line"> appBarConfiguration =</span><br><span class="line"> AppBarConfiguration.Builder(R.id.tasks_fragment_dest, R.id.statistics_fragment_dest)</span><br><span class="line"> .setDrawerLayout(drawerLayout)</span><br><span class="line"> .build()</span><br><span class="line"> setupActionBarWithNavController(navController, appBarConfiguration)</span><br><span class="line"> findViewById<NavigationView>(R.id.nav_view)</span><br><span class="line"> .setupWithNavController(navController)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>该应用采用 Navigation component 管理页面跳转,跳转关系在<code>nav_graph.xml</code>文件中以可视化的方式呈现。我们这里不需要关心跳转组件的具体用法, 只要知道它可以启动我们要分析的<code>TasksFragment</code>就可以了。</p><h2 id="数据"><a href="#数据" class="headerlink" title="数据"></a>数据</h2><p>我习惯从数据开始分析代码走向,看一下<code>data</code>目录的结构:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">λ tree data</span><br><span class="line">data</span><br><span class="line">|-- Result.kt</span><br><span class="line">|-- Task.kt</span><br><span class="line">`-- <span class="built_in">source</span></span><br><span class="line"> |-- DefaultTasksRepository.kt</span><br><span class="line"> |-- TasksDataSource.kt</span><br><span class="line"> |-- TasksRepository.kt</span><br><span class="line"> `-- <span class="built_in">local</span></span><br><span class="line"> |-- TasksDao.kt</span><br><span class="line"> |-- TasksLocalDataSource.kt</span><br><span class="line"> `-- ToDoDatabase.kt</span><br></pre></td></tr></table></figure><h3 id="Result-kt-与-Task-kt"><a href="#Result-kt-与-Task-kt" class="headerlink" title="Result.kt 与 Task.kt"></a>Result.kt 与 Task.kt</h3><p><code>Result.kt</code>是一个数据请求的结果封装类,业务层对数据的请求均是通过 Result 对象进行封装。这个类使用到了多个 Kotlin 特性,容我在注释里一一说明。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">sealed</span> <span class="class"><span class="keyword">class</span> <span class="title">Result</span><<span class="type">out R</span>> </span>{ <span class="comment">// 密封类,将继承限制在类内部;out 类型,协变,保留子类型化关系</span></span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Success</span><<span class="type">out T</span>></span>(<span class="keyword">val</span> <span class="keyword">data</span>: T) : Result<T>() <span class="comment">// data 类,协变类型T可以用作构造参数</span></span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Error</span></span>(<span class="keyword">val</span> exception: Exception) : Result<<span class="built_in">Nothing</span>>() <span class="comment">// Nothing类型,永不返回</span></span><br><span class="line"> <span class="keyword">object</span> Loading : Result<<span class="built_in">Nothing</span>>() <span class="comment">// object直接创建对象</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">toString</span><span class="params">()</span></span>: String {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">when</span> (<span class="keyword">this</span>) {</span><br><span class="line"> <span class="keyword">is</span> Success<*> -> <span class="string">"Success[data=<span class="variable">$data</span>]"</span> <span class="comment">// *表示不关心具体类型</span></span><br><span class="line"> <span class="keyword">is</span> Error -> <span class="string">"Error[exception=<span class="variable">$exception</span>]"</span></span><br><span class="line"> Loading -> <span class="string">"Loading"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * `true` if [Result] is of type [Success] & holds non-null [Success.data].</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">val</span> Result<*>.succeeded <span class="comment">// 扩展属性,注意命名不是isSuccess(Chinglish)</span></span><br><span class="line"> <span class="keyword">get</span>() = <span class="keyword">this</span> <span class="keyword">is</span> Success && <span class="keyword">data</span> != <span class="literal">null</span></span><br></pre></td></tr></table></figure><p><code>Task.kt</code>描述了任务对象,由<code>title</code>、<code>description</code>、<code>completed</code>和<code>id</code>四个字段构成,同时借助 Room 组件自动关联到名为<strong>tasks</strong>的数据表。</p><h3 id="数据接口:TasksDataSource-kt-与-TasksRepository-kt"><a href="#数据接口:TasksDataSource-kt-与-TasksRepository-kt" class="headerlink" title="数据接口:TasksDataSource.kt 与 TasksRepository.kt"></a>数据接口:TasksDataSource.kt 与 TasksRepository.kt</h3><p><code>TasksDataSource.kt</code>和<code>TasksRepository.kt</code>是两个接口类,内容十分相似:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// TasksDataSource.kt</span></span><br><span class="line"><span class="comment">// Main entry point for accessing tasks data.</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">TasksDataSource</span> </span>{</span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">getTasks</span><span class="params">()</span></span>: Result<List<Task>></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">getTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span>: Result<Task></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">saveTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">completeTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">completeTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">activateTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">activateTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">clearCompletedTasks</span><span class="params">()</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">deleteAllTasks</span><span class="params">()</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">deleteTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// TasksRepository.kt</span></span><br><span class="line"><span class="comment">// Interface to the data layer.</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">TasksRepository</span> </span>{</span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">getTasks</span><span class="params">(forceUpdate: <span class="type">Boolean</span> = <span class="literal">false</span>)</span></span>: Result<List<Task>></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">getTask</span><span class="params">(taskId: <span class="type">String</span>, forceUpdate: <span class="type">Boolean</span> = <span class="literal">false</span>)</span></span>: Result<Task></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">saveTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">completeTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">completeTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">activateTask</span><span class="params">(task: <span class="type">Task</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">activateTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">clearCompletedTasks</span><span class="params">()</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">deleteAllTasks</span><span class="params">()</span></span></span><br><span class="line"> suspend <span class="function"><span class="keyword">fun</span> <span class="title">deleteTask</span><span class="params">(taskId: <span class="type">String</span>)</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到两者的方法几乎是一样的:名字和数量相同,区别仅仅在于<code>TasksRepository</code>中的个别方法多了<code>forceUpdate</code>参数。不过,这两个接口在语义上是不同的。</p><ul><li><code>TasksDatasource</code>是底层数据封装,数据可能来自网络,也可能来自于文件、数据库。</li><li><code>TasksRepository</code>是数据层对外的接口,业务代码通过该接口对数据进行增删改查。<code>forceUpdate</code>参数作用于接口实现类内部的缓存。</li><li><code>suspend</code>关键字说明它们均为 Coroutines 接口。</li></ul><p>然后我们来看下对外的接口是如何给到使用者的。<code>TodoApplication</code>类继承自<code>Application</code>,其中有一个成员变量<code>taskRepository</code>。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// TodoApplication.kt</span></span><br><span class="line"><span class="keyword">val</span> taskRepository: TasksRepository</span><br><span class="line"> <span class="keyword">get</span>() = ServiceLocator.provideTasksRepository(<span class="keyword">this</span>)</span><br><span class="line"> </span><br><span class="line"><span class="comment">// ServiceLocator.kt</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">provideTasksRepository</span><span class="params">(context: <span class="type">Context</span>)</span></span>: TasksRepository {</span><br><span class="line"> synchronized(<span class="keyword">this</span>) {</span><br><span class="line"> <span class="keyword">return</span> tasksRepository ?: tasksRepository ?: createTasksRepository(context)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">createTasksRepository</span><span class="params">(context: <span class="type">Context</span>)</span></span>: TasksRepository {</span><br><span class="line"> <span class="keyword">return</span> DefaultTasksRepository(FakeTasksRemoteDataSource, createTaskLocalDataSource(context))</span><br><span class="line">}</span><br><span class="line"><span class="comment">// TodoApplication.taskRepository 通过 Fragment 的扩展方法给到各个 Fragment</span></span><br><span class="line"><span class="comment">// FragmentExt.kt</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> Fragment.<span class="title">getViewModelFactory</span><span class="params">()</span></span>: ViewModelFactory {</span><br><span class="line"> <span class="keyword">val</span> repository = (requireContext().applicationContext <span class="keyword">as</span> TodoApplication).taskRepository</span><br><span class="line"> <span class="keyword">return</span> ViewModelFactory(repository)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="local-目录:本地数据实现"><a href="#local-目录:本地数据实现" class="headerlink" title="local 目录:本地数据实现"></a>local 目录:本地数据实现</h3><p>local 目录下是<code>TasksDatasource</code>的本地实现,与此相对的,若数据来源于网络,则还应当有一个 remote 目录。</p><ul><li><code>ToDoDatabase</code> 是数据库声明</li><li><code>TasksDao</code> 声明 tasks 表的 CRUD 操作</li><li><code>TasksLocalDataSource</code> 是<code>TasksDataSource</code>的本地实现,使用<code>Dispatchers.IO</code>作为协程上下文,调用<code>TasksDao</code>完成数据操作</li></ul><h3 id="数据层总结"><a href="#数据层总结" class="headerlink" title="数据层总结"></a>数据层总结</h3><p>相比于曾经分析过的<strong>todo-mvp</strong>和<strong>todo-mvp-clean</strong>,最直观的感受是,v2在保证数据接口语义不变的前提下,借助 Coroutines 简化了原有的回调写法,用同步的方式写异步的代码。此外,像使用 Room 做 ORM、local/remote 两套数据实现等,与之前的项目并无不同。</p><hr><h2 id="ViewModel"><a href="#ViewModel" class="headerlink" title="ViewModel"></a>ViewModel</h2><h3 id="背景知识:ViewModel-与-LiveData"><a href="#背景知识:ViewModel-与-LiveData" class="headerlink" title="背景知识:ViewModel 与 LiveData"></a>背景知识:ViewModel 与 LiveData</h3><p>ViewModel 与 LiveData 都是 Android Jetpack 中的架构组件,它们通常组合使用,达到将数据和视图解耦的目的。</p><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191011_architecture_master_v2/jetpack.png" alt="jetpack" title=""> </div> <div class="image-caption">jetpack</div> </figure><p><strong>ViewModel</strong></p><ul><li>避免屏幕旋转等事件发生时,保存在 Activity 中的数据被销毁并重建</li><li>异步回调时防止内存泄漏、Context 为 Null</li><li>将数据和视图解耦,防止出现 God Activities 和 God Fragments</li></ul><figure class="image-bubble"> <div class="img-lightbox"> <div class="overlay"></div> <img src="/img/191011_architecture_master_v2/viewmodel.png" alt="viewmodel" title=""> </div> <div class="image-caption">viewmodel</div> </figure><p><strong>LiveData</strong></p><ul><li>DataBinding 思想的一种实现,数据/视图双向绑定</li><li>与生命周期关联,页面销毁后自动将其从订阅者列表去除</li></ul><h3 id="ViewModelFactory"><a href="#ViewModelFactory" class="headerlink" title="ViewModelFactory"></a>ViewModelFactory</h3><p>ViewModel 的创建采用工厂模式进行统一管理。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ViewModelFactory.kt</span></span><br><span class="line"><span class="meta">@Suppress(<span class="meta-string">"UNCHECKED_CAST"</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ViewModelFactory</span> <span class="keyword">constructor</span></span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> tasksRepository: TasksRepository</span><br><span class="line">) : ViewModelProvider.NewInstanceFactory() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="type"><T : ViewModel></span> <span class="title">create</span><span class="params">(modelClass: <span class="type">Class</span><<span class="type">T</span>>)</span></span> =</span><br><span class="line"> with(modelClass) {</span><br><span class="line"> <span class="keyword">when</span> {</span><br><span class="line"> isAssignableFrom(StatisticsViewModel::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>) -></span></span><br><span class="line"> StatisticsViewModel(tasksRepository)</span><br><span class="line"> isAssignableFrom(TaskDetailViewModel::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>) -></span></span><br><span class="line"> TaskDetailViewModel(tasksRepository)</span><br><span class="line"> isAssignableFrom(AddEditTaskViewModel::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>) -></span></span><br><span class="line"> AddEditTaskViewModel(tasksRepository)</span><br><span class="line"> isAssignableFrom(TasksViewModel::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>) -></span></span><br><span class="line"> TasksViewModel(tasksRepository)</span><br><span class="line"> <span class="keyword">else</span> -></span><br><span class="line"> <span class="keyword">throw</span> IllegalArgumentException(<span class="string">"Unknown ViewModel class: <span class="subst">${modelClass.name}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">as</span> T</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>还记得上文提到过为 Fragment 增加的扩展函数吗?在每一个页面(Fragment)里通过这个扩展函数获取到工厂类,进而获得对应 ViewModel 类的实例。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// FragmentExt.kt</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> Fragment.<span class="title">getViewModelFactory</span><span class="params">()</span></span>: ViewModelFactory {</span><br><span class="line"> <span class="keyword">val</span> repository = (requireContext().applicationContext <span class="keyword">as</span> TodoApplication).taskRepository</span><br><span class="line"> <span class="keyword">return</span> ViewModelFactory(repository)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// TasksFragment.kt</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TasksFragment</span> : <span class="type">Fragment</span></span>() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> viewModel <span class="keyword">by</span> viewModels<TasksViewModel> { getViewModelFactory() }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接下来是 ViewModel 类,它承担了与 Presenter 类似的职责,是处理业务逻辑的地方。如果需要的话,可以增加一个 Domain 层,负责提取出来的业务逻辑(Use cases),提供复用,这样就变成了 <a href="https://github.com/googlesamples/android-architecture/tree/usecases" target="_blank" rel="noopener">MVVM-Clean</a> 模式。在 master 分支上还没有 domain 层。</p><h3 id="TasksViewModel"><a href="#TasksViewModel" class="headerlink" title="TasksViewModel"></a>TasksViewModel</h3><p>ViewModel 接受一个 TasksRepository 参数,用作数据层接口。(在clean架构里,这里传入的不是Repository,而是UseCases)。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TasksViewModel</span></span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> tasksRepository: TasksRepository</span><br><span class="line">) : ViewModel() {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>随后声明了一系列变量作为页面数据&状态,这里采用“一个对象,两个变量”的成对写法,略显繁琐,不知道有没有更优美的处理方法。这样做的目的是把对变量的修改关闭,对外(即LiveData)仅提供读取变量的接口,只可以在 ViewModel 内部修改变量。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _items = MutableLiveData<List<Task>>().apply { value = emptyList() } <span class="comment">// 以下划线开头的为私有变量,apply 和 with 的用法要分清,相当于调用 _items.setValue(emptyList()); return _items;</span></span><br><span class="line"><span class="keyword">val</span> items: LiveData<List<Task>> = _items</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _dataLoading = MutableLiveData<<span class="built_in">Boolean</span>>()</span><br><span class="line"><span class="keyword">val</span> dataLoading: LiveData<<span class="built_in">Boolean</span>> = _dataLoading</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _currentFilteringLabel = MutableLiveData<<span class="built_in">Int</span>>()</span><br><span class="line"><span class="keyword">val</span> currentFilteringLabel: LiveData<<span class="built_in">Int</span>> = _currentFilteringLabel</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _noTasksLabel = MutableLiveData<<span class="built_in">Int</span>>()</span><br><span class="line"><span class="keyword">val</span> noTasksLabel: LiveData<<span class="built_in">Int</span>> = _noTasksLabel</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _noTaskIconRes = MutableLiveData<<span class="built_in">Int</span>>()</span><br><span class="line"><span class="keyword">val</span> noTaskIconRes: LiveData<<span class="built_in">Int</span>> = _noTaskIconRes</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _tasksAddViewVisible = MutableLiveData<<span class="built_in">Boolean</span>>()</span><br><span class="line"><span class="keyword">val</span> tasksAddViewVisible: LiveData<<span class="built_in">Boolean</span>> = _tasksAddViewVisible</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _snackbarText = MutableLiveData<Event<<span class="built_in">Int</span>>>()</span><br><span class="line"><span class="keyword">val</span> snackbarText: LiveData<Event<<span class="built_in">Int</span>>> = _snackbarText</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">var</span> _currentFiltering = TasksFilterType.ALL_TASKS <span class="comment">// Not used at the moment private val isDataLoadingError = MutableLiveData<Boolean>()</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _openTaskEvent = MutableLiveData<Event<String>>()</span><br><span class="line"><span class="keyword">val</span> openTaskEvent: LiveData<Event<String>> = _openTaskEvent</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> _newTaskEvent = MutableLiveData<Event<<span class="built_in">Unit</span>>>()</span><br><span class="line"><span class="keyword">val</span> newTaskEvent: LiveData<Event<<span class="built_in">Unit</span>>> = _newTaskEvent</span><br><span class="line"></span><br><span class="line"><span class="comment">// This LiveData depends on another so we can use a transformation.</span></span><br><span class="line"><span class="keyword">val</span> empty: LiveData<<span class="built_in">Boolean</span>> = Transformations.map(_items) {</span><br><span class="line"> it.isEmpty()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以说管理上面这些数据是 ViewModel 最主要的职责了,从功能上区分,这些数据可以分成<strong>3种</strong>。</p><ol><li>业务实体如 items(任务对象列表),ViewModel 通过修改这类对象,借助于 DataBinding 更新 UI</li><li>数据状态对象如 dataLoading(是否正在加载数据)、currentFilteringLabel(当前的过滤器文字)、noTasksLabel(没有任务的文字)、snackbarText(提示栏文字),这一类对象不进行持久化存储,但是也会影响到 UI 显示</li><li>事件包装对象如 openTaskEvent(打开某个任务,在点击列表中的任务时触发)、newTaskEvent(创建一个任务,在点击+时触发),它们负责通知页面进行跳转——这部分设计得不佳,为了 DataBinding 而强行 DataBinding</li></ol><p>总之贯彻的思想是:一切变动都是数据变动,数据变动通过 DataBinding 自动投射到 UI。</p><hr><h2 id="视图"><a href="#视图" class="headerlink" title="视图"></a>视图</h2><p>视图也就是<code>TasksFragment.kt</code>类,负责初始化布局,触发 ViewModel 进行首次加载。这里的 Fragment 继承自 androidx 中的 Fragment。</p><p>在<code>onCreateView</code>里初始化 DataBinding(数据)。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreateView</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> inflater: <span class="type">LayoutInflater</span>, container: <span class="type">ViewGroup</span>?,</span></span></span><br><span class="line"><span class="function"><span class="params"> savedInstanceState: <span class="type">Bundle</span>?</span></span></span><br><span class="line"><span class="function"><span class="params">)</span></span>: View? {</span><br><span class="line"> viewDataBinding = TasksFragBinding.inflate(inflater, container, <span class="literal">false</span>).apply {</span><br><span class="line"> viewmodel = viewModel</span><br><span class="line"> }</span><br><span class="line"> setHasOptionsMenu(<span class="literal">true</span>)</span><br><span class="line"> <span class="keyword">return</span> viewDataBinding.root</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在<code>onActivityCreated</code>里初始化 UI(视图),初始化完成后启动加载(代码最后一行)。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onActivityCreated</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onActivityCreated(savedInstanceState)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Set the lifecycle owner to the lifecycle of the view</span></span><br><span class="line"> viewDataBinding.lifecycleOwner = <span class="keyword">this</span>.viewLifecycleOwner</span><br><span class="line"> setupSnackbar()</span><br><span class="line"> setupListAdapter()</span><br><span class="line"> setupRefreshLayout(viewDataBinding.refreshLayout, viewDataBinding.tasksList)</span><br><span class="line"> setupNavigation()</span><br><span class="line"> setupFab()</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Always reloading data for simplicity. Real apps should only do this on first load and</span></span><br><span class="line"> <span class="comment">// when navigating back to this destination. <span class="doctag">TODO:</span> https://issuetracker.google.com/79672220</span></span><br><span class="line"> viewModel.loadTasks(<span class="literal">true</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>此外,Fragment 中还会进行设置 OnClickListener、Adapter 等操作,比较简单,不赘述。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>v2 的项目设计时采取了 MVVM 思想,旨在解决 MVP 模式下Presenter 层过于庞大的问题。其实 MVP-Clean 模式已经对此有一些缓解。而 MVVM 做的更彻底,干脆把数据对 UI 的控制完全交给框架自动进行。这样做的好处显而易见,但也并不是没有缺点,比如一旦出了问题,如果没有掌握个中原理,调试时必定摸不清头绪。总而言之,这是一个值得学习与尝试的架构设计。</p><hr><h2 id="Bonus:usecases"><a href="#Bonus:usecases" class="headerlink" title="Bonus:usecases"></a>Bonus:usecases</h2><p><a href="https://github.com/googlesamples/android-architecture/tree/usecases" target="_blank" rel="noopener">usecases</a> 是 v2 中的一个 Stable<br> 分支(另一个是<a href="https://github.com/googlesamples/android-architecture/tree/dagger-android" target="_blank" rel="noopener">dagger-android</a>)。usecases 以解耦、抽象、单向依赖为核心设计理念,这也是 Clean 架构的核心思想。</p><ul><li>表现层只能访问到领域层/用例层,不知道数据层的存在</li><li>领域层/用例层只能访问到数据层,无法访问表现层</li><li>数据层无法访问表现层和领域层</li></ul><p>领域层(或者叫用例层),即 Domain Layer,是由多个 UseCase 组成的,每一个 UseCase 对应一个业务逻辑。以“加载单个Task”为例,可以看到 UseCase 里直接将请求转发给 TasksRepository 来处理,逻辑十分简单。</p><p>在分析具体差别之前,可以先看一遍 usecase 分支与 master 分支的 diff:<a href="https://github.com/googlesamples/android-architecture/compare/usecases#files_bucket" target="_blank" rel="noopener">https://github.com/googlesamples/android-architecture/compare/usecases#files_bucket</a></p><p><strong>GetTaskUseCase.kt</strong></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GetTaskUseCase</span></span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> tasksRepository: TasksRepository</span><br><span class="line"> ) {</span><br><span class="line"> suspend <span class="keyword">operator</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(taskId: <span class="type">String</span>, forceUpdate: <span class="type">Boolean</span> = <span class="literal">false</span>)</span></span>: Result<Task> {</span><br><span class="line"> wrapEspressoIdlingResource {</span><br><span class="line"> <span class="keyword">return</span> tasksRepository.getTask(taskId, forceUpdate)</span><br><span class="line"> }</span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>而获取 Tasks 列表的 Usecase 相对复杂一些,包括了原本在 TasksViewModel 中处理的过滤逻辑,从另一个角度看,这相当于减轻了 ViewModel 的负担。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GetTasksUseCase</span></span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> tasksRepository: TasksRepository</span><br><span class="line">) {</span><br><span class="line"> suspend <span class="keyword">operator</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> forceUpdate: <span class="type">Boolean</span> = <span class="literal">false</span>,</span></span></span><br><span class="line"><span class="function"><span class="params"> currentFiltering: <span class="type">TasksFilterType</span> = ALL_TASKS</span></span></span><br><span class="line"><span class="function"><span class="params"> )</span></span>: Result<List<Task>> {</span><br><span class="line"></span><br><span class="line"> wrapEspressoIdlingResource {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> tasksResult = tasksRepository.getTasks(forceUpdate)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Filter tasks</span></span><br><span class="line"> <span class="keyword">if</span> (tasksResult <span class="keyword">is</span> Success && currentFiltering != ALL_TASKS) {</span><br><span class="line"> <span class="keyword">val</span> tasks = tasksResult.<span class="keyword">data</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> tasksToShow = mutableListOf<Task>()</span><br><span class="line"> <span class="comment">// We filter the tasks based on the requestType</span></span><br><span class="line"> <span class="keyword">for</span> (task <span class="keyword">in</span> tasks) {</span><br><span class="line"> <span class="keyword">when</span> (currentFiltering) {</span><br><span class="line"> ACTIVE_TASKS -> <span class="keyword">if</span> (task.isActive) {</span><br><span class="line"> tasksToShow.add(task)</span><br><span class="line"> }</span><br><span class="line"> COMPLETED_TASKS -> <span class="keyword">if</span> (task.isCompleted) {</span><br><span class="line"> tasksToShow.add(task)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> -> NotImplementedError()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> Success(tasksToShow)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> tasksResult</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至于 ViewModel 里,将原来的 Repository 参数改为当前 ViewModel 用到的 UseCase 参数即可,所有处理数据的请求都有 UseCase 来接管。</p><p><strong>TasksViewModel.kt</strong></p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TasksViewModel</span></span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> getTasksUseCase: GetTasksUseCase,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> clearCompletedTasksUseCase: ClearCompletedTasksUseCase,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> completeTaskUseCase: CompleteTaskUseCase,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> activateTaskUseCase: ActivateTaskUseCase</span><br><span class="line">) : ViewModel() {</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Over~</p>]]></content>
<summary type="html">
<blockquote>
<p>Move stones, not mountains.</p>
</blockquote>
<p>Google 官方的 <a href="https://github.com/googlesamples/android-architecture"
</summary>
<category term="架构" scheme="https://lilei.pro/tags/%E6%9E%B6%E6%9E%84/"/>
</entry>
</feed>