[{"data":1,"prerenderedAt":4213},["ShallowReactive",2],{"navigation_docs":3,"-android-state-management":181,"-android-state-management-surround":4208},[4,27,34,41,116,146,153,169],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"博客","i-lucide-pen-line","\u002Fblog","0.blog",[10,12,17,22],{"title":5,"path":7,"stem":11,"icon":6},"0.blog\u002Findex",{"title":13,"path":14,"stem":15,"icon":16},"创建基于 UIKit 的原生 iOS 项目","\u002Fblog\u002Fcreate-uikit-ios-project","0.blog\u002F01.create-uikit-ios-project","i-lucide-app-window",{"title":18,"path":19,"stem":20,"icon":21},"Lockify：把账号和密码留在自己的手机里","\u002Fblog\u002Flockify","0.blog\u002F02.lockify","i-lucide-lock",{"title":23,"path":24,"stem":25,"icon":26},"PicDepot：只靠 Cloudflare 的自建图床","\u002Fblog\u002Fpicdepot","0.blog\u002F03.picdepot","i-lucide-images",{"title":28,"path":29,"stem":30,"children":31,"icon":33},"年度总结","\u002Fannual-review","1.annual-review\u002Findex",[32],{"title":28,"path":29,"stem":30,"icon":33},"i-lucide-git-commit",{"title":35,"path":36,"stem":37,"children":38,"icon":40},"故事","\u002Fstories","2.stories\u002Findex",[39],{"title":35,"path":36,"stem":37,"icon":40},"i-lucide-feather",{"title":42,"icon":43,"path":44,"stem":45,"children":46},"Android","i-simple-icons-android","\u002Fandroid","3.android",[47,49,54,112],{"title":42,"path":44,"stem":48,"icon":43},"3.android\u002Findex",{"title":50,"path":51,"stem":52,"icon":53},"初探Navigation","\u002Fandroid\u002Fcompose-navigation","3.android\u002F01.compose-navigation","i-lucide-route",{"title":55,"icon":56,"path":57,"stem":58,"children":59},"安卓代码加固","i-lucide-shield","\u002Fandroid\u002Fhidden-hardening","3.android\u002F02.hidden-hardening\u002Findex",[60,63,67,72,77,82,87,92,97,102,107],{"title":61,"path":57,"stem":58,"icon":62},"安卓代码加固：总览","i-lucide-map",{"title":64,"path":65,"stem":66,"icon":21},"安卓代码加固（1）：加密DEX","\u002Fandroid\u002Fhidden-hardening\u002Fencrypt-dex","3.android\u002F02.hidden-hardening\u002F01.encrypt-dex",{"title":68,"path":69,"stem":70,"icon":71},"安卓代码加固（2）：Native解密","\u002Fandroid\u002Fhidden-hardening\u002Fnative-decrypt","3.android\u002F02.hidden-hardening\u002F02.native-decrypt","i-lucide-cpu",{"title":73,"path":74,"stem":75,"icon":76},"安卓代码加固（3）：内存加载","\u002Fandroid\u002Fhidden-hardening\u002Fmemory-load","3.android\u002F02.hidden-hardening\u002F03.memory-load","i-lucide-memory-stick",{"title":78,"path":79,"stem":80,"icon":81},"安卓代码加固（4）：指令分发","\u002Fandroid\u002Fhidden-hardening\u002Fopcode-dispatch","3.android\u002F02.hidden-hardening\u002F04.opcode-dispatch","i-lucide-hash",{"title":83,"path":84,"stem":85,"icon":86},"安卓代码加固（5）：XOR混淆","\u002Fandroid\u002Fhidden-hardening\u002Fxor-obfuscate","3.android\u002F02.hidden-hardening\u002F05.xor-obfuscate","i-lucide-shuffle",{"title":88,"path":89,"stem":90,"icon":91},"安卓代码加固（6）：密钥外置","\u002Fandroid\u002Fhidden-hardening\u002Fkey-external","3.android\u002F02.hidden-hardening\u002F06.key-external","i-lucide-key",{"title":93,"path":94,"stem":95,"icon":96},"安卓代码加固（7）：依赖隔离","\u002Fandroid\u002Fhidden-hardening\u002Fdep-isolate","3.android\u002F02.hidden-hardening\u002F07.dep-isolate","i-lucide-layers",{"title":98,"path":99,"stem":100,"icon":101},"安卓代码加固（8）：构建校验","\u002Fandroid\u002Fhidden-hardening\u002Fbuild-verify","3.android\u002F02.hidden-hardening\u002F08.build-verify","i-lucide-shield-check",{"title":103,"path":104,"stem":105,"icon":106},"安卓代码加固（9）：防录屏","\u002Fandroid\u002Fhidden-hardening\u002Fflag-secure","3.android\u002F02.hidden-hardening\u002F09.flag-secure","i-lucide-eye-off",{"title":108,"path":109,"stem":110,"icon":111},"安卓代码加固（10）：ABI门禁","\u002Fandroid\u002Fhidden-hardening\u002Fabi-gate","3.android\u002F02.hidden-hardening\u002F10.abi-gate","i-lucide-git-compare",{"title":113,"path":114,"stem":115,"icon":96},"状态管理","\u002Fandroid\u002Fstate-management","3.android\u002F03.state-management",{"title":117,"icon":118,"path":119,"stem":120,"children":121},"iOS","i-simple-icons-apple","\u002Fios","4.ios",[122,124,129,134,138,141],{"title":117,"path":119,"stem":123,"icon":118},"4.ios\u002Findex",{"title":125,"path":126,"stem":127,"icon":128},"iOS面试题1","\u002Fios\u002Fios-interview1","4.ios\u002F01.ios-interview1","i-lucide-sparkles",{"title":130,"path":131,"stem":132,"icon":133},"设置启动图","\u002Fios\u002Fset-launch-screen","4.ios\u002F02.set-launch-screen","i-lucide-image",{"title":135,"path":136,"stem":137,"icon":53},"路由管理","\u002Fios\u002Frouting","4.ios\u002F03.routing",{"title":113,"path":139,"stem":140,"icon":96},"\u002Fios\u002Fstate-management","4.ios\u002F04.state-management",{"title":142,"path":143,"stem":144,"icon":145},"事件传递","\u002Fios\u002Fevent-delivery","4.ios\u002F05.event-delivery","i-lucide-pointer",{"title":147,"path":148,"stem":149,"children":150,"icon":152},"Flutter","\u002Fflutter","5.flutter\u002Findex",[151],{"title":147,"path":148,"stem":149,"icon":152},"i-simple-icons-flutter",{"title":154,"icon":155,"path":156,"stem":157,"children":158,"page":168},"面试","i-lucide-graduation-cap","\u002Finterview","6.interview",[159,164],{"title":160,"path":161,"stem":162,"icon":163},"Flutter面试题1","\u002Finterview\u002Fflutter-interview1","6.interview\u002F01.flutter-interview1","i-lucide-boxes",{"title":165,"path":166,"stem":167,"icon":71},"Flutter面试题2","\u002Finterview\u002Fflutter-interview2","6.interview\u002F02.flutter-interview2",false,{"title":170,"icon":171,"path":172,"stem":173,"children":174},"算法学习","i-lucide-binary","\u002Falgorithm","7.algorithm",[175,177],{"title":170,"path":172,"stem":176,"icon":171},"7.algorithm\u002Findex",{"title":178,"path":179,"stem":180},"算法基础1：复杂度分析","\u002Falgorithm\u002Ftime-space-complexity","7.algorithm\u002F01.time-space-complexity",{"id":182,"title":113,"body":183,"description":4187,"extension":4188,"links":4189,"meta":4199,"navigation":4203,"path":114,"seo":4204,"stem":115,"__hash__":4207},"docs\u002F3.android\u002F03.state-management.md",{"type":184,"value":185,"toc":4168},"minimark",[186,190,211,229,233,243,349,352,358,540,553,567,572,576,583,607,1009,1034,1037,1146,1155,1161,1185,1208,1225,1228,1234,1249,1363,1395,1408,1419,1422,1440,1455,1473,1477,1491,1494,1509,1520,1526,1546,1566,1569,1581,1587,1602,1605,1619,1622,1773,1776,1780,1794,1906,1975,1981,1987,1990,1998,2224,2227,2365,2400,2405,2408,2436,2619,2643,2777,2789,2803,2806,2823,3251,3275,3284,3287,3298,3555,3560,3580,3586,3589,3611,3880,3954,3976,3979,3985,3988,4078,4081,4164],[187,188,189],"p",{},"我把 Android 的状态按活多久分成四层。先问三件事：状态放在哪，谁可以改，改完界面怎么跟上。",[187,191,192,193,197,198,201,202,205,206,210],{},"Compose 用 ",[194,195,196],"code",{},"remember"," 回答最短的那一截。一屏的字段和动作放进 ViewModel，界面读一份 ",[194,199,200],{},"UiState","。Room 和 DataStore 的 ",[194,203,204],{},"Flow"," 活在进程里的仓库上。导航栈单独记在 ",[207,208,209],"a",{"href":51},"初探 Navigation","。Orbit、Mavericks、Molecule、Circuit、MVIKotlin 再规定动作和副作用走哪条函数。界面最后仍是读这份状态。",[187,212,213,214,216,217,220,221,224,225,228],{},"记账应用是手写的这一套：仓库出 ",[194,215,204],{},"，ViewModel 收成 ",[194,218,219],{},"StateFlow","，Route 用 ",[194,222,223],{},"collectAsStateWithLifecycle()"," 订阅，纯展示的 Screen 只接收 ",[194,226,227],{},"uiState"," 和回调。下面先按这个记，再看三方库把同一条路收成什么形状。",[230,231,232],"h2",{"id":232},"四层",[234,235,241],"pre",{"className":236,"code":238,"language":239,"meta":240},[237],"language-text","这一块组合\n  remember                 重组还在，离开组合就没了\n  rememberSaveable         旋转和进程重建还在，值要能保存\n  derivedStateOf           从别的 Compose 状态算出来\n\n这一屏\n  ViewModel\n  StateFlow\u003CUiState>       当前值，后来的订阅者马上拿到\n  SharedFlow               只发生一次，不留在状态里\n  viewModel(key = …)       同一个存储里区分实例\n\n这一进程\n  Repository               单例\n  Room \u002F DataStore 的 Flow 查询结果和偏好\n\n导航\n  NavBackStack、当前标签    哪一页在栈上，和这一页的字段分开\n","text","",[194,242,238],{"__ignoreMap":240},[244,245,246,265],"table",{},[247,248,249],"thead",{},[250,251,252,256,259,262],"tr",{},[253,254,255],"th",{},"层",[253,257,258],{},"谁持有",[253,260,261],{},"旋转之后",[253,263,264],{},"进程被杀之后",[266,267,268,283,301,318,331],"tbody",{},[250,269,270,275,278,281],{},[271,272,273],"td",{},[194,274,196],{},[271,276,277],{},"这块组合",[271,279,280],{},"丢",[271,282,280],{},[250,284,285,290,296,299],{},[271,286,287],{},[194,288,289],{},"rememberSaveable",[271,291,292,293],{},"这块组合，经 ",[194,294,295],{},"SavedState",[271,297,298],{},"还在",[271,300,298],{},[250,302,303,308,313,315],{},[271,304,305,306],{},"ViewModel 里的 ",[194,307,219],{},[271,309,310],{},[194,311,312],{},"ViewModelStore",[271,314,298],{},[271,316,317],{},"重新创建，初值重新来",[250,319,320,323,326,328],{},[271,321,322],{},"仓库里的 Flow",[271,324,325],{},"进程内单例",[271,327,298],{},[271,329,330],{},"从数据库或 DataStore 再读",[250,332,333,339,344,346],{},[271,334,335,336],{},"返回栈上的 ",[194,337,338],{},"NavKey",[271,340,341],{},[194,342,343],{},"rememberNavBackStack",[271,345,298],{},[271,347,348],{},"键能序列化就还在",[230,350,351],{"id":351},"组合里的值",[187,353,354,355,357],{},"一个对话框开没开、一次还没提交的输入，先放在调用它的组合里。",[194,356,196],{}," 把存储留在这块组合的身份上。组合函数再次执行时，拿到的是上次的值。",[234,359,364],{"className":360,"code":361,"filename":362,"language":363,"meta":240,"style":240},"language-kotlin shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","@Composable\ninternal fun AddExpenseRoute(viewModel: AddExpenseViewModel) {\n    \u002F\u002F 方式：rememberSaveable\n    \u002F\u002F 对话框开关跟着这块组合走。旋转之后还开着。\n    \u002F\u002F 进程重建时布尔值能写进 SavedState，所以用 rememberSaveable。\n    var showCreate by rememberSaveable { mutableStateOf(false) }\n    val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n    AddExpenseScreen(\n        uiState = uiState,\n        showCreateCategory = showCreate,\n        onShowCreateCategory = { showCreate = true },\n    )\n}\n","CreateCategoryDialog.kt","kotlin",[194,365,366,375,400,407,413,419,450,470,477,486,498,509,528,534],{"__ignoreMap":240},[367,368,371],"span",{"class":369,"line":370},"line",1,[367,372,374],{"class":373},"sBMFI","@Composable\n",[367,376,378,382,386,390,394,397],{"class":369,"line":377},2,[367,379,381],{"class":380},"spNyl","internal",[367,383,385],{"class":384},"sMK4o"," fun",[367,387,389],{"class":388},"s2Zo4"," AddExpenseRoute",[367,391,393],{"class":392},"sTEyZ","(viewModel: ",[367,395,396],{"class":373},"AddExpenseViewModel",[367,398,399],{"class":392},") {\n",[367,401,403],{"class":369,"line":402},3,[367,404,406],{"class":405},"sHwdD","    \u002F\u002F 方式：rememberSaveable\n",[367,408,410],{"class":369,"line":409},4,[367,411,412],{"class":405},"    \u002F\u002F 对话框开关跟着这块组合走。旋转之后还开着。\n",[367,414,416],{"class":369,"line":415},5,[367,417,418],{"class":405},"    \u002F\u002F 进程重建时布尔值能写进 SavedState，所以用 rememberSaveable。\n",[367,420,422,425,428,431,434,437,440,443,447],{"class":369,"line":421},6,[367,423,424],{"class":384},"    var",[367,426,427],{"class":392}," showCreate ",[367,429,430],{"class":380},"by",[367,432,433],{"class":388}," rememberSaveable",[367,435,436],{"class":392}," { ",[367,438,439],{"class":388},"mutableStateOf",[367,441,442],{"class":392},"(",[367,444,446],{"class":445},"sfNiH","false",[367,448,449],{"class":392},") }\n",[367,451,453,456,459,461,464,467],{"class":369,"line":452},7,[367,454,455],{"class":384},"    val",[367,457,458],{"class":392}," uiState ",[367,460,430],{"class":380},[367,462,463],{"class":392}," viewModel.uiState.",[367,465,466],{"class":388},"collectAsStateWithLifecycle",[367,468,469],{"class":392},"()\n",[367,471,473],{"class":369,"line":472},8,[367,474,476],{"emptyLinePlaceholder":475},true,"\n",[367,478,480,483],{"class":369,"line":479},9,[367,481,482],{"class":388},"    AddExpenseScreen",[367,484,485],{"class":392},"(\n",[367,487,489,492,495],{"class":369,"line":488},10,[367,490,491],{"class":392},"        uiState ",[367,493,494],{"class":384},"=",[367,496,497],{"class":392}," uiState,\n",[367,499,501,504,506],{"class":369,"line":500},11,[367,502,503],{"class":392},"        showCreateCategory ",[367,505,494],{"class":384},[367,507,508],{"class":392}," showCreate,\n",[367,510,512,515,517,520,522,525],{"class":369,"line":511},12,[367,513,514],{"class":392},"        onShowCreateCategory ",[367,516,494],{"class":384},[367,518,519],{"class":392}," { showCreate ",[367,521,494],{"class":384},[367,523,524],{"class":445}," true",[367,526,527],{"class":392}," },\n",[367,529,531],{"class":369,"line":530},13,[367,532,533],{"class":392},"    )\n",[367,535,537],{"class":369,"line":536},14,[367,538,539],{"class":392},"}\n",[187,541,542,543,545,546,549,550,552],{},"纯展示的 Screen 不自己 ",[194,544,196],{}," 这份开关。开关从 Route 传进来，预览时直接给 ",[194,547,548],{},"true"," 或 ",[194,551,446],{},"。",[187,554,555,556,558,559,562,563,566],{},"能从别的状态算出来的值做成计算，例如金额字符串空不空。再存一份 ",[194,557,196],{},"，用 ",[194,560,561],{},"LaunchedEffect"," 去同步，两份会分叉。",[194,564,565],{},"derivedStateOf"," 留给「输入每次变，但派生结果很少变」：过滤条件在动，过滤后的列表引用要稳定，避免读到它的子组合每次都重组。",[187,568,569,571],{},[194,570,196],{}," 的初值表达式每次进入组合都会执行，Compose 只采纳第一次的结果。列表、账户、网络结果放在下一层的 ViewModel 里。",[230,573,575],{"id":574},"一屏一份-uistate","一屏一份 UiState",[187,577,578,579,582],{},"这一屏要给界面的字段收成一个 data class。用户做的事收成 ",[194,580,581],{},"sealed interface","。ViewModel 持有这两样，Screen 只画、只把事件交上去。",[187,584,585,586,589,590,593,594,597,598,600,601,603,604,606],{},"记账首页的当前账户、本月收支、预算和最近账单在 ",[194,587,588],{},"BillHomeUiState"," 里。点账户芯片是 ",[194,591,592],{},"BillHomeUiEvent.Select","。ViewModel 里 ",[194,595,596],{},"onEvent"," 调用仓库把「正在展示的账户」写回去，首页的 ",[194,599,204],{}," 随后发出新的一行，",[194,602,227],{}," 跟着变。界面不写数据库，也不改 ",[194,605,227],{}," 的字段。",[234,608,611],{"className":360,"code":609,"filename":610,"language":363,"meta":240,"style":240},"data class HomeUiState(\n    val loading: Boolean = true,\n    val monthExpense: Long = 0,\n)\n\nsealed interface HomeUiEvent {\n    data class SelectAccount(val accountId: Long) : HomeUiEvent\n}\n\nclass HomeViewModel(\n    private val stats: StatsRepository,\n) : ViewModel() {\n    val uiState: StateFlow\u003CHomeUiState> = stats.observeMonth()\n        .map { summary ->\n            HomeUiState(loading = false, monthExpense = summary.expenseAmount)\n        }\n        .stateIn(\n            scope = viewModelScope,\n            \u002F\u002F 订阅者离开 5 秒内再回来，上游查询不断。\n            \u002F\u002F 再久就把上游停掉，下次进入重新收集。\n            started = SharingStarted.WhileSubscribed(5_000),\n            initialValue = HomeUiState(),\n        )\n\n    fun onEvent(event: HomeUiEvent) {\n        when (event) {\n            is HomeUiEvent.SelectAccount -> select(event.accountId)\n        }\n    }\n\n    private fun select(accountId: Long) {\n        viewModelScope.launch { stats.setDisplayed(accountId) }\n    }\n}\n","HomeViewModel.kt",[194,612,613,626,644,662,667,671,685,711,715,719,729,745,755,783,797,819,825,835,846,852,858,880,893,899,904,921,931,949,954,960,965,981,999,1004],{"__ignoreMap":240},[367,614,615,618,621,624],{"class":369,"line":370},[367,616,617],{"class":380},"data",[367,619,620],{"class":384}," class",[367,622,623],{"class":373}," HomeUiState",[367,625,485],{"class":392},[367,627,628,630,633,636,639,641],{"class":369,"line":377},[367,629,455],{"class":384},[367,631,632],{"class":392}," loading: ",[367,634,635],{"class":373},"Boolean",[367,637,638],{"class":384}," =",[367,640,524],{"class":445},[367,642,643],{"class":392},",\n",[367,645,646,648,651,654,656,660],{"class":369,"line":402},[367,647,455],{"class":384},[367,649,650],{"class":392}," monthExpense: ",[367,652,653],{"class":373},"Long",[367,655,638],{"class":384},[367,657,659],{"class":658},"sbssI"," 0",[367,661,643],{"class":392},[367,663,664],{"class":369,"line":409},[367,665,666],{"class":392},")\n",[367,668,669],{"class":369,"line":415},[367,670,476],{"emptyLinePlaceholder":475},[367,672,673,676,679,682],{"class":369,"line":421},[367,674,675],{"class":380},"sealed",[367,677,678],{"class":384}," interface",[367,680,681],{"class":373}," HomeUiEvent",[367,683,684],{"class":392}," {\n",[367,686,687,690,692,695,697,700,703,705,708],{"class":369,"line":452},[367,688,689],{"class":380},"    data",[367,691,620],{"class":384},[367,693,694],{"class":373}," SelectAccount",[367,696,442],{"class":392},[367,698,699],{"class":384},"val",[367,701,702],{"class":392}," accountId: ",[367,704,653],{"class":373},[367,706,707],{"class":392},") : ",[367,709,710],{"class":373},"HomeUiEvent\n",[367,712,713],{"class":369,"line":472},[367,714,539],{"class":392},[367,716,717],{"class":369,"line":479},[367,718,476],{"emptyLinePlaceholder":475},[367,720,721,724,727],{"class":369,"line":488},[367,722,723],{"class":384},"class",[367,725,726],{"class":373}," HomeViewModel",[367,728,485],{"class":392},[367,730,731,734,737,740,743],{"class":369,"line":500},[367,732,733],{"class":380},"    private",[367,735,736],{"class":384}," val",[367,738,739],{"class":392}," stats: ",[367,741,742],{"class":373},"StatsRepository",[367,744,643],{"class":392},[367,746,747,749,752],{"class":369,"line":511},[367,748,707],{"class":392},[367,750,751],{"class":373},"ViewModel",[367,753,754],{"class":392},"() {\n",[367,756,757,759,762,764,767,770,773,775,778,781],{"class":369,"line":530},[367,758,455],{"class":384},[367,760,761],{"class":392}," uiState: ",[367,763,219],{"class":373},[367,765,766],{"class":392},"\u003C",[367,768,769],{"class":373},"HomeUiState",[367,771,772],{"class":392},"> ",[367,774,494],{"class":384},[367,776,777],{"class":392}," stats.",[367,779,780],{"class":388},"observeMonth",[367,782,469],{"class":392},[367,784,785,788,791,794],{"class":369,"line":536},[367,786,787],{"class":392},"        .",[367,789,790],{"class":388},"map",[367,792,793],{"class":392}," { summary ",[367,795,796],{"class":380},"->\n",[367,798,800,803,806,808,811,814,816],{"class":369,"line":799},15,[367,801,802],{"class":388},"            HomeUiState",[367,804,805],{"class":392},"(loading ",[367,807,494],{"class":384},[367,809,810],{"class":445}," false",[367,812,813],{"class":392},", monthExpense ",[367,815,494],{"class":384},[367,817,818],{"class":392}," summary.expenseAmount)\n",[367,820,822],{"class":369,"line":821},16,[367,823,824],{"class":392},"        }\n",[367,826,828,830,833],{"class":369,"line":827},17,[367,829,787],{"class":392},[367,831,832],{"class":388},"stateIn",[367,834,485],{"class":392},[367,836,838,841,843],{"class":369,"line":837},18,[367,839,840],{"class":392},"            scope ",[367,842,494],{"class":384},[367,844,845],{"class":392}," viewModelScope,\n",[367,847,849],{"class":369,"line":848},19,[367,850,851],{"class":405},"            \u002F\u002F 订阅者离开 5 秒内再回来，上游查询不断。\n",[367,853,855],{"class":369,"line":854},20,[367,856,857],{"class":405},"            \u002F\u002F 再久就把上游停掉，下次进入重新收集。\n",[367,859,861,864,866,869,872,874,877],{"class":369,"line":860},21,[367,862,863],{"class":392},"            started ",[367,865,494],{"class":384},[367,867,868],{"class":392}," SharingStarted.",[367,870,871],{"class":388},"WhileSubscribed",[367,873,442],{"class":392},[367,875,876],{"class":658},"5_000",[367,878,879],{"class":392},"),\n",[367,881,883,886,888,890],{"class":369,"line":882},22,[367,884,885],{"class":392},"            initialValue ",[367,887,494],{"class":384},[367,889,623],{"class":388},[367,891,892],{"class":392},"(),\n",[367,894,896],{"class":369,"line":895},23,[367,897,898],{"class":392},"        )\n",[367,900,902],{"class":369,"line":901},24,[367,903,476],{"emptyLinePlaceholder":475},[367,905,907,910,913,916,919],{"class":369,"line":906},25,[367,908,909],{"class":384},"    fun",[367,911,912],{"class":388}," onEvent",[367,914,915],{"class":392},"(event: ",[367,917,918],{"class":373},"HomeUiEvent",[367,920,399],{"class":392},[367,922,924,928],{"class":369,"line":923},26,[367,925,927],{"class":926},"s7zQu","        when",[367,929,930],{"class":392}," (event) {\n",[367,932,934,937,940,943,946],{"class":369,"line":933},27,[367,935,936],{"class":384},"            is",[367,938,939],{"class":392}," HomeUiEvent.SelectAccount ",[367,941,942],{"class":380},"->",[367,944,945],{"class":388}," select",[367,947,948],{"class":392},"(event.accountId)\n",[367,950,952],{"class":369,"line":951},28,[367,953,824],{"class":392},[367,955,957],{"class":369,"line":956},29,[367,958,959],{"class":392},"    }\n",[367,961,963],{"class":369,"line":962},30,[367,964,476],{"emptyLinePlaceholder":475},[367,966,968,970,972,974,977,979],{"class":369,"line":967},31,[367,969,733],{"class":380},[367,971,385],{"class":384},[367,973,945],{"class":388},[367,975,976],{"class":392},"(accountId: ",[367,978,653],{"class":373},[367,980,399],{"class":392},[367,982,984,987,990,993,996],{"class":369,"line":983},32,[367,985,986],{"class":392},"        viewModelScope.",[367,988,989],{"class":388},"launch",[367,991,992],{"class":392}," { stats.",[367,994,995],{"class":388},"setDisplayed",[367,997,998],{"class":392},"(accountId) }\n",[367,1000,1002],{"class":369,"line":1001},33,[367,1003,959],{"class":392},[367,1005,1007],{"class":369,"line":1006},34,[367,1008,539],{"class":392},[187,1010,1011,1013,1014,1016,1017,1019,1020,1023,1024,1027,1028,1030,1031,1033],{},[194,1012,832],{}," 把冷的 ",[194,1015,204],{}," 收成热的 ",[194,1018,219],{},"。后来的订阅者马上拿到当前值，首帧用 ",[194,1021,1022],{},"initialValue","，所以加载中是一份明确的空状态，金额先是 0 这个事实写在 ",[194,1025,1026],{},"loading"," 上。开屏等到首页 ",[194,1029,1026],{}," 变成 ",[194,1032,446],{},"（或最多再等 2 秒）才揭开，避免数字先显示 0 再跳成真实值。",[187,1035,1036],{},"Route 这样订阅：",[234,1038,1041],{"className":360,"code":1039,"filename":1040,"language":363,"meta":240,"style":240},"@Composable\ninternal fun HomeRoute(\n    viewModel: HomeViewModel = viewModel(factory = HomeViewModel.Factory),\n) {\n    val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n    HomeScreen(\n        uiState = uiState,\n        onSelectAccount = { id -> viewModel.onEvent(HomeUiEvent.SelectAccount(id)) },\n    )\n}\n","HomeRoute.kt",[194,1042,1043,1047,1058,1079,1083,1097,1104,1112,1138,1142],{"__ignoreMap":240},[367,1044,1045],{"class":369,"line":370},[367,1046,374],{"class":373},[367,1048,1049,1051,1053,1056],{"class":369,"line":377},[367,1050,381],{"class":380},[367,1052,385],{"class":384},[367,1054,1055],{"class":388}," HomeRoute",[367,1057,485],{"class":392},[367,1059,1060,1063,1066,1068,1071,1074,1076],{"class":369,"line":402},[367,1061,1062],{"class":392},"    viewModel: ",[367,1064,1065],{"class":373},"HomeViewModel",[367,1067,638],{"class":384},[367,1069,1070],{"class":388}," viewModel",[367,1072,1073],{"class":392},"(factory ",[367,1075,494],{"class":384},[367,1077,1078],{"class":392}," HomeViewModel.Factory),\n",[367,1080,1081],{"class":369,"line":409},[367,1082,399],{"class":392},[367,1084,1085,1087,1089,1091,1093,1095],{"class":369,"line":415},[367,1086,455],{"class":384},[367,1088,458],{"class":392},[367,1090,430],{"class":380},[367,1092,463],{"class":392},[367,1094,466],{"class":388},[367,1096,469],{"class":392},[367,1098,1099,1102],{"class":369,"line":421},[367,1100,1101],{"class":388},"    HomeScreen",[367,1103,485],{"class":392},[367,1105,1106,1108,1110],{"class":369,"line":452},[367,1107,491],{"class":392},[367,1109,494],{"class":384},[367,1111,497],{"class":392},[367,1113,1114,1117,1119,1122,1124,1127,1129,1132,1135],{"class":369,"line":472},[367,1115,1116],{"class":392},"        onSelectAccount ",[367,1118,494],{"class":384},[367,1120,1121],{"class":392}," { id ",[367,1123,942],{"class":380},[367,1125,1126],{"class":392}," viewModel.",[367,1128,596],{"class":388},[367,1130,1131],{"class":392},"(HomeUiEvent.",[367,1133,1134],{"class":388},"SelectAccount",[367,1136,1137],{"class":392},"(id)) },\n",[367,1139,1140],{"class":369,"line":479},[367,1141,533],{"class":392},[367,1143,1144],{"class":369,"line":488},[367,1145,539],{"class":392},[187,1147,1148,1150,1151,1154],{},[194,1149,223],{}," 在生命周期低于 ",[194,1152,1153],{},"STARTED"," 时停止收集。页面退到后台，这段组合不再因为数据库更新而重组。",[187,1156,1157,1160],{},[194,1158,1159],{},"SharingStarted"," 按这一屏会不会提前被订上来选：",[1162,1163,1164,1172,1179],"ul",{},[1165,1166,1167,1168,1171],"li",{},"可以离开的页面用 ",[194,1169,1170],{},"WhileSubscribed(5_000)","。首页、设置列表、预算都是这种。",[1165,1173,1174,1175,1178],{},"进程一启动就要的值用 ",[194,1176,1177],{},"Eagerly","。主题偏好在 Activity 里就收集，启动动画期间 DataStore 有时间读完。",[1165,1180,1181,1182,1184],{},"记一笔也用 ",[194,1183,1177],{},"。账单页先把这个 ViewModel 订上，用户点加号时分类和账户已经在状态里。",[187,1186,1187,1188,1190,1191,1194,1195,1197,1198,1200,1201,1204,1205,1207],{},"ViewModel 里也可以放 ",[194,1189,439],{},"，Compose 直接读，不必 ",[194,1192,1193],{},"collectAsState","。上游已经是 Room 的 ",[194,1196,204],{}," 时，我继续用 ",[194,1199,219],{},"：",[194,1202,1203],{},"combine","、",[194,1206,832],{}," 和生命周期收集是同一套。",[187,1209,1210,1211,1204,1214,1217,1218,1204,1221,1224],{},"界面上的行用专门的 UI 类型，例如 ",[194,1212,1213],{},"BillRowUi",[194,1215,1216],{},"AccountChoice","。Room 的 ",[194,1219,1220],{},"Bill",[194,1222,1223],{},"Account"," 停在仓库边界。分类名在 ViewModel 里按 id 填好，Screen 不再查表。",[230,1226,1227],{"id":1227},"只发生一次的结果",[187,1229,1230,1231,1233],{},"保存成功、新建分类失败，写进 ",[194,1232,200],{}," 会留下。旋转或重新进入组合时，Route 会再看到这份值，返回或弹窗会再走一次。",[187,1235,1236,1237,1240,1241,1244,1245,1248],{},"这种结果用 ",[194,1238,1239],{},"SharedFlow","。不设重放，后来的订阅者拿不到已经发出的那次。Route 在 ",[194,1242,1243],{},"LaunchedEffect(viewModel)"," 里收集，然后调用 ",[194,1246,1247],{},"onBack"," 或关掉对话框。",[234,1250,1253],{"className":360,"code":1251,"filename":1252,"language":363,"meta":240,"style":240},"private val _finished = MutableSharedFlow\u003CUnit>(extraBufferCapacity = 1)\nval finished: SharedFlow\u003CUnit> = _finished.asSharedFlow()\n\nprivate fun finishSave(token: Int) {\n    if (token != saveToken) return\n    _finished.tryEmit(Unit)\n}\n","AddExpenseViewModel.kt",[194,1254,1255,1285,1310,1314,1331,1348,1359],{"__ignoreMap":240},[367,1256,1257,1260,1262,1265,1267,1270,1272,1275,1278,1280,1283],{"class":369,"line":370},[367,1258,1259],{"class":380},"private",[367,1261,736],{"class":384},[367,1263,1264],{"class":392}," _finished ",[367,1266,494],{"class":384},[367,1268,1269],{"class":388}," MutableSharedFlow",[367,1271,766],{"class":392},[367,1273,1274],{"class":373},"Unit",[367,1276,1277],{"class":392},">(extraBufferCapacity ",[367,1279,494],{"class":384},[367,1281,1282],{"class":658}," 1",[367,1284,666],{"class":392},[367,1286,1287,1289,1292,1294,1296,1298,1300,1302,1305,1308],{"class":369,"line":377},[367,1288,699],{"class":384},[367,1290,1291],{"class":392}," finished: ",[367,1293,1239],{"class":373},[367,1295,766],{"class":392},[367,1297,1274],{"class":373},[367,1299,772],{"class":392},[367,1301,494],{"class":384},[367,1303,1304],{"class":392}," _finished.",[367,1306,1307],{"class":388},"asSharedFlow",[367,1309,469],{"class":392},[367,1311,1312],{"class":369,"line":402},[367,1313,476],{"emptyLinePlaceholder":475},[367,1315,1316,1318,1320,1323,1326,1329],{"class":369,"line":409},[367,1317,1259],{"class":380},[367,1319,385],{"class":384},[367,1321,1322],{"class":388}," finishSave",[367,1324,1325],{"class":392},"(token: ",[367,1327,1328],{"class":373},"Int",[367,1330,399],{"class":392},[367,1332,1333,1336,1339,1342,1345],{"class":369,"line":415},[367,1334,1335],{"class":926},"    if",[367,1337,1338],{"class":392}," (token ",[367,1340,1341],{"class":384},"!=",[367,1343,1344],{"class":392}," saveToken) ",[367,1346,1347],{"class":926},"return\n",[367,1349,1350,1353,1356],{"class":369,"line":421},[367,1351,1352],{"class":392},"    _finished.",[367,1354,1355],{"class":388},"tryEmit",[367,1357,1358],{"class":392},"(Unit)\n",[367,1360,1361],{"class":369,"line":452},[367,1362,539],{"class":392},[234,1364,1367],{"className":360,"code":1365,"filename":1366,"language":363,"meta":240,"style":240},"LaunchedEffect(viewModel) {\n    viewModel.finished.collect { onBack() }\n}\n","AddExpenseRoute.kt",[194,1368,1369,1376,1391],{"__ignoreMap":240},[367,1370,1371,1373],{"class":369,"line":370},[367,1372,561],{"class":388},[367,1374,1375],{"class":392},"(viewModel) {\n",[367,1377,1378,1381,1384,1386,1388],{"class":369,"line":377},[367,1379,1380],{"class":392},"    viewModel.finished.",[367,1382,1383],{"class":388},"collect",[367,1385,436],{"class":392},[367,1387,1247],{"class":388},[367,1389,1390],{"class":392},"() }\n",[367,1392,1393],{"class":369,"line":402},[367,1394,539],{"class":392},[187,1396,1397,1400,1401,1403,1404,1407],{},[194,1398,1399],{},"extraBufferCapacity = 1"," 让 ViewModel 在没有订阅者的瞬间也能 ",[194,1402,1355],{}," 成功一次。保存用一个自增的 ",[194,1405,1406],{},"saveToken","：离开页面时作废进行中的那次，迟到的成功不会把新表单清掉，也不会把已经离开的页面再退一次。",[187,1409,1410,1411,1414,1415,1418],{},"ViewModel 到此为止。它不持有 ",[194,1412,1413],{},"AppNavigator","，也不调用 ",[194,1416,1417],{},"pop","。下一页去哪，由路由表里的回调决定。",[230,1420,322],{"id":1421},"仓库里的-flow",[187,1423,1424,1425,1427,1428,1431,1432,1435,1436,1439],{},"查询是 ",[194,1426,204],{},"，写入是 ",[194,1429,1430],{},"suspend"," 函数。",[194,1433,1434],{},"BillRepository"," 用 Hilt 做成单例，转账在 ",[194,1437,1438],{},"withTransaction"," 里写两条并改两个账户余额。支出金额一变，预算按账单重新合计，仓库不另写一份「已花费」给界面缓存。",[187,1441,1442,1443,1445,1446,1448,1449,1451,1452,1454],{},"ViewModel 用 ",[194,1444,1203],{}," 把账户、账单、预算、分类和偏好收成 ",[194,1447,200],{},"。某一路还没来时，",[194,1450,1026],{}," 保持 ",[194,1453,548],{},"。首页另外把账户条先暴露出来，正文继续等。正文还没跟上时，顶栏沿用和卡片同一份当前账户，切换账户时两边一起变。",[187,1456,1457,1458,1461,1462,1465,1466,1469,1470,1472],{},"偏好放 DataStore，经 ",[194,1459,1460],{},"AppPreferencesRepository"," 出去。设置页和主题读的是同一个 ",[194,1463,1464],{},"SettingsViewModel","：Activity 用 ",[194,1467,1468],{},"preferences"," 决定深色和主题，设置列表用 ",[194,1471,227],{}," 画分组。写入只走仓库。",[230,1474,1476],{"id":1475},"viewmodel-存在哪","ViewModel 存在哪",[187,1478,1479,1482,1483,1486,1487,1490],{},[194,1480,1481],{},"viewModel()"," 找最近的 ",[194,1484,1485],{},"ViewModelStoreOwner","。记账应用没有给每个导航入口挂 ",[194,1488,1489],{},"rememberViewModelStoreNavEntryDecorator()","，所以实例落在 Activity 上。旋转不会新建，离开页面也不会清掉。",[187,1492,1493],{},"这带来两件具体的事。",[187,1495,1496,1497,1500,1501,1504,1505,1508],{},"账单页和开屏共用 ",[194,1498,1499],{},"BillHomeViewModel","，不传 key。记一笔用 ",[194,1502,1503],{},"\"add-expense-$typeCode\"","，账单页和记一笔页因此是同一个实例。账户编辑用 ",[194,1506,1507],{},"\"account-editor-${accountId ?: \"new\"}\"","，新建和每一个账户各一份。",[187,1510,1511,1512,1515,1516,1519],{},"工厂只在这个 key 第一次创建时执行。之后再进，",[194,1513,1514],{},"factory"," 里的新参数被忽略。全部账单目前只按类型取 ViewModel，没有把 ",[194,1517,1518],{},"accountId"," 写进 key。换一个账户再打开「查看全部」，列表仍是上一个账户。参数会变的页面，key 里带上那一段。",[187,1521,1522,1523,1525],{},"每个入口单独一个 ",[194,1524,312],{}," 时，离开页面实例会清掉，预加载就要改成别的做法：把要提前准备的数据放进仓库，页面自己的 ViewModel 进去再订。记账应用选 Activity 这一档，是为了让记一笔的表单和分类在打开之前就位。",[187,1527,1528,1529,1531,1532,1534,1535,1538,1539,1542,1543,1545],{},"进程被杀后 ViewModel 会新建。返回栈上的 ",[194,1530,338],{}," 能还原，Route 再次组合时工厂用还原回来的 ",[194,1533,1518],{}," 创建。草稿若只活在 ViewModel 的 ",[194,1536,1537],{},"MutableStateFlow"," 里，这次会丢。要活过进程重建的小字段，放进 ",[194,1540,1541],{},"SavedStateHandle","，或者放在 ",[194,1544,289],{}," 里。大列表走仓库再查。",[187,1547,1548,1549,1552,1553,1556,1557,1204,1559,552,1562,1565],{},"依赖用 ",[194,1550,1551],{},"viewModelFactory"," 从 Hilt ",[194,1554,1555],{},"EntryPoint"," 取仓库，构造函数再接收 ",[194,1558,1518],{},[194,1560,1561],{},"typeCode",[194,1563,1564],{},"@HiltViewModel"," 适合没有导航参数的那一屏。参数来自路由时，工厂把参数和仓库拼在一起。",[230,1567,1568],{"id":1568},"和导航分开",[187,1570,1571,1572,1574,1575,1577,1578,1580],{},"返回栈、当前标签是导航状态，",[194,1573,343],{}," 和 ",[194,1576,289],{}," 持有。这一页的金额、选中的分类是页面状态。两边各有一份，通过 id 相连：路由把 ",[194,1579,1518],{}," 交给 Route，Route 交给 ViewModel 工厂。",[187,1582,1583,1584,1586],{},"设置里的对话框放在设置 ViewModel 的短暂状态里，因为这份 ViewModel 挂在 Activity 上，旋转之后还在。记一笔的日期选择只属于那次编辑，放在 Route 的 ",[194,1585,289],{}," 里。",[187,1588,1589,1590,1593,1594,1596,1597,1574,1599,1601],{},"跳转、替换栈顶、",[194,1591,1592],{},"dropUnlessResumed"," 写在 ",[207,1595,209],{"href":51},"。这篇只规定：页面状态的出口是 ",[194,1598,200],{},[194,1600,1239],{},"，导航器留在路由表。",[230,1603,1604],{"id":1604},"三方状态管理",[187,1606,1607,1608,1611,1612,1614,1615,1618],{},"上面是手写的。状态一个 data class，动作一组类型，副作用在 ",[194,1609,1610],{},"viewModelScope"," 里，做完再写状态或发 ",[194,1613,1239],{},"。屏少的时候这样够用。屏一多，每页的 ",[194,1616,1617],{},"reduce","、订阅和一次性结果会各写各的。",[187,1620,1621],{},"下面这些库规定的是这一屏的状态怎么变。Room、DataStore、仓库仍然留在原处。",[244,1623,1624,1640],{},[247,1625,1626],{},[250,1627,1628,1631,1634,1637],{},[253,1629,1630],{},"名字",[253,1632,1633],{},"它规定什么",[253,1635,1636],{},"一次性结果",[253,1638,1639],{},"我怎么看",[266,1641,1642,1658,1680,1708,1730,1754],{},[250,1643,1644,1647,1650,1653],{},[271,1645,1646],{},"RxJava",[271,1648,1649],{},"一条可取消的事件流",[271,1651,1652],{},"订阅里处理",[271,1654,1655,1656],{},"老工程的绑定层。新页面的请求我用 ",[194,1657,1430],{},[250,1659,1660,1663,1672,1677],{},[271,1661,1662],{},"Orbit",[271,1664,1665,1666,1669,1670],{},"ViewModel 上的 ",[194,1667,1668],{},"intent"," \u002F ",[194,1671,1617],{},[271,1673,1674],{},[194,1675,1676],{},"postSideEffect",[271,1678,1679],{},"最接近现在这套，样板收进容器",[250,1681,1682,1685,1698,1705],{},[271,1683,1684],{},"Mavericks",[271,1686,1687,1688,1691,1692,1691,1695],{},"不可变 ",[194,1689,1690],{},"State","，",[194,1693,1694],{},"setState",[194,1696,1697],{},"Async",[271,1699,1700,1701,1704],{},"状态里的 ",[194,1702,1703],{},"Async.Fail","，或自己的事件",[271,1706,1707],{},"Airbnb 系。状态默认可恢复",[250,1709,1710,1713,1721,1724],{},[271,1711,1712],{},"Molecule",[271,1714,1715,1718,1719],{},[194,1716,1717],{},"@Composable"," 算出模型，输出 ",[194,1720,219],{},[271,1722,1723],{},"模型里的字段，或另开一条流",[271,1725,1726,1727,1729],{},"逻辑想用 ",[194,1728,561],{}," 写",[250,1731,1732,1735,1745,1751],{},[271,1733,1734],{},"Circuit",[271,1736,1737,1740,1741,1744],{},[194,1738,1739],{},"present()"," 返回带 ",[194,1742,1743],{},"eventSink"," 的状态",[271,1746,1747,1748],{},"状态里的字段；导航走 Presenter 的 ",[194,1749,1750],{},"Navigator",[271,1752,1753],{},"状态和页面栈要同一套时",[250,1755,1756,1759,1765,1770],{},[271,1757,1758],{},"MVIKotlin",[271,1760,1761,1764],{},[194,1762,1763],{},"Store","：Intent 进，State 出",[271,1766,1767],{},[194,1768,1769],{},"Label",[271,1771,1772],{},"要时间旅行、要跨端时。常和 Decompose 一起用",[187,1774,1775],{},"同一类形状还有 Ballast、FlowRedux。动作进、状态出、副作用在旁边。下面六种够对照。",[1777,1778,1646],"h3",{"id":1779},"rxjava",[187,1781,1782,1785,1786,1789,1790,1793],{},[194,1783,1784],{},"Observable"," 表示以后还会来的值。",[194,1787,1788],{},"BehaviorRelay"," 记住当前值，后来的订阅者马上拿到它。订阅放进 ",[194,1791,1792],{},"CompositeDisposable","，控制器或收集方释放时一起取消。",[234,1795,1798],{"className":360,"code":1796,"filename":1797,"language":363,"meta":240,"style":240},"class RxCounterViewModel : ViewModel() {\n    val count = BehaviorRelay.createDefault(0)\n\n    fun increment() {\n        count.accept(count.value!! + 1)\n    }\n\n    override fun onCleared() {\n        \u002F\u002F Relay 本身不持有界面。订阅在收集那一侧 dispose。\n    }\n}\n","RxCounterViewModel.kt",[194,1799,1800,1814,1836,1840,1849,1873,1877,1881,1893,1898,1902],{"__ignoreMap":240},[367,1801,1802,1804,1807,1810,1812],{"class":369,"line":370},[367,1803,723],{"class":384},[367,1805,1806],{"class":373}," RxCounterViewModel",[367,1808,1809],{"class":392}," : ",[367,1811,751],{"class":373},[367,1813,754],{"class":392},[367,1815,1816,1818,1821,1823,1826,1829,1831,1834],{"class":369,"line":377},[367,1817,455],{"class":384},[367,1819,1820],{"class":392}," count ",[367,1822,494],{"class":384},[367,1824,1825],{"class":392}," BehaviorRelay.",[367,1827,1828],{"class":388},"createDefault",[367,1830,442],{"class":392},[367,1832,1833],{"class":658},"0",[367,1835,666],{"class":392},[367,1837,1838],{"class":369,"line":402},[367,1839,476],{"emptyLinePlaceholder":475},[367,1841,1842,1844,1847],{"class":369,"line":409},[367,1843,909],{"class":384},[367,1845,1846],{"class":388}," increment",[367,1848,754],{"class":392},[367,1850,1851,1854,1857,1860,1863,1866,1869,1871],{"class":369,"line":415},[367,1852,1853],{"class":392},"        count.",[367,1855,1856],{"class":388},"accept",[367,1858,1859],{"class":392},"(count.",[367,1861,1862],{"class":380},"value",[367,1864,1865],{"class":384},"!!",[367,1867,1868],{"class":384}," +",[367,1870,1282],{"class":658},[367,1872,666],{"class":392},[367,1874,1875],{"class":369,"line":421},[367,1876,959],{"class":392},[367,1878,1879],{"class":369,"line":452},[367,1880,476],{"emptyLinePlaceholder":475},[367,1882,1883,1886,1888,1891],{"class":369,"line":472},[367,1884,1885],{"class":380},"    override",[367,1887,385],{"class":384},[367,1889,1890],{"class":388}," onCleared",[367,1892,754],{"class":392},[367,1894,1895],{"class":369,"line":479},[367,1896,1897],{"class":405},"        \u002F\u002F Relay 本身不持有界面。订阅在收集那一侧 dispose。\n",[367,1899,1900],{"class":369,"line":488},[367,1901,959],{"class":392},[367,1903,1904],{"class":369,"line":500},[367,1905,539],{"class":392},[234,1907,1910],{"className":360,"code":1908,"filename":1909,"language":363,"meta":240,"style":240},"viewModel.count\n    .observeOn(AndroidSchedulers.mainThread())\n    .subscribe { count -> label.text = count.toString() }\n    .also { disposable.add(it) }\n","RxCounterBind.kt",[194,1911,1912,1917,1934,1959],{"__ignoreMap":240},[367,1913,1914],{"class":369,"line":370},[367,1915,1916],{"class":392},"viewModel.count\n",[367,1918,1919,1922,1925,1928,1931],{"class":369,"line":377},[367,1920,1921],{"class":392},"    .",[367,1923,1924],{"class":388},"observeOn",[367,1926,1927],{"class":392},"(AndroidSchedulers.",[367,1929,1930],{"class":388},"mainThread",[367,1932,1933],{"class":392},"())\n",[367,1935,1936,1938,1941,1944,1946,1949,1951,1954,1957],{"class":369,"line":402},[367,1937,1921],{"class":392},[367,1939,1940],{"class":388},"subscribe",[367,1942,1943],{"class":392}," { count ",[367,1945,942],{"class":380},[367,1947,1948],{"class":392}," label.text ",[367,1950,494],{"class":384},[367,1952,1953],{"class":392}," count.",[367,1955,1956],{"class":388},"toString",[367,1958,1390],{"class":392},[367,1960,1961,1963,1966,1969,1972],{"class":369,"line":409},[367,1962,1921],{"class":392},[367,1964,1965],{"class":388},"also",[367,1967,1968],{"class":392}," { disposable.",[367,1970,1971],{"class":388},"add",[367,1973,1974],{"class":392},"(it) }\n",[187,1976,1977,1978,1980],{},"状态仍在 ViewModel 里。Rx 负责运送值。绑到界面的那一段可以用 ",[194,1979,1784],{}," 的主线程调度；网络那一段失败在仓库里处理，界面收到的是已经变成状态的值。",[187,1982,1983,1984,1986],{},"新页面我用协程和 ",[194,1985,204],{},"。工程里已经有 Rx 的绑定，继续留在那些页面上。",[1777,1988,1662],{"id":1989},"orbit",[187,1991,1992,1993,1995,1996,552],{},"Orbit 把现在的 ViewModel 收成一个容器。状态是 data class，副作用是另一个类型。改状态只能走 ",[194,1994,1617],{},"，一次性结果走 ",[194,1997,1676],{},[234,1999,2002],{"className":360,"code":2000,"filename":2001,"language":363,"meta":240,"style":240},"data class CounterState(val count: Int = 0)\n\nsealed interface CounterSideEffect {\n    data object Finished : CounterSideEffect\n}\n\nclass CounterViewModel :\n    ContainerHost\u003CCounterState, CounterSideEffect>,\n    ViewModel() {\n\n    override val container = container\u003CCounterState, CounterSideEffect>(CounterState())\n\n    fun increment() = intent {\n        reduce { state.copy(count = state.count + 1) }\n    }\n\n    fun save() = intent {\n        \u002F\u002F 请求放在 intent 里。完成后要么 reduce，要么 postSideEffect。\n        postSideEffect(CounterSideEffect.Finished)\n    }\n}\n","CounterContainer.kt",[194,2003,2004,2028,2032,2043,2058,2062,2066,2076,2091,2098,2102,2134,2138,2154,2180,2184,2188,2203,2208,2216,2220],{"__ignoreMap":240},[367,2005,2006,2008,2010,2013,2015,2017,2020,2022,2024,2026],{"class":369,"line":370},[367,2007,617],{"class":380},[367,2009,620],{"class":384},[367,2011,2012],{"class":373}," CounterState",[367,2014,442],{"class":392},[367,2016,699],{"class":384},[367,2018,2019],{"class":392}," count: ",[367,2021,1328],{"class":373},[367,2023,638],{"class":384},[367,2025,659],{"class":658},[367,2027,666],{"class":392},[367,2029,2030],{"class":369,"line":377},[367,2031,476],{"emptyLinePlaceholder":475},[367,2033,2034,2036,2038,2041],{"class":369,"line":402},[367,2035,675],{"class":380},[367,2037,678],{"class":384},[367,2039,2040],{"class":373}," CounterSideEffect",[367,2042,684],{"class":392},[367,2044,2045,2047,2050,2053,2055],{"class":369,"line":409},[367,2046,689],{"class":380},[367,2048,2049],{"class":384}," object",[367,2051,2052],{"class":373}," Finished",[367,2054,1809],{"class":392},[367,2056,2057],{"class":373},"CounterSideEffect\n",[367,2059,2060],{"class":369,"line":415},[367,2061,539],{"class":392},[367,2063,2064],{"class":369,"line":421},[367,2065,476],{"emptyLinePlaceholder":475},[367,2067,2068,2070,2073],{"class":369,"line":452},[367,2069,723],{"class":384},[367,2071,2072],{"class":373}," CounterViewModel",[367,2074,2075],{"class":392}," :\n",[367,2077,2078,2081,2083,2086,2089],{"class":369,"line":472},[367,2079,2080],{"class":392},"    ContainerHost",[367,2082,766],{"class":384},[367,2084,2085],{"class":392},"CounterState, CounterSideEffect",[367,2087,2088],{"class":384},">",[367,2090,643],{"class":392},[367,2092,2093,2096],{"class":369,"line":479},[367,2094,2095],{"class":388},"    ViewModel",[367,2097,754],{"class":392},[367,2099,2100],{"class":369,"line":488},[367,2101,476],{"emptyLinePlaceholder":475},[367,2103,2104,2106,2108,2111,2113,2116,2118,2121,2124,2127,2130,2132],{"class":369,"line":500},[367,2105,1885],{"class":380},[367,2107,736],{"class":384},[367,2109,2110],{"class":392}," container ",[367,2112,494],{"class":384},[367,2114,2115],{"class":388}," container",[367,2117,766],{"class":392},[367,2119,2120],{"class":373},"CounterState",[367,2122,2123],{"class":392},", ",[367,2125,2126],{"class":373},"CounterSideEffect",[367,2128,2129],{"class":392},">(",[367,2131,2120],{"class":388},[367,2133,1933],{"class":392},[367,2135,2136],{"class":369,"line":511},[367,2137,476],{"emptyLinePlaceholder":475},[367,2139,2140,2142,2144,2147,2149,2152],{"class":369,"line":530},[367,2141,909],{"class":384},[367,2143,1846],{"class":388},[367,2145,2146],{"class":392},"() ",[367,2148,494],{"class":384},[367,2150,2151],{"class":388}," intent",[367,2153,684],{"class":392},[367,2155,2156,2159,2162,2165,2168,2170,2173,2176,2178],{"class":369,"line":536},[367,2157,2158],{"class":388},"        reduce",[367,2160,2161],{"class":392}," { state.",[367,2163,2164],{"class":388},"copy",[367,2166,2167],{"class":392},"(count ",[367,2169,494],{"class":384},[367,2171,2172],{"class":392}," state.count ",[367,2174,2175],{"class":384},"+",[367,2177,1282],{"class":658},[367,2179,449],{"class":392},[367,2181,2182],{"class":369,"line":799},[367,2183,959],{"class":392},[367,2185,2186],{"class":369,"line":821},[367,2187,476],{"emptyLinePlaceholder":475},[367,2189,2190,2192,2195,2197,2199,2201],{"class":369,"line":827},[367,2191,909],{"class":384},[367,2193,2194],{"class":388}," save",[367,2196,2146],{"class":392},[367,2198,494],{"class":384},[367,2200,2151],{"class":388},[367,2202,684],{"class":392},[367,2204,2205],{"class":369,"line":837},[367,2206,2207],{"class":405},"        \u002F\u002F 请求放在 intent 里。完成后要么 reduce，要么 postSideEffect。\n",[367,2209,2210,2213],{"class":369,"line":848},[367,2211,2212],{"class":388},"        postSideEffect",[367,2214,2215],{"class":392},"(CounterSideEffect.Finished)\n",[367,2217,2218],{"class":369,"line":854},[367,2219,959],{"class":392},[367,2221,2222],{"class":369,"line":860},[367,2223,539],{"class":392},[187,2225,2226],{},"界面用 Orbit 的收集函数。副作用的 lambda 跟着组合的生命周期，页面停在后台时不再接收。",[234,2228,2231],{"className":360,"code":2229,"filename":2230,"language":363,"meta":240,"style":240},"@Composable\nfun CounterScreen(viewModel: CounterViewModel = viewModel()) {\n    val state by viewModel.collectAsState()\n    viewModel.collectSideEffect { effect ->\n        when (effect) {\n            CounterSideEffect.Finished -> { \u002F* 交给导航 *\u002F }\n        }\n    }\n    Button(onClick = viewModel::increment) {\n        Text(\"${state.count}\")\n    }\n}\n","CounterOrbitScreen.kt",[194,2232,2233,2237,2257,2272,2285,2292,2307,2311,2315,2333,2357,2361],{"__ignoreMap":240},[367,2234,2235],{"class":369,"line":370},[367,2236,374],{"class":373},[367,2238,2239,2242,2245,2247,2250,2252,2254],{"class":369,"line":377},[367,2240,2241],{"class":384},"fun",[367,2243,2244],{"class":388}," CounterScreen",[367,2246,393],{"class":392},[367,2248,2249],{"class":373},"CounterViewModel",[367,2251,638],{"class":384},[367,2253,1070],{"class":388},[367,2255,2256],{"class":392},"()) {\n",[367,2258,2259,2261,2264,2266,2268,2270],{"class":369,"line":402},[367,2260,455],{"class":384},[367,2262,2263],{"class":392}," state ",[367,2265,430],{"class":380},[367,2267,1126],{"class":392},[367,2269,1193],{"class":388},[367,2271,469],{"class":392},[367,2273,2274,2277,2280,2283],{"class":369,"line":409},[367,2275,2276],{"class":392},"    viewModel.",[367,2278,2279],{"class":388},"collectSideEffect",[367,2281,2282],{"class":392}," { effect ",[367,2284,796],{"class":380},[367,2286,2287,2289],{"class":369,"line":415},[367,2288,927],{"class":926},[367,2290,2291],{"class":392}," (effect) {\n",[367,2293,2294,2297,2299,2301,2304],{"class":369,"line":421},[367,2295,2296],{"class":392},"            CounterSideEffect.Finished ",[367,2298,942],{"class":380},[367,2300,436],{"class":392},[367,2302,2303],{"class":405},"\u002F* 交给导航 *\u002F",[367,2305,2306],{"class":392}," }\n",[367,2308,2309],{"class":369,"line":452},[367,2310,824],{"class":392},[367,2312,2313],{"class":369,"line":472},[367,2314,959],{"class":392},[367,2316,2317,2320,2323,2325,2328,2331],{"class":369,"line":479},[367,2318,2319],{"class":388},"    Button",[367,2321,2322],{"class":392},"(onClick ",[367,2324,494],{"class":384},[367,2326,2327],{"class":392}," viewModel::",[367,2329,2330],{"class":388},"increment",[367,2332,399],{"class":392},[367,2334,2335,2338,2340,2344,2347,2350,2353,2355],{"class":369,"line":488},[367,2336,2337],{"class":388},"        Text",[367,2339,442],{"class":392},[367,2341,2343],{"class":2342},"sfazB","\"",[367,2345,2346],{"class":384},"${",[367,2348,2349],{"class":392},"state.count",[367,2351,2352],{"class":384},"}",[367,2354,2343],{"class":2342},[367,2356,666],{"class":392},[367,2358,2359],{"class":369,"line":500},[367,2360,959],{"class":392},[367,2362,2363],{"class":369,"line":511},[367,2364,539],{"class":392},[187,2366,2367,2368,2370,2371,2373,2374,2376,2377,2380,2381,2383,2384,2387,2388,2390,2391,2393,2394,2397,2398,552],{},"这和记账应用的 ",[194,2369,596],{}," + ",[194,2372,1239],{}," 是同一条路。差别是 ",[194,2375,1617],{}," 拿到的是容器里的当前 ",[194,2378,2379],{},"state","，连续两次 ",[194,2382,2330],{}," 各在当时的值上加一，不用自己 ",[194,2385,2386],{},"update","。上游 ",[194,2389,204],{}," 用 ",[194,2392,1668],{}," 里的 ",[194,2395,2396],{},"repeatOnSubscription"," 收集，订阅者离开就停，对应手写的 ",[194,2399,871],{},[187,2401,2402,2403,552],{},"我在一屏的副作用已经多到要单测「这个动作之后状态是什么、发出了哪个副作用」时，会换成 Orbit。一个开关继续用现在的 ",[194,2404,596],{},[1777,2406,1684],{"id":2407},"mavericks",[187,2409,2410,2411,2414,2415,2417,2418,1200,2420,1204,2423,1204,2426,1204,2429,552,2432,2435],{},"Mavericks 也是一屏一份不可变状态，挂在 ",[194,2412,2413],{},"MavericksViewModel"," 上。同步修改用 ",[194,2416,1694],{},"。一次请求用 ",[194,2419,1697],{},[194,2421,2422],{},"Uninitialized",[194,2424,2425],{},"Loading",[194,2427,2428],{},"Success",[194,2430,2431],{},"Fail",[194,2433,2434],{},"execute"," 把挂起函数的结果写进状态的那一个字段。",[234,2437,2440],{"className":360,"code":2438,"filename":2439,"language":363,"meta":240,"style":240},"data class CounterState(\n    val count: Int = 0,\n    val fact: Async\u003CString> = Uninitialized,\n) : MavericksState\n\nclass CounterViewModel(\n    initialState: CounterState,\n) : MavericksViewModel\u003CCounterState>(initialState) {\n\n    fun increment() = setState { copy(count = count + 1) }\n\n    fun loadFact() {\n        suspend { NumberApi.fact(awaitState().count) }.execute { copy(fact = it) }\n    }\n}\n","CounterMavericks.kt",[194,2441,2442,2452,2466,2487,2494,2498,2506,2515,2532,2536,2565,2569,2578,2611,2615],{"__ignoreMap":240},[367,2443,2444,2446,2448,2450],{"class":369,"line":370},[367,2445,617],{"class":380},[367,2447,620],{"class":384},[367,2449,2012],{"class":373},[367,2451,485],{"class":392},[367,2453,2454,2456,2458,2460,2462,2464],{"class":369,"line":377},[367,2455,455],{"class":384},[367,2457,2019],{"class":392},[367,2459,1328],{"class":373},[367,2461,638],{"class":384},[367,2463,659],{"class":658},[367,2465,643],{"class":392},[367,2467,2468,2470,2473,2475,2477,2480,2482,2484],{"class":369,"line":402},[367,2469,455],{"class":384},[367,2471,2472],{"class":392}," fact: ",[367,2474,1697],{"class":373},[367,2476,766],{"class":392},[367,2478,2479],{"class":373},"String",[367,2481,772],{"class":392},[367,2483,494],{"class":384},[367,2485,2486],{"class":392}," Uninitialized,\n",[367,2488,2489,2491],{"class":369,"line":409},[367,2490,707],{"class":392},[367,2492,2493],{"class":373},"MavericksState\n",[367,2495,2496],{"class":369,"line":415},[367,2497,476],{"emptyLinePlaceholder":475},[367,2499,2500,2502,2504],{"class":369,"line":421},[367,2501,723],{"class":384},[367,2503,2072],{"class":373},[367,2505,485],{"class":392},[367,2507,2508,2511,2513],{"class":369,"line":452},[367,2509,2510],{"class":392},"    initialState: ",[367,2512,2120],{"class":373},[367,2514,643],{"class":392},[367,2516,2517,2519,2521,2523,2525,2527,2530],{"class":369,"line":472},[367,2518,707],{"class":392},[367,2520,2413],{"class":373},[367,2522,766],{"class":392},[367,2524,2120],{"class":373},[367,2526,2129],{"class":392},[367,2528,2529],{"class":373},"initialState",[367,2531,399],{"class":392},[367,2533,2534],{"class":369,"line":479},[367,2535,476],{"emptyLinePlaceholder":475},[367,2537,2538,2540,2542,2544,2546,2549,2551,2553,2555,2557,2559,2561,2563],{"class":369,"line":488},[367,2539,909],{"class":384},[367,2541,1846],{"class":388},[367,2543,2146],{"class":392},[367,2545,494],{"class":384},[367,2547,2548],{"class":388}," setState",[367,2550,436],{"class":392},[367,2552,2164],{"class":388},[367,2554,2167],{"class":392},[367,2556,494],{"class":384},[367,2558,1820],{"class":392},[367,2560,2175],{"class":384},[367,2562,1282],{"class":658},[367,2564,449],{"class":392},[367,2566,2567],{"class":369,"line":500},[367,2568,476],{"emptyLinePlaceholder":475},[367,2570,2571,2573,2576],{"class":369,"line":511},[367,2572,909],{"class":384},[367,2574,2575],{"class":388}," loadFact",[367,2577,754],{"class":392},[367,2579,2580,2583,2586,2589,2591,2594,2597,2599,2601,2603,2606,2608],{"class":369,"line":530},[367,2581,2582],{"class":380},"        suspend",[367,2584,2585],{"class":392}," { NumberApi.",[367,2587,2588],{"class":388},"fact",[367,2590,442],{"class":392},[367,2592,2593],{"class":388},"awaitState",[367,2595,2596],{"class":392},"().count) }.",[367,2598,2434],{"class":388},[367,2600,436],{"class":392},[367,2602,2164],{"class":388},[367,2604,2605],{"class":392},"(fact ",[367,2607,494],{"class":384},[367,2609,2610],{"class":392}," it) }\n",[367,2612,2613],{"class":369,"line":536},[367,2614,959],{"class":392},[367,2616,2617],{"class":369,"line":799},[367,2618,539],{"class":392},[187,2620,2621,2623,2624,2626,2627,2629,2630,2632,2633,2635,2636,2638,2639,2642],{},[194,2622,2434],{}," 进行中时 ",[194,2625,2588],{}," 是 ",[194,2628,2425],{},"，成功是 ",[194,2631,2428],{},"，失败是 ",[194,2634,2431],{},"。界面按这个字段画进度和错误，不必再并列一个 ",[194,2637,1026],{}," 布尔和一个 ",[194,2640,2641],{},"error"," 字符串。",[234,2644,2647],{"className":360,"code":2645,"filename":2646,"language":363,"meta":240,"style":240},"@Composable\nfun CounterScreen(viewModel: CounterViewModel = mavericksViewModel()) {\n    val state by viewModel.collectAsState()\n    Text(\"${state.count}\")\n    when (val fact = state.fact) {\n        is Success -> Text(fact())\n        is Fail -> Text(\"失败\")\n        else -> Unit\n    }\n}\n","CounterMavericksScreen.kt",[194,2648,2649,2653,2670,2684,2703,2721,2740,2758,2769,2773],{"__ignoreMap":240},[367,2650,2651],{"class":369,"line":370},[367,2652,374],{"class":373},[367,2654,2655,2657,2659,2661,2663,2665,2668],{"class":369,"line":377},[367,2656,2241],{"class":384},[367,2658,2244],{"class":388},[367,2660,393],{"class":392},[367,2662,2249],{"class":373},[367,2664,638],{"class":384},[367,2666,2667],{"class":388}," mavericksViewModel",[367,2669,2256],{"class":392},[367,2671,2672,2674,2676,2678,2680,2682],{"class":369,"line":402},[367,2673,455],{"class":384},[367,2675,2263],{"class":392},[367,2677,430],{"class":380},[367,2679,1126],{"class":392},[367,2681,1193],{"class":388},[367,2683,469],{"class":392},[367,2685,2686,2689,2691,2693,2695,2697,2699,2701],{"class":369,"line":409},[367,2687,2688],{"class":388},"    Text",[367,2690,442],{"class":392},[367,2692,2343],{"class":2342},[367,2694,2346],{"class":384},[367,2696,2349],{"class":392},[367,2698,2352],{"class":384},[367,2700,2343],{"class":2342},[367,2702,666],{"class":392},[367,2704,2705,2708,2711,2713,2716,2718],{"class":369,"line":415},[367,2706,2707],{"class":926},"    when",[367,2709,2710],{"class":392}," (",[367,2712,699],{"class":384},[367,2714,2715],{"class":392}," fact ",[367,2717,494],{"class":384},[367,2719,2720],{"class":392}," state.fact) {\n",[367,2722,2723,2726,2729,2731,2734,2736,2738],{"class":369,"line":421},[367,2724,2725],{"class":384},"        is",[367,2727,2728],{"class":392}," Success ",[367,2730,942],{"class":380},[367,2732,2733],{"class":388}," Text",[367,2735,442],{"class":392},[367,2737,2588],{"class":388},[367,2739,1933],{"class":392},[367,2741,2742,2744,2747,2749,2751,2753,2756],{"class":369,"line":452},[367,2743,2725],{"class":384},[367,2745,2746],{"class":392}," Fail ",[367,2748,942],{"class":380},[367,2750,2733],{"class":388},[367,2752,442],{"class":392},[367,2754,2755],{"class":2342},"\"失败\"",[367,2757,666],{"class":392},[367,2759,2760,2763,2766],{"class":369,"line":472},[367,2761,2762],{"class":926},"        else",[367,2764,2765],{"class":380}," ->",[367,2767,2768],{"class":392}," Unit\n",[367,2770,2771],{"class":369,"line":479},[367,2772,959],{"class":392},[367,2774,2775],{"class":369,"line":488},[367,2776,539],{"class":392},[187,2778,2779,2780,2782,2783,2785,2786,2788],{},"状态默认会保存。进程重建后上一份 ",[194,2781,2120],{}," 能回来，因此字段要是 Mavericks 能写入 ",[194,2784,295],{}," 的类型。账单列表这种会变长的数据继续放仓库，状态里留 id 和 ",[194,2787,1697],{}," 的结果摘要。",[187,2790,2791,2792,1574,2794,2796,2797,2799,2800,2802],{},"工程已经是 Mavericks 时，新页面沿用 ",[194,2793,1694],{},[194,2795,2434],{},"。从零开始的 Compose 应用，我用手写的 ",[194,2798,200],{},"，请求的加载和失败做成状态里的字段，形状和 ",[194,2801,1697],{}," 一样，只是不引入这套恢复机制。",[1777,2804,1712],{"id":2805},"molecule",[187,2807,2808,2809,2811,2812,2815,2816,552,2818,1204,2820,2822],{},"Molecule 让一段 ",[194,2810,1717],{}," 函数生产模型，自己不发出任何 UI。",[194,2813,2814],{},"launchMolecule"," 在协程里重组它，输出 ",[194,2817,219],{},[194,2819,561],{},[194,2821,196],{}," 因此能写在 ViewModel 这一侧。",[234,2824,2827],{"className":360,"code":2825,"filename":2826,"language":363,"meta":240,"style":240},"data class CounterModel(val count: Int = 0)\n\nsealed interface CounterEvent {\n    data object Increment : CounterEvent\n}\n\nabstract class MoleculeViewModel\u003CEvent, Model> : ViewModel() {\n    private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)\n    private val events = MutableSharedFlow\u003CEvent>(extraBufferCapacity = 20)\n\n    val models: StateFlow\u003CModel> by lazy(LazyThreadSafetyMode.NONE) {\n        scope.launchMolecule(mode = RecompositionMode.ContextClock) { models(events) }\n    }\n\n    fun take(event: Event) {\n        events.tryEmit(event)\n    }\n\n    @Composable\n    protected abstract fun models(events: Flow\u003CEvent>): Model\n}\n\nclass CounterViewModel : MoleculeViewModel\u003CCounterEvent, CounterModel>() {\n    @Composable\n    override fun models(events: Flow\u003CCounterEvent>): CounterModel {\n        var count by remember { mutableIntStateOf(0) }\n        LaunchedEffect(Unit) {\n            events.collect { event ->\n                when (event) {\n                    CounterEvent.Increment -> count += 1\n                }\n            }\n        }\n        return CounterModel(count)\n    }\n}\n","MoleculeCounter.kt",[194,2828,2829,2852,2856,2867,2881,2885,2889,2916,2938,2964,2968,3001,3022,3026,3030,3043,3053,3057,3061,3066,3094,3098,3102,3126,3130,3152,3175,3183,3195,3202,3217,3222,3227,3231,3241,3246],{"__ignoreMap":240},[367,2830,2831,2833,2835,2838,2840,2842,2844,2846,2848,2850],{"class":369,"line":370},[367,2832,617],{"class":380},[367,2834,620],{"class":384},[367,2836,2837],{"class":373}," CounterModel",[367,2839,442],{"class":392},[367,2841,699],{"class":384},[367,2843,2019],{"class":392},[367,2845,1328],{"class":373},[367,2847,638],{"class":384},[367,2849,659],{"class":658},[367,2851,666],{"class":392},[367,2853,2854],{"class":369,"line":377},[367,2855,476],{"emptyLinePlaceholder":475},[367,2857,2858,2860,2862,2865],{"class":369,"line":402},[367,2859,675],{"class":380},[367,2861,678],{"class":384},[367,2863,2864],{"class":373}," CounterEvent",[367,2866,684],{"class":392},[367,2868,2869,2871,2873,2876,2878],{"class":369,"line":409},[367,2870,689],{"class":380},[367,2872,2049],{"class":384},[367,2874,2875],{"class":373}," Increment",[367,2877,1809],{"class":392},[367,2879,2880],{"class":373},"CounterEvent\n",[367,2882,2883],{"class":369,"line":415},[367,2884,539],{"class":392},[367,2886,2887],{"class":369,"line":421},[367,2888,476],{"emptyLinePlaceholder":475},[367,2890,2891,2894,2896,2899,2901,2904,2906,2909,2912,2914],{"class":369,"line":452},[367,2892,2893],{"class":380},"abstract",[367,2895,620],{"class":384},[367,2897,2898],{"class":373}," MoleculeViewModel",[367,2900,766],{"class":392},[367,2902,2903],{"class":373},"Event",[367,2905,2123],{"class":392},[367,2907,2908],{"class":373},"Model",[367,2910,2911],{"class":392},"> : ",[367,2913,751],{"class":373},[367,2915,754],{"class":392},[367,2917,2918,2920,2922,2925,2927,2930,2933,2935],{"class":369,"line":472},[367,2919,733],{"class":380},[367,2921,736],{"class":384},[367,2923,2924],{"class":392}," scope ",[367,2926,494],{"class":384},[367,2928,2929],{"class":388}," CoroutineScope",[367,2931,2932],{"class":392},"(viewModelScope.coroutineContext ",[367,2934,2175],{"class":384},[367,2936,2937],{"class":392}," AndroidUiDispatcher.Main)\n",[367,2939,2940,2942,2944,2947,2949,2951,2953,2955,2957,2959,2962],{"class":369,"line":479},[367,2941,733],{"class":380},[367,2943,736],{"class":384},[367,2945,2946],{"class":392}," events ",[367,2948,494],{"class":384},[367,2950,1269],{"class":388},[367,2952,766],{"class":392},[367,2954,2903],{"class":373},[367,2956,1277],{"class":392},[367,2958,494],{"class":384},[367,2960,2961],{"class":658}," 20",[367,2963,666],{"class":392},[367,2965,2966],{"class":369,"line":488},[367,2967,476],{"emptyLinePlaceholder":475},[367,2969,2970,2972,2975,2977,2979,2981,2983,2985,2988,2990,2993,2996,2999],{"class":369,"line":500},[367,2971,455],{"class":384},[367,2973,2974],{"class":392}," models: ",[367,2976,219],{"class":373},[367,2978,766],{"class":392},[367,2980,2908],{"class":373},[367,2982,772],{"class":392},[367,2984,430],{"class":373},[367,2986,2987],{"class":373}," lazy",[367,2989,442],{"class":392},[367,2991,2992],{"class":373},"LazyThreadSafetyMode",[367,2994,2995],{"class":392},".",[367,2997,2998],{"class":373},"NONE",[367,3000,399],{"class":392},[367,3002,3003,3006,3008,3011,3013,3016,3019],{"class":369,"line":511},[367,3004,3005],{"class":392},"        scope.",[367,3007,2814],{"class":388},[367,3009,3010],{"class":392},"(mode ",[367,3012,494],{"class":384},[367,3014,3015],{"class":392}," RecompositionMode.ContextClock) { ",[367,3017,3018],{"class":388},"models",[367,3020,3021],{"class":392},"(events) }\n",[367,3023,3024],{"class":369,"line":530},[367,3025,959],{"class":392},[367,3027,3028],{"class":369,"line":536},[367,3029,476],{"emptyLinePlaceholder":475},[367,3031,3032,3034,3037,3039,3041],{"class":369,"line":799},[367,3033,909],{"class":384},[367,3035,3036],{"class":388}," take",[367,3038,915],{"class":392},[367,3040,2903],{"class":373},[367,3042,399],{"class":392},[367,3044,3045,3048,3050],{"class":369,"line":821},[367,3046,3047],{"class":392},"        events.",[367,3049,1355],{"class":388},[367,3051,3052],{"class":392},"(event)\n",[367,3054,3055],{"class":369,"line":827},[367,3056,959],{"class":392},[367,3058,3059],{"class":369,"line":837},[367,3060,476],{"emptyLinePlaceholder":475},[367,3062,3063],{"class":369,"line":848},[367,3064,3065],{"class":373},"    @Composable\n",[367,3067,3068,3071,3074,3076,3079,3082,3084,3086,3088,3091],{"class":369,"line":854},[367,3069,3070],{"class":380},"    protected",[367,3072,3073],{"class":380}," abstract",[367,3075,385],{"class":384},[367,3077,3078],{"class":388}," models",[367,3080,3081],{"class":392},"(events: ",[367,3083,204],{"class":373},[367,3085,766],{"class":392},[367,3087,2903],{"class":373},[367,3089,3090],{"class":392},">): ",[367,3092,3093],{"class":373},"Model\n",[367,3095,3096],{"class":369,"line":860},[367,3097,539],{"class":392},[367,3099,3100],{"class":369,"line":882},[367,3101,476],{"emptyLinePlaceholder":475},[367,3103,3104,3106,3108,3110,3113,3115,3118,3120,3123],{"class":369,"line":895},[367,3105,723],{"class":384},[367,3107,2072],{"class":373},[367,3109,1809],{"class":392},[367,3111,3112],{"class":373},"MoleculeViewModel",[367,3114,766],{"class":392},[367,3116,3117],{"class":373},"CounterEvent",[367,3119,2123],{"class":392},[367,3121,3122],{"class":373},"CounterModel",[367,3124,3125],{"class":392},">() {\n",[367,3127,3128],{"class":369,"line":901},[367,3129,3065],{"class":373},[367,3131,3132,3134,3136,3138,3140,3142,3144,3146,3148,3150],{"class":369,"line":906},[367,3133,1885],{"class":380},[367,3135,385],{"class":384},[367,3137,3078],{"class":388},[367,3139,3081],{"class":392},[367,3141,204],{"class":373},[367,3143,766],{"class":392},[367,3145,3117],{"class":373},[367,3147,3090],{"class":392},[367,3149,3122],{"class":373},[367,3151,684],{"class":392},[367,3153,3154,3157,3159,3161,3164,3166,3169,3171,3173],{"class":369,"line":923},[367,3155,3156],{"class":384},"        var",[367,3158,1820],{"class":392},[367,3160,430],{"class":380},[367,3162,3163],{"class":388}," remember",[367,3165,436],{"class":392},[367,3167,3168],{"class":388},"mutableIntStateOf",[367,3170,442],{"class":392},[367,3172,1833],{"class":658},[367,3174,449],{"class":392},[367,3176,3177,3180],{"class":369,"line":933},[367,3178,3179],{"class":388},"        LaunchedEffect",[367,3181,3182],{"class":392},"(Unit) {\n",[367,3184,3185,3188,3190,3193],{"class":369,"line":951},[367,3186,3187],{"class":392},"            events.",[367,3189,1383],{"class":388},[367,3191,3192],{"class":392}," { event ",[367,3194,796],{"class":380},[367,3196,3197,3200],{"class":369,"line":956},[367,3198,3199],{"class":926},"                when",[367,3201,930],{"class":392},[367,3203,3204,3207,3209,3211,3214],{"class":369,"line":962},[367,3205,3206],{"class":392},"                    CounterEvent.Increment ",[367,3208,942],{"class":380},[367,3210,1820],{"class":392},[367,3212,3213],{"class":384},"+=",[367,3215,3216],{"class":658}," 1\n",[367,3218,3219],{"class":369,"line":967},[367,3220,3221],{"class":392},"                }\n",[367,3223,3224],{"class":369,"line":983},[367,3225,3226],{"class":392},"            }\n",[367,3228,3229],{"class":369,"line":1001},[367,3230,824],{"class":392},[367,3232,3233,3236,3238],{"class":369,"line":1006},[367,3234,3235],{"class":926},"        return",[367,3237,2837],{"class":388},[367,3239,3240],{"class":392},"(count)\n",[367,3242,3244],{"class":369,"line":3243},35,[367,3245,959],{"class":392},[367,3247,3249],{"class":369,"line":3248},36,[367,3250,539],{"class":392},[187,3252,3253,3256,3257,3260,3261,3263,3264,3267,3268,1204,3271,3274],{},[194,3254,3255],{},"ContextClock"," 用调用方协程上下文里的帧时钟驱动重组，Android 上把它和 ",[194,3258,3259],{},"AndroidUiDispatcher.Main"," 放在一起。界面收集 ",[194,3262,3018],{},"，点击调用 ",[194,3265,3266],{},"take","。Presenter 里出现 ",[194,3269,3270],{},"Text",[194,3272,3273],{},"Button"," 就越界了：这段组合只为了算出模型。",[187,3276,3277,3278,3280,3281,3283],{},"我在状态依赖彼此取消、分支很多，用 ",[194,3279,1203],{}," 已经难读时，会考虑 Molecule。它和记账应用的边界仍然相同：模型出去，事件进来。差别是计算过程写成组合，而不是写成 ",[194,3282,1203],{}," 的 lambda。",[1777,3285,1734],{"id":3286},"circuit",[187,3288,3289,3290,2626,3292,3294,3295,3297],{},"Circuit 把一屏拆成 Presenter 和 UI。Presenter 的 ",[194,3291,1739],{},[194,3293,1717],{},"，返回状态。事件放在状态里的 ",[194,3296,1743],{},"。UI 只调用这个函数，不拿 ViewModel。",[234,3299,3302],{"className":360,"code":3300,"filename":3301,"language":363,"meta":240,"style":240},"data class CounterState(\n    val count: Int,\n    val eventSink: (CounterEvent) -> Unit,\n) : CircuitUiState\n\nsealed interface CounterEvent : CircuitUiEvent {\n    data object Increment : CounterEvent\n}\n\nclass CounterPresenter : Presenter\u003CCounterState> {\n    @Composable\n    override fun present(): CounterState {\n        var count by remember { mutableIntStateOf(0) }\n        return CounterState(count) { event ->\n            when (event) {\n                CounterEvent.Increment -> count += 1\n            }\n        }\n    }\n}\n\n@Composable\nfun Counter(state: CounterState) {\n    Button(onClick = { state.eventSink(CounterEvent.Increment) }) {\n        Text(\"${state.count}\")\n    }\n}\n","CircuitCounter.kt",[194,3303,3304,3314,3324,3340,3347,3351,3366,3378,3382,3386,3405,3409,3425,3445,3456,3463,3476,3480,3484,3488,3492,3496,3500,3514,3529,3547,3551],{"__ignoreMap":240},[367,3305,3306,3308,3310,3312],{"class":369,"line":370},[367,3307,617],{"class":380},[367,3309,620],{"class":384},[367,3311,2012],{"class":373},[367,3313,485],{"class":392},[367,3315,3316,3318,3320,3322],{"class":369,"line":377},[367,3317,455],{"class":384},[367,3319,2019],{"class":392},[367,3321,1328],{"class":373},[367,3323,643],{"class":392},[367,3325,3326,3328,3331,3333,3336,3338],{"class":369,"line":402},[367,3327,455],{"class":384},[367,3329,3330],{"class":392}," eventSink: (",[367,3332,3117],{"class":373},[367,3334,3335],{"class":392},") -> ",[367,3337,1274],{"class":373},[367,3339,643],{"class":392},[367,3341,3342,3344],{"class":369,"line":409},[367,3343,707],{"class":392},[367,3345,3346],{"class":373},"CircuitUiState\n",[367,3348,3349],{"class":369,"line":415},[367,3350,476],{"emptyLinePlaceholder":475},[367,3352,3353,3355,3357,3359,3361,3364],{"class":369,"line":421},[367,3354,675],{"class":380},[367,3356,678],{"class":384},[367,3358,2864],{"class":373},[367,3360,1809],{"class":392},[367,3362,3363],{"class":373},"CircuitUiEvent",[367,3365,684],{"class":392},[367,3367,3368,3370,3372,3374,3376],{"class":369,"line":452},[367,3369,689],{"class":380},[367,3371,2049],{"class":384},[367,3373,2875],{"class":373},[367,3375,1809],{"class":392},[367,3377,2880],{"class":373},[367,3379,3380],{"class":369,"line":472},[367,3381,539],{"class":392},[367,3383,3384],{"class":369,"line":479},[367,3385,476],{"emptyLinePlaceholder":475},[367,3387,3388,3390,3393,3395,3398,3400,3402],{"class":369,"line":488},[367,3389,723],{"class":384},[367,3391,3392],{"class":373}," CounterPresenter",[367,3394,1809],{"class":392},[367,3396,3397],{"class":373},"Presenter",[367,3399,766],{"class":392},[367,3401,2120],{"class":373},[367,3403,3404],{"class":392},"> {\n",[367,3406,3407],{"class":369,"line":500},[367,3408,3065],{"class":373},[367,3410,3411,3413,3415,3418,3421,3423],{"class":369,"line":511},[367,3412,1885],{"class":380},[367,3414,385],{"class":384},[367,3416,3417],{"class":388}," present",[367,3419,3420],{"class":392},"(): ",[367,3422,2120],{"class":373},[367,3424,684],{"class":392},[367,3426,3427,3429,3431,3433,3435,3437,3439,3441,3443],{"class":369,"line":530},[367,3428,3156],{"class":384},[367,3430,1820],{"class":392},[367,3432,430],{"class":380},[367,3434,3163],{"class":388},[367,3436,436],{"class":392},[367,3438,3168],{"class":388},[367,3440,442],{"class":392},[367,3442,1833],{"class":658},[367,3444,449],{"class":392},[367,3446,3447,3449,3451,3454],{"class":369,"line":536},[367,3448,3235],{"class":926},[367,3450,2012],{"class":388},[367,3452,3453],{"class":392},"(count) { event ",[367,3455,796],{"class":380},[367,3457,3458,3461],{"class":369,"line":799},[367,3459,3460],{"class":926},"            when",[367,3462,930],{"class":392},[367,3464,3465,3468,3470,3472,3474],{"class":369,"line":821},[367,3466,3467],{"class":392},"                CounterEvent.Increment ",[367,3469,942],{"class":380},[367,3471,1820],{"class":392},[367,3473,3213],{"class":384},[367,3475,3216],{"class":658},[367,3477,3478],{"class":369,"line":827},[367,3479,3226],{"class":392},[367,3481,3482],{"class":369,"line":837},[367,3483,824],{"class":392},[367,3485,3486],{"class":369,"line":848},[367,3487,959],{"class":392},[367,3489,3490],{"class":369,"line":854},[367,3491,539],{"class":392},[367,3493,3494],{"class":369,"line":860},[367,3495,476],{"emptyLinePlaceholder":475},[367,3497,3498],{"class":369,"line":882},[367,3499,374],{"class":373},[367,3501,3502,3504,3507,3510,3512],{"class":369,"line":895},[367,3503,2241],{"class":384},[367,3505,3506],{"class":388}," Counter",[367,3508,3509],{"class":392},"(state: ",[367,3511,2120],{"class":373},[367,3513,399],{"class":392},[367,3515,3516,3518,3520,3522,3524,3526],{"class":369,"line":901},[367,3517,2319],{"class":388},[367,3519,2322],{"class":392},[367,3521,494],{"class":384},[367,3523,2161],{"class":392},[367,3525,1743],{"class":388},[367,3527,3528],{"class":392},"(CounterEvent.Increment) }) {\n",[367,3530,3531,3533,3535,3537,3539,3541,3543,3545],{"class":369,"line":906},[367,3532,2337],{"class":388},[367,3534,442],{"class":392},[367,3536,2343],{"class":2342},[367,3538,2346],{"class":384},[367,3540,2349],{"class":392},[367,3542,2352],{"class":384},[367,3544,2343],{"class":2342},[367,3546,666],{"class":392},[367,3548,3549],{"class":369,"line":923},[367,3550,959],{"class":392},[367,3552,3553],{"class":369,"line":933},[367,3554,539],{"class":392},[187,3556,3557,3559],{},[194,3558,1739],{}," 标了 presenter 这个组合目标，在里面调用 UI 组合会报警。状态要稳定，事件类型保持不可变。",[187,3561,3562,3563,3565,3566,3569,3570,3572,3573,3575,3576,3579],{},"留存分三档，和这篇开头的表对应：",[194,3564,196],{}," 只活过重组；",[194,3567,3568],{},"rememberRetained"," 活过返回栈和旋转；",[194,3571,289],{}," 再活过进程被杀。导航用 ",[194,3574,1750],{},"，可以放在 Presenter 里。",[194,3577,3578],{},"Screen"," 是可保存的键，参数在构造 Presenter 时注进来。",[187,3581,3582,3583,3585],{},"记账应用把导航留在路由表，把一次性离开页面做成 ",[194,3584,1239],{},"。Circuit 把「去下一页」也看成 Presenter 里的一次调用。页面栈和这一屏的状态要在同一个对象里测时，用它。只想管一屏字段时，它把导航也带进来了。",[1777,3587,1758],{"id":3588},"mvikotlin",[187,3590,3591,3592,3594,3595,3597,3598,3600,3601,3597,3603,3606,3607,3610],{},"MVIKotlin 的单位是 ",[194,3593,1763],{},"。Intent 进去，",[194,3596,1690],{}," 从 ",[194,3599,2379],{}," 出来，一次性的 ",[194,3602,1769],{},[194,3604,3605],{},"labels"," 出来。Executor 做副作用，需要改状态时 ",[194,3608,3609],{},"dispatch"," 一条消息。Reducer 是纯函数，只根据消息产出新状态。",[234,3612,3615],{"className":360,"code":3613,"filename":3614,"language":363,"meta":240,"style":240},"data class State(val count: Int = 0)\n\nsealed interface Intent {\n    data object Increment : Intent\n}\n\nsealed interface Msg {\n    data object Bump : Msg\n}\n\nclass CounterExecutor : CoroutineExecutor\u003CIntent, Nothing, State, Msg, Nothing>() {\n    override fun executeIntent(intent: Intent) {\n        when (intent) {\n            Intent.Increment -> dispatch(Msg.Bump)\n        }\n    }\n}\n\nclass CounterReducer : Reducer\u003CState, Msg> {\n    override fun State.reduce(msg: Msg): State = when (msg) {\n        Msg.Bump -> copy(count = count + 1)\n    }\n}\n","CounterStore.kt",[194,3616,3617,3640,3644,3655,3668,3672,3676,3687,3701,3705,3709,3746,3762,3769,3782,3786,3790,3794,3798,3820,3850,3872,3876],{"__ignoreMap":240},[367,3618,3619,3621,3623,3626,3628,3630,3632,3634,3636,3638],{"class":369,"line":370},[367,3620,617],{"class":380},[367,3622,620],{"class":384},[367,3624,3625],{"class":373}," State",[367,3627,442],{"class":392},[367,3629,699],{"class":384},[367,3631,2019],{"class":392},[367,3633,1328],{"class":373},[367,3635,638],{"class":384},[367,3637,659],{"class":658},[367,3639,666],{"class":392},[367,3641,3642],{"class":369,"line":377},[367,3643,476],{"emptyLinePlaceholder":475},[367,3645,3646,3648,3650,3653],{"class":369,"line":402},[367,3647,675],{"class":380},[367,3649,678],{"class":384},[367,3651,3652],{"class":373}," Intent",[367,3654,684],{"class":392},[367,3656,3657,3659,3661,3663,3665],{"class":369,"line":409},[367,3658,689],{"class":380},[367,3660,2049],{"class":384},[367,3662,2875],{"class":373},[367,3664,1809],{"class":392},[367,3666,3667],{"class":373},"Intent\n",[367,3669,3670],{"class":369,"line":415},[367,3671,539],{"class":392},[367,3673,3674],{"class":369,"line":421},[367,3675,476],{"emptyLinePlaceholder":475},[367,3677,3678,3680,3682,3685],{"class":369,"line":452},[367,3679,675],{"class":380},[367,3681,678],{"class":384},[367,3683,3684],{"class":373}," Msg",[367,3686,684],{"class":392},[367,3688,3689,3691,3693,3696,3698],{"class":369,"line":472},[367,3690,689],{"class":380},[367,3692,2049],{"class":384},[367,3694,3695],{"class":373}," Bump",[367,3697,1809],{"class":392},[367,3699,3700],{"class":373},"Msg\n",[367,3702,3703],{"class":369,"line":479},[367,3704,539],{"class":392},[367,3706,3707],{"class":369,"line":488},[367,3708,476],{"emptyLinePlaceholder":475},[367,3710,3711,3713,3716,3718,3721,3723,3726,3728,3731,3733,3735,3737,3740,3742,3744],{"class":369,"line":500},[367,3712,723],{"class":384},[367,3714,3715],{"class":373}," CounterExecutor",[367,3717,1809],{"class":392},[367,3719,3720],{"class":373},"CoroutineExecutor",[367,3722,766],{"class":392},[367,3724,3725],{"class":373},"Intent",[367,3727,2123],{"class":392},[367,3729,3730],{"class":373},"Nothing",[367,3732,2123],{"class":392},[367,3734,1690],{"class":373},[367,3736,2123],{"class":392},[367,3738,3739],{"class":373},"Msg",[367,3741,2123],{"class":392},[367,3743,3730],{"class":373},[367,3745,3125],{"class":392},[367,3747,3748,3750,3752,3755,3758,3760],{"class":369,"line":511},[367,3749,1885],{"class":380},[367,3751,385],{"class":384},[367,3753,3754],{"class":388}," executeIntent",[367,3756,3757],{"class":392},"(intent: ",[367,3759,3725],{"class":373},[367,3761,399],{"class":392},[367,3763,3764,3766],{"class":369,"line":530},[367,3765,927],{"class":926},[367,3767,3768],{"class":392}," (intent) {\n",[367,3770,3771,3774,3776,3779],{"class":369,"line":536},[367,3772,3773],{"class":392},"            Intent.Increment ",[367,3775,942],{"class":380},[367,3777,3778],{"class":388}," dispatch",[367,3780,3781],{"class":392},"(Msg.Bump)\n",[367,3783,3784],{"class":369,"line":799},[367,3785,824],{"class":392},[367,3787,3788],{"class":369,"line":821},[367,3789,959],{"class":392},[367,3791,3792],{"class":369,"line":827},[367,3793,539],{"class":392},[367,3795,3796],{"class":369,"line":837},[367,3797,476],{"emptyLinePlaceholder":475},[367,3799,3800,3802,3805,3807,3810,3812,3814,3816,3818],{"class":369,"line":848},[367,3801,723],{"class":384},[367,3803,3804],{"class":373}," CounterReducer",[367,3806,1809],{"class":392},[367,3808,3809],{"class":373},"Reducer",[367,3811,766],{"class":392},[367,3813,1690],{"class":373},[367,3815,2123],{"class":392},[367,3817,3739],{"class":373},[367,3819,3404],{"class":392},[367,3821,3822,3824,3826,3828,3830,3832,3835,3837,3840,3842,3844,3847],{"class":369,"line":854},[367,3823,1885],{"class":380},[367,3825,385],{"class":384},[367,3827,3625],{"class":373},[367,3829,2995],{"class":392},[367,3831,1617],{"class":388},[367,3833,3834],{"class":392},"(msg: ",[367,3836,3739],{"class":373},[367,3838,3839],{"class":392},"): ",[367,3841,1690],{"class":373},[367,3843,638],{"class":384},[367,3845,3846],{"class":926}," when",[367,3848,3849],{"class":392}," (msg) {\n",[367,3851,3852,3855,3857,3860,3862,3864,3866,3868,3870],{"class":369,"line":860},[367,3853,3854],{"class":392},"        Msg.Bump ",[367,3856,942],{"class":380},[367,3858,3859],{"class":388}," copy",[367,3861,2167],{"class":392},[367,3863,494],{"class":384},[367,3865,1820],{"class":392},[367,3867,2175],{"class":384},[367,3869,1282],{"class":658},[367,3871,666],{"class":392},[367,3873,3874],{"class":369,"line":882},[367,3875,959],{"class":392},[367,3877,3878],{"class":369,"line":895},[367,3879,539],{"class":392},[234,3881,3884],{"className":360,"code":3882,"filename":3883,"language":363,"meta":240,"style":240},"val store = storeFactory.create(\n    initialState = State(),\n    executorFactory = ::CounterExecutor,\n    reducer = CounterReducer(),\n)\nstore.accept(Intent.Increment)\n","CounterStoreFactory.kt",[194,3885,3886,3903,3914,3929,3940,3944],{"__ignoreMap":240},[367,3887,3888,3890,3893,3895,3898,3901],{"class":369,"line":370},[367,3889,699],{"class":384},[367,3891,3892],{"class":392}," store ",[367,3894,494],{"class":384},[367,3896,3897],{"class":392}," storeFactory.",[367,3899,3900],{"class":388},"create",[367,3902,485],{"class":392},[367,3904,3905,3908,3910,3912],{"class":369,"line":377},[367,3906,3907],{"class":392},"    initialState ",[367,3909,494],{"class":384},[367,3911,3625],{"class":388},[367,3913,892],{"class":392},[367,3915,3916,3919,3921,3924,3927],{"class":369,"line":402},[367,3917,3918],{"class":392},"    executorFactory ",[367,3920,494],{"class":384},[367,3922,3923],{"class":392}," ::",[367,3925,3926],{"class":388},"CounterExecutor",[367,3928,643],{"class":392},[367,3930,3931,3934,3936,3938],{"class":369,"line":409},[367,3932,3933],{"class":392},"    reducer ",[367,3935,494],{"class":384},[367,3937,3804],{"class":388},[367,3939,892],{"class":392},[367,3941,3942],{"class":369,"line":415},[367,3943,666],{"class":392},[367,3945,3946,3949,3951],{"class":369,"line":421},[367,3947,3948],{"class":392},"store.",[367,3950,1856],{"class":388},[367,3952,3953],{"class":392},"(Intent.Increment)\n",[187,3955,3956,3957,3263,3960,3962,3963,3965,3966,3969,3970,3972,3973,3975],{},"界面收集 ",[194,3958,3959],{},"store.state",[194,3961,1856],{},"。请求放在 Executor 里，结果再 ",[194,3964,3609],{}," 一条消息，由 Reducer 写进状态。要通知界面离开这一页时，用 ",[194,3967,3968],{},"publish"," 发一个 ",[194,3971,1769],{},"，作用和 ",[194,3974,1239],{}," 相同。",[187,3977,3978],{},"库不绑定协程，协程是扩展模块。调试可以打开时间旅行：记录 Intent 和状态，倒回去重放。跨 Android 和 iOS 共用这一块时，Store 放在公共代码里，界面各写各的。导航常用旁边的 Decompose，Store 不负责栈。",[187,3980,3981,3982,3984],{},"我在这块逻辑要脱离 Android、还要能重放一条失败路径时用它。只服务 Android 的一屏，Orbit 或手写的 ",[194,3983,1617],{}," 更短。",[230,3986,3987],{"id":3987},"我先按这个选",[1162,3989,3990,3998,4003,4013,4022,4028,4034,4044,4047,4050,4056,4064,4072,4075],{},[1165,3991,3992,3993,3995,3996,552],{},"值只活在一块组合里：",[194,3994,196],{},"。旋转或进程重建还要在，而且值能保存：",[194,3997,289],{},[1165,3999,4000,4001,552],{},"能算出来的值做成计算。派生结果很少变、读它的子组合又很重：",[194,4002,565],{},[1165,4004,4005,4006,1204,4008,4010,4011,552],{},"这一屏的字段和动作：ViewModel 加 ",[194,4007,200],{},[194,4009,596],{},"。界面用 ",[194,4012,223],{},[1165,4014,4015,4016,4019,4020,552],{},"可以离开的页面：",[194,4017,4018],{},"SharingStarted.WhileSubscribed(5_000)","。启动就要、或要提前订上的页面：",[194,4021,1177],{},[1165,4023,4024,4025,4027],{},"保存成功、创建完成：",[194,4026,1239],{},"，Route 里收集后导航。",[1165,4029,4030,4031,4033],{},"列表和偏好：仓库的 ",[194,4032,204],{},"。界面模型在 ViewModel 里组装好。",[1165,4035,4036,4037,4040,4041,4043],{},"ViewModel 挂在 Activity 上时，会变的路由参数写进 ",[194,4038,4039],{},"viewModel"," 的 key。每个入口单独一个 ",[194,4042,312],{}," 时，离开页面用销毁来区分实例。",[1165,4045,4046],{},"导航栈和当前标签留在路由那篇。ViewModel 不持有导航器。",[1165,4048,4049],{},"老页面已经是 Rx：绑定继续用 RxJava。",[1165,4051,4052,4053,4055],{},"想保留 ViewModel，又要把 ",[194,4054,1617],{}," 和副作用收成可测的形状：Orbit。",[1165,4057,4058,4059,1574,4061,4063],{},"工程已经是 Mavericks：继续 ",[194,4060,1694],{},[194,4062,2434],{},"。状态能保存，大列表留在仓库。",[1165,4065,4066,4067,4069,4070,552],{},"状态转移用组合和 ",[194,4068,561],{}," 更好读：Molecule，输出仍是 ",[194,4071,219],{},[1165,4073,4074],{},"页面栈和 Presenter 要放在一起测：Circuit。",[1165,4076,4077],{},"公共代码、时间旅行：MVIKotlin 的 Store。导航另用 Decompose 或现有的返回栈。",[230,4079,4080],{"id":4080},"参考资料",[1162,4082,4083,4096,4105,4113,4125,4135,4146,4157],{},[1165,4084,4085,1200,4091,4093,4094],{},[207,4086,4090],{"href":4087,"rel":4088},"https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fui\u002Fcompose\u002Fstate",[4089],"nofollow","Compose 中的状态",[194,4092,196],{},"、状态提升、",[194,4095,565],{},[1165,4097,4098,4102,4103],{},[207,4099,751],{"href":4100,"rel":4101},"https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Flibraries\u002Farchitecture\u002Fviewmodel",[4089],"：作用域和 ",[194,4104,1481],{},[1165,4106,4107,4112],{},[207,4108,4111],{"href":4109,"rel":4110},"https:\u002F\u002Fkotlinlang.org\u002Fapi\u002Fkotlinx.coroutines\u002Fkotlinx-coroutines-core\u002Fkotlinx.coroutines.flow\u002F-state-flow\u002F",[4089],"StateFlow 和 SharedFlow","：当前值和一次性事件",[1165,4114,4115,1200,4119,1204,4121,1204,4123],{},[207,4116,1662],{"href":4117,"rel":4118},"https:\u002F\u002Forbit-mvi.org\u002F",[4089],[194,4120,1668],{},[194,4122,1617],{},[194,4124,1676],{},[1165,4126,4127,1200,4131,1574,4133],{},[207,4128,1684],{"href":4129,"rel":4130},"https:\u002F\u002Fairbnb.io\u002Fmavericks\u002F",[4089],[194,4132,1694],{},[194,4134,1697],{},[1165,4136,4137,1200,4141,4143,4144],{},[207,4138,1712],{"href":4139,"rel":4140},"https:\u002F\u002Fgithub.com\u002Fcashapp\u002Fmolecule",[4089],[194,4142,2814],{}," 把组合变成 ",[194,4145,219],{},[1165,4147,4148,4152,4153,1204,4155],{},[207,4149,1734],{"href":4150,"rel":4151},"https:\u002F\u002Fslackhq.github.io\u002Fcircuit\u002F",[4089],"：Presenter、",[194,4154,1743],{},[194,4156,3568],{},[1165,4158,4159,4163],{},[207,4160,1758],{"href":4161,"rel":4162},"https:\u002F\u002Fgithub.com\u002Farkivanov\u002FMVIKotlin",[4089],"：Store、Executor、Reducer、Label",[4165,4166,4167],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":240,"searchDepth":377,"depth":377,"links":4169},[4170,4171,4172,4173,4174,4175,4176,4177,4185,4186],{"id":232,"depth":377,"text":232},{"id":351,"depth":377,"text":351},{"id":574,"depth":377,"text":575},{"id":1227,"depth":377,"text":1227},{"id":1421,"depth":377,"text":322},{"id":1475,"depth":377,"text":1476},{"id":1568,"depth":377,"text":1568},{"id":1604,"depth":377,"text":1604,"children":4178},[4179,4180,4181,4182,4183,4184],{"id":1779,"depth":402,"text":1646},{"id":1989,"depth":402,"text":1662},{"id":2407,"depth":402,"text":1684},{"id":2805,"depth":402,"text":1712},{"id":3286,"depth":402,"text":1734},{"id":3588,"depth":402,"text":1758},{"id":3987,"depth":377,"text":3987},{"id":4080,"depth":377,"text":4080},"Compose 从 remember 排到 ViewModel 的 UiState。记账应用用仓库的 Flow 合成一屏状态，一次事件走 SharedFlow。Orbit、Mavericks、Molecule、Circuit、MVIKotlin 规定动作和副作用怎么走。","md",[4190,4191,4195,4197,4198],{"label":209,"icon":53,"to":51},{"label":4192,"icon":4193,"to":4100,"target":4194},"ViewModel 概览","i-lucide-book-open","_blank",{"label":4196,"icon":4193,"to":4087,"target":4194},"Compose 状态",{"label":1662,"icon":4193,"to":4117,"target":4194},{"label":1734,"icon":4193,"to":4150,"target":4194},{"date":4200,"tags":4201},"2026-10-20",[42,4202,113,751],"Compose",{"icon":96},{"title":4205,"description":4206},"Android 状态管理：Compose、ViewModel 与三方库","记账应用把状态分成组合、页面、进程和导航四层。ViewModel 用 StateFlow 暴露 UiState，用 SharedFlow 送一次性结果。再记 Orbit、Mavericks、Molecule、Circuit 和 MVIKotlin。","qaMXozvTOUwp3LXUfxI7cCqCU6SDFi2L0GWRhhxYhzg",[4209,4211],{"title":108,"path":109,"stem":110,"description":4210,"icon":111},"so 与加密实现约定整数版本 12。对不上就拒绝加载，避免新旧 AAR 错位。",{"title":117,"path":119,"stem":123,"description":4212,"icon":118},"iOS 开发实践与笔记。",1792494961558]