-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
298 lines (188 loc) · 474 KB
/
atom.xml
File metadata and controls
298 lines (188 loc) · 474 KB
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Prototype Z</title>
<link href="/atom.xml" rel="self"/>
<link href="http://prototypez.github.io/"/>
<updated>2018-10-21T14:56:03.710Z</updated>
<id>http://prototypez.github.io/</id>
<author>
<name>Prototype Z</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>一种在 Library 模块中调用 Application 模块功能的方法</title>
<link href="http://prototypez.github.io/2018/10/21/call-app-methods-from-library/"/>
<id>http://prototypez.github.io/2018/10/21/call-app-methods-from-library/</id>
<published>2018-10-21T14:15:00.000Z</published>
<updated>2018-10-21T14:56:03.710Z</updated>
<content type="html"><![CDATA[<p>在<a href="/2018/09/15/app-joint-introduction/">上一篇分享</a>中,我介绍的主题是如何在一个 Android 项目中使用 <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 进行组件化。有读者找到我说:我知道组件化很美好,但是项目改造总是需要成本的,而且这个成本是我们目前承受不了的,我们目前能做的最多的只能是 <strong>模块化</strong>,即把主 app 模块的功能拆出来,放到新的 <strong>library</strong> 模块里,但目前面临的最直接的问题就是,<strong>拆出来的新模块如何调用主 app 模块里的方法</strong>。能否不用组件化那一整套工具,先用 <strong>最轻量级的方法</strong> 解决这个跨模块方法调用的问题?</p><a id="more"></a><p>答案当然是肯定的,<a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 就是这样一个轻量级的工具,如果你不想组件化,那么你完全可以把它当成一个只是用来解决跨模块方法调用的问题的小工具。</p><h2 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h2><p>假设目前已经从 <strong>app</strong> 模块拆出了一个 <strong>module1</strong> 模块,<strong>app</strong> 模块是主 app 模块, 而 <strong>module1</strong> 模块是 library 模块。如果 <strong>module1</strong> 模块想要调用 <strong>app</strong> 模块的功能,那么肯定需要存在一个接口,这个接口两个模块要都能访问到。所以这个接口存在的位置就只能是 <strong>app</strong> 模块和 <strong>module1</strong> 模块都依赖的公共模块。这里的这个公共模块,它可以是已有的公共模块,也可以是新创建的公共模块。我们在这个公共模块里声明这个接口,即 <strong>app</strong> 模块希望被 <strong>module1</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><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">AppService</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// start Activity from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">startActivityOfApp</span><span class="params">(context: <span class="type">Context</span>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// get Fragment from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">obtainFragmentOfApp</span><span class="params">()</span></span>: Fragment</span><br><span class="line"></span><br><span class="line"> <span class="comment">// call synchronous method from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">callMethodSyncOfApp</span><span class="params">()</span></span>: String</span><br><span class="line"></span><br><span class="line"> <span class="comment">// call asynchronous method from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">callMethodAsyncOfApp</span><span class="params">(callback: <span class="type">AppCallback</span><<span class="type">AppEntity</span>>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// get RxJava Observable from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">observableOfApp</span><span class="params">()</span></span>: Observable<AppEntity></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个接口由于处于公共模块,所以两个模块都能访问到这个接口。接下来我们就来解决这个问题,即在 <strong>module1</strong> 模块里调用 <strong>app</strong> 模块的方法。</p><h2 id="问题解决"><a href="#问题解决" class="headerlink" title="问题解决"></a>问题解决</h2><p>在声明上面这个接口以后,我们在 <strong>app</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><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="meta">@ServiceProvider</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AppServiceImpl</span> : <span class="type">AppService {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">startActivityOfApp</span><span class="params">(context: <span class="type">Context</span>)</span></span> {</span><br><span class="line"> AppActivity.start(context)</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">obtainFragmentOfApp</span><span class="params">()</span></span>: Fragment {</span><br><span class="line"> <span class="keyword">return</span> AppFragment.newInstance()</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">callMethodSyncOfApp</span><span class="params">()</span></span>: String {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"syncMethodResultApp"</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">callMethodAsyncOfApp</span><span class="params">(callback: <span class="type">AppCallback</span><<span class="type">AppEntity</span>>)</span></span> {</span><br><span class="line"> Thread { callback.onResult(AppEntity(<span class="string">"asyncMethodResultApp"</span>)) }.start()</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">observableOfApp</span><span class="params">()</span></span>: Observable<AppEntity> {</span><br><span class="line"> <span class="keyword">return</span> Observable.just(AppEntity(<span class="string">"rxJavaResultApp"</span>))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>需要注意一点,我们在实现类上标记了 <code>@ServiceProvider</code> 注解。然后我们就可以像下面这样,从 <strong>module1</strong> 模块里调用 <strong>app</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="keyword">val</span> appService = AppJoint.service(AppService::<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="comment">// call methods inside AppService</span></span><br><span class="line">appService.callMethodSyncOfApp()</span><br><span class="line">appService.observableOfApp().subscribe()</span><br><span class="line">appService.startActivityOfApp(context)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>即使我们没有引入组件化的任何概念,我们也能轻松解决模块化中最常遇见的跨模块方法调用的这一类问题。</p><p>关于如何在项目中引入 <strong>AppJoint</strong>,可以前往 <strong>AppJoint</strong> 的 Github 主页:<a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">https://github.com/PrototypeZ/AppJoint</a>, 核心代码不超过 500 行,您可以使用开箱即用的版本,也可以自行在项目中引入。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>在模块化之后,您可能会对项目的组件化产生兴趣,欢迎继续阅读我的组件化经验分享 <a href="http://prototypez.github.io/2018/09/15/app-joint-introduction/">『回归初心:极简 Android 组件化方案 — AppJoint』</a>,希望可以给您的项目组件化带去一点点帮助,谢谢! : )</p><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>在<a href="/2018/09/15/app-joint-introduction/">上一篇分享</a>中,我介绍的主题是如何在一个 Android 项目中使用 <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 进行组件化。有读者找到我说:我知道组件化很美好,但是项目改造总是需要成本的,而且这个成本是我们目前承受不了的,我们目前能做的最多的只能是 <strong>模块化</strong>,即把主 app 模块的功能拆出来,放到新的 <strong>library</strong> 模块里,但目前面临的最直接的问题就是,<strong>拆出来的新模块如何调用主 app 模块里的方法</strong>。能否不用组件化那一整套工具,先用 <strong>最轻量级的方法</strong> 解决这个跨模块方法调用的问题?</p>
</summary>
<category term="Android 组件化" scheme="http://prototypez.github.io/tags/Android-%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
</entry>
<entry>
<title>Cross module method invocation made easy</title>
<link href="http://prototypez.github.io/2018/10/10/cross-module-method-invocation-made-easy/"/>
<id>http://prototypez.github.io/2018/10/10/cross-module-method-invocation-made-easy/</id>
<published>2018-10-10T12:57:00.000Z</published>
<updated>2018-10-12T16:31:35.763Z</updated>
<content type="html"><![CDATA[<p>Are you building an Android App with multiple modules? If so, I guess you maybe facing the same problem as me, that is: <strong>Cross Module Method Invocation</strong>. It’s easy to call methods from library modules in our application module. But it’s annoying to invoke methods from the application module in our library modules. </p><a id="more"></a><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>In order to solve this problem, I wrote a simple tool called <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a>. With <strong>AppJoint</strong>, now we can call any methods from any module in anywhere. </p><p>Assuming our project structure is like below:</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">projectRoot</span><br><span class="line"> +--app</span><br><span class="line"> +--module1</span><br><span class="line"> +--module2</span><br></pre></td></tr></table></figure><p>Here, <code>app</code> is an application module and <code>module1</code>/<code>module2</code> are library modules. <code>app</code> module depends on <code>module1</code> and <code>module2</code>:</p><p>If we wish the <code>app</code> module to provide services to other modules like <code>module1</code> and <code>module2</code>, we need to create a new library first and define service interfaces inside it and make all modules have the dependency of this new module.</p><p>For example, I create a new library module named <code>service</code> and create several kotlin interfaces which representing the services that each module wants to provide for other modules to use:</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><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">projectRoot</span><br><span class="line"> +--app</span><br><span class="line"> +--module1</span><br><span class="line"> +--module2 </span><br><span class="line"> +--service</span><br><span class="line"> | +--main</span><br><span class="line"> | | +--kotlin</span><br><span class="line"> | | | +--com.yourPackage</span><br><span class="line"> | | | | +--AppService.kt</span><br><span class="line"> | | | | +--Module1Service.kt</span><br><span class="line"> | | | | +--Module2Service.kt</span><br></pre></td></tr></table></figure><ul><li>Methods in <code>AppService</code> are provided by the <code>app</code> module for other modules to use</li><li>Methods in <code>Module1Service</code> are provided by the <code>module1</code> module for other modules to use</li><li>Methods in <code>Module2Service</code> are provided by the <code>module2</code> module for other modules to use</li></ul><p>All modules should include the <code>service</code> module excluding the <code>service</code> module itself:</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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> ...</span><br><span class="line"> implementation project(<span class="string">":service"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Maybe our <code>AppService.kt</code> source codes are like below:</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">AppService</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">// start Activity from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">startActivityOfApp</span><span class="params">(context: <span class="type">Context</span>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// get Fragment from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">obtainFragmentOfApp</span><span class="params">()</span></span>: Fragment</span><br><span class="line"></span><br><span class="line"> <span class="comment">// call synchronous method from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">callMethodSyncOfApp</span><span class="params">()</span></span>: String</span><br><span class="line"></span><br><span class="line"> <span class="comment">// call asynchronous method from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">callMethodAsyncOfApp</span><span class="params">(callback: <span class="type">AppCallback</span><<span class="type">AppEntity</span>>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// get RxJava Observable from app module</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">observableOfApp</span><span class="params">()</span></span>: Observable<AppEntity></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>And then we write an implementation of <code>AppService</code> in the <code>app</code> module:</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="meta">@ServiceProvider</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AppServiceImpl</span> : <span class="type">AppService {</span></span></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">startActivityOfApp</span><span class="params">(context: <span class="type">Context</span>)</span></span> {</span><br><span class="line"> AppActivity.start(context)</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">obtainFragmentOfApp</span><span class="params">()</span></span>: Fragment {</span><br><span class="line"> <span class="keyword">return</span> AppFragment.newInstance()</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">callMethodSyncOfApp</span><span class="params">()</span></span>: String {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"syncMethodResultApp"</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">callMethodAsyncOfApp</span><span class="params">(callback: <span class="type">AppCallback</span><<span class="type">AppEntity</span>>)</span></span> {</span><br><span class="line"> Thread { callback.onResult(AppEntity(<span class="string">"asyncMethodResultApp"</span>)) }.start()</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">observableOfApp</span><span class="params">()</span></span>: Observable<AppEntity> {</span><br><span class="line"> <span class="keyword">return</span> Observable.just(AppEntity(<span class="string">"rxJavaResultApp"</span>))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Note that we add a <code>@ServiceProvider</code> annotation on the <code>AppServiceImpl</code> class.</p><p>Now, if we need to call methods of <code>AppService</code> inside <code>module1</code> or <code>module2</code>, we just need to write codes as below:</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> appService = AppJoint.service(AppService::<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="comment">// call methods inside AppService</span></span><br><span class="line">appService.callMethodSyncOfApp()</span><br></pre></td></tr></table></figure><p>That’s all, the <code>@ServiceProvider</code> annotation and the <code>AppJoint.service</code> method are the only two API.</p><h2 id="Getting-started"><a href="#Getting-started" class="headerlink" title="Getting started"></a>Getting started</h2><p>It’s easy to include AppJoint.</p><ol><li>Add the <strong>AppJoint</strong> plugin dependency to the <code>build.gradle</code> file in project root:</li></ol><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><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">buildscript {</span><br><span class="line"> ...</span><br><span class="line"> dependencies {</span><br><span class="line"> ...</span><br><span class="line"> classpath <span class="string">'io.github.prototypez:app-joint:{latest_version}'</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="2"><li>Add the <strong>AppJoint</strong> dependency to every module:</li></ol><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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> ...</span><br><span class="line"> implementation <span class="string">"io.github.prototypez:app-joint-core:{latest_version}"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="3"><li>Apply the <strong>AppJoint</strong> plugin to your main app module: </li></ol><figure class="highlight groovy"><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">apply <span class="string">plugin:</span> <span class="string">'com.android.application'</span></span><br><span class="line">apply <span class="string">plugin:</span> <span class="string">'app-joint'</span></span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Github of AppJoint: <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">https://github.com/PrototypeZ/AppJoint</a></p><p>Have fun!</p>]]></content>
<summary type="html">
<p>Are you building an Android App with multiple modules? If so, I guess you maybe facing the same problem as me, that is: <strong>Cross Module Method Invocation</strong>. It’s easy to call methods from library modules in our application module. But it’s annoying to invoke methods from the application module in our library modules. </p>
</summary>
<category term="calling methods of other modules including the application module" scheme="http://prototypez.github.io/tags/calling-methods-of-other-modules-including-the-application-module/"/>
</entry>
<entry>
<title>回归初心:极简 Android 组件化方案 — AppJoint</title>
<link href="http://prototypez.github.io/2018/09/15/app-joint-introduction/"/>
<id>http://prototypez.github.io/2018/09/15/app-joint-introduction/</id>
<published>2018-09-15T06:26:00.000Z</published>
<updated>2018-10-07T09:02:27.799Z</updated>
<content type="html"><![CDATA[<p>Android 组件化的概念大概从两年前开始有人讨论,到目前为止,技术已经慢慢沉淀下来,越来越多团队开源了自己组件化框架。本人所在团队从去年开始调研组件化框架,在了解社区众多组件化方案之后,决定自研组件化方案。为什么明明已经有很多轮子可以用了,却还是决定要自己造个新轮子呢?</p><a id="more"></a><p>主要的原因是在调研了诸多组件化方案之后,发现尽管它们都有各自的优点,但是依然有一些地方不是令人十分满意。而其中最重要的一个因素就是引入组件化方案成本较高,对已有项目改造过大。我想这一点应该很多人都有相同的体会,很多时候 <strong>我们对于项目的重构是需要与新需求的迭代同步进行的</strong> ,几乎很难停下来只做项目的组件化。</p><p>另外一点,我不太希望自己的项目和某一款组件化框架 <strong>强耦合</strong>。 Activity 的路由方案也好,跨模块的同步或异步方法调用也好,我希望能够沿用项目已有的调用方式,而不是使用某款组件化框架自己特定的调用方式。例如某个接口已经基于 <strong>RxJava</strong> 封装为了 <code>Observable</code> 的接口,我就不太希望因为组件化的关系,这个接口位于另一个模块之后,我就不得不用这个组件化框架定义的方式去调用,我还是希望以 <strong>RxJava</strong> 的方式去调用。</p><h2 id="回归初心"><a href="#回归初心" class="headerlink" title="回归初心"></a>回归初心</h2><p>我认为目前想要进行组件化的项目应该可以分为两类:</p><ul><li>包含有一个 <strong>application</strong> 模块,以及一些技术组件的 <strong>library</strong> 模块(业务无关)。</li><li>除了 <strong>application</strong> 模块以外,已经存在若干包含业务的 <strong>library</strong> 模块和技术的 <strong>library</strong> 模块。</li></ul><p>无论是哪种类型的项目,面临的问题应该都是类似的,<strong>那就是项目大起来以后,编译实在是太慢了</strong>。</p><p>除此以外,就是 <strong>跨模块的功能调用非常不便</strong> ,这个问题主要体现在上面列举的第二种类型的项目。本人所在的项目在组件化之前就是上面列举的第二种类型的项目,<strong>application</strong> 模块最早用来承载业务逻辑代码,随着业务发展,大概是某位开发人员觉得, “不行,这样下去 <strong>application</strong> 模块代码数量会失控的”,于是后续新的业务模块都会新开一个 <strong>library</strong> 模块进行开发,就这样断断续续目前有了大概 20+ 个 <strong>library</strong> 模块(业务相关模块,技术模块不包含在内)。</p><p>这种做法是符合软件工程思想的,但是也带来了一些棘手的问题,由于 <strong>application</strong> 模块里的业务功能和 <strong>library</strong> 模块里的业务功能在逻辑地位上是平等的,所以难免会有互相调用的情况,但是它们在项目依赖层次上却不是处于相等的地位,<strong>application</strong> 调用 <strong>library</strong> 倒没事,但是反过来调用就成了问题。另外,剩下这 20 + 个 <strong>library</strong> 模块在依赖层次中也不全是属于同一层次的,<strong>library</strong> 模块之间互相依赖也很复杂。</p><p>所以我期望的组件化方案要求解决的问题很简单:</p><ul><li>业务模块单独编译,单独运行,而不是耗费大量时间全量编译整个 App</li><li>跨模块的调用应该优雅,无论两个模块在依赖树中处于什么样的位置,都可以很简单的互相调用</li><li>不要有太多的学习成本,沿用目前已有的开发方式,避免代码和具体的组件化框架绑定</li><li>组件化的过程可以是渐进的,立即拆分代码不是组件化的前置条件</li><li>轻量级,不要引入过多中间层次(例如序列化反序列化)导致不必要的性能开销以及维护复杂度</li></ul><p>基于上述的思想,我们开发了 <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 这个框架用来帮助我们实现组件化。</p><p><img src="https://raw.githubusercontent.com/PrototypeZ/AppJoint/master/app-joint-logo.png" alt=""></p><p><strong>AppJoint</strong> 是一个非常简单有效的方案,引入 <strong>AppJoint</strong> 进行组件化所有的 API 只包含 <strong>3</strong> 个注解,加 <strong>1</strong> 个方法,这可能是目前最简单的组件化方案了,我们的框架不追求功能要多么复杂强大,只专注于框架本身实用、简单与高效。而且整体实现也非常简单,核心源码 <strong>不到500行</strong>。</p><h2 id="模块独立运行遇到的问题"><a href="#模块独立运行遇到的问题" class="headerlink" title="模块独立运行遇到的问题"></a>模块独立运行遇到的问题</h2><p>本人接触最早的组件化方案是 <a href="https://github.com/luojilab/DDComponentForAndroid" target="_blank" rel="noopener">DDComponentForAndroid</a>,学习这个方案给了我很多启发,在这个方案中,作者提出,可以在 <code>gradle.properties</code> 中新增一个变量 <code>isRunAlone=true</code> ,用来控制某个业务模块是 <strong>以 library 模块集成到 App 的全量编译中</strong> 还是 <strong>以 application 模块独立编译启动</strong> 。不知道是不是很多人也受了相同的启发,后面很多的组件化框架都是使用类似的方案:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (isRunAlone.toBoolean()) { </span><br><span class="line"> apply <span class="string">plugin:</span> <span class="string">'com.android.application'</span></span><br><span class="line">} <span class="keyword">else</span> { </span><br><span class="line"> apply <span class="string">plugin:</span> <span class="string">'com.android.library'</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据我本人的实践,这种方式有一些缺点。首先有一些开源框架在 <strong>library</strong> 模块中和在 <strong>application</strong> 模块中使用方法是不一样的,例如 <a href="https://github.com/JakeWharton/butterknife" target="_blank" rel="noopener">ButterKinfe</a> , 在 <strong>application</strong> 中使用 <code>R.id.xxx</code>,在 <strong>library</strong> 模块中使用 <code>R2.id.xxx</code> ,如果想组件化,代码必须保证在两种情况下都可用,所以基本只能抛弃 <strong>ButterKnife</strong> 了,这会给项目带来巨大的改造成本。</p><p>除此以外,还有一些开源框架是只能在 <strong>application</strong> 模块中配置的,配置完以后对整个项目的所有 <strong>library</strong> 模块都生效的,例如一些字节码修改的框架(比如 AOP 一类的),这是一种情况。还有一种情况,如果原先项目已经是多模块的情况下,可能多个模块的初始化都是放在 <strong>application</strong> 模块里,因为 <strong>application</strong> 模块是 <strong>上帝模块</strong>,他可以访问到项目中任意一块代码,所以在这里做初始化是最省事的。但是现在拆分为模块之后,因为每个模块需要独立运行,所以模块需要负责自身的初始化,可是有时候这个模块的初始化是只能在 <strong>application</strong> 模块里才可以做的,我们把这段逻辑下放到 <strong>library</strong> 之后,如何初始化就成了问题。</p><p>这两种情况,如果我们使用 <code>gradle.properties</code> 中的变量来切换 <strong>application</strong> 和 <strong>library</strong> 的话,我们势必需要在这个模块中维护两套逻辑,一套是在 <strong>application</strong> 模式下的启动逻辑,一套是在 <strong>library</strong> 模式下的启动逻辑。原先这个模块是专注自己本身的业务逻辑的,现在不得不为了能够独立作为 <strong>application</strong> 启动,而加入许多其他代码。一方面 <code>build.gradle</code> 文件中会充满很多 <code>if - else</code>,另一方面 Java 源码中也会加入许多判断是否独立运行的逻辑。</p><p>最终 Release App 打包时,这些模块是作为 <strong>library</strong> 存在的,但是我们为了组件化已经在这个模块中加入了很多帮助该模块独立运行(以 <strong>application</strong> 模式)的代码(例如模块需要单独运行,需要一个属于这个模块的 Laucher Activity),虽然这些代码在线上不会生效,可是从洁癖的角度来讲,这些代码其实不应该被打包进去。其实说了这么多无非就是想说明,如果我们希望通过某个变量来控制模块以 <strong>application</strong> 形式还是以 <strong>library</strong> 形式存在,那么我们肯定要在这个模块中加入维护两者的差异的代码,而且可能代码量还不少,最后代码呈现的状态可能是不太优雅的。</p><p>此外模块中的 <code>AndroidManifest.xml</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (isRunAlone.toBoolean()) {</span><br><span class="line"> manifest.srcFile <span class="string">'src/main/runalone/AndroidManifest.xml'</span></span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> manifest.srcFile <span class="string">'src/main/AndroidManifest.xml'</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但是 <strong>xml</strong> 毕竟不是代码,没有封装继承这些面向对象的特性,所以每当我们增加、修改、删除四大组件的时候,都需要记得要在两个 <code>AndroidManifest.xml</code> 都做对应的修改。除了 <code>AndroidManifest.xml</code> 以外,资源文件也存在这个问题,虽然工作量不至于特别巨大,但这样的做法其实已经违背了面向对象的设计原则。</p><p>最后还有一个问题,每当模块在 <strong>application</strong> 模式和 <strong>library</strong> 模式之间进行切换的时候,都需要重新 <strong>Gradle Sync</strong> 一次,我想既然是需要组件化的项目那肯定已经是那种编译速度极慢的项目了,即使是 <strong>Gradle Sync</strong> 也需要等待不少时间,这点也是我们不太能接收的。</p><h2 id="创建多个-Application-模块"><a href="#创建多个-Application-模块" class="headerlink" title="创建多个 Application 模块"></a>创建多个 Application 模块</h2><p>我们最后是如何解决模块的单独编译运行这个问题的呢?答案是 <strong>为每个模块新建一个对应的 application 模块</strong> 。也许你会对此表示怀疑:如果为每个业务模块配一个用于独立启动的 <strong>application</strong> 模块,那模块会显得特别多,项目看起来会非常的乱的。但是其实我们可以把所有用于独立启动业务模块的 <strong>application</strong> 模块收录到一个目录中:</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><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">projectRoot</span><br><span class="line"> +--app</span><br><span class="line"> +--module1</span><br><span class="line"> +--module2</span><br><span class="line"> +--standalone</span><br><span class="line"> | +--module1Standalone</span><br><span class="line"> | +--module2Standalone</span><br></pre></td></tr></table></figure><p>在上面这个项目结构图中,<code>app</code> 模块是全量编译的 <strong>application</strong> 模块入口,<code>module1</code> 和 <code>module2</code> 是两个业务 <strong>library</strong> 模块, <code>module1Standalone</code> 和 <code>module2Standalone</code> 是分别使用来独立启动 <code>module1</code> 和 <code>module2</code> 的 2 个 <strong>application</strong> 模块,这两个模块都被收录在 <code>standalone</code> 文件夹下面。事实上,<code>standalone</code> 目录下的模块很少需要修改,所以这个目录大多数情况下是属于折叠状态,不会影响整个项目结构的美观。</p><p>这样一来,在项目根目录下的 <code>settings.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><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main app</span></span><br><span class="line">include <span class="string">':app'</span></span><br><span class="line"><span class="comment">// library modules</span></span><br><span class="line">include <span class="string">':module1'</span></span><br><span class="line">include <span class="string">':module2'</span></span><br><span class="line"><span class="comment">// for standalone modules</span></span><br><span class="line">include <span class="string">':standalone:module1Standalone'</span></span><br><span class="line">include <span class="string">':standalone:module2Standalone'</span></span><br></pre></td></tr></table></figure><p>在主 App 模块(<code>app</code> 模块)的 <code>build.gradle</code> 文件里,我们只需要依赖 <code>module1</code> 和 <code>module2</code> ,两个 <strong>standalone</strong> 模块只和各自对应的业务模块的独立启动有关,它们不需要被 <code>app</code> 模块依赖,所以 <code>app</code> 模块的 <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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> implementation project(<span class="string">':module1'</span>)</span><br><span class="line"> implementation project(<span class="string">':module1'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那些用于独立运行的 <strong>application</strong> 模块里的 <code>build.gradle</code> 文件中,就只有一个依赖,那就是需要被独立运行的 <strong>library</strong> 模块。以 <code>standalone/module1Standalone</code> 为例,它对应的 <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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> implementation project(<span class="string">':module1'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>在 Android Studio 中创建模块,默认模块是位于项目根目录之下的,如果希望把模块移动到某个文件夹下面,需要对模块右键,选择 “Refactor – Move” 移动到指定目录之下。</p></blockquote><p>当我们创建好这些 <strong>application</strong> 模块之后,在 Android Studio 的运行小三角按钮旁边,就可以选择我们需要运行哪个模块了:</p><p><img src="/images/standalone.png" alt=""></p><p>这样一来,我们首先可以感受到的一点就是模块不再需要改 <strong>gradle.properties</strong> 文件切换 <strong>library</strong> 和 <strong>application</strong> 状态了,也不再需要忍受 <strong>Gradle Sync</strong> 浪费宝贵的开发时间,想全量编译就全量编译,想单独启动就单独启动。</p><p>由于专门用于单独启动的 <strong>standalone 模块</strong> 的存在,业务的 <strong>library</strong> 模块只需要按自己是 <strong>library</strong> 模块这一种情况开发即可,不需要考虑自己会变成 <strong>application</strong> 模块,所以无论是新开发一个业务模块还是从一个老的业务模块改造成组件化形式的模块,所要做的工作都会比之前更轻松。而之前提到的,为了让业务模块单独启动所需要的配置、初始化工作都可以放到 <strong>standalone 模块</strong> 里,并且不用担心这些代码被打包到最终 Release 的 App 中,前面例子中提到的用来使模块单独启动的 Launcher Activity,只要把它放到 <strong>standalone 模块</strong> 模块即可。</p><p><code>AndroidManifest.xml</code> 和资源文件的维护也变轻松了。四大组件的增删改只需要在业务的 <strong>library</strong> 模块修改即可,不需要维护两份 <code>AndroidManifest.xml</code> 了,<strong>standalone 模块</strong> 里的 <code>AndroidManifest.xml</code> 只需要包含模块独立启动时和 <strong>library</strong> 模块中的 <code>AndroidManifest.xml</code> 不同的地方即可(例如 Launcher Activity 、图标等),编译工具会自动完成两个文件的 merge。 </p><blockquote><p>推荐在 <strong>standalone 模块</strong> 内指定一个不同于主 App 的 applicationId,即模块单独启动的 App 与主 App 可以在手机内共存。</p></blockquote><p>我们分析一下这个方案,和原先的比,首先缺点是,引入了很多新的 <strong>standalone 模块</strong>,项目似乎变复杂了。但是优点也是明显的,组件化的逻辑更加清晰,尤其是在老项目改造情况下,所需要付出的工作量更少,而且不需要在开发期间频繁 <strong>Gradle Sync</strong>。 总的来说,改造后的组件化项目更符合软件工程的设计原则,尤其是<a href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle" target="_blank" rel="noopener">开闭原则</a>(open for extension, but closed for modification)。</p><p>介绍到这里为止,我们还没有使用任何 <a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 的 API,我们之所以没有借助任何组件化框架的 API 来实现模块的独立启动,是因为本文一开始提出的,<strong>我们不希望项目和任何组件化框架强绑定</strong>, 包括 <strong>AppJoint</strong> 框架本身,<strong>AppJoint</strong> 框架本身的设计是与项目松耦合的,所以使用了 <strong>AppJoint</strong> 框架进行组件化的项目,如果今后希望可以切换到其它更优秀的组件化方案,理论上是很轻松的。</p><h2 id="为每个模块准备-Application"><a href="#为每个模块准备-Application" class="headerlink" title="为每个模块准备 Application"></a>为每个模块准备 Application</h2><p>在组件化之前,我们常常把项目中需要在启动时完成的初始化行为,放在自定义的 <code>Application</code> 中,根据本人的项目经验,初始化行为可以分为以下两类:</p><ul><li><strong>业务相关的初始化</strong>。例如服务器推送长连接建立,数据库的准备,从服务器拉取 CMS 配置信息等。</li><li><strong>与业务无关的技术组件的初始化</strong>。例如日志工具、统计工具、性能监控、崩溃收集、兼容性方案等。</li></ul><p>我们在上一步中,为每个业务模块建立了独立运行的 <strong>standalone 模块</strong> ,但是此时还并不能把业务模块独立启动起来,因为模块的初始化工作并没有完成。我们在前面介绍 <strong>AppJoint</strong> 的设计思想的时候,曾经说过我们希望组件化方案最好 『<strong>不要有太多的学习成本,沿用目前已有的开发方式</strong>』,所以这里我们的解决方案是,在每个业务模块里新建一个自定义的 <code>Application</code> 类,用来实现该业务模块的初始化逻辑,这里以在 <code>module1</code> 中新建自定义 <code>Application</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="meta">@ModuleSpec</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1Application</span> <span class="keyword">extends</span> <span class="title">Application</span> </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">void</span> <span class="title">onCreate</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate();</span><br><span class="line"> <span class="comment">// do module1 initialization</span></span><br><span class="line"> Log.i(<span class="string">"module1"</span>, <span class="string">"module1 init is called"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上面的代码所示,我们在 <code>module1</code> 中新建一个自定义的 <code>Application</code> 类,名为 <code>Module1Application</code>。那我们是不是应该把与这个模块有关的所有初始化逻辑都放在这个类里面呢?并不完全是这样。</p><p>首先,对于前面提到的当前模块的 <strong>业务相关的初始化</strong> ,毫无疑问应该放在这个 <code>Module1Application</code> 类中,但是针对前面提到的该模块的 <strong>与业务无关的技术组件的初始化</strong> 放在这里就不是很合适了。</p><p>首先,从逻辑上考虑,业务无关的技术组件的初始化应该放在一个统一的地方,把它们放在主 App 的自定义 <code>Application</code> 类中比较合适,如果每个模块为了自己可以独立编译运行,都要自己初始化一遍,那么所有代码最后一起全量编译的时候,这些初始化行为就会在代码中出现好几次,这样既不合理,也可能会造成潜在问题。</p><p>那么,如果我们在 <code>Module1Application</code> 中做判断,如果它自身处于独立编译运行状态,就执行技术组件的初始化,反之,若它处于全量编译运行状态中,就不执行技术组件的初始化,由主 App 的 <code>Application</code> 来实现这些逻辑,这样是否可以呢?理论上这种方案可行,但是这么做就会遇到和前面提到的 『在 <code>gradle.properties</code> 中维护一个变量来控制模块是否独立编译』同样的问题,我们不希望把和业务无关的逻辑(用于业务模块独立启动的逻辑)打包进最终 Release 的 App。 </p><p>那应该如何解决这个问题呢?解决方案和前面一小节类似,我们不是为 <code>module1</code> 模块准备了一个 <code>module1Standalone</code> 模块吗?既然技术相关的组件的初始化并不是 <code>module1</code> 模块的核心,只和 <code>module1</code> 模块的独立启动有关,那么放在 <code>module1Standalone</code> 模块里是最合适的,因为这个模块只会在 <code>module1</code> 的独立编译运行中使用到,它的任何代码都不会被打包到最终 Release 的 App 中。我们可以在 <code>module1Standalone</code> 中定义一个 <code>Module1StandaloneApplication</code> 类,它从 <code>Module1Application</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1StandaloneApplication</span> <span class="keyword">extends</span> <span class="title">Module1Application</span> </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">void</span> <span class="title">onCreate</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// module1 init inside super.onCreate()</span></span><br><span class="line"> <span class="keyword">super</span>.onCreate();</span><br><span class="line"> <span class="comment">// initialization only used for running module1 standalone</span></span><br><span class="line"> Log.i(<span class="string">"module1Standalone"</span>, <span class="string">"module1Standalone init is called"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>并且我们在 <code>module1Standalone</code> 模块的 <code>AndroidManifest.xml</code> 中把 <code>Module1StandaloneApplication</code> 设置为 Standalone App 使用的自定义 <code>Application</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></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">application</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:icon</span>=<span class="string">"@mipmap/module1_launcher"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:label</span>=<span class="string">"@string/module1_app_name"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:theme</span>=<span class="string">"@style/AppTheme"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:name</span>=<span class="string">".Module1StandaloneApplication"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">".Module1MainActivity"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.MAIN"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.LAUNCHER"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">activity</span>></span></span><br><span class="line"><span class="tag"></<span class="name">application</span>></span></span><br></pre></td></tr></table></figure><blockquote><p>在上面的代码中,我们除了设置了自定义的 <code>Application</code> 以外,还设置了一个 <strong>Launcher Activity</strong> (<code>Module1MainActivity</code>),这个 <code>Activity</code> 即为模块的启动 <code>Activity</code>,由于它只存在于模块的独立编译运行期间,App 全量打包时是不包含这个 <code>Module1MainActivity</code> 的,所以我们可以在里面定义一些方便模块独立调试的功能,例如快速前往某个页面以及创建 Mock 数据。</p></blockquote><p>这样,只要我们单独运行 <code>module1Standalone</code> 这个模块的时候,使用的 <code>Application</code> 类就是 <code>Module1StandaloneApplication</code>。在开发时,我们需要单独调试 <code>module1</code> 时,我们只需要启动 <code>module1Standalone</code> 这个模块进行调试即可;而在 App 需要全量编译时,我们则正常启动原来的主 App 。无论是哪种情况, <code>module1</code> 这个模块始终是以 <code>library</code> 形式存在的,<strong>这意味着,如果我们希望把原先的业务模块改造成组件化模块,需要的改造量缩小很多</strong>,我们改造的过程主要是在 <strong>增加代码</strong>,而不是 <strong>修改代码</strong>,这点符合软件工程中的『<strong>开闭原则</strong>』。</p><p>写到这里,我们其实还有一个问题没有解决。<code>Module1Application</code> 目前除了被 <code>Module1StandaloneApplication</code> 继承以外,没有被任何其它地方引用到。您可能会有疑问:那我们如何保证 App 全量编译运行时,<code>Module1Application</code> 里的初始化逻辑会被调用到呢?细心的您可能早就已经发现了:我们在上面定义 <code>Module1Application</code> 时,同时标记了一个注解 <code>@ModuleSpec</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="meta">@ModuleSpec</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1Application</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>{</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个注解的作用是告知 <strong>AppJoint</strong> 框架,我们需要确保当前模块该 <code>Application</code> 中的初始化行为,能够在最终全量编译时,被主 App 的 <code>Application</code> 类调用到。所以对应的,我们的主 App 模块(<code>app</code> 模块)的自定义 <code>Application</code> 类也需要被一个注解 – <code>AppSpec</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="meta">@AppSpec</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">App</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>{</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面代码中的 <code>App</code> 为主 App 对应的自定义 <code>Application</code> 类,我们给这个类上方标记了 <code>@AppSpec</code> 注解,这样系统在执行 <code>App</code> 自身初始化的同时会一并执行这些子模块的 <code>Application</code> 里对应声明周期的初始化。即:</p><ul><li><code>App</code> 执行 <code>onCreate</code> 方法时,保证也同时执行 <code>Module1Application</code> 和 <code>Module2Application</code> 的 <code>onCreate</code> 方法 。</li><li><code>App</code> 执行 <code>attachBaseContext</code> 方法时,保证也同时执行 <code>Module1Application</code> 和 <code>Module2Application</code> 的 <code>attachBaseContext</code> 方法。 </li><li>依次类推,当 <code>App</code> 执行某个生命周期方法时,保证子模块的 <code>Application</code> 的对应的生命周期方法也会被执行。</li></ul><p>这样,我们通过 <strong>AppJoint</strong> 的 <code>@ModuleSpec</code> 和 <code>@AppSpec</code> 两个注解,在主 App 的 <code>Application</code> 和子模块的 <code>Application</code> 之间建立了联系,保证了在全量编译运行时,所有业务模块的初始化行为都能被保证执行。</p><p>到这里为止,我们已经处理好了业务模块在 <strong>独立编译运行模式</strong> 和 <strong>全量编译运行模式</strong> 这两种情况下的初始化问题,目前关于 <code>Application</code> 还有一个潜在问题,我们的项目在组件化之前,我们经常会在 <code>Applictaion</code> 类的 <code>onCreate</code> 周期保存当前 <code>Appliction</code> 的引用,然后在应用的任何地方都可以使用这个 <code>Application</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">App</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> App INSTANCE;</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">onCreate</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate();</span><br><span class="line"> INSTANCE = <span class="keyword">this</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这么处理之后,我们可以在项目任意位置通过 <code>App.INSTANCE</code> 使用 <strong>Application Context</strong> 对象。但是,现在组件化改造以后,以 <code>module1</code> 为例,在独立运行模式时,应用的 <code>Application</code> 对象是 <code>Module1StandaloneApplication</code> 的实例,而在全量编译运行模式时,应用的 <code>Application</code> 对象是主 App 模块的 <code>App</code> 的实例,我们如何能像之前一样,做到在项目中任何一个地方都能获取到当前使用的 <code>Application</code> 实例呢?</p><p>我们可以把项目中所有自定义 <code>Application</code> 内部保存的自身的 <code>Application</code> 实例的类型,从具体的自定义类,改为标准的 <code>Application</code> 类型,以 <code>Module1Application</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@ModuleSpec</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1Application</span> <span class="keyword">extends</span> <span class="title">Application</span> </span>{</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Application INSTANCE;</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">onCreate</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate();</span><br><span class="line"> INSTANCE = (Application)getApplicationContext()</span><br><span class="line"> <span class="comment">// do module1 initialization</span></span><br><span class="line"> Log.i(<span class="string">"module1"</span>, <span class="string">"module1 init is called"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们可以看到,如果按原来的写法, <code>INSTANCE</code> 的类型一般是具体的自定义类型 <code>Module1Application</code>,现在我们改成了 <code>Application</code>。同时 <code>onCreate</code> 方法里为 <code>INSTANCE</code> 赋值的语句不再是 <code>INSTANCE = this</code>,而是 <code>INSTANCE = (Application)getApplicationContext()</code>。这样处理以后,就可以保证 <code>module1</code> 里面的代码,无论是在 App 全量编译模式下,还是独立编译调试模式下,都可以通过 <code>Module1Application.INSTANCE</code> 访问当前的 <code>Application</code> 实例。这是由于 <strong>AppJoint</strong> 框架 <strong>保证了当主 App 的 <code>App</code> 对象被调用 <code>attachBaseContext</code> 回调时,所有组件化业务模块的 <code>Application</code> 也会被调用 <code>attachBaseContext</code> 这个回调</strong>。</p><p>这样,我们在 <code>module1</code> 这个模块里的任何位置使用 <code>Module1Application.INSTANCE</code> 总能正确地获得 <code>Application</code> 的实例。对应的,我们使用相同的方法在 <code>module2</code> 这个模块里,也可以在任何位置使用 <code>Module2Application.INSTANCE</code> 正确地获得 <code>Application</code> 的实例,而不需要知道当前处于独立编译运行状态还是全量编译运行状态。</p><blockquote><p><strong>一定不要</strong> 依赖某个业务模块自身定义的 <code>Application</code> 类的实例(例如 <code>Module1Application</code> 的实例),因为在运行时真正使用的 <code>Application</code> 实例可能不是它。</p></blockquote><p>我们已经解决业务模块在 <strong>单独编译运行模式</strong> 下和在 <strong>App 全量编译模式</strong> 下,初始化逻辑应该如何组织的问题。我们沿用了我们熟悉的自定义 <code>Application</code> 方案,来承载各个模块的初始化行为,同时利用 <strong>AppJoint</strong> 这个胶水,把每个模块的初始化逻辑集成到最终全量编译的 App 中。而这一切和 <strong>AppJoint</strong> 有关的 API 仅仅是两个注解,这里很好的说明了 <strong>AppJoint</strong> 是个学习成本低的工具,我们可以沿用我们已有的开发方式而不是改造我们原有的代码逻辑导致项目和组件化框架造成过度耦合。</p><h2 id="跨模块方法的调用"><a href="#跨模块方法的调用" class="headerlink" title="跨模块方法的调用"></a>跨模块方法的调用</h2><p>虽然目前每个模块已经有独立编译运行的可能了,但是开发一个成熟的 App 我们还有一个重要的问题没有解决,那就是跨模块的方法调用。因为我们的业务模块无论是从业务逻辑上考虑还是从在依赖树上的位置考虑,都应该是具有同等的地位的,体现在依赖层次上,这些业务模块应该是平级的,且互相之间没有依赖:</p><p><img src="/images/app-joint-structure-1.png" alt=""></p><p>上图是我们比较理想情况下的组件化的最终状态,<code>App</code> 模块不承载任何业务逻辑,它的作用仅仅是作为一个 <code>application</code> 壳把 <code>Module1</code> ~ <code>Module(n)</code> 这个 n 个模块的功能都集成在一起成为一个完整的 App。<code>Module1</code> ~ <code>Module(n)</code> 这 n 个模块互相之间不存在任何交叉依赖,它们各自仅包含各自的业务逻辑。这种方式虽然完成了业务模块之间的解耦,但是给我们带来的新的挑战:业务模块之间互相调用彼此的功能是非常常见且合理的需求,但是由于这些模块在依赖层次上位于同一层次,所以显然是无法直接调用的。</p><p>此外,上图的这种形态是组件化的最终的理想状态,如果我们要将项目改造以达到这种状态,毫无疑问需要付出巨大的时间成本。在业务快速迭代期间,这是我们无法承担的成本,我们只能逐渐地改造项目,也就是说,<code>App</code> 模块内的业务代码是被逐渐拆解出来形成新的独立模块的,这意味着在组件化过程的相当长一段时间内,<code>App</code> 内还是存在业务代码的,而被拆解出来的模块内的业务逻辑代码,是有可能调用到 <code>App</code> 模块内的代码的。这是一种很尴尬的状态,在依赖层次中,位于依赖层次较低位置的代码反而要去调用依赖层次较高位置的代码。</p><p>针对这种情况,我们比较容易想到,我们再新建一个模块,例如 <code>router</code> 模块,我们在这个模块内定义 <strong>所有业务模块希望暴露给其它模块调用的方法</strong>,如下图:</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><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">projectRoot</span><br><span class="line"> +--app</span><br><span class="line"> +--module1</span><br><span class="line"> +--module2</span><br><span class="line"> +--standalone</span><br><span class="line"> +--router</span><br><span class="line"> | +--main</span><br><span class="line"> | | +--java</span><br><span class="line"> | | | +--com.yourPackage</span><br><span class="line"> | | | | +--AppRouter.java</span><br><span class="line"> | | | | +--Module1Router.java</span><br><span class="line"> | | | | +--Module2Router.java</span><br></pre></td></tr></table></figure><p>在上面的项目结构层次中,我们在新建的 <code>router</code> 模块下定义了 3 个 <strong>接口</strong>:</p><ul><li><code>AppRouter</code> 接口声明了 <code>app</code> 模块暴露给 <code>module1</code>、<code>module2</code> 的方法的定义。</li><li><code>Module1Router</code> 接口声明了 <code>module1</code> 模块暴露给 <code>app</code>、<code>module2</code> 的方法的定义。</li><li><code>Module2Router</code> 接口声明了 <code>module2</code> 模块暴露给 <code>module1</code>、<code>app</code> 的方法的定义。</li></ul><p>以 <code>AppRouter</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></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">AppRouter</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 普通的同步方法调用</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function">String <span class="title">syncMethodOfApp</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="comment"> * 以 RxJava 形式封装的异步方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function">Observable<String> <span class="title">asyncMethod1OfApp</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="comment"> * 以 Callback 形式封装的异步方法</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">asyncMethod2OfApp</span><span class="params">(Callback<String> callback)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们在 <code>AppRouter</code> 这个接口内定义了 1 个同步方法,2 个异步方法,这些方法是 <code>app</code> 模块需要暴露给 <code>module1</code> 、 <code>module2</code> 的方法,同时 <code>app</code> 模块自身也需要提供这个接口的实现,所以首先我们需要在 <code>app</code> 、<code>module1</code> 、<code>module2</code> 这三个模块的 <code>build.gradle</code> 文件中依赖 <code>router</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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> <span class="comment">// Other dependencies</span></span><br><span class="line"> ...</span><br><span class="line"> api project(<span class="string">":router"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>这里依赖 <code>router</code> 模块的方式是使用 <code>api</code> 而不是 <code>implementation</code> 是为了把 <code>router</code> 模块的信息暴露给依赖了这些业务模块的 <strong>standalone 模块</strong>,<code>app</code> 模块由于没有别的模块依赖它,不受上面所说的限制,可以写成 <code>implementation</code> 依赖。</p></blockquote><p>然后我们回到 <code>app</code> 模块,为刚刚在 <code>router</code> 定义的 <code>AppRouter</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@ServiceProvider</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AppRouterImpl</span> <span class="keyword">implements</span> <span class="title">AppRouter</span> </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> String <span class="title">syncMethodOfApp</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"syncMethodResult"</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> Observable<String> <span class="title">asyncMethod1OfApp</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Observable.just(<span class="string">"asyncMethod1Result"</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">asyncMethod2OfApp</span><span class="params">(<span class="keyword">final</span> Callback<String> callback)</span> </span>{</span><br><span class="line"> <span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() {</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"> callback.onResult(<span class="string">"asyncMethod2Result"</span>);</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们可以发现,我们把 <code>app</code> 模块内的方法暴露给其它模块的方式和我们平时写代码并没有什么不同,就是声明一个接口提供给其它模块,同时在自己内部编写一个这个接口的实现类。无论是同步还是异步,无论是 Callback 的方式,还是 RxJava 的方式,都可以使用我们原有的开发方式。唯一的区别就是,我们在 <code>AppRouterImpl</code> 实现类上方标记了一个 <code>@ServiceProvider</code> 注解,这个注解的作用是用来通知 <code>AppJoint</code> 框架在 <code>AppRouter</code> 和 <code>AppRouterImpl</code> 之间建立联系,这样其它模块就可以通过 <code>AppJoint</code> 找到一个 <code>AppRouter</code> 的实例并调用里面的方法了。</p><p>假设现在 <code>module1</code> 中需要调用 <code>app</code> 模块中的 <code>asyncMethod1OfApp</code> 方法,由于 <code>app</code> 模块已经把这个方法声明在了 <code>router</code> 模块的 <code>AppRouter</code> 接口中了,<code>module1</code> 由于也依赖了 <code>router</code> 模块,所以 <code>module1</code> 内可以访问到 <code>AppRouter</code> 这个接口,但是却访问不到 <code>AppRouterImpl</code> 这个实现类,因为这个类定义在 <code>app</code> 模块内,这时候我们可以使用 <strong>AppJoint</strong> 来帮助 <code>module1</code> 获取 <code>AppRouter</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></pre></td><td class="code"><pre><span class="line">AppRouter appRouter = AppJoint.service(AppRouter.class);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获得同步调用的结果 </span></span><br><span class="line">String syncResult = appRouter.syncMethodOfApp();</span><br><span class="line"><span class="comment">// 发起异步调用</span></span><br><span class="line">appRouter.asyncMethod1OfApp()</span><br><span class="line"> .subscribe((result) -> {</span><br><span class="line"> <span class="comment">// handle asyncResult</span></span><br><span class="line"> });</span><br><span class="line"><span class="comment">// 发起异步调用</span></span><br><span class="line">appRouter.asyncMethod2OfApp(<span class="keyword">new</span> Callback<String>() {</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">onResult</span><span class="params">(String data)</span> </span>{</span><br><span class="line"> <span class="comment">// handle asyncResult</span></span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>在上面的代码中,我们可以看到,除了第一步获取 <code>AppRouter</code> 接口的实例我们用到了 <strong>AppJoint</strong> 的 API <code>AppJoint.service</code> 以外,剩下的代码,<code>module1</code> 调用 <code>app</code> 模块内的方法的方式,和我们原来的开发方式没有任何区别。<code>AppJoint.service</code> 就是 <strong>AppJoint</strong> 所有 API 里唯一的那个方法。</p><p>也就是说,如果一个模块需要提供方法供其他模块调用,需要做以下步骤:</p><ul><li>把接口声明在 <code>router</code> 模块中</li><li>在自己模块内部实现上一步中声明的接口,同时在实现类上标记 <code>@ServiceProvider</code> 注解</li></ul><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></pre></td><td class="code"><pre><span class="line">AppRouter appRouter = AppJoint.service(AppRouter.class);</span><br><span class="line">Module1Router module1Router = AppJoint.service(Module1Router.class);</span><br><span class="line">Module2Router module2Router = AppJoint.service(Module2Router.class);</span><br></pre></td></tr></table></figure><p>这种方法不仅仅可以保证处于相同依赖层次的业务模块可以互相调用彼此的方法,还可以支持从业务模块中调用 <code>app</code> 模块内的方法。这样就可以 <strong>保证我们组件化的过程可以是渐进的</strong> ,我们不需要一口气把 <code>app</code> 模块中的所有功能全部拆分到各个业务模块中,我们可以逐渐地把功能拆分出来,以保证我们的业务迭代和组件化改造同时进行。当我们的 <code>AppRouter</code> 里面的方法越来越少直到最后可以把这个类从项目中安全删除的时候,我们的组件化改造就完成了。</p><h2 id="模块独立编译运行模式下跨模块方法的调用"><a href="#模块独立编译运行模式下跨模块方法的调用" class="headerlink" title="模块独立编译运行模式下跨模块方法的调用"></a>模块独立编译运行模式下跨模块方法的调用</h2><p>上面一个小结中我们已经介绍了使用 <strong>AppJoint</strong> 在 App 全量编译运行期间,业务模块之间跨模块方法调用的解决方案。在全量编译期间,我们可以通过 <code>AppJoint.service</code> 这个方法找到指定模块提供的接口的实例,但是在模块单独编译运行期间,其它的模块是不参与编译的,它们的代码也不会打包进用于模块独立运行的 <strong>standalaone 模块</strong>,我们如何解决在模块单独编译运行模式下,跨模块调用的代码依然有效呢?</p><p>以 <code>module1</code> 为例,首先为了便于在 <code>module1</code> 内部任何地方都可以调用其它模块的方法,我们创建一个 <code>RouterServices</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></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">RouterServices</span> </span>{</span><br><span class="line"> <span class="comment">// app 模块对外暴露的接口</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> AppRouter sAppRouter = AppJoint.service(AppRouter.class);</span><br><span class="line"> <span class="comment">// module2 模块对外暴露的接口</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Module2Router sModule2Router = AppJoint.service(Module2Router.class);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有了这个类以后,我们在 <code>module1</code> 内部如果需要调用其它模块的功能,我们只需要使用 <code>RouterServices.sAppRouter</code> 和 <code>RouterServices.sModule2Router</code> 这两个对象就可以了。但是如果是刚刚提到的 <code>module1</code> 独立编译运行的情况,即启动的 <code>application</code> 模块是 <code>module1Standalone</code>, 那么 <code>RouterServices.sAppRouter</code> 和 <code>RouterServices.sModule2Router</code> 这两个对象的值均为 <code>null</code> ,这是因为 <code>app</code> 和 <code>module2</code> 这两个模块此时是没有被编译进来的。</p><p>如果我们需要在这种情况下保证已有的 <code>module1</code> 内部的通过 <code>RouterServices.sAppRouter</code> 和 <code>RouterServices.sModule2Router</code> 进行跨模块方法调用的代码依然能工作,我们就需要对这两个引用手动赋值,即我们需要创建 Mock 了 <code>AppRouter</code> 和 <code>Module2Router</code> 功能的类。这些类由于只对 <code>module1</code> 的独立编译运行有意义,所以这些类最合适的位置是放在 <code>module1Standalone</code> 这个模块内,以 <code>AppRouter</code> 的 Mock 类 <code>AppRouterMock</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></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">AppRouterMock</span> <span class="keyword">implements</span> <span class="title">AppRouter</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> String <span class="title">syncMethodOfApp</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"mockSyncMethodOfApp"</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> Observable<String> <span class="title">asyncMethod1OfApp</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Observable.just(<span class="string">"mockAsyncMethod1OfApp"</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">asyncMethod2OfApp</span><span class="params">(<span class="keyword">final</span> Callback<String> callback)</span> </span>{</span><br><span class="line"> <span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() {</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"> callback.onResult(<span class="string">"mockAsyncMethod2Result"</span>);</span><br><span class="line"> }</span><br><span class="line"> }).start();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>已经创建好了 Mock 类,接下来我们要做的是,在 <code>module1</code> 独立编译运行的模式下,用 Mock 类的对象,去替换 <code>RouterServices</code> 里面的对应的引用,由于这些逻辑只和 <code>module1</code> 的独立编译运行有关,我们不希望这些逻辑被打包进真正 Release 的 App 中,那么最合适的地方就是 <code>Module1StandaloneApplication</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1StandaloneApplication</span> <span class="keyword">extends</span> <span class="title">Module1Application</span> </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">void</span> <span class="title">onCreate</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// module1 init inside super.onCreate()</span></span><br><span class="line"> <span class="keyword">super</span>.onCreate();</span><br><span class="line"> <span class="comment">// initialization only used for running module1 standalone</span></span><br><span class="line"> Log.i(<span class="string">"module1Standalone"</span>, <span class="string">"module1Standalone init is called"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Replace instances inside RouterServices</span></span><br><span class="line"> RouterServices.sAppRouter = <span class="keyword">new</span> AppRouterMock();</span><br><span class="line"> RouterServices.sModule2Router = <span class="keyword">new</span> Module2RouterMock();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有了上面的初始化动作以后,我们就可以在 <code>module1</code> 内部安全地使用 <code>RouterServices.sAppRouter</code> 和 <code>RouterServices.sModule2Router</code> 这两个对象进行跨模块的方法调用了,无论当前是处于 App 全量编译模式还是 <code>modul1Standalone</code> 独立编译运行模式。</p><h2 id="跨模块启动-Activity-和-Fragment"><a href="#跨模块启动-Activity-和-Fragment" class="headerlink" title="跨模块启动 Activity 和 Fragment"></a>跨模块启动 Activity 和 Fragment</h2><p>在组件化改造过程中,除了跨模块的方法调用之外,跨模块启动 Activity 和跨模块引用 Fragment 也是我们经常遇到的需求。目前社区中大多数组件化方案都是使用自定义私有协议,使用 <strong>URL-Scheme</strong> 的方式来实现跨模块 Activity 的启动,这一块已经有很多成熟的方案了,有的组件化方案直接推荐使用 <strong>ARouter</strong> 来实现这块功能。<strong>但是 AppJoint 没有使用这类方案</strong>。</p><p>本文开头曾经介绍过,<strong>AppJoint</strong> 所有的 API 只包含 <strong>3</strong> 个注解加 <strong>1</strong> 个方法,而这些 API 我们在前文中已经都介绍完了,也就是说,<strong>我们没有提供专门的 API 来实现跨模块的 Activity / Fragment 调用</strong>。</p><p>我们回想一下,在没有实现组件化时,我们启动 Activity 的推荐写法如下,首先在被启动的 Activity 内实现一个静态 <code>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><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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyActivity</span> <span class="keyword">extends</span> <span class="title">AppCompatActivity</span> </span>{</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">start</span><span class="params">(Context context, String param1, Integer param2)</span> </span>{</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(context, MyActivity.class); </span><br><span class="line"> intent.putExtra(<span class="string">"param1"</span>, param1); </span><br><span class="line"> intent.putExtra(<span class="string">"param2"</span>, param2); </span><br><span class="line"> context.startActivity(intent);</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">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后我们如果在其它 Activity 中启动这个 <code>MyActivity</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">MyActivity.start(param1, param2);</span><br></pre></td></tr></table></figure><p>这里的思想是,服务的提供者应该把复杂的逻辑放在自己这里,而只提供给调用者一个简单的接口,用这个简单的接口隔离具体实现的复杂性,这是符合软件工程思想的。</p><p>那么如果目前 <code>module1</code> 模块中有一个 <code>Module1Activity</code>,现在这个 Activity 希望能够从 <code>module2</code> 启动,应该如何写呢?首先,在 <code>router</code> 模块的 <code>Module1Router</code> 内声明启动 <code>Module1Activity</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">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Module1Router</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 启动 Module1Activity</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">startModule1Activity</span><span class="params">(Context context)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后在 <code>module1</code> 模块里 <code>Module1Router</code> 对应的实现类 <code>Module1RouterImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@ServiceProvider</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1RouterImpl</span> <span class="keyword">implements</span> <span class="title">Module1Router</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">startModule1Activity</span><span class="params">(Context context)</span> </span>{</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(context, Module1Activity.class);</span><br><span class="line"> context.startActivity(intent);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样, <code>module2</code> 中就可以通过下面的方式启动 <code>module1</code> 中的 <code>Module1Activity</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">RouterServices.sModule1Router.startModule1Activity(context);</span><br></pre></td></tr></table></figure><p>跨模块获取 <code>Fragment</code> 实例也是类似的方法,我们在 <code>Module1Router</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">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Module1Router</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 启动 Module1Activity</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">startModule1Activity</span><span class="params">(Context context)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获取 Module1Fragment</span></span><br><span class="line"> <span class="function">Fragment <span class="title">obtainModule1Fragment</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>差不多的写法,我们只要在 <code>Module1RouterImpl</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@ServiceProvider</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Module1RouterImpl</span> <span class="keyword">implements</span> <span class="title">Module1Router</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">startModule1Activity</span><span class="params">(Context context)</span> </span>{</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(context, Module1Activity.class);</span><br><span class="line"> context.startActivity(intent);</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> Fragment <span class="title">obtainModule1Fragment</span><span class="params">()</span> </span>{</span><br><span class="line"> Fragment fragment = <span class="keyword">new</span> Module1Fragment();</span><br><span class="line"> Bundle bundle = <span class="keyword">new</span> Bundle();</span><br><span class="line"> bundle.putString(<span class="string">"param1"</span>, <span class="string">"value1"</span>);</span><br><span class="line"> bundle.putString(<span class="string">"param2"</span>, <span class="string">"value2"</span>);</span><br><span class="line"> fragment.setArguments(bundle);</span><br><span class="line"> <span class="keyword">return</span> fragment;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>前面提到过,目前社区大多数组件化方案都是使用 <strong>自定义私有协议,利用 URL-Scheme 的方式来实现跨模块页面跳转</strong> 的,即类似 <strong>ARouter</strong> 的那种方案,为什么 <strong>AppJoint</strong> 不采用这种方案呢?</p><p>原因其实很简单,假设项目中没有组件化的需求,我们在同一个模块内进行 Activity 的跳转,肯定不会采用 <strong>URL-Scheme</strong> 方式进行跳转,我们肯定是自己创建 <code>Intent</code> 进行跳转的。其实说到底,使用 <strong>URL-Scheme</strong> 进行跳转是 <strong>不得已而为之</strong>,它只是手段,不是目的,因为在组件化之后,模块之间彼此的 Activity 变得不可见了,所以我们转而使用 <strong>URL-Scheme</strong> 的方式进行跳转。</p><p>现在 <strong>AppJoint</strong> 重新支持了使用代码进行跳转,只需要把跳转的逻辑抽象为接口中的方法暴露给其它模块,其它模块就可以调用这个方法实现跳转逻辑。除此以外,使用接口提供跳转逻辑相比 <strong>URL-Scheme</strong> 方式还有什么优势呢?</p><ol><li><p>类型安全。充分利用 Java 这种静态类型语言的编译器检查功能,通过接口暴露的跳转方法,无论是传参还是返回值,如果类型错误,在编译期间就能发现错误,而使用 <strong>URL-Scheme</strong> 进行跳转,如果发生类型上的错误,只能在运行期间才能发现错误。</p></li><li><p>效率高。即使是使用 <strong>URL-Scheme</strong> 进行跳转,底层仍然是构造 <code>Intent</code> 进行跳转,但是却额外引入了对跳转 URL 进行构造和解析的过程,涉及到额外的序列化和反序列化逻辑,降低了代码的执行效率。而使用接口提供的跳转逻辑,我们直接构造 <code>Intent</code> 进行跳转,不涉及到任何额外的序列化和反序列化操作,和我们日常的 Activity 跳转逻辑执行效率相同。</p></li><li><p>IDE 友好。使用 <strong>URL-Scheme</strong> 进行跳转,IDE 无法提供任何智能提示,只能依靠完善的文档或者开发者自身检查来确保跳转逻辑的正确性,而通过接口提供跳转逻辑可以最大限度发挥 IDE 的智能提示功能,确保我们的跳转逻辑是正确的。</p></li><li><p>易于重构。使用 <strong>URL-Scheme</strong> 进行跳转,如果遇到跳转逻辑需要重构的情况,例如 Activity 名字的修改,参数名称的修改,参数数量的增删,只能依靠开发者对使用到跳转逻辑的地方一个一个修改,而且无法确保全部都修改正确了,因为编译器无法帮我们检查。而通过接口提供的跳转逻辑代码需要重构时,编译器可以自动帮助我们检查,一旦有地方没有改对,直接在编译期报错,而且 IDE 都提供了智能重构的功能,我们可以方便地对接口中定义的方法进行重构。</p></li><li><p>学习成本低。我们可以沿用我们熟悉的开发方式,不需要去学习 <strong>URL-Scheme</strong> 跳转框架的 API。这样还可以保证我们的跳转逻辑不与具体的框架强绑定,我们通过接口隔离了跳转逻辑的真正实现,即使使用 <strong>AppJoint</strong> 进行跳转,我们也可以在随时把跳转逻辑切换到其他方案,包括 <strong>URL-Scheme</strong> 方式。</p></li></ol><p>我个人的实践,目前项目中同一进程内的页面跳转已经全部由 <strong>AppJoint</strong> 的方式实现,目前只有跨进程的页面启动交给了 <strong>URL-Scheme</strong> 这种方式(例如从浏览器唤醒 App 某个页面)。</p><p>最后再提一点,由于跨模块启动 Activity 沿用了跨模块方法调用的开发方式,在业务模块单独编译运行模式下,我们也需要 Mock 这些启动方法。既然我们是在独立调试某个业务模块,我们肯定不是真的希望跳转到那些页面,我们在 Mock 方法里直接输出 Log 或者 Toast 即可。</p><h2 id="现在就开始组件化"><a href="#现在就开始组件化" class="headerlink" title="现在就开始组件化"></a>现在就开始组件化</h2><p>到这里为止,使用 <strong>AppJoint</strong> 进行组件化的介绍就已经结束了。<a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">AppJoint</a> 的 Github 地址为:<a href="https://github.com/PrototypeZ/AppJoint" target="_blank" rel="noopener">https://github.com/PrototypeZ/AppJoint</a> 。核心代码不超过 500 行,您完全可以快速掌握这个工具加速您的组件化开发,只要 Fork 一份代码即可。如果您不想自己引入工程,我们也提供了一个开箱即用的版本,您可以直接通过 <strong>Gradle</strong> 引入。</p><ol><li>在项目根目录的 <code>build.gradle</code> 文件中添加 <strong>AppJoint插件</strong> 依赖:</li></ol><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><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">buildscript {</span><br><span class="line"> ...</span><br><span class="line"> dependencies {</span><br><span class="line"> ...</span><br><span class="line"> classpath <span class="string">'io.github.prototypez:app-joint:{latest_version}'</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="2"><li>在主 App 模块和每个组件化的模块添加 <strong>AppJoint</strong> 依赖:</li></ol><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></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> ...</span><br><span class="line"> implementation <span class="string">"io.github.prototypez:app-joint-core:{latest_version}"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="3"><li>在主 App 模块应用 <strong>AppJoint插件</strong>: </li></ol><figure class="highlight groovy"><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">apply <span class="string">plugin:</span> <span class="string">'com.android.application'</span></span><br><span class="line">apply <span class="string">plugin:</span> <span class="string">'app-joint'</span></span><br></pre></td></tr></table></figure><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>通过本文的介绍,我们其实可以发现 <strong>AppJoint</strong> 是个思想很简单的组件化方案。虽然简单,但是却直接而且够用,尽管没有像其它的组件化方案那样提供了各种各样强大的 API,但是却足以胜任大多数中小型项目,这是我们一以贯之的设计理念。</p><p>如果您感觉这个项目对您有帮助,希望可以点一个 Star ,谢谢 : ) 。文章很长,感谢您耐心读完。由于本人能力有限,文章可能存在纰漏的地方,欢迎各位指正,再次谢谢大家!</p><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>Android 组件化的概念大概从两年前开始有人讨论,到目前为止,技术已经慢慢沉淀下来,越来越多团队开源了自己组件化框架。本人所在团队从去年开始调研组件化框架,在了解社区众多组件化方案之后,决定自研组件化方案。为什么明明已经有很多轮子可以用了,却还是决定要自己造个新轮子呢?</p>
</summary>
<category term="Android 组件化" scheme="http://prototypez.github.io/tags/Android-%E7%BB%84%E4%BB%B6%E5%8C%96/"/>
</entry>
<entry>
<title>RxJava 沉思录(四):总结</title>
<link href="http://prototypez.github.io/2018/09/01/thoughts-in-rxjava-4/"/>
<id>http://prototypez.github.io/2018/09/01/thoughts-in-rxjava-4/</id>
<published>2018-09-01T07:31:00.000Z</published>
<updated>2018-09-08T07:54:03.897Z</updated>
<content type="html"><![CDATA[<p>本文是 “RxJava 沉思录” 系列的最后一篇分享。本系列所有分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><p>我们在本系列开篇中,曾经留了一个问题:RxJava 是否可以让我们的代码更简洁?作为本系列的最后一篇分享,我们将详细地探讨这个问题。承接前面两篇 “时间维度” 和 “空间维度” 的探讨,我们首先从 <strong>RxJava 的维度</strong> 开始说起。</p><a id="more"></a><h2 id="RxJava-的维度"><a href="#RxJava-的维度" class="headerlink" title="RxJava 的维度"></a>RxJava 的维度</h2><p>在前面两篇分享中,我们解读了很多案例,最终得出结论:<strong>RxJava 通过 <code>Observable</code> 这个统一的接口,对其相关的事件,在空间维度和事件维度进行重新组织,来简化我们日常的事件驱动编程</strong>。</p><p>前文中提到:</p><blockquote><p>有了 <code>Observable</code> 以后的 RxJava 才刚刚插上了想象力的翅膀。</p></blockquote><p>RxJava 所有想象力的基石和源泉在于 <code>Observable</code> 这个统一的接口,有了它,配合我们各种各样的操作符,才可以在时间空间维度玩出花样。</p><p>我们回想一下原先我们基于 Callback 的编程范式:</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></pre></td><td class="code"><pre><span class="line">btn.setOnClickListener(v -> {</span><br><span class="line"> <span class="comment">// handle click event</span></span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>在基于 <code>Callback</code> 的编程范式中,我们的 <code>Callback</code> 是 <strong>没有维度</strong> 的。它只能够 <strong>响应孤立的事件</strong>,即来一个事件,我处理一个事件。假设同一个事件前后存在依赖关系,或者不同事件之间存在依赖关系,无论是时间维度还是空间维度,如果我们还是继续用 <code>Callback</code> 的方式处理,我们必然需要新增许多额外的数据结构来保存中间的上下文信息,同时 <code>Callback</code> 本身的逻辑也需要修改,观察者的逻辑会变得不那么纯粹。</p><p>但是 RxJava 给我们的事件驱动型编程带来了新的思路,<strong>RxJava 的 <code>Observable</code> 一下子把我们的维度拓展到了时间和空间两个维度</strong>。如果事件与事件间存在依赖关系,原先我们需要新增的数据结构以及在 Callback 内写的额外的控制逻辑的代码,现在都可以不用写,我们只需要利用 <code>Observable</code> 的操作符对事件在时间和空间维度进行重新组织,就可以实现一样的效果,而观察者的逻辑几乎不需要修改。</p><p>所以如果把 RxJava 的编程思想和传统的面向 Callback 的编程思想进行对比,用一个词形容的话,那就是 <strong>降维打击</strong>。</p><p>这是我认为目前大多数与 RxJava 有关的技术分享没有提到的一个非常重要的点,并且我认为这才是 RxJava 最精髓最核心的思想。RxJava 对我们日常编程最重要的贡献,就是提升了我们原先对于事件驱动型编程的思考的维度,给人一种大梦初醒的感觉,和这点比起来,所谓的 “链式写法” 这种语法糖什么的,根本不值一提。</p><h2 id="生产者消费者模式中-RxJava-扮演的角色"><a href="#生产者消费者模式中-RxJava-扮演的角色" class="headerlink" title="生产者消费者模式中 RxJava 扮演的角色"></a>生产者消费者模式中 RxJava 扮演的角色</h2><p>无论是同步还是异步,我们日常的事件驱动型编程可以被看成是一种 “<strong>生产者——消费者</strong>” 模型:<br><img src="http://prototypez.github.io/images/rxjava-graph-1.png" alt="Callback"></p><p>在异步的情况下,我们的代码可以被分为两大块,一块生产事件,一块消费事件,两者通过 Callback 联系起来。而 Callback 是轻量级的,大多数和 Callback 相关的逻辑就仅仅是设置回调和取消设置的回调而已。</p><p>如果我们的项目中引入了 RxJava ,我们可以发现,“<strong>生产者——消费者</strong>” 这个模型中,中间多了一层 RxJava 相关的逻辑层:<br><img src="http://prototypez.github.io/images/rxjava-graph-2.png" alt="RxJava"></p><p>而这一层的作用,我们在之前的讨论中已经明确,是用来对生产者产生的事件进行重新组织的。这个架构之下,生产者这一层的变化不会很大,直接受影响的是消费者这一层,由于 RxJava 这一层对事件进行了“预处理”,消费者这一层代码会比之前轻很多。同时由于 RxJava 取代了原先的 Callback 这一层,RxJava 这一层的代码是会比原先 Callback 这一层更厚。</p><p>这么做还会有什么其他的好处呢?首先最直接的好处便是代码会更易于测试。原先生产者和消费者之间是耦合的,由于现在引入了 RxJava,生产者和消费者之间没有直接的耦合关系,测试的时候可以很方便的对生产者和消费者分开进行测试。比如原先网络请求相关逻辑,测试就不是很方便,但是如果我们使用 RxJava 进行解耦以后,观察者仅仅只是耦合 <code>Observable</code> 这个接口而已,我们可以自己手动创建用于测试的 <code>Observable</code>,这些 <code>Observable</code> 负责发射 Mock 的数据,这样就可以很方便的对观察者的代码进行测试,而不需要真正的去发起网络请求。</p><h2 id="取消订阅与-Scheduler"><a href="#取消订阅与-Scheduler" class="headerlink" title="取消订阅与 Scheduler"></a>取消订阅与 Scheduler</h2><p>取消订阅这个功能也是我们在观察者模式中经常用到的一个功能点,尤其是在 Android 开发领域,由于 Activity 生命周期的关系,我们经常需要将网络请求与 Activity 生命周期绑定,即在 Activity 销毁的时候取消所有未完成的网络请求。</p><p>常规面向 Callback 的编程方式我们无法在观察者这一层完成取消订阅这一逻辑,我们常常需要找到事件生产者这一层才能完成取消订阅。例如我们需要取消点击事件的订阅时,我们不得不找到点击事件产生的源头,来取消订阅: </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">btn.setOnClickListener(<span class="keyword">null</span>);</span><br></pre></td></tr></table></figure><p>然而在 RxJava 的世界里,取消订阅这个逻辑终于下放到观察者这一层了。事件的生产者需要在提供 <code>Observable</code> 的同时,实现当它的观察者取消订阅时,它应该实现的逻辑(例如释放资源);事件的观察者当订阅一个 <code>Observable</code> 时,它同时会得到一个 <code>Disposable</code> ,观察者希望取消订阅事件的时候,只需要通过这个接口通知事件生产者即可,完全不需要了解事件是如何产生的、事件的源头在哪里。</p><p>至此,生产者和消费者在 RxJava 的世界里已经完成了彻底的解耦。除此以外,RxJava 还提供了好用的线程池,在 <strong>生产者——消费者</strong> 这个模型里,我们常常会要求两者工作在不同的线程中,切换线程是刚需,RxJava 完全考虑到了这一点,并且把切换线程的功能封装成了 <code>subscribeOn</code> 和 <code>observerOn</code> 两个操作符,我们可以在事件流处理的任何时机随意切换线程,鉴于这一块已经有很多资料了,这里不再详细展开。</p><h2 id="面向-Observable-的-AOP:compose-操作符"><a href="#面向-Observable-的-AOP:compose-操作符" class="headerlink" title="面向 Observable 的 AOP:compose 操作符"></a>面向 Observable 的 AOP:compose 操作符</h2><p>这一块不属于 RxJava 的核心 Feature,但是如果掌握好这块,可以让我们使用 RxJava 编程效率大大提升。</p><p>我们举一个实际的例子,Activity 内发起的网络请求都需要绑定生命周期,即我们需要在 Activity 销毁的时候取消订阅所有未完成的网络请求。假设我目前已经可以获得一个 <code>Observable<ActivityEvent></code>, 这是一个能接收到 Activity 生命周期的 <code>Observable</code>(获取方法可以借鉴三方框架 <a href="https://github.com/trello/RxLifecycle.git" target="_blank" rel="noopener">RxLifecycle</a>,或者自己内建一个不可见 Fragment,用来接收生命周期的回调)。</p><p>那么用来保证每一个网络请求都能绑定 Activity 生命周期的代码应如下所示:</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">NetworkApi</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Call<List<Photo>> getAllPhotos();</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">class</span> <span class="title">MainActivity</span> <span class="keyword">extends</span> <span class="title">Activity</span> </span>{</span><br><span class="line"></span><br><span class="line"> Observable<ActivityEvent> lifecycle = ...</span><br><span class="line"> NetworkApi networkApi = ...</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">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// 发起请求同时绑定生命周期</span></span><br><span class="line"> networkApi.getAllPhotos()</span><br><span class="line"> .compose(bindToLifecycle())</span><br><span class="line"> .subscribe(result -> {</span><br><span class="line"> <span class="comment">// handle results</span></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> <T> <span class="function">ObservableTransformer<T, T> <span class="title">bindToLifecycle</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> upstream.takeUntil(</span><br><span class="line"> lifecycle.filter(ActivityEvent.DESTROY::equals)</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>如果您之前没有接触过 <code>ObservableTransformer</code>, 这里做一个简单介绍,它通常和 <code>compose</code> 操作符一起使用,用来把一个 <code>Observable</code> 进行加工、修饰,甚至替换为另一个 <code>Observable</code>。</p></blockquote><p>在这里我们封装了一个 <code>bindToLifecycle</code> 方法,它的返回类型是 <code>ObservableTransformer</code>,在 <code>ObservableTransformer</code> 内部,我们修饰了原 <code>Observable</code>, 使其可以在接收到 Activity 的 DESTROY 事件的时候自动取消订阅,这个逻辑是由 <code>takeUntil</code> 这个操作符完成的。其实我们可以把这个 <code>bindToLifecycle</code> 方法抽取出来,放到公共的工具类,这样任何的 Activity 内部发起的网络请求时,都只需要加一行 <code>.compose(bindToLifecycle())</code> 就可以保证绑定生命周期了,从此再也不必担心由于网络请求引起的内存泄漏和崩溃了。</p><p>事实上我们还可以有更多玩法, 上面 <code>ObservableTransformer</code> 内部的 <code>upstream</code> 对象,就是一个 <code>Observable</code>,也就是说可以调用它的 <code>doOnSubscribe</code> 和 <code>doOnTerminate</code> 方法,我们可以在这两个方法里实现 Loading 动画的显隐:</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="keyword">private</span> <T> <span class="function">ObservableTransformer<T, T> <span class="title">applyLoading</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> upstream</span><br><span class="line"> .doOnSubscribe(() -> {</span><br><span class="line"> loading.show();</span><br><span class="line"> })</span><br><span class="line"> .doOnTerminae(() -> {</span><br><span class="line"> loading.dismiss();</span><br><span class="line"> }); </span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,我们的网络请求只要调用两个 <code>compose</code> 操作符,就可以完成生命周期的绑定以及与之对应的 Loading 动画的显隐了:</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></pre></td><td class="code"><pre><span class="line">networkApi.getAllPhotos()</span><br><span class="line"> .compose(bindToLifecycle())</span><br><span class="line"> .compose(applyLoading())</span><br><span class="line"> .subscribe(result -> {</span><br><span class="line"> <span class="comment">// handle results</span></span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>操作符 <code>compose</code> 是 RxJava 给我们提供的可以面向 <code>Observable</code> 进行 AOP 的接口,善加利用就可以帮我们节省大量的时间和精力。</p><h2 id="RxJava-真的让你的代码更简洁?"><a href="#RxJava-真的让你的代码更简洁?" class="headerlink" title="RxJava 真的让你的代码更简洁?"></a>RxJava 真的让你的代码更简洁?</h2><p>在前文中,我们还留了一个问题尚未解答:RxJava 真的更简洁吗?本文中列举了很多实际的例子,我们也看到了,从代码量看,有时候使用 RxJava 的版本比 Callback 的版本更少,有时候两者差不多,有时候 Callback 版本的代码反而更少。所以我们可能无法从代码量上对两者做出公正的考量,所以我们需要从其他方面,例如代码的阅读难度、可维护性上去评判了。</p><p>首先我想要明确一点,RxJava 是一个 “<strong>夹带了私货</strong>” 的框架,它本身最重要的贡献是提升了我们思考事件驱动型编程的维度,但是它与此同时又逼迫我们去接受了函数式编程。函数式编程在处理集合、列表这些数据结构时相比较指令式编程具有先天的优势,我理解框架的设计者,由于框架本身提升了我们对事件思考的维度,那么无论是时间维度还是空间维度,一连串发射出来的事件其实就可以被看成许许多多事件的集合,既然是集合,那肯定是使用函数式的风格去处理更加优雅。</p><p><img src="http://prototypez.github.io/images/rxjava-graph-3.png" alt=""></p><p>原先的时候,我们接触的函数式编程只是用于处理静态的数据,当我们接触了 RxJava 之后,发现动态的异步事件组成的集合居然也可以使用函数式编程的方式去处理,我不由地佩服框架设计者的脑洞大开。事实上,RxJava 很多操作符都是直接照搬函数式编程中处理集合的函数,例如:<code>map</code>, <code>filter</code>, <code>flatMap</code>, <code>reduce</code> 等等。</p><p>但是,函数式编程是一把双刃剑,它也会给你带来不利的因素,一方面,这意味着你的团队都需要了解函数式编程的思想,另一方面,函数式的编程风格,意味着代码会比原先更加抽象。</p><p>比如在前面的分享中 “<em>实现一个具有多种类型的 RecyclerView</em>” 这个例子中, <code>combineLatest</code> 这个操作符,完成了原先 <code>onOk()</code> 方法、<code>resultTypes</code>、<code>responseList</code> 一起配合才完成的任务。虽然原先的版本代码不够内聚,不如 RxJava 版本的简练,但是如果从可阅读性和可维护性上来看,我认为原先的版本更好,因为我看到这几个方法和字段,可以推测出这段代码的意图是什么,可是如果是 <code>combineLatest</code> 这个操作符,也许我写的那个时候我知道我是什么意图,一旦过一段时间回来看,我对着这个这个 <code>combineLatest</code> 操作符可能就一脸懵逼了,我必须从这个事件流最开始的地方从上往下捋一遍,结合实际的业务逻辑,我才能回想起为什么当时要用 <code>combineLatest</code> 这个操作符了。</p><p>再举一个例子,在 “<em>社交软件上消息的点赞与取消点赞</em>” 这个例子中,如果我不是对这种“把事件流中相邻事件进行比较”的编码方式了如指掌的话,一旦隔一段时间,我再次面对这几个 <code>debounce</code> 、<code>zipWith</code>、<code>flatMap</code> 操作符时,我可能会怀疑自己写的代码。自己写的代码都如此,更何况大多数情况下我们需要面对别人写的代码。</p><p>这就是为什么 RxJava 写出的代码会更加抽象,<strong>因为 RxJava 的操作符是我们平时处理业务逻辑时常用方法的高度抽象</strong>。 <code>combineLatest</code> 是对我们自己写的 <code>onOk</code> 等方法的抽象,<code>zipWith</code> 帮我们省略了本来要写的中间变量,<code>debounce</code> 操作符替代了我们本来要写的计时器逻辑。从功能上来讲两者其实是等价的,只不过 RxJava 给我们提供了高度抽象凝练,更加具有普适性的写法。</p><p>在本文前半部分,我们说到过,有的人认为 RxJava 是简洁的,而有的人的看法则完全相反,这件事的本质在于大家对 <strong>简洁</strong> 的期望不同,大多数人认为的简洁指得是代码简单好理解,而高度抽象的代码是不满足这一点的,所以很多人最后发现理解抽象的 RxJava 代码需要花更多的时间,反而不 “简洁” 。认为 RxJava 简洁的人所认为的 <strong>简洁</strong> 更像是那种类似数学概念上的那种 <strong>简洁</strong>,这是因为函数式编程的抽象风格与数学更接近。我们举个例子,大家都知道牛顿第二定律,可是你知道牛顿在《自然哲学的数学原理》上发表牛顿二定律的时候的原始公式表示是什么样的吗:</p><p><img src="http://prototypez.github.io/images/newton's-second-law.jpg" alt="Newton's second law"></p><p>公式中的 <strong>p</strong> 表示动量,这是牛顿所认为的”简洁”,而我们大多数人认为简单好记的版本是 “<strong>物体的加速度等于施加在物体上的力除以物体的质量</strong>”。</p><p>这就是为什么,我在前面提前下了那个结论:<strong>对于大多数人,RxJava 不等于简洁</strong>,有时候甚至是更难以理解的代码以及更低的项目可维护性。</p><p>而目前大多数我看到的有关 RxJava 的技术文章举例说明的所谓 “逻辑简洁” 或者是 “随着程序逻辑的复杂性提高,依然能够保持简洁” 的例子大多数都是不恰当的。一方面他们仅仅停留在 Callback 的维度,举那种依次执行的异步任务的例子,完全没有点到 RxJava 对处理问题的维度的提升这一点;二是举的那些例子实在难以令人信服,至少我并没有觉得那些例子用了 RxJava 相比 Callback 有多么大的提升。</p><h2 id="RxJava-是否适合你的项目"><a href="#RxJava-是否适合你的项目" class="headerlink" title="RxJava 是否适合你的项目"></a>RxJava 是否适合你的项目</h2><p>综上所述,我们可以得出这样的结论,<strong>RxJava 是一个思想优秀的框架,而且是那种在工程领域少见的带有学院派气息和理想主义色彩的框架,他是一种新型的事件驱动型编程范式。 RxJava 最重要的贡献,就是提升了我们原先对于事件驱动型编程的思考的维度,允许我们可以从时间和空间两个维度去重新组织事件。</strong></p><p>此外,RxJava 好在哪,真的和“观察者模式”、“链式编程”、“线程池”、“解决 Callback Hell”等等关系没那么大,这些特性相比上面总结的而言,都是微不足道的。</p><p>我是不会用“简洁”、“逻辑简洁”、“清晰”、“优雅” 那样空洞的字眼去描述 RxJava 这个框架的,这确实是一个学习曲线陡峭的框架,而且如果团队成员整体对函数式编程认识不够深刻的话,项目的后期维护也是充满风险的。</p><p>当然我希望你也不要因此被我吓到,我个人是推崇 RxJava 的,在我本人参与的项目中已经大规模铺开使用了 RxJava。本文前面提到过:</p><blockquote><p>RxJava 是一种新的 <strong>事件驱动型</strong> 编程范式,它以异步为切入点,试图一统 <strong>同步</strong> 和 <strong>异步</strong> 的世界。</p></blockquote><p>在我参与的项目中,我已经渐渐能感受到这种 <strong>“天下大同”</strong> 的感觉了。这也是为什么我能听到很多人都会说 “一旦用了 RxJava 就很难再放弃了”。</p><p>也许这时候你会问我,到底推不推荐大家使用 RxJava ?我认为是这样,如果你认为在你的项目里,Callback 模式已经不能满足你的日常需要,事件之间存在复杂的依赖关系,你需要从更高的维度空间去重新思考你的问题,或者说你需要经常在时间或者空间维度上去重新组织你的事件,那么恭喜你, RxJava 正是为你打造的;如果你认为在你的项目里,目前使用 Callback 模式已经很好满足了你的日常开发需要,简单的业务逻辑也根本玩不出什么新花样,那么 RxJava 就是不适合你的。</p><p>(完)</p><p>本文属于 “RxJava 沉思录” 系列,欢迎阅读本系列的其他分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>本文是 “RxJava 沉思录” 系列的最后一篇分享。本系列所有分享:</p>
<ul>
<li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li>
<li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li>
<li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li>
<li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li>
</ul>
<p>我们在本系列开篇中,曾经留了一个问题:RxJava 是否可以让我们的代码更简洁?作为本系列的最后一篇分享,我们将详细地探讨这个问题。承接前面两篇 “时间维度” 和 “空间维度” 的探讨,我们首先从 <strong>RxJava 的维度</strong> 开始说起。</p>
</summary>
<category term="RxJava" scheme="http://prototypez.github.io/tags/RxJava/"/>
</entry>
<entry>
<title>RxJava 沉思录(三):时间维度</title>
<link href="http://prototypez.github.io/2018/08/31/thoughts-in-rxjava-3/"/>
<id>http://prototypez.github.io/2018/08/31/thoughts-in-rxjava-3/</id>
<published>2018-08-31T07:31:00.000Z</published>
<updated>2018-09-08T07:54:03.894Z</updated>
<content type="html"><![CDATA[<p>本文是 “RxJava 沉思录” 系列的第三篇分享。本系列所有分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><p>在上一篇分享中,我们应该已经对 <strong>Observable 在空间维度上重新组织事件的能力</strong> 印象深刻了,那么自然而然的,我们容易联想到时间维度,事实上就我个人而言,我认为 <strong>Observable 在时间维度上的重新组织事件的能力</strong> 相比较其空间维度的能力更为突出。与上一篇类似,本文接下来将通过列举真实的例子来阐述这一论点。</p><a id="more"></a><h2 id="点击事件防抖动"><a href="#点击事件防抖动" class="headerlink" title="点击事件防抖动"></a>点击事件防抖动</h2><p>这是一个比较常见的情景,用户在手机比较卡顿的时候,点击某个按钮,正常应该启动一个页面,但是手机比较卡,没有立即启动,用户就点了好几下,结果等手机回过神来的时候,就会启动好几个一样的页面。</p><p>这个需求用 Callback 的方式比较难处理,但是相信用过 RxJava 的开发者都知道怎么处理:</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></pre></td><td class="code"><pre><span class="line">RxView.clicks(btn)</span><br><span class="line"> .debounce(<span class="number">500</span>, TimeUnit.MILLISECONDS)</span><br><span class="line"> .observerOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(o -> {</span><br><span class="line"> <span class="comment">// handle clicks</span></span><br><span class="line"> })</span><br></pre></td></tr></table></figure><blockquote><p><code>debounce</code> 操作符产生一个新的 <code>Observable</code>, 这个 <code>Observable</code> 只发射原 <code>Observable</code> 中时间间隔小于指定阈值的最大子序列的最后一个元素。 <a href="http://reactivex.io/documentation/operators/debounce.html" target="_blank" rel="noopener">参考资料:Debounce</a></p></blockquote><p>虽然这个例子比较简单,但是它很好的表达了 <strong>Observable 可以在时间维度上对其发射的事件进行重新组织</strong> , 从而做到之前 Callback 形式不容易做到的事情。</p><h2 id="社交软件上消息的点赞与取消点赞"><a href="#社交软件上消息的点赞与取消点赞" class="headerlink" title="社交软件上消息的点赞与取消点赞"></a>社交软件上消息的点赞与取消点赞</h2><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="keyword">boolean</span> like;</span><br><span class="line"></span><br><span class="line">likeBtn.setOnClickListener(v -> {</span><br><span class="line"> <span class="keyword">if</span> (like) {</span><br><span class="line"> <span class="comment">// 取消点赞</span></span><br><span class="line"> sendCancelLikeRequest(postId);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 点赞</span></span><br><span class="line"> sendLikeRequest(postId);</span><br><span class="line"> }</span><br><span class="line"> like = !like;</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p><em>以下图片素材资源来自 <a href="https://dribbble.com/shots/2547034-Twitter-like-button-mashup" target="_blank" rel="noopener">Dribbble</a></em><br><img src="https://cdn.dribbble.com/users/75982/screenshots/2547034/twitter_like_button.gif" alt="Dribbble"></p><p>如果你碰巧实现了一个非常酷炫的点赞动画,用户可能会玩得不亦乐乎,这个时候可能会对后端服务器造成一定的压力,因为每次点赞与取消点赞都会发起网络请求,假如很多用户同时在玩这个点赞动画,服务器可能会不堪重负。</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><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">boolean</span> like;</span><br><span class="line">PublishSubject<Boolean> likeAction = PublishSubject.create();</span><br><span class="line"></span><br><span class="line">likeBtn.setOnClickListener(v -> {</span><br><span class="line"> likeAction.onNext(like);</span><br><span class="line"> like = !like;</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line">likeAction.debounce(<span class="number">1000</span>, TimeUnit.MILLISECONDS)</span><br><span class="line"> .observerOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(like -> {</span><br><span class="line"> <span class="keyword">if</span> (like) {</span><br><span class="line"> sendCancelLikeRequest(postId);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> sendLikeRequest(postId);</span><br><span class="line"> }</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>写到这个份上,其实已经可以解决服务器压力过大的问题了,但是还是有优化空间,假设当前是已赞状态,用户快速点击 2 下,按照上面的代码,还是会发送一次点赞的请求,由于当前是已赞状态,再发送一次点赞请求是没有意义的,所以我们优化的目标就是将这一类事件过滤掉:</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">Observable<Boolean> debounced = likeAction.debounce(<span class="number">1000</span>, TimeUnit.MILLISECONDS);</span><br><span class="line">debounced.zipWith(</span><br><span class="line"> debounced.startWith(like),</span><br><span class="line"> (last, current) -> last == current ? <span class="keyword">new</span> Pair<>(<span class="keyword">false</span>, <span class="keyword">false</span>) : <span class="keyword">new</span> Pair<>(<span class="keyword">true</span>, current)</span><br><span class="line">)</span><br><span class="line"> .flatMap(pair -> pair.first ? Observable.just(pair.second) : Observable.empty())</span><br><span class="line"> .subscribe(like -> {</span><br><span class="line"> <span class="keyword">if</span> (like) {</span><br><span class="line"> sendCancelLikeRequest(postId);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> sendLikeRequest(postId);</span><br><span class="line"> }</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p><code>zipWith</code> 操作符可以把两个 <code>Observable</code> 发射的相同序号(同为第 <strong>x</strong> 个)的元素,进行运算转换,得到的新元素作为新的 <code>Observable</code> 对应序号所发射的元素。<a href="http://reactivex.io/documentation/operators/zip.html" target="_blank" rel="noopener">参考资料:ZipWith</a></p></blockquote><p>上面的代码,我们可以看到,首先我们对事件流做了一次 <code>debounce</code> 操作,得到 <code>debounced</code> 事件流,然后我们把 <code>debounced</code> 事件流和 <code>debounced.startWith(like)</code> 事件流做了一次 <code>zipWith</code> 操作。相当于新的这个 <code>Observable</code> 中发射的第 <strong>n</strong> 个元素(<strong>n >= 2</strong>)是由 <code>debounced</code> 事件流中的第 <strong>n</strong> 和 第 <strong>n-1</strong> 个元素运算得到的(新的这个 <code>Observable</code> 中发射的第 <strong>1</strong> 个元素是由 <code>debounced</code> 事件流中的第 <strong>1</strong> 个元素和原始点赞状态 <code>like</code> 运算而来)。</p><p>运算的结果是得到一个 <code>Pair</code> 对象,它是一个双布尔类型二元组,二元组第一个元素为 <strong>true</strong> 代表这个事件不该被忽略,应该被观察者观察到;若为 <strong>false</strong> 则应该被忽略。二元组的第二个元素仅在第一个元素为 <strong>true</strong> 的情况下才有意义,<strong>true</strong> 表示应该发起一次点赞操作,而 <strong>false</strong> 表示应该发起一次取消点赞操作。上面提到的“<strong>运算</strong>”具体运算的规则是,比较两个元素,若相等,则把二元组的第一个元素置为 <strong>false</strong>,若不相等,则把二元组的第一个元素置为 <strong>true</strong>, 同时把二元组的第二个元素置为 <code>debounced</code> 事件流发射的那个元素。</p><p>随后的 <code>flatMap</code> 操作符完成了两个逻辑,一是过滤掉二元组第一个元素为 <strong>false</strong> 的二元组,二是把二元组转化回最初的 <code>Boolean</code> 事件流。其实这个逻辑也可由 <code>filter</code> 和 <code>map</code> 两个操作符配合完成,这里为了简单用了一个操作符。</p><p>虽然上面用了不少篇幅解释了每个操作符的意义,但其实核心思想是简单的,就是在原先 <code>debounce</code> 操作符的基础上,把得到的事件流里每个元素和它的上一个元素做比较,如果这个元素和上个元素相同(例如在已赞状态下再次发起点赞操作), 就把这个元素过滤掉,这样最终的观察者里只会在在真正需要改变点赞状态的时候才会发起网络请求了。</p><p>我们考虑用 Callback 实现相同逻辑,虽然比较本次操作与上次操作这样的逻辑通过 Callback 也可以做到,但是 <code>debounce</code> 这个操作符完成的任务,如果要使用 Callback 来实现就非常复杂了,我们需要定义一个计时器,还要负责启动与关闭这个计时器,我们的 Callback 内部会掺杂进很多和观察者本身无关的逻辑,相比 RxJava 版本的纯粹相去甚远。</p><h2 id="检测双击事件"><a href="#检测双击事件" class="headerlink" title="检测双击事件"></a>检测双击事件</h2><p>首先,我们需要定义双击事件,不妨先规定两次点击小于 500 毫秒则为一次双击事件。我们先使用 Callback 的方式实现:</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">long</span> lastClickTimeStamp;</span><br><span class="line"></span><br><span class="line">btn.setOnClickListener(v -> {</span><br><span class="line"> <span class="keyword">if</span> (System.currentTimeMillis() - lastClickTimeStamp < <span class="number">500</span>) {</span><br><span class="line"> <span class="comment">// handle double click</span></span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>上面的代码很容易理解,我们引入一个中间变量 <code>lastClickTimeStamp</code>, 通过比较点击事件发生时和上一次点击事件的时间差是否小于 500 毫秒,来确认是否发生了一次双击事件。那么如何通过 RxJava 来实现呢?就和上一个例子一样,我们可以在时间维度对 <code>Observable</code> 发射的事件进行重新组织,只过滤出与上次点击事件间隔小于 500 毫秒的点击事件,代码如下:</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">Observable<Long> clicks = RxView.clicks(btn)</span><br><span class="line"> .map(o -> System.currentTimeMillis())</span><br><span class="line"> .share();</span><br><span class="line"> </span><br><span class="line">clicks.zipWith(clicks.skip(<span class="number">1</span>), (t1, t2) -> t2 - t1)</span><br><span class="line"> .filter(interval -> interval < <span class="number">500</span>)</span><br><span class="line"> .subscribe(o -> {</span><br><span class="line"> <span class="comment">// handle double click</span></span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>我们再一次用到了 <code>zipWith</code> 操作符来对事件流自身相邻的两个元素做比较,另外这次代码中使用了 <code>share</code> 操作符,用来保证点击事件的 <code>Observable</code> 被转为 <strong>Hot Observable</strong>。</p><blockquote><p>在<code>RxJava</code>中,<code>Observable</code>可以被分为<code>Hot Observable</code>与<code>Cold Observable</code>,引用《Learning Reactive Programming with Java 8》中一个形象的比喻(翻译后的意思):我们可以这样认为,<code>Cold Observable</code>在每次被订阅的时候为每一个<code>Subscriber</code>单独发送可供使用的所有元素,而<code>Hot Observable</code>始终处于运行状态当中,在它运行的过程中,向它的订阅者发射元素(发送广播、事件),我们可以把<code>Hot Observable</code>比喻成一个电台,听众从某个时刻收听这个电台开始就可以听到此时播放的节目以及之后的节目,但是无法听到电台此前播放的节目,而<code>Cold Observable</code>就像音乐 CD ,人们购买 CD 的时间可能前后有差距,但是收听 CD 时都是从第一个曲目开始播放的。也就是说同一张 CD ,每个人收听到的内容都是一样的, 无论收听时间早或晚。</p></blockquote><p>仅仅是上面这个双击检测的例子,还不能体现 RxJava 的优越性,我们把需求改得更复杂一点:如果用户在“短时间”内连续多次点击,只能算一次双击操作。这个需求是合理的,因为如果按照上面 Callback 的写法,虽然可以检测出双击操作,但是如果用户快速点击 <strong>n</strong> 次(间隔均小于 500 毫秒,<strong>n >= 2</strong>), 就会触发 <strong>n - 1</strong> 次双击事件,假设双击处理函数里需要发起网络请求,会对服务器造成压力。要实现这个需求其实也简单,和上一个例子类似,我们用到了 <code>debounce</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">Observable<Object> clicks = RxView.clicks(btn).share()</span><br><span class="line"></span><br><span class="line">clicks.buffer(clicks.debounce(<span class="number">500</span>, TimeUnit.MILLISECONDS))</span><br><span class="line"> .filter(events -> events.size >= <span class="number">2</span>)</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(o -> {</span><br><span class="line"> <span class="comment">// handle double click</span></span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p><code>buffer</code> 操作符接受一个 <code>Observable</code> 为参数,这个 <code>Observable</code> 所发射的元素是什么不重要,重要的是这些元素发射的时间点,这些时间点会在时间维度上把原来那个 <code>Observable</code> 所发射的元素划分为一系列元素的组,<code>buffer</code> 操作符返回的新的 <code>Observable</code> 发射的元素即为那些“组”。<br><a href="http://reactivex.io/documentation/operators/buffer.html" target="_blank" rel="noopener">参考资料: Buffer</a></p></blockquote><p>上面的代码通过 <code>buffer</code> 和 <code>debounce</code> 两个操作符很巧妙的把点击事件流转化为了我们关心的 “短时间内点击次数超过 2 次” 的事件流,而且新的事件流中任意两个相邻事件间隔必定大于 500 毫秒。</p><p>在这个例子中,如果我们想要使用 Callback 去实现相似逻辑,代码量肯定是巨大的,而且鲁棒性也无法保证。</p><h2 id="搜索提示"><a href="#搜索提示" class="headerlink" title="搜索提示"></a>搜索提示</h2><p>我们平时使用的搜索框中,常常是当用户输入一部分内容后,下方就会显示对应的搜索提示,以支付宝为例,当在搜索框输入“蚂蚁”关键词后,下方自动刷新和关键词相关的结果:</p><p><img src="http://prototypez.github.io/images/alipay-demo.png" alt=""></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></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">Api</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"path/to/api"</span>)</span><br><span class="line"> Observable<List<String>> queryKeyword(String keyword);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查询接口现在已经确定下来,我们考虑一下在实现这个需求的过程中需要考虑哪些因素:</p><ol><li>防止用户输入过快,触发过多网络请求,需要对输入事件做一下防抖动。</li><li>用户在输入关键词过程中可能触发多次请求,那么,如果后一次请求的结果先返回,前一次请求的结果后返回,这种情况应该保证界面展示的是后一次请求的结果。</li><li>用户在输入关键词过程中可能触发多次请求,那么,如果后一次请求的结果返回时,前一次请求的结果尚未返回的情况下,就应该取消前一次请求。</li></ol><p>综合考虑上面的因素以后,我们使用 RxJava 实现的对应的代码如下:</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">RxTextView.textChanges(input)</span><br><span class="line"> .debounce(<span class="number">300</span>, TimeUnit.MILLISECONDS)</span><br><span class="line"> .switchMap(text -> api.queryKeyword(text.toString()))</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(results -> {</span><br><span class="line"> <span class="comment">// handle results</span></span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p><code>switchMap</code> 这个操作符与 <code>flatMap</code> 操作符类似,但是区别是如果原 <code>Observable</code> 中的两个元素,通过 <code>switchMap</code> 操作符都转为 <code>Observable</code> 之后,如果后一个元素对应的 <code>Observable</code> 发射元素时,前一个元素对应的 <code>Observable</code> 尚未发射完所有元素,那么前一个元素对应的 <code>Observable</code> 会被自动取消订阅,尚未发射完的元素也不会体现在 <code>switchMap</code> 操作符调用后产生的新的 <code>Observable</code> 发射的元素中。<br><a href="http://reactivex.io/documentation/operators/flatmap.html" target="_blank" rel="noopener">参考资料:SwitchMap</a></p></blockquote><p>我们分析上面的代码,可以发现: <code>debounce</code> 操作符解决了问题 <strong>1</strong>,<code>switchMap</code> 操作符解决了问题 <strong>2</strong>、<strong>3</strong>。这个例子可以很好的说明,<strong>RxJava 的 <code>Observable</code> 可以通过一系列操作符从时间的维度上重新组织事件,从而简化观察者的逻辑</strong>。这个例子如果使用 Callback 来实现,肯定是十分复杂的,需要设置计时器以及一堆中间变量,观察者中也会掺杂进很多额外的逻辑,用来保证事件与事件的依赖关系。</p><p>(未完待续)</p><p>本文属于 “RxJava 沉思录” 系列,欢迎阅读本系列的其他分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>本文是 “RxJava 沉思录” 系列的第三篇分享。本系列所有分享:</p>
<ul>
<li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li>
<li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li>
<li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li>
<li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li>
</ul>
<p>在上一篇分享中,我们应该已经对 <strong>Observable 在空间维度上重新组织事件的能力</strong> 印象深刻了,那么自然而然的,我们容易联想到时间维度,事实上就我个人而言,我认为 <strong>Observable 在时间维度上的重新组织事件的能力</strong> 相比较其空间维度的能力更为突出。与上一篇类似,本文接下来将通过列举真实的例子来阐述这一论点。</p>
</summary>
<category term="RxJava" scheme="http://prototypez.github.io/tags/RxJava/"/>
</entry>
<entry>
<title>RxJava 沉思录(二):空间维度</title>
<link href="http://prototypez.github.io/2018/08/30/thoughts-in-rxjava-2/"/>
<id>http://prototypez.github.io/2018/08/30/thoughts-in-rxjava-2/</id>
<published>2018-08-30T07:31:00.000Z</published>
<updated>2018-09-08T07:54:03.889Z</updated>
<content type="html"><![CDATA[<p>本文是 “RxJava 沉思录” 系列的第二篇分享。本系列所有分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><p>在上一篇分享中,我们澄清了目前有关 RxJava 的几个最流行的误解,它们分别是:“<strong>链式编程是 RxJava 的厉害之处</strong>”,“<strong>RxJava 等于异步加简洁</strong>”,“<strong>RxJava 是用来解决 Callback Hell 的</strong>”。在上一篇的最后,我们了解了 RxJava 其实给我们最基础的功能就是帮我们统一了所有异步回调的接口。但是 RxJava 并不止于此,本文我们将首先介绍 <strong>Observable 在空间维度上重新组织事件的能力</strong>。</p><a id="more"></a><h2 id="从一个简单的例子说起"><a href="#从一个简单的例子说起" class="headerlink" title="从一个简单的例子说起"></a>从一个简单的例子说起</h2><p>情景:有一个相册应用,从网络获取当前用户的照片列表,展示在 RecyclerView 里:</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">NetworkApi</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Call<List<Photo>> getAllPhotos();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面是使用 Retrofit 定义的从网络获取照片的 API 的接口。大家都知道,如果我们使用 Retrofit 的 <strong>RxJavaCallAdapter</strong> 就可以把接口中的返回类型从 <code>Call<List<Photo>></code> 转为 <code>Observable<List<Photo>></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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">NetworkApi</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<Photo>> getAllPhotos();</span><br><span class="line">}</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></pre></td><td class="code"><pre><span class="line">NetworkApi networkApi = ...</span><br><span class="line">networkApi.getAllPhotos()</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(photos -> {</span><br><span class="line"> adapter.setData(photos);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>现在新加一个需求,请求当前用户照片列表这个网络请求,需要加入缓存功能(缓存的是网络响应中的图片的URL,图片的 Bitmap 缓存交给专门的图片加载框架,例如 <strong>Glide</strong>),也就是说,当用户希望展示图片列表时,先去缓存读取用户的照片列表进行加载(如果缓存里有这个接口的上次访问的数据),同时发起网络请求,待网络请求返回之后,更新缓存,同时使用使用最新的返回数据刷新照片列表。如果我们选择使用 <strong>JakeWharton</strong> 的 <a href="https://github.com/JakeWharton/DiskLruCache" target="_blank" rel="noopener">DiskLruCache</a> 作为我们的缓存介质,那么上面的代码将变为:</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">DiskLruCache cache = ... </span><br><span class="line">DiskLruCache.Snapshot snapshot = cache.get(<span class="string">"getAllPhotos"</span>);</span><br><span class="line"><span class="keyword">if</span> (snapshot != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 读取缓存数据并反序列化</span></span><br><span class="line"> List<Photo> cachedPhotos = <span class="keyword">new</span> Gson().fromJson(</span><br><span class="line"> snapshot.getString(VALUE_INDEX),</span><br><span class="line"> <span class="keyword">new</span> TypeToken<List<Photo>>(){}.getType()</span><br><span class="line"> );</span><br><span class="line"> <span class="comment">// 刷新照片列表</span></span><br><span class="line"> adapter.setData(photos);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line">}</span><br><span class="line">NetworkApi networkApi = ...</span><br><span class="line">networkApi.getAllPhotos()</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(photos -> {</span><br><span class="line"> adapter.setData(photos);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 更新缓存</span></span><br><span class="line"> DiskLruCache.Editor editor = cache.edit(<span class="string">"getAllPhotos"</span>);</span><br><span class="line"> editor.set(VALUE_INDEX, <span class="keyword">new</span> Gson().toJson(photos)).commit();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>上面的代码就是最直观的可以解决需求的代码,我们进一步思考一下,读取文件缓存也属于耗时操作,我们最好把它封装为异步任务,既然网络请求已经被封装成 <code>Observable</code> 了,我们尝试把读取文件缓存也封装为 <code>Observable</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></pre></td><td class="code"><pre><span class="line">Observable<List<Photo>> cachedObservable = Observable.create(emitter -> {</span><br><span class="line"> DiskLruCache.Snapshot snapshot = cache.get(<span class="string">"getAllPhotos"</span>);</span><br><span class="line"> <span class="keyword">if</span> (snapshot != <span class="keyword">null</span>) {</span><br><span class="line"> List<Photo> cachedPhotos = <span class="keyword">new</span> Gson().fromJson(</span><br><span class="line"> snapshot.getString(VALUE_INDEX),</span><br><span class="line"> <span class="keyword">new</span> TypeToken<List<Photo>>(){}.getType()</span><br><span class="line"> );</span><br><span class="line"> emitter.onNext(cachedPhotos);</span><br><span class="line"> } </span><br><span class="line"> emitter.onComplete();</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>到目前为止,发起网络请求和读取缓存这两个异步操作都被我们封装成了 <code>Observable</code> 的形式,前面做了这么多铺垫,接下来进入正题:把原先的面向 Callback 的异步操作统一改写为 <code>Observable</code> 的形式以后,首先带来的好处就是可以对 <strong>Observable 在空间维度上进行重新组织</strong>。</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">networkApi.getAllPhotos()</span><br><span class="line"> .doOnNext(photos -> </span><br><span class="line"> <span class="comment">// 更新缓存</span></span><br><span class="line"> cache.edit(<span class="string">"getAllPhotos"</span>)</span><br><span class="line"> .set(VALUE_INDEX, <span class="keyword">new</span> Gson().toJson(photos))</span><br><span class="line"> .commit()</span><br><span class="line"> )</span><br><span class="line"> <span class="comment">// 读取现有缓存</span></span><br><span class="line"> .startWith(cachedObservable)</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(photos -> {</span><br><span class="line"> adapter.setData(photos);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p>调用 <code>startWith</code> 操作符后,会生成一个新的 Observable,新的 <code>Observable</code> 会首先发射传入的 <code>Observable</code> 包含的元素,而后才会发射原来的 <code>Observable</code> 包含的元素。例如 <code>Observable</code> A 包含 a1, a2 两个元素, <code>Observable</code> B 包含 b1, b2 两个元素,那么 b.startWith(a) 返回的新 <code>Observable</code> 发射序列顺序为: a1, a2, b1, b2。—— <a href="http://reactivex.io/documentation/operators/startwith.html" target="_blank" rel="noopener">参考资料:StartWith</a> </p></blockquote><p>在上面的例子中,我们连接了网络请求和读取缓存这两个 Observable,原先需要分别处理结果的两个异步任务,我们现在把它们结合成了一个,指定了一个观察者就满足了需求。这个观察者会被回调 2 次,第一次是来自缓存的结果,第二次是来自网络的结果,体现在界面上就是列表刷新了两次。</p><p>这里引发了我们的思考,原先 Callback 的写法,如果我们有 <strong>n</strong> 个异步任务,我们就需要指定 <strong>n</strong> 个回调;而如果在 <strong>n</strong> 个异步任务都已经被封装成 <code>Observable</code> 的情况下,我们就可以对 <code>Observable</code> 进行分类、组合、变换,经过这样的处理以后,我们的观察者的数量就会减少,而且职责会变的简单而直接,只需要对它所关心的数据类型做出响应,而不需要关心数据从何而来,经历过怎样的变化。</p><p>我们再进一步,上面的例子再加一个需求:如果从网络请求回来的数据和缓存中提前响应的数据一致,就不需要再刷新一次了。也就是说,如果缓存数据和网络数据一致,那缓存数据刷新一次列表以后,网络数据不需要再去刷新一次列表了。</p><p>我们考虑一下,如果我们使用传统 Callback 的形式,指定了两个 Callback 去处理这个需求,为了保证第二次网络请求回来的相同数据不刷新,我们势必需要在两个 Callback 之外,定义一个变量来保存缓存数据,然后在网络请求的回调内,比较两个值,来决定是否需要刷新界面。</p><p>但如果我们用 RxJava 如何来实现这个需求,该如何写呢:</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">networkApi.getAllPhotos()</span><br><span class="line"> .doOnNext(photos -> </span><br><span class="line"> cache.edit(<span class="string">"getAllPhotos"</span>)</span><br><span class="line"> .set(VALUE_INDEX, <span class="keyword">new</span> Gson().toJson(photos))</span><br><span class="line"> .commit()</span><br><span class="line"> )</span><br><span class="line"> .startWith(cachedObservable)</span><br><span class="line"> <span class="comment">// 保证不会出现相同数据</span></span><br><span class="line"> .distinctUntilChanged()</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(photos -> {</span><br><span class="line"> adapter.setData(photos);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p><code>distinctUntilChanged</code> 操作符用来确保 <code>Observable</code> 发射的元素里,相邻的两个元素必须是不相等的。 <a href="http://reactivex.io/documentation/operators/distinct.html" target="_blank" rel="noopener">参考资料:Distinct</a></p></blockquote><p>与原先的写法相比,只多了一行 <code>.distinctUntilChanged()</code> ( 我们假设用于比较两个对象是否相等的 <code>equals</code> 方法已经实现 ),就可以满足,在网络数据和缓存数据一致的情况下,观察者只回调一次。</p><p>我们比较一下使用 Callback 的写法和使用 <code>Observable</code> 进行组装的写法,可以发现,使用 Callback 的写法,经常会由于需求的变化,导致 Callback 内部的逻辑发生变动,而使用 <code>Observable</code> 的写法,观察者的核心逻辑则较为稳定,很少发生变化(本例中为刷新列表)。<strong>Observable 通过内置的操作符对自身发射的元素在空间维度上重新组织,或者与其他的 <code>Observable</code> 一起在空间维度上进行重新组织,使得观察者的逻辑简单而直接,不需要关心数据从何而来,从而使观察者的逻辑较为稳定</strong>。</p><h2 id="一个复杂的例子"><a href="#一个复杂的例子" class="headerlink" title="一个复杂的例子"></a>一个复杂的例子</h2><p>情景:实现一个具有多种类型的 RecyclerView,如图所示:</p><p><img src="http://prototypez.github.io/images/complex-list.png" alt=""></p><p>假设列表中有 3 种类型的数据,这 3 种类型共同填充了一个 RecyclerView,简单起见,我们定义 Retrofit 接口如下:</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">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">NetworkApi</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemA>> getItemListOfTypeA();</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemB>> getItemListOfTypeB();</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemC>> getItemListOfTypeC();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>到目前为止,情况还是简单的, 我只要维护 3 个 RecyclerView 并分别各自更新即可。但是我们现在接到新加需求,这 3 种类型的数据在列表中出现的顺序是可配置的,而且 3 种类型数据不一定全部需要展示,也就是说可能展示 3 种,也可能只展示其中 2 种。我们定义与之对应的接口:</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></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">NetworkApi</span> </span>{</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemA>> getItemListOfTypeA();</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemB>> getItemListOfTypeB();</span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<ItemC>> getItemListOfTypeC();</span><br><span class="line"> <span class="comment">// 需要展示的数据顺序</span></span><br><span class="line"> <span class="meta">@GET</span>(<span class="string">"/path/to/api"</span>)</span><br><span class="line"> Observable<List<String>> getColumns();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>新加的 <code>getColumns</code> 接口,返回的数据形如:</p><ul><li><code>["a", "b", "c"]</code></li><li><code>["b", "a"]</code></li><li><code>["b", "c"]</code></li></ul><p>首先考虑使用普通的 Callback 形式如何来实现这个需求。由于 3 种数据现在顺序可变,数量也无法确定,如果还是考虑由多个 RecyclerView 来维护的话需要在布局中调用 <code>addView</code>, <code>removeView</code><br>来添加移除 RecyclerView,这样的话性能上不够好,我们考虑把所有数据填充到一个 RecyclerView 中,不同类型的数据通过不同 ItemType 进行区分。下面的代码中我依然使用了 <code>Observable</code> ,只是我仅仅把它当成普通的 Callback 功能使用:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> NetworkApi networkApi = ...</span><br><span class="line"><span class="comment">// 不同类型数据出现的顺序</span></span><br><span class="line"><span class="keyword">private</span> List<String> resultTypes;</span><br><span class="line"><span class="comment">// 这些类型对应的数据的集合</span></span><br><span class="line"><span class="keyword">private</span> LinkedList<List<? extends Item>> responseList;</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">refresh</span><span class="params">()</span> </span>{</span><br><span class="line"> networkApi.getColumns().subscribe(columns -> {</span><br><span class="line"> <span class="comment">// 保存配置的栏目顺序</span></span><br><span class="line"> resultTypes = columns;</span><br><span class="line"> responseList = <span class="keyword">new</span> LinkedList<>(Collections.nCopies(columns.size(), <span class="keyword">new</span> ArrayList<>()));</span><br><span class="line"> <span class="keyword">for</span> (String type : columns) {</span><br><span class="line"> <span class="keyword">switch</span> (type) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"a"</span>:</span><br><span class="line"> networkApi.getItemListOfTypeA().subscribe(data -> onOk(<span class="string">"a"</span>, data));</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"b"</span>:</span><br><span class="line"> networkApi.getItemListOfTypeB().subscribe(data -> onOk(<span class="string">"b"</span>, data));</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"c"</span>:</span><br><span class="line"> networkApi.getItemListOfTypeC().subscribe(data -> onOk(<span class="string">"c"</span>, data));</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"> });</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">onOk</span><span class="params">(String type, List<? extends Item> response)</span> </span>{</span><br><span class="line"> <span class="comment">// 按配置的顺序,更新对应位置上的数据</span></span><br><span class="line"> responseList.set(resultTypes.indexOf(type), response);</span><br><span class="line"> <span class="comment">// 把当前已返回的数据填充到一个 List 中</span></span><br><span class="line"> List<Item> data = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">for</span> (List<? extends Item> itemList: responseList) {</span><br><span class="line"> data.addAll(itemList);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 更新列表</span></span><br><span class="line"> adapter.setData(data);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面的代码,为了避免 <strong>Callback Hell</strong> 出现,我已经提前把 <code>onOk</code> 提到了外部层次,使代码便于从上往下阅读。但是不知道你有没有和我相同的感觉,就是类似这样的代码总给人一种不是很 “<strong>内聚</strong>” 的感觉,就是为了把 Callback 展平,导致一些中间变量被暴露到了外层空间。</p><p>带着这个问题,我们先分析一下数据流动:</p><ol><li><code>refresh</code> 方法发起第一次请求,得到需要被展示的 <strong>n</strong> 种数据的类型以及顺序。</li><li>根据第一次请求的结果,发起 <strong>n</strong> 次请求,分别得到每种数据的结果。</li><li><code>onOk</code> 方法作为观察者, 会被回调 <strong>n</strong> 次,按照第一个接口里返回的顺序正确的汇总 <strong>2</strong> 中每个数据接口返回的结果,并且通知界面更新。 </li></ol><p>有点像写作文一样,这是一种 <strong>总——分——总</strong> 的结构。</p><h2 id="Observable-在空间维度重新组织事件"><a href="#Observable-在空间维度重新组织事件" class="headerlink" title="Observable 在空间维度重新组织事件"></a>Observable 在空间维度重新组织事件</h2><p>接下来我们使用 RxJava 来实现这个需求,我们会用到 RxJava 的一些操作符,来对 <code>Observable</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></pre></td><td class="code"><pre><span class="line">NetworkApi networkApi = ...</span><br><span class="line"></span><br><span class="line">networkApi.getColumns()</span><br><span class="line"> .map(types -> {</span><br><span class="line"> List<Observable<? extends List<? extends Item>>> requestObservableList = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">for</span> (String type : types) {</span><br><span class="line"> <span class="keyword">switch</span> (type) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"a"</span>:</span><br><span class="line"> requestObservableList.add(</span><br><span class="line"> networkApi.getItemListOfTypeA().startWith(<span class="keyword">new</span> ArrayList<ItemA>())</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"b"</span>:</span><br><span class="line"> requestObservableList.add(</span><br><span class="line"> networkApi.getItemListOfTypeB().startWith(<span class="keyword">new</span> ArrayList<ItemB>())</span><br><span class="line"> );</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"c"</span>:</span><br><span class="line"> requestObservableList.add(</span><br><span class="line"> networkApi.getItemListOfTypeC().startWith(<span class="keyword">new</span> ArrayList<ItemC>())</span><br><span class="line"> );</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"> <span class="keyword">return</span> requestObservableList;</span><br><span class="line"> })</span><br><span class="line"> .flatMap(requestObservables -> Observable.combineLatest(requestObservables, objects -> {</span><br><span class="line"> List<Item> items = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"> <span class="keyword">for</span> (Object response : objects) {</span><br><span class="line"> items.addAll((List<? extends Item>) response);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> items;</span><br><span class="line"> }))</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(data -> {</span><br><span class="line"> adapter.setData(data);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>我们一步一步分析 RxJava 处理的具体步骤。首先是第一步,获取需要展示的栏目列表,这是最简单的,<code>networkApi.getColumns()</code> 这个方法返回是一个只发射一个元素的 <code>Observable</code>,这个元素即为展示的栏目列表,为了方便后续讨论,假设栏目的顺序为 <code>["a", "b", "c"]</code>, 如下图所示:</p><p><img src="http://prototypez.github.io/images/rxjava-demo-1.png" alt=""></p><p>接下来的操作符是 <code>map</code> 操作符,原来的 <code>Observable</code> 进行了变换,变成了一个新的 <code>Observable</code>,新的 <code>Observable</code> 还是只发射一个元素,这个元素的类型还是 List ,只不过 List 内部的数据类型从原先的字符串(代表数据类型)变成了 <code>Observable</code>。<code>Observable</code> 发射的元素还可以是 “<code>Observable</code> 的 List ” 吗?是的,没有什么不可以 : ) </p><p><img src="http://prototypez.github.io/images/rxjava-demo-2.png" alt=""></p><blockquote><p><code>map</code> 操作符负责把一个 <code>Observable</code> 里发射的元素全部进行转换,生成一个发射新的元素的 <code>Observable</code>,元素的种类会发生改变,但是发射的元素的数量不会发生改变。 <a href="http://reactivex.io/documentation/operators/map.html" target="_blank" rel="noopener">参考资料:Map</a></p></blockquote><p>这个操作,在业务上的含义是,根据上一步取回的栏目列表,即 <code>["a", "b", "c"]</code>,根据不同的数据类型,分别发起请求去获取对应栏目的数据列表,例如栏目类型是 <code>a</code> 的话,就对应发起 <code>networkApi.getItemListOfTypeA()</code> 请求。这里有一点值得注意,就是每一个具体的请求后面都跟了一个 <code>.startWith(new ArrayList<>())</code>,也就是说每个具体请求栏目内容的 <code>Observable</code> 在返回真正的数据 List 之前都会返回一个空的 List ,这里这么处理的原因我们会在下一步中解释。</p><p>接下来这一步可能是最难理解的一步了,<code>map</code> 操作之后,紧接着是 <code>flatMap</code> 操作符,而 <code>flatMap</code> 操作符传入的 lambda 表达式内部,又调用了 <code>Observable.combineLatest</code> 操作符,我们先从里面的 <code>combineLatest</code> 操作符开始讲起,请看下图:</p><p><img src="http://prototypez.github.io/images/rxjava-demo-3.png" alt=""></p><p><code>combineLatest</code> 操作符的第一个参数 <code>requestObservables</code>,它的类型是 <code>Observable</code> 的 List,它就是上一步中 <code>map</code> 操作符进行变换之后,新的 <code>Observable</code> 发射的数据,即由</p><ul><li><code>networkApi.getItemListOfTypeA().startWith(...)</code></li><li><code>networkApi.getItemListOfTypeB().startWith(...)</code></li><li><code>networkApi.getItemListOfTypeC().startWith(...)</code></li></ul><p>3 个 <code>Observable</code> 组成的 List。</p><p><code>combineLatest</code> 操作符的第二个参数是个 lambda 表达式,这个 lambda 表达式的参数类型是 <code>Object[]</code>,这个数组的长度等于 <code>requestObservables</code> 的长度,<code>Object[]</code> 数组中每个元素即为 <code>requestObservables</code> 中每个 <code>Observable</code> 发射的元素,即:</p><ul><li><code>Object[0]</code> 对应 <code>requestObservables[0]</code> 发射的元素</li><li><code>Object[1]</code> 对应 <code>requestObservables[1]</code> 发射的元素</li><li><code>Object[2]</code> 对应 <code>requestObservables[2]</code> 发射的元素</li></ul><p>那这个 lambda 表达式被调用的时机是什么时候呢?当 <code>requestObservables</code> 中任意一个 <code>Observable</code> 发射一个元素时,这个元素便会和 <code>requestObservables</code> 中剩余的所有 <code>Observable</code> <strong>最近一次</strong> 发射的元素一起,作为参数调用这个 lambda 表达式。</p><p>那么整个 <code>combineLatest</code> 操作符的作用就是,返回一个新的 <code>Observable</code>, 根据第一个参数里输入的一组 <code>Obsevable</code>,按照上面说的时机,调用第二个参数里的那个 lambda 表达式,把这个 lambda 表达式的返回值,作为新的 <code>Observable</code> 发射的值,lambda 被调用几次,就发射几个元素。</p><blockquote><p><a href="http://reactivex.io/documentation/operators/combinelatest.html" target="_blank" rel="noopener">参考资料:CombineLatest</a></p></blockquote><p>我们这里 lambda 表达式内部的逻辑比较简单,就是把 3 个接口里返回的数据进行汇总,组成一个新的 List 。我们再回过头看上面那张图,我们可以看到,<code>Observable.combinLatest</code> 返回的新的 <code>Observable</code> 一共发射了 4 个元素,它们分别是:</p><ul><li><code>[]</code></li><li><code>[{ItemB}, {ItemB}, ...]</code></li><li><code>[{ItemA}, {ItemA}, ..., {ItemB}, {ItemB}, ...]</code></li><li><code>[{ItemA}, {ItemA}, ..., {ItemB}, {ItemB}, ..., {ItemC}, {ItemC}, ...]</code></li></ul><p>前面留了一个问题没有解释,为什么 3 个获取具体的栏目数据的接口需要调用 <code>startWith</code> 操作符发射一个空白列表,就像这样:<code>networkApi.getItemListOfTypeA().startWith(...)</code>,现在这个答案应该清晰了,如果不调用这个操作符,那么 <code>combineLatest</code> 操作符生成的新 <code>Observable</code> 将会只发射一个元素, 即上面 4 个元素的最后一个,从用户的感受来看,必须要等所有栏目全部请求成功以后才会一次性展示,而不是渐进地展示。</p><p>说完了内部的 <code>combineLatest</code> 操作符,现在该说外层的 <code>flatMap</code> 操作符了,<code>flatMap</code> 操作符也会生成一个新的 <code>Observable</code>,它会通过传入的 lambda 表达式,把旧的 <code>Observable</code> 里发射的每一个元素都映射成一个 <code>Observable</code>,然后把这些 <code>Observable</code> 发射的所有元素作为新的 <code>Observable</code> 发射的元素。</p><blockquote><p><a href="http://reactivex.io/documentation/operators/flatmap.html" target="_blank" rel="noopener">参考资料:FlatMap</a></p></blockquote><p>由于我们这里的情况,调用 <code>flatMap</code> 之前的 <code>Observable</code> 只发射了一个元素,所以 <code>flatMap</code> 之后生成的新 <code>Observable</code> 发射的元素,就是 <code>flatMap</code> 操作符传入的那个 lambda 表达式执行完生成的那个 <code>Observable</code> 所发射的元素,也就是说 <code>flatMap</code> 操作符执行完后的那个新的 <code>Observable</code> 发射的元素,和我们刚刚讨论的 <code>combineLatest</code> 操作符执行完后的 <code>Observable</code> 发射的元素是一致的。</p><p>到这里为止,RxJava 实现的版本的每一步我们都解释完了,我们回过头重新梳理一下 RxJava 对 <code>Observable</code> 进行变换的过程,如下图:</p><p><img src="http://prototypez.github.io/images/rxjava-demo-4.png" alt=""></p><p>通过 RxJava 的操作符,我们把 <code>networkApi</code> 里的 4 个接口返回的 4 个 <code>Observable</code>,<strong>在空间维度进行了重新组织</strong>,最终把它们转成了一个 <code>Observable</code>,这个 <code>Observable</code> 发射的元素类型是 <code>List<Item></code>,而这正是我们的观察者 – Adapter 所关心的数据类型,观察者只需要监听这个 <code>Observable</code> ,并更新数据即可。</p><p>我们在讲 RxJava 实现的这个版本之前的时候,说到过 Callback 实现的版本不够 <strong>内聚</strong>,比较一下现在这个 RxJava 的版本,确实可以发现的确 RxJava 这个版本更内聚。但是并非 Callback 版本没有办法做到更内聚,我们可以把 Callback 版本里的 <code>onOk</code>, <code>refresh</code>,<code>resultTypes</code>, <code>responseList</code> 这几个方法和字段封装到一个对象中,对外只暴露 <code>refresh</code> 方法和一个设置观察者的方法,也可以做到一样的内聚,但是这就需要额外的工作量了。可如果我们使用 RxJava 就不一样了,它提供了一堆现成的操作符,通过 <code>Observable</code> 之间的变换与重组,直接就可以写出内聚的代码。</p><p>在上面代码里出现的所有操作符中,最核心的一个操作符就是 <code>combineLatest</code> 操作符,仔细比较 RxJava 版本和 Callback 版本就可以发现,<code>combineLatest</code> 操作符的功能其实和 Callback 版本里的 <code>onOk</code> 方法前半部分, <code>resultTypes</code>, <code>responseList</code> 合在一起功能是相当的,一方面负责收集多个接口返回的数据,另一方面保证收集回来的数据的顺序是和上一个接口返回的应该展示的数据的顺序是一致的。 </p><h2 id="一种更加函数式的写法"><a href="#一种更加函数式的写法" class="headerlink" title="一种更加函数式的写法"></a>一种更加函数式的写法</h2><p>从代码量上来看,RxJava 版本与 Callback 版本相差无几,对函数式编程比较擅长的人来说,RxJava 版本里 <code>for</code> 循环的写法,不够 “<strong>函数式</strong>”,我们可以把原来的写法改成一种更紧凑、更函数式的写法:</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">NetworkApi networkApi = ...</span><br><span class="line"></span><br><span class="line">netWorkApi.getColumns()</span><br><span class="line"> .flatMap(types -> Observable.fromIterable(types)</span><br><span class="line"> .map(type -> {</span><br><span class="line"> <span class="keyword">switch</span> (type) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"a"</span>: <span class="keyword">return</span> netWorkApi.getItemListOfTypeA().startWith(<span class="keyword">new</span> ArrayList<ItemA>());</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"b"</span>: <span class="keyword">return</span> netWorkApi.getItemListOfTypeB().startWith(<span class="keyword">new</span> ArrayList<ItemB>());</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"c"</span>: <span class="keyword">return</span> netWorkApi.getItemListOfTypeC().startWith(<span class="keyword">new</span> ArrayList<ItemC>());</span><br><span class="line"> <span class="keyword">default</span>: <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException();</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> .<List<Observable<? extends List<? extends Item>>>>collectInto(<span class="keyword">new</span> ArrayList<>(), List::add)</span><br><span class="line"> .toObservable()</span><br><span class="line"> )</span><br><span class="line"> .flatMap(requestObservables -> Observable.combineLates(requestObservables, objects -> objects))</span><br><span class="line"> .flatMap(objects -> Observable.fromArray(objects)</span><br><span class="line"> .<List<Item>>collectInto(<span class="keyword">new</span> ArrayList<>(), (items, o) -> items.addAll((List<Item>) o))</span><br><span class="line"> .toObservable()</span><br><span class="line"> )</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe(data -> {</span><br><span class="line"> adapter.setData(data);</span><br><span class="line"> adapter.notifyDataSetChanged();</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><blockquote><p>这里引入了一个新的操作符 <code>collectInto</code>,用于把一个 <code>Observable</code> 里面发射的元素,收集到一个可变的容器内部,本例中用它来替换 <code>for</code> 循环相关逻辑,具体内容这里不再详细展开。<br><a href="http://reactivex.io/documentation/operators/reduce.html" target="_blank" rel="noopener">参考资料:CollectInto</a></p></blockquote><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>第二个例子花了这么大篇幅来讲,超出了我一开始的预期,这也可以看出来的确 RxJava <strong>学习的曲线是陡峭的</strong>,不过我认为这个例子很好的表达我这一小节要阐述的观点,即 <strong>Observable 在空间维度上对事件的重新组织,让我们的事件驱动型编程更具想象力</strong> ,因为原先的编程中,我们面对多少个异步任务,就会写多少个回调,如果任务之间有依赖关系,我们的做法就是修改观察者(回调函数)逻辑以及新增数据结构保证依赖关系,RxJava 给我们带来的新思路是,<code>Observable</code> 的事件在到达观察者之前,可以先通过操作符进行一系列变换(当然变换的规则还是和具体业务逻辑有关的),对观察者屏蔽数据产生的复杂性,只提供给观察者简单的数据接口。</p><p>那么是否在这个例子中,是否 RxJava 的版本更好呢,我个人的观点是虽然 RxJava 版本展现了其更有想象力的编程方式,但是就这个具体的例子,<strong>两者并没有太大的差距</strong>。RxJava 可以写出更短更内聚的代码,但是编写和理解的难度较大;Callback 版本虽然朴实无华,但是便于编写以及理解,可维护性更好。对于两者的好坏,我们也不要过于着急下结论,不妨继续看看 RxJava 还有什么其他的优势。</p><p>(未完待续)</p><p>本文属于 “RxJava 沉思录” 系列,欢迎阅读本系列的其他分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>本文是 “RxJava 沉思录” 系列的第二篇分享。本系列所有分享:</p>
<ul>
<li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li>
<li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li>
<li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li>
<li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li>
</ul>
<p>在上一篇分享中,我们澄清了目前有关 RxJava 的几个最流行的误解,它们分别是:“<strong>链式编程是 RxJava 的厉害之处</strong>”,“<strong>RxJava 等于异步加简洁</strong>”,“<strong>RxJava 是用来解决 Callback Hell 的</strong>”。在上一篇的最后,我们了解了 RxJava 其实给我们最基础的功能就是帮我们统一了所有异步回调的接口。但是 RxJava 并不止于此,本文我们将首先介绍 <strong>Observable 在空间维度上重新组织事件的能力</strong>。</p>
</summary>
<category term="RxJava" scheme="http://prototypez.github.io/tags/RxJava/"/>
</entry>
<entry>
<title>RxJava 沉思录(一):你认为 RxJava 真的好用吗?</title>
<link href="http://prototypez.github.io/2018/08/29/thoughts-in-rxjava-1/"/>
<id>http://prototypez.github.io/2018/08/29/thoughts-in-rxjava-1/</id>
<published>2018-08-29T07:31:00.000Z</published>
<updated>2018-09-08T07:54:03.883Z</updated>
<content type="html"><![CDATA[<p>本人两年前第一次接触 RxJava,和大多数初学者一样,看的第一篇 RxJava 入门文章是扔物线写的<a href="https://gank.io/post/560e15be2dca930e00da1083#toc_31" target="_blank" rel="noopener">《给 Android 开发者的 RxJava 详解》</a>,这篇文章流传之广,相信几乎所有学习 RxJava 的开发者都阅读过。尽管那篇文章定位读者是 RxJava 入门的初学者,但是阅读完之后还是觉得懵懵懂懂,总感觉依然不是很理解这个框架设计理念以及优势。</p><a id="more"></a><p>随后工作中有机会使用 RxJava 重构了项目的网络请求以及缓存层,期间陆陆续续又重构了数据访问层,以及项目中其他的一些功能模块,无一例外,我们都选择使用了 RxJava 。</p><p>最近翻看一些技术文章,发现涉及 RxJava 的文章还是大多以入门为主,我尝试从一个初学者的角度阅读,发现很多文章都没讲到关键的概念点,举的例子也不够恰当。回想起两年前刚刚学习 RxJava 的自己,虽然看了许多 RxJava 入门的文章,但是始终无法理解 RxJava 究竟好在哪里,所以一定是哪里出问题了。于是有了这一篇反思,希望能和你一起重新思考 RxJava,以及重新思考 RxJava 是否真的让我们的开发变得更轻松。</p><h2 id="观察者模式有那么神奇吗"><a href="#观察者模式有那么神奇吗" class="headerlink" title="观察者模式有那么神奇吗?"></a>观察者模式有那么神奇吗?</h2><p>几乎所有 RxJava 入门介绍,都会用一定的篇幅去介绍 “<strong>观察者模式</strong>”,告诉你观察者模式是 RxJava 的核心,是基石:</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">observable.subscribe(<span class="keyword">new</span> Observer<String>() {</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">onNext</span><span class="params">(String s)</span> </span>{</span><br><span class="line"> Log.d(tag, <span class="string">"Item: "</span> + s);</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">onCompleted</span><span class="params">()</span> </span>{</span><br><span class="line"> Log.d(tag, <span class="string">"Completed!"</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">onError</span><span class="params">(Throwable e)</span> </span>{</span><br><span class="line"> Log.d(tag, <span class="string">"Error!"</span>);</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>年少的我不明觉厉:“好厉害,原来这是观察者模式”,但是心里还是感觉有点不对劲:“这代码是不是有点丑?接收到数据的回调名字居然叫 <code>onNext</code> ? ”</p><p>但是其实观察者并不是什么新鲜的概念,即使你是新手,你肯定也已经写过不少观察者模式的代码了,你能看懂下面一行代码说明你已经对观察者模式了然于胸了:</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">button.setOnClickListener(v -> doSomething());</span><br></pre></td></tr></table></figure><p>这就是观察者模式,<code>OnClickListener</code> 订阅了 button 的点击事件,就这么简单。原生的写法对比上面 RxJava 那一长串的写法,是不是要简单多了。有人可能会说,RxJava 也可以写成一行表示:</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">RxView.clicks(button).subscribe(v -> doSomething());</span><br></pre></td></tr></table></figure><p>先不说这么写需要引入 <a href="https://github.com/JakeWharton/RxBinding" target="_blank" rel="noopener">RxBinding</a> 这个第三方库,不考虑这点,这两种写法最多也只是打个平手,完全体现不出 RxJava 有任何优势。</p><p>这就是我要说的第一个论点,如果仅仅只是为了使用 RxJava 的观察者模式,而把原先 Callback 的形式,改为 RxJava 的 <code>Observable</code> 订阅模式是没有价值的,你只是把一种观察者模式改写成了另一种观察者模式。我是实用主义者,使用 RxJava 不是为了炫技,所以观察者模式是我们使用 RxJava 的理由吗?当然不是。</p><h2 id="链式编程很厉害吗"><a href="#链式编程很厉害吗" class="headerlink" title="链式编程很厉害吗?"></a>链式编程很厉害吗?</h2><p>链式编程也是每次提到 <code>RxJava</code> 的时候总会出现的一个高频词汇,很多人形容链式编程是 <code>RxJava</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">Observable.from(folders)</span><br><span class="line"> .flatMap((Func1) (folder) -> { Observable.from(file.listFiles()) })</span><br><span class="line"> .filter((Func1) (file) -> { file.getName().endsWith(<span class="string">".png"</span>) })</span><br><span class="line"> .map((Func1) (file) -> { getBitmapFromFile(file) })</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .observeOn(AndroidSchedulers.mainThread())</span><br><span class="line"> .subscribe((Action1) (bitmap) -> { imageCollectorView.addImage(bitmap) });</span><br></pre></td></tr></table></figure><p>这段代码出现的频率非常的高,好像是 RxJava 的链式编程给我们带来的好处的最佳佐证。然而平心而论,我看到这个例子的时候,内心是平静的,并没有像大多数文章写得那样,内心产生“它很长,但是很清晰”的心理活动。</p><p>首先,<code>flatMap</code>, <code>filter</code>, <code>map</code> 这几个操作符,对于没有函数式编程经验的初学者来讲,并不好理解。其次,虽然这段代码用了很多 RxJava 的操作符,但是其逻辑本质并不复杂,就是在后台线程把某个文件夹里面的以 <strong>png</strong> 结尾的图片文件解析出来,交给 UI 线程进行渲染。</p><p>上面这段代码,还带有一个反例,使用 <code>new Thread()</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> Thread() {</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"> <span class="keyword">super</span>.run();</span><br><span class="line"> <span class="keyword">for</span> (File folder : folders) {</span><br><span class="line"> File[] files = folder.listFiles();</span><br><span class="line"> <span class="keyword">for</span> (File file : files) {</span><br><span class="line"> <span class="keyword">if</span> (file.getName().endsWith(<span class="string">".png"</span>)) {</span><br><span class="line"> <span class="keyword">final</span> Bitmap bitmap = getBitmapFromFile(file);</span><br><span class="line"> getActivity().runOnUiThread(<span class="keyword">new</span> Runnable() {</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"> imageCollectorView.addImage(bitmap);</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">}.start();</span><br></pre></td></tr></table></figure><p>对比两种写法,可以发现,之所以 RxJava 版本的缩进减少了,是因为它利用了函数式的操作符,把原本嵌套的 <code>for</code> 循环逻辑展平到了同一层次,事实上,我们也可以把上面那个反例的嵌套逻辑展平,既然要用 <code>lambda</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> Thread(() -> {</span><br><span class="line"> File[] pngFiles = <span class="keyword">new</span> File[]{};</span><br><span class="line"> <span class="keyword">for</span> (File folder : folders) {</span><br><span class="line"> pngFiles = ArrayUtils.addAll(pngFiles, folder.listFiles());</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (File file : pngFiles) {</span><br><span class="line"> <span class="keyword">if</span> (file.getName().endsWith(<span class="string">".png"</span>)) {</span><br><span class="line"> <span class="keyword">final</span> Bitmap bitmap = getBitmapFromFile(file);</span><br><span class="line"> getActivity().runOnUiThread(() -> imageCollectorView.addImage(bitmap));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}).start();</span><br></pre></td></tr></table></figure><p>坦率地讲,这段代码除了 <code>new Thread().start()</code> 有槽点以外,没什么大毛病。RxJava 版本确实代码更少,同时省去了一个中间变量 <code>pngFiles</code>,这得益于函数式编程的 API,但是实际开发中,这两种写法无论从性能还是项目可维护性上来看,并没有太大的差距,甚至,如果团队并不熟悉函数式编程,后一种写法反而更容易被大家接受。</p><p>回到刚才说的“链式编程”,RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函数式编程风格带到了带到了低版本 Android 系统上,确实带给我们一些方便,但是仅此而已吗?到目前为止我并没有看到 RxJava 在处理事件尤其是异步事件上有什么特别的手段。</p><p>准确的来说,我的关注点并不在大多数文章鼓吹的“链式编程”这一点上,把多个依次执行的异步操作的调用转化为类似同步代码调用那样的自上而下执行,并不是什么新鲜事,而且就这个具体的例子,使用 Android 原生的 <code>AsyncTask</code> 或者 <code>Handler</code> 就可以满足需求,RxJava 相比原生的写法无法体现它的优势。</p><p>除此以外,对于处理异步任务,还有 <code>Promise</code> 这个流派,使用类似这样的 API:</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">promise</span><br><span class="line"> .then(r1 -> task1(r1))</span><br><span class="line"> .then(r2 -> task2(r2))</span><br><span class="line"> .then(r3 -> task3(r3))</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><p>难道不是比 RxJava 更加简洁直观吗?而且还不需要引入函数式编程的内容。这种写法,跟所谓的“逻辑简洁”也根本没什么关系,所以从目前看来,RxJava 在我心目只是个 <strong>“哦,还挺不错”</strong> 的框架,但是并没有惊艳到我。</p><p>以上是我要说的第二个论点,链式编程的形式只是一种语法糖,通过函数式的操作符可以把嵌套逻辑展平,通过别的方法也可以把嵌套逻辑展平,这只是普通操作,也有其他框架可以做到相似效果。</p><h2 id="RxJava-等于异步加简洁吗"><a href="#RxJava-等于异步加简洁吗" class="headerlink" title="RxJava 等于异步加简洁吗?"></a>RxJava 等于异步加简洁吗?</h2><p>相信阅读过本文开头介绍的那篇 RxJava 入门文 <a href="https://gank.io/post/560e15be2dca930e00da1083#toc_31" target="_blank" rel="noopener">《给 Android 开发者的 RxJava 详解》</a> 的开发者一定对文中两个小标题印象深刻:</p><blockquote><p>RxJava 到底是什么? —— 一个词:<strong>异步</strong></p></blockquote><blockquote><p>RxJava 好在哪? —— 一个词:<strong>简洁</strong></p></blockquote><p>首先感谢扔物线,很用心地为初学者准备了这篇简洁朴实的入门文。但是我还是想要指出,这样的表达是<strong>不够严谨的</strong>。</p><p>虽然我们使用 RxJava 的场景大多数与异步有关,但是这个框架并不是与异步等价的。举个简单的例子:</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">Observable.just(<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>).subscribe(System.out::println);</span><br></pre></td></tr></table></figure><p>上面的代码就是同步执行的,和异步没有关系。事实上,RxJava 除非你显式切换到其他的 <code>Scheduler</code>,或者你使用的某些操作符隐式指定了其他 <code>Scheduler</code>,否则 <strong>RxJava 相关代码就是同步执行的</strong>。 </p><p>这种设计和这个框架的野心有关,RxJava 是一种新的 <strong>事件驱动型</strong> 编程范式,它以异步为切入点,试图一统 <strong>同步</strong> 和 <strong>异步</strong> 的世界。<br>本文前面提到过:</p><blockquote><p>RxJava 把目前 Android Sdk 24 以上才支持的 Java 8 Stream 函数式编程风格带到了带到了低版本 Android 系统上。</p></blockquote><p>所以只要你愿意,你完全可以在日常的同步编程上使用 RxJava,就好像你在使用 Java 8 的 Stream API。( 但是两者并不等价,因为 RxJava 是事件驱动型编程 )</p><p>如果你把日常的同步编程,封装为同步事件的 <code>Observable</code>,那么你会发现,同步和异步这两种情况被 RxJava 统一了, 两者具有一样的接口,可以被无差别的对待,同步和异步之间的协作也可以变得比之前更容易。</p><p>所以,到此为止,我这里的结论是:<strong>RxJava 不等于异步</strong>。</p><p>那么 RxJava 等于 <strong>简洁</strong> 吗?我相信有一些人会说 “是的,RxJava 很简洁”,也有一些人会说 “不,RxJava 太糟糕了,一点都不简洁”。这两种说法我都能理解,其实问题的本质在于对 <strong>简洁</strong> 这个词的定义上。关于这个问题,后续会有一个小节专门讨论,但是我想提前先下一个结论,<strong>对于大多数人,RxJava 不等于简洁</strong>,有时候甚至是更难以理解的代码以及更低的项目可维护性。</p><h2 id="RxJava-是用来解决-Callback-Hell-的吗"><a href="#RxJava-是用来解决-Callback-Hell-的吗" class="headerlink" title="RxJava 是用来解决 Callback Hell 的吗?"></a>RxJava 是用来解决 Callback Hell 的吗?</h2><p>很多 RxJava 的入门文都宣扬:RxJava 是用来解决 <strong>Callback Hell</strong> (有些翻译为“回调地狱”)问题的,指的是过多的异步调用嵌套导致的代码呈现出的难以阅读的状态。</p><p>我并不赞同这一点。<strong>Callback Hell</strong> 这个问题,最严重的重灾区是在 Web 领域,是使用 JavaScript 最常见的问题之一,以至于专门有一个网站 <a href="http://callbackhell.com/" target="_blank" rel="noopener">callbackhell.com</a> 来讨论这个问题,由于客户端编程和 Web 前端编程具有一定的相似性,Android 编程或多或少也存在这个问题。</p><p>上面这个网站中,介绍了几种规避 Callback Hell 的常见方法,无非就是把嵌套的层次移到外层空间来,不要使用匿名的回调函数,为每个回调函数命名。如果是 Java 的话,对应的,避免使用匿名内部类,为每个内部类的对象,分配一个对象名。当然,也可以使用框架来解决这类问题,使用类似 <code>Promise</code> 那样的专门为异步编程打造的框架,Android 平台上也有类似的开源版本 <a href="https://github.com/jdeferred/jdeferred" target="_blank" rel="noopener">jdeferred</a>。</p><p>在我看来,<strong>jdeferred</strong> 那样的框架,更像是那种纯粹的用来解决 <strong>Callback Hell</strong> 的框架。 至于 RxJava,前面也提到过,它是一个更有野心的框架,正确使用了 RxJava 的话,确实不会有 <strong>Callback Hell</strong> 再出现了,但如果说 RxJava 就是用来解决 <strong>Callback Hell</strong> 的,那就有点高射炮打蚊子的意味了。</p><h2 id="如何理解-RxJava"><a href="#如何理解-RxJava" class="headerlink" title="如何理解 RxJava"></a>如何理解 RxJava</h2><p>也许阅读了前面几小节内容之后,你的心中会和曾经的我一样,对 RxJava 产生一些消极的想法,并且会产生一种疑问:那么 RxJava 存在的意义究竟是什么呢?</p><p>举几个常见的例子:</p><ol><li><p>为 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></pre></td><td class="code"><pre><span class="line">btn.setOnClickListener(<span class="keyword">new</span> OnClickListener() {</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">onClick</span><span class="params">(View v)</span> </span>{</span><br><span class="line"> <span class="comment">// callback body</span></span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure></li><li><p>Service 组件绑定操作:</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="keyword">private</span> ServiceConnection mConnection = <span class="keyword">new</span> ServiceConnection() {</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">onServiceConnected</span><span class="params">(ComponentName className, IBinder service)</span> </span>{</span><br><span class="line"> <span class="comment">// callback body</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">void</span> <span class="title">onServiceDisconnected</span><span class="params">(ComponentName arg0)</span> </span>{</span><br><span class="line"> <span class="comment">// callback body</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">bindService(intent, mConnection, Context.BIND_AUTO_CREATE);</span><br></pre></td></tr></table></figure></li><li><p>使用 Retrofit 发起网络请求:</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></pre></td><td class="code"><pre><span class="line">Call<List<Photo>> call = service.getAllPhotos();</span><br><span class="line">call.enqueue(<span class="keyword">new</span> Callback<List<Photo>>() {</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">onResponse</span><span class="params">(Call<List<Photo>> call, Response<List<Photo>> response)</span> </span>{</span><br><span class="line"> <span class="comment">// callback body</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">void</span> <span class="title">onFailure</span><span class="params">(Call<List<Photo>> call, Throwable t)</span> </span>{</span><br><span class="line"> <span class="comment">// callback body</span></span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure></li></ol><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></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">Callback</span><<span class="title">T</span>> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onResult</span><span class="params">(T result)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么以上 3 种情况,对应的回调变成了:</p><ol><li>为 View 设置点击事件对应的回调为 <code>Callback<View></code></li><li>Service 组件绑定操作对应的回调为 <code>Callback<Pair<CompnentName, IBinder>></code> (onServiceConnected)、 <code>Callback<CompnentName></code> (onServiceDisconnected)</li><li>使用 Retrofit 发起网络请求对应的回调为 <code>Callback<List<Photo>></code> (onResponse)、 <code>Callback<Throwable></code> (onFailure)</li></ol><p>只要按照这种思路,我们可以把所有的异步回调封装成 <code>Callback<T></code> 的形式,我们不再需要去记忆不同的回调,只需要和一种回调交互就可以了。</p><p>写到这里,你应该已经明白了,RxJava 存在首先最基本的意义就是 <strong>统一了所有异步任务的回调接口</strong> 。而这个接口就是 <code>Observable<T></code>,这和刚刚的 <code>Callback<T></code> 其实是一个意思。此外,我们可以考虑让这个回调更通用一点 —— 可以被回调多次,对应的,<code>Observable</code> 表示的就是一个事件流,它可以发射一系列的事件(<code>onNext</code>),包括一个终止信号(<code>onComplete</code>)。</p><p>如果 RxJava 单单只是统一了回调的话,其实还并没有什么了不起的。统一回调这件事情,除了满足强迫症以外,额外的收益有限,而且需要改造已有代码,短期来看属于负收益。但是 <code>Observable</code> 属于 RxJava 的基础设施,<strong>有了 <code>Observable</code> 以后的 RxJava 才刚刚插上了想象力的翅膀</strong>。</p><p>(未完待续)</p><p>本文属于 “RxJava 沉思录” 系列,欢迎阅读本系列的其他分享:</p><ul><li><a href="/2018/08/29/thoughts-in-rxjava-1/">RxJava 沉思录(一):你认为 RxJava 真的好用吗?</a></li><li><a href="/2018/08/30/thoughts-in-rxjava-2/">RxJava 沉思录(二):空间维度</a></li><li><a href="/2018/08/31/thoughts-in-rxjava-3/">RxJava 沉思录(三):时间维度</a></li><li><a href="/2018/09/01/thoughts-in-rxjava-4/">RxJava 沉思录(四):总结</a></li></ul><hr><p>如果您对我的技术分享感兴趣,欢迎关注我的个人公众号:麻瓜日记,不定期更新原创技术分享,谢谢!:)</p><p><img src="http://prototypez.github.io/images/qrcode.jpg" alt=""></p>]]></content>
<summary type="html">
<p>本人两年前第一次接触 RxJava,和大多数初学者一样,看的第一篇 RxJava 入门文章是扔物线写的<a href="https://gank.io/post/560e15be2dca930e00da1083#toc_31" target="_blank" rel="noopener">《给 Android 开发者的 RxJava 详解》</a>,这篇文章流传之广,相信几乎所有学习 RxJava 的开发者都阅读过。尽管那篇文章定位读者是 RxJava 入门的初学者,但是阅读完之后还是觉得懵懵懂懂,总感觉依然不是很理解这个框架设计理念以及优势。</p>
</summary>
<category term="RxJava" scheme="http://prototypez.github.io/tags/RxJava/"/>
</entry>
<entry>
<title>如何优雅地构建易维护、可复用的 Android 业务流程(二)</title>
<link href="http://prototypez.github.io/2018/06/25/best-practices-of-android-process-management-2/"/>
<id>http://prototypez.github.io/2018/06/25/best-practices-of-android-process-management-2/</id>
<published>2018-06-24T16:00:00.000Z</published>
<updated>2018-09-02T07:31:28.141Z</updated>
<content type="html"><![CDATA[<p>这是关于如何在 Android 中封装业务流程经验分享的第二篇,第一篇在<a href="http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/">这里</a>。所谓 <strong>业务流程</strong> ,指的是一系列页面的集合,这些页面肩负着一个特定职责,负责和用户交互,从用户端收集信息。业务流程有时候由用户主动触发,而有时候是由于某些条件不满足而触发,当流程完成以后,有时候只是简单地回到发起流程的页面,用流程的结果更新那个页面;而有时候是继续之前 <strong>由于触发流程而中断</strong> 的操作;还有些时候是则是转入新的流程。</p><a id="more"></a><h2 id="回顾"><a href="#回顾" class="headerlink" title="回顾"></a>回顾</h2><p>在<a href="http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/">上一篇分享</a>中,我根据自己在公司项目中的实践,列举了七点流程框架应该解决的问题。同时也记录了在选型过程中调研和尝试的若干种方案,它们的优点与不足,以及最后为什么放弃的原因,这些方案分别是:</p><ul><li>简单的基于 <code>startActivityForResult</code>/<code>onActivityResult</code></li><li>基于 EventBus 或者其他基于事件总线的方案</li><li>简单的基于 <code>FLAG_ACTIVITY_CLEAR_TOP</code> 或者设置 launchMode 为 singleTop / singleTask </li><li>开辟新的 Activity 任务栈</li></ul><p>上一篇分享中提出的最后一个方案 – <strong>使用 Fragment 框架封装流程</strong>,是我相对而言比较满意的方案,它并不是完美的方案,至少它没有全部解决我自己提出的对流程框架的七个问题,但是从现阶段来看,在复杂程度,易用性和可靠性上,这种方案已经足够满足我们日常开发所需,在我发现更好的方案之前,我认为还是值得介绍一下的:)</p><p>简单回顾一下,这种方案就是一个 Activity 对应一个流程,这个 Activity 就是这个流程对外暴露的接口,具体暴露的接口是 <code>startActivityForResult</code> 和 <code>onActivityResult</code>, 任何触发流程的位置,都只通过这两个方法,和代表流程的 Activity 进行交互。</p><p>流程的每一个步骤(页面),被封装为一个个 Fragment, Fragment 只和宿主 Activity 交互,通常就是把本步骤的数据传递给宿主 Activity 以及通知宿主 Activity 本步骤已做完。宿主 Activity 即流程 Activity。</p><p>流程 Activity 除了担当本流程对外接口任务以外,还要承担流程内部步骤间的流转,其实就是对代表步骤的 Fragment 的添加与移除。</p><p>以登录流程为例(包含两个步骤:用户名密码验证、需要手机验证码的两步验证), 整个流程与流程触发点(例如首页信息流点赞操作)的交互以及流程内部 Fragment 栈如下图所示:</p><p><img src="/images/login-sample-5.png" alt=""></p><p>而流程宿主 Activity 与代表流程的每个具体步骤的 Fragment 的交互可以用下图来表示:</p><p><img src="/images/login-sample-4.png" alt=""></p><h2 id="如何优化流程与外部交互接口"><a href="#如何优化流程与外部交互接口" class="headerlink" title="如何优化流程与外部交互接口"></a>如何优化流程与外部交互接口</h2><p>上一篇分享中,最后有提到基于 <code>startActivityForResult</code> 和 <code>onActivityResult</code> 两个方法来发起流程和接收流程结果是 <strong>不优雅</strong> 的,而且这种写法也不利于流程在其他位置被复用。例如登录操作在点赞时可能被触发,在评论时也可能被触发,常规写法只会让 Activity / Fragment 过于臃肿。</p><p>我们希望的结果是,发起流程可以被封装为一个普通的异步操作,然后我们就可以像对待普通的异步任务那样,为这个流程指派一个观察者来监听异步结果。但是封装的难点在于我们并不容易在 <code>startActivityForResult</code> 的位置获取一个对象,这个对象可以在 <code>onActivityResult</code> 的时候获得回调,根源在于 <code>onActivityResult</code> 并不属于 Activity / Fragment 的生命周期函数,所以无论是 Google 官方的 <a href="https://developer.android.com/topic/libraries/architecture/lifecycle" target="_blank" rel="noopener">Lifecycle Component</a> 还是第三方的 <a href="https://github.com/trello/RxLifecycle" target="_blank" rel="noopener">RxLifecycle</a> 都不包含这个回调。</p><p>但是我们还是有机会通过别的办法在 <code>startActivityForResult</code> 的位置拿到 <code>onActivityResult</code> 这个回调的观察者。其中一种方案就是借鉴 <a href="https://github.com/bumptech/glide.git" target="_blank" rel="noopener">Glide</a> 的思想,Glide 可以为异步操作绑定 Activity 的生命周期,它的原理就是为发起请求的 Activity 添加一个 <strong>不可见的 Fragment</strong>,大家知道,Fragment 也可以发起 <code>startActivityForResult</code> 操作,并通过 <code>onActivityResult</code> 接受结果。 </p><p>到这里,我们的思路就清晰了。我们的 Activity 自己不需要发起 <code>startActivityForResult</code>,而是新建一个不可见的 Fragemnt ,然后把这个任务交给它,Fragment 就相当于一个观察者, Activity 持有这个 Fragment 对象,Fragment 可以在自己收到 <code>onActivityResult</code> 的时候把结果通知 Activity。</p><p>这种做法有两个好处:首先,</p><ol><li><p>如果我们自己创建一个观察者,那么通常会被放在全局作用空间。那么就需要仔细考虑对象生命周期绑定问题,以防止可能造成的内存泄漏。Fragment 属于 Android 框架的一部分,只要正常使用(例如不要错误使用 static 引用,谨慎使用匿名内部类),就不会造成内存泄漏。</p></li><li><p>自己创建的观察者对象,在 <strong>进程被杀死重新创建</strong> 或者 <strong>后台Activity被回收</strong> 的情况下,可能无法正常恢复,一方面可能导致无法接收到 <code>onActivityResult</code> 的结果,另一方面有可能导致应用 Crash(通常是因为空指针)。而如果我们把观察者的任务交给 Fragment,由于 Fragment 被 Activity 的 FragmentManager 管理,即使 Activity 由于系统的原因被销毁重新创建了,还是可以保证观察者自身被正确恢复,并且正常收到 <code>onActivityResult</code> 回调。</p></li></ol><h2 id="使用-RxJava-进行封装"><a href="#使用-RxJava-进行封装" class="headerlink" title="使用 RxJava 进行封装"></a>使用 RxJava 进行封装</h2><p>上一小节提到,我们希望可以像对待一个普通的异步任务一样,对待业务流程。对于像我们这样的已经在项目中引入 <a href="https://github.com/ReactiveX/RxJava" target="_blank" rel="noopener">RxJava</a> 的团队来说,使用 RxJava 封装自然是首选。</p><p>首先,<code>onActivityResult</code> 这个回调中回传给我们的 3 个参数,我们单独封装为一个类:</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ActivityResult</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> requestCode;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> resultCode;</span><br><span class="line"> <span class="keyword">private</span> Intent data;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">ActivityResult</span><span class="params">(<span class="keyword">int</span> requestCode, <span class="keyword">int</span> resultCode, Intent data)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.requestCode = requestCode;</span><br><span class="line"> <span class="keyword">this</span>.resultCode = resultCode;</span><br><span class="line"> <span class="keyword">this</span>.data = data;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// getters & setters</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>本文在第一节的时候提到:流程 <strong>有时候是由于某些条件不满足而触发</strong> 的,举一个简单的例子:社交 App 的点赞操作需要登录态才可以进行,那么处理点赞事件的代码很有可能是这样的:</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">likeBtn.setOnClickListener(v -> {</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> doLike();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> startLoginProcess()</span><br><span class="line"> .subscribe(<span class="keyword">this</span>::doLike);</span><br><span class="line"> }</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>上面的代码中,我们假设 <code>startLoginProcess</code> 为一个封装好的登录流程,它的返回类型为 <code>Observable<ActivityResult></code>。像这样的条件检测并且发起流程的类似代码很多,一个异步任务,把原本逻辑上流畅的一个代码流程给拆成两部分。为了可以让这部分更优雅,我们其实可以把 <code>LoginInfo.isLogin()</code> 为 <code>true</code> 这种情况也视为 <code>startLoginProcess</code> 这个 <code>Observable</code> 所发射的数据。到目前为止 <code>ActivityResult</code> 这个对象我们已经单独封装好了,我们可以自行实例化这个对象,无需依赖 <code>onActivityResult</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="function"><span class="keyword">public</span> Observable<ActivityResult> <span class="title">loginState</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> <span class="keyword">return</span> Observable.just(<span class="keyword">new</span> ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, <span class="keyword">new</span> Intent()));</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> startLoginProcess();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样一来,检测用户是否登录,在 <strong>用户已经登录</strong> 和 <strong>用户一开始没登录但是通过登录流程以后登录成功</strong> 的情况下,执行点赞操作的代码就变成了下面这样:</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">likeBtn.setOnClickListener(v -> {</span><br><span class="line"> loginState()</span><br><span class="line"> .subscribe(<span class="keyword">this</span>::doLike);</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>虽然整体上代码量并没有变少,但是逻辑上更加清晰了,<code>loginState</code> 这个方法也更容易被其他地方所复用了。</p><p>接下来的部分是 承担着 <code>onActivityResult</code> 这个方法的观察者 的责任的 Fragment,根据刚刚的讨论结果,这个 Fragment 需要提供一个方法,允许除了在它自己得到 <code>onActivityResult</code> 回调时实例化一个 <code>ActivityResult</code> 这种情况以外,也可以手动插入一个 <code>ActivityResult</code> 对象,这样的封装可以在应对 <strong>条件检测 - 发起流程</strong> 这类情景时,对外部有更加简洁和一致的接口。具体代码如下:</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></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">ActivityResultFragment</span> <span class="keyword">extends</span> <span class="title">Fragment</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> BehaviorSubject<ActivityResult> mActivityResultSubject = BehaviorSubject.create();</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">onActivityResult</span><span class="params">(<span class="keyword">int</span> requestCode, <span class="keyword">int</span> resultCode, Intent data)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onActivityResult(requestCode, resultCode, data);</span><br><span class="line"> mActivityResultSubject.onNext(<span class="keyword">new</span> ActivityResult(requestCode, resultCode, data));</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> Observable<ActivityResult> <span class="title">getActivityResultObservable</span><span class="params">(Activity activity)</span> </span>{</span><br><span class="line"> FragmentManager fragmentManager = activity.getFragmentManager();</span><br><span class="line"> ActivityResultFragment fragment = (ActivityResultFragment) fragmentManager.findFragmentByTag(</span><br><span class="line"> ActivityResultFragment.class.getCanonicalName());</span><br><span class="line"> <span class="keyword">if</span> (fragment == <span class="keyword">null</span>) {</span><br><span class="line"> fragment = <span class="keyword">new</span> ActivityResultFragment();</span><br><span class="line"> fragmentManager.beginTransaction()</span><br><span class="line"> .add(fragment, ActivityResultFragment.class.getCanonicalName())</span><br><span class="line"> .commit();</span><br><span class="line"> fragmentManager.executePendingTransactions();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> fragment.mActivityResultSubject;</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">startActivityForResult</span><span class="params">(Activity activity, Intent intent, <span class="keyword">int</span> requestCode)</span> </span>{</span><br><span class="line"> FragmentManager fragmentManager = activity.getFragmentManager();</span><br><span class="line"> ActivityResultFragment fragment = (ActivityResultFragment) fragmentManager.findFragmentByTag(</span><br><span class="line"> ActivityResultFragment.class.getCanonicalName());</span><br><span class="line"> <span class="keyword">if</span> (fragment == <span class="keyword">null</span>) {</span><br><span class="line"> fragment = <span class="keyword">new</span> ActivityResultFragment();</span><br><span class="line"> fragmentManager.beginTransaction()</span><br><span class="line"> .add(fragment, ActivityResultFragment.class.getCanonicalName())</span><br><span class="line"> .commit();</span><br><span class="line"> fragmentManager.executePendingTransactions();</span><br><span class="line"> }</span><br><span class="line"> fragment.startActivityForResult(intent, requestCode);</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">insertActivityResult</span><span class="params">(Activity activity, ActivityResult activityResult)</span> </span>{</span><br><span class="line"> FragmentManager fragmentManager = activity.getFragmentManager();</span><br><span class="line"> ActivityResultFragment fragment= (ActivityResultFragment) fragmentManager.findFragmentByTag(</span><br><span class="line"> ActivityResultFragment.class.getCanonicalName());</span><br><span class="line"> <span class="keyword">if</span> (fragment == <span class="keyword">null</span>) {</span><br><span class="line"> fragment = <span class="keyword">new</span> ActivityResultFragment();</span><br><span class="line"> fragmentManager.beginTransaction()</span><br><span class="line"> .add(fragment, ActivityResultFragment.class.getCanonicalName())</span><br><span class="line"> .commit();</span><br><span class="line"> fragmentManager.executePendingTransactions();</span><br><span class="line"> }</span><br><span class="line"> fragment.mActivityResultSubject.onNext(activityResult);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 <code>ActivityResultFragment</code> 这个类中:</p><ol><li><p><code>mActivityResultSubject</code> 即为发射 <code>ActivityResult</code> 的 Observable ;</p></li><li><p><code>getActivityResultObservable</code> 这个方法是用于在 Activity 中获取不可见 Fragment 对应的 <code>Observable<ActivityResult></code>,借鉴了 Glide 的思想;</p></li><li><p><code>onActivityResult</code> 这个方法里,Fragment 把自己接收到的数据封装为 <code>ActivityResult</code> 传递给 <code>mActivityResultSubject</code>;</p></li><li><p><code>startActivityForResult</code> 这个方法是用来被 Activity 调用的,Activity 把本来应该由自己发起的 <code>startActivityForResult</code> 交给由这个 Fragment 来发起;</p></li><li><p><code>insertActivityResult</code> 这个方法的作用前面解释过了,是为了给调用流程的调用者,提供一致的接口,主要优化 <strong>条件检测 - 发起流程</strong> 这种情景。</p></li></ol><h2 id="可复用流程的封装"><a href="#可复用流程的封装" class="headerlink" title="可复用流程的封装"></a>可复用流程的封装</h2><p>到目前为止,基于 RxJava,需要封装流程所需的基础设施已经准备完毕。我们来尝试封装一个流程,以登录流程为例,按上一篇讨论的结果,登录流程可能包含多个页面(用户名、密码验证,手机验证码两步验证等),也可能有子流程(忘记密码),但是对于“登录”这个流程,它对外只暴露一个代表它这个流程的 Activity,无论它内部跳转多么复杂,外部与这个登录流程交互也非常简单,只需要通过 <code>startActivityForResult</code> 和 <code>onActivityResult</code> 这两个方法。而这两个方法在上一节已经可以被很方便的封装,我们以登录流程为例,假设登录流程只对外暴露 <code>LoginActivity</code> 这一个 Activity,代码如下:</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></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">LoginActivity</span> <span class="keyword">extends</span> <span class="title">Activity</span> </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">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// your other code</span></span><br><span class="line"></span><br><span class="line"> loginBtn.setOnClickListener(v -> {</span><br><span class="line"> <span class="comment">// 简单起见,略去请求部分,直接登录成功</span></span><br><span class="line"> <span class="keyword">this</span>.setResult(RESULT_OK);</span><br><span class="line"> finish();</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">public</span> <span class="keyword">static</span> Observable<ActivityResult> <span class="title">loginState</span><span class="params">(Activity activity)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> ActivityResultFragment.insertActivityResult(</span><br><span class="line"> activity,</span><br><span class="line"> <span class="keyword">new</span> ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, <span class="keyword">new</span> Intent())</span><br><span class="line"> );</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(activity, LoginActivity.class);</span><br><span class="line"> ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ActivityResultFragment.getActivityResultObservable(activity)</span><br><span class="line"> .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)</span><br><span class="line"> .filter(ar -> ar.getResultCode() == RESULT_OK);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个 Activity 对外提供一个静态的 <code>loginState</code> 方法,返回类型为 <code>Observable<ActivityResult></code>,在已经登录的情况下,Observable 会立即发送一个 <code>ActivityResult</code> 表示登录成功,在非登录态下, 会唤起登录流程,如果登录流程最后的结果是登录成功,Observable 也会发送一个 <code>ActivityResult</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">likeBtn.setOnClickListener(v -> {</span><br><span class="line"> LoginActivity.loginState(<span class="keyword">this</span>)</span><br><span class="line"> .subscribe(<span class="keyword">this</span>::doLike);</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p>原先需要书写复杂的 <code>startActivityForResult</code> 和 <code>onActivityResult</code> 两个方法,才能完成和登录流程交互,而且还需要在发起流程前先确认是否当前已经是登录态,现在只需要一行 <code>LoginActivity.loginState()</code>, 然后指定一个 Observer 即可达到一样的效果,更重要的是,写法变简单了以后,整个登录流程变得非常容易复用,任何需要检查登录态然后再做操作的地方,都只需要这一行代码即可完成登录态检测,实现了 <strong>流程的高度可复用</strong>。 </p><p>这种写法可以在流程完成以后,继续之前的操作(例如本例中点赞),不需要用户重新进行一遍先前被流程所打断的操作(例如本例中的点赞操作)。但是细心的你可能并不这么认为,因为上面的代码本质上是有问题的,问题在于在上面 <code>LoginActivity.loginState()</code> 的调用在 <code>likeBtn.setOnClickListener</code> 的内部回调,那么考虑极端情况,如果登录流程被唤起,而发起登录流程的 Activity 不幸被系统回收,那么当登录流程做完回到的发起登录流程的 Activity 将会是系统重新创建的 Activity,这个全新的 Activity 是没有执行过 <code>likeBtn.setOnClickListener</code> 的内部回调的任何代码的,所以 <code>.subscribe()</code> 方法指定的观察者不会受到任何回调,<code>this::doLike</code> 不会被执行。</p><p>为了可以让封装的流程兼容这种情况,可以采用这种方案:修改 <code>loginState</code> 方法,使其返回 <a href="http://reactivex.io/RxJava/javadoc/io/reactivex/ObservableTransformer.html" target="_blank" rel="noopener">ObservableTransformer</a>, 我们重命名 <code>loginState</code> 方法为 <code>ensureLogin</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ObservableTransformer<T, ActivityResult> <span class="title">ensureLogin</span><span class="params">(Activity activity)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> {</span><br><span class="line"> Observable<ActivityResult> loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)</span><br><span class="line"> .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)</span><br><span class="line"> .filter(ar -> ar.getResultCode() == RESULT_OK);</span><br><span class="line"></span><br><span class="line"> upstream.subscribe(t -> {</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> ActivityResultFragment.insertActivityResult(</span><br><span class="line"> activity,</span><br><span class="line"> <span class="keyword">new</span> ActivityResult(REQUEST_CODE_LOGIN, RESULT_OK, <span class="keyword">new</span> Intent())</span><br><span class="line"> );</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(activity, LoginActivity.class);</span><br><span class="line"> ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);</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> loginOkResult;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>如果您之前没有接触过 <code>ObservableTransformer</code>, 这里做一个简单介绍,它通常和 <code>compose</code> 操作符一起使用,用来把一个 <code>Observable</code> 进行加工、修饰,甚至替换为另一个 <code>Observable</code>。</p></blockquote><p>登录流程的封装,现在对外体现为 <code>ensureLogin</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></pre></td><td class="code"><pre><span class="line">RxView.clicks(likeBtn)</span><br><span class="line"> .compose(LoginActivity.ensureLogin(<span class="keyword">this</span>))</span><br><span class="line"> .subscribe(<span class="keyword">this</span>::doLike);</span><br></pre></td></tr></table></figure><p>这里的 <code>RxView.clicks</code> 使用了 <a href="https://github.com/JakeWharton/RxBinding" target="_blank" rel="noopener">RxBinding</a> 这个开源库,用于把 View 的事件,转化为 <code>Observable</code>,当然其实你也可以自己封装。改完这种写法以后,刚刚提到的极端情况下也可以正常工作了,即使发起流程的页面在流程被唤起后被系统回收,在流程完成以后回到发起页,发起页被重新创建了,发起页的 <code>Observer</code> 依然可以正常收到流程结果,之前被中端的操作得以继续执行。</p><p>现在我们可以稍微总结一下,根据上一篇和本篇提出建议,如何封装一个业务流程:</p><ol><li>一个业务流程对应一个 Activity,这个 Activity 作为对外的接口以及流程内部步骤的调度者;</li><li>一个流程内部的一个步骤对应一个 Fragment,这个 Fragment 只负责完成自己的任务以及把自己的数据反馈给 Activity;</li><li>流程对外暴露的接口应该封装为一个 <code>ObservableTransformer</code>,流程发起者应该提供发起流程的 <code>Observable</code>(例如以 <code>RxView.clicks</code> 的形式提供),两者通过 <code>compose</code> 操作符关联起来。</li></ol><p>这是我个人实践出的一套封装流程的经验,它并不是完美的方案,但是在可靠性、可复用程度、接口简单程度上已经足以胜任我个人的日常开发,所以才有了这两篇分享。</p><p>我们已经封装了一个最简单的流程 – 登录流程,但是实际项目中往往会遇到更严峻的挑战,例如流程组合与流程嵌套。</p><h2 id="复杂流程实践:流程组合"><a href="#复杂流程实践:流程组合" class="headerlink" title="复杂流程实践:流程组合"></a>复杂流程实践:流程组合</h2><p>举例:某款基金销售 App,在用户点击购买基金时,可能存在如下图流程:<br><img src="/images/process-combine-1.png" alt=""></p><p>可用从上图中看出,某个未登录用户想要购买一款基金的最长路径包含:<strong>登录 - 绑卡 - 风险测评 - 投资者适当性管理</strong> 这几个步骤。但是,并不是所有用户都要经历所有这些步骤,例如,如果用户已登录并且已做过风险测评,那这个用户只需要再做 <strong>绑卡 - 适当性管理</strong> 这两步就可以了。</p><p>这样的一个需求,如果用传统的写法来写,可以预见肯定会在 click 事件处理的地方罗列很多 <code>if - else</code> :<br> <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></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">buyFundBtn.setOnClickListener(v -> handleBuyFund());</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">// 处理结果</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onActivityResult</span><span class="params">(<span class="keyword">int</span> requestCode, <span class="keyword">int</span> resultCode, Intent data)</span> </span>{</span><br><span class="line"> <span class="keyword">switch</span> (requestCode) {</span><br><span class="line"> <span class="keyword">case</span> REQUEST_LOGIN:</span><br><span class="line"> <span class="keyword">case</span> REQUEST_ADD_BANKCARD:</span><br><span class="line"> <span class="keyword">case</span> REQUEST_RISK_TEST:</span><br><span class="line"> <span class="keyword">case</span> REQUEST_INVESTMNET_PROMPT:</span><br><span class="line"> <span class="keyword">if</span> (resultCode == RESULT_OK) handleBuyFund(); </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"></span><br><span class="line"><span class="comment">// ...</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">handleBuyFund</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 判断是否已登录</span></span><br><span class="line"> <span class="keyword">if</span> (!isLogin()) {</span><br><span class="line"> startLogin();</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><span class="line"> <span class="keyword">if</span> (!hasBankcard()) {</span><br><span class="line"> startAddBankcard();</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><span class="line"> <span class="keyword">if</span> (!isRisktestDone()) {</span><br><span class="line"> startRiskTest();</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><span class="line"> <span class="keyword">if</span> (investmentPrompt()) {</span><br><span class="line"> startInvestmentPrompt();</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"> startBuyFundActivity();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>上面这种写法一方面代码比较长,另一方面,流程发起和结果处理分散在两处,代码较为不易维护。我们分析一下,整个大的流程是几个小流程的组合,我们可以把上面的图上的流程换一种画法:</p><p><img src="/images/process-combine-2.png" alt=""></p><p>按照上文的思想,我们令每个流程对外暴露一个 Activity,并且已经使用 RxJava <code>ObservableTransformer</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></pre></td><td class="code"><pre><span class="line">RxView.clicks(buyFundBtn)</span><br><span class="line"> <span class="comment">// 确保未登录情况下,发起登录流程,已登录情况下自动流转至下一个流程</span></span><br><span class="line"> .compose(ActivityLogin.ensureLogin(<span class="keyword">this</span>))</span><br><span class="line"> <span class="comment">// 确保未绑卡情况下,发起绑卡流程,已绑卡情况下自动流转至下一个流程</span></span><br><span class="line"> .compose(ActivityBankcardManage.ensureHavingBankcard(<span class="keyword">this</span>))</span><br><span class="line"> <span class="comment">// 确保未风险测评情况下,发起风险测评流程,已测评情况下自动流转至下一个流程</span></span><br><span class="line"> .compose(ActivityRiskTest.ensureRiskTestDone(<span class="keyword">this</span>))</span><br><span class="line"> <span class="comment">// 确保需要适当性提示情况下,发起适当性提示,已提示或不需要提示情况下自动流转至下一个流程</span></span><br><span class="line"> .compose(ActivityInvestmentPrompt.ensureInvestmentPromptOk(<span class="keyword">this</span>))</span><br><span class="line"> <span class="comment">// 所有条件都满足,进入购买基金页</span></span><br><span class="line"> .subscribe(v -> startBuyFundActivity(<span class="keyword">this</span>));</span><br></pre></td></tr></table></figure><p>通过 RxJava 的良好封装,我们做到了可以用更少的代码来表达更复杂的逻辑。上面的例子中的 4 个被组合的流程,它们有一个共同的特点,就是彼此独立,互相不依赖其它剩余流程的结果,现实中,我们可能会遇到这样的情况: B 流程启动,需要依赖 A 流程完成的结果,为了能满足这种情况,我们只需要对上面的封装稍作修改。</p><p>假设绑卡流程需要依赖登录流程完成后的用户信息,那么首先,在登录流程结束调用 <code>setResult</code> 的位置, 传递用户信息:<br><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">this</span>.setResult(</span><br><span class="line"> RESULT_OK, </span><br><span class="line"> IntentBuilder.newInstance().putExtra(<span class="string">"user"</span>, user).build()</span><br><span class="line">);</span><br><span class="line">finish();</span><br></pre></td></tr></table></figure></p><p>然后,修改 <code>ensureLogin</code> 方法,使经过 <code>ObservableTransformer</code> 处理后,返回的新的 <code>Observable</code> 由发射 <code>ActivityResult</code> 改为发射 <code>User</code>:<br><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="function"><span class="keyword">public</span> <span class="keyword">static</span> ObservableTransformer<T, User> <span class="title">ensureLogin</span><span class="params">(Activity activity)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> {</span><br><span class="line"> Observable<ActivityResult> loginOkResult = ActivityResultFragment.getActivityResultObservable(activity)</span><br><span class="line"> .filter(ar -> ar.getRequestCode() == REQUEST_CODE_LOGIN)</span><br><span class="line"> .filter(ar -> ar.getResultCode() == RESULT_OK)</span><br><span class="line"> .map(ar -> (User)ar.getData.getParcelableExtra(<span class="string">"user"</span>));</span><br><span class="line"></span><br><span class="line"> upstream.subscribe(t -> {</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> ActivityResultFragment.insertActivityResult(</span><br><span class="line"> activity,</span><br><span class="line"> <span class="keyword">new</span> ActivityResult(</span><br><span class="line"> REQUEST_CODE_LOGIN, </span><br><span class="line"> RESULT_OK, </span><br><span class="line"> IntentBuilder.newInstance().putExtra(<span class="string">"user"</span>, LoginInfo.getUser()).build()</span><br><span class="line"> )</span><br><span class="line"> );</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(activity, LoginActivity.class);</span><br><span class="line"> ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_CODE_LOGIN);</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> loginOkResult;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>与此同时,原来的 <code>ensureHavingBankcard</code> 方法的 <code>ObservableTransformer</code> 方法接受的 Observable 原来是任意类型 T 的,由于我们现在规定,绑卡流程需要依赖登录流程的结果 User ,所以我们把 T 类型,改为 User 类型:</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="function"><span class="keyword">public</span> <span class="keyword">static</span> ObservableTransformer<User, ActivityResult> <span class="title">ensureHavingBankcard</span><span class="params">(Activity activity)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> {</span><br><span class="line"> Observable<ActivityResult> bankcardOk = ActivityResultFragment.getActivityResultObservable(activity)</span><br><span class="line"> .filter(ar -> ar.getRequestCode() == REQUEST_ADD_BANKCARD)</span><br><span class="line"> .filter(ar -> ar.getResultCode() == RESULT_OK);</span><br><span class="line"></span><br><span class="line"> upstream.subscribe(user -> {</span><br><span class="line"> <span class="keyword">if</span> (getBankcardNum() > <span class="number">0</span>) {</span><br><span class="line"> ActivityResultFragment.insertActivityResult(</span><br><span class="line"> activity,</span><br><span class="line"> <span class="keyword">new</span> ActivityResult(</span><br><span class="line"> REQUEST_ADD_BANKCARD, </span><br><span class="line"> RESULT_OK, </span><br><span class="line"> <span class="keyword">new</span> Intent()</span><br><span class="line"> )</span><br><span class="line"> );</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(activity, AddBankcardActivity.class);</span><br><span class="line"> intent.putExtra(<span class="string">"user"</span>, user);</span><br><span class="line"> ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_ADD_BANKCARD);</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> bankcardOk;</span><br><span class="line"> }</span><br><span class="line">}</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></pre></td><td class="code"><pre><span class="line">RxView.clicks(someBtn)</span><br><span class="line"> .compose(ActivityLogin.ensureLogin(<span class="keyword">this</span>))</span><br><span class="line"> .compose(ActivityBankcardManage.ensureHavingBankcard(<span class="keyword">this</span>))</span><br><span class="line"> .subscribe(v -> doSomething());</span><br></pre></td></tr></table></figure><p>除此以外,绑卡流程还是可复用的,它是依赖可以返回 User 的流程的,所以只要是其他可以返回 User 作为结果的流程,都可以与绑卡流程组合。</p><h2 id="复杂流程实践:流程嵌套"><a href="#复杂流程实践:流程嵌套" class="headerlink" title="复杂流程实践:流程嵌套"></a>复杂流程实践:流程嵌套</h2><p>举例:登录流程中的登录页面,除了可以选择用户名密码登录外,往往还提供其他选项,最典型的就是注册和忘记密码两个功能:</p><p><img src="/images/process-insert-1.png" alt=""></p><p>从直觉上,我们肯定是认为注册和忘记密码应该是不属于登录这个流程的,它们是相对独立的两个流程,也就是说在登录这流程内部,嵌入了其它的流程,我把这种情况称之为流程的嵌套。</p><p>按照同样的套路,我们应该先把注册、忘记密码这两个流程使用 <code>ObservableTransformer</code> 进行封装,然后我们把上图流程按照本文思想整理一下,如下:</p><p><img src="/images/process-insert-2.png" alt=""></p><p>可以看到,现在的区别是,发起流程的地方不再是一个普通的 Activity,而是另一个流程中的某个步骤,按照先前的讨论,流程中的步骤是由 Fragment 承载的。所以这里有两种处理方法,一种是 Fragment 把发起流程的任务交给宿主 Activity,由宿主 Activity 分配给属于它的“看不见的 Fragment” 去发起流程并处理结果,另一种是直接由该 Fragmnet 发起流程,由于 Fragment 也有属于它自己的 ChildFragmentManager,所以只需要对“<strong>使用 RxJava 进行封装</strong>”这一节中的相关方法做一些修改即可支持由 Fragment 内部发起流程,具体修改内容为把 <code>activity.getFragmentManager()</code> 改为 <code>fragment.getChildFragmentManager()</code> 即可。</p><p>在具体应用中,本人使用的是后一种,即 <strong>直接由 Fragment 发起流程</strong>,因为被嵌套的流程往往和主流程有关联,即嵌套流程的结果有可能改变主流程的流转分支,所以直接由 Fragment 发起流程并处理结果比较方便一点,如果交给宿主 Activity 可能需要额外多写一些代码进行 Activity - Fragment 的通信才能实现相同效果。</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><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></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">LoginFragment</span> <span class="keyword">extends</span> <span class="title">Fragment</span> </span>{</span><br><span class="line"> <span class="comment">// UI references.</span></span><br><span class="line"> <span class="keyword">private</span> EditText mPhoneView;</span><br><span class="line"> <span class="keyword">private</span> EditText mPasswordView;</span><br><span class="line"></span><br><span class="line"> LoginCallback mCallback;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Nullable</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> View <span class="title">onCreateView</span><span class="params">(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, <span class="keyword">false</span>);</span><br><span class="line"> <span class="comment">// Set up the login form.</span></span><br><span class="line"> mPhoneView = view.findViewById(R.id.phone);</span><br><span class="line"> mPasswordView = view.findViewById(R.id.password);</span><br><span class="line"></span><br><span class="line"> Button signInButton = view.findViewById(R.id.sign_in);</span><br><span class="line"> signInButton.setOnClickListener(<span class="keyword">new</span> View.OnClickListener() {</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">onClick</span><span class="params">(View view)</span> </span>{</span><br><span class="line"> String phone = mPhoneView.getText().toString();</span><br><span class="line"> String pwd = mPasswordView.getText().toString();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mCallback != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// mock login ok</span></span><br><span class="line"> mCallback.onLoginOk(<span class="keyword">true</span>, <span class="keyword">new</span> User(</span><br><span class="line"> UUID.randomUUID().toString(),</span><br><span class="line"> <span class="string">"Jack"</span>,</span><br><span class="line"> mPhoneView.getText().toString()</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">return</span> view;</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">void</span> <span class="title">setLoginCallback</span><span class="params">(LoginCallback callback)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.mCallback = callback;</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">LoginCallback</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onLoginOk</span><span class="params">(<span class="keyword">boolean</span> needSmsVerify, User user)</span></span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的代码中,<code>LoginCallback</code> 这个接口作用是,登录这个步骤,收集完信息,与服务器交互完毕后,把结果回传给宿主 Activity,由 Activity 决定后续步骤的流转。上面的例子中做了一部分简化,在 <code>onClick</code> 处理函数里没有发起和服务端的交互,而是直接 Mock 了一个请求成功的结果。</p><p>现在的需求是,在登录这个步骤里,嵌入两个步骤:</p><ol><li>一个是注册流程,而且注册成功后直接视为登录成功,不需要再走剩余的登录流程步骤;</li><li>另一个是忘记密码流程,忘记密码流程本质是重置密码,但是即使密码重置成功还是需要用户使用新密码登录,不会直接在重置密码后自动登录。</li></ol><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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> View <span class="title">onCreateView</span><span class="params">(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> View view = inflater.inflate(R.layout.fragment_login_by_pwd, container, <span class="keyword">false</span>);</span><br><span class="line"> <span class="comment">// Set up the login form.</span></span><br><span class="line"> mPhoneView = view.findViewById(R.id.phone);</span><br><span class="line"> mPasswordView = view.findViewById(R.id.password);</span><br><span class="line"></span><br><span class="line"> Button signInButton = view.findViewById(R.id.sign_in);</span><br><span class="line"> Button mPwdResetBtn = view.findViewById(R.id.pwd_reset);</span><br><span class="line"> Button mRegisterBtn = view.findViewById(R.id.register);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 直接登录</span></span><br><span class="line"> signInButton.setOnClickListener(<span class="keyword">new</span> View.OnClickListener() {</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">onClick</span><span class="params">(View view)</span> </span>{</span><br><span class="line"> String phone = mPhoneView.getText().toString();</span><br><span class="line"> String pwd = mPasswordView.getText().toString();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mCallback != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// mock login ok</span></span><br><span class="line"> mCallback.onLoginOk(<span class="keyword">true</span>, <span class="keyword">new</span> User(</span><br><span class="line"> UUID.randomUUID().toString(),</span><br><span class="line"> <span class="string">"Jack"</span>,</span><br><span class="line"> mPhoneView.getText().toString()</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="comment">// 发起注册流程</span></span><br><span class="line"> RxView.clicks(mRegisterBtn)</span><br><span class="line"> .compose(RegisterActivity.startRegister(<span class="keyword">this</span>))</span><br><span class="line"> .subscribe(user -> {</span><br><span class="line"> <span class="keyword">if</span> (mCallback != <span class="keyword">null</span>) {</span><br><span class="line"> mCallback.onRegisterOk(user);</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"> RxView.clicks(mPwdResetBtn)</span><br><span class="line"> .compose(PwdResetActivity.startPwdReset(<span class="keyword">this</span>))</span><br><span class="line"> .subscribe();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> view;</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">LoginCallback</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onLoginOk</span><span class="params">(<span class="keyword">boolean</span> needSmsVerify, User user)</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onRegisterOk</span><span class="params">(User user)</span></span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的代码里,<code>RegisterActivity.startRegister</code> 和 <code>PwdResetActivity.startPwdReset</code> 两个方法即为使用了 <code>ObservableTransformer</code> 封装的注册流程和忘记密码流程。同时可以看到,<code>LoginCallback</code> 这个接口里多了一个方法 <code>onRegisterOk</code>,也就是说登录这个步骤不再只有 <code>onLoginOk</code> 这一种情况通知宿主 Activity 了,在内嵌注册流程成功的情况下,也可以通知宿主 Activity,然后让宿主 Activity 决定后续流转,当然这种情况,根据需求注册成功也是属于登录成功的一种,宿主 Activity 通过 <code>setResult</code> 方法把整个登录流程的状态标记为登录成功,<code>finish</code> 自己,同时把用户信息传递给发起登录流程的地方。</p><p>但是为什么内嵌的注册流程需要把流程的结果回传给登录流程的宿主 Activity,而内嵌的忘记密码流程没有设置一个类似的方法回调登录流程的宿主 Activity 呢?因为注册成功这件事影响了登录流程的走向(注册成功直接视为登录成功,登录流程状态置为成功,并通知发起登录流程的地方本次登录结果为成功),而忘记密码流程最后的重置密码成功并不影响登录流程走向(即使重置密码成功依然需要在登录界面使用新密码登录)。</p><p>按照上面的分析,登录流程的宿主 Activity,负责分发流程步骤的逻辑的相关代码如下所示:</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></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">LoginActivity</span> <span class="keyword">extends</span> <span class="title">Activity</span> </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 class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> <span class="comment">// 用户名密码验证步骤</span></span><br><span class="line"> loginFragment.setLoginCallback(<span class="keyword">new</span> LoginFragment.LoginCallback() {</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">onLoginOk</span><span class="params">(<span class="keyword">boolean</span> needSmsVerify, User user)</span> </span>{</span><br><span class="line"> <span class="comment">// 用户名密码验证成功</span></span><br><span class="line"> <span class="keyword">if</span> (needSmsVerify) {</span><br><span class="line"> <span class="comment">// 需要短信验证码的两步验证</span></span><br><span class="line"> push(loginBySmsFragment);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 登录成功</span></span><br><span class="line"> setResult(</span><br><span class="line"> RESULT_OK, </span><br><span class="line"> IntentBuilder.newInstance().putExtra(<span class="string">"user"</span>, user).build()</span><br><span class="line"> );</span><br><span class="line"> finish();</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">onRegisterOk</span><span class="params">(User user)</span> </span>{</span><br><span class="line"> <span class="comment">// 注册成功, 直接登录</span></span><br><span class="line"> setResult(</span><br><span class="line"> RESULT_OK, </span><br><span class="line"> IntentBuilder.newInstance().putExtra(<span class="string">"user"</span>, user).build()</span><br><span class="line"> );</span><br><span class="line"> finish();</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"> loginBySmsFragment.setSmsLoginCallback(<span class="keyword">new</span> LoginBySmsFragment.LoginBySmsCallback() {</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">onSmsVerifyOk</span><span class="params">(User user)</span> </span>{</span><br><span class="line"> <span class="comment">// 短信验证成功</span></span><br><span class="line"> setResult(</span><br><span class="line"> RESULT_OK, </span><br><span class="line"> IntentBuilder.newInstance().putExtra(<span class="string">"user"</span>, user).build()</span><br><span class="line"> );</span><br><span class="line"> finish();</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><br></pre></td></tr></table></figure><p>可以看到,即使是流程嵌套的情况下,使用 RxJava 封装的流程依然不会使流程跳转的代码显得十分混乱,这点十分可贵,因为这意味着今后流程相关代码不会成为项目中难以维护的模块,而是清晰且高内聚的。</p><h2 id="流程上下文保存"><a href="#流程上下文保存" class="headerlink" title="流程上下文保存"></a>流程上下文保存</h2><p>到目前为止,我们还剩最后一个问题需要解决,即涉及流程的相关上下文保存。具体包含两部分,一是流程触发点,发起流程的位置,需要对发起流程前的上下文进行保存,另一部分是流程中间步骤的结果,也需要进行保存。</p><p><strong>1. 流程中间步骤的结果的保存</strong></p><p>要对流程中间步骤的结果进行保存是因为,按照我们前面的讨论,流程中每个步骤(即Fragment),会和用户交互,然后把该步骤的结果传递给宿主 Activity,那么假设流程没有做完,并且该步骤的结果可能会被后续步骤使用,那么宿主 Activity 是有必要保存这个结果的,那么通常这个结果会以这个 Activity 的成员变量的形式被保存,问题在于 Activity 一旦被置于后台,就随时可能被系统回收,此时可能流程并没有做完,如果没有对 Activity 的成员变量做保存和恢复处理,当下次 Activity 回到前台以后,这个流程的状态就处于不确定状态了,甚至可能崩溃。</p><p>解决的方案很显然,继承 Activity 的 <code>onSaveInstanceState</code> 和 <code>onRestoreInstanceState</code>(或者 <code>onCreate</code>) 这两个方法,在这两个方法内部实现变量的保存与恢复操作。如果你觉得实现这两个方法会使你的代码非常丑陋,那么我推荐你使用 <a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">SaveState</a> 这个工具,使用它,你只需要在需要保存和恢复的成员变量上标记一个 <code>@AutoRestore</code> 注解,框架就会自动帮你保存和恢复成员变量,你不需要写任何额外的代码。</p><p><strong>2. 发起流程前的上下文的保存</strong></p><p>和 <strong>1</strong> 的原因类似,流程一旦被唤起,发起流程的 Activity 就处于后台状态,这是一种可能被系统回收的状态。举个例子, 有一个理财产品列表页,用户未登录状态,现在要求用户点击任何一个理财产品,先把用户带到登录界面,待登录流程完成后,把用户带到具体的理财产品购买页。列表的点击事件设置一般分两种,一是为列表中每个 Item 设置一个点击处理函数,另一种是为所有 Item 设置同一个点击处理函数。以给列表所有 Item 设置同一个点击处理函数为例:</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><br><span class="line"><span class="comment">// 所有 Item 的点击事件对应的 Observable,其发射的元素为点击位置</span></span><br><span class="line">Observable<Integer> itemClicks = ...</span><br><span class="line"></span><br><span class="line">itemClicks</span><br><span class="line"> .compose(LoginActivity.ensureLogin(activity))</span><br><span class="line"> .subscribe(<span class="comment">/** 不知道怎么写 **/</span>);</span><br></pre></td></tr></table></figure><p><code>subscribe</code> 里面的观察者不知道怎么写了是因为 <code>LoginActivity.ensureLogin</code> 这个 <code>ObservableTransformer</code> 会把 <code>Observable<T></code> 转为 <code>Observable<ActivityResult></code>, 所以观察者里只知道登录成功了,不知道最初是点击哪个理财产品触发的登录操作,所以不知道应该如何去启动购买页面。</p><p>我们遇到的困境是当流程完成以后,我们不知道发起流程前的上下文是什么,导致我们无法在观察者里做正确的后续逻辑。一种直观的解决方案就是,我们把发起流程时的上下文数据打包进 <code>startActivityForResult</code> 的 Intent 里面,用一个保留的 Key 值去存储,同时确保流程完成时, <code>setResult</code> 调用时,会把刚刚流程传入的上下文数据,同样以一个保留的 Key 值回传给发起流程的地方。</p><p>如果这样处理以后,我们回过头看刚才的情况,我们再实现一个 <code>LoginActivity.ensureLoginWithContext</code> 方法,它的返回值为 <code>ObservableTransformer<Bundle, Bundle></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="function"><span class="keyword">public</span> <span class="keyword">static</span> ObservableTransformer<Bundle, Bundle> <span class="title">ensureLoginWithContext</span><span class="params">(AppCompatActivity activity)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> upstream -> {</span><br><span class="line"> upstream.subscribe(contextData -> {</span><br><span class="line"> <span class="keyword">if</span> (LoginInfo.isLogin()) {</span><br><span class="line"> ActivityResultFragment.insertActivityResult(</span><br><span class="line"> activity,</span><br><span class="line"> <span class="keyword">new</span> ActivityResult(REQUEST_LOGIN, RESULT_OK, <span class="keyword">null</span>, contextData)</span><br><span class="line"> );</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent(activity, LoginActivity.class);</span><br><span class="line"> ActivityResultFragment.startActivityForResult(activity, intent, REQUEST_LOGIN, contextData);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> ActivityResultFragment.getActivityResultObservable(activity)</span><br><span class="line"> .filter(ar -> ar.getRequestCode() == REQUEST_LOGIN)</span><br><span class="line"> .filter(ar -> ar.getResultCode() == RESULT_OK)</span><br><span class="line"> .map(ActivityResult::getRequestContextData);</span><br><span class="line"> };</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面的代码中的 <code>ensureLoginWithContext</code> 和原先的 <code>ensureLogin</code> 方法相比,除了返回值的泛型类型不同以外,在内部实现里,调用的 <code>ActivityResult</code> 的构造方法以及 <code>startActivityForResult</code> 方法和原先的版本比都多了一个 <code>Bundle</code> 类型的 <code>contextData</code> 参数,这个参数即为需要保存的流程发起前的上下文。最后看整个方法的 return 语句,多了一个 <code>map</code> 操作符,用来把 <code>ActivityResult</code> 里保存的流程的上下文重新取出来。这里的逻辑就是刚刚提到的:在流程发起前,将流程发起前的上下文信息通过 <code>Bundle</code> 传递给流程,最后流程结束时再原封不动返回给流程发起的地方,以便流程发起点可以知晓它发起流程前的状态。这几个方法的具体实现可以参考 <a href="https://github.com/PrototypeZ/Sq" target="_blank" rel="noopener">Sq</a> 这个框架。</p><p>在经过这样处理以后,列表 Item 的点击事件发起登录流程的代码如下所示:<br><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">itemClicks</span><br><span class="line"> .map(index -> BundleBuilder.newInstance().putInt(<span class="string">"index"</span>, index).build())</span><br><span class="line"> .compose(LoginActivity.ensureLoginWithContext(<span class="keyword">this</span>))</span><br><span class="line"> .map(bundle -> bundle.getInt(<span class="string">"index"</span>))</span><br><span class="line"> .subscribe(index -> {</span><br><span class="line"> <span class="comment">// modification of item in position $index</span></span><br><span class="line"> adapter.notifyItemChanged(index);</span><br><span class="line"> });</span><br></pre></td></tr></table></figure></p><p>在 <code>compose</code> 操作符前后,分别多了一个 <code>map</code> 操作符,分别负责把上下文打包以及从流程结果中把原来的上下文解包取出来。</p><p>流程上下文的保存还有一个注意点,就是流程在结束时,即调用 <code>setResult</code> 时,需要保证把先前传入的的上下文再塞回去到结果里去,只有做到了这点,上面的代码才是有效的,这些工作如果总是手动来做会很繁琐,您可以选择自己封装,或者直接使用下一节介绍的开箱即用的工具。</p><h2 id="如何快速使用"><a href="#如何快速使用" class="headerlink" title="如何快速使用"></a>如何快速使用</h2><p>到这里为止,对于封装业务流程相关所有经验分享已经介绍完毕,如果您看到这里,对于本文以及本文的上一篇提出的流程方案感兴趣,您有两种方法集成到自己的项目里,一是参照文中的代码,自己实现(核心代码都已在文中,稍作修改即可); 另一种方法是直接使用封装好的版本,这个项目的名字是 <a href="https://github.com/PrototypeZ/Sq" target="_blank" rel="noopener">Sq</a>, 您只需要把依赖添加到 Gradle,开箱即用。</p><p><a href="https://github.com/PrototypeZ/Sq" target="_blank" rel="noopener"><img src="https://raw.githubusercontent.com/PrototypeZ/Sq/master/sq-logo.png" alt=""></a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>文章很长,感谢您耐心读完。由于本人能力有限,文章可能存在纰漏的地方,欢迎各位指正。关于如何对业务流程进行封装,因为我并没有看到过很多技术文章对这一块进行讨论,所以我个人的见解会有不全面的地方,如果您有更好的方案,欢迎一起讨论。谢谢大家!</p>]]></content>
<summary type="html">
<p>这是关于如何在 Android 中封装业务流程经验分享的第二篇,第一篇在<a href="http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/">这里</a>。所谓 <strong>业务流程</strong> ,指的是一系列页面的集合,这些页面肩负着一个特定职责,负责和用户交互,从用户端收集信息。业务流程有时候由用户主动触发,而有时候是由于某些条件不满足而触发,当流程完成以后,有时候只是简单地回到发起流程的页面,用流程的结果更新那个页面;而有时候是继续之前 <strong>由于触发流程而中断</strong> 的操作;还有些时候是则是转入新的流程。</p>
</summary>
<category term="Android 业务流程" scheme="http://prototypez.github.io/tags/Android-%E4%B8%9A%E5%8A%A1%E6%B5%81%E7%A8%8B/"/>
</entry>
<entry>
<title>不需要再手写 onSaveInstanceState 了,因为你的时间非常值钱</title>
<link href="http://prototypez.github.io/2018/06/06/using-save-state/"/>
<id>http://prototypez.github.io/2018/06/06/using-save-state/</id>
<published>2018-06-05T16:00:00.000Z</published>
<updated>2018-09-02T07:31:22.852Z</updated>
<content type="html"><![CDATA[<p>如果你是一个有经验的 Android 程序员,那么你肯定手写过许多 <code>onSaveInstanceState</code> 以及 <code>onRestoreInstanceState</code> 方法用来保持 Activity 的状态,因为 Activity 在变为不可见以后,系统随时可能把它回收用来释放内存。<strong>重写 Activity 中的 <code>onSaveInstanceState</code> 方法</strong> 是 Google 推荐的用来保持 Activity 状态的做法。<br><a id="more"></a></p><h2 id="Google-推荐的最佳实践"><a href="#Google-推荐的最佳实践" class="headerlink" title="Google 推荐的最佳实践"></a>Google 推荐的最佳实践</h2><p><code>onSaveInstanceState</code> 方法会提供给我们一个 <code>Bundle</code> 对象用来保存我们想保存的值,但是 <code>Bundle</code> 存储是基于 key - value 这样一个形式,所以我们需要定义一些额外的 <code>String</code> 类型的 key 常量,最后我们的项目中会充斥着这样代码:</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="keyword">static</span> <span class="keyword">final</span> String STATE_SCORE = <span class="string">"playerScore"</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> String STATE_LEVEL = <span class="string">"playerLevel"</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="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onSaveInstanceState</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// Save the user's current game state</span></span><br><span class="line"> savedInstanceState.putInt(STATE_SCORE, mCurrentScore);</span><br><span class="line"> savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Always call the superclass so it can save the view hierarchy state</span></span><br><span class="line"> <span class="keyword">super</span>.onSaveInstanceState(savedInstanceState);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>保存完状态之后,为了能在系统重新实例化这个 Activity 的时候恢复先前被系统杀死前的状态,我们在 <code>onCreate</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="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState); <span class="comment">// Always call the superclass first</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Check whether we're recreating a previously destroyed instance</span></span><br><span class="line"> <span class="keyword">if</span> (savedInstanceState != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// Restore value of members from saved state</span></span><br><span class="line"> mCurrentScore = savedInstanceState.getInt(STATE_SCORE);</span><br><span class="line"> mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Probably initialize members with default values for a new instance</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>当然,恢复这个操作也可以在 <code>onRestoreInstanceState</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onRestoreInstanceState</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// Always call the superclass so it can restore the view hierarchy</span></span><br><span class="line"> <span class="keyword">super</span>.onRestoreInstanceState(savedInstanceState);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Restore state members from saved instance</span></span><br><span class="line"> mCurrentScore = savedInstanceState.getInt(STATE_SCORE);</span><br><span class="line"> mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="解放你的双手"><a href="#解放你的双手" class="headerlink" title="解放你的双手"></a>解放你的双手</h2><p>上面的方案当然是正确的。但是并不优雅,为了保持变量的值,引入了两个方法 ( <code>onSaveInstanceState</code> 和 <code>onRestoreInstanceState</code> ) 和两个常量 ( 为了存储两个变量而定义的两个常量,仅仅为了放到 <code>Bundle</code> 里面)。</p><p>为了更好地解决这个问题,我写了 <a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">SaveState</a> 这个插件:</p><p><img src="https://raw.githubusercontent.com/PrototypeZ/SaveState/master/logo.png" alt="save-state-logo"></p><p>在使用了 <a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">SaveState</a> 这个插件以后,保持 Activity 的状态的写法如下:</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="class"><span class="keyword">class</span> <span class="title">MyActivity</span> <span class="keyword">extends</span> <span class="title">Activity</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> <span class="keyword">int</span> myInt;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> IBinder myRpcCall;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> String result;</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">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// Your code here</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>没错,你只需要在需要保持的变量上标记 <code>@AutoRestore</code> 注解即可,无需去管那几个烦人的 Activity 回调,也不需要定义多余的 <code>String</code> 类型 key 常量。</p><p>那么,除了 Activity 以外,Fragment 能自动保持状态吗?答案是: Yes!</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyFragment</span> <span class="keyword">extends</span> <span class="title">Fragment</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> User currentLoginUser;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> List<Map<String, Object>> networkResponse;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Nullable</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> View <span class="title">onCreateView</span><span class="params">(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="comment">// Your code here</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用方法和 Activity 一模一样!不止如此,使用场景还可以推广到 <code>View</code>, 从此,你的自定义 View,也可以把状态保持这个任务交给 <a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">SaveState</a> :</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyView</span> <span class="keyword">extends</span> <span class="title">View</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> String someText;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> Size size;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@AutoRestore</span></span><br><span class="line"> <span class="keyword">float</span>[] myFloatArray;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">MainView</span><span class="params">(Context context)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context);</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">MainView</span><span class="params">(Context context, @Nullable AttributeSet attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(context, attrs);</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">MainView</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">super</span>(context, attrs, defStyleAttr);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="现在就使用-SaveState"><a href="#现在就使用-SaveState" class="headerlink" title="现在就使用 SaveState"></a>现在就使用 SaveState</h2><p>引入 <a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">SaveState</a> 的方法也十分简单:</p><p>首先,在项目根目录的 <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><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">buildscript {</span><br><span class="line"></span><br><span class="line"> repositories {</span><br><span class="line"> google()</span><br><span class="line"> jcenter()</span><br><span class="line"> }</span><br><span class="line"> dependencies {</span><br><span class="line"> <span class="comment">// your other dependencies</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// dependency for save-state</span></span><br><span class="line"> classpath <span class="string">"io.github.prototypez:save-state:${latest_version}"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后,在 <strong>application</strong> 和 <strong>library</strong> 模块的 <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></pre></td><td class="code"><pre><span class="line">apply <span class="string">plugin:</span> <span class="string">'com.android.application'</span></span><br><span class="line"><span class="comment">// apply plugin: 'com.android.library'</span></span><br><span class="line">apply <span class="string">plugin:</span> <span class="string">'save.state'</span></span><br></pre></td></tr></table></figure><p>万事具备!再也不需要写烦人的回调,因为你的时间非常值钱!做了一点微小的工作,如果我帮你节省下来了喝一杯咖啡的时间,希望你可以帮我点一个 Star,谢谢 :)</p><p>SaveState Github 地址:<a href="https://github.com/PrototypeZ/SaveState" target="_blank" rel="noopener">https://github.com/PrototypeZ/SaveState</a></p>]]></content>
<summary type="html">
<p>如果你是一个有经验的 Android 程序员,那么你肯定手写过许多 <code>onSaveInstanceState</code> 以及 <code>onRestoreInstanceState</code> 方法用来保持 Activity 的状态,因为 Activity 在变为不可见以后,系统随时可能把它回收用来释放内存。<strong>重写 Activity 中的 <code>onSaveInstanceState</code> 方法</strong> 是 Google 推荐的用来保持 Activity 状态的做法。<br>
</summary>
<category term="Android onSaveInstanceState" scheme="http://prototypez.github.io/tags/Android-onSaveInstanceState/"/>
</entry>
<entry>
<title>如何优雅地构建易维护、可复用的 Android 业务流程</title>
<link href="http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/"/>
<id>http://prototypez.github.io/2018/04/30/best-practices-of-android-process-management/</id>
<published>2018-04-29T16:00:01.000Z</published>
<updated>2018-09-02T07:30:39.794Z</updated>
<content type="html"><![CDATA[<p>有一定实际 Android 项目开发经验的人,一定曾经在项目中处理过很多重复的业务流程。例如开发一个社交 App ,那么出于用户体验考虑,会需要允许匿名用户(不登录的用户)可以浏览信息流的内容(或者只能浏览受限的内容),当用户想要进一步操作(例如点赞)时,提示用户需要登录或者注册,用户完成这个流程才可以继续刚刚的操作。而如果用户需要进行更深入的互动(例如评论,发布状态),则需要实名认证或者补充手机号这样的流程完成才可以继续操作。</p><a id="more"></a><p>而上面列举的还只是比较简单的情况,流程之间还可以互相组合。例如:匿名用户点击了评论,那么需要连续做完:</p><ol><li>登录/注册</li><li>实名认证</li></ol><p>这两个流程才可以继续评论某条信息。另外 1 中,登录流程还可能嵌套“忘记密码”或者“密码找回”这样的流程,也有可能因为服务端检测到用户异地登录插入一个两步验证/手机号验证流程。</p><h2 id="需要解决的问题"><a href="#需要解决的问题" class="headerlink" title="需要解决的问题"></a>需要解决的问题</h2><p><strong>(一) 流程的体验应当流畅</strong></p><p>根据本人使用市面上 App 的经验,处理业务流程按体验分类可以分为两类,一种是触发流程完成后,回到原页面,没有任何反应,用户需要再点一下刚才的按钮,或者重新操作一遍刚才触发流程的行为,才能进行原来想要的操作。另外一种是,流程完成后,如果之前不满足的某些条件此时已经满足,那么自动帮用户继续刚刚被打断的操作。显然,后一种更符合用户的预期,如果我们需要开发一个新的流程框架,那么这个问题需要被解决。</p><p><strong>(二) 流程需要支持嵌套</strong></p><p>如果在进行一个流程的过程中,某些条件不满足,需要触发一个新的流程,应当可以启动那个流程,完成操作,并且返回继续当前流程。</p><p><strong>(三) 流程步骤间数据传递应当简单</strong></p><p>传统 Activity 之间数据传递是基于 Intent 的,所以数据类型需要支持 <code>Parcelable</code> 或者 <code>Serializable</code> ,并且需要以 <code>key-value</code> 的方式往 Intent 内填充,这是有一定局限性的。此外,流程步骤间有些数据是共享的,有些是独有的,如何方便地去读写这些数据?</p><blockquote><p>有人可能会说,那可以把这些数据放到一个公共的空间,想要读写这些数据的 Activity 自行访问这些数据。但是如果真的这样,带来的新问题是:应用进程是可能在任意时刻销毁重建的,重建以后内存中保存的这些数据也消失了。如果不希望看到这样,就需要考虑数据持久化,而持久化的数据也只是被这一次流程用到,何时应该销毁这些数据?持久化的数据需要考虑自身的生命周期的问题,这引入了额外的复杂度。且并没有比使用 Intent 传递方便多少。</p></blockquote><p><strong>(四) 流程需要适应 Android 组件生命周期</strong></p><p>前面说到了应用进程销毁重建的问题,由于很多操作触发流程以后,启动的流程页面是基于 Activity 实现的,所以完成流程回到的 Activity 实例很有可能不是原来触发流程时的那个 Activity 实例,原来那个实例可能已经被销毁了,必须有合适的手段保证流程完成后,回到触发流程的页面可以正确恢复上下文。</p><p><strong>(五) 流程需要可以简单复用</strong></p><p>还有流程往往是可以复用的,例如登录流程可以在应用的很多地方触发,所以触发后流程结束以后的跳转页面也都是不一样的,不可以在流程结束的页面写死跳转的页面。</p><p><strong>(六) 流程页面在完成后需要比较容易销毁</strong></p><p>流程结束以后,流程每个步骤页面可以简单地销毁,回到最初触发流程的界面。</p><p><strong>(七) 流程进行中回退行为的处理</strong></p><p>如果一个流程包含多个中间步骤,用户进行到中间某个步骤,按返回键时,行为应该如何定义?在大多数情况下,应该支持返回上一个步骤,但是在某些情况下,也应当支持直接返回到流程起始步骤。</p><h2 id="方案一:基于-startActivityForResult"><a href="#方案一:基于-startActivityForResult" class="headerlink" title="方案一:基于 startActivityForResult"></a>方案一:基于 startActivityForResult</h2><p>其实说起流程这个事情,我们最容易想到的应该就是 Android 原生提供给我们的 <a href="https://developer.android.com/reference/android/app/Activity.html?hl=zh-tw#startActivity(android.content.Intent%29" target="_blank" rel="noopener">startActivityForResult</a> 方法,以 Android 官网中的一个<a href="https://developer.android.com/training/basics/intents/result?hl=zh-cn" target="_blank" rel="noopener">例子</a>(从通讯录中选择一个联系人)为例:</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="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> PICK_CONTACT_REQUEST = <span class="number">1</span>; <span class="comment">// The request code</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">pickContact</span><span class="params">()</span> </span>{</span><br><span class="line"> Intent pickContactIntent = <span class="keyword">new</span> Intent(Intent.ACTION_PICK, Uri.parse(<span class="string">"content://contacts"</span>));</span><br><span class="line"> pickContactIntent.setType(Phone.CONTENT_TYPE); <span class="comment">// Show user only contacts w/ phone numbers</span></span><br><span class="line"> startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);</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">protected</span> <span class="keyword">void</span> <span class="title">onActivityResult</span><span class="params">(<span class="keyword">int</span> requestCode, <span class="keyword">int</span> resultCode, Intent data)</span> </span>{</span><br><span class="line"> <span class="comment">// Check which request we're responding to</span></span><br><span class="line"> <span class="keyword">if</span> (requestCode == PICK_CONTACT_REQUEST) {</span><br><span class="line"> <span class="comment">// Make sure the request was successful</span></span><br><span class="line"> <span class="keyword">if</span> (resultCode == RESULT_OK) {</span><br><span class="line"> <span class="comment">// The user picked a contact.</span></span><br><span class="line"> <span class="comment">// The Intent's data Uri identifies which contact was selected.</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Do something with the contact here (bigger example below)</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面的例子中,当用户点击按钮(或者其他操作)时,<code>pickContact</code> 方法被触发,系统启动通讯录,用户从通讯录中选择联系人以后,回到原页面,继续处理接下来的逻辑。<strong>从通讯录选择用户并返回结果</strong> 就可以被看作为一个流程。</p><p>不过上面的流程是属于比较简单的情况,因为流程逻辑只有一个页面,而有时候一个复杂流程可能包含多个页面:例如注册,包含手机号验证界面(接收验证码验证),设置昵称页面,设置密码页面。假设注册流程是从登录界面启动的,那么使用 <code>startActivityForResult</code> 来实现注册流程的 Activity 任务栈的变化如下图所示:</p><p><img src="/images/register-sample.png" alt=""></p><p>上图的注册流程实现细节如下:</p><ol><li>登录界面通过 <code>startActivityForResult</code> 启动注册页面的第一个界面 —- 验证手机号;</li><li>手机号验证成功后,验证手机号界面通过 <code>startActivityForResult</code> 启动设置昵称页面;</li><li>昵称检查合法后,昵称信息通过 <code>onActivityResult</code> 返回给验证手机号界面,验证手机号界面通过 <code>startActivityForResult</code> 启动设置密码界面,由于设置密码是最后一个流程,验证手机号界面把之前收集好的手机号信息,昵称信息都一并传递给密码界面,密码检查合法后,根据现有的手机号、昵称、密码发起注册;</li><li>注册成功后,服务器返回注册用户信息,设置密码界面通过 <code>onActivityResult</code> 把注册结果反馈给设置手机号界面;</li><li>注册成功,设置手机号界面结束自己,同时把注册成功信息通过 <code>onActivityResult</code> 反馈给流程发起者(本例中即登录界面);</li></ol><p>通过这个例子可以看出来,<strong>手机号验证界面</strong> 不仅承担了在注册流程中验证手机号的功能,还承担了<strong>注册流程对外的接口的职责</strong>。也就是说,触发注册流程的任意位置,都不需要对注册流程的细节有任何了解,而只需要通过 <code>startActivityForResult</code> 和 <code>onActivityResult</code> 与流程对外暴露的 Activity 交互即可,如下图:</p><p><img src="/images/register-sample-2.png" alt=""></p><p>上面的例子中可能有一点令您疑惑:为什么每个步骤需要返回到验证手机号页面,然后由验证手机号页面负责启动下个步骤呢?一方面,由于验证手机号是流程的第一个页面,它承担了流程调度者的身份,所以由它来进行步骤的分发,这样的好处是每个步骤(除了第一步)之间是解耦和内聚的,每个步骤只需要做好自己的事情并且通过 <code>onActivityResult</code> 返回数据即可,假如后续流程的步骤发生增删,维护起来比较简单;另一方面,由于每个步骤做完都返回,当最后一个步骤做完以后,之前流程的中间页面都不存在了,不需要手动去销毁做完的流程页,这样编码起来也比较方便。</p><p>但是这么做带来一个小小的副作用:如果在流程的中间步骤按返回键,就会回到流程的第一个步骤,而用户有时候是希望可以回到上一个步骤。为了让用户可以在按返回键的时候返回上一个步骤,就必须要把每个步骤的 Activity 压栈,但是这样做的话最后一步做完之后如何销毁流程相关的所有 Activity 又是一个问题。</p><p>为了解决流程相关 Activity 的销毁问题,需要对上面的图做一点修改,如下:</p><p><img src="/images/register-sample-3.png" alt=""></p><p>原先,每个步骤做完自己的任务以后只需要结束自己并返回结果,修改后,每个步骤做完自己的任务后不结束自己,也不返回结果,同时需要负责启动流程的下一个步骤(通过 <code>startActivityForResult</code>),当它的下一个步骤结束并返回它的结果的时候,这个步骤能在自己的 <code>onActivityResult</code> 里接住,它在<code>onActivityResult</code>里需要做的是把自己的结果和它的下一个步骤的结果合在一起,传递给它的上一个步骤,并结束自己。</p><p>通过这样,实现了用户按返回键所需要的行为,但是这种做法的缺点是造成了流程内步骤间的耦合,一方面是启动顺序之间的耦合,另一方面由于<strong>需要同时携带它下个步骤的结果并返回</strong>造成的数据的耦合。</p><p>除此以外我还见过有人会单独使用一个栈,来保存流程中启动过的 Activity , 然后在流程结束后自己去手动依次销毁每个 Activity。我不太喜欢这种方法,它相比上面的方法没有解决实质问题,而且需要额外维护一个数据结构,同时还要考虑生命周期,得不偿失。</p><p>最后总结一下前文, <code>startActivityForResult</code> 这个方法有着它自己的优势:</p><blockquote><ol><li>足够简单,原生支持。</li><li>可以处理流程返回结果,继续处理触发流程前的操作。</li><li>流程封装良好,可复用。</li><li>虽然引入了额外的 <code>requestCode</code>,但是在某种程度上保留了请求的上下文。</li></ol></blockquote><p>但是这个原生方案存在的问题也是显而易见的:</p><blockquote><ol><li>写法过于 Dirty,发起请求和处理结果的逻辑被分散在两处,不易维护。</li><li>页面中如果存在的多个请求,不同流程回调都被杂糅在一个 <code>onActivityResult</code> 里,不易维护。</li><li>如果一个流程包含多个页面,代码编写会非常繁琐,显得力不从心。</li><li>流程步骤间数据共享基于 Intent,没有解决 <strong>问题(三)</strong>。</li><li>流程页面的自动销毁和流程进行中回退行为存在矛盾,<strong>问题(六)</strong> 和 <strong>问题(七)</strong> 没有很好地解决。</li></ol></blockquote><p>实际开发中,这几个问题都非常突出,影响开发效率,所以无法直接拿来使用。</p><h2 id="方案二:EventBus-或者其他基于事件总线的解决方案"><a href="#方案二:EventBus-或者其他基于事件总线的解决方案" class="headerlink" title="方案二:EventBus 或者其他基于事件总线的解决方案"></a>方案二:EventBus 或者其他基于事件总线的解决方案</h2><p>基于事件解耦也是一种比较优雅的解决方案,尤其是著名的 EventBus 框架了,它实现了非常经典的发布订阅模型,完成了出色的解耦:<br><img src="/images/EventBus-Publish-Subscribe.png" alt=""></p><p>我相信很多 Android 开发者都曾经很愉快地使用过这个框架……………………………………最后放弃了它,或者只在小范围使用它。比如说我,目前已经在项目中逐渐删除使用 EventBus 的代码,并且使用 RxJava 作为替代。</p><p>通过具体的代码一窥 EventBus 的基本用法:</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> setContentView(R.layout.activity_main);</span><br><span class="line"></span><br><span class="line"> EventBus.getDefault().register(<span class="keyword">this</span>);</span><br><span class="line"> EventBus.getDefault().post(<span class="keyword">new</span> MessageEvent(<span class="string">"hello"</span>,<span class="string">"world"</span>));</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Subscribe</span>(threadMode = ThreadMode.MainThread)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">helloEventBus</span><span class="params">(MessageEvent message)</span></span>{</span><br><span class="line"> mText.setText(message.name);</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">protected</span> <span class="keyword">void</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"> EventBus.getDefault().unregister(<span class="keyword">this</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">MessageEvent</span> </span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">final</span> String name;</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">final</span> String password;</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">MessageEvent</span><span class="params">(String name, String password)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.name = name;</span><br><span class="line"> <span class="keyword">this</span>.password = password;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那它有什么不足之处呢?首先,发起一个一般的异步任务,开发者期望在回调中得到的是 <strong>这个任务</strong> 的结果,而在 EventBus 的概念中,回调中传递的是“事件”(例子中的 MessageEvent)。这里稍稍有点不同,理论上,异步任务的结果的数据类型可以就是事件的数据类型,这样两个概念就统一了,然而实际中还是有很多场合无法这样处理, 举个例子:A Activity 和 B Activity 都需要请求一个网络接口,如果把网络请求的响应的对象类型直接作为事件类型提供给它们的 Subscriber,就会产生混乱,如下图。</p><p><img src="/images/event-bus-weakness-1.png" alt=""></p><p>图中,A Activity 和 B Activity 都发起同一个网络请求(可能参数不同,例如查天气接口,一个是查北京的天气,另一个是查上海的天气),那么他们的响应结果类是一样的,如果把这个响应结果直接作为事件类型提供给 EventBus 的回调,那么造成的结果就是两个 Activity 都收到两次消息。我把它称为 <strong>事件传播在空间上引起的混乱</strong>。</p><p>解决的方案通常是封装一个事件,把 Response 作为这个事件携带的数据:</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ResponseEvent</span> </span>{</span><br><span class="line"> String sender;</span><br><span class="line"> Response response;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在把响应对象封装成事件之后,加入了一个 <code>sender</code> 字段,用来区分这个响应应该对应哪个 Subscriber ,这样就解决了上述问题。</p><p>不仅仅在空间上, <strong>事件传播还可以在时间上引起混乱</strong>,想象一种情况,如果先后发起两个相同类型的请求,但是处理他们的回调是不同的。如果用传统的设置回调的方法,只要给这两个请求设置两个回调就可以了,但是如果使用 EventBus ,由于他们的请求类型相同,所以他们数据返回类型也相同,如果直接把返回数据类型当成事件类型,那么在 EventBus 的事件处理回调中无法区分这两个请求(无法保证一先一后的两个请求一定也是一先一后返回)。解决的方案也类似上面的方案,只要把 <code>sender</code> 这个字段换成类似 <code>timestamp</code> 这样的字段就可以了。</p><p>归根结底,事件传播在空间和时间上引起混乱的深层次原因是,把传统的“为每个异步请求设置一个回调”这种模式,变成了“设置一个回调,用来响应某一种事件”这种模式。传统的方式是一个具体的请求和一个具体的回调之间是强关联,一个具体的回调服务于一个具体的请求,而 EventBus 把两者给解耦开了,<strong>回调和请求之间是弱关联,回调只和事件类型之间是强关联</strong>。</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><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="comment">// File: ActivityA.java</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> setContentView(R.layout.activity_a);</span><br><span class="line"></span><br><span class="line"> EventBus.getDefault().register(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line"> findViewById(R.id.start).setOnClickListener(</span><br><span class="line"> v -> startActivity(<span class="keyword">new</span> Intent(<span class="keyword">this</span>, ActivityB.class))</span><br><span class="line"> )</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Subscribe</span>(threadMode = ThreadMode.MainThread)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">helloEventBus</span><span class="params">(MessageEvent message)</span></span>{</span><br><span class="line"> mText.setText(message.name);</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">protected</span> <span class="keyword">void</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"> EventBus.getDefault().unregister(<span class="keyword">this</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">// File: ActivityB.java</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> setContentView(R.layout.activity_b);</span><br><span class="line"></span><br><span class="line"> findViewById(R.id.btn).setOnClickListener(v -> {</span><br><span class="line"> EventBus.getDefault().post(<span class="keyword">new</span> MessageEvent(<span class="string">"hello"</span>,<span class="string">"world"</span>));</span><br><span class="line"> finish();</span><br><span class="line"> })</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码的意图主要是:在 ActivityA 点击按钮,启动 ActivtyB, ActivtyB 承载了一个业务流程,当 ActivityB 所承担的流程任务完成以后,点击它页面内的按钮,结束流程,把结果数据通过 EventBus 传递回 ActivityA ,同时结束自己,把用户带回 ActivityA。</p><p>理想情况下,这样是没有问题,但是如果在开启了 Android 系统中的 “<strong>开发者选项</strong> - <strong>不保留活动</strong>”选项以后,ActivityA 不会收到来自 ActivityB 的任何消息。“<strong>不保留活动</strong>”这个选项其实是模拟了当系统内存不足的时候,会销毁 Activity 栈中用户不可见的 Activity 这一特性。这点在低端机中非常常见,经常玩着玩着手机,突然接个电话,回来发现整个页面都重新加载了。那么原因已经显而易见了:因为 ActivityA 在被系统销毁的时候执行了 <code>onDestroy</code>,从 EventBus 中移除了自身回调,因此无法接收到来自 ActivityB 的回调了。能不能不移除回调呢?当然是不能,因为这样会造成内存泄漏,更糟。</p><p>熟悉 EventBus 的朋友应该对 <code>postSticky</code> 这个方法不陌生,确实,在这种情况下,<code>postSticky</code> 这个方法可以让事件多存活一段时间,直到它的消费者出现把它消费掉。但是这个方法也有一些副作用,使用<code>postSticky</code>发送的事件需要由 Subscriber 手动把事件移除,这就导致,如果事件有多个消费者,那写代码的时候就不知道应该在什么时候把事件移除,需要增加一个计数器或者别的什么手段,引入了额外的复杂度。<code>postSticky</code>的事件只是为了保证 Activity 重走生命周期后内部回调依然可以收到事件,却污染了全局的空间,这种做法我觉得非常不优雅。</p><p>写到这里,这篇文章快成了 EventBus 批判文了,其实 EventBus 本身没问题,只是我们使用者要考虑场景,不能滥用,还是有些场合比较适用的,但是对于业务流程处理这个任务来说,我并不认为这是一个很好的应用场景。</p><p>上述陈述中,很多例子我都使用了“异步任务”作为例子来阐述,主要是我认为其实在用户操作中我们插入的业务流程也可以视为一种异步任务,反正最后结果都是异步返回给调用者的。所以我认为 EventBus 不适合异步任务的那些点,同样不适合业务流程。</p><p>其他的事件总线解决方案基本类似,Android 原生的 Broadcast 如果不考虑它的跨进程特性的话,在处理业务流程这件事情上基本可以认为是个低配版的 EventBus ,所以这里不再赘述。</p><h2 id="方案三:FLAG-ACTIVITY-CLEAR-TOP-或许是一种方案"><a href="#方案三:FLAG-ACTIVITY-CLEAR-TOP-或许是一种方案" class="headerlink" title="方案三:FLAG_ACTIVITY_CLEAR_TOP 或许是一种方案"></a>方案三:FLAG_ACTIVITY_CLEAR_TOP 或许是一种方案</h2><p>由于考虑使用第三方的框架始终无法避开 Android 生命周期的问题(上一节 EventBus 案例中 Activity 的销毁与重建丢失上下文的例子)。我们还是倾向于从 Android 原生框架中寻找符合我们要求的功能组件。这时我从 <code>Intent Flags</code> 中找到了 <code>FLAG_ACTIVITY_CLEAR_TOP</code>, 官方文档在<a href="https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP" target="_blank" rel="noopener">这里</a>, 我不打算照搬文档,但是想把其中一个例子翻译一下:</p><blockquote><p>如果一个 Activity 任务栈有下列 Activity:A, B, C, D. 如果这时 D 调用 <code>startActivity()</code>, 并且作为参数的 <code>Intent</code> 最后解析为要启动 Activity B(这个 <code>Intent</code> 中包含 <code>FLAG_ACTIVITY_CLEAR_TOP</code> ), 那么 C 和 D 都会销毁,B 会接收到这个 <code>Intent</code>, 最后这个任务栈应该是这样:A, B。</p></blockquote><p>这段只描述了现象,<a href="https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP" target="_blank" rel="noopener">文档</a>中还描述了更细节的数据流动,建议仔细阅读消化文档描述,我只把其中最重要的一块单独翻译一下:</p><blockquote><p>上面的例子中的 B 会是下面两种结果之一</p><ol><li>在 <code>onNewIntent</code> 回调中接收到来自 D 传递过来的 Intent</li><li>B 会销毁重建, 而重建的 <code>Intent</code> 就是由 D 传递过来的那个 <code>Intent</code></li></ol></blockquote><blockquote><p>如果 B 的 <code>launchMode</code> 被申明为 <code>multiple</code>(即<code>standard</code>)<strong>且</strong> Intent Flags 中<strong>没有</strong> <code>FLAG_ACTIVITY_SINGLE_TOP</code>, 那么就是上面的结果2。剩下的情况(<code>launchMode</code> 被申明为<strong>非</strong> <code>multiple</code> <strong>或</strong> Intent Flags 中<strong>有</strong> <code>FLAG_ACTIVITY_SINGLE_TOP</code>),就是结果1.</p></blockquote><p>上面的描述中,B 的结果1 就很适合我们业务流程的封装,为什么这么说呢,这里举一个例子。背景:一个社交 App, 首页信息流。假设所有 Activity 都在一个任务栈中,那么这个任务栈的变化如下图所示:</p><p><img src="/images/login-sample.png" alt=""></p><p>(1) 匿名用户浏览了一会,进行了一次点赞操作,此时触发登录流程,登录界面被弹出来;<br>(2) 用户输完正确的用户名密码后(假设为老用户),服务器接收到登录请求后,检测到风险,发起两步验证(需要短信验证),客户端弹出短信验证页面进行验证;<br>(3) 用户输入正确的验证码,点击登录,回到信息流页面,同时页面上点赞操作已经成功。</p><p>如何实现第3步中描述的现象呢? 只要在 Activity C 里面,登录成功的逻辑里添加启动 Activity A 的逻辑,同时给这个启动 Activity A 的 Intent 同时加上 <code>FLAG_ACTIVITY_CLEAR_TOP</code> <code>FLAG_ACTIVITY_SINGLE_TOP</code> 两个 Intent Flag 即可(所有 Activity 的 <code>launchMode</code> 均为 <code>standard</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">Intent intent = <span class="keyword">new</span> Intent(ActivityC.<span class="keyword">this</span>, ActivityA.class);</span><br><span class="line">intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);</span><br><span class="line"><span class="comment">// put your data here</span></span><br><span class="line"><span class="comment">// intent.putExtra("data", "result");</span></span><br><span class="line">startActivity(intent);</span><br></pre></td></tr></table></figure><p>使用这种方法,优点如下:</p><ol><li>可以把用户重新带回到触发流程之前页面;</li><li>有携带数据(本例中可以是用户的登录信息)的回调;</li><li>在回调中可以帮助用户继续完成之前被打断的操作;</li><li>流程页面全部自动销毁,甚至 Activity C 自身的 <code>finish()</code> 方法都不需要调用;</li><li>即使打开<strong>不保留活动</strong>依然有效,Acitivity A 的 <code>onNewIntent</code> 回调会在 <code>onCreate</code> 之后被调用;</li><li>对流程进行一半时的页面回退支持良好;</li></ol><p>看上去这种方法似乎比 <strong>方案一</strong> 要好很多, 但是其实上面的例子还是有点问题:最后一步 Activity C 显式启动了 Activity A。流程页不应该和触发流程的页面发生任何耦合,不然流程就无法复用,所以应该想一种机制,可以让两者不耦合,同时又可以把流程完成后携带的数据传递给流程触发的地方。目前能想到比较合适的手段就是 <strong>方案一</strong> 中的 <code>startActivityForResult</code>了,具体做法是,Activity A 只和 Activity B 通过 <code>startActivityForResult</code> 和 <code>onActivityResult</code> 进行交互,流程最后一个页面则通过上述的 <code>onNewIntent</code> 把流程结束相关数据带回流程第一个页面(Activity B),由 Activity B 通过 <code>onActivityResult</code> 把数据传递给流程触发者,具体逻辑如下图所示:</p><p><img src="/images/login-sample-3.png" alt=""></p><p>这样流程封装和复用的问题解决了,但是这个方案还是存在一些缺点:</p><ol><li>和 <code>startActivityForResult</code> 一样,写法 Dirty,如果流程很多,维护较为不易;</li><li>即使是同一个流程,在同一个页面中也存在复用的情况,不增加新字段无法在 <code>onNewIntent</code> 里面区分;</li><li><strong>问题(三)</strong> 没有得到解决,<code>onNewIntent</code> 数据传递也是基于 Intent 的, 也没有用于步骤间共享数据的措施,共享的数据可能需要从头传到尾;</li><li>步骤之间有轻微的耦合:每个步骤需要负责启动它的下一个步骤;</li></ol><p>其中缺点2解释一下,点赞会触发登录,评论也会触发登录,两者登录成功都会返回信息流页面。不增加额外字段,<code>onNewIntent</code> 只是接收到了用户的登录信息,并不知道刚刚进行的是点赞还是评论。</p><p>这个方案和纯 <code>startActivityForResult</code> 的方案(<strong>方案一</strong>)有一种互补的感觉,一个擅长流程页不支持回退的情况,另一种擅长流程页支持回退的情况,而且它们都没有很好解决 <strong>问题(三)</strong> , 我们需要进一步探索是否有更优方案。</p><h2 id="方案四:利用新开辟的-Activity-栈来完成业务流程"><a href="#方案四:利用新开辟的-Activity-栈来完成业务流程" class="headerlink" title="方案四:利用新开辟的 Activity 栈来完成业务流程"></a>方案四:利用新开辟的 Activity 栈来完成业务流程</h2><p>由于我们目前接手的项目中的流程页面,都是基于 Activity 实现的,那么自然而然就能想到应该让处理流程的 Activity 们更加内聚,如果流程相关 Activity 都是在一个独立的 Activity 任务栈中,那么当流程处理完以后,只要在拿到流程的最终结果以后销毁那个任务栈即可,简单又粗暴。</p><p>如果依然使用上面那个信息流登录的例子的话,Activity 任务栈的变化应该如下图所示:</p><p><img src="/images/login-sample-2.png" alt=""></p><p>要实现图中的效果,那么需要考虑两个问题:</p><blockquote><ol><li>如何开启一个新的任务栈,把涉及流程的 Activity 都放到里面?</li><li>如何在流程结束以后销毁流程占用的任务栈,同时把流程结果返回到触发流程的页面?</li></ol></blockquote><p>问题1相对而言比较简单,我们把流程相关的所有 Activity 显式设置 <code>taskAffinity</code> (例如 com.flowtest.flowA), 注意不要和 Application 的 packageName 相同,因为 Activity 默认的 <code>taskAffinity</code> 就是 packageName。启动流程的时候,在启动流程入口 Activity 的 <code>Intent</code> 中增加 <code>FLAG_ACTIVITY_NEW_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></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- In AndroidManifest.xml --></span></span><br><span class="line"><span class="tag"><<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">".ActivityB"</span> <span class="attr">android:taskAffinity</span>=<span class="string">"com.flowtest.flowA"</span>/></span></span><br></pre></td></tr></table></figure><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">// In ActivityA.java</span></span><br><span class="line">Intent intent = <span class="keyword">new</span> Intent(ActivityA.<span class="keyword">this</span>, ActivityB.class);</span><br><span class="line">intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);</span><br><span class="line">startActivity(intent);</span><br></pre></td></tr></table></figure><p>而流程中的其他 Activity 的启动方式不需要做任何修改,因为它们的 <code>taskAffinity</code> 与 流程入口 Activity 相同,所以它们会被自动置入同一个任务栈中。</p><p>问题2稍微复杂一点,据我所知,目前 Android 还没有提供在任务栈之间相互通信的手段,那我们只能回过头继续考虑 Activity 之间数据传递的方法。首先,出于流程复用性考虑, 流程依然还是暴露 Activity B, 而流程触发者(Activity A) 通过 Activity B 以及<code>startActivityForResult</code> 和 <code>onActivityResult</code> 两个方法与流程交互; 其次,流程内部的步骤要和 Activity B 的交互的话,有 <code>onNewIntent</code> 以及 <code>onActivityResult</code> 这两种回调的方法。</p><p>看上去这种思路比较有希望,但是经过几次试验,我放弃了这种做法,原因是一旦开辟一个新的任务栈来处理,手机上<strong>最近应用列表</strong>上,就会多一个App的栏位(多出来的那个代表流程任务栈),也就是说用户在做流程的时候如果按 Home 键切换出去,那他想回来的时候,按 <strong>最近应用列表</strong>,他会看到两个任务,他不知道回哪个。即使流程完成, <strong>最近应用列表</strong> 中还会保留着那个位置,后续依然会给用户造成困惑。另外,任务栈切换时的默认动画和 Activty 默认切换动画不同(虽然可以修改成一样),会在使用过程中感觉有一丝怪异。</p><h2 id="方案五:使用-Fragment-框架封装流程"><a href="#方案五:使用-Fragment-框架封装流程" class="headerlink" title="方案五:使用 Fragment 框架封装流程"></a>方案五:使用 Fragment 框架封装流程</h2><p>到目前为止,上面各种方案中,相对能使用的方案,只有方案一和方案三。方案一中又存在一对矛盾,如果希望流程内所有步骤都能优雅销毁,步骤之间耦合更松散,就没法保证回退行为;回退行为有保证以后,流程步骤的销毁就不够优雅了,步骤之间耦合也紧一些;方案三中,流程步骤销毁的问题和回退得以优雅解决,但是步骤间的耦合没有解决。我们希望一种能够两全其美的方案,步骤之间耦合松散,回退优雅,销毁也容易。</p><p>仔细分析两种方案的优缺点,其实不难得出结论:之所以仅靠 Activity 之间交互难以达成上述目标本质上是由于 Activity 任务栈没有开放给我们足够的 API,我们与任务栈能做的事情有限。看到这里其实就容易想到 Android 中,除了 Activity ,Fragment 也是拥有 Back Stack 的,如果我们把流程页以 Fragment 封装,就可以在一个 Activity 内通过 Fragment 切换完成流程;由于 Activity 与 Fragment Back Stack 生命周期同在,Activity 就成了理想的保存 Fragment Back Stack 状态(流程状态)的理想场所;此外,只要调用 Activity 的 <code>finish()</code> 方法就可以清空 Fragment Back Stack!</p><p>仍然以登录两步验证为例,经过 Fragment 改造以后,触发流程的点只会启动一个 Activity ,并且只和这个 Activity 交互,如下图所示:<br><img src="/images/login-sample-5.png" alt=""></p><p>Activity A 通过 <code>startActivityForResult</code> 启动 ActivityLogin,ActivityLogin 在内部通过 Fragment 把业务流程完成,<code>finish</code> 自身,并且把流程结果通过 <code>onActivityResult</code> 返回给 Activity A。流程包含的两个步骤被封装成两个 Fragment , 它们与宿主 Activity 的交互如下图所示:<br><img src="/images/login-sample-4.png" alt=""></p><ol><li><p>ActivityLogin 启动流程第一个页面 —- 密码登录,通过 <code>push</code> 方法(本例中的方法皆伪代码)把 Fragment A 展示到用户面前,用户登录密码验证成功,通过 <code>onLoginOk</code> 方法回调 ActivityLogin,ActivityLogin 保存该步骤必要信息。</p></li><li><p>ActivityLogin 启动流程第二个页面 —- 两步验证,同时附带上个步骤的信息传递给 Fragment B,也是通过 <code>push</code> 方法,手机短信验证成功,通过 <code>onValidataOk</code> 方法回调 ActivityLogin, ActivityLogin 把这步的数据和之前步骤的数据打包,通过 <code>onActivityResult</code> 传递给流程触发点。 </p></li></ol><p>再回过头看开头,我们对新的流程框架提出了7个待解决问题,再看本方案,我们可以发现,除了 <strong>问题(三)</strong> 还存疑,其余的问题应该说都得到了妥善的解决。</p><p>正常情况下,添加 Fragment 是不带有动画的,没有像 Activity 切换那样的默认动画。为了可以使 Fragment 的切换给用户的感觉和 Activity 的体验一致,我建议把 Fragment 的切换动画设置成和 Activity 一样。首先,给 Activity 指定切换动画(不同手机 ROM 的默认 Activity 切换动画不一样,为了使 App 体验一致强烈推荐手动设置切换动画)。</p><p>以向左滑动进入、向右滑动推出的动画为例,<strong>styles.xml</strong> 中设置主题如下:<br><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"><span class="comment"><!-- Base application theme. --></span></span><br><span class="line"><span class="tag"><<span class="name">style</span> <span class="attr">name</span>=<span class="string">"AppTheme"</span> <span class="attr">parent</span>=<span class="string">"Theme.AppCompat.Light.NoActionBar"</span>></span><span class="undefined"></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">item</span> <span class="attr">name</span>=<span class="string">"android:windowAnimationStyle"</span>></span>@style/ActivityAnimation<span class="tag"></<span class="name">item</span>></span></span></span><br><span class="line"><span class="xml"> <span class="comment"><!-- Customize your theme here. --></span></span></span><br><span class="line"><span class="undefined"> ...</span></span><br><span class="line"><span class="undefined"></span><span class="tag"></<span class="name">style</span>></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- Activity 进入、退出动画 --></span></span><br><span class="line"><span class="tag"><<span class="name">style</span> <span class="attr">name</span>=<span class="string">"ActivityAnimation"</span> <span class="attr">parent</span>=<span class="string">"android:Animation.Activity"</span>></span><span class="undefined"></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">item</span> <span class="attr">name</span>=<span class="string">"android:activityOpenEnterAnimation"</span>></span>@anim/push_in_left<span class="tag"></<span class="name">item</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">item</span> <span class="attr">name</span>=<span class="string">"android:activityCloseEnterAnimation"</span>></span>@anim/push_in_right<span class="tag"></<span class="name">item</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">item</span> <span class="attr">name</span>=<span class="string">"android:activityCloseExitAnimation"</span>></span>@anim/push_out_right<span class="tag"></<span class="name">item</span>></span></span></span><br><span class="line"><span class="xml"> <span class="tag"><<span class="name">item</span> <span class="attr">name</span>=<span class="string">"android:activityOpenExitAnimation"</span>></span>@anim/push_out_left<span class="tag"></<span class="name">item</span>></span></span></span><br><span class="line"><span class="undefined"></span><span class="tag"></<span class="name">style</span>></span></span><br></pre></td></tr></table></figure></p><p>定义进场和退场动画,动画文件放在 <strong>res/anim</strong> 文件夹下:</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- file: push_in_left.xml --></span></span><br><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">set</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">translate</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:fromXDelta</span>=<span class="string">"100%p"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:toXDelta</span>=<span class="string">"0"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:duration</span>=<span class="string">"400"</span>/></span> </span><br><span class="line"><span class="tag"></<span class="name">set</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- file: push_in_right.xml --></span></span><br><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">set</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">translate</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:fromXDelta</span>=<span class="string">"-25%p"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:toXDelta</span>=<span class="string">"0"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:duration</span>=<span class="string">"400"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">set</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- file: push_out_right.xml --></span></span><br><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">set</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">translate</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:fromXDelta</span>=<span class="string">"0"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:toXDelta</span>=<span class="string">"100%p"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:duration</span>=<span class="string">"400"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">set</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- file: push_out_left.xml --></span></span><br><span class="line"><?xml version="1.0" encoding="utf-8"?></span><br><span class="line"><span class="tag"><<span class="name">set</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">translate</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:fromXDelta</span>=<span class="string">"0"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:toXDelta</span>=<span class="string">"-25%p"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:duration</span>=<span class="string">"400"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">set</span>></span></span><br></pre></td></tr></table></figure><p>所以,加上 Fragment 的切换动画以后,上面的 <code>push</code> 方法的实现如下:<br><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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">push</span><span class="params">(Fragment fragment, String tag)</span> </span>{</span><br><span class="line"> List<Fragment> currentFragments = fragmentManager.getFragments();</span><br><span class="line"> FragmentTransaction transaction = fragmentManager.beginTransaction();</span><br><span class="line"> <span class="keyword">if</span> (currentFragments.size() != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 流程中,第一个步骤的 Fragment 进场不需要动画,其余步骤需要</span></span><br><span class="line"> transaction.setCustomAnimations(</span><br><span class="line"> R.anim.push_in_left,</span><br><span class="line"> R.anim.push_out_left,</span><br><span class="line"> R.anim.push_in_right,</span><br><span class="line"> R.anim.push_out_right</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line"> transaction.add(R.id.fragment_container, fragment, tag);</span><br><span class="line"> <span class="keyword">if</span> (currentFragments.size() != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 从流程的第二个步骤的 Fragment 进场开始,需要同时隐藏上一个 Fragment,这样才能看到切换动画</span></span><br><span class="line"> transaction</span><br><span class="line"> .hide(currentFragments.get(currentFragments.size() - <span class="number">1</span>))</span><br><span class="line"> .addToBackStack(tag);</span><br><span class="line"> }</span><br><span class="line"> transaction.commit();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>每个代表流程中一个具体步骤的 Fragment 的职责也是清晰的:收集信息,完成步骤,并把该步骤的结果返回给宿主 Activity。该步骤本身不负责启动下一个步骤,与其他步骤之间也是松耦合的,一个具体的例子如下:</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></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">PhoneRegisterFragment</span> <span class="keyword">extends</span> <span class="title">Fragment</span> </span>{</span><br><span class="line"></span><br><span class="line"> PhoneValidateCallback mPhoneValidateCallback;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Nullable</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> View <span class="title">onCreateView</span><span class="params">(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> View view = inflater.inflate(R.layout.fragment_simple_content, container, <span class="keyword">false</span>);</span><br><span class="line"> Button button = view.findViewById(R.id.action);</span><br><span class="line"> EditText input = view.findViewById(R.id.input);</span><br><span class="line"> button.setOnClickListener(<span class="keyword">new</span> View.OnClickListener() {</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">onClick</span><span class="params">(View view)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (mPhoneValidateCallback != <span class="keyword">null</span>) {</span><br><span class="line"> mPhoneValidateCallback.onPhoneValidateOk(input.getText().toString());</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> view;</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">void</span> <span class="title">setPhoneValidateCallback</span><span class="params">(PhoneValidateCallback phoneValidateCallback)</span> </span>{</span><br><span class="line"> mPhoneValidateCallback = phoneValidateCallback;</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">PhoneValidateCallback</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">onPhoneValidateOk</span><span class="params">(String phoneNumber)</span></span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时候,作为一系列流程步骤的宿主 Activity 的职责也明确了:</p><ol><li>作为流程对外暴露的接口,对外数据交互(<code>startActivityForResult</code> 和 <code>onActivityResult</code>)</li><li>负责流程步骤的调度,决定步骤间调用的先后顺序</li><li>流程步骤间数据共享的通道</li></ol><p>举一个例子,注册流程由3个步骤组成:验证手机号、设置昵称、设置密码,流程 Activity 如下所示:<br><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></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">RegisterActivity</span> <span class="keyword">extends</span> <span class="title">BaseActivity</span> </span>{</span><br><span class="line"></span><br><span class="line"> String phoneNumber;</span><br><span class="line"> String nickName;</span><br><span class="line"> User mUser;</span><br><span class="line"></span><br><span class="line"> PhoneRegisterFragment mPhoneRegisterFragment;</span><br><span class="line"> NicknameCheckFragment mNicknameCheckFragment;</span><br><span class="line"> PasswordSetFragment mPasswordSetFragment;</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">protected</span> <span class="keyword">void</span> <span class="title">onCreate</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 保证无论 Activity 无论在首次启动还是销毁重建的情况下都能获取正确的 </span></span><br><span class="line"><span class="comment"> * Fragment 实例</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> mPhoneRegisterFragment = findOrCreateFragment(PhoneRegisterFragment.class);</span><br><span class="line"> mNicknameCheckFragment = findOrCreateFragment(NicknameCheckFragment.class);</span><br><span class="line"> mPasswordSetFragment = findOrCreateFragment(PasswordSetFragment.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果是首次启动,把流程的第一个步骤代表的 Fragment 压栈</span></span><br><span class="line"> <span class="keyword">if</span> (savedInstanceState == <span class="keyword">null</span>) {</span><br><span class="line"> push(mPhoneRegisterFragment);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 负责验证完手机号后启动设置昵称</span></span><br><span class="line"> mPhoneRegisterFragment.setPhoneValidateCallback(<span class="keyword">new</span> PhoneRegisterFragment.PhoneValidateCallback() {</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">onPhoneValidateOk</span><span class="params">(String phoneNumber)</span> </span>{</span><br><span class="line"> RegisterActivity.<span class="keyword">this</span>.phoneNumber = phoneNumber;</span><br><span class="line"></span><br><span class="line"> push(mNicknameCheckFragment);</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"> mNicknameCheckFragment.setNicknameCheckCallback(<span class="keyword">new</span> NicknameCheckFragment.NicknameCheckCallback() {</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">onNicknameCheckOk</span><span class="params">(String nickname)</span> </span>{</span><br><span class="line"> RegisterActivity.<span class="keyword">this</span>.nickName = nickName;</span><br><span class="line"></span><br><span class="line"> mPasswordSetFragment.setParams(phoneNumber, nickName);</span><br><span class="line"> push(mPasswordSetFragment);</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"> mPasswordSetFragment.setRegisterCallback(<span class="keyword">new</span> PasswordSetFragment.PasswordSetCallback() {</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">onRegisterOk</span><span class="params">(User user)</span> </span>{</span><br><span class="line"> mUser = user;</span><br><span class="line"> Intent intent = <span class="keyword">new</span> Intent();</span><br><span class="line"> intent.putExtra(<span class="string">"user"</span>, mUser);</span><br><span class="line"> setResult(RESULT_OK, intent);</span><br><span class="line"> finish();</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><p>其中 <code>findOrCreateFragment</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <T extends Fragment> <span class="function">T <span class="title">findOrCreateFragment</span><span class="params">(@NonNull Class<T> fragmentClass)</span> </span>{</span><br><span class="line"> String tag = fragmentClass.fragmentClass.getCanonicalName();</span><br><span class="line"> FragmentManager fragmentManager = getSupportFragmentManager();</span><br><span class="line"> T fragment = (T) fragmentManager.findFragmentByTag(tag);</span><br><span class="line"> <span class="keyword">if</span> (fragment == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> fragment = fragmentClass.newInstance();</span><br><span class="line"> } <span class="keyword">catch</span> (InstantiationException e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> } <span class="keyword">catch</span> (IllegalAccessException e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> fragment;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看到这里,您也许会对 <code>findOrCreateFragment</code> 这个方法实现有一定疑问,主要是针对使用 <code>Class.newInstance</code> 这个方法对 Fragment 进行实例化这行代码。通常来说,Google 推荐在 Fragment 里自己实现一个 <code>newInstance</code> 方法来负责对 Fragment 的实例化,同时,Fragment 应该包含一个无参构造函数,Fragment 初始化的参数不应该以构造函数的参数的形式存在,而是应该通过 <code>Fragment.setArguments</code> 方法进行传递,符合上面要求的 <code>newInstance</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> MyFragment <span class="title">newInstance</span><span class="params">(<span class="keyword">int</span> someInt)</span> </span>{</span><br><span class="line"> MyFragment myFragment = <span class="keyword">new</span> MyFragment();</span><br><span class="line"></span><br><span class="line"> Bundle args = <span class="keyword">new</span> Bundle();</span><br><span class="line"> args.putInt(<span class="string">"someInt"</span>, someInt);</span><br><span class="line"> myFragment.setArguments(args);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> myFragment;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>因为使用 Fragment.setArguments 方法设置的参数,可以在 Activity 销毁重建时(重建过程也包含重建原来 Activity 管理的那些 Fragment),传递给那些被 Activity 恢复的 Fragment。</p></blockquote><p>但是这边的代码为什么要这么处理呢?首先,Activity 只要进入后台,就有可能在某个时刻被杀死,所以当我们回到某个 Activity 的时候,我们应该有意识:这个 Activity 可能是刚刚离开前的那个 Activity,也有可能是已经被杀死,但是重新被创建的新 Activity。如果是重新被创建的情况,那么之前 Activity 内的状态可能已经丢失了。也就是说对于给每个流程步骤的 Fragment 设置的回调(<code>setPhoneValidateCallback</code>、 <code>setNicknameCheckCallback</code> 、 <code>setRegisterCallback</code>)有可能已经无效了,因为 Activity 重新创建以后,内存中是一个新的对象,这个对象只经历了 <code>onCreate</code>、<code>onStart</code>、<code>onResume</code> 这些回调,如果给 Fragment 设置回调的调用不在这些生命周期函数里,那么这些状态就已经丢失了(可以通过 <strong>开发者选项里</strong> 的 <strong>不保留活动</strong> 选项进行验证)。</p><p>但是有一个解决方法,就是把设置 Fragment 回调的调用写在 Activity 的 <code>onCreate</code> 函数里(因为无论是全新的 Activity 还是重建的 Activity 都会走 <code>onCreate</code> 生命周期),如本例中的 <code>onCreate</code> 方法的写法。但是这就要求在 <code>onCreate</code> 函数内,需要获取所有 Fragment 的实例(无论是首次全新创建的 Fragment,还是被恢复情况下,利用 FragmentManager 查找到的系统帮我们自动恢复的那个 Fragment)。</p><p>但是流程中,很常见的情况是,某个步骤启动所需要的参数,依赖于上个步骤。如果使用 Google 推荐的那个最佳实践,很显然,我们在初始化的时候需要准备好所有参数,这是不现实的,Activity 的 <code>onCreate</code> 函数里肯定没有准备好靠后的步骤的 Fragment 初始化所需要的参数。</p><p>这里就产生了一个矛盾:一方面为了保证销毁重建情况下,流程继续可用,需要在 <code>onCreate</code><br>期间获得所有 Fragment 实例;另一方面,无法在 <code>onCreate</code> 期间准备好所有 Fragment 初始化所需要的参数,用来以 Google 最佳实践实例化 Fragment。</p><p>这里的解决方案就是上面的 <code>findOrCreateFragment</code> 方法,不完全使用 Google 最佳实践。利用 <strong>Fragment 应该包含一个无参构造函数</strong> 这一点,通过反射,实例化 Fragment。<br><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">fragment = fragmentClass.newInstance();</span><br></pre></td></tr></table></figure></p><p>利用 <strong>Fragment 初始化的参数不应该以构造函数的参数存在,而是应该通过 <code>Fragment.setArguments</code> 方法进行传递</strong> 这一点,在每个步骤结束的回调里启动下一个步骤的代码(本例中的 <code>push</code> 方法)之前,通过 <code>Fragment.setArguments</code> 方法传值。 <code>PasswordSetFragment.setParams</code> 的方法如下(底层就是 <code>Fragment.setArguments</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setParams</span><span class="params">(String phone, String nickname)</span> </span>{</span><br><span class="line"> Bundle bundle = <span class="keyword">new</span> Bundle();</span><br><span class="line"> bundle.putString(<span class="string">"phone"</span>, phone);</span><br><span class="line"> bundle.putString(<span class="string">"nickname"</span>, nickname);</span><br><span class="line"> setArguments(bundle);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其实通过静态分析代码可以发现,调用 <code>push</code> 方法显示的 Fragment 实例,都是在 FragmentManger 中尚未存在的,也就是说,都是那些只被通过反射实例化以后,却还没有真正走过任何 Fragment 生命周期函数的 <strong>准新 Fragment</strong>。所以说,虽然我们代码上好像和谷歌推荐的写法不一样了,但本质上依然遵循谷歌推荐的最佳实践。</p><p>看到这里,这个通过 Fragment Back Stack 实现的流程框架的所有关键细节就都说完了。这个方案对比 <strong>方案一</strong> 和 <strong>方案三</strong> 显然是更好的方案,因为它综合了这两个方案的优点。我们来总结一下这个方案的优点:</p><ol><li>流程步骤间是解耦的,每个步骤职责清晰,只需要完成自己的事并且通知给宿主;</li><li>回退支持良好,用户体验流畅;</li><li>销毁流程只需要调用 Activity 的 <code>finish</code> 方法,非常轻量级;</li><li>只有一个 Activity 代表这个流程暴露给外部,封装良好而且易于复用;</li><li>流程步骤间数据的共享变得更简单</li></ol><p>再回顾一下本文一开始提出的流程框架需要解决的 7 个问题,可以发现除了 <strong>问题(三)</strong> 没有完全解决以外,其余问题应该都是得到了较为满意的解决。我们来看一下 <strong>问题(三)</strong>,这个问题的提出的前提是,流程的每个步骤是基于 Activity 实现的,虽然使用基于 Fragment 的方案以后,Fragment 回调给 Activity 的数据不再受 Bundle 支持格式的限制,但是从 Activity <code>push</code> 启动 Fragment 需要先调用 <code>setArguments</code> 方法,而这个方法支持的格式依然受 Bundle 的限制。如果我们希望 Android 在 Activity 销毁后重建时正确恢复 Fragment ,我们只能接受这一点。</p><p>另外,虽然 Fragment 传递给 Activity 的数据格式不受限制了,考虑到 Activity 有可能销毁重建,为了保持 Activity 的状态,我们还是需要实现 Activity 的 <code>onSaveInstanceState</code> 方法和 <code>onRestoreInstanceState</code> 方法,而这两个方法依然是和 Bundle 打交道的:</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="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onSaveInstanceState</span><span class="params">(Bundle outState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onSaveInstanceState(outState);</span><br><span class="line"> outState.putString(<span class="string">"phoneNumber"</span>, phoneNumber);</span><br><span class="line"> outState.putString(<span class="string">"nickName"</span>, nickName);</span><br><span class="line"> outState.putSerializable(<span class="string">"user"</span>, mUser);</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">protected</span> <span class="keyword">void</span> <span class="title">onRestoreInstanceState</span><span class="params">(Bundle savedInstanceState)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.onRestoreInstanceState(savedInstanceState);</span><br><span class="line"> <span class="keyword">if</span> (savedInstanceState == <span class="keyword">null</span>) <span class="keyword">return</span>;</span><br><span class="line"> phoneNumber = savedInstanceState.getString(<span class="string">"phoneNumber"</span>);</span><br><span class="line"> nickName = savedInstanceState.getString(<span class="string">"nickName"</span>);</span><br><span class="line"> mUser = (User) savedInstanceState.getSerializable(<span class="string">"user"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>也就是说,如果我们希望我们的 Activity/Fragment 能历经生命周期摧残,而始终以正确的姿态被系统恢复,那么我们就要保证我们的数据是能够被打包进 Bundle 的。我们牺牲了编码上的便利性,换取代码执行的正确性。所以目前看来,<strong>问题(三)</strong>虽然没有被我们解决或者绕过,但是其实本质上它的存在是可以被接受的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在探讨和比较了上面这么多方案以后,我们终于找到相对而言最适合解决方案 —- <strong>方案(五):基于 Fragment 封装流程框架</strong>。但是这还不是终点,虽然在理论指标上,这个方案满足了我们的需求,但是实际开发中,还是有一些小问题等待被解决。比如:</p><ol><li>流程对外暴露的接口是 startActivityForResult/onActivityResult,基于这个 API 进行开发,很难称得上是“优雅”;</li><li>发起流程的上下文应该如何保存,<code>requestCode</code> 能保存的信息量有限,尤其是在 ListView / RecyclerView 的场合下;</li><li>或许我们应该借助一个框架来帮助我们实现流程框架,而不是手写很多重复代码;</li></ol><p>等等。</p><p>在下一篇分享中,我将继续介绍如何更优雅地去使用、封装流程框架,欢迎继续关注!<br><!-- 应该在result数据中,把请求数据也带上 --></p>]]></content>
<summary type="html">
<p>有一定实际 Android 项目开发经验的人,一定曾经在项目中处理过很多重复的业务流程。例如开发一个社交 App ,那么出于用户体验考虑,会需要允许匿名用户(不登录的用户)可以浏览信息流的内容(或者只能浏览受限的内容),当用户想要进一步操作(例如点赞)时,提示用户需要登录或者注册,用户完成这个流程才可以继续刚刚的操作。而如果用户需要进行更深入的互动(例如评论,发布状态),则需要实名认证或者补充手机号这样的流程完成才可以继续操作。</p>
</summary>
<category term="Android 业务流程" scheme="http://prototypez.github.io/tags/Android-%E4%B8%9A%E5%8A%A1%E6%B5%81%E7%A8%8B/"/>
</entry>
<entry>
<title>小米4 Android 6.0 版本 Root 并安装 Xposed 框架攻略</title>
<link href="http://prototypez.github.io/2016/05/16/root-and-install-Xposed-framework-on-XiaoMi4-with-Android-M/"/>
<id>http://prototypez.github.io/2016/05/16/root-and-install-Xposed-framework-on-XiaoMi4-with-Android-M/</id>
<published>2016-05-16T01:47:16.000Z</published>
<updated>2018-09-01T14:45:24.520Z</updated>
<content type="html"><![CDATA[<p>本人自 Android 开发入坑一周年以来,向来对Root设备不太感冒。我对Root的设备的态度和对苹果的越狱差不多。大学期间有个舍友越狱了自己的 iPhone,据说从此可以下载许多的收费应用和游戏,所以越狱给我的映像就是破解软件的收费限制,Root也差不多。自己成为一名开发者之后,深感 Google 大法好,不 Root 也能把 Android 玩得很好,然而我毕竟还是 too young too simple,在微信群的抢红包大战中败给越狱之后的 iPhone 的抢红包插件之后,我决定研究 Android 上的抢红包插件。</p><a id="more"></a><p>上 Github 搜红包插件,果然层出不穷,知乎大神纷纷表示情绪稳定,谈笑间抛出一个名词 – AccessibilityService。据说该 API 设计的初衷是为有障碍人士提供更方便的操作手机的的选择,只要用户为 App 打开这个这个权限的开关,App 就可以模拟用户对应用进行操作:解析当前界面,点击,等等。我下载了 Github 上一个高 star 的抢红包插件,测试了一下,确实可以帮助用户点开红包,并拆开,比手点是快了点,但是这个东西一点都不稳定,常常红包出现了,却不去点,或者点开了红包不拆开。另一个问题就是,这样抢红包依然太慢,还是要观看拆红包的那个动画,达不到毫秒级抢红包,与越狱后的 iPhone 抢红包插件性能相去甚远。</p><p>另一个容易想到的方法就是在红包到达时,调用微信的拆红包操作 API(假设能逆向微信代码,并且找到对应代码),这里涉及到的技术难点是 Hook 微信的方法,目前有比较成熟的方案便是 <a href="https://github.com/rovo89/Xposed" target="_blank" rel="noopener">Xposed</a>。</p><h2 id="Root-小米4"><a href="#Root-小米4" class="headerlink" title="Root 小米4"></a>Root 小米4</h2><p>Xposed 需要 Root 权限,我手上有一个小米4,大概一个月以前跟着 MIUI 升级升到了 Android 6.0.1,发现以前很好用的傻瓜 Root 工具都不能用了,包括 KingRoot, Root精灵,Root大师等等,似乎 Android 6.0.1 对 Root 这件事控制得更加严格了。Google 了一圈 “Android 6.0 小米4 Root”的关键字以后也没有找到现成的教程,本来开始有点心灰意冷,突然看到 MIUI 论坛有个人评论,“我还没发现 SuperSU 不能 Root 的系统”,这下点燃的我的希望。</p><p>我找到了一个 SuperSU 的<a href="http://www.zdfans.com/3562.html" target="_blank" rel="noopener">最新版本</a>,然后被一堆名词看得一脸懵逼。</p><blockquote><p>刷机包使用步骤:</p></blockquote><blockquote><p>方法一:Recovery 刷入(推荐)</p></blockquote><blockquote><p>1、下载刷机包,复制到设备的SD卡中;</p></blockquote><blockquote><p>2、设备进入 CWM/TWRP Recovery(原厂 Recovery 不能刷);</p></blockquote><blockquote><p>3、在 Recovery 中将刚刚复制到 SD 卡的刷机包刷入;</p></blockquote><blockquote><p>4、重启设备,更新完成。</p></blockquote><p><img src="http://img.doutula.com/production/uploads/image//2016/02/28/20160228614309_TquwJx.jpeg" alt=""></p><p>作为一个 Android 开发居然对上面这些名词完全没概念真是惭愧。在搜索引擎里补了一番课之后,明白了 Recovery 大概是 Android 系统的一种恢复机制,进入这个模式以后可以对系统进行一些修改,包括写入一些新的内容,甚至重新写入整个系统。如果要刷入 SuperSU 的话需要使用第三方的 Recovery 写入,CWM 和 TWRP 都是第三方的 Recovery。</p><p>随即 Google 了 <a href="https://www.clockworkmod.com/rommanager" target="_blank" rel="noopener">CWM</a> 和 <a href="https://www.clockworkmod.com/rommanager" target="_blank" rel="noopener">TWRP</a> 的两个工具的官网, 发现没有对应小米4 ROM的下载,于是转战搜索国内百度,找到了一个国内大神为小米4定制的 <a href="http://www.muzisoft.com/recovery/93443.html" target="_blank" rel="noopener">CWM Recovery</a>,然后我需要为我的小米4刷入这个 Recovery。关闭手机,按住音量下键与开关键开机,手机进入 FastBoot 模式,把手机与电脑使用 USB 线连接起来。把上面提到的 Recovery 解压以后,里面有一个文件名叫<strong>一键刷入Recovry.bat</strong>(这里提一下,本人使用 Windows 系统,如果使用 Mac 或者 Linux 就需要你打开这个文件,参考着修改成对应的版本了。),打开命令行,进入这个文件所在的目录,执行这个文件,就OK了,简直是我等懒人的福音。手机会重启进入 CMW Recovery。</p><h2 id="刷入-Super-SU"><a href="#刷入-Super-SU" class="headerlink" title="刷入 Super SU"></a>刷入 Super SU</h2><p>在 CMW Recovery 的起始界面,会有“系统1”,“系统2”这样的选项,选择“系统1”进入,这时候你的命令行应该还停留在刚才的目录,在把 <em>Super SU</em> 刷入之前,首先我们需要把下载下来的 Super SU 的复制到手机SD卡上,在命令行执行 <code>adb push /path/to/your/super-su /sdcard/</code>, 然后在 CMW Recovery 的界面选择从 SD 卡刷入指定 zip 包。</p><p>很快就刷完了,这时候从界面上选择返回,重启手机,在重启前,该程序会询问是否禁止小米的自动安装Recovery脚本,该脚本会自动安装小米官方 Recovery,从而导致即使我们在 FastBoot 刷入了第三方 Recovery 无效,因为在等到下次进入时,小米的 Recovery 会覆盖第三方的 Recovery,所以到底要不要覆盖,取决于你自己的需求吧,重启结束后,你会发现,手机已经 Root 了,可以下载一个需要 Root 才能玩的 App,你会发现它会请求 Root 权限了,不过,没有什么特殊需求尽量不要去允许 App 的 Root 权限请求。此时我的心情大概是这样的。</p><p><img src="http://pics.sc.chinaz.com/Files/pic/faces/3574/01.jpg" alt=""></p><h2 id="安装-Xposed-框架"><a href="#安装-Xposed-框架" class="headerlink" title="安装 Xposed 框架"></a>安装 Xposed 框架</h2><p>这个简单,上应用宝市场,直接搜一个<strong>Xposed Installer</strong>,下载就可以了。不过马上就碰到坑了,进入<strong>Xposed Installer</strong>后所有和“安装”,“卸载”有关的按钮都不可点,而且会有错误提示,Xposed 框架未激活。</p><p><img src="https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcTGzszyt15kpG_4J8sUQIWi4AtU4YLWVTozmPFVQxqwUn-Hem_uRA" alt=""></p><p>没办法,只好再上 Google 去补课了,在<a href="http://repo.xposed.info/module/de.robv.android.xposed.installer" target="_blank" rel="noopener">Xposed</a>的官网,我发现在一开头的申明就说到,如果是 Android 5.0 及以上的版本,安装方法请移步到<a href="http://forum.xda-developers.com/showthread.php?t=3034811" target="_blank" rel="noopener">另外一个帖子</a>,我跟了过去。帖子的大意就是,你需要首先安装最新的<strong>Xposed Installer</strong>(应用宝上的已经是最新的了),然后你需要刷入一个名为<strong>xposed*.zip</strong>的 zip 包,帖子里提供了链接,需要注意的是 sdk 版本选 23,且架构选 amd,因为小米处理器是32位的。</p><p>好在我们之前已经有过通过第三方 Recovery 刷入 zip 包的经验了,我们重复之前的方法,刷入这个 zip 包,这次刷完重启,就是漫长的等待,就和小米每次更新系统时那么慢。</p><p>功夫不负有心人,我们重启系统,发现。。。Xposed 框架依然没激活,尽管这时候报错信息已经发生改变了。经过多番查找,原因是刚刚刷入的 zip 包对小米支持不好,所以无效了。我也找到了解决方案,在刚才的论坛中,居然有国外大神发布了<a href="http://forum.xda-developers.com/xposed/unofficial-xposed-miui-t3367634" target="_blank" rel="noopener">针对 MIUI 修改的 Xposed 的 zip 刷机包</a>,简直是真爱米粉,可见小米还是有挺强的国际号召力的。</p><p>这里的具体过程我也就不说了,还是重复上面的类似操作,这时候刷机完重启手机,你会发现 Xposed 已经成功激活了!</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>我已经装上了抢红包插件,我要和 iOS 红包插件大战五百回合。</p><p><img src="http://www.people.com.cn/mediafile/pic/20151001/74/11063092384485792194.jpg" alt=""></p>]]></content>
<summary type="html">
<p>本人自 Android 开发入坑一周年以来,向来对Root设备不太感冒。我对Root的设备的态度和对苹果的越狱差不多。大学期间有个舍友越狱了自己的 iPhone,据说从此可以下载许多的收费应用和游戏,所以越狱给我的映像就是破解软件的收费限制,Root也差不多。自己成为一名开发者之后,深感 Google 大法好,不 Root 也能把 Android 玩得很好,然而我毕竟还是 too young too simple,在微信群的抢红包大战中败给越狱之后的 iPhone 的抢红包插件之后,我决定研究 Android 上的抢红包插件。</p>
</summary>
</entry>
<entry>
<title>RxJava 常见误区(一):过度使用 Subject</title>
<link href="http://prototypez.github.io/2016/04/30/rxjava-common-mistakes-1/"/>
<id>http://prototypez.github.io/2016/04/30/rxjava-common-mistakes-1/</id>
<published>2016-04-29T16:00:00.000Z</published>
<updated>2018-09-02T07:31:25.139Z</updated>
<content type="html"><![CDATA[<p>准备写这篇文章的时候看了下 RxJava 在 Github 上已经 12000+ 个 star 了,可见火爆程度,自己使用 RxJava 也已经有一小段时间。最初是在社区对 RxJava 一片赞扬之声下,开始使用 RxJava 来代替项目中一些简单异步请求,到后来才开始接触一些高级玩法,这中间阅读别人的代码加上自己踩的坑,慢慢积累了一些经验,很多都是新手容易犯的错误和 RxJava 容易被误解的地方。这些内容一篇文章写不完,所以我打算写成一个系列,这篇文章是这个系列的第一篇。</p><a id="more"></a><h2 id="谨慎使用Subject"><a href="#谨慎使用Subject" class="headerlink" title="谨慎使用Subject"></a>谨慎使用Subject</h2><p><code>Subject</code>既是<code>Observable</code>也是<code>Observer</code>,由于它自己本身是<code>Observer</code>,所以项目中任何地方都可以调用它的<code>onNext</code>方法(只要能获得该 Subject 的引用)。看起来很好对不对?比起<code>Observable.create</code>, <code>Observable.from</code>, <code>Observable.just</code>方便多了,这三个工厂方法都有一个特点,那就是所构建出来的 Observable 发射的元素是确定的,甚至在很多例子中,待发射的元素就像常量一样在编译期就已经可以确定。我在一开始学习这些入门的小例子的时候心里也在想,实际情况哪有这样简单:用户与 UI 交互的事件,移动设备网络类型的改变( WIFI 与蜂窝网络的切换),服务器推送消息的到达,这些事件何时发生和产生的数量都是在运行时才能得知,怎么可能用这些工厂方法简单地就发射几个固定的值。</p><p>直到我遇见了<code>Subject</code>。我可以先创建一个一开始什么元素都不发射的<code>Observable</code>(<code>Subject</code>是<code>Observable</code>的子类),并且同时创建对应的<code>Subscriber</code>订阅这个<code>Observable</code>,然后在我觉得某个 Ready 的时机,调用这个<code>Subject</code>对象的<code>onNext</code>方法,向它的<code>Subscriber</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></pre></td><td class="code"><pre><span class="line">PublishSubject<String> subject = PublishSubject.create();</span><br><span class="line"></span><br><span class="line">subject.map(String::length)</span><br><span class="line"> .subscribe(System.out::println);</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 在某个Ready的时机</span></span><br><span class="line">subject.onNext(<span class="string">"Puppy"</span>);</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"><span class="comment">// 当某个时刻subjcet已经完成了使命</span></span><br><span class="line">subject.onCompleted();</span><br></pre></td></tr></table></figure><h2 id="使用-Subject-可能导致错过真正关心的事件"><a href="#使用-Subject-可能导致错过真正关心的事件" class="headerlink" title="使用 Subject 可能导致错过真正关心的事件"></a>使用 Subject 可能导致错过真正关心的事件</h2><p>到目前看来,一切都顺理成章,对比<code>Observable</code>,<code>Subject</code>优势明显,可以按需在合适的时机发射元素,似乎是<code>Subject</code>更能满足日常任务需求,更激进一点,干脆就用<code>Subject</code>来代替所有的<code>Observable</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></pre></td><td class="code"><pre><span class="line">PublishSubject<String> operation = PublishSubject.create();</span><br><span class="line">operation</span><br><span class="line"> .subscribe(<span class="keyword">new</span> Subscriber<String>() {</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">onCompleted</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"completed"</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">onError</span><span class="params">(Throwable e)</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">onNext</span><span class="params">(String s)</span> </span>{</span><br><span class="line"> System.out.println(s);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line">operation.onNext(<span class="string">"Foo"</span>);</span><br><span class="line">operation.onNext(<span class="string">"Bar"</span>);</span><br><span class="line">operation.onCompleted();</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">Foo</span><br><span class="line">Bar</span><br><span class="line">completed</span><br></pre></td></tr></table></figure><p>稍微改一下,使用 RxJava 的调度器<code>Scheduler</code>指定<code>operation</code>对象从 IO 线程发射元素,代码如下(本文中的代码都是从<code>main</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></pre></td><td class="code"><pre><span class="line">PublishSubject<String> operation = PublishSubject.create();</span><br><span class="line">operation</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .subscribe(<span class="keyword">new</span> Subscriber<String>() {</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">onCompleted</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"completed"</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">onError</span><span class="params">(Throwable e)</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">onNext</span><span class="params">(String s)</span> </span>{</span><br><span class="line"> System.out.println(s);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line">operation.onNext(<span class="string">"Foo"</span>);</span><br><span class="line">operation.onNext(<span class="string">"Bar"</span>);</span><br><span class="line">operation.onCompleted();</span><br><span class="line">sleep(<span class="number">2000</span>);</span><br></pre></td></tr></table></figure><p>以上代码实际输出的结果为:<br><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">completed</span><br></pre></td></tr></table></figure></p><p>上面这段代码中,除了加上调度器以外,最后还增加了一行代码使当前线程休眠 2 秒,原因是<code>operation</code>对象改从 IO 线程发射元素以后,main 线程由于运行到最后一行直接退出了,导致整个进程结束,此时 IO 线程还没有开始发射元素,所以这 2 秒是用来等待 IO 线程启动起来并把该做的事情做完。</p><p>经过改动后的代码并没有接收到<code>Foo</code>和<code>Bar</code>,如果把最后一行<code>sleep(2000)</code>去掉,那么 Console 将不会输出任何内容。这便是我们需要谨慎使用<code>Subject</code>的第一个理由: <strong>使用 Subject 可能导致错过真正关心的事件</strong>。</p><p>在<code>RxJava</code>中,<code>Observable</code>可以被分为<code>Hot Observable</code>与<code>Cold Observable</code>,引用《Learning Reactive Programming with Java 8》中一个形象的比喻(翻译后的意思):</p><blockquote><p>我们可以这样认为,<code>Cold Observable</code>在每次被订阅的时候为每一个<code>Subscriber</code>单独发送可供使用的所有元素,而<code>Hot Observable</code>始终处于运行状态当中,在它运行的过程中,向它的订阅者发射元素(发送广播、事件),我们可以把<code>Hot Observable</code>比喻成一个电台,听众从某个时刻收听这个电台开始就可以听到此时播放的节目以及之后的节目,但是无法听到电台此前播放的节目,而<code>Cold Observable</code>就像音乐 CD ,人们购买 CD 的时间可能前后有差距,但是收听 CD 时都是从第一个曲目开始播放的。也就是说同一张 CD ,每个人收听到的内容都是一样的, 无论收听时间早或晚。</p></blockquote><p><code>Subjcet</code>是属于<code>Hot Observable</code>的。<code>Cold Observable</code>可以转化为<code>Hot Observable</code>, 但是反过来却不行。回过头来解释上面的例子为什么最后只输出了<code>completed</code>: 因为<code>operation</code>对象发射元素的线程被指派到了 IO 线程,相应的Subscriber也工作在 IO 线程,而 IO 线程第一次被Scheduler调用,还没起来(正在初始化),发射前两个元素<code>Foo</code>,<code>Bar</code>是在主线程,主线程的这两个元素往 IO 线程转发的过程中由于 IO 线程还没有起来,就被丢弃了(电台即使没有一个听众,照样可以播音)。<code>complete</code>信号比较特殊,在<code>Reactive X</code>的设计中,该信号优先级非常高,所以总是可以被优先捕获,不过这是另外一个话题。</p><p>所以使用<code>Subject</code>的时候,我们必须小心翼翼地设计程序,确保消息发送的时机是在<code>Subscriber</code>已经Ready的时候,否则我们就很容易错过我们关心的事件,当代码今后面临重构的时候,其他的程序员也必须知道这个逻辑,否则就很容易引入 Bug 。如果我们不希望错过任何事件,那么我们应该尽可能使用<code>Cold Observable</code>,上面的例子如果<code>operation</code>对象使用<code>Observable.just</code>, <code>Observable.from</code>来构造,就不会有这种问题了。</p><p>其实,错过事件这种情况一般发生在临界条件下,比如我刚刚声明一个<code>Subscriber</code>并且希望立即发送一个事件给它。这时候最好不要使用<code>Subject</code>而是使用<code>Observable.create(OnSubscribe)</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></pre></td><td class="code"><pre><span class="line">Observable<String> operation = Observable.create(subscriber -> {</span><br><span class="line"> subscriber.onNext(<span class="string">"Foo"</span>);</span><br><span class="line"> subscriber.onNext(<span class="string">"Bar"</span>);</span><br><span class="line"> subscriber.onCompleted();</span><br><span class="line">});</span><br><span class="line">operation</span><br><span class="line"> .subscribeOn(Schedulers.io())</span><br><span class="line"> .subscribe(<span class="keyword">new</span> Subscriber<String>() {</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">onCompleted</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"completed"</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">onError</span><span class="params">(Throwable e)</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">onNext</span><span class="params">(String s)</span> </span>{</span><br><span class="line"> System.out.println(s);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line">sleep(<span class="number">2000</span>);</span><br></pre></td></tr></table></figure><h2 id="Subjcet-不是线程安全的"><a href="#Subjcet-不是线程安全的" class="headerlink" title="Subjcet 不是线程安全的"></a>Subjcet 不是线程安全的</h2><p>使用<code>Subject</code>的第二个问题便是它 <strong>不是线程安全的</strong> ,如果在不同的线程调用它的<code>onNext</code>方法,很有可能造成竞态条件(race conditions),我们应该尽可能避免这种情况的出现,因为除非在代码中写足够详细的注释,否则日后维护这段代码的程序员很可能莫名其妙地踩了坑。如果你认为你确实有必要使用<code>Subject</code>, 那么请把它转化为<code>SerializedSubject</code>,它可以保证如果多个线程同时调用<code>onNext</code>方法,依然是线程安全的。</p><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">SerializedSubject<Integer,Integer> subject =</span><br><span class="line"> PublishSubject.<Integer>create().toSerialized();</span><br></pre></td></tr></table></figure><h2 id="Subject-使事件的发送变得不可预知"><a href="#Subject-使事件的发送变得不可预知" class="headerlink" title="Subject 使事件的发送变得不可预知"></a>Subject 使事件的发送变得不可预知</h2><p>最后一个我们应该谨慎对待<code>Subject</code>的原因就是它 <strong>让事件的发送变得不可预知</strong>。由于<code>Observable.create</code>使用的例子上面已经给出,再看另外两个工厂方法<code>Observable.just</code>和<code>Observable.from</code>的例子:</p><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">Observable<String> values = Observable.just(<span class="string">"Foo"</span>, <span class="string">"Bar"</span>);</span><br><span class="line">Observable myObservable = Observable.from(<span class="keyword">new</span> String[]{<span class="string">"Foo"</span>,<span class="string">"Bar"</span>});</span><br></pre></td></tr></table></figure><p>无论是<code>Observable.create</code>, <code>Observable.from</code> 还是 <code>Observable.just</code> , 这些 <code>Cold Observable</code> 都有一个显著的优点就是数据的来源可预知,我知道将会发送哪些数据,这些数据是什么类型。但是<code>Subject</code>就不一样,我如果创建一个<code>Subject</code>,那么代码任何地方只要能 Get 到这个引用,就可以随意使用它发射元素,滥用的后果导致代码越来越难以维护,我不知道其他人是否在某个我不知道的地方发射了我不知道的元素,我相信谁都不愿意维护这样的代码。这是一种反模式,就和 C 语言当初模块化的理念尚未深入人心的时候全局变量带来的灾难一样。</p><p>也许看到这里你会想,说了半天好像又回到起点了,<code>Subject</code>带给编程的灵活性不推荐用,为了这些理由又要重新用那三个不灵活的工厂方法,确实不能满足需求啊。我们回顾一下之前提到过的编程中经常遇到的实际情况:</p><blockquote><p>用户与 UI 交互的事件</p></blockquote><blockquote><p>移动设备网络类型的改变( WIFI 与蜂窝网络的切换)</p></blockquote><blockquote><p>服务器推送消息的到达</p></blockquote><p>其实这些事件往往都是以注册监听器的接口提供给程序员的,我们完全可以使用<code>Observable.create</code>这个工厂方法来创建Observable:</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="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">ViewClickOnSubscribe</span> <span class="keyword">implements</span> <span class="title">Observable</span>.<span class="title">OnSubscribe</span><<span class="title">Void</span>> </span>{</span><br><span class="line"> <span class="keyword">final</span> View view;</span><br><span class="line"></span><br><span class="line"> ViewClickOnSubscribe(View view) {</span><br><span class="line"> <span class="keyword">this</span>.view = view;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">call</span><span class="params">(<span class="keyword">final</span> Subscriber<? <span class="keyword">super</span> Void> subscriber)</span> </span>{</span><br><span class="line"> verifyMainThread();</span><br><span class="line"></span><br><span class="line"> View.OnClickListener listener = <span class="keyword">new</span> View.OnClickListener() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onClick</span><span class="params">(View v)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!subscriber.isUnsubscribed()) {</span><br><span class="line"> subscriber.onNext(<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"> view.setOnClickListener(listener);</span><br><span class="line"></span><br><span class="line"> subscriber.add(<span class="keyword">new</span> MainThreadSubscription() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">onUnsubscribe</span><span class="params">()</span> </span>{</span><br><span class="line"> view.setOnClickListener(<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></pre></td></tr></table></figure><p>以上代码来自<a href="https://github.com/JakeWharton" target="_blank" rel="noopener">Jake Wharton</a>的 Android 项目 <a href="https://github.com/JakeWharton/RxBinding" target="_blank" rel="noopener">RxBinding</a> ,目的是将 Android UI 上的用户与控件交互产生的事件转化为<code>Observable</code>提供给程序员。上面的代码思路很简单,就是当有一个<code>Subscriber</code>想要订阅<code>View</code>的点击事件的时候,就为这个<code>View</code>在 Android Framework 里注册一个点击的回调(<code>view.setOnClickListener(listener)</code>), 每当点击事件来临的时候就去调用<code>Subscriber</code>的<code>onNext</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></pre></td><td class="code"><pre><span class="line">PublishSubject<String> subject = PublishSubject.create();</span><br><span class="line">View.OnClickListener listener = <span class="keyword">new</span> View.OnClickListener() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onClick</span><span class="params">(View v)</span> </span>{</span><br><span class="line"> subject.onNext(<span class="keyword">null</span>);</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line">view.setOnClickListener(listener);</span><br></pre></td></tr></table></figure><p>这里的<code>subject</code>还只是整个项目局部的代码,我们并不知道其他地方有没有把<code>subject</code>对象给怎么样,潜在的风险就是我们刚刚讨论的 <strong>可能会错过临界情况下的事件</strong>、 <strong>线程不安全</strong>、 <strong>事件来源不可预知</strong>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我们已经了解到了<code>Subject</code>给我们带来的灵活性以及风险,所以在实际项目中使用的时候我推荐更多地使用<code>Observable</code>提供的3个工厂方法,而慎重使用<code>Subject</code>,其实90%的情况都可以使用那3个工厂方法解决,如果你确定要使用<code>Subject</code>,那么确保:1. 这是一个 <code>Hot Observable</code> 且你有对应措施保证不会错过临界的事件;2. 有对应的线程安全措施;3. 模块化代码,确保事件的发送源在掌控中,事件的发送完全可预期。对了,另外加上必要的注释:)</p>]]></content>
<summary type="html">
<p>准备写这篇文章的时候看了下 RxJava 在 Github 上已经 12000+ 个 star 了,可见火爆程度,自己使用 RxJava 也已经有一小段时间。最初是在社区对 RxJava 一片赞扬之声下,开始使用 RxJava 来代替项目中一些简单异步请求,到后来才开始接触一些高级玩法,这中间阅读别人的代码加上自己踩的坑,慢慢积累了一些经验,很多都是新手容易犯的错误和 RxJava 容易被误解的地方。这些内容一篇文章写不完,所以我打算写成一个系列,这篇文章是这个系列的第一篇。</p>
</summary>
<category term="RxJava" scheme="http://prototypez.github.io/tags/RxJava/"/>
</entry>
<entry>
<title>Android Studio 的 10 个你很有可能不知道的技巧</title>
<link href="http://prototypez.github.io/2016/04/19/about-10-things-you-probably-didn-t-know-you-could-do-in-android-studio/"/>
<id>http://prototypez.github.io/2016/04/19/about-10-things-you-probably-didn-t-know-you-could-do-in-android-studio/</id>
<published>2016-04-19T07:15:01.000Z</published>
<updated>2018-09-01T14:45:24.512Z</updated>
<content type="html"><![CDATA[<p>本文原文出处来自 <a href="https://medium.com/google-developers/about-10-things-you-probably-didn-t-know-you-could-do-in-android-studio-de231071b375#.9wc67rigb" target="_blank" rel="noopener">Medium</a> , Android Studio 是每一个 Android 开发每天都要使用的工具,但是即使你是一个经验丰富的开发人员,你也可能已经错过了许多可以节约生命的技巧,这篇文章也许就可以帮助你掌握它们其中的一部分。我不会一字一句地翻译,而是以最简洁易懂的方式介绍给你,同时提供必要的注解和延伸,让你可以在一遍快速阅读之后迅速掌握。</p><a id="more"></a><p><img src="/images/1--wEOUYr835kBIb3oT0Syyg.gif" alt="你也像这样过度依赖鼠标吗?"></p><h2 id="当你想不起来某个功能怎么用的时候"><a href="#当你想不起来某个功能怎么用的时候" class="headerlink" title="当你想不起来某个功能怎么用的时候"></a>当你想不起来某个功能怎么用的时候</h2><p>如果你是 Windows/Linux 用户, 那么请按<code>Ctrl + Shift + A</code>, 如果你是 Mac 用户,那么请按<code>Command + Shift + A</code>,在这个万能的输入框内可以输入你想要执行的操作(当然是英文),列表中会显示对应的可选操作以及快捷键。不仅仅是操作,如果你只是想改变某个设置的时候,也可以使用这个功能,例如你想设置<code>Gradle</code>为 offline work 的话,可以在输入框输入<code>offline</code>,对应的的结果中选择<code>Toggle Offline Work</code>即可,再比如你需要打开粘贴代码时候的<code>Auto Import</code>功能,那么也只要在输入框中输入<code>Auto Import</code>然后选择对应项即可。</p><p><img src="/images/1-zmhPiKsZCVAG-OBUrc5z9A.gif" alt=""></p><h2 id="修改快捷键"><a href="#修改快捷键" class="headerlink" title="修改快捷键"></a>修改快捷键</h2><p>在 Android Studio 中所有快捷键都是可以自定义的。请唤起伟大的<code>Ctrl + Shift + A</code>,输入<code>keymap</code>,选择位于<code>Settings > Keymap</code>的那个选项,这里能看到所有的快键键,一般不建议在原有快捷键方案上直接修改快捷键,而是拷贝一份现有的方案再在上面改,在<code>Keymaps</code>的下拉框中选择一份现有的方案(默认为 Default),点击右边的<code>Copy</code>,然后在列表中需要修改的快捷键的项目上右键,选择<code>Add Keyboard Shortcut</code>,然后就可以设置自己喜欢的快捷键了,如果设置的快捷键与其它按键有冲突,会以红色错误信息提示。</p><p><img src="/images/android-studio-shortcut-setting.gif" alt=""></p><p>由于列表中快捷键数量比较多,所以我们还可以利用右边的搜索框进行搜索,例如需要修改基本自动补全的快捷键我们只要输入<code>Basic</code>,就可以在结果中找到对应的项了。</p><h2 id="你需要了解的自动补全"><a href="#你需要了解的自动补全" class="headerlink" title="你需要了解的自动补全"></a>你需要了解的自动补全</h2><p>一般使用 Android Studio 的时候,自动提示会在你想要提示的时候自动出现,比如输入<code>Log.</code>,就会提示一堆比如<code>Log.d()</code>, <code>Log.e</code>, <code>Log.i()</code>。不过如果你在自动提示的时候手一抖选错的话,比如想选<code>Log.d()</code>结果选了<code>Log.e()</code>,你是不是会把<code>.e()</code>都删掉,然后再输入一个<code>.</code>,其实遇到这种需要重新手动呼起自动补全的情形只需要使用<code>Ctrl + Alt + 空格</code>就可以了。</p><p><img src="/images/android-studio-auto-completion.gif" alt=""></p><p>其实还有两种方法:一是基本补全<code>Ctrl + 空格</code>,然而 Windows 用户表示不开心,因为这和 Windwos 系统切换输入法快捷键冲突,如果你不想修改这个快捷键,那么使用<code>Ctrl + Alt + 空格</code>作为替代,如果你想修改这个快键键,那么你可以使用上一小节的方法设置新的快捷键,在<code>Keymaps</code>界面搜索<code>Basic</code>,然后在过滤后的结果中选择<code>Code</code>-><code>Completion</code>-><code>Basic</code>进行设置;另一种方法是智能补全<code>Ctrl + Shift + 空格</code>, 不过智能补全远远不止这个功能,当你调用方法时,可以使用智能补全在当前上下文联想符合该方法形参类型的变量。</p><h2 id="在自动提示以后使用-Tab-键替换当前的方法或值"><a href="#在自动提示以后使用-Tab-键替换当前的方法或值" class="headerlink" title="在自动提示以后使用 Tab 键替换当前的方法或值"></a>在自动提示以后使用 Tab 键替换当前的方法或值</h2><p>如果我们手动呼出自动补全的时候,当前位置已经有对应的方法或者变量(比如原来调用Obj的A方法,然后我们把光标定位到A方法的位置,呼出自动补全,希望自动补全的B方法代替A方法),这时候如果我们选中补全的的项目,按下回车,那么补全的内容会插入到原来内容的前面,这不是我们想要的内容,其实这时候不应该按回车,而是<code>Tab</code>。<br><img src="/images/1-AhdlQUqM71fvvGG_v8b6dQ.gif" alt=""></p><h2 id="当你写完了一行代码"><a href="#当你写完了一行代码" class="headerlink" title="当你写完了一行代码"></a>当你写完了一行代码</h2><p>这种情况非常常见,当你写完一行代码的时候,光标并不在当前行的末尾,比较常见的是光标右边还有 N 个 右括号,这时候你会怎么办?可能你会使用方向键把光标移动到行末,然后手动输入一个<code>;</code>。其实有更简单的方案,那就是<code>Ctrl + Shift + 回车</code>,这个快捷键会帮助你自动补全当前表达式所缺的部分,包括在行末输入<code>;</code>,值得一提的是,该快捷键对<code>if</code>,<code>else</code>,<code>for</code>,<code>while</code>控制循环同样有效。<br><img src="/images/android-studio-expression-completion.gif" alt=""><br>还有一种情况,光标并不在行尾,但是你希望可以在下一行插入一个空行,<code>Shift + 回车</code>可以帮你完成这个任务。</p><h2 id="三个定位的小技巧"><a href="#三个定位的小技巧" class="headerlink" title="三个定位的小技巧"></a>三个定位的小技巧</h2><p>使用 4 个方向键定位光标是我们最熟悉的方法,但是其实可以更方便的。在按方向键的同时按住<code>Ctrl</code>,可以一个单词一个单词移动;在按上下键的同时按住<code>Alt</code>, 可以一个节点(方法或者字段)一个节点移动;如果在按上下键的同时按住<code>Shift + Ctrl</code>,可以把当前行和上下行交换位置。</p><h2 id="根据后缀自动生成的代码模板"><a href="#根据后缀自动生成的代码模板" class="headerlink" title="根据后缀自动生成的代码模板"></a>根据后缀自动生成的代码模板</h2><p>日常使用编辑器的过程中,有很多固定格式的写法,例如<code>if-else</code>, <code>for</code> 等等,使用代码模板可以更快得帮助我们生成期望的代码。举个例子,先输入一个集合类型的变量并且加上后缀<code>.fori</code>,这时候自动提示会提示按照该集合类型生成<code>for</code>循环,同理,布尔类型的表达式加上<code>.if</code>后缀也可以生成以该表达式为条件的<code>if</code>语句。<br><img src="/images/1-Mt2-SylWiSTRZ3kRece21w.gif" alt=""><br>实际上,上面这两种代码模板是<code>IntelliJ</code>自带的,Android Studio 还提供了许多与 Android 有关的代码模板,比如生成<code>Toast</code>和<code>Parcelable</code>的模板,查看所有可用模板以及自定义模板的方法是首先<code>Ctrl + Shift + A</code>呼出万能的搜索框,输入<code>Live templates</code>,选择位于<code>Settings</code>的<code>Live Templates</code>,在这里就可以看到所有可用的代码模板。</p><h2 id="Debug-时自定义对象显示的技巧"><a href="#Debug-时自定义对象显示的技巧" class="headerlink" title="Debug 时自定义对象显示的技巧"></a>Debug 时自定义对象显示的技巧</h2><p>在调试代码的时候,我们常常需要查看一个对象的值。尤其是自定义的对象,我们常常没有实现它的<code>toString</code>方法,那么这个对象在 IDE 的值就是 <code>ClassName:HashValue</code> 这个样子。我们需要点开这个对象查看它里面各个成员的值。在不实现<code>toString</code>方法的情况下其实有更好的方法。在 Debug 的<code>Variable</code>窗口中右键需要查看的对象,选择<code>View as</code>,既可以设置该对象在 Debug 状态下显示的方式。<br><img src="/images/1-D6xJW1DalD9K8miVemhmAg.gif" alt=""></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>感谢你看到这里 :)</p>]]></content>
<summary type="html">
<p>本文原文出处来自 <a href="https://medium.com/google-developers/about-10-things-you-probably-didn-t-know-you-could-do-in-android-studio-de231071b375#.9wc67rigb" target="_blank" rel="noopener">Medium</a> , Android Studio 是每一个 Android 开发每天都要使用的工具,但是即使你是一个经验丰富的开发人员,你也可能已经错过了许多可以节约生命的技巧,这篇文章也许就可以帮助你掌握它们其中的一部分。我不会一字一句地翻译,而是以最简洁易懂的方式介绍给你,同时提供必要的注解和延伸,让你可以在一遍快速阅读之后迅速掌握。</p>
</summary>
<category term="Android Studio" scheme="http://prototypez.github.io/tags/Android-Studio/"/>
</entry>
</feed>