{"id":1284,"date":"2026-09-14T14:57:50","date_gmt":"2026-09-14T14:57:50","guid":{"rendered":"https:\/\/boochlin.com\/?p=1284"},"modified":"2026-09-14T14:58:07","modified_gmt":"2026-09-14T14:58:07","slug":"funny-vote-app-kotlin-%e8%88%87-rxjava-%e7%9a%84%e9%81%b2%e4%be%86%e6%95%91%e8%b4%96%ef%bc%88%e4%ba%8c%ef%bc%89","status":"publish","type":"post","link":"https:\/\/boochlin.com\/?p=1284","title":{"rendered":"Funny vote APP \u2013 Kotlin \u8207 RxJava \u7684\u9072\u4f86\u6551\u8d16\uff08\u4e8c\uff09"},"content":{"rendered":"<p>\u597d\u4e00\u9663\u5b50\u2026\u2026\u597d\u5427\uff0c\u4e0d\u662f\u597d\u4e00\u9663\u5b50\uff0c\u662f\u904e\u4e86\u5feb\u5341\u5e74\u624d\u4f86\u66f4\u65b0\u4e0b\u4e00\u7bc7\u3002\u4e0a\u4e00\u7bc7\u5728\u7d50\u5c3e\u4fe1\u8a93\u65e6\u65e6\u653e\u8a71\u8aaa\uff1a<em>\u300c\u81f3\u65bc Kotlin \u7248\u672c\u6211\u5247\u653e\u5230\u4e0b\u4e00\u7bc7\uff0c\u5c0d\u5c0f\u5f1f\u4f86\u8aaa Kotlin \u7b97\u662f\u771f\u7684\u5b8c\u5168\u6c92\u7528\u904e\uff0c\u6240\u4ee5\u65b0\u958b\u6587\u7ae0\uff01\u300d<\/em> \u7d50\u679c\u9019\u4e00\u653e\uff0c\u76f4\u63a5\u5f9e Android 8.0 Oreo \u4e00\u8def\u8de8\u5230\u4e86 Android 16\u3002\u53ef\u898b\u300c\u6020\u60f0\u300d\u9019\u6a94\u4e8b\uff0c\u53ea\u8981\u990a\u6210\u7fd2\u6163\uff0c\u5341\u5e74\u904e\u53bb\u771f\u7684\u8ddf\u7728\u773c\u4e00\u6a23\u5feb\u3002<\/p>\n<p>\u79c9\u6301\u8457 Something Record \u7684\u7cbe\u795e\uff0c\u65e2\u7136\u81ea\u5df1\u6316\u4e86\u5751\uff0c\u8dea\u8457\u4e5f\u8981\u628a\u5b83\u586b\u5b8c\u3002\u9019\u7bc7\u4f9d\u7136\u5ef6\u7e8c Funny Vote \u7684\u91cd\u69cb\u5fc3\u8def\u6b77\u7a0b\uff0c<strong>\u4e0d\u6703\u624b\u628a\u624b\u6559\u4f60 Kotlin \u8a9e\u6cd5\uff0c\u4e5f\u4e0d\u6253\u7b97\u8cbc\u843d\u843d\u9577\u7684\u6559\u5b78\u4ee3\u78bc\uff0c\u4e3b\u8981\u9084\u662f\u8a18\u9304\u6211\u7576\u5e74\u662f\u600e\u9ebc\u88ab Google \u7684\u65b0\u73a9\u5177\u75db\u6241\u7684\u771f\u5be6 murmur\u3002<\/strong><\/p>\n<p><strong>\u672c\u7bc7\u5c0d\u61c9\u4e4b GitHub \u5206\u652f<\/strong>\uff1a<\/p>\n<ul>\n<li>\u97ff\u61c9\u5f0f\u9032\u5316\u7248\uff1a<a href=\"https:\/\/github.com\/boochlin06\/FunnyVote\/tree\/mvp_rxjava\" target=\"_blank\" rel=\"noreferrer noopener\">Funny Vote <code>mvp_rxjava<\/code> branch on GitHub<\/a><\/li>\n<li>\u8a9e\u8a00\u5148\u884c\u91cd\u69cb\u7248\uff1a<a href=\"https:\/\/github.com\/boochlin06\/FunnyVote\/tree\/mvp_kotlin\" target=\"_blank\" rel=\"noreferrer noopener\">Funny Vote <code>mvp_kotlin<\/code> branch on GitHub<\/a><\/li>\n<\/ul>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<h2 class=\"wp-block-heading\">\u4e00\u3001 \u81ea\u6253\u81c9\u7684\u958b\u5834\uff1a\u7576\u5e74\u8ab0\u8aaa RxJava \u96de\u817f\u6bd4\u61f6\u89ba\u7684\uff1f<\/h2>\n<p>\u5728\u4e0a\u4e00\u7bc7\u804a Dagger 2 \u7684\u6642\u5019\uff0c\u6211\u66fe\u5927\u8a00\u4e0d\u615a\u5730\u5beb\u904e\u9019\u6bb5\u8a71\uff1a<\/p>\n<blockquote class=\"wp-block-quote\">\n<p><em>\u300c\u4f60\u8aaa\u70ba\u4f55\u6c92\u6709\u4e00\u8d77\u5f15\u5165 RxJava\uff1f\u2026\u2026\u6211\u7d55\u5c0d\u4e0d\u6703\u8aaa\u56e0\u70ba\u770b\u8d77\u4f86\u592a\u7c21\u55ae\u4e86\uff0c\u6240\u4ee5\u6211\u61f6\u5f97\u7528\u2026\u2026\u96d6\u7136\u6211\u77e5\u9053\u9019\u6709\u9ede\u96de\u817f\u6bd4\u61f6\u89ba\u7684\u554f\u984c\uff0c\u4f46\u662f\u56e0\u70ba\u9019\u4e4b\u5f8c\u6211\u6253\u7b97\u76f4\u63a5\u5beb Kotlin \u7248\u672c\uff0c\u6240\u4ee5\u5c31\u5ffd\u7565\u4e86\u3002\u300d<\/em><\/p>\n<\/blockquote>\n<p>\u99ac\u7684\uff0c\u6bcf\u6b21\u56de\u982d\u770b\u81ea\u5df1\u4ee5\u524d\u5beb\u7684\u5e79\u8a71\uff0c\u90fd\u6703\u60f3\u642d\u6642\u5149\u6a5f\u56de\u53bb\u8cde\u81ea\u5df1\u5169\u5df4\u638c\u3002<\/p>\n<p>\u8eab\u70ba\u4e00\u500b\u6709\u67b6\u69cb\u5f37\u8feb\u75c7\u7684 Android \u5de5\u7a0b\u5e2b\uff0c\u7576\u4f60\u770b\u8457 Google \u5b98\u65b9\u7684 Android Architecture Blueprints \u4e0a\uff0c\u6bcf\u500b sample \u5c08\u6848\u90fd\u6f02\u6f02\u4eae\u4eae\u4e26\u6392\u8457 <code>todo-mvp<\/code>\u3001<code>todo-mvp-dagger<\/code>\u3001<code>todo-mvp-rxjava<\/code> \u6642\uff0c\u4f60\u5fc3\u88e1\u90a3\u80a1\u300c\u7a7a\u4e86\u4e00\u584a\u6c92\u88dc\u9f4a\u300d\u7684\u8e81\u9b31\u611f\u5c31\u6703\u958b\u59cb\u4f5c\u795f\u3002\u5634\u5df4\u4e0a\u8aaa\u96de\u817f\u6bd4\u61f6\u89ba\uff0c\u8eab\u9ad4\u5012\u662f\u5f88\u8aa0\u5be6\u2014\u2014\u5728\u771f\u6b63\u78b0 Kotlin \u4e4b\u524d\uff0c\u6211\u9084\u662f\u5077\u5077\u958b\u4e86\u4e00\u500b <code>mvp_rxjava<\/code> \u5206\u652f\uff0c\u628a\u5c08\u6848\u7d66\u300c\u4e32\u6d41\u5316\u300d\u4e86\u3002<\/p>\n<p>\uff08\u9806\u5e36\u4e00\u63d0\uff0c\u5728 2026 \u5e74\u9019\u6ce2\u8001\u5c08\u6848\u5fa9\u6d3b\u8207\u7dad\u8b77\u5de5\u7a0b\u4e2d\uff0c\u6211\u4e5f\u9806\u624b\u628a\u9019\u500b\u8001\u5206\u652f\u5347\u7d1a\u5230\u4e86 RxJava 2\uff0c\u628a\u53e4\u65e9\u7684 <code>CompositeSubscription<\/code> \u63db\u6210\u4e86\u5927\u5bb6\u66f4\u719f\u6089\u7684 <code>CompositeDisposable<\/code>\uff0c\u514d\u5f97\u73fe\u4ee3 Android \u5de5\u7a0b\u5e2b\u770b\u5230 <code>Subscription<\/code> \u9084\u4ee5\u70ba\u6211\u5728\u5beb\u4ec0\u9ebc\u4e0a\u53e4\u9ed1\u9b54\u6cd5\uff09\u3002<\/p>\n<\/p>\n<h3 class=\"wp-block-heading\">RxJava \u771f\u7684\u6709\u90a3\u9ebc\u8212\u723d\uff1f<\/h3>\n<p>\u672c\u4f86\u5728 MVP \u67b6\u69cb\u4e0b\uff0c\u6700\u75db\u82e6\u7684\u5c31\u662f Presenter \u547c\u53eb Model \u6642\u90a3\u6eff\u5751\u6eff\u8c37\u7684 <code>Callback&lt;T&gt;<\/code>\u3002\u5c24\u5176\u7576\u4f60\u8981\u505a\u300c\u5148\u6aa2\u67e5\u767b\u5165\u72c0\u614b -> \u6488\u53d6\u6295\u7968\u8a73\u60c5 -> \u67e5\u8a62\u672c\u5730\u662f\u5426\u6709\u6536\u85cf\u904e\u300d\u9019\u7a2e\u9023\u7e8c\u975e\u540c\u6b65\u93c8\u6642\uff0c\u4ee3\u78bc\u5c31\u6703\u4e00\u8def\u5f80\u53f3\u4e0b\u89d2\u659c\u659c\u5730\u6e9c\u6ed1\u68af\u6e9c\u51fa\u53bb\uff0c\u4fd7\u7a31\u300c\u56de\u547c\u5730\u7344\uff08Callback Hell\uff09\u300d\u3002<\/p>\n<p>\u5f15\u9032 RxJava \u5f8c\uff0c\u7528 <code>flatMap<\/code> \u93c8\u5f0f\u8abf\u7528\u76f4\u63a5\u62c9\u6210\u4e00\u689d\u76f4\u7dda\uff0c\u518d\u52a0\u4e0a\u5ba3\u544a\u5f0f\u7684\u57f7\u884c\u7dd2\u5207\u63db\uff0c\u7576\u4e0b\u5beb\u8d77\u4f86\u771f\u7684\u723d\u611f\u5341\u8db3\uff1a<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-java\">\/\/ 2017 \u5e74\u5728 VoteDetailPresenter \u88e1\u7684\u7d93\u5178 RxJava 1.x \u4e32\u6d41\nmSubscriptions.add(\n    voteDataRepository.getVoteData(voteCode)\n        .subscribeOn(schedulerProvider.io())        \/\/ \u80cc\u666f\u9ed8\u9ed8\u642c\u78da\n        .observeOn(schedulerProvider.ui())        \/\/ \u5207\u56de\u4e3b\u57f7\u884c\u7dd2\u9080\u529f\n        .subscribe(new Observer&lt;VoteData&gt;() {\n            @Override\n            public void onNext(VoteData data) { view.showVoteDetail(data); }\n            @Override\n            public void onError(Throwable e)  { view.showErrorMessage(e.getMessage()); }\n            @Override\n            public void onCompleted()         { view.hideProgress(); }\n        })\n);<\/code><\/pre>\n<h3 class=\"wp-block-heading\">\u4f46\u662f\uff01\u4ee3\u50f9\u662f\u4ec0\u9ebc\u5462\uff1f<\/h3>\n<ol>\n<li><strong>\u9661\u5ced\u5230\u60f3\u649e\u7246\u7684\u5b78\u7fd2\u66f2\u7dda<\/strong>\uff1a<br \/>\u672c\u4f86\u4ee5\u70ba\u53ea\u662f\u63db\u500b\u5beb\u6cd5\uff0c\u7d50\u679c\u8dd1\u53bb\u67e5\u6587\u4ef6\u624d\u767c\u73fe\u9019\u73a9\u610f\u6839\u672c\u662f\u53e6\u4e00\u500b\u5b87\u5b99\u3002\u5149\u662f\u4e00\u500b\u300c\u628a\u8cc7\u6599\u5f80\u4e0b\u50b3\u300d\uff0c\u5c31\u6709 <code>flatMap<\/code>\u3001<code>concatMap<\/code>\u3001<code>switchMap<\/code>\uff1b\u60f3\u63a7\u5236\u767c\u5c04\u983b\u7387\uff0c\u53c8\u6709 <code>debounce<\/code>\u3001<code>throttleFirst<\/code>\u3002\u9019\u54ea\u88e1\u662f\u300c\u770b\u8d77\u4f86\u592a\u7c21\u55ae\u300d\uff0c\u9019\u6839\u672c\u662f\u628a\u5de5\u7a0b\u5e2b\u63a8\u9032\u64cd\u4f5c\u7b26\u7684\u7121\u5e95\u6df1\u6df5\u3002<\/li>\n<li><strong>\u5fd8\u8a18\u53d6\u6d88\u8a02\u95b1\u7684\u8a18\u61b6\u9ad4\u5927\u5674\u767c<\/strong>\uff1a<br \/>\u5728 Java \u6642\u4ee3\u6c92\u6709\u73fe\u4ee3\u5354\u7a0b\u7684\u751f\u547d\u9031\u671f\u611f\u77e5\u3002\u4f60\u5728 Presenter \u88e1 <code>subscribe<\/code> \u4e4b\u5f8c\uff0c\u5982\u679c\u6c92\u6709\u4e56\u4e56\u62ff\u4e00\u500b <code>CompositeSubscription<\/code> \u63a5\u4f4f\u5b83\uff0c\u4e26\u4e14\u5728 Activity \u92b7\u6bc0\u6642\u547c\u53eb\uff1a\n<pre class=\"wp-block-code\"><code class=\"language-java\">@Override\npublic void unsubscribe() {\n    mSubscriptions.clear(); \/\/ \u6f0f\u6389\u9019\u884c\uff0c\u4f60\u5c31\u7b49\u8457\u6536\u5d29\u6f70\u5831\u544a\n}<\/code><\/pre>\n<p>\u53ea\u8981\u4f7f\u7528\u8005\u5728\u7db2\u8def\u5f88\u6162\u7684\u6642\u5019\u8f49\u500b\u87a2\u5e55\u6216\u6309\u4e0a\u4e00\u9801\uff0c\u5df2\u7d93\u92b7\u6bc0\u7684 View \u5c31\u6703\u88ab\u80cc\u666f\u7684 <code>Observer<\/code> \u72e0\u72e0\u6293\u4f4f\u4e0d\u653e\uff0c\u63a5\u8457\u5728\u67d0\u500b\u98a8\u548c\u65e5\u9e97\u7684\u4e0b\u5348\u5674\u51fa <code>IllegalStateException: Activity has been destroyed<\/code> \u7d66\u4f60\u770b\u3002<\/li>\n<\/ol>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<h2 class=\"wp-block-heading\">\u4e8c\u3001 \u8e0f\u5165 Kotlin 1.2\uff1a\u8ab0\u51c6\u4f60\u5c0d\u8457 Java \u8aaa\u300c\u6211\u6c92\u6709 NPE\u300d\uff1f<\/h2>\n<p>\u73a9\u5b8c\u4e86 RxJava\uff0c\u7d42\u65bc\u4f86\u5230 2017~2018 \u5e74 Android \u958b\u767c\u754c\u6700\u9a5a\u5929\u52d5\u5730\u7684\u9769\u547d\u2014\u2014Google \u5728 I\/O \u5927\u6703\u4e0a\u5ba3\u5e03\uff1a<strong>Kotlin \u6b63\u5f0f\u6210\u70ba Android \u7684\u4e00\u7d1a\u958b\u767c\u8a9e\u8a00\uff08First-class citizen\uff09\u3002<\/strong><\/p>\n<p>\u90a3\u6642\u5019\u5404\u5927\u8ad6\u58c7\u90fd\u5728\u760b\u72c2\u9f13\u5439 Kotlin\uff0c\u8aaa\u4ec0\u9ebc\u300c\u7a7a\u5b89\u5168\uff08Null Safety\uff09\u300d\u3001\u300c\u518d\u4e5f\u4e0d\u6703\u6709 NullPointerException\u300d\u3001\u300c\u4ee3\u78bc\u91cf\u6e1b\u5c11\u4e00\u534a\u300d\u3002\u7576\u6642\u770b\u8457 Funny Vote \u88e1\u9762\u5e7e\u5341\u500b Java \u985e\u5225\uff0c\u6eff\u5730\u90fd\u662f <code>getVoteTitle()<\/code>\u3001<code>setVoteTitle()<\/code>\uff0c\u4ee5\u53ca\u6bcf\u6b21\u5f9e\u5f8c\u7aef\u62ff JSON \u90fd\u8981\u6230\u6230\u5162\u5162\u5beb <code>if (data != null &amp;&amp; data.getOptions() != null)<\/code>\uff0c\u6211\u5fc3\u4e00\u6a6b\uff0c\u6c7a\u5b9a\u6574\u7897\u7aef\u8d70\uff0c\u958b\u51fa <code>mvp_kotlin<\/code> \u5206\u652f\u628a\u6574\u500b\u5c08\u6848\u8f49\u6210 Kotlin 1.2\u3002<\/p>\n<h3 class=\"wp-block-heading\">\u521d\u5617\u751c\u982d\uff1aData Class \u8207 Synthetics \u771f\u9999<\/h3>\n<p>\u4e00\u958b\u59cb\u8f49\u63db\u7684\u723d\u611f\u662f\u771f\u7684\u7121\u6cd5\u5426\u8a8d\uff1a<\/p>\n<ul>\n<li><strong>\u80a5\u5927 POJO \u7684\u5927\u5c60\u6bba<\/strong>\uff1a\u4ee5\u524d\u5beb\u4e00\u500b <code>VoteData.java<\/code>\uff0c\u5305\u542b\u4e86 ID\u3001\u6a19\u984c\u3001\u9078\u9805\u6e05\u55ae\u3001\u904e\u671f\u6642\u9593\uff0c\u52a0\u4e0a Constructor\u3001Getter\u3001Setter\u3001<code>equals()<\/code>\u3001<code>hashCode()<\/code>\uff0c\u52d5\u8f12 200 \u884c\u8d77\u8df3\u3002\u63db\u6210 Kotlin \u7684 <code>data class<\/code> \u5f8c\uff0c\u5341\u884c\u641e\u5b9a\u3002<\/li>\n<li><strong>\u6dd8\u6c70 ButterKnife \u8207 findViewById<\/strong>\uff1a\u7576\u5e74\u6700\u7099\u624b\u53ef\u71b1\u7684\u9ed1\u79d1\u6280\u53eb <code>kotlinx.android.synthetic<\/code>\uff08\u4fd7\u7a31 Kotlin Synthetics\uff09\u3002\u53ea\u8981\u5728 Activity \u9802\u90e8 <code>import kotlinx.android.synthetic.main.activity_vote_detail.*<\/code>\uff0c\u4f60\u5728\u4ee3\u78bc\u88e1\u76f4\u63a5\u6572 XML \u4e0a\u7684 ID \u5c31\u80fd\u76f4\u63a5\u7576 View \u7269\u4ef6\u64cd\u4f5c\uff01\u9023 <code>@Bind<\/code> \u90fd\u61f6\u5f97\u5beb\u4e86\uff0c\u7c21\u76f4\u662f\u61f6\u4eba\u7684\u6975\u81f4\u3002<\/li>\n<\/ul>\n<pre class=\"wp-block-code\"><code class=\"language-kotlin\">\/\/ 2018 \u5e74\u7684 Funny Vote\uff1a\u9023 findViewById \u90fd\u4e0d\u7528\u5beb\u7684\u865b\u5047\u7e41\u69ae\noverride fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    setContentView(R.layout.activity_vote_detail)\n    \n    \/\/ \u76f4\u63a5\u7528 XML ID \u64cd\u4f5c\uff01\u7576\u5e74\u89ba\u5f97\u5c4c\u70b8\u5929\n    txtVoteTitle.text = vote.title\n    btnSubmitPoll.setOnClickListener { presenter.submitPoll() }\n}<\/code><\/pre>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<h2 class=\"wp-block-heading\">\u4e09\u3001 \u7406\u60f3\u5f88\u8c50\u6eff\uff0c\u73fe\u5be6\u5f88\u9aa8\u611f\uff1a\u90a3\u4e9b\u4e3b\u6d41\u6587\u7ae0\u4e0d\u6562\u544a\u8a34\u4f60\u7684 Kotlin \u7ffb\u8eca\u73fe\u5834<\/h2>\n<p>\u5982\u679c\u4f60\u4ee5\u70ba\u8f49\u5230 Kotlin \u5f9e\u6b64\u5929\u4e0b\u592a\u5e73\uff0c\u90a3\u4f60\u80af\u5b9a\u6c92\u5728\u7b2c\u4e00\u7dda\u8e29\u904e\u5751\u3002\u7576\u6211\u628a\u5168\u90e8\u4ee3\u78bc\u8f49\u904e\u53bb\u4e4b\u5f8c\uff0c\u8fce\u63a5\u6211\u7684\u4e0d\u662f\u638c\u8072\uff0c\u800c\u662f\u4e00\u9023\u4e32\u63a5\u8e35\u800c\u4f86\u7684\u9748\u7570\u73fe\u8c61\uff1a<\/p>\n<h3 class=\"wp-block-heading\">1. Mockito \u55ae\u5143\u6e2c\u8a66\u88ab Kotlin\u300c\u9810\u8a2d Final\u300d\u75db\u6241\u4e00\u9813<\/h3>\n<p>\u4e0a\u4e00\u7bc7\u6211\u6700\u81ea\u8c6a\u7684\u5c31\u662f\u66ff Funny Vote \u5efa\u7f6e\u4e86\u5b8c\u6574\u7684 Mockito \u55ae\u5143\u6e2c\u8a66\u8207 Travis CI \u81ea\u52d5\u5316\u6d41\u7a0b\u3002\u7d50\u679c\u5207\u5230 Kotlin \u5f8c\uff0c\u57f7\u884c <code>.\/gradlew test<\/code>\uff0c\u6574\u500b Terminal \u5674\u51fa\u6eff\u6c5f\u7d05\u7684\u6e2c\u8a66\u932f\u8aa4\uff1a<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-text\">org.mockito.exceptions.base.MockitoException: \nCannot mock\/spy class com.heaton.funnyvote.data.VoteDataRepository\nMockito cannot mock\/spy because :\n - final class<\/code><\/pre>\n<p><strong>\u6211\u7576\u5834\u50bb\u773c\u3002<\/strong> Java \u6642\u4ee3\u6240\u6709 class \u8207 method \u9810\u8a2d\u90fd\u662f <code>open<\/code>\uff08\u53ef\u88ab\u7e7c\u627f\u8207\u8986\u5beb\uff09\uff1b\u4f46 Kotlin \u70ba\u4e86\u63a8\u5d07 Effective Java \u7684\u7406\u5ff5\uff0c<strong>\u6240\u6709\u7684\u985e\u5225\u8207\u65b9\u6cd5\u9810\u8a2d\u5168\u90fd\u662f <code>final<\/code>\uff01<\/strong> \u504f\u504f Mockito 2.x \u65e9\u671f\u4f9d\u8cf4\u52d5\u614b\u4ee3\u7406\uff08Cglib \/ ByteBuddy\uff09\u4f86\u5efa\u7acb Mock \u7269\u4ef6\uff0c\u9047\u5230 <code>final<\/code> \u76f4\u63a5\u8209\u96d9\u624b\u6295\u964d\u3002<\/p>\n<p>\u70ba\u4e86\u4fee\u5fa9\u9019\u500b\u667a\u969c\u554f\u984c\uff0c\u6211\u4e0d\u5f97\u4e0d\u53bb\u7ffb GitHub \u8352\u90ca\u91ce\u5916\u7684\u89e3\u6cd5\uff1a\u8981\u561b\u624b\u52d5\u5728\u6240\u6709\u8981\u6e2c\u8a66\u7684\u985e\u5225\u524d\u9762\u52a0\u4e0a <code>open<\/code>\uff08\u7b49\u65bc\u70ba\u4e86\u6e2c\u8a66\u7834\u58de\u751f\u7522\u4ee3\u78bc\u7684\u5c01\u88dd\uff09\uff1b\u8981\u561b\u53bb\u958b\u4e00\u500b\u795e\u7955\u7684\u6a94\u6848 <code>src\/test\/resources\/mockito-extensions\/org.mockito.plugins.MockMaker<\/code>\uff0c\u5728\u88e1\u9762\u5beb\u4e0a\u4e00\u884c\u51b7\u9580\u8a2d\u5b9a <code>mock-maker-inline<\/code>\uff1b\u6700\u5f8c\u9084\u5f97\u70ba\u4e86 Kotlin \u7684 Nullable \u6a5f\u5236\uff0c\u624b\u5beb\u4e00\u5957\u81ea\u88fd\u7684 <code>MockitoKotlinHelpers.kt<\/code>\uff1a<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-kotlin\">\/\/ 2018 \u5e74\u5728 Funny Vote \u88e1\u70ba\u4e86\u8b93 Mockito \u9589\u5634\u800c\u624b\u5beb\u7684 Helper\nfun &lt;T&gt; eq(obj: T): T = Mockito.eq&lt;T&gt;(obj)\nfun &lt;T&gt; any(): T = Mockito.any&lt;T&gt;()\ninline fun &lt;reified T : Any&gt; argumentCaptor(): ArgumentCaptor&lt;T&gt; = \n    ArgumentCaptor.forClass(T::class.java)<\/code><\/pre>\n<p>\u5beb\u5b8c\u771f\u7684\u60f3\u4ef0\u5929\u9577\u7b11\uff1a\u6211\u5230\u5e95\u662f\u4f86\u4eab\u53d7 Kotlin \u7684\u512a\u96c5\uff0c\u9084\u662f\u4f86\u5e6b\u6e2c\u8a66\u6846\u67b6\u64e6\u5c41\u80a1\u7684\uff1f<\/p>\n<h3 class=\"wp-block-heading\">2. Anko \u7684\u77ed\u547d\u80cc\u53db\uff1a\u8e29\u9032\u88ab\u5b98\u65b9\u62cb\u68c4\u7684\u751c\u871c\u9677\u9631<\/h3>\n<p>\u90a3\u6642\u5019\u5354\u7a0b\uff08Coroutines\uff09\u5728 Kotlin 1.2 \u9084\u639b\u8457\u300cExperimental\uff08\u5be6\u9a57\u6027\uff09\u300d\u7684\u8b66\u544a\u6a19\u8a8c\uff0c\u6c92\u4eba\u6562\u5728\u6b63\u5f0f\u5c08\u6848\u7528\u3002\u90a3\u80cc\u666f\u57f7\u884c\u7dd2\u600e\u9ebc\u89e3\uff1f<br \/>JetBrains \u5b98\u65b9\u7576\u6642\u5927\u529b\u63a8\u92b7\u81ea\u5bb6\u7684\u5957\u4ef6\u5eab\u2014\u2014<strong>Anko<\/strong>\u3002\u5b83\u63d0\u4f9b\u4e86\u4e00\u500b\u770b\u8d77\u4f86\u7121\u6bd4\u7d72\u6ed1\u7684\u8a9e\u6cd5\u7cd6 <code>doAsync { ... uiThread { ... } }<\/code>\uff0c\u76f4\u63a5\u53d6\u4ee3\u96e3\u7528\u7684 <code>AsyncTask<\/code>\uff1a<\/p>\n<pre class=\"wp-block-code\"><code class=\"language-kotlin\">\/\/ \u7576\u5e74\u89ba\u5f97\u597d\u68d2\u68d2\u7684 Anko \u5beb\u6cd5\ndoAsync {\n    val vote = repository.getVoteFromRemote(voteId)\n    uiThread {\n        view.showVote(vote)\n    }\n}<\/code><\/pre>\n<p>\u7d50\u679c\u5462\uff1f\u9019\u6771\u897f\u7528\u8d77\u4f86\u6709\u591a\u723d\uff0c\u5e7e\u5e74\u5f8c\u6b7b\u5f97\u5c31\u6709\u591a\u6158\u3002\u5e7e\u5e74\u5f8c JetBrains \u5b98\u65b9\u7121\u60c5\u5ba3\u5e03 <strong>\u300cAnko \u6c38\u4e45\u5ee2\u68c4\uff08Deprecated\uff09\u300d<\/strong>\uff0c\u6574\u500b\u793e\u7fa4\u9813\u6642\u8b8a\u6210\u5c0f\u4e11\uff0c\u5168\u90e8\u5c08\u6848\u53c8\u5f97\u56de\u982d\u91cd\u5beb\u3002\u9019\u4ef6\u4e8b\u6559\u6703\u4e86\u6211\u8eab\u70ba\u5de5\u7a0b\u5e2b\u6700\u91cd\u8981\u7684\u4e00\u5802\u8ab2\uff1a<strong>\u5343\u842c\u4e0d\u8981\u70ba\u4e86\u4e00\u6642\u7684\u8a9e\u6cd5\u7cd6\u723d\u611f\uff0c\u628a\u8eab\u5bb6\u6027\u547d\u62bc\u5728\u975e\u6a19\u6e96\u5eab\u7684\u73a9\u5177\u4e0a\u3002<\/strong><\/p>\n<h3 class=\"wp-block-heading\">3. Kotlin Synthetics \u9047\u5230 <code>&lt;include&gt;<\/code> Layout \u7684\u898b\u9b3c\u65e5\u5e38<\/h3>\n<p>\u9084\u6709\u524d\u9762\u63d0\u5230\u7684 Kotlin Synthetics\uff0c\u5b83\u5728\u55ae\u7d14\u7684 XML \u4e0a\u904b\u4f5c\u826f\u597d\uff0c\u4f46 Funny Vote \u7684\u8a73\u60c5\u9801\u5f88\u8907\u96dc\uff0c\u62c6\u6210\u4e86 <code>include_toolbar.xml<\/code>\u3001<code>include_author.xml<\/code>\u3001<code>include_function_bar.xml<\/code>\u3002<br \/>\u7576\u4f60\u5728\u591a\u500b <code>&lt;include&gt;<\/code> \u88e1\u4e0d\u5c0f\u5fc3\u7528\u4e86\u76f8\u540c\u7684\u547d\u540d\uff08\u4f8b\u5982\u90fd\u53eb <code>txtTitle<\/code> \u6216 <code>imgIcon<\/code>\uff09\uff0cSynthetics \u5728\u81ea\u52d5\u751f\u6210\u5feb\u53d6 View \u6642\u5c31\u6703\u7cbe\u795e\u5206\u88c2\uff0c\u5728 Fragment \u88e1\u66f4\u662f\u4e09\u4e0d\u4e94\u6642\u7d66\u4f60\u6293\u932f View \u751a\u81f3\u662f\u76f4\u63a5\u62cb\u51fa <code>NullPointerException<\/code>\u3002<\/p>\n<p>\u6700\u8af7\u523a\u7684\u662f\u4ec0\u9ebc\uff1f<strong>Kotlin \u4e3b\u6253\u300c\u6d88\u6ec5 NPE\u300d\uff0c\u7d50\u679c\u4f60\u7528\u5b83\u7684\u5b98\u65b9\u63d2\u4ef6 Synthetics\uff0c\u5728\u904b\u884c\u671f\u5674\u51fa\u4e00\u500b\u66f4\u96e3\u9664\u932f\u7684 NPE\u3002<\/strong><br \/><em>(\u9019\u4e5f\u662f\u70ba\u4ec0\u9ebc Google \u5f8c\u4f86\u75db\u5b9a\u601d\u75db\uff0c\u628a Synthetics \u6574\u7d44\u5224\u6b7b\u5211\uff0c\u5168\u9762\u6539\u63a8 ViewBinding \u8207\u5f8c\u4f86\u7684 Jetpack Compose)<\/em>\u3002<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n<h2 class=\"wp-block-heading\">\u56db\u3001 \u672c\u7bc7\u7e3d\u7d50\uff1a\u91cd\u69cb\u9019\u6a94\u4e8b\uff0c\u75db\u4e26\u5feb\u6a02\u8457<\/h2>\n<p>\u6298\u9a30\u4e86\u5e7e\u9031\uff0c\u628a <code>mvp_rxjava<\/code> \u8207 <code>mvp_kotlin<\/code> \u9019\u5169\u5ea7\u5927\u5c71\u7ffb\u904e\u53bb\u4e4b\u5f8c\uff0cFunny Vote \u78ba\u5be6\u8b8a\u5f97\u66f4\u73fe\u4ee3\u4e86\u4e00\u9ede\u9ede\u3002\u81f3\u5c11\u5728 2018 \u5e74\u7684\u7576\u4e0b\uff0c\u6253\u958b\u4ee3\u78bc\u5eab\u770b\u8457\u5168 Kotlin \u7684 class\u3001\u7c21\u6f54\u7684 data class\uff0c\u4ee5\u53ca\u900f\u904e RxJava \u4e32\u8d77\u4f86\u7684\u8cc7\u6599\u7ba1\u7dda\uff0c\u5fc3\u88e1\u9084\u662f\u6709\u4e00\u9ede\u9ede\u5fae\u8584\u7684\u6210\u5c31\u611f\u3002<\/p>\n<p>\u4f46\u4f60\u554f\u6211\u554f\u984c\u5168\u90fd\u89e3\u6c7a\u4e86\u55ce\uff1f<strong>\u5b8c\u5168\u6c92\u6709\u3002<\/strong><\/p>\n<p>\u672c\u8cea\u4e0a\uff0c\u5b83\u4f9d\u7136\u662f\u4e00\u96bb\u5957\u8457 Kotlin \u5916\u76ae\u7684 <strong>MVP \u5de8\u7378<\/strong>\uff1a<\/p>\n<ul>\n<li>Presenter \u4f9d\u7136\u8981\u624b\u52d5\u7dad\u8b77 View \u7684\u4ecb\u9762\u751f\u547d\u9031\u671f\uff1b<\/li>\n<li>\u65cb\u8f49\u87a2\u5e55\u6642\uff0c\u90a3\u4e9b Callback \u8207 Subscription \u4f9d\u7136\u50cf\u672a\u7206\u5f48\u4e00\u6a23\u8b93\u4eba\u795e\u7d93\u7dca\u7e43\uff1b<\/li>\n<li>\u756b\u9762\u4e0a\u90a3\u4e9b 30+ \u4efd XML \u6a94\u6848\u4f9d\u7136\u5728\u6bcf\u6b21\u6539\u9700\u6c42\u6642\uff0c\u903c\u8457\u4f60\u53bb\u7ffb\u627e View \u7684\u968e\u5c64\u3002<\/li>\n<\/ul>\n<p>\u9019\u4e5f\u662f\u70ba\u4ec0\u9ebc\u5728\u6c89\u5bc2\u4e86\u5e7e\u5e74\u5f8c\uff0c\u7576\u6211\u770b\u8457 Android \u5b98\u65b9\u628a Kotlin \u63a8\u9032\u5230 2.0\u3001\u5fb9\u5e95\u6dd8\u6c70 XML \u7aef\u51fa <strong>Jetpack Compose<\/strong>\uff0c\u4ee5\u53ca\u5c07\u67b6\u69cb\u6536\u6582\u81f3 <strong>MVI\uff08\u55ae\u5411\u8cc7\u6599\u6d41 UDF\uff09<\/strong> \u6642\uff0c\u6211\u5fc3\u88e1\u90a3\u80a1\u5f37\u8feb\u75c7\u7684\u706b\u82d7\u53c8\u518d\u6b21\u88ab\u9ede\u71c3\u4e86\u2026\u2026<\/p>\n<p>\u81f3\u65bc\u5f9e XML \u8de8\u8d8a\u5230 Compose \u906d\u9047\u7684\u300c\u72c0\u614b\u6495\u88c2\u707d\u96e3\u300d\uff0c\u4ee5\u53ca\u70ba\u4e86\u89e3\u6c7a\u9019\u500b\u554f\u984c\u4e0d\u5f97\u4e0d\u76f4\u4e0a\u7684 MVI \u67b6\u69cb\uff0c\u6211\u5011\u5c31\u4e0b\u7bc7\u7e7c\u7e8c murmur \u5427\uff01\uff08\u9019\u6b21\u6211\u4fdd\u8b49\u4e0d\u6703\u518d\u62d6\u5341\u5e74\u4e86\uff0c\u61c9\u8a72\u5566\u3002\uff09<\/p>\n<blockquote class=\"wp-block-quote\">\n<p>\u201cAny fool can write code that a computer can understand. Good programmers write code that humans can understand.\u201d<br \/>\u2014 Martin Fowler<\/p>\n<p><em>(\u4f46\u5728 2018 \u5e74\u5beb Kotlin + Mockito \u7684\u6642\u5019\uff0c\u6211\u89ba\u5f97\u9023\u96fb\u8166\u90fd\u5feb\u4e0d\u7406\u89e3\u6211\u4e86\u3002)<\/em><\/p>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>\u597d\u4e00\u9663\u5b50\u2026\u2026\u597d\u5427\uff0c\u4e0d\u662f\u597d\u4e00\u9663\u5b50\uff0c\u662f\u904e\u4e86\u5feb\u5341\u5e74\u624d\u4f86\u66f4\u65b0\u4e0b\u4e00\u7bc7\u3002\u4e0a\u4e00\u7bc7\u5728\u7d50\u5c3e\u4fe1\u8a93\u65e6\u65e6\u653e\u8a71\u8aaa\uff1a\u300c\u81f3\u65bc Kotlin  [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1285,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[86,169],"tags":[6,175,179,128],"_links":{"self":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1284"}],"collection":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1284"}],"version-history":[{"count":3,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1284\/revisions"}],"predecessor-version":[{"id":1307,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1284\/revisions\/1307"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/media\/1285"}],"wp:attachment":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1284"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1284"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1284"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}