Repository: arkivanov/Essenty Branch: master Commit: 703543ddf558 Files: 137 Total size: 391.7 KB Directory structure: gitextract_dhrtofoi/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── back-handler/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── back-handler.api │ │ ├── back-handler.klib.api │ │ └── jvm/ │ │ └── back-handler.api │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── backhandler/ │ │ └── AndroidBackHandler.kt │ ├── androidUnitTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── backhandler/ │ │ ├── AndroidBackHandlerTest.kt │ │ ├── AndroidBackHandlerWithLifecycleTest.kt │ │ └── OnBackPressedCallbackAdapterTest.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── backhandler/ │ │ ├── BackCallback.kt │ │ ├── BackDispatcher.kt │ │ ├── BackEvent.kt │ │ ├── BackHandler.kt │ │ ├── BackHandlerOwner.kt │ │ ├── DefaultBackDispatcher.kt │ │ └── Utils.kt │ └── commonTest/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── backhandler/ │ └── DefaultBackDispatcherTest.kt ├── build.gradle.kts ├── deps.versions.toml ├── detekt.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── instance-keeper/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── instance-keeper.api │ │ ├── instance-keeper.klib.api │ │ └── jvm/ │ │ └── instance-keeper.api │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── instancekeeper/ │ │ └── AndroidExt.kt │ ├── androidUnitTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── instancekeeper/ │ │ └── AndroidInstanceKeeperTest.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── instancekeeper/ │ │ ├── DefaultInstanceKeeperDispatcher.kt │ │ ├── ExperimentalInstanceKeeperApi.kt │ │ ├── InstanceKeeper.kt │ │ ├── InstanceKeeperDispatcher.kt │ │ ├── InstanceKeeperExt.kt │ │ └── InstanceKeeperOwner.kt │ └── commonTest/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── instancekeeper/ │ ├── DefaultInstanceKeeperDispatcherTest.kt │ └── InstanceKeeperExtTest.kt ├── lifecycle/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── lifecycle.api │ │ ├── jvm/ │ │ │ └── lifecycle.api │ │ └── lifecycle.klib.api │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ └── AndroidExt.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ ├── Lifecycle.kt │ │ ├── LifecycleExt.kt │ │ ├── LifecycleOwner.kt │ │ ├── LifecycleRegistry.kt │ │ ├── LifecycleRegistryExt.kt │ │ └── LifecycleRegistryImpl.kt │ ├── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ ├── LifecycleExtTest.kt │ │ └── LifecycleRegistryTest.kt │ ├── itvosMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ └── ApplicationLifecycle.kt │ └── itvosTest/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── lifecycle/ │ ├── ApplicationLifecyclePlatformTest.kt │ └── ApplicationLifecycleTest.kt ├── lifecycle-coroutines/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── lifecycle-coroutines.api │ │ ├── jvm/ │ │ │ └── lifecycle-coroutines.api │ │ └── lifecycle-coroutines.klib.api │ ├── build.gradle.kts │ └── src/ │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ └── coroutines/ │ │ ├── CoroutineScopeWithLifecycle.kt │ │ ├── DispatchersExt.kt │ │ ├── FlowWithLifecycle.kt │ │ └── RepeatOnLifecycle.kt │ └── commonTest/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── lifecycle/ │ └── coroutines/ │ ├── CoroutineScopeWithLifecycleTest.kt │ ├── DispatchersExtTest.kt │ └── LifecycleCoroutinesExtTest.kt ├── lifecycle-reaktive/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── lifecycle-reaktive.api │ │ ├── jvm/ │ │ │ └── lifecycle-reaktive.api │ │ └── lifecycle-reaktive.klib.api │ ├── build.gradle.kts │ └── src/ │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── lifecycle/ │ │ └── reaktive/ │ │ └── DisposableWithLifecycle.kt │ └── commonTest/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── lifecycle/ │ └── reaktive/ │ └── DisposableWithLifecycleTest.kt ├── settings.gradle.kts ├── state-keeper/ │ ├── .gitignore │ ├── api/ │ │ ├── android/ │ │ │ └── state-keeper.api │ │ ├── jvm/ │ │ │ └── state-keeper.api │ │ └── state-keeper.klib.api │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ ├── AndroidExt.kt │ │ ├── BundleExt.kt │ │ └── PersistableBundleExt.kt │ ├── androidUnitTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ ├── AndroidStateKeeperTest.kt │ │ ├── BundleExtTest.kt │ │ └── TestUtils.android.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ ├── DefaultStateKeeperDispatcher.kt │ │ ├── ExperimentalStateKeeperApi.kt │ │ ├── PolymorphicSerializer.kt │ │ ├── SerializableContainer.kt │ │ ├── StateKeeper.kt │ │ ├── StateKeeperDispatcher.kt │ │ ├── StateKeeperExt.kt │ │ ├── StateKeeperOwner.kt │ │ ├── Utils.kt │ │ └── base64/ │ │ ├── Decoder.kt │ │ ├── Dictionaries.kt │ │ ├── Encoder.kt │ │ └── README.md │ ├── commonTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ ├── CodingTest.kt │ │ ├── DefaultStateKeeperDispatcherTest.kt │ │ ├── PolymorphicSerializerTest.kt │ │ ├── SerializableContainerTest.kt │ │ ├── SerializableData.kt │ │ ├── StateKeeperExtTest.kt │ │ ├── TestUtils.kt │ │ └── base64/ │ │ ├── Base64ImplTest.kt │ │ └── README.md │ ├── javaMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ └── Utils.java.kt │ ├── jsTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── arkivanov/ │ │ └── essenty/ │ │ └── statekeeper/ │ │ └── DefaultStateKeeperDispatcherJsTest.kt │ └── nonJavaMain/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── statekeeper/ │ └── Utils.kt ├── state-keeper-benchmarks/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── res/ │ │ └── AndroidManifest.xml │ └── test/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── statekeeper/ │ └── benchmarks/ │ └── Benchmarks.kt ├── tools/ │ └── check-publication/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ ├── androidMain/ │ │ └── AndroidManifest.xml │ └── commonMain/ │ └── kotlin/ │ └── com/ │ └── arkivanov/ │ └── essenty/ │ └── tools/ │ └── checkpublication/ │ └── Dummy.kt └── utils-internal/ ├── .gitignore ├── build.gradle.kts └── src/ ├── androidMain/ │ └── AndroidManifest.xml └── commonMain/ └── kotlin/ └── com/ └── arkivanov/ └── essenty/ └── utils/ └── internal/ ├── ExperimentalEssentyApi.kt └── InternalEssentyApi.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] insert_final_newline = true [{*.kt, *.kts}] max_line_length = 140 ij_kotlin_packages_to_use_import_on_demand = ^ ij_continuation_indent_size = 4 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 ij_kotlin_block_comment_at_first_column = true ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = true ij_kotlin_call_parameters_wrap = on_every_item ij_kotlin_catch_on_new_line = false ij_kotlin_class_annotation_wrap = split_into_lines ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_for_chained_calls = false ij_kotlin_continuation_indent_for_expression_bodies = false ij_kotlin_continuation_indent_in_argument_lists = false ij_kotlin_continuation_indent_in_elvis = false ij_kotlin_continuation_indent_in_if_conditions = false ij_kotlin_continuation_indent_in_parameter_lists = false ij_kotlin_continuation_indent_in_supertype_lists = false ij_kotlin_else_on_new_line = false ij_kotlin_enum_constants_wrap = off ij_kotlin_extends_list_wrap = normal ij_kotlin_field_annotation_wrap = split_into_lines ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = true ij_kotlin_import_nested_classes = false ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 2 ij_kotlin_keep_blank_lines_in_code = 2 ij_kotlin_keep_blank_lines_in_declarations = 2 ij_kotlin_keep_first_column_comment = true ij_kotlin_keep_indents_on_empty_lines = false ij_kotlin_keep_line_breaks = true ij_kotlin_lbrace_on_next_line = false ij_kotlin_line_comment_add_space = false ij_kotlin_line_comment_at_first_column = true ij_kotlin_method_annotation_wrap = split_into_lines ij_kotlin_method_call_chain_wrap = normal ij_kotlin_method_parameters_new_line_after_left_paren = true ij_kotlin_method_parameters_right_paren_on_new_line = true ij_kotlin_method_parameters_wrap = on_every_item ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true ij_kotlin_space_after_type_colon = true ij_kotlin_space_before_catch_parentheses = true ij_kotlin_space_before_comma = false ij_kotlin_space_before_extend_colon = true ij_kotlin_space_before_for_parentheses = true ij_kotlin_space_before_if_parentheses = true ij_kotlin_space_before_lambda_arrow = true ij_kotlin_space_before_type_colon = false ij_kotlin_space_before_when_parentheses = true ij_kotlin_space_before_while_parentheses = true ij_kotlin_spaces_around_additive_operators = true ij_kotlin_spaces_around_assignment_operators = true ij_kotlin_spaces_around_equality_operators = true ij_kotlin_spaces_around_function_type_arrow = true ij_kotlin_spaces_around_logical_operators = true ij_kotlin_spaces_around_multiplicative_operators = true ij_kotlin_spaces_around_range = false ij_kotlin_spaces_around_relational_operators = true ij_kotlin_spaces_around_unary_operator = false ij_kotlin_spaces_around_when_arrow = true ij_kotlin_variable_annotation_wrap = off ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: arkivanov custom: ["https://www.buymeacoffee.com/arkivanov"] ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: pull_request: paths-ignore: - 'docs/**' jobs: linux-build: name: Build on Linux runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install Java uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 - name: Update dependencies run: sudo apt-get update - name: Install dependencies run: sudo apt-get install nodejs chromium-browser - name: Build uses: gradle/gradle-build-action@v2 with: arguments: build -Dsplit_targets macos-build: name: Build on macOS runs-on: macos-14 steps: - name: Checkout uses: actions/checkout@v3 - name: Install Java uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 - name: Build project uses: gradle/gradle-build-action@v2 with: arguments: build -Dsplit_targets ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: workflow_dispatch: jobs: create-staging-repository: runs-on: ubuntu-latest name: Create staging repository outputs: repository_id: ${{ steps.create.outputs.repository_id }} steps: - id: create uses: nexus-actions/create-nexus-staging-repo@v1.3.0 with: username: arkivanov password: ${{ secrets.SONATYPE_PASSWORD }} staging_profile_id: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} description: Created by GitHub Actions base_url: https://s01.oss.sonatype.org/service/local/ publish: name: Publish runs-on: macos-14 needs: create-staging-repository steps: - name: Checkout uses: actions/checkout@v3 - name: Install Java uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 - name: Publish env: SONATYPE_REPOSITORY_ID: ${{ needs.create-staging-repository.outputs.repository_id }} SONATYPE_USER_NAME: ${{ secrets.SONATYPE_USER_NAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} run: ./gradlew publish close-staging-repository: name: Close staging repository runs-on: ubuntu-latest needs: [ create-staging-repository, publish ] steps: - name: Close staging repository uses: nexus-actions/release-nexus-staging-repo@v1.1 with: username: arkivanov password: ${{ secrets.SONATYPE_PASSWORD }} staging_repository_id: ${{ needs.create-staging-repository.outputs.repository_id }} base_url: https://s01.oss.sonatype.org/service/local/ close_only: 'true' check-publication: name: Check publication runs-on: macos-14 needs: close-staging-repository steps: - name: Checkout uses: actions/checkout@v1 - name: Install Java uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 - name: Check publication run: ./gradlew kotlinUpgradeYarnLock :tools:check-publication:build -Pcheck_publication ================================================ FILE: .gitignore ================================================ *.iml .gradle local.properties .idea /build .DS_Store .kotlin ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ [![Maven Central](https://img.shields.io/maven-central/v/com.arkivanov.essenty/lifecycle?color=blue)](https://search.maven.org/search?q=g:com.arkivanov.essenty) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Twitter URL](https://img.shields.io/badge/Twitter-@arkann1985-blue.svg?style=social&logo=twitter)](https://twitter.com/arkann1985) # Essenty The most essential libraries for Kotlin Multiplatform development. Supported targets: - `android` - `jvm` - `js` - `wasmJs` - `ios` - `watchos` - `tvos` - `macos` - `linuxX64` ## Lifecycle When writing Kotlin Multiplatform (common) code we often need to handle lifecycle events of a screen. For example, to stop background operations when the screen is destroyed, or to reload some data when the screen is activated. Essenty provides the `Lifecycle` API to help with lifecycle handling in the common code. It is very similar to [Android Activity lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle). ### Setup Groovy: ```groovy // Add the dependency, typically under the commonMain source set implementation "com.arkivanov.essenty:lifecycle:" ``` Kotlin: ```kotlin // Add the dependency, typically under the commonMain source set implementation("com.arkivanov.essenty:lifecycle:") ``` ### Lifecycle state transitions ### Content The main [Lifecycle](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/Lifecycle.kt) interface provides ability to observe the lifecycle state changes. There are also handy [extension functions](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleExt.kt) for convenience. The [LifecycleRegistry](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistry.kt) interface extends both the `Lifecycle` and the `Lifecycle.Callbacks` at the same time. It can be used to manually control the lifecycle, for example in tests. You can also find some useful [extension functions](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryExt.kt). The [LifecycleOwner](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleOwner.kt) just holds the `Lifecyle`. It may be implemented by an arbitrary class, to provide convenient API. #### Android extensions From Android, the `Lifecycle` can be obtained by using special functions, can be found [here](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/androidMain/kotlin/com/arkivanov/essenty/lifecycle/AndroidExt.kt). #### iOS and tvOS extensions There is [ApplicationLifecycle](https://github.com/arkivanov/Essenty/blob/master/lifecycle/src/itvosMain/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecycle.kt) available for `ios` and `tvos` targets. It follows the `UIApplication` lifecycle notifications. > ⚠️ Since this implementation subscribes to `UIApplication` global lifecycle events, the instance and all its registered callbacks (and whatever they capture) will stay in memory until the application is destroyed or until `ApplicationLifecycle#destroy` method is called. It's ok to use it in a global scope like `UIApplicationDelegate`, but it may cause memory leaks when used in a narrower scope like `UIViewController` if it gets destroyed earlier. Use the `destroy` method to destroy the lifecycle manually and prevent memory leaks. #### Reaktive extensions There are some useful `Lifecycle` extensions for Reaktive. - Automatic management of `Disposable` and `DisposableScope` by `Lifecycle`, can be found [here](https://github.com/arkivanov/Essenty/blob/master/lifecycle-reaktive/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycle.kt). #### Coroutines extensions There are some useful `Lifecycle` extensions for Coroutines. - Automatic management of `CoroutineScope` by `Lifecycle`, can be found [here](https://github.com/arkivanov/Essenty/blob/master/lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycle.kt) - `Flow.withLifecycle(Lifecycle): Flow` - can be found [here](https://github.com/arkivanov/Essenty/blob/master/lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycle.kt). - `Lifecycle.repeatOnLifecycle(block)` - can be found [here](https://github.com/arkivanov/Essenty/blob/master/lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycle.kt). ### Usage example #### Observing the Lifecyle The lifecycle can be observed using its `subscribe`/`unsubscribe` methods: ```kotlin import com.arkivanov.essenty.lifecycle.Lifecycle class SomeLogic(lifecycle: Lifecycle) { init { lifecycle.subscribe( object : Lifecycle.Callbacks { override fun onCreate() { // Handle lifecycle created } // onStart, onResume, onPause, onStop are also available override fun onDestroy() { // Handle lifecycle destroyed } } ) } } ``` Or using the extension functions: ```kotlin import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.doOnCreate import com.arkivanov.essenty.lifecycle.doOnDestroy import com.arkivanov.essenty.lifecycle.subscribe class SomeLogic(lifecycle: Lifecycle) { init { lifecycle.subscribe( onCreate = { /* Handle lifecycle created */ }, // onStart, onResume, onPause, onStop are also available onDestroy = { /* Handle lifecycle destroyed */ } ) lifecycle.doOnCreate { // Handle lifecycle created } // doOnStart, doOnResume, doOnPause, doOnStop are also available lifecycle.doOnDestroy { // Handle lifecycle destroyed } } } ``` #### Using the LifecycleRegistry manually A default implementation of the `LifecycleRegisty` interface can be instantiated using the corresponding builder function: ```kotlin import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.resume import com.arkivanov.essenty.lifecycle.destroy val lifecycleRegistry = LifecycleRegistry() val someLogic = SomeLogic(lifecycleRegistry) lifecycleRegistry.resume() // At some point later lifecycleRegistry.destroy() ``` ## StateKeeper When writing common code targeting Android, it might be required to preserve some data over process death or Android configuration changes. For this purpose, Essenty provides the `StateKeeper` API, which is inspired by the AndroidX [SavedStateHandle](https://developer.android.com/reference/androidx/lifecycle/SavedStateHandle). ### Setup Groovy: ```groovy // Add the dependency, typically under the commonMain source set implementation "com.arkivanov.essenty:state-keeper:" ``` Kotlin: ```kotlin // Add the dependency, typically under the commonMain source set implementation("com.arkivanov.essenty:state-keeper:") ``` ### Content The main [StateKeeper](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeper.kt) interface provides ability to register/unregister state suppliers, and also to consume any previously saved state. You can also find some handy [extension functions](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt). You can also find some handy [extension functions](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt). The [StateKeeperDispatcher](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperDispatcher.kt) interface extends `StateKeeper` and allows state saving, by calling all registered state providers. The [StateKeeperOwner](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperOwner.kt) interface is just a holder of `StateKeeper`. It may be implemented by an arbitrary class, to provide convenient API. #### Android extensions From Android side, `StateKeeper` can be obtained by using special functions, can be found [here](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/AndroidExt.kt). There are also some handy [extension functions](https://github.com/arkivanov/Essenty/blob/master/state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt) for serializing/deserializing `KSerializable` objects to/from [Bundle](https://developer.android.com/reference/android/os/Bundle): - `fun Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy)` - `fun Bundle.getSerializable(key: String?, strategy: DeserializationStrategy): T?` - `fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?)` - `fun Bundle.getSerializableContainer(key: String?): SerializableContainer?` Similar extensions are also available for [PersistableBundle](https://developer.android.com/reference/android/os/PersistableBundle). ### Usage example #### Using StateKeeper > ⚠️ Make sure you [setup](https://github.com/Kotlin/kotlinx.serialization#setup) `kotlinx-serialization` properly. ```kotlin import com.arkivanov.essenty.statekeeper.StateKeeper import kotlinx.serialization.Serializable class SomeLogic(stateKeeper: StateKeeper) { // Use the saved State if any, otherwise create a new State private var state: State = stateKeeper.consume(key = "SAVED_STATE", strategy = State.serializer()) ?: State() init { // Register the State supplier stateKeeper.register(key = "SAVED_STATE", strategy = State.serializer()) { state } } @Serializable private class State( val someValue: Int = 0 ) } ``` #### Saveable properties (experimental since version `2.2.0-alpha01`) ```kotlin import com.arkivanov.essenty.statekeeper.StateKeeper import com.arkivanov.essenty.statekeeper.saveable import kotlinx.serialization.Serializable class SomeLogic(stateKeeper: StateKeeper) { private var state: State by stateKeeper.saveable(serializer = State.serializer(), init = ::State) @Serializable private class State(val someValue: Int = 0) } ``` #### Saveable state holders (experimental since version `2.2.0-alpha01`) ```kotlin import com.arkivanov.essenty.statekeeper.StateKeeper import com.arkivanov.essenty.statekeeper.saveable import kotlinx.serialization.Serializable class SomeLogic(stateKeeper: StateKeeper) { private val viewModel by stateKeeper.saveable(serializer = State.serializer(), state = ViewModel::state) { savedState -> ViewModel(state = savedState ?: State()) } private class ViewModel(var state: State) @Serializable private class State(val someValue: Int = 0) } ``` ##### Polymorphic serialization (experimental) Sometimes it might be necessary to serialize an interface or an abstract class that you don't own but have implemented. For this purpose Essenty provides `polymorphicSerializer` function that can be used to create custom polymorphic serializers for unowned base types. For example a third-party library may have the following interface. ```kotlin interface Filter { // Omitted code } ``` Then we can have multiple implementations of `Filter`. ```kotlin @Serializable class TextFilter(val text: String) : Filter { /* Omitted code */ } @Serializable class RatingFilter(val stars: Int) : Filter { /* Omitted code */ } ``` Now we can create a polymorphic serializer for `Filter` as follows. It can be used to save and restore `Filter` directly via StateKeeper, or to have `Filter` as part of another `Serializable` class. ```kotlin import com.arkivanov.essenty.statekeeper.polymorphicSerializer import com.slack.circuit.runtime.screen.Screen import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic object FilterSerializer : KSerializer by polymorphicSerializer( SerializersModule { polymorphic(Filter::class) { subclass(TextFilter::class, TextFilter.serializer()) subclass(RatingFilter::class, RatingFilter.serializer()) } } ) ``` #### Using the StateKeeperDispatcher manually On Android, the `StateKeeper` obtained via one of the extensions described above automatically saves and restores the state. On other platforms (if needed) the state can be saved and restored manually. A default implementation of `StateKeeperDisptacher` interface can be instantiated using the corresponding builder function. The state can be encoded as a JSON string and saved using the corresponding platform-specific API. ```kotlin import com.arkivanov.essenty.statekeeper.SerializableContainer import com.arkivanov.essenty.statekeeper.StateKeeper import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher val stateKeeperDispatcher = StateKeeperDispatcher(/*Previously saved state, or null*/) val someLogic = SomeLogic(stateKeeperDispatcher) // At some point later when it's time to save the state val savedState: SerializableContainer = stateKeeperDispatcher.save() // The returned SerializableContainer can now be saved using the corresponding platform-specific API ``` ## InstanceKeeper When writing common code targetting Android, it might be required to retain objects over Android configuration changes. This use case is covered by the `InstanceKeeper` API, which is similar to the AndroidX [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel). ### Setup Groovy: ```groovy // Add the dependency, typically under the commonMain source set implementation "com.arkivanov.essenty:instance-keeper:" ``` Kotlin: ```kotlin // Add the dependency, typically under the commonMain source set implementation("com.arkivanov.essenty:instance-keeper:") ``` ### Content The main [InstanceKeeper](https://github.com/arkivanov/Essenty/blob/master/instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeper.kt) interface is responsible for storing object instances, represented by the [InstanceKeeper.Instance] interface. Instances of the `InstanceKeeper.Instance` interface survive Android Configuration changes, the `InstanceKeeper.Instance.onDestroy()` method is called when `InstanceKeeper` goes out of scope (e.g. the screen is finished). You can also find some handy [extension functions](https://github.com/arkivanov/Essenty/blob/master/instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperExt.kt). The [InstanceKeeperDispatcher](https://github.com/arkivanov/Essenty/blob/master/instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher.kt) interface extends `InstanceKeeper` and adds ability to destroy all registered instances. The [InstanceKeeperOwner](https://github.com/arkivanov/Essenty/blob/master/instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner.kt) interface is just a holder of `InstanceKeeper`. It may be implemented by an arbitrary class, to provide convenient API. #### Android extensions From Android side, `InstanceKeeper` can be obtained by using special functions, can be found [here](https://github.com/arkivanov/Essenty/blob/master/instance-keeper/src/androidMain/kotlin/com/arkivanov/essenty/instancekeeper/AndroidExt.kt). ### Usage example #### Using the InstanceKeeper ```kotlin import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.getOrCreate class SomeLogic(instanceKeeper: InstanceKeeper) { // Get the existing instance or create a new one private val viewModel = instanceKeeper.getOrCreate { ViewModel() } } /* * Survives Android configuration changes. * ⚠️ Pay attention to not leak any dependencies. */ class ViewModel : InstanceKeeper.Instance { override fun onDestroy() { // Called when the screen is finished } } ``` ##### Alternative way (experimental since version 2.2.0-alpha01, stable since 2.2.0) ```kotlin class SomeLogic(instanceKeeperOwner: InstanceKeeperOwner) : InstanceKeeperOwner by instanceKeeperOwner { // Get the existing instance or create a new one private val viewModel = retainedInstance { ViewModel() } } ``` #### Using the InstanceKeeperDispatcher manually A default implementation of the `InstanceKeeperDispatcher` interface can be instantiated using the corresponding builder function: ```kotlin import com.arkivanov.essenty.instancekeeper.InstanceKeeper import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher // Create a new instance of InstanceKeeperDispatcher, or reuse an existing one val instanceKeeperDispatcher = InstanceKeeperDispatcher() val someLogic = SomeLogic(instanceKeeperDispatcher) // At some point later instanceKeeperDispatcher.destroy() ``` ## BackHandler The `BackHandler` API provides ability to handle back button clicks (e.g. the Android device's back button), in common code. This API is similar to AndroidX [OnBackPressedDispatcher](https://developer.android.com/reference/androidx/activity/OnBackPressedDispatcher). ### Setup Groovy: ```groovy // Add the dependency, typically under the commonMain source set implementation "com.arkivanov.essenty:back-handler:" ``` Kotlin: ```kotlin // Add the dependency, typically under the commonMain source set implementation("com.arkivanov.essenty:back-handler:") ``` ### Content The [BackHandler](https://github.com/arkivanov/Essenty/blob/master/back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackHandler.kt) interface provides ability to register and unregister back button callbacks. When the device's back button is pressed, all registered callbacks are called in reverse order, the first enabled callback is called and the iteration finishes. > Starting from `v1.2.x`, when the device's back button is pressed, all registered callbacks are sorted in ascending order first by priority and then by index, the last enabled callback is called. [BackCallback](https://github.com/arkivanov/Essenty/blob/master/back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackCallback.kt) allows handling back events, including predictive back gestures. The [BackDispatcher](https://github.com/arkivanov/Essenty/blob/master/back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackDispatcher.kt) interface extends `BackHandler` and is responsible for triggering the registered callbacks. The `BackDispatcher.back()` method triggers all registered callbacks in reverse order, and returns `true` if an enabled callback was called, and `false` if no enabled callback was found. #### Android extensions From Android side, `BackHandler` can be obtained by using special functions, can be found [here](https://github.com/arkivanov/Essenty/blob/master/back-handler/src/androidMain/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandler.kt). ### Predictive Back Gesture Both `BackHandler` and `BackDispatcher` bring the new [Android Predictive Back Gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) to Kotlin Multiplatform. #### Predictive Back Gesture on Android On Android, the predictive back gesture only works starting with Android T. On Android T, it works only between Activities, if enabled in the system settings. Starting with Android U, the predictive back gesture also works between application's screens inside an Activity. In the latter case, back gesture events can be handled using `BackCallback`. #### Predictive Back Gesture on other platforms On all other platforms, predictive back gestures can be dispatched manually via `BackDispatcher`. This can be done e.g. by adding an overlay on top of the UI and handling touch events manually. ### Usage example #### Using the BackHandler ```kotlin import com.arkivanov.essenty.backhandler.BackHandler class SomeLogic(backHandler: BackHandler) { private val callback = BackCallback { // Called when the back button is pressed } init { backHandler.register(callback) // Disable the callback when needed callback.isEnabled = false } } ``` #### Using the BackDispatcher manually A default implementation of the `BackDispatcher` interface can be instantiated using the corresponding builder function: ```kotlin import com.arkivanov.essenty.backhandler.BackDispatcher val backDispatcher = BackDispatcher() val someLogic = SomeLogic(backDispatcher) if (!backDispatcher.back()) { // The back pressed event was not handled } ``` ## Author Twitter: [@arkann1985](https://twitter.com/arkann1985) If you like this project you can always Buy Me A Coffee ;-) ================================================ FILE: back-handler/.gitignore ================================================ /build ================================================ FILE: back-handler/api/android/back-handler.api ================================================ public final class com/arkivanov/essenty/backhandler/AndroidBackHandlerKt { public static final fun BackHandler (Landroidx/activity/OnBackPressedDispatcher;)Lcom/arkivanov/essenty/backhandler/BackHandler; public static final fun BackHandler (Landroidx/activity/OnBackPressedDispatcher;Landroidx/lifecycle/LifecycleOwner;)Lcom/arkivanov/essenty/backhandler/BackHandler; public static final fun backHandler (Landroidx/activity/OnBackPressedDispatcherOwner;)Lcom/arkivanov/essenty/backhandler/BackHandler; public static final fun connectOnBackPressedCallback (Lcom/arkivanov/essenty/backhandler/BackDispatcher;)Landroidx/activity/OnBackPressedCallback; } public abstract class com/arkivanov/essenty/backhandler/BackCallback { public static final field Companion Lcom/arkivanov/essenty/backhandler/BackCallback$Companion; public static final field PRIORITY_DEFAULT I public static final field PRIORITY_MAX I public static final field PRIORITY_MIN I public fun ()V public fun (ZI)V public synthetic fun (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public final fun getPriority ()I public final fun isEnabled ()Z public abstract fun onBack ()V public fun onBackCancelled ()V public fun onBackProgressed (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public fun onBackStarted (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public final fun setEnabled (Z)V public final fun setPriority (I)V } public final class com/arkivanov/essenty/backhandler/BackCallback$Companion { } public final class com/arkivanov/essenty/backhandler/BackCallbackKt { public static final fun BackCallback (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/backhandler/BackCallback; public static synthetic fun BackCallback$default (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackCallback; } public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler { public abstract fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public abstract fun back ()Z public abstract fun cancelPredictiveBack ()V public abstract fun isEnabled ()Z public abstract fun progressPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public abstract fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public abstract fun startPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)Z } public final class com/arkivanov/essenty/backhandler/BackDispatcherKt { public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher; } public final class com/arkivanov/essenty/backhandler/BackEvent { public fun ()V public fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)V public synthetic fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()F public final fun component2 ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public final fun component3 ()F public final fun component4 ()F public final fun copy (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)Lcom/arkivanov/essenty/backhandler/BackEvent; public static synthetic fun copy$default (Lcom/arkivanov/essenty/backhandler/BackEvent;FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackEvent; public fun equals (Ljava/lang/Object;)Z public final fun getProgress ()F public final fun getSwipeEdge ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public final fun getTouchX ()F public final fun getTouchY ()F public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/arkivanov/essenty/backhandler/BackEvent$SwipeEdge : java/lang/Enum { public static final field LEFT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static final field RIGHT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static final field UNKNOWN Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static fun values ()[Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; } public abstract interface class com/arkivanov/essenty/backhandler/BackHandler { public abstract fun isRegistered (Lcom/arkivanov/essenty/backhandler/BackCallback;)Z public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackCallback;)V public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackCallback;)V } public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner { public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; } ================================================ FILE: back-handler/api/back-handler.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: abstract interface com.arkivanov.essenty.backhandler/BackDispatcher : com.arkivanov.essenty.backhandler/BackHandler { // com.arkivanov.essenty.backhandler/BackDispatcher|null[0] abstract val isEnabled // com.arkivanov.essenty.backhandler/BackDispatcher.isEnabled|{}isEnabled[0] abstract fun (): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackDispatcher.isEnabled.|(){}[0] abstract fun addEnabledChangedListener(kotlin/Function1) // com.arkivanov.essenty.backhandler/BackDispatcher.addEnabledChangedListener|addEnabledChangedListener(kotlin.Function1){}[0] abstract fun back(): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackDispatcher.back|back(){}[0] abstract fun cancelPredictiveBack() // com.arkivanov.essenty.backhandler/BackDispatcher.cancelPredictiveBack|cancelPredictiveBack(){}[0] abstract fun progressPredictiveBack(com.arkivanov.essenty.backhandler/BackEvent) // com.arkivanov.essenty.backhandler/BackDispatcher.progressPredictiveBack|progressPredictiveBack(com.arkivanov.essenty.backhandler.BackEvent){}[0] abstract fun removeEnabledChangedListener(kotlin/Function1) // com.arkivanov.essenty.backhandler/BackDispatcher.removeEnabledChangedListener|removeEnabledChangedListener(kotlin.Function1){}[0] abstract fun startPredictiveBack(com.arkivanov.essenty.backhandler/BackEvent): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackDispatcher.startPredictiveBack|startPredictiveBack(com.arkivanov.essenty.backhandler.BackEvent){}[0] } abstract interface com.arkivanov.essenty.backhandler/BackHandler { // com.arkivanov.essenty.backhandler/BackHandler|null[0] abstract fun isRegistered(com.arkivanov.essenty.backhandler/BackCallback): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackHandler.isRegistered|isRegistered(com.arkivanov.essenty.backhandler.BackCallback){}[0] abstract fun register(com.arkivanov.essenty.backhandler/BackCallback) // com.arkivanov.essenty.backhandler/BackHandler.register|register(com.arkivanov.essenty.backhandler.BackCallback){}[0] abstract fun unregister(com.arkivanov.essenty.backhandler/BackCallback) // com.arkivanov.essenty.backhandler/BackHandler.unregister|unregister(com.arkivanov.essenty.backhandler.BackCallback){}[0] } abstract interface com.arkivanov.essenty.backhandler/BackHandlerOwner { // com.arkivanov.essenty.backhandler/BackHandlerOwner|null[0] abstract val backHandler // com.arkivanov.essenty.backhandler/BackHandlerOwner.backHandler|{}backHandler[0] abstract fun (): com.arkivanov.essenty.backhandler/BackHandler // com.arkivanov.essenty.backhandler/BackHandlerOwner.backHandler.|(){}[0] } abstract class com.arkivanov.essenty.backhandler/BackCallback { // com.arkivanov.essenty.backhandler/BackCallback|null[0] constructor (kotlin/Boolean = ..., kotlin/Int = ...) // com.arkivanov.essenty.backhandler/BackCallback.|(kotlin.Boolean;kotlin.Int){}[0] final var isEnabled // com.arkivanov.essenty.backhandler/BackCallback.isEnabled|{}isEnabled[0] final fun (): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackCallback.isEnabled.|(){}[0] final fun (kotlin/Boolean) // com.arkivanov.essenty.backhandler/BackCallback.isEnabled.|(kotlin.Boolean){}[0] final var priority // com.arkivanov.essenty.backhandler/BackCallback.priority|{}priority[0] final fun (): kotlin/Int // com.arkivanov.essenty.backhandler/BackCallback.priority.|(){}[0] final fun (kotlin/Int) // com.arkivanov.essenty.backhandler/BackCallback.priority.|(kotlin.Int){}[0] abstract fun onBack() // com.arkivanov.essenty.backhandler/BackCallback.onBack|onBack(){}[0] final fun addEnabledChangedListener(kotlin/Function1) // com.arkivanov.essenty.backhandler/BackCallback.addEnabledChangedListener|addEnabledChangedListener(kotlin.Function1){}[0] final fun removeEnabledChangedListener(kotlin/Function1) // com.arkivanov.essenty.backhandler/BackCallback.removeEnabledChangedListener|removeEnabledChangedListener(kotlin.Function1){}[0] open fun onBackCancelled() // com.arkivanov.essenty.backhandler/BackCallback.onBackCancelled|onBackCancelled(){}[0] open fun onBackProgressed(com.arkivanov.essenty.backhandler/BackEvent) // com.arkivanov.essenty.backhandler/BackCallback.onBackProgressed|onBackProgressed(com.arkivanov.essenty.backhandler.BackEvent){}[0] open fun onBackStarted(com.arkivanov.essenty.backhandler/BackEvent) // com.arkivanov.essenty.backhandler/BackCallback.onBackStarted|onBackStarted(com.arkivanov.essenty.backhandler.BackEvent){}[0] final object Companion { // com.arkivanov.essenty.backhandler/BackCallback.Companion|null[0] final const val PRIORITY_DEFAULT // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_DEFAULT|{}PRIORITY_DEFAULT[0] final fun (): kotlin/Int // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_DEFAULT.|(){}[0] final const val PRIORITY_MAX // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_MAX|{}PRIORITY_MAX[0] final fun (): kotlin/Int // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_MAX.|(){}[0] final const val PRIORITY_MIN // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_MIN|{}PRIORITY_MIN[0] final fun (): kotlin/Int // com.arkivanov.essenty.backhandler/BackCallback.Companion.PRIORITY_MIN.|(){}[0] } } final class com.arkivanov.essenty.backhandler/BackEvent { // com.arkivanov.essenty.backhandler/BackEvent|null[0] constructor (kotlin/Float = ..., com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge = ..., kotlin/Float = ..., kotlin/Float = ...) // com.arkivanov.essenty.backhandler/BackEvent.|(kotlin.Float;com.arkivanov.essenty.backhandler.BackEvent.SwipeEdge;kotlin.Float;kotlin.Float){}[0] final val progress // com.arkivanov.essenty.backhandler/BackEvent.progress|{}progress[0] final fun (): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.progress.|(){}[0] final val swipeEdge // com.arkivanov.essenty.backhandler/BackEvent.swipeEdge|{}swipeEdge[0] final fun (): com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge // com.arkivanov.essenty.backhandler/BackEvent.swipeEdge.|(){}[0] final val touchX // com.arkivanov.essenty.backhandler/BackEvent.touchX|{}touchX[0] final fun (): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.touchX.|(){}[0] final val touchY // com.arkivanov.essenty.backhandler/BackEvent.touchY|{}touchY[0] final fun (): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.touchY.|(){}[0] final fun component1(): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.component1|component1(){}[0] final fun component2(): com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge // com.arkivanov.essenty.backhandler/BackEvent.component2|component2(){}[0] final fun component3(): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.component3|component3(){}[0] final fun component4(): kotlin/Float // com.arkivanov.essenty.backhandler/BackEvent.component4|component4(){}[0] final fun copy(kotlin/Float = ..., com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge = ..., kotlin/Float = ..., kotlin/Float = ...): com.arkivanov.essenty.backhandler/BackEvent // com.arkivanov.essenty.backhandler/BackEvent.copy|copy(kotlin.Float;com.arkivanov.essenty.backhandler.BackEvent.SwipeEdge;kotlin.Float;kotlin.Float){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // com.arkivanov.essenty.backhandler/BackEvent.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.arkivanov.essenty.backhandler/BackEvent.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.arkivanov.essenty.backhandler/BackEvent.toString|toString(){}[0] final enum class SwipeEdge : kotlin/Enum { // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge|null[0] enum entry LEFT // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.LEFT|null[0] enum entry RIGHT // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.RIGHT|null[0] enum entry UNKNOWN // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.UNKNOWN|null[0] final val entries // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.entries|#static{}entries[0] final fun (): kotlin.enums/EnumEntries // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.entries.|#static(){}[0] final fun valueOf(kotlin/String): com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.valueOf|valueOf#static(kotlin.String){}[0] final fun values(): kotlin/Array // com.arkivanov.essenty.backhandler/BackEvent.SwipeEdge.values|values#static(){}[0] } } final fun com.arkivanov.essenty.backhandler/BackCallback(kotlin/Boolean = ..., kotlin/Int = ..., kotlin/Function1? = ..., kotlin/Function1? = ..., kotlin/Function0? = ..., kotlin/Function0): com.arkivanov.essenty.backhandler/BackCallback // com.arkivanov.essenty.backhandler/BackCallback|BackCallback(kotlin.Boolean;kotlin.Int;kotlin.Function1?;kotlin.Function1?;kotlin.Function0?;kotlin.Function0){}[0] final fun com.arkivanov.essenty.backhandler/BackDispatcher(): com.arkivanov.essenty.backhandler/BackDispatcher // com.arkivanov.essenty.backhandler/BackDispatcher|BackDispatcher(){}[0] ================================================ FILE: back-handler/api/jvm/back-handler.api ================================================ public abstract class com/arkivanov/essenty/backhandler/BackCallback { public static final field Companion Lcom/arkivanov/essenty/backhandler/BackCallback$Companion; public static final field PRIORITY_DEFAULT I public static final field PRIORITY_MAX I public static final field PRIORITY_MIN I public fun ()V public fun (ZI)V public synthetic fun (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public final fun getPriority ()I public final fun isEnabled ()Z public abstract fun onBack ()V public fun onBackCancelled ()V public fun onBackProgressed (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public fun onBackStarted (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public final fun setEnabled (Z)V public final fun setPriority (I)V } public final class com/arkivanov/essenty/backhandler/BackCallback$Companion { } public final class com/arkivanov/essenty/backhandler/BackCallbackKt { public static final fun BackCallback (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/backhandler/BackCallback; public static synthetic fun BackCallback$default (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackCallback; } public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler { public abstract fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public abstract fun back ()Z public abstract fun cancelPredictiveBack ()V public abstract fun isEnabled ()Z public abstract fun progressPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)V public abstract fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V public abstract fun startPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)Z } public final class com/arkivanov/essenty/backhandler/BackDispatcherKt { public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher; } public final class com/arkivanov/essenty/backhandler/BackEvent { public fun ()V public fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)V public synthetic fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()F public final fun component2 ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public final fun component3 ()F public final fun component4 ()F public final fun copy (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)Lcom/arkivanov/essenty/backhandler/BackEvent; public static synthetic fun copy$default (Lcom/arkivanov/essenty/backhandler/BackEvent;FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackEvent; public fun equals (Ljava/lang/Object;)Z public final fun getProgress ()F public final fun getSwipeEdge ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public final fun getTouchX ()F public final fun getTouchY ()F public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class com/arkivanov/essenty/backhandler/BackEvent$SwipeEdge : java/lang/Enum { public static final field LEFT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static final field RIGHT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static final field UNKNOWN Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; public static fun values ()[Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; } public abstract interface class com/arkivanov/essenty/backhandler/BackHandler { public abstract fun isRegistered (Lcom/arkivanov/essenty/backhandler/BackCallback;)Z public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackCallback;)V public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackCallback;)V } public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner { public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; } ================================================ FILE: back-handler/build.gradle.kts ================================================ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.backhandler" } kotlin { setupSourceSets { val android by bundle() common.main.dependencies { implementation(project(":utils-internal")) } android.main.dependencies { implementation(deps.androidx.activity.activityKtx) } } } ================================================ FILE: back-handler/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: back-handler/src/androidMain/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandler.kt ================================================ package com.arkivanov.essenty.backhandler import androidx.activity.BackEventCompat import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.lifecycle.LifecycleOwner /** * Creates a new instance of [BackHandler] and attaches it to the provided AndroidX [OnBackPressedDispatcher]. */ fun BackHandler(onBackPressedDispatcher: OnBackPressedDispatcher): BackHandler = BackDispatcher().also { dispatcher -> onBackPressedDispatcher.addCallback(dispatcher.connectOnBackPressedCallback()) } /** * Creates a new instance of [BackHandler] and attaches it to the provided AndroidX [OnBackPressedDispatcher] * only when the [LifecycleOwner]'s Lifecycle is [STARTED][androidx.lifecycle.Lifecycle.State.STARTED]. */ fun BackHandler( onBackPressedDispatcher: OnBackPressedDispatcher, lifecycleOwner: LifecycleOwner, ): BackHandler = BackDispatcher().also { dispatcher -> onBackPressedDispatcher.addCallback(lifecycleOwner, dispatcher.connectOnBackPressedCallback()) } /** * Creates a new instance of [BackHandler] and attaches it to the AndroidX [OnBackPressedDispatcher]. */ fun OnBackPressedDispatcherOwner.backHandler(): BackHandler = BackHandler(onBackPressedDispatcher = onBackPressedDispatcher) /** * Creates a new instance of [OnBackPressedCallback] and connects it with this [BackDispatcher]. * All events from the returned [OnBackPressedCallback] are forwarded to this [BackDispatcher]. * The enabled state from this [BackDispatcher] is forwarded to the returned [OnBackPressedCallback]. */ fun BackDispatcher.connectOnBackPressedCallback(): OnBackPressedCallback = OnBackPressedCallbackAdapter(dispatcher = this) private class OnBackPressedCallbackAdapter( private val dispatcher: BackDispatcher, ) : OnBackPressedCallback(enabled = dispatcher.isEnabled) { init { dispatcher.addEnabledChangedListener { isEnabled = it } } override fun handleOnBackPressed() { dispatcher.back() } override fun handleOnBackStarted(backEvent: BackEventCompat) { dispatcher.startPredictiveBack(backEvent.toEssentyBackEvent()) } override fun handleOnBackProgressed(backEvent: BackEventCompat) { dispatcher.progressPredictiveBack(backEvent.toEssentyBackEvent()) } override fun handleOnBackCancelled() { dispatcher.cancelPredictiveBack() } private fun BackEventCompat.toEssentyBackEvent(): BackEvent = BackEvent( progress = progress, swipeEdge = when (swipeEdge) { BackEventCompat.EDGE_LEFT -> BackEvent.SwipeEdge.LEFT BackEventCompat.EDGE_RIGHT -> BackEvent.SwipeEdge.RIGHT else -> BackEvent.SwipeEdge.UNKNOWN }, touchX = touchX, touchY = touchY, ) } ================================================ FILE: back-handler/src/androidUnitTest/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandlerTest.kt ================================================ package com.arkivanov.essenty.backhandler import androidx.activity.BackEventCompat import androidx.activity.BackEventCompat.Companion.EDGE_LEFT import androidx.activity.BackEventCompat.Companion.EDGE_RIGHT import androidx.activity.OnBackPressedDispatcher import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class AndroidBackHandlerTest { private val dispatcher = OnBackPressedDispatcher() private val handler = BackHandler(onBackPressedDispatcher = dispatcher) @Test fun WHEN_created_THEN_hasEnabledCallbacks_false() { assertFalse(dispatcher.hasEnabledCallbacks()) } @Test fun WHEN_enabled_callback_registered_THEN_hasEnabledCallbacks_true() { handler.register(callback(isEnabled = true)) assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun WHEN_disabled_callback_registered_THEN_hasEnabledCallbacks_false() { handler.register(callback(isEnabled = false)) assertFalse(dispatcher.hasEnabledCallbacks()) } @Test fun WHEN_multiple_callbacks_registered_and_one_enabled_THEN_hasEnabledCallbacks_true() { handler.register(callback(isEnabled = false)) handler.register(callback(isEnabled = true)) handler.register(callback(isEnabled = false)) assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_multiple_disabled_callbacks_WHEN_one_callback_enabled_THEN_hasEnabledCallbacks_true() { val callback2 = callback(isEnabled = false) handler.register(callback(isEnabled = false)) handler.register(callback2) handler.register(callback(isEnabled = false)) callback2.isEnabled = true assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_disabled_except_one_THEN_hasEnabledCallbacks_true() { val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true)) callbacks.forEach(handler::register) callbacks.drop(1).forEach { it.isEnabled = false } assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_disabled_THEN_hasEnabledCallbacks_false() { val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true)) callbacks.forEach(handler::register) callbacks.forEach { it.isEnabled = false } assertFalse(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_removed_THEN_hasEnabledCallbacks_false() { val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true)) callbacks.forEach(handler::register) callbacks.forEach(handler::unregister) assertFalse(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_removed_except_one_THEN_hasEnabledCallbacks_true() { val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true)) callbacks.forEach(handler::register) callbacks.drop(1).forEach(handler::unregister) assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_all_callbacks_disabled_WHEN_onBackPressed_THEN_callbacks_not_called() { var isCalled = false repeat(3) { handler.register(callback(isEnabled = false) { isCalled = true }) } dispatcher.onBackPressed() assertFalse(isCalled) } @Test fun GIVEN_all_callbacks_enabled_WHEN_onBackPressed_THEN_only_last_callback_called() { val called = MutableList(3) { false } repeat(called.size) { index -> handler.register(callback(isEnabled = true) { called[index] = true }) } dispatcher.onBackPressed() assertContentEquals(listOf(false, false, true), called) } @Test fun GIVEN_only_one_callback_enabled_WHEN_onBackPressed_THEN_only_enabled_callback_called() { val called = MutableList(3) { false } repeat(called.size) { index -> handler.register(callback(isEnabled = index == 0) { called[index] = true }) } dispatcher.onBackPressed() assertContentEquals(listOf(true, false, false), called) } @Test fun GIVEN_multiple_enabled_callbacks_registered_and_all_callbacks_removed_except_one_WHEN_onBackPressed_THEN_callback_called() { val called = MutableList(3) { false } val callbacks = List(called.size) { index -> callback(isEnabled = true) { called[index] = true } } callbacks.forEach(handler::register) callbacks.drop(1).forEach(handler::unregister) dispatcher.onBackPressed() assertContentEquals(listOf(true, false, false), called) } @Test fun GIVEN_enabled_callbacks_registered_with_priorities_WHEN_onBackPressed_THEN_last_callback_with_higher_priority_called() { val list = ArrayList() handler.register(callback(isEnabled = true, priority = 0) { list += 1 }) handler.register(callback(isEnabled = true, priority = 1) { list += 2 }) handler.register(callback(isEnabled = true, priority = 2) { list += 3 }) handler.register(callback(isEnabled = true, priority = 1) { list += 4 }) handler.register(callback(isEnabled = true, priority = 2) { list += 5 }) handler.register(callback(isEnabled = true, priority = 0) { list += 6 }) handler.register(callback(isEnabled = true, priority = 1) { list += 7 }) handler.register(callback(isEnabled = true, priority = 0) { list += 8 }) dispatcher.onBackPressed() assertContentEquals(listOf(5), list) } @Test fun GIVEN_enabled_callbacks_registered_with_priorities_WHEN_priority_changed_and_onBackPressed_THEN_last_callback_with_higher_priority_called() { val list = ArrayList() handler.register(callback(isEnabled = true, priority = 0) { list += 1 }) val callback = callback(isEnabled = true, priority = 1) { list += 2 } handler.register(callback) handler.register(callback(isEnabled = true, priority = 2) { list += 3 }) callback.priority = 3 dispatcher.onBackPressed() assertContentEquals(listOf(2), list) } @Test fun WHEN_progress_with_back_THEN_callbacks_called() { val receivedEvents = ArrayList() handler.register( BackCallback( onBackStarted = { receivedEvents += it }, onBackProgressed = { receivedEvents += it }, onBackCancelled = { receivedEvents += "Cancel" }, onBack = { receivedEvents += "Back" }, ) ) dispatcher.dispatchOnBackStarted(BackEventCompat(progress = 0.1F, swipeEdge = EDGE_LEFT, touchX = 1F, touchY = 2F)) dispatcher.dispatchOnBackProgressed(BackEventCompat(progress = 0.2F, swipeEdge = EDGE_RIGHT, touchX = 2F, touchY = 3F)) dispatcher.dispatchOnBackProgressed(BackEventCompat(progress = 0.3F, swipeEdge = EDGE_LEFT, touchX = 3F, touchY = 4F)) dispatcher.onBackPressed() assertContentEquals( listOf( BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F), BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F), BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F), "Back", ), receivedEvents, ) } @Test fun WHEN_progress_with_cancel_THEN_callbacks_called() { val receivedEvents = ArrayList() handler.register( BackCallback( onBackStarted = { receivedEvents += it }, onBackProgressed = { receivedEvents += it }, onBackCancelled = { receivedEvents += "Cancel" }, onBack = { receivedEvents += "Back" }, ) ) dispatcher.dispatchOnBackStarted(BackEventCompat(progress = 0.1F, swipeEdge = EDGE_LEFT, touchX = 1F, touchY = 2F)) dispatcher.dispatchOnBackProgressed(BackEventCompat(progress = 0.2F, swipeEdge = EDGE_RIGHT, touchX = 2F, touchY = 3F)) dispatcher.dispatchOnBackProgressed(BackEventCompat(progress = 0.3F, swipeEdge = EDGE_LEFT, touchX = 3F, touchY = 4F)) dispatcher.dispatchOnBackCancelled() assertContentEquals( listOf( BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F), BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F), BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F), "Cancel", ), receivedEvents, ) } private fun callback( isEnabled: Boolean = true, priority: Int = BackCallback.PRIORITY_DEFAULT, onBack: () -> Unit = {}, ): BackCallback = BackCallback( isEnabled = isEnabled, priority = priority, onBack = onBack, ) } ================================================ FILE: back-handler/src/androidUnitTest/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandlerWithLifecycleTest.kt ================================================ package com.arkivanov.essenty.backhandler import androidx.activity.OnBackPressedDispatcher import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class AndroidBackHandlerWithLifecycleTest { private val dispatcher = OnBackPressedDispatcher() private val lifecycleOwner = LifecycleOwnerImpl() @Test fun GIVEN_lifecycle_created_WHEN_handler_created_THEN_hasEnabledCallbacks_returns_false() { lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) val handler = handler() handler.register(callback()) assertFalse(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_lifecycle_started_WHEN_handler_created_THEN_hasEnabledCallbacks_returns_true() { lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) val handler = handler() handler.register(callback()) assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_handler_created_WHEN_lifecycle_started_THEN_hasEnabledCallbacks_returns_true() { val handler = handler() handler.register(callback()) lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) assertTrue(dispatcher.hasEnabledCallbacks()) } @Test fun GIVEN_lifecycle_started_WHEN_lifecycle_stopped_THEN_hasEnabledCallbacks_returns_false() { lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) val handler = handler() handler.register(callback()) lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP) assertFalse(dispatcher.hasEnabledCallbacks()) } private fun handler(): BackHandler = BackHandler( onBackPressedDispatcher = dispatcher, lifecycleOwner = lifecycleOwner, ) private fun callback(): BackCallback = BackCallback(isEnabled = true, onBack = {}) private class LifecycleOwnerImpl : LifecycleOwner { override val lifecycle: LifecycleRegistry = LifecycleRegistry.createUnsafe(this) } } ================================================ FILE: back-handler/src/androidUnitTest/kotlin/com/arkivanov/essenty/backhandler/OnBackPressedCallbackAdapterTest.kt ================================================ package com.arkivanov.essenty.backhandler import androidx.activity.BackEventCompat import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class OnBackPressedCallbackAdapterTest { private val dispatcher = BackDispatcher() @Test fun WHEN_connected_with_empty_dispatcher_THEN_disabled() { val callback = dispatcher.connectOnBackPressedCallback() assertFalse(callback.isEnabled) } @Test fun WHEN_connected_with_not_empty_disabled_dispatcher_THEN_disabled() { dispatcher.register(BackCallback(isEnabled = false, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() assertFalse(callback.isEnabled) } @Test fun WHEN_connected_with_not_empty_enabled_dispatcher_THEN_enabled() { dispatcher.register(BackCallback(isEnabled = true, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_empty_dispatcher_WHEN_disabled_callback_registered_in_dispatcher_THEN_disabled() { val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = false, onBack = {})) assertFalse(callback.isEnabled) } @Test fun GIVEN_connected_with_empty_dispatcher_WHEN_enabled_callback_registered_in_dispatcher_THEN_enabled() { val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = true, onBack = {})) assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_disabled_dispatcher_WHEN_disabled_callback_registered_in_dispatcher_THEN_disabled() { dispatcher.register(BackCallback(isEnabled = false, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = false, onBack = {})) assertFalse(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_disabled_dispatcher_WHEN_enabled_callback_registered_in_dispatcher_THEN_enabled() { dispatcher.register(BackCallback(isEnabled = false, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = true, onBack = {})) assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_enabled_dispatcher_WHEN_disabled_callback_registered_in_dispatcher_THEN_enabled() { dispatcher.register(BackCallback(isEnabled = true, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = false, onBack = {})) assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_enabled_dispatcher_WHEN_enabled_callback_registered_in_dispatcher_THEN_enabled() { dispatcher.register(BackCallback(isEnabled = true, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.register(BackCallback(isEnabled = true, onBack = {})) assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_disabled_dispatcher_WHEN_all_callbacks_unregistered_from_dispatcher_THEN_disabled() { val essentyCallback = BackCallback(isEnabled = false, onBack = {}) dispatcher.register(essentyCallback) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.unregister(essentyCallback) assertFalse(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_enabled_dispatcher_WHEN_not_all_callbacks_unregistered_from_dispatcher_THEN_enabled() { val essentyCallback = BackCallback(isEnabled = true, onBack = {}) dispatcher.register(essentyCallback) dispatcher.register(BackCallback(isEnabled = true, onBack = {})) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.unregister(essentyCallback) assertTrue(callback.isEnabled) } @Test fun GIVEN_connected_with_not_empty_enabled_dispatcher_WHEN_all_callbacks_unregistered_from_dispatcher_THEN_disabled() { val essentyCallback = BackCallback(isEnabled = true, onBack = {}) dispatcher.register(essentyCallback) val callback = dispatcher.connectOnBackPressedCallback() dispatcher.unregister(essentyCallback) assertFalse(callback.isEnabled) } @Test fun WHEN_progress_with_back_THEN_forwards() { val callback = dispatcher.connectOnBackPressedCallback() val receivedEvents = ArrayList() dispatcher.register( BackCallback( onBackStarted = { receivedEvents += it }, onBackProgressed = { receivedEvents += it }, onBackCancelled = { receivedEvents += "Cancel" }, onBack = { receivedEvents += "Back" }, ) ) callback.handleOnBackStarted(BackEventCompat(progress = 0.1F, swipeEdge = BackEventCompat.EDGE_LEFT, touchX = 1F, touchY = 2F)) callback.handleOnBackProgressed(BackEventCompat(progress = 0.2F, swipeEdge = BackEventCompat.EDGE_RIGHT, touchX = 2F, touchY = 3F)) callback.handleOnBackProgressed(BackEventCompat(progress = 0.3F, swipeEdge = BackEventCompat.EDGE_LEFT, touchX = 3F, touchY = 4F)) callback.handleOnBackPressed() assertContentEquals( listOf( BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F), BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F), BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F), "Back", ), receivedEvents, ) } @Test fun WHEN_progress_with_cancel_THEN_forwards() { val callback = dispatcher.connectOnBackPressedCallback() val receivedEvents = ArrayList() dispatcher.register( BackCallback( onBackStarted = { receivedEvents += it }, onBackProgressed = { receivedEvents += it }, onBackCancelled = { receivedEvents += "Cancel" }, onBack = { receivedEvents += "Back" }, ) ) callback.handleOnBackStarted(BackEventCompat(progress = 0.1F, swipeEdge = BackEventCompat.EDGE_LEFT, touchX = 1F, touchY = 2F)) callback.handleOnBackProgressed(BackEventCompat(progress = 0.2F, swipeEdge = BackEventCompat.EDGE_RIGHT, touchX = 2F, touchY = 3F)) callback.handleOnBackProgressed(BackEventCompat(progress = 0.3F, swipeEdge = BackEventCompat.EDGE_LEFT, touchX = 3F, touchY = 4F)) callback.handleOnBackCancelled() assertContentEquals( listOf( BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F), BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F), BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F), "Cancel", ), receivedEvents, ) } } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackCallback.kt ================================================ package com.arkivanov.essenty.backhandler import kotlin.properties.Delegates /** * A callback for back button handling. * * @param isEnabled the initial enabled state of the callback. * @param priority determines the order of callback execution. * When calling, callbacks are sorted in ascending order first by priority and then by index, * the last enabled callback gets called. */ abstract class BackCallback( isEnabled: Boolean = true, var priority: Int = PRIORITY_DEFAULT, ) { private var enabledListeners = emptySet<(Boolean) -> Unit>() /** * Controls the enabled state of the callback. */ var isEnabled: Boolean by Delegates.observable(isEnabled) { _, _, newValue -> enabledListeners.forEach { it(newValue) } } /** * Registers the specified [listener] to be called when the enabled state of the callback changes. */ fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { this.enabledListeners += listener } /** * Unregisters the specified [listener]. */ fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { this.enabledListeners -= listener } /** * Called when the back button is pressed, or the predictive back gesture is finished. */ abstract fun onBack() /** * Called when the predictive back gesture starts. */ open fun onBackStarted(backEvent: BackEvent) { } /** * Called on every progress of the predictive back gesture. */ open fun onBackProgressed(backEvent: BackEvent) { } /** * Called when the predictive back gesture is cancelled. */ open fun onBackCancelled() { } companion object { const val PRIORITY_DEFAULT: Int = 0 const val PRIORITY_MIN: Int = Int.MIN_VALUE const val PRIORITY_MAX: Int = Int.MAX_VALUE } } fun BackCallback( isEnabled: Boolean = true, priority: Int = 0, onBackStarted: ((BackEvent) -> Unit)? = null, onBackProgressed: ((BackEvent) -> Unit)? = null, onBackCancelled: (() -> Unit)? = null, onBack: () -> Unit, ): BackCallback = object : BackCallback(isEnabled = isEnabled, priority = priority) { override fun onBackStarted(backEvent: BackEvent) { onBackStarted?.invoke(backEvent) } override fun onBackProgressed(backEvent: BackEvent) { onBackProgressed?.invoke(backEvent) } override fun onBackCancelled() { onBackCancelled?.invoke() } override fun onBack() { onBack.invoke() } } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackDispatcher.kt ================================================ package com.arkivanov.essenty.backhandler import kotlin.js.JsName /** * Provides a way to manually trigger back button handlers. */ interface BackDispatcher : BackHandler { /** * Returns `true` if there is at least one enabled handler, `false` otherwise. */ val isEnabled: Boolean /** * Adds the provided [listener], which will be called every time the enabled state of * this [BackDispatcher] changes. */ fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) /** * Removes the provided enabled state changed [listener]. */ fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) /** * If no predictive back gesture is currently in progress, finds the last enabled * callback with the highest priority and calls [BackCallback.onBack]. * * If the predictive back gesture is currently in progress, calls [BackCallback.onBack] on * the previously selected callback. * * @return `true` if any callback was triggered, `false` otherwise. */ fun back(): Boolean /** * Starts handling the predictive back gesture. Picks one of the enabled callback (if any) * that will be handling the gesture and calls [BackCallback.onBackStarted]. * * @return `true` if any callback was triggered, `false` otherwise. */ fun startPredictiveBack(backEvent: BackEvent): Boolean /** * Calls [BackCallback.onBackProgressed] on the previously selected callback. */ fun progressPredictiveBack(backEvent: BackEvent) /** * Calls [BackCallback.onBackCancelled] on the previously selected callback. */ fun cancelPredictiveBack() } /** * Creates and returns a default implementation of [BackDispatcher]. */ @JsName("backDispatcher") fun BackDispatcher(): BackDispatcher = DefaultBackDispatcher() ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackEvent.kt ================================================ package com.arkivanov.essenty.backhandler /** * Represents an event of the predictive back gesture. * * @param progress progress factor of the back gesture, must be between 0 and 1. * @param swipeEdge Indicates which edge the gesture is being performed from. * @param touchX absolute X location of the touch point of this event. * @param touchY absolute Y location of the touch point of this event. */ data class BackEvent( val progress: Float = 0F, val swipeEdge: SwipeEdge = SwipeEdge.UNKNOWN, val touchX: Float = 0F, val touchY: Float = 0F, ) { init { require(progress in 0F..1F) { "The 'progress' argument must be between 0 and 1 (both inclusive)" } } enum class SwipeEdge { UNKNOWN, LEFT, RIGHT, } } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackHandler.kt ================================================ package com.arkivanov.essenty.backhandler /** * A handler for back button presses. */ interface BackHandler { /** * Checks whether the provided [BackCallback] is registered or not. */ fun isRegistered(callback: BackCallback): Boolean /** * Registers the specified [callback] to be called when the back button is invoked. */ fun register(callback: BackCallback) /** * Unregisters the specified [callback]. */ fun unregister(callback: BackCallback) } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackHandlerOwner.kt ================================================ package com.arkivanov.essenty.backhandler /** * Represents a holder of [BackHandler]. It may be implemented by an arbitrary class, to provide convenient API. */ interface BackHandlerOwner { val backHandler: BackHandler } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/DefaultBackDispatcher.kt ================================================ package com.arkivanov.essenty.backhandler internal class DefaultBackDispatcher : BackDispatcher { private var set = emptySet() private var progressData: ProgressData? = null override val isEnabled: Boolean get() = set.any(BackCallback::isEnabled) private var enabledChangedListeners = emptySet<(Boolean) -> Unit>() private var hasEnabledCallback: Boolean = false private val onCallbackEnabledChanged: (Boolean) -> Unit = { onCallbackEnabledChanged() } private fun onCallbackEnabledChanged() { val hasEnabledCallback = isEnabled if (this.hasEnabledCallback != hasEnabledCallback) { this.hasEnabledCallback = hasEnabledCallback enabledChangedListeners.forEach { it.invoke(hasEnabledCallback) } } } override fun isRegistered(callback: BackCallback): Boolean = callback in set override fun register(callback: BackCallback) { check(callback !in set) { "Callback is already registered" } this.set += callback callback.addEnabledChangedListener(onCallbackEnabledChanged) onCallbackEnabledChanged() } override fun unregister(callback: BackCallback) { check(callback in set) { "Callback is not registered" } this.set -= callback callback.removeEnabledChangedListener(onCallbackEnabledChanged) if (callback == progressData?.callback) { progressData?.callback = null callback.onBackCancelled() } onCallbackEnabledChanged() } override fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { enabledChangedListeners += listener } override fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { enabledChangedListeners -= listener } override fun back(): Boolean { val callback = progressData?.callback ?: set.findMostImportant() progressData = null callback?.onBack() return callback != null } override fun startPredictiveBack(backEvent: BackEvent): Boolean { val callback = set.findMostImportant() ?: return false progressData = ProgressData(startEvent = backEvent, callback = callback) callback.onBackStarted(backEvent) return true } override fun progressPredictiveBack(backEvent: BackEvent) { val progressData = progressData ?: return if (progressData.callback == null) { progressData.callback = set.findMostImportant() progressData.callback?.onBackStarted(progressData.startEvent) } progressData.callback?.onBackProgressed(backEvent) } override fun cancelPredictiveBack() { progressData?.callback?.onBackCancelled() progressData = null } private class ProgressData( val startEvent: BackEvent, var callback: BackCallback?, ) } ================================================ FILE: back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/Utils.kt ================================================ package com.arkivanov.essenty.backhandler internal fun Iterable.findMostImportant(): BackCallback? = sortedBy(BackCallback::priority).lastOrNull(BackCallback::isEnabled) ================================================ FILE: back-handler/src/commonTest/kotlin/com/arkivanov/essenty/backhandler/DefaultBackDispatcherTest.kt ================================================ package com.arkivanov.essenty.backhandler import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class DefaultBackDispatcherTest { private val dispatcher = DefaultBackDispatcher() @Test fun WHEN_created_THEN_disabled() { assertFalse(dispatcher.isEnabled) } @Test fun WHEN_enabled_callback_registered_THEN_enabled() { dispatcher.register(callback(isEnabled = true)) assertTrue(dispatcher.isEnabled) } @Test fun WHEN_two_enabled_callbacks_registered_THEN_enabled() { dispatcher.register(callback(isEnabled = true)) dispatcher.register(callback(isEnabled = true)) assertTrue(dispatcher.isEnabled) } @Test fun WHEN_two_callbacks_registered_one_disabled_THEN_enabled() { dispatcher.register(callback(isEnabled = false)) dispatcher.register(callback(isEnabled = true)) assertTrue(dispatcher.isEnabled) } @Test fun GIVEN_two_enabled_callbacks_registered_WHEN_one_callback_disabled_THEN_enabled() { val callback1 = callback(isEnabled = true) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = true)) callback1.isEnabled = false assertTrue(dispatcher.isEnabled) } @Test fun GIVEN_two_enabled_callbacks_registered_WHEN_all_callbacks_disabled_THEN_disabled() { val callback1 = callback(isEnabled = true) val callback2 = callback(isEnabled = true) dispatcher.register(callback1) dispatcher.register(callback2) callback1.isEnabled = false callback2.isEnabled = false assertFalse(dispatcher.isEnabled) } @Test fun WHEN_two_disabled_callbacks_registered_THEN_disabled() { dispatcher.register(callback(isEnabled = false)) dispatcher.register(callback(isEnabled = false)) assertFalse(dispatcher.isEnabled) } @Test fun GIVEN_enabled_callback_registered_WHEN_callback_unregistered_THEN_disabled() { val callback = callback(isEnabled = true) dispatcher.register(callback) dispatcher.unregister(callback) assertFalse(dispatcher.isEnabled) } @Test fun GIVEN_two_enabled_callbacks_registered_WHEN_one_callback_unregistered_THEN_enabled() { val callback1 = callback(isEnabled = true) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = true)) dispatcher.unregister(callback1) assertTrue(dispatcher.isEnabled) } @Test fun GIVEN_two_enabled_callbacks_registered_WHEN_all_callback_unregistered_THEN_disabled() { val callback1 = callback(isEnabled = true) val callback2 = callback(isEnabled = true) dispatcher.register(callback1) dispatcher.register(callback2) dispatcher.unregister(callback1) dispatcher.unregister(callback2) assertFalse(dispatcher.isEnabled) } @Test fun GIVEN_enabled_callbacks_registered_WHEN_back_THEN_last_callback_called() { val list = ArrayList() dispatcher.register(callback(isEnabled = true) { list += 1 }) dispatcher.register(callback(isEnabled = true) { list += 2 }) dispatcher.back() assertContentEquals(listOf(2), list) } @Test fun GIVEN_enabled_callbacks_registered_with_priorities_WHEN_back_THEN_last_callback_with_higher_priority_called() { val list = ArrayList() dispatcher.register(callback(isEnabled = true, priority = 0) { list += 1 }) dispatcher.register(callback(isEnabled = true, priority = 1) { list += 2 }) dispatcher.register(callback(isEnabled = true, priority = 2) { list += 3 }) dispatcher.register(callback(isEnabled = true, priority = 1) { list += 4 }) dispatcher.register(callback(isEnabled = true, priority = 2) { list += 5 }) dispatcher.register(callback(isEnabled = true, priority = 0) { list += 6 }) dispatcher.register(callback(isEnabled = true, priority = 1) { list += 7 }) dispatcher.register(callback(isEnabled = true, priority = 0) { list += 8 }) dispatcher.back() assertContentEquals(listOf(5), list) } @Test fun GIVEN_enabled_callbacks_registered_with_priorities_WHEN_priority_changed_and_back_THEN_last_callback_with_higher_priority_called() { val list = ArrayList() dispatcher.register(callback(isEnabled = true, priority = 0) { list += 1 }) val callback = callback(isEnabled = true, priority = 1) { list += 2 } dispatcher.register(callback) dispatcher.register(callback(isEnabled = true, priority = 2) { list += 3 }) callback.priority = 3 dispatcher.back() assertContentEquals(listOf(2), list) } @Test fun GIVEN_callbacks_not_registered_WHEN_back_THEN_returned_false() { val result = dispatcher.back() assertFalse(result) } @Test fun GIVEN_enabled_callback_registered_WHEN_back_THEN_returned_true() { dispatcher.register(callback(isEnabled = true)) val result = dispatcher.back() assertTrue(result) } @Test fun GIVEN_enabled_callbacks_registered_and_then_some_disabled_WHEN_back_THEN_last_enabled_callback_called() { val list = ArrayList() val callback2 = callback(isEnabled = true) { list += 2 } val callback4 = callback(isEnabled = true) { list += 4 } dispatcher.register(callback(isEnabled = true) { list += 1 }) dispatcher.register(callback2) dispatcher.register(callback(isEnabled = true) { list += 3 }) dispatcher.register(callback4) callback2.isEnabled = false callback4.isEnabled = false dispatcher.back() assertContentEquals(listOf(3), list) } @Test fun GIVEN_some_disabled_callbacks_registered_WHEN_back_THEN_returned_true() { val callback2 = callback(isEnabled = true) val callback4 = callback(isEnabled = true) dispatcher.register(callback(isEnabled = true)) dispatcher.register(callback2) dispatcher.register(callback(isEnabled = true)) dispatcher.register(callback4) callback2.isEnabled = false callback4.isEnabled = false val result = dispatcher.back() assertTrue(result) } @Test fun GIVEN_some_disabled_callbacks_registered_WHEN_back_THEN_last_enabled_callback_called() { val list = ArrayList() dispatcher.register(callback(isEnabled = true) { list += 1 }) dispatcher.register(callback(isEnabled = false) { list += 2 }) dispatcher.register(callback(isEnabled = true) { list += 3 }) dispatcher.register(callback(isEnabled = false) { list += 4 }) dispatcher.back() assertContentEquals(listOf(3), list) } @Test fun GIVEN_callbacks_registered_and_some_disabled_WHEN_back_THEN_returned_true() { dispatcher.register(callback(isEnabled = true)) dispatcher.register(callback(isEnabled = false)) dispatcher.register(callback(isEnabled = true)) dispatcher.register(callback(isEnabled = false)) val result = dispatcher.back() assertTrue(result) } @Test fun GIVEN_enabled_callbacks_registered_and_then_all_disabled_WHEN_back_THEN_returned_false() { val callback1 = callback(isEnabled = true) val callback2 = callback(isEnabled = true) dispatcher.register(callback1) dispatcher.register(callback2) callback1.isEnabled = false callback2.isEnabled = false val result = dispatcher.back() assertFalse(result) } @Test fun GIVEN_disabled_callbacks_registered_WHEN_back_THEN_returned_false() { dispatcher.register(callback = callback(isEnabled = false)) dispatcher.register(callback = callback(isEnabled = false)) val result = dispatcher.back() assertFalse(result) } @Test fun GIVEN_callback_not_registered_WHEN_startPredictiveBack_THEN_returns_false() { val result = dispatcher.startPredictiveBack(BackEvent()) assertFalse(result) } @Test fun GIVEN_disabled_callback_registered_WHEN_startPredictiveBack_THEN_returns_false() { dispatcher.register(callback = callback(isEnabled = false)) val result = dispatcher.startPredictiveBack(BackEvent()) assertFalse(result) } @Test fun GIVEN_enabled_callback_registered_WHEN_startPredictiveBack_THEN_returns_true() { dispatcher.register(callback = callback(isEnabled = true)) val result = dispatcher.startPredictiveBack(BackEvent()) assertTrue(result) } @Test fun WHEN_progress_with_back_THEN_callbacks_called() { val startEvent = BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F) val progressEvent1 = BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F) val progressEvent2 = BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F) val callback = LoggingCallback() dispatcher.register(callback) dispatcher.startPredictiveBack(startEvent) dispatcher.progressPredictiveBack(progressEvent1) dispatcher.progressPredictiveBack(progressEvent2) dispatcher.back() callback.assertEvents( "start" to startEvent, "progress" to progressEvent1, "progress" to progressEvent2, "back" to null, ) } @Test fun WHEN_progress_with_cancel_THEN_callbacks_called() { val startEvent = BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F) val progressEvent1 = BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F) val progressEvent2 = BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F) val callback = LoggingCallback() dispatcher.register(callback) dispatcher.startPredictiveBack(startEvent) dispatcher.progressPredictiveBack(progressEvent1) dispatcher.progressPredictiveBack(progressEvent2) dispatcher.cancelPredictiveBack() callback.assertEvents( "start" to startEvent, "progress" to progressEvent1, "progress" to progressEvent2, "cancel" to null, ) } @Test fun GIVEN_callback_registered_and_gesture_started_WHEN_unregister_THEN_callback_cancelled() { val callback = LoggingCallback() dispatcher.register(callback) dispatcher.startPredictiveBack(BackEvent()) callback.clear() dispatcher.unregister(callback) callback.assertEvents("cancel" to null) } @Test fun GIVEN_two_callbacks_registered_and_gesture_started_and_progress_callback_unregistered_WHEN_progress_THEN_another_callback_started_and_progressed() { val callback1 = LoggingCallback() val callback2 = LoggingCallback() dispatcher.register(callback1) dispatcher.register(callback2) dispatcher.startPredictiveBack(BackEvent(progress = 0F)) dispatcher.progressPredictiveBack(BackEvent(progress = 0.1F)) dispatcher.unregister(callback2) dispatcher.progressPredictiveBack(BackEvent(progress = 0.2F)) callback1.assertEvents("start" to BackEvent(progress = 0F), "progress" to BackEvent(progress = 0.2F)) } @Test fun GIVEN_two_callbacks_registered_and_gesture_started_and_progress_callback_unregistered_WHEN_back_THEN_another_callback_back() { val callback1 = LoggingCallback() val callback2 = LoggingCallback() dispatcher.register(callback1) dispatcher.register(callback2) dispatcher.startPredictiveBack(BackEvent(progress = 0F)) dispatcher.progressPredictiveBack(BackEvent(progress = 0.1F)) dispatcher.unregister(callback2) dispatcher.back() callback1.assertEvents("back" to null) } @Test fun GIVEN_two_callbacks_registered_and_gesture_started_and_progress_callback_unregistered_WHEN_cancel_THEN_another_callback_not_called() { val callback1 = LoggingCallback() val callback2 = LoggingCallback() dispatcher.register(callback1) dispatcher.register(callback2) dispatcher.startPredictiveBack(BackEvent(progress = 0F)) dispatcher.progressPredictiveBack(BackEvent(progress = 0.1F)) dispatcher.unregister(callback2) dispatcher.cancelPredictiveBack() callback1.assertEvents() } @Test fun WHEN_another_callback_registered_while_in_progress_THEN_old_callback_called() { val startEvent = BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F) val progressEvent1 = BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F) val progressEvent2 = BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F) val callback = LoggingCallback() dispatcher.register(callback) dispatcher.startPredictiveBack(startEvent) dispatcher.progressPredictiveBack(progressEvent1) dispatcher.register(LoggingCallback()) dispatcher.progressPredictiveBack(progressEvent2) dispatcher.cancelPredictiveBack() callback.assertEvents( "start" to startEvent, "progress" to progressEvent1, "progress" to progressEvent2, "cancel" to null, ) } @Test fun WHEN_another_callback_registered_while_in_progress_THEN_new_callback_not_called() { val startEvent = BackEvent(progress = 0.1F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 1F, touchY = 2F) val progressEvent1 = BackEvent(progress = 0.2F, swipeEdge = BackEvent.SwipeEdge.RIGHT, touchX = 2F, touchY = 3F) val progressEvent2 = BackEvent(progress = 0.3F, swipeEdge = BackEvent.SwipeEdge.LEFT, touchX = 3F, touchY = 4F) val callback = LoggingCallback() dispatcher.register(LoggingCallback()) dispatcher.startPredictiveBack(startEvent) dispatcher.progressPredictiveBack(progressEvent1) dispatcher.register(callback) dispatcher.progressPredictiveBack(progressEvent2) dispatcher.cancelPredictiveBack() callback.assertEvents() } @Test fun GIVEN_EnabledChanged_listener_added_WHEN_enabled_callback_registered_THEN_listener_called_with_true() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } dispatcher.register(callback(isEnabled = true, onBack = {})) assertContentEquals(listOf(true), events) } @Test fun GIVEN_EnabledChanged_listener_added_WHEN_disabled_callback_registered_THEN_listener_not_called() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } dispatcher.register(callback(isEnabled = false, onBack = {})) assertContentEquals(emptyList(), events) } @Test fun GIVEN_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_unregistered_THEN_listener_called_with_false() { val events = ArrayList() val callback = callback(isEnabled = true, onBack = {}) dispatcher.register(callback) dispatcher.addEnabledChangedListener { events += it } dispatcher.unregister(callback) assertContentEquals(listOf(false), events) } @Test fun GIVEN_disabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_unregistered_THEN_listener_not_called() { val events = ArrayList() val callback = callback(isEnabled = false, onBack = {}) dispatcher.register(callback) dispatcher.addEnabledChangedListener { events += it } dispatcher.unregister(callback) assertContentEquals(emptyList(), events) } @Test fun GIVEN_disabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_enabled_THEN_listener_called_with_true() { val events = ArrayList() val callback = callback(isEnabled = false, onBack = {}) dispatcher.register(callback) dispatcher.addEnabledChangedListener { events += it } callback.isEnabled = true assertContentEquals(listOf(true), events) } @Test fun GIVEN_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_disabled_THEN_listener_called_with_false() { val events = ArrayList() val callback = callback(isEnabled = true, onBack = {}) dispatcher.register(callback) dispatcher.addEnabledChangedListener { events += it } callback.isEnabled = false assertContentEquals(listOf(false), events) } @Test fun GIVEN_two_disabled_callback_registered_and_EnabledChanged_listener_added_WHEN_one_callback_enabled_THEN_listener_called_with_true() { val events = ArrayList() val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = false, onBack = {})) dispatcher.addEnabledChangedListener { events += it } callback1.isEnabled = true assertContentEquals(listOf(true), events) } @Test fun GIVEN_two_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_one_callback_disabled_THEN_listener_not_called() { val events = ArrayList() val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = true, onBack = {})) dispatcher.addEnabledChangedListener { events += it } callback1.isEnabled = false assertContentEquals(emptyList(), events) } @Test fun GIVEN_one_disabled_and_one_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_enabled_THEN_listener_not_called() { val events = ArrayList() val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = true, onBack = {})) dispatcher.addEnabledChangedListener { events += it } callback1.isEnabled = true assertContentEquals(emptyList(), events) } @Test fun GIVEN_one_disabled_and_one_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_callback_disabled_THEN_listener_called_with_false() { val events = ArrayList() val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = false, onBack = {})) dispatcher.addEnabledChangedListener { events += it } callback1.isEnabled = false assertContentEquals(listOf(false), events) } @Test fun GIVEN_one_disabled_and_one_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_enabled_callback_unregistered_THEN_listener_called_with_false() { val events = ArrayList() val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = false, onBack = {})) dispatcher.addEnabledChangedListener { events += it } dispatcher.unregister(callback1) assertContentEquals(listOf(false), events) } @Test fun GIVEN_one_disabled_and_one_enabled_callback_registered_and_EnabledChanged_listener_added_WHEN_disabled_callback_unregistered_THEN_listener_not_called() { val events = ArrayList() val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = true, onBack = {})) dispatcher.addEnabledChangedListener { events += it } dispatcher.unregister(callback1) assertContentEquals(emptyList(), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_disabled_callback_registered_WHEN_callback_enabled_THEN_listener_called_with_true() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback = callback(isEnabled = false, onBack = {}) dispatcher.register(callback) events.clear() callback.isEnabled = true assertContentEquals(listOf(true), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_enabled_callback_registered_WHEN_callback_disabled_THEN_listener_called_with_false() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback = callback(isEnabled = true, onBack = {}) dispatcher.register(callback) events.clear() callback.isEnabled = false assertContentEquals(listOf(false), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_two_disabled_callback_registered_WHEN_one_callback_enabled_THEN_listener_called_with_true() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = false, onBack = {})) events.clear() callback1.isEnabled = true assertContentEquals(listOf(true), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_two_enabled_callback_registered_WHEN_one_callback_disabled_THEN_listener_not_called() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(callback(isEnabled = true, onBack = {})) events.clear() callback1.isEnabled = false assertContentEquals(emptyList(), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_one_disabled_and_one_enabled_callback_registered_WHEN_callback_enabled_THEN_listener_not_called() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = true, onBack = {})) events.clear() callback1.isEnabled = true assertContentEquals(emptyList(), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_one_disabled_and_one_enabled_callback_registered_WHEN_callback_disabled_THEN_listener_called_with_false() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = false, onBack = {})) events.clear() callback1.isEnabled = false assertContentEquals(listOf(false), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_one_disabled_and_one_enabled_callback_registered_WHEN_enabled_callback_unregistered_THEN_listener_called_with_false() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = true, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = false, onBack = {})) events.clear() dispatcher.unregister(callback1) assertContentEquals(listOf(false), events) } @Test fun GIVEN_EnabledChanged_listener_added_and_one_disabled_and_one_enabled_callback_registered_WHEN_disabled_callback_unregistered_THEN_listener_not_called() { val events = ArrayList() dispatcher.addEnabledChangedListener { events += it } val callback1 = callback(isEnabled = false, onBack = {}) dispatcher.register(callback1) dispatcher.register(BackCallback(isEnabled = true, onBack = {})) events.clear() dispatcher.unregister(callback1) assertContentEquals(emptyList(), events) } @Test fun GIVEN_callback_not_registered_WHEN_isRegistered_THEN_returns_false() { val isRegistered = dispatcher.isRegistered(callback()) assertFalse(isRegistered) } @Test fun GIVEN_callback_registered_WHEN_isRegistered_for_another_callback_THEN_returns_false() { dispatcher.register(callback()) val isRegistered = dispatcher.isRegistered(callback()) assertFalse(isRegistered) } @Test fun GIVEN_callback_registered_WHEN_isRegistered_for_same_callback_THEN_returns_true() { val callback = callback() dispatcher.register(callback) val isRegistered = dispatcher.isRegistered(callback) assertTrue(isRegistered) } private fun callback( isEnabled: Boolean = true, priority: Int = 0, onBack: () -> Unit = {}, ): BackCallback = BackCallback(isEnabled = isEnabled, priority = priority, onBack = onBack) private class LoggingCallback : BackCallback() { private val events: MutableList> = ArrayList() override fun onBackStarted(backEvent: BackEvent) { events += "start" to backEvent } override fun onBackProgressed(backEvent: BackEvent) { events += "progress" to backEvent } override fun onBack() { events += "back" to null } override fun onBackCancelled() { events += "cancel" to null } fun clear() { events.clear() } fun assertEvents(vararg events: Pair) { assertContentEquals(events.toList(), this.events) } } } ================================================ FILE: build.gradle.kts ================================================ import com.arkivanov.gradle.AndroidConfig import com.arkivanov.gradle.BinaryCompatibilityValidatorConfig import com.arkivanov.gradle.PublicationConfig import com.arkivanov.gradle.ensureUnreachableTasksDisabled import com.arkivanov.gradle.iosCompat import com.arkivanov.gradle.macosCompat import com.arkivanov.gradle.setupDefaults import com.arkivanov.gradle.setupDetekt import com.arkivanov.gradle.tvosCompat import com.arkivanov.gradle.watchosCompat import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { gradlePluginPortal() mavenCentral() google() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } dependencies { classpath(deps.kotlin.kotlinGradlePlug) classpath(deps.android.gradle) classpath(deps.kotlinx.binaryCompatibilityValidator) classpath(deps.detekt.gradleDetektPlug) classpath(deps.jetbrains.kotlin.serializationGradlePlug) } } plugins { id("com.arkivanov.gradle.setup") } setupDefaults( multiplatformConfigurator = { androidTarget() jvm() js { browser() nodejs() } wasmJs { browser() } linuxX64() iosCompat() watchosCompat() tvosCompat() macosCompat() }, androidConfig = AndroidConfig( minSdkVersion = 15, compileSdkVersion = 34, targetSdkVersion = 34, ), publicationConfig = PublicationConfig( group = "com.arkivanov.essenty", version = deps.versions.essenty.get(), projectName = "Essenty", projectDescription = "Essential libraries for Kotlin Multiplatform", projectUrl = "https://github.com/arkivanov/Essenty", scmUrl = "scm:git:git://github.com/arkivanov/Essenty.git", licenseName = "The Apache License, Version 2.0", licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt", developerId = "arkivanov", developerName = "Arkadii Ivanov", developerEmail = "arkann1985@gmail.com", signingKey = System.getenv("SIGNING_KEY"), signingPassword = System.getenv("SIGNING_PASSWORD"), repositoryUrl = "https://s01.oss.sonatype.org/service/local/staging/deployByRepositoryId/${System.getenv("SONATYPE_REPOSITORY_ID")}", repositoryUserName = System.getenv("SONATYPE_USER_NAME"), repositoryPassword = System.getenv("SONATYPE_PASSWORD"), ), binaryCompatibilityValidatorConfig = BinaryCompatibilityValidatorConfig(klib = true), ) setupDetekt() ensureUnreachableTasksDisabled() allprojects { repositories { mavenCentral() google() } afterEvaluate { extensions.findByType()?.apply { sourceSets { all { languageSettings.optIn("com.arkivanov.essenty.utils.internal.InternalEssentyApi") } } } } } ================================================ FILE: deps.versions.toml ================================================ [versions] essenty = "2.5.0" kotlin = "2.1.0" kotlinxBinaryCompatibilityValidator = "0.16.3" kotlinxCoroutines = "1.9.0" detektGradlePlugin = "1.23.6" junit = "4.13.2" androidGradle = "8.0.2" androidxLifecycle = "2.6.2" androidxSavedstate = "1.2.1" androidxActivity = "1.8.1" jetbrainsKotlinxSerialization = "1.6.3" robolectric = "4.9.1" reaktive = "2.1.0" [libraries] kotlin-kotlinGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlinx-binaryCompatibilityValidator = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version.ref = "kotlinxBinaryCompatibilityValidator" } kotlinx-coroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } detekt-gradleDetektPlug = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detektGradlePlugin" } android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradle" } androidx-lifecycle-lifecycleCommonJava8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidxLifecycle" } androidx-lifecycle-lifecycleRuntime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "androidxLifecycle" } androidx-lifecycle-lifecycleViewmodelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } androidx-savedstate-savedstateKtx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref = "androidxSavedstate" } androidx-activity-activityKtx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivity" } jetbrains-kotlin-serializationGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } jetbrains-kotlinx-kotlinxSerializationCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "jetbrainsKotlinxSerialization" } jetbrains-kotlinx-kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jetbrainsKotlinxSerialization" } robolectric-robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } reaktive-reaktive = { group = "com.badoo.reaktive", name = "reaktive", version.ref = "reaktive" } ================================================ FILE: detekt.yml ================================================ complexity: CyclomaticComplexMethod: threshold: 15 ignoreSingleWhenExpression: true ignoreSimpleWhenEntries: true ignoreNestingFunctions: false CognitiveComplexMethod: active: true threshold: 15 LongParameterList: active: false TooManyFunctions: active: false exceptions: PrintStackTrace: active: false TooGenericExceptionCaught: active: false naming: FunctionNaming: excludes: ['**/test/**', '**/*Test/**'] ignoreAnnotated: [ 'Composable' ] MemberNameEqualsClassName: active: false style: ForbiddenComment: values: ['FIXME:', 'STOPSHIP:'] MagicNumber: active: false MaxLineLength: maxLineLength: 140 excludes: ['**/test/**', '**/*Test/**'] ReturnCount: active: false ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ kotlin.code.style=official org.gradle.jvmargs=-Xmx2g org.gradle.parallel=true org.gradle.caching=true systemProp.org.gradle.internal.publish.checksums.insecure=true android.useAndroidX=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.applyDefaultHierarchyTemplate=false # For compatibility with Kotlin 1.9.0 android.experimental.lint.version=8.1.0 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: instance-keeper/.gitignore ================================================ /build ================================================ FILE: instance-keeper/api/android/instance-keeper.api ================================================ public final class com/arkivanov/essenty/instancekeeper/AndroidExtKt { public static final fun InstanceKeeper (Landroidx/lifecycle/ViewModelStore;Z)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; public static synthetic fun InstanceKeeper$default (Landroidx/lifecycle/ViewModelStore;ZILjava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; public static final fun instanceKeeper (Landroidx/lifecycle/ViewModelStoreOwner;Z)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; public static synthetic fun instanceKeeper$default (Landroidx/lifecycle/ViewModelStoreOwner;ZILjava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; } public abstract interface annotation class com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi : java/lang/annotation/Annotation { } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper { public abstract fun get (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public abstract fun put (Ljava/lang/Object;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V public abstract fun remove (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { public abstract fun onDestroy ()V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance$DefaultImpls { public static fun onDestroy (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$SimpleInstance : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { public fun (Ljava/lang/Object;)V public final fun getInstance ()Ljava/lang/Object; public fun onDestroy ()V } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher : com/arkivanov/essenty/instancekeeper/InstanceKeeper { public abstract fun destroy ()V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcherKt { public static final fun InstanceKeeperDispatcher ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher; } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperExtKt { public static final fun getOrCreate (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public static final fun getOrCreateCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; public static final fun getOrCreateSimple (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun retainedCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; public static final fun retainedInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public static final fun retainedSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner { public abstract fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; } ================================================ FILE: instance-keeper/api/instance-keeper.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: open annotation class com.arkivanov.essenty.instancekeeper/ExperimentalInstanceKeeperApi : kotlin/Annotation { // com.arkivanov.essenty.instancekeeper/ExperimentalInstanceKeeperApi|null[0] constructor () // com.arkivanov.essenty.instancekeeper/ExperimentalInstanceKeeperApi.|(){}[0] } abstract interface com.arkivanov.essenty.instancekeeper/InstanceKeeper { // com.arkivanov.essenty.instancekeeper/InstanceKeeper|null[0] abstract fun get(kotlin/Any): com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance? // com.arkivanov.essenty.instancekeeper/InstanceKeeper.get|get(kotlin.Any){}[0] abstract fun put(kotlin/Any, com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance) // com.arkivanov.essenty.instancekeeper/InstanceKeeper.put|put(kotlin.Any;com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance){}[0] abstract fun remove(kotlin/Any): com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance? // com.arkivanov.essenty.instancekeeper/InstanceKeeper.remove|remove(kotlin.Any){}[0] abstract interface Instance { // com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance|null[0] open fun onDestroy() // com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance.onDestroy|onDestroy(){}[0] } final class <#A1: out kotlin/Any?> SimpleInstance : com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance { // com.arkivanov.essenty.instancekeeper/InstanceKeeper.SimpleInstance|null[0] constructor (#A1) // com.arkivanov.essenty.instancekeeper/InstanceKeeper.SimpleInstance.|(1:0){}[0] final val instance // com.arkivanov.essenty.instancekeeper/InstanceKeeper.SimpleInstance.instance|{}instance[0] final fun (): #A1 // com.arkivanov.essenty.instancekeeper/InstanceKeeper.SimpleInstance.instance.|(){}[0] } } abstract interface com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher : com.arkivanov.essenty.instancekeeper/InstanceKeeper { // com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher|null[0] abstract fun destroy() // com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher.destroy|destroy(){}[0] } abstract interface com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner { // com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner|null[0] abstract val instanceKeeper // com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner.instanceKeeper|{}instanceKeeper[0] abstract fun (): com.arkivanov.essenty.instancekeeper/InstanceKeeper // com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner.instanceKeeper.|(){}[0] } final fun com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher(): com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher // com.arkivanov.essenty.instancekeeper/InstanceKeeperDispatcher|InstanceKeeperDispatcher(){}[0] final inline fun <#A: com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreate(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreate|getOrCreate@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/retainingInstance(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingInstance|retainingInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedInstance(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedInstance|retainedInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainingInstance(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingInstance|retainingInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreateSimple(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreateSimple|getOrCreateSimple@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/retainingSimpleInstance(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingSimpleInstance|retainingSimpleInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedSimpleInstance(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedSimpleInstance|retainedSimpleInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainingSimpleInstance(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingSimpleInstance|retainingSimpleInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreateCloseable(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreateCloseable|getOrCreateCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/retainingCloseable(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingCloseable|retainingCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedCloseable(kotlin/Any, kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedCloseable|retainedCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainingCloseable(kotlin/Any? = ..., crossinline kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.instancekeeper/retainingCloseable|retainingCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Any?;kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreate(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreate|getOrCreate@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified com.arkivanov.essenty.instancekeeper/InstanceKeeper.Instance> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedInstance(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedInstance|retainedInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreateSimple(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreateSimple|getOrCreateSimple@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified kotlin/Any?> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedSimpleInstance(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedSimpleInstance|retainedSimpleInstance@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeper).com.arkivanov.essenty.instancekeeper/getOrCreateCloseable(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/getOrCreateCloseable|getOrCreateCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeper(kotlin.Function0<0:0>){0§}[0] final inline fun <#A: reified kotlin/AutoCloseable> (com.arkivanov.essenty.instancekeeper/InstanceKeeperOwner).com.arkivanov.essenty.instancekeeper/retainedCloseable(kotlin/Function0<#A>): #A // com.arkivanov.essenty.instancekeeper/retainedCloseable|retainedCloseable@com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner(kotlin.Function0<0:0>){0§}[0] ================================================ FILE: instance-keeper/api/jvm/instance-keeper.api ================================================ public abstract interface annotation class com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi : java/lang/annotation/Annotation { } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper { public abstract fun get (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public abstract fun put (Ljava/lang/Object;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V public abstract fun remove (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { public abstract fun onDestroy ()V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance$DefaultImpls { public static fun onDestroy (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$SimpleInstance : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { public fun (Ljava/lang/Object;)V public final fun getInstance ()Ljava/lang/Object; public fun onDestroy ()V } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher : com/arkivanov/essenty/instancekeeper/InstanceKeeper { public abstract fun destroy ()V } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcherKt { public static final fun InstanceKeeperDispatcher ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher; } public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperExtKt { public static final fun getOrCreate (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public static final fun getOrCreateCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; public static final fun getOrCreateSimple (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun retainedCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; public static final fun retainedInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; public static final fun retainedSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; } public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner { public abstract fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; } ================================================ FILE: instance-keeper/build.gradle.kts ================================================ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.instancekeeper" } kotlin { setupSourceSets { val android by bundle() common.main.dependencies { implementation(project(":utils-internal")) } android.main.dependencies { implementation(deps.androidx.lifecycle.lifecycleViewmodelKtx) } } } ================================================ FILE: instance-keeper/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: instance-keeper/src/androidMain/kotlin/com/arkivanov/essenty/instancekeeper/AndroidExt.kt ================================================ package com.arkivanov.essenty.instancekeeper import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.get /** * Creates a new instance of [InstanceKeeper] and attaches it to the provided AndroidX [ViewModelStore]. * * @param discardRetainedInstances a flag indicating whether any previously retained instances should be * discarded and destroyed or not, default value is `false`. */ fun InstanceKeeper( viewModelStore: ViewModelStore, discardRetainedInstances: Boolean = false, ): InstanceKeeper = ViewModelProvider( viewModelStore, object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T = InstanceKeeperViewModel() as T } ) .get() .apply { if (discardRetainedInstances) { recreate() } } .instanceKeeperDispatcher /** * Creates a new instance of [InstanceKeeper] and attaches it to the AndroidX [ViewModelStore]. * * @param discardRetainedInstances a flag indicating whether any previously retained instances should be * discarded and destroyed or not, default value is `false`. */ fun ViewModelStoreOwner.instanceKeeper(discardRetainedInstances: Boolean = false): InstanceKeeper = InstanceKeeper(viewModelStore = viewModelStore, discardRetainedInstances = discardRetainedInstances) internal class InstanceKeeperViewModel : ViewModel() { var instanceKeeperDispatcher: InstanceKeeperDispatcher = InstanceKeeperDispatcher() private set override fun onCleared() { instanceKeeperDispatcher.destroy() } fun recreate() { instanceKeeperDispatcher.destroy() instanceKeeperDispatcher = InstanceKeeperDispatcher() } } ================================================ FILE: instance-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/instancekeeper/AndroidInstanceKeeperTest.kt ================================================ package com.arkivanov.essenty.instancekeeper import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import kotlin.test.Test import kotlin.test.assertNotSame import kotlin.test.assertSame import kotlin.test.assertTrue @Suppress("TestFunctionName") class AndroidInstanceKeeperTest { @Test fun retains_instances() { val owner = TestOwner() var instanceKeeper = owner.instanceKeeper() val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) instanceKeeper = owner.instanceKeeper() val instance2 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) assertSame(instance1, instance2) } @Test fun GIVEN_discardRetainedInstances_is_true_on_restore_THEN_instances_not_retained() { val owner = TestOwner() var instanceKeeper = owner.instanceKeeper() val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) instanceKeeper = owner.instanceKeeper(discardRetainedInstances = true) val instance2 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) assertNotSame(instance1, instance2) } @Test fun GIVEN_discardRetainedInstances_is_true_on_restore_THEN_old_instances_destroyed() { val owner = TestOwner() val instanceKeeper = owner.instanceKeeper() val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) owner.instanceKeeper(discardRetainedInstances = true) assertTrue(instance1.isDestroyed) } private class TestOwner : ViewModelStoreOwner { override val viewModelStore: ViewModelStore = ViewModelStore() } private class TestInstance : InstanceKeeper.Instance { var isDestroyed: Boolean = false override fun onDestroy() { isDestroyed = true } } } ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/DefaultInstanceKeeperDispatcher.kt ================================================ package com.arkivanov.essenty.instancekeeper import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance internal class DefaultInstanceKeeperDispatcher : InstanceKeeperDispatcher { private val map = HashMap() private var isDestroyed = false override fun get(key: Any): Instance? = map[key] override fun put(key: Any, instance: Instance) { check(key !in map) { "Another instance is already associated with the key: $key" } map[key] = instance if (isDestroyed) { instance.onDestroy() } } override fun remove(key: Any): Instance? = map.remove(key) override fun destroy() { if (!isDestroyed) { isDestroyed = true map.values.toList().forEach(Instance::onDestroy) } } } ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi.kt ================================================ package com.arkivanov.essenty.instancekeeper /** * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. */ @RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class ExperimentalInstanceKeeperApi ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeper.kt ================================================ package com.arkivanov.essenty.instancekeeper /** * A generic keyed store of [Instance] objects. Instances are destroyed at the end of the * [InstanceKeeper]'s scope, which is typically tied to the scope of a back stack entry. * E.g. instances are retained over Android configuration changes, and destroyed when the * corresponding back stack entry is popped. */ interface InstanceKeeper { /** * Returns an instance with the given [key], or `null` if no instance with the given key exists. */ fun get(key: Any): Instance? /** * Stores the given [instance] with the given [key]. Throws [IllegalStateException] if another * instance is already registered with the given [key]. */ fun put(key: Any, instance: Instance) /** * Removes an instance with the given [key]. This does not destroy the instance. */ fun remove(key: Any): Instance? /** * Represents a destroyable instance. */ interface Instance { /** * Called at the end of the [InstanceKeeper]'s scope. */ fun onDestroy() {} } /** * Are simple [Instance] wrapper for cases when destroying is not required. */ class SimpleInstance(val instance: T) : Instance } ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher.kt ================================================ package com.arkivanov.essenty.instancekeeper import kotlin.js.JsName /** * Represents a destroyable [InstanceKeeper]. */ interface InstanceKeeperDispatcher : InstanceKeeper { /** * Destroys all existing instances. Instances are not cleared, so that they can be * accessed later. Any new instances will be immediately destroyed. */ fun destroy() } /** * Creates a default implementation of [InstanceKeeperDispatcher]. */ @JsName("instanceKeeperDispatcher") fun InstanceKeeperDispatcher(): InstanceKeeperDispatcher = DefaultInstanceKeeperDispatcher() ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperExt.kt ================================================ package com.arkivanov.essenty.instancekeeper import com.arkivanov.essenty.instancekeeper.InstanceKeeper.SimpleInstance import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.typeOf /** * Returns a previously stored [InstanceKeeper.Instance] of type [T] with the given key, * or creates and stores a new one if it doesn't exist. */ inline fun InstanceKeeper.getOrCreate(key: Any, factory: () -> T): T { @Suppress("UNCHECKED_CAST") // Assuming the type per key is always the same var instance: T? = get(key) as T? if (instance == null) { instance = factory() put(key, instance) } return instance } /** * Returns a previously stored [InstanceKeeper.Instance] of type [T], * or creates and stores a new one if it doesn't exist. Uses `typeOf()` as key. * * Deprecated. Using `getOrCreate` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. */ @Deprecated( message = "Using getOrCreate without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "getOrCreate(key = , factory = factory)", ) ) inline fun InstanceKeeper.getOrCreate(factory: () -> T): T = getOrCreate(key = typeOf(), factory = factory) /** * Returns a previously stored [AutoCloseable] instance of type [T] with the given [key], * or creates and stores a new one if it doesn't exist. * * @param key a key to store and retrieve the instance. * @param factory a function creating a new instance of type [T]. */ inline fun InstanceKeeper.getOrCreateCloseable(key: Any, factory: () -> T): T = getOrCreate(key = key) { val instance = factory() object : InstanceKeeper.Instance { val instance: T = instance override fun onDestroy() { instance.close() } } }.instance /** * Returns a previously stored [AutoCloseable] instance of type [T], * or creates and stores a new one if it doesn't exist. Uses `typeOf()` as key. * * Deprecated. Using `getOrCreateCloseable` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. * * @param factory a function creating a new instance of type [T]. */ @Deprecated( message = "Using getOrCreateCloseable without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "getOrCreateCloseable(key = , factory = factory)", ) ) inline fun InstanceKeeper.getOrCreateCloseable(factory: () -> T): T = getOrCreateCloseable(key = typeOf(), factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreate]. */ inline fun InstanceKeeperOwner.retainedInstance(key: Any, factory: () -> T): T = instanceKeeper.getOrCreate(key = key, factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreate]. * * Deprecated. Using `retainedInstance` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. */ @Deprecated( message = "Using retainedInstance without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "retainedInstance(key = , factory = factory)", ) ) inline fun InstanceKeeperOwner.retainedInstance(factory: () -> T): T = instanceKeeper.getOrCreate(key = typeOf(), factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreateCloseable]. */ inline fun InstanceKeeperOwner.retainedCloseable(key: Any, factory: () -> T): T = instanceKeeper.getOrCreateCloseable(key = key, factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreateCloseable]. * * Deprecated. Using `retainedCloseable` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. */ @Deprecated( message = "Using retainedCloseable without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "retainedCloseable(key = , factory = factory)", ) ) inline fun InstanceKeeperOwner.retainedCloseable(factory: () -> T): T = instanceKeeper.getOrCreateCloseable(key = typeOf(), factory = factory) /** * Returns a previously stored instance of type [T] with the given key, * or creates and stores a new one if it doesn't exist. * * This overload is for simple cases when instance destroying is not required. */ inline fun InstanceKeeper.getOrCreateSimple(key: Any, factory: () -> T): T = getOrCreate(key = key) { SimpleInstance(factory()) } .instance /** * Returns a previously stored instance of type [T], * or creates and stores a new one if it doesn't exist. Uses `typeOf()` as key. * * This overload is for simple cases when instance destroying is not required. * * Deprecated. Using `getOrCreateSimple` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. */ @Deprecated( message = "Using getOrCreateSimple without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "getOrCreateSimple(key = , factory = factory)", ) ) inline fun InstanceKeeper.getOrCreateSimple(factory: () -> T): T = getOrCreateSimple(key = typeOf(), factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreateSimple]. */ inline fun InstanceKeeperOwner.retainedSimpleInstance(key: Any, factory: () -> T): T = instanceKeeper.getOrCreateSimple(key = key, factory = factory) /** * A convenience function for [InstanceKeeper.getOrCreateSimple]. * * Deprecated. Using `retainedSimpleInstance` without the `key` parameter may crash when [T] * refers to a type with type (generic) parameters (e.g. a `StateFlow>`) and R8 Full Mode enabled. * See [KT-42913](https://youtrack.jetbrains.com/issue/KT-42913) for more information. */ @Deprecated( message = "Using retainedSimpleInstance without the key parameter is unsafe. Please use the other variant.", replaceWith = ReplaceWith( expression = "retainedSimpleInstance(key = , factory = factory)", ) ) inline fun InstanceKeeperOwner.retainedSimpleInstance(factory: () -> T): T = instanceKeeper.getOrCreateSimple(key = typeOf(), factory = factory) /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingInstance { RetainedViewModel() } * * private class RetainedViewModel : InstanceKeeper.Instance { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property, extends [InstanceKeeper.Instance]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreate */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeper.retainingInstance( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val instance = getOrCreate(key = key ?: "RETAINING_INSTANCE_${property.name}", factory = factory) ReadOnlyProperty { _, _ -> instance } } /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingInstance { RetainedViewModel() } * * private class RetainedViewModel : InstanceKeeper.Instance { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property, extends [InstanceKeeper.Instance]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreate */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeperOwner.retainingInstance( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = instanceKeeper.retainingInstance(key = key, factory = factory) /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * This overload is for simple cases when instance destroying is not required. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingSimpleInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingSimpleInstance { RetainedClass() } * * private class RetainedClass { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreateSimple */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeper.retainingSimpleInstance( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val instance = getOrCreateSimple(key = key ?: "RETAINING_SIMPLE_INSTANCE_${property.name}", factory = factory) ReadOnlyProperty { _, _ -> instance } } /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * This overload is for simple cases when instance destroying is not required. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingSimpleInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingSimpleInstance { RetainedClass() } * * private class RetainedClass { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreateSimple */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeperOwner.retainingSimpleInstance( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = instanceKeeper.retainingSimpleInstance(key = key, factory = factory) /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * This overload is for simple cases when instance destroying is not required. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingSimpleInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingSimpleInstance { RetainedViewModel() } * * private class RetainedViewModel : AutoCloseable { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property, extends [AutoCloseable]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreateCloseable */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeper.retainingCloseable( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val instance = getOrCreateCloseable(key = key ?: "RETAINING_CLOSEABLE_${property.name}", factory = factory) ReadOnlyProperty { _, _ -> instance } } /** * Helper function for creating a * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically retained using [InstanceKeeper]. * * This overload is for simple cases when instance destroying is not required. * * Example: * * ``` * import com.arkivanov.essenty.instancekeeper.InstanceKeeper * import com.arkivanov.essenty.instancekeeper.InstanceKeeperOwner * import com.arkivanov.essenty.instancekeeper.retainingSimpleInstance * * class SomeLogic(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { * private val viewModel by retainingSimpleInstance { RetainedViewModel() } * * private class RetainedViewModel : AutoCloseable { * // ... * } * } * ``` * * @param key an optional key for storing and retrieving the instance. If not provided, then the * property name is used as a key. * @param factory a function creating a new instance of type [T]. * @param T a type of the property, extends [AutoCloseable]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. * @see getOrCreateCloseable */ @ExperimentalInstanceKeeperApi inline fun InstanceKeeperOwner.retainingCloseable( key: Any? = null, crossinline factory: () -> T, ): PropertyDelegateProvider> = instanceKeeper.retainingCloseable(key = key, factory = factory) ================================================ FILE: instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner.kt ================================================ package com.arkivanov.essenty.instancekeeper /** * Represents a holder of [InstanceKeeper]. */ interface InstanceKeeperOwner { val instanceKeeper: InstanceKeeper } ================================================ FILE: instance-keeper/src/commonTest/kotlin/com/arkivanov/essenty/instancekeeper/DefaultInstanceKeeperDispatcherTest.kt ================================================ package com.arkivanov.essenty.instancekeeper import kotlin.test.Test import kotlin.test.assertFails import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertSame import kotlin.test.assertTrue @Suppress("TestFunctionName") class DefaultInstanceKeeperDispatcherTest { @Test fun GIVEN_no_instance_put_WHEN_get_THEN_returns_null() { val dispatcher = DefaultInstanceKeeperDispatcher() dispatcher.put(key = "key1", instance = TestInstance()) val returnedInstance = dispatcher.get(key = "key2") assertNull(returnedInstance) } @Test fun GIVEN_instance_put_WHEN_get_THEN_returns_instance() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance1 = TestInstance() val instance2 = TestInstance() dispatcher.put(key = "key1", instance = instance1) dispatcher.put(key = "key2", instance = instance2) val returnedInstance1 = dispatcher.get(key = "key1") val returnedInstance2 = dispatcher.get(key = "key2") assertSame(instance1, returnedInstance1) assertSame(instance2, returnedInstance2) } @Test fun GIVEN_instance_put_WHEN_put_with_same_key_THEN_throws_exception() { val dispatcher = DefaultInstanceKeeperDispatcher() dispatcher.put(key = "key", instance = TestInstance()) assertFails { dispatcher.put(key = "key", instance = TestInstance()) } } @Test fun GIVEN_instance_with_same_key_put_twice_WHEN_get_THEN_returns_original_instance() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.put(key = "key", instance = instance) try { dispatcher.put(key = "key", instance = TestInstance()) } catch (ignored: Exception) { } val returnedInstance = dispatcher.get(key = "key") assertSame(instance, returnedInstance) } @Test fun GIVEN_instances_put_WHEN_destroy_THEN_instances_destroyed() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance1 = TestInstance() val instance2 = TestInstance() dispatcher.put(key = "key1", instance = instance1) dispatcher.put(key = "key2", instance = instance2) dispatcher.destroy() assertTrue(instance1.isDestroyed) assertTrue(instance2.isDestroyed) } @Test fun GIVEN_instance_put_and_destroyed_WHEN_get_THEN_returns_instance() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.put(key = "key", instance = instance) dispatcher.destroy() val returnedInstance = dispatcher.get(key = "key") assertSame(instance, returnedInstance) } @Test fun GIVEN_destroyed_WHEN_put_THEN_does_not_throw() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.destroy() dispatcher.put(key = "key", instance = instance) } @Test fun GIVEN_destroyed_and_put_WHEN_get_THEN_returns_instance() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.destroy() dispatcher.put(key = "key", instance = instance) val returnedInstance = dispatcher.get(key = "key") assertSame(instance, returnedInstance) } @Test fun GIVEN_not_empty_and_destroyed_WHEN_destroy_THEN_instance_is_not_destroyed_second_time() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.put(key = "key", instance = instance) dispatcher.destroy() instance.isDestroyed = false dispatcher.destroy() assertFalse(instance.isDestroyed) } @Test fun GEVEN_instance_not_put_WHEN_remove_THEN_returns_null() { val dispatcher = DefaultInstanceKeeperDispatcher() val returnedInstance = dispatcher.remove(key = "key") assertNull(returnedInstance) } @Test fun GEVEN_instance_put_WHEN_remove_THEN_returns_instance() { val instance = TestInstance() val dispatcher = DefaultInstanceKeeperDispatcher() dispatcher.put("key", instance) val returnedInstance = dispatcher.remove(key = "key") assertSame(instance, returnedInstance) } @Test fun GEVEN_instance_removed_WHEN_remove_THEN_returns_null() { val instance = TestInstance() val dispatcher = DefaultInstanceKeeperDispatcher() dispatcher.put("key", instance) dispatcher.remove("key") val returnedInstance = dispatcher.remove(key = "key") assertNull(returnedInstance) } @Test fun GIVEN_instance_put_and_destroyed_WHEN_remove_THEN_returns_instance() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.put(key = "key", instance = instance) dispatcher.destroy() val returnedInstance = dispatcher.remove(key = "key") assertSame(instance, returnedInstance) } @Test fun GIVEN_instance_put_and_destroyed_and_removed_WHEN_remove_THEN_returns_null() { val dispatcher = DefaultInstanceKeeperDispatcher() val instance = TestInstance() dispatcher.put(key = "key", instance = instance) dispatcher.destroy() dispatcher.remove(key = "key") val returnedInstance = dispatcher.remove(key = "key") assertNull(returnedInstance) } @Test fun GIVEN_instance_put_WHEN_destroy_and_instance_calls_get_in_onDestroy_THEN_returns_null() { val dispatcher = DefaultInstanceKeeperDispatcher() var returnedInstance: Any? = Any() dispatcher.put(key = "key", instance = TestInstance(onDestroy = { returnedInstance = null })) dispatcher.destroy() assertNull(returnedInstance) } @Test fun GIVEN_instances_put_WHEN_destroy_and_instance_calls_remove_in_onDestroy_THEN_does_not_throw() { val dispatcher = DefaultInstanceKeeperDispatcher() dispatcher.put(key = "key1", instance = TestInstance(onDestroy = { dispatcher.remove(key = "key1") })) dispatcher.put(key = "key2", instance = TestInstance(onDestroy = { dispatcher.remove(key = "key2") })) dispatcher.destroy() } private class TestInstance( private val onDestroy: () -> Unit = {}, ) : InstanceKeeper.Instance { var isDestroyed: Boolean = false override fun onDestroy() { isDestroyed = true onDestroy.invoke() } } } ================================================ FILE: instance-keeper/src/commonTest/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperExtTest.kt ================================================ package com.arkivanov.essenty.instancekeeper import kotlin.test.Test import kotlin.test.assertNotSame import kotlin.test.assertSame @Suppress("TestFunctionName", "DEPRECATION") @OptIn(ExperimentalInstanceKeeperApi::class) class InstanceKeeperExtTest { private val dispatcher = InstanceKeeperDispatcher() @Test fun WHEN_getOrCreate_with_same_key_called_second_time_THEN_returns_same_instance() { val thing1 = dispatcher.getOrCreate(key = "key") { ThingInstance() } val thing2 = dispatcher.getOrCreate(key = "key") { ThingInstance() } assertSame(thing1, thing2) } @Test fun WHEN_getOrCreate_with_different_key_called_second_time_THEN_returns_new_instance() { val thing1 = dispatcher.getOrCreate(key = "key1") { ThingInstance() } val thing2 = dispatcher.getOrCreate(key = "key2") { ThingInstance() } assertNotSame(thing1, thing2) } @Test fun WHEN_getOrCreate_with_same_type_called_second_time_THEN_returns_same_instance() { val thing1 = dispatcher.getOrCreate { ThingInstance() } val thing2 = dispatcher.getOrCreate { ThingInstance() } assertSame(thing1, thing2) } @Test fun WHEN_getOrCreate_with_different_type_called_second_time_THEN_returns_new_instance() { val thing1 = dispatcher.getOrCreate { ThingInstance() } val thing2 = dispatcher.getOrCreate { ThingInstance() } assertNotSame>(thing1, thing2) } @Test fun WHEN_getOrCreateSimple_with_same_key_called_second_time_THEN_returns_same_instance() { val thing1 = dispatcher.getOrCreateSimple(key = "key") { Thing() } val thing2 = dispatcher.getOrCreateSimple(key = "key") { Thing() } assertSame(thing1, thing2) } @Test fun WHEN_getOrCreateSimple_with_different_key_called_second_time_THEN_returns_new_instance() { val thing1 = dispatcher.getOrCreateSimple(key = "key1") { Thing() } val thing2 = dispatcher.getOrCreateSimple(key = "key2") { Thing() } assertNotSame(thing1, thing2) } @Test fun WHEN_getOrCreateSimple_with_same_type_called_second_time_THEN_returns_same_instance() { val thing1 = dispatcher.getOrCreateSimple { Thing() } val thing2 = dispatcher.getOrCreateSimple { Thing() } assertSame(thing1, thing2) } @Test fun WHEN_getOrCreateSimple_with_different_type_called_second_time_THEN_returns_new_instance() { val thing1 = dispatcher.getOrCreateSimple { Thing() } val thing2 = dispatcher.getOrCreateSimple { Thing() } assertNotSame>(thing1, thing2) } @Test fun retainingInstance_retains_instance() { val instanceKeeper = InstanceKeeperDispatcher() val component1 = Component(instanceKeeper) val component2 = Component(instanceKeeper) assertSame(component1.instance, component2.instance) } @Test fun retainingSimpleInstance_retains_instance() { val instanceKeeper = InstanceKeeperDispatcher() val component1 = Component(instanceKeeper) val component2 = Component(instanceKeeper) assertSame(component1.simpleInstance, component2.simpleInstance) } @Test fun retainingClosable_retains_instance() { val instanceKeeper = InstanceKeeperDispatcher() val component1 = Component(instanceKeeper) val component2 = Component(instanceKeeper) assertSame(component1.closeable, component2.closeable) } @Suppress("unused") private class Thing @Suppress("unused") private class ThingInstance : InstanceKeeper.Instance @Suppress("unused") private class ThingCloseable : AutoCloseable { override fun close() { // no-op } } private class Component(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { val instance by retainingInstance { ThingInstance() } val simpleInstance by retainingSimpleInstance { Thing() } val closeable by retainingCloseable { ThingCloseable() } } } ================================================ FILE: lifecycle/.gitignore ================================================ /build ================================================ FILE: lifecycle/api/android/lifecycle.api ================================================ public final class com/arkivanov/essenty/lifecycle/AndroidExtKt { public static final fun asEssentyLifecycle (Landroidx/lifecycle/Lifecycle;)Lcom/arkivanov/essenty/lifecycle/Lifecycle; public static final fun essentyLifecycle (Landroidx/lifecycle/LifecycleOwner;)Lcom/arkivanov/essenty/lifecycle/Lifecycle; } public abstract interface class com/arkivanov/essenty/lifecycle/Lifecycle { public abstract fun getState ()Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public abstract fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public abstract fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } public abstract interface class com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks { public abstract fun onCreate ()V public abstract fun onDestroy ()V public abstract fun onPause ()V public abstract fun onResume ()V public abstract fun onStart ()V public abstract fun onStop ()V } public final class com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks$DefaultImpls { public static fun onCreate (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onDestroy (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onPause (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onResume (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onStart (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onStop (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } public final class com/arkivanov/essenty/lifecycle/Lifecycle$State : java/lang/Enum { public static final field CREATED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field DESTROYED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field INITIALIZED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field RESUMED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field STARTED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static fun values ()[Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; } public final class com/arkivanov/essenty/lifecycle/LifecycleExtKt { public static final fun doOnCreate (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;)V public static final fun doOnCreate (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;)V public static final fun doOnDestroy (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;)V public static final fun doOnDestroy (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;)V public static final fun doOnPause (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnPause (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnPause$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnPause$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnResume (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnResume (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnResume$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnResume$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnStart (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnStart (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnStart$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnStart$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnStop (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnStop (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnStop$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnStop$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks; public static synthetic fun subscribe$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks; } public abstract interface class com/arkivanov/essenty/lifecycle/LifecycleOwner { public abstract fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle; } public abstract interface class com/arkivanov/essenty/lifecycle/LifecycleRegistry : com/arkivanov/essenty/lifecycle/Lifecycle, com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks { } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistry$DefaultImpls { public static fun onCreate (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onDestroy (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onPause (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onResume (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onStart (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onStop (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistryExtKt { public static final fun create (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun destroy (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun pause (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun resume (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun start (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun stop (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistryKt { public static final fun LifecycleRegistry ()Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry; public static final fun LifecycleRegistry (Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;)Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry; } ================================================ FILE: lifecycle/api/jvm/lifecycle.api ================================================ public abstract interface class com/arkivanov/essenty/lifecycle/Lifecycle { public abstract fun getState ()Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public abstract fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public abstract fun unsubscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } public abstract interface class com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks { public abstract fun onCreate ()V public abstract fun onDestroy ()V public abstract fun onPause ()V public abstract fun onResume ()V public abstract fun onStart ()V public abstract fun onStop ()V } public final class com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks$DefaultImpls { public static fun onCreate (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onDestroy (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onPause (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onResume (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onStart (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V public static fun onStop (Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks;)V } public final class com/arkivanov/essenty/lifecycle/Lifecycle$State : java/lang/Enum { public static final field CREATED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field DESTROYED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field INITIALIZED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field RESUMED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static final field STARTED Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; public static fun values ()[Lcom/arkivanov/essenty/lifecycle/Lifecycle$State; } public final class com/arkivanov/essenty/lifecycle/LifecycleExtKt { public static final fun doOnCreate (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;)V public static final fun doOnCreate (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;)V public static final fun doOnDestroy (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;)V public static final fun doOnDestroy (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/jvm/functions/Function0;)V public static final fun doOnPause (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnPause (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnPause$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnPause$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnResume (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnResume (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnResume$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnResume$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnStart (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnStart (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnStart$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnStart$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun doOnStop (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;)V public static final fun doOnStop (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;)V public static synthetic fun doOnStop$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static synthetic fun doOnStop$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V public static final fun subscribe (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks; public static synthetic fun subscribe$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/lifecycle/Lifecycle$Callbacks; } public abstract interface class com/arkivanov/essenty/lifecycle/LifecycleOwner { public abstract fun getLifecycle ()Lcom/arkivanov/essenty/lifecycle/Lifecycle; } public abstract interface class com/arkivanov/essenty/lifecycle/LifecycleRegistry : com/arkivanov/essenty/lifecycle/Lifecycle, com/arkivanov/essenty/lifecycle/Lifecycle$Callbacks { } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistry$DefaultImpls { public static fun onCreate (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onDestroy (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onPause (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onResume (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onStart (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static fun onStop (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistryExtKt { public static final fun create (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun destroy (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun pause (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun resume (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun start (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V public static final fun stop (Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;)V } public final class com/arkivanov/essenty/lifecycle/LifecycleRegistryKt { public static final fun LifecycleRegistry ()Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry; public static final fun LifecycleRegistry (Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;)Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry; } ================================================ FILE: lifecycle/api/lifecycle.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Alias: ios => [iosArm64, iosSimulatorArm64, iosX64] // Alias: tvos => [tvosArm64, tvosSimulatorArm64, tvosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: abstract interface com.arkivanov.essenty.lifecycle/Lifecycle { // com.arkivanov.essenty.lifecycle/Lifecycle|null[0] abstract val state // com.arkivanov.essenty.lifecycle/Lifecycle.state|{}state[0] abstract fun (): com.arkivanov.essenty.lifecycle/Lifecycle.State // com.arkivanov.essenty.lifecycle/Lifecycle.state.|(){}[0] abstract fun subscribe(com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks) // com.arkivanov.essenty.lifecycle/Lifecycle.subscribe|subscribe(com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks){}[0] abstract fun unsubscribe(com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks) // com.arkivanov.essenty.lifecycle/Lifecycle.unsubscribe|unsubscribe(com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks){}[0] final enum class State : kotlin/Enum { // com.arkivanov.essenty.lifecycle/Lifecycle.State|null[0] enum entry CREATED // com.arkivanov.essenty.lifecycle/Lifecycle.State.CREATED|null[0] enum entry DESTROYED // com.arkivanov.essenty.lifecycle/Lifecycle.State.DESTROYED|null[0] enum entry INITIALIZED // com.arkivanov.essenty.lifecycle/Lifecycle.State.INITIALIZED|null[0] enum entry RESUMED // com.arkivanov.essenty.lifecycle/Lifecycle.State.RESUMED|null[0] enum entry STARTED // com.arkivanov.essenty.lifecycle/Lifecycle.State.STARTED|null[0] final val entries // com.arkivanov.essenty.lifecycle/Lifecycle.State.entries|#static{}entries[0] final fun (): kotlin.enums/EnumEntries // com.arkivanov.essenty.lifecycle/Lifecycle.State.entries.|#static(){}[0] final fun valueOf(kotlin/String): com.arkivanov.essenty.lifecycle/Lifecycle.State // com.arkivanov.essenty.lifecycle/Lifecycle.State.valueOf|valueOf#static(kotlin.String){}[0] final fun values(): kotlin/Array // com.arkivanov.essenty.lifecycle/Lifecycle.State.values|values#static(){}[0] } abstract interface Callbacks { // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks|null[0] open fun onCreate() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onCreate|onCreate(){}[0] open fun onDestroy() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onDestroy|onDestroy(){}[0] open fun onPause() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onPause|onPause(){}[0] open fun onResume() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onResume|onResume(){}[0] open fun onStart() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onStart|onStart(){}[0] open fun onStop() // com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks.onStop|onStop(){}[0] } } abstract interface com.arkivanov.essenty.lifecycle/LifecycleOwner { // com.arkivanov.essenty.lifecycle/LifecycleOwner|null[0] abstract val lifecycle // com.arkivanov.essenty.lifecycle/LifecycleOwner.lifecycle|{}lifecycle[0] abstract fun (): com.arkivanov.essenty.lifecycle/Lifecycle // com.arkivanov.essenty.lifecycle/LifecycleOwner.lifecycle.|(){}[0] } abstract interface com.arkivanov.essenty.lifecycle/LifecycleRegistry : com.arkivanov.essenty.lifecycle/Lifecycle, com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks // com.arkivanov.essenty.lifecycle/LifecycleRegistry|null[0] final fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/subscribe(kotlin/Function0? = ..., kotlin/Function0? = ..., kotlin/Function0? = ..., kotlin/Function0? = ..., kotlin/Function0? = ..., kotlin/Function0? = ...): com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks // com.arkivanov.essenty.lifecycle/subscribe|subscribe@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Function0?;kotlin.Function0?;kotlin.Function0?;kotlin.Function0?;kotlin.Function0?;kotlin.Function0?){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/create() // com.arkivanov.essenty.lifecycle/create|create@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/destroy() // com.arkivanov.essenty.lifecycle/destroy|destroy@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/pause() // com.arkivanov.essenty.lifecycle/pause|pause@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/resume() // com.arkivanov.essenty.lifecycle/resume|resume@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/start() // com.arkivanov.essenty.lifecycle/start|start@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun (com.arkivanov.essenty.lifecycle/LifecycleRegistry).com.arkivanov.essenty.lifecycle/stop() // com.arkivanov.essenty.lifecycle/stop|stop@com.arkivanov.essenty.lifecycle.LifecycleRegistry(){}[0] final fun com.arkivanov.essenty.lifecycle/LifecycleRegistry(): com.arkivanov.essenty.lifecycle/LifecycleRegistry // com.arkivanov.essenty.lifecycle/LifecycleRegistry|LifecycleRegistry(){}[0] final fun com.arkivanov.essenty.lifecycle/LifecycleRegistry(com.arkivanov.essenty.lifecycle/Lifecycle.State): com.arkivanov.essenty.lifecycle/LifecycleRegistry // com.arkivanov.essenty.lifecycle/LifecycleRegistry|LifecycleRegistry(com.arkivanov.essenty.lifecycle.Lifecycle.State){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnCreate(crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnCreate|doOnCreate@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnDestroy(crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnDestroy|doOnDestroy@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnPause(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnPause|doOnPause@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnResume(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnResume|doOnResume@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnStart(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnStart|doOnStart@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle/doOnStop(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnStop|doOnStop@com.arkivanov.essenty.lifecycle.Lifecycle(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnCreate(crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnCreate|doOnCreate@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnDestroy(crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnDestroy|doOnDestroy@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnPause(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnPause|doOnPause@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnResume(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnResume|doOnResume@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnStart(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnStart|doOnStart@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Boolean;kotlin.Function0){}[0] final inline fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle/doOnStop(kotlin/Boolean = ..., crossinline kotlin/Function0) // com.arkivanov.essenty.lifecycle/doOnStop|doOnStop@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.Boolean;kotlin.Function0){}[0] // Targets: [ios, tvos] final class com.arkivanov.essenty.lifecycle/ApplicationLifecycle : com.arkivanov.essenty.lifecycle/Lifecycle { // com.arkivanov.essenty.lifecycle/ApplicationLifecycle|null[0] constructor () // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.|(){}[0] final val state // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.state|{}state[0] final fun (): com.arkivanov.essenty.lifecycle/Lifecycle.State // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.state.|(){}[0] final fun destroy() // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.destroy|destroy(){}[0] final fun subscribe(com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks) // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.subscribe|subscribe(com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks){}[0] final fun unsubscribe(com.arkivanov.essenty.lifecycle/Lifecycle.Callbacks) // com.arkivanov.essenty.lifecycle/ApplicationLifecycle.unsubscribe|unsubscribe(com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks){}[0] } ================================================ FILE: lifecycle/build.gradle.kts ================================================ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.dependsOn import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.lifecycle" } kotlin { setupSourceSets { val android by bundle() val itvos by bundle() (iosSet + tvosSet) dependsOn itvos itvos dependsOn common common.main.dependencies { implementation(project(":utils-internal")) } android.main.dependencies { implementation(deps.androidx.lifecycle.lifecycleCommonJava8) implementation(deps.androidx.lifecycle.lifecycleRuntime) } } } ================================================ FILE: lifecycle/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: lifecycle/src/androidMain/kotlin/com/arkivanov/essenty/lifecycle/AndroidExt.kt ================================================ package com.arkivanov.essenty.lifecycle import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import com.arkivanov.essenty.lifecycle.Lifecycle as EssentyLifecycle /** * Converts AndroidX [Lifecycle] to Essenty [Lifecycle][EssentyLifecycle] */ fun Lifecycle.asEssentyLifecycle(): EssentyLifecycle = EssentyLifecycleInterop(this) /** * Converts AndroidX [Lifecycle] to Essenty [Lifecycle][EssentyLifecycle] */ fun LifecycleOwner.essentyLifecycle(): EssentyLifecycle = lifecycle.asEssentyLifecycle() private class EssentyLifecycleInterop( private val delegate: Lifecycle ) : EssentyLifecycle { private val observerMap = HashMap() override val state: EssentyLifecycle.State get() = delegate.currentState.toEssentyLifecycleState() override fun subscribe(callbacks: EssentyLifecycle.Callbacks) { check(callbacks !in observerMap) { "Already subscribed" } val observer = AndroidLifecycleObserver(delegate = callbacks, onDestroy = { observerMap -= callbacks }) observerMap[callbacks] = observer delegate.addObserver(observer) } override fun unsubscribe(callbacks: EssentyLifecycle.Callbacks) { observerMap.remove(callbacks)?.also { delegate.removeObserver(it) } } } private fun Lifecycle.State.toEssentyLifecycleState(): EssentyLifecycle.State = when (this) { Lifecycle.State.DESTROYED -> EssentyLifecycle.State.DESTROYED Lifecycle.State.INITIALIZED -> EssentyLifecycle.State.INITIALIZED Lifecycle.State.CREATED -> EssentyLifecycle.State.CREATED Lifecycle.State.STARTED -> EssentyLifecycle.State.STARTED Lifecycle.State.RESUMED -> EssentyLifecycle.State.RESUMED } private class AndroidLifecycleObserver( private val delegate: EssentyLifecycle.Callbacks, private val onDestroy: () -> Unit, ) : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { delegate.onCreate() } override fun onStart(owner: LifecycleOwner) { delegate.onStart() } override fun onResume(owner: LifecycleOwner) { delegate.onResume() } override fun onPause(owner: LifecycleOwner) { delegate.onPause() } override fun onStop(owner: LifecycleOwner) { delegate.onStop() } override fun onDestroy(owner: LifecycleOwner) { delegate.onDestroy() onDestroy.invoke() } } ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/Lifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle /** * A holder of [Lifecycle.State] that can be observed for changes. * * Possible transitions: * * ``` * [INITIALIZED] ──┐ * ↓ * ┌── [CREATED] ──┐ * ↓ ↑ ↓ * [DESTROYED] └── [STARTED] ──┐ * ↑ ↓ * └── [RESUMED] * ``` */ interface Lifecycle { /** * The current state of the [Lifecycle]. */ val state: State /** * Subscribes the given [callbacks] to state changes. */ fun subscribe(callbacks: Callbacks) /** * Unsubscribes the given [callbacks] from state changes. */ fun unsubscribe(callbacks: Callbacks) /** * Defines the possible states of the [Lifecycle]. */ enum class State { DESTROYED, INITIALIZED, CREATED, STARTED, RESUMED } /** * The callbacks of the [Lifecycle]. Each callback is called on the corresponding state change. */ interface Callbacks { fun onCreate() { } fun onStart() { } fun onResume() { } fun onPause() { } fun onStop() { } fun onDestroy() { } } } ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleExt.kt ================================================ package com.arkivanov.essenty.lifecycle /** * A convenience method for [Lifecycle.subscribe]. */ fun Lifecycle.subscribe( onCreate: (() -> Unit)? = null, onStart: (() -> Unit)? = null, onResume: (() -> Unit)? = null, onPause: (() -> Unit)? = null, onStop: (() -> Unit)? = null, onDestroy: (() -> Unit)? = null ): Lifecycle.Callbacks = object : Lifecycle.Callbacks { override fun onCreate() { onCreate?.invoke() } override fun onStart() { onStart?.invoke() } override fun onResume() { onResume?.invoke() } override fun onPause() { onPause?.invoke() } override fun onStop() { onStop?.invoke() } override fun onDestroy() { onDestroy?.invoke() } }.also(::subscribe) /** * Registers the callback [block] to be called when this [Lifecycle] is created. */ inline fun Lifecycle.doOnCreate(crossinline block: () -> Unit) { subscribe( object : Lifecycle.Callbacks { override fun onCreate() { unsubscribe(this) block() } } ) } /** * Registers the callback [block] to be called when this [Lifecycle] is started. * * @param isOneTime if `true` then the callback is automatically unregistered right before * the first call, default value is `false`. */ inline fun Lifecycle.doOnStart(isOneTime: Boolean = false, crossinline block: () -> Unit) { subscribe( object : Lifecycle.Callbacks { override fun onStart() { if (isOneTime) { unsubscribe(this) } block() } } ) } /** * Registers the callback [block] to be called when this [Lifecycle] is resumed. * * @param isOneTime if `true` then the callback is automatically unregistered right before * the first call, default value is `false`. */ inline fun Lifecycle.doOnResume(isOneTime: Boolean = false, crossinline block: () -> Unit) { subscribe( object : Lifecycle.Callbacks { override fun onResume() { if (isOneTime) { unsubscribe(this) } block() } } ) } /** * Registers the callback [block] to be called when this [Lifecycle] is paused. * * @param isOneTime if `true` then the callback is automatically unregistered right before * the first call, default value is `false`. */ inline fun Lifecycle.doOnPause(isOneTime: Boolean = false, crossinline block: () -> Unit) { subscribe( object : Lifecycle.Callbacks { override fun onPause() { if (isOneTime) { unsubscribe(this) } block() } } ) } /** * Registers the callback [block] to be called when this [Lifecycle] is stopped. * * @param isOneTime if `true` then the callback is automatically unregistered right before * the first call, default value is `false`. */ inline fun Lifecycle.doOnStop(isOneTime: Boolean = false, crossinline block: () -> Unit) { subscribe( object : Lifecycle.Callbacks { override fun onStop() { if (isOneTime) { unsubscribe(this) } block() } } ) } /** * Registers the callback [block] to be called when this [Lifecycle] is destroyed. * Calls the [block] immediately if the [Lifecycle] is already destroyed. */ inline fun Lifecycle.doOnDestroy(crossinline block: () -> Unit) { if (state == Lifecycle.State.DESTROYED) { block() } else { subscribe( object : Lifecycle.Callbacks { override fun onDestroy() { block() } } ) } } /** * Convenience method for [Lifecycle.doOnCreate]. */ inline fun LifecycleOwner.doOnCreate(crossinline block: () -> Unit) { lifecycle.doOnCreate(block) } /** * Convenience method for [Lifecycle.doOnStart]. */ inline fun LifecycleOwner.doOnStart(isOneTime: Boolean = false, crossinline block: () -> Unit) { lifecycle.doOnStart(isOneTime = isOneTime, block = block) } /** * Convenience method for [Lifecycle.doOnResume]. */ inline fun LifecycleOwner.doOnResume(isOneTime: Boolean = false, crossinline block: () -> Unit) { lifecycle.doOnResume(isOneTime = isOneTime, block = block) } /** * Convenience method for [Lifecycle.doOnPause]. */ inline fun LifecycleOwner.doOnPause(isOneTime: Boolean = false, crossinline block: () -> Unit) { lifecycle.doOnPause(isOneTime = isOneTime, block = block) } /** * Convenience method for [Lifecycle.doOnStop]. */ inline fun LifecycleOwner.doOnStop(isOneTime: Boolean = false, crossinline block: () -> Unit) { lifecycle.doOnStop(isOneTime = isOneTime, block = block) } /** * Convenience method for [Lifecycle.doOnDestroy]. */ inline fun LifecycleOwner.doOnDestroy(crossinline block: () -> Unit) { lifecycle.doOnDestroy(block) } ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleOwner.kt ================================================ package com.arkivanov.essenty.lifecycle /** * Represents a holder of [Lifecycle]. */ interface LifecycleOwner { val lifecycle: Lifecycle } ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistry.kt ================================================ package com.arkivanov.essenty.lifecycle import kotlin.js.JsName /** * Represents [Lifecycle] and [Lifecycle.Callbacks] at the same time. * Can be used to manually control the [Lifecycle]. */ interface LifecycleRegistry : Lifecycle, Lifecycle.Callbacks /** * Creates a default implementation of [LifecycleRegistry]. */ @JsName("lifecycleRegistry") fun LifecycleRegistry(): LifecycleRegistry = LifecycleRegistry(initialState = Lifecycle.State.INITIALIZED) /** * Creates a default implementation of [LifecycleRegistry] with the specified [initialState]. */ fun LifecycleRegistry( initialState: Lifecycle.State, ): LifecycleRegistry = LifecycleRegistryImpl(initialState) ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryExt.kt ================================================ package com.arkivanov.essenty.lifecycle /** * Drives the state of the [Lifecycle] forward to [Lifecycle.State.CREATED]. * Does nothing if the state is already [Lifecycle.State.CREATED] or greater, or [Lifecycle.State.DESTROYED]. */ fun LifecycleRegistry.create() { if (state == Lifecycle.State.INITIALIZED) { onCreate() } } /** * Drives the state of the [Lifecycle] forward to [Lifecycle.State.STARTED]. * Does nothing if the state is already [Lifecycle.State.STARTED] or greater, or [Lifecycle.State.DESTROYED]. */ fun LifecycleRegistry.start() { create() if (state == Lifecycle.State.CREATED) { onStart() } } /** * Drives the state of the [Lifecycle] forward to [Lifecycle.State.RESUMED]. * Does nothing if the state is already [Lifecycle.State.RESUMED] or greater, or [Lifecycle.State.DESTROYED]. */ fun LifecycleRegistry.resume() { start() if (state == Lifecycle.State.STARTED) { onResume() } } /** * Drives the state of the [Lifecycle] backward to [Lifecycle.State.STARTED]. * Does nothing if the state is already [Lifecycle.State.STARTED] or lower. */ fun LifecycleRegistry.pause() { if (state == Lifecycle.State.RESUMED) { onPause() } } /** * Drives the state of the [Lifecycle] backward to [Lifecycle.State.CREATED]. * Does nothing if the state is already [Lifecycle.State.CREATED] or lower. */ fun LifecycleRegistry.stop() { pause() if (state == Lifecycle.State.STARTED) { onStop() } } /** * Drives the state of the [Lifecycle] backward to [Lifecycle.State.DESTROYED]. * Does nothing if the state is already [Lifecycle.State.DESTROYED]. */ fun LifecycleRegistry.destroy() { stop() if (state == Lifecycle.State.CREATED) { onDestroy() } } ================================================ FILE: lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryImpl.kt ================================================ package com.arkivanov.essenty.lifecycle import com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks import com.arkivanov.essenty.lifecycle.Lifecycle.State internal class LifecycleRegistryImpl(initialState: State) : LifecycleRegistry { private var callbacks = emptySet() private var _state = initialState override val state: State get() = _state override fun subscribe(callbacks: Callbacks) { check(callbacks !in this.callbacks) { "Already subscribed" } this.callbacks += callbacks val state = _state if (state >= State.CREATED) { callbacks.onCreate() } if (state >= State.STARTED) { callbacks.onStart() } if (state >= State.RESUMED) { callbacks.onResume() } } override fun unsubscribe(callbacks: Callbacks) { this.callbacks -= callbacks } override fun onCreate() { checkState(State.INITIALIZED) _state = State.CREATED callbacks.forEach(Callbacks::onCreate) } override fun onStart() { checkState(State.CREATED) _state = State.STARTED callbacks.forEach(Callbacks::onStart) } override fun onResume() { checkState(State.STARTED) _state = State.RESUMED callbacks.forEach(Callbacks::onResume) } override fun onPause() { checkState(State.RESUMED) _state = State.STARTED callbacks.reversed().forEach(Callbacks::onPause) } override fun onStop() { checkState(State.STARTED) _state = State.CREATED callbacks.reversed().forEach(Callbacks::onStop) } override fun onDestroy() { checkState(State.CREATED) _state = State.DESTROYED callbacks.reversed().forEach(Callbacks::onDestroy) callbacks = emptySet() } private fun checkState(required: State) { check(_state == required) { "Expected state $required but was $_state" } } } ================================================ FILE: lifecycle/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/LifecycleExtTest.kt ================================================ package com.arkivanov.essenty.lifecycle import com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks import com.arkivanov.essenty.lifecycle.Lifecycle.State import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @Suppress("TestFunctionName") class LifecycleExtTest { private val owner = TestLifecycleOwner() private val events = ArrayList() @Test fun WHEN_doOnCreate_THEN_not_called() { State.entries.forEach { state -> owner.state = state owner.doOnCreate(callback()) } assertNoEvents() } @Test fun WHEN_doOnCreate_and_onCreate_called_THEN_called() { owner.doOnCreate(callback()) owner.call(Callbacks::onCreate) assertOneEvent() } @Test fun WHEN_doOnStart_THEN_not_called() { State.entries.forEach { state -> owner.state = state owner.doOnStart(block = callback()) } assertNoEvents() } @Test fun WHEN_doOnStart_and_onStart_called_multiple_times_THEN_called_multiple_times() { owner.doOnStart(block = callback()) owner.call(Callbacks::onStart) owner.call(Callbacks::onStart) assertEquals(2, events.size) } @Test fun WHEN_doOnStart_isOneTime_true_and_onStart_called_multiple_times_THEN_called_once() { owner.doOnStart(isOneTime = true, block = callback()) owner.call(Callbacks::onStart) owner.call(Callbacks::onStart) assertOneEvent() } @Test fun WHEN_doOnResume_THEN_not_called() { State.entries.forEach { state -> owner.state = state owner.doOnResume(block = callback()) } assertNoEvents() } @Test fun WHEN_doOnResume_and_onResume_called_multiple_times_THEN_called_multiple_times() { owner.doOnResume(block = callback()) owner.call(Callbacks::onResume) owner.call(Callbacks::onResume) assertEquals(2, events.size) } @Test fun WHEN_doOnResume_isOneTime_true_and_onResume_called_multiple_times_THEN_called_once() { owner.doOnResume(isOneTime = true, block = callback()) owner.call(Callbacks::onResume) owner.call(Callbacks::onResume) assertOneEvent() } @Test fun WHEN_doOnPause_THEN_not_called() { State.entries.forEach { state -> owner.state = state owner.doOnPause(block = callback()) } assertNoEvents() } @Test fun WHEN_doOnPause_and_onPause_called_multiple_times_THEN_called_multiple_times() { owner.doOnPause(block = callback()) owner.call(Callbacks::onPause) owner.call(Callbacks::onPause) assertEquals(2, events.size) } @Test fun WHEN_doOnPause_isOneTime_true_and_onPause_called_multiple_times_THEN_called_once() { owner.doOnPause(isOneTime = true, block = callback()) owner.call(Callbacks::onPause) owner.call(Callbacks::onPause) assertOneEvent() } @Test fun WHEN_doOnStop_THEN_not_called() { State.entries.forEach { state -> owner.state = state owner.doOnStop(block = callback()) } assertNoEvents() } @Test fun WHEN_doOnStop_and_onStop_called_multiple_times_THEN_called_multiple_times() { owner.doOnStop(block = callback()) owner.call(Callbacks::onStop) owner.call(Callbacks::onStop) assertEquals(2, events.size) } @Test fun WHEN_doOnStop_isOneTime_true_and_onStop_called_multiple_times_THEN_called_once() { owner.doOnStop(isOneTime = true, block = callback()) owner.call(Callbacks::onStop) owner.call(Callbacks::onStop) assertOneEvent() } @Test fun GIVEN_state_INITIALIZED_WHEN_doOnDestroy_THEN_not_called() { owner.doOnDestroy(callback()) assertNoEvents() } @Test fun GIVEN_state_CREATED_WHEN_doOnDestroy_THEN_not_called() { owner.state = State.CREATED owner.doOnDestroy(callback()) assertNoEvents() } @Test fun GIVEN_state_CREATED_WHEN_doOnDestroy_and_onDestroy_called_THEN_called() { owner.state = State.CREATED owner.doOnDestroy(callback()) owner.call(Callbacks::onDestroy) assertOneEvent() } @Test fun GIVEN_state_DESTROYED_WHEN_doOnDestroy_THEN_called() { owner.state = State.DESTROYED owner.doOnDestroy(callback()) assertOneEvent() } private fun assertNoEvents() { assertContentEquals(emptyList(), events) } private fun assertOneEvent() { assertEquals(1, events.size) } private fun callback(name: String = "event"): () -> Unit = { events += name } private class TestLifecycleOwner : LifecycleOwner { override val lifecycle: TestLifecycle = TestLifecycle() var state: State by lifecycle::state fun call(call: (Callbacks) -> Unit) { lifecycle.callbacks.forEach(call) } } private class TestLifecycle : Lifecycle { override var state: State = State.INITIALIZED var callbacks: MutableSet = HashSet() override fun subscribe(callbacks: Callbacks) { this.callbacks += callbacks } override fun unsubscribe(callbacks: Callbacks) { this.callbacks -= callbacks } } } ================================================ FILE: lifecycle/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryTest.kt ================================================ package com.arkivanov.essenty.lifecycle import kotlin.test.Test import kotlin.test.assertEquals @Suppress("TestFunctionName") class LifecycleRegistryTest { private val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.INITIALIZED) @Test fun WHEN_called_THEN_calls_subscribers_in_correct_order() { val events = ArrayList() fun callbacks(name: String): Lifecycle.Callbacks = object : Lifecycle.Callbacks { override fun onCreate() { events += "onCreate $name" } override fun onStart() { events += "onStart $name" } override fun onResume() { events += "onResume $name" } override fun onPause() { events += "onPause $name" } override fun onStop() { events += "onStop $name" } override fun onDestroy() { events += "onDestroy $name" } } registry.subscribe(callbacks(name = "1")) registry.subscribe(callbacks(name = "2")) registry.onCreate() registry.onStart() registry.onResume() registry.onPause() registry.onStop() registry.onDestroy() assertEquals( listOf( "onCreate 1", "onCreate 2", "onStart 1", "onStart 2", "onResume 1", "onResume 2", "onPause 2", "onPause 1", "onStop 2", "onStop 1", "onDestroy 2", "onDestroy 1" ), events ) } @Test fun WHEN_unsubscribed_and_called_THEN_callbacks_not_called() { val events = ArrayList() val callbacks = object : Lifecycle.Callbacks { override fun onCreate() { events += "onCreate" } } registry.subscribe(callbacks) registry.unsubscribe(callbacks) registry.onCreate() assertEquals(emptyList(), events) } @Test fun WHEN_unsubscribed_from_callback_and_called_THEN_callbacks_not_called() { val events = ArrayList() val callbacks = object : Lifecycle.Callbacks { override fun onCreate() { registry.unsubscribe(this) } override fun onStart() { events += "onStart" } } registry.subscribe(callbacks) registry.onCreate() registry.onStart() assertEquals(emptyList(), events) } @Test fun WHEN_created_with_initial_state_THEN_state_returns_that_state() { val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.RESUMED) assertEquals(Lifecycle.State.RESUMED, registry.state) } @Test fun GIVEN_created_with_initial_state_WHEN_subscribed_THEN_callbacks_called() { val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.RESUMED) val events = ArrayList() val callbacks = object : Lifecycle.Callbacks { override fun onCreate() { events += "onCreate" } override fun onStart() { events += "onStart" } override fun onResume() { events += "onResume" } } registry.subscribe(callbacks) assertEquals(listOf("onCreate", "onStart", "onResume"), events) } } ================================================ FILE: lifecycle/src/itvosMain/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle import platform.Foundation.NSNotification import platform.Foundation.NSNotificationCenter import platform.Foundation.NSNotificationName import platform.Foundation.NSOperationQueue import platform.UIKit.UIApplication import platform.UIKit.UIApplicationDidBecomeActiveNotification import platform.UIKit.UIApplicationDidEnterBackgroundNotification import platform.UIKit.UIApplicationState import platform.UIKit.UIApplicationWillEnterForegroundNotification import platform.UIKit.UIApplicationWillResignActiveNotification import platform.UIKit.UIApplicationWillTerminateNotification import platform.darwin.NSObjectProtocol /** * An implementation of [Lifecycle] that follows the [UIApplication] lifecycle notifications. * * Since this implementation subscribes to [UIApplication] global lifecycle events, * the instance and all its registered callbacks (and whatever they capture) will stay in * memory until the application is destroyed. It's ok to use it in a global scope like * `UIApplicationDelegate`, but it may cause memory leaks when used in a narrower scope like * `UIViewController` if it gets destroyed earlier. */ class ApplicationLifecycle internal constructor( private val platform: Platform, private val lifecycle: LifecycleRegistry = LifecycleRegistry(), ) : Lifecycle by lifecycle { constructor() : this(platform = DefaultPlatform) private val willEnterForegroundObserver = platform.addObserver(UIApplicationWillEnterForegroundNotification) { lifecycle.start() } private val didBecomeActiveObserver = platform.addObserver(UIApplicationDidBecomeActiveNotification) { lifecycle.resume() } private val willResignActiveObserver = platform.addObserver(UIApplicationWillResignActiveNotification) { lifecycle.pause() } private val didEnterBackgroundObserver = platform.addObserver(UIApplicationDidEnterBackgroundNotification) { lifecycle.stop() } private val willTerminateObserver = platform.addObserver(UIApplicationWillTerminateNotification) { lifecycle.destroy() } init { platform.addOperationOnMainQueue { if (lifecycle.state == Lifecycle.State.INITIALIZED) { when (platform.applicationState) { UIApplicationState.UIApplicationStateActive -> lifecycle.resume() UIApplicationState.UIApplicationStateInactive -> lifecycle.start() UIApplicationState.UIApplicationStateBackground -> lifecycle.create() else -> lifecycle.create() } } } doOnDestroy { platform.removeObserver(willEnterForegroundObserver) platform.removeObserver(didBecomeActiveObserver) platform.removeObserver(willResignActiveObserver) platform.removeObserver(didEnterBackgroundObserver) platform.removeObserver(willTerminateObserver) } } /** * Destroys this [ApplicationLifecycle] moving it to [Lifecycle.State.DESTROYED] state. * Also unsubscribes from all [UIApplication] lifecycle notifications. * * If the current state is [Lifecycle.State.INITIALIZED], then the lifecycle is first * moved to [Lifecycle.State.CREATED] state and then immediately to [Lifecycle.State.DESTROYED] state. */ fun destroy() { if (lifecycle.state == Lifecycle.State.INITIALIZED) { lifecycle.create() } lifecycle.destroy() } internal interface Platform { val applicationState: UIApplicationState fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol fun removeObserver(observer: NSObjectProtocol) fun addOperationOnMainQueue(block: () -> Unit) } internal object DefaultPlatform : Platform { override val applicationState: UIApplicationState get() = UIApplication.sharedApplication.applicationState override fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = NSNotificationCenter.defaultCenter.addObserverForName( name = name, `object` = null, queue = NSOperationQueue.mainQueue, usingBlock = block, ) override fun removeObserver(observer: NSObjectProtocol) { NSNotificationCenter.defaultCenter.removeObserver(observer) } override fun addOperationOnMainQueue(block: () -> Unit) { NSOperationQueue.mainQueue.addOperationWithBlock(block) } } } ================================================ FILE: lifecycle/src/itvosTest/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecyclePlatformTest.kt ================================================ package com.arkivanov.essenty.lifecycle import platform.Foundation.NSNotificationCenter import platform.UIKit.UIApplicationWillEnterForegroundNotification import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @Suppress("TestFunctionName") class ApplicationLifecyclePlatformTest { private val notificationName = UIApplicationWillEnterForegroundNotification private val platform = ApplicationLifecycle.DefaultPlatform @Test fun WHEN_addObserver_and_notification_posted_THEN_notification_received() { val objects = ArrayList() platform.addObserver(notificationName) { objects += it?.`object` } NSNotificationCenter.defaultCenter.postNotificationName(aName = notificationName, `object` = "str") assertContentEquals(listOf("str"), objects) } @Test fun GIVEN_observer_added_WHEN_removeObserver_and_notification_posted_THEN_notification_not_received() { val objects = ArrayList() val observer = platform.addObserver(notificationName) { objects += it?.`object` } platform.removeObserver(observer) NSNotificationCenter.defaultCenter.postNotificationName(aName = notificationName, `object` = "str") assertEquals(emptyList(), objects) } } ================================================ FILE: lifecycle/src/itvosTest/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecycleTest.kt ================================================ package com.arkivanov.essenty.lifecycle import platform.Foundation.NSNotification import platform.Foundation.NSNotificationName import platform.UIKit.UIApplicationDidBecomeActiveNotification import platform.UIKit.UIApplicationDidEnterBackgroundNotification import platform.UIKit.UIApplicationState import platform.UIKit.UIApplicationState.UIApplicationStateActive import platform.UIKit.UIApplicationState.UIApplicationStateBackground import platform.UIKit.UIApplicationState.UIApplicationStateInactive import platform.UIKit.UIApplicationWillEnterForegroundNotification import platform.UIKit.UIApplicationWillResignActiveNotification import platform.UIKit.UIApplicationWillTerminateNotification import platform.darwin.NSObject import platform.darwin.NSObjectProtocol import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @Suppress("TestFunctionName") class ApplicationLifecycleTest { private val platform = TestPlatform() private val lifecycle = ApplicationLifecycle(platform = platform) @Test fun WHEN_WillEnterForeground_notification_THEN_state_STARTED() { postNotification(UIApplicationWillEnterForegroundNotification) assertEquals(Lifecycle.State.STARTED, lifecycle.state) } @Test fun WHEN_DidBecomeActive_notification_THEN_state_RESUMED() { postNotification(UIApplicationDidBecomeActiveNotification) assertEquals(Lifecycle.State.RESUMED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_WHEN_WillResignActive_notification_THEN_state_STARTED() { postNotification(UIApplicationDidBecomeActiveNotification) postNotification(UIApplicationWillResignActiveNotification) assertEquals(Lifecycle.State.STARTED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_WHEN_DidEnterBackground_notification_THEN_state_CREATED() { postNotification(UIApplicationDidBecomeActiveNotification) postNotification(UIApplicationDidEnterBackgroundNotification) assertEquals(Lifecycle.State.CREATED, lifecycle.state) } @Test fun GIVEN_state_STARTED_WHEN_DidEnterBackground_notification_THEN_state_CREATED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) assertEquals(Lifecycle.State.CREATED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_WHEN_WillTerminate_notification_THEN_state_DESTROYED() { postNotification(UIApplicationDidBecomeActiveNotification) postNotification(UIApplicationWillTerminateNotification) assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_WHEN_WillTerminate_notification_THEN_observers_removed() { postNotification(UIApplicationDidBecomeActiveNotification) postNotification(UIApplicationWillTerminateNotification) platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_STATED_WHEN_WillTerminate_notification_THEN_state_DESTROYED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationWillTerminateNotification) assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_STATED_WHEN_WillTerminate_notification_THEN_observers_removed() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationWillTerminateNotification) platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_CREATED_WHEN_WillTerminate_notification_THEN_state_DESTROYED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) postNotification(UIApplicationWillTerminateNotification) assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_CREATED_WHEN_WillTerminate_notification_THEN_observers_removed() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) postNotification(UIApplicationWillTerminateNotification) platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_INITIALIZED_WHEN_destroy_THEN_state_DESTROYED() { lifecycle.destroy() assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_INITIALIZED_WHEN_destroy_THEN_observers_removed() { lifecycle.destroy() platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_RESUMED_WHEN_destroy_THEN_state_DESTROYED() { postNotification(UIApplicationDidBecomeActiveNotification) lifecycle.destroy() assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_WHEN_destroy_THEN_observers_removed() { postNotification(UIApplicationDidBecomeActiveNotification) lifecycle.destroy() platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_STATED_WHEN_destroy_THEN_state_DESTROYED() { postNotification(UIApplicationWillEnterForegroundNotification) lifecycle.destroy() assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_STATED_WHEN_destroy_THEN_observers_removed() { postNotification(UIApplicationWillEnterForegroundNotification) lifecycle.destroy() platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_CREATED_WHEN_destroy_THEN_state_DESTROYED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) lifecycle.destroy() assertEquals(Lifecycle.State.DESTROYED, lifecycle.state) } @Test fun GIVEN_state_CREATED_WHEN_destroy_THEN_observers_removed() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) lifecycle.destroy() platform.assertNotificationObserversEmpty() } @Test fun GIVEN_state_INITIALIZED_and_applicationState_Active_WHEN_main_queue_processed_THEN_state_RESUMED() { platform.applicationState = UIApplicationStateActive platform.processMainQueue() assertEquals(Lifecycle.State.RESUMED, lifecycle.state) } @Test fun GIVEN_state_INITIALIZED_and_applicationState_Inactive_WHEN_main_queue_processed_THEN_state_STARTED() { platform.applicationState = UIApplicationStateInactive platform.processMainQueue() assertEquals(Lifecycle.State.STARTED, lifecycle.state) } @Test fun GIVEN_state_INITIALIZED_and_applicationState_Background_WHEN_main_queue_processed_THEN_state_CREATED() { platform.applicationState = UIApplicationStateBackground platform.processMainQueue() assertEquals(Lifecycle.State.CREATED, lifecycle.state) } @Test fun GIVEN_state_CREATED_and_applicationState_Active_WHEN_main_queue_processed_THEN_state_CREATED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) platform.applicationState = UIApplicationStateActive platform.processMainQueue() assertEquals(Lifecycle.State.CREATED, lifecycle.state) } @Test fun GIVEN_state_CREATED_and_applicationState_Inactive_WHEN_main_queue_processed_THEN_state_CREATED() { postNotification(UIApplicationWillEnterForegroundNotification) postNotification(UIApplicationDidEnterBackgroundNotification) platform.applicationState = UIApplicationStateInactive platform.processMainQueue() assertEquals(Lifecycle.State.CREATED, lifecycle.state) } @Test fun GIVEN_state_RESUMED_and_applicationState_Background_WHEN_main_queue_processed_THEN_state_RESUMED() { postNotification(UIApplicationDidBecomeActiveNotification) platform.applicationState = UIApplicationStateBackground platform.processMainQueue() assertEquals(Lifecycle.State.RESUMED, lifecycle.state) } private fun postNotification(name: NSNotificationName) { platform.postNotification(name = name) } private class TestPlatform : ApplicationLifecycle.Platform { private val notificationObservers = HashMap Unit>>() private val mainQueue = ArrayList<() -> Unit>() override var applicationState: UIApplicationState = UIApplicationStateActive override fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol { val handle = NSObject() notificationObservers += handle to (name to block) return handle } override fun removeObserver(observer: NSObjectProtocol) { notificationObservers -= observer } fun postNotification(name: NSNotificationName) { notificationObservers.values .firstOrNull { (observerName, _) -> observerName == name } ?.second ?.invoke(null) } fun assertNotificationObserversEmpty() { assertTrue(notificationObservers.isEmpty()) } override fun addOperationOnMainQueue(block: () -> Unit) { mainQueue += block } fun processMainQueue() { val blocks = mainQueue.toList() mainQueue.clear() blocks.forEach { it() } } } } ================================================ FILE: lifecycle-coroutines/.gitignore ================================================ /build ================================================ FILE: lifecycle-coroutines/api/android/lifecycle-coroutines.api ================================================ public final class com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleKt { public static final fun coroutineScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; public static synthetic fun coroutineScope$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/CoroutineScope; public static final fun withLifecycle (Lkotlinx/coroutines/CoroutineScope;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lkotlinx/coroutines/CoroutineScope; } public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt { public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } ================================================ FILE: lifecycle-coroutines/api/jvm/lifecycle-coroutines.api ================================================ public final class com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleKt { public static final fun coroutineScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; public static synthetic fun coroutineScope$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/CoroutineScope; public static final fun withLifecycle (Lkotlinx/coroutines/CoroutineScope;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lkotlinx/coroutines/CoroutineScope; } public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt { public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt { public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } ================================================ FILE: lifecycle-coroutines/api/lifecycle-coroutines.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: final fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.coroutines/coroutineScope(kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines/CoroutineScope // com.arkivanov.essenty.lifecycle.coroutines/coroutineScope|coroutineScope@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.coroutines.CoroutineContext){}[0] final fun (kotlinx.coroutines/CoroutineScope).com.arkivanov.essenty.lifecycle.coroutines/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle): kotlinx.coroutines/CoroutineScope // com.arkivanov.essenty.lifecycle.coroutines/withLifecycle|withLifecycle@kotlinx.coroutines.CoroutineScope(com.arkivanov.essenty.lifecycle.Lifecycle){}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).com.arkivanov.essenty.lifecycle.coroutines/flowWithLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle, com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines.flow/Flow<#A> // com.arkivanov.essenty.lifecycle.coroutines/flowWithLifecycle|flowWithLifecycle@kotlinx.coroutines.flow.Flow<0:0>(com.arkivanov.essenty.lifecycle.Lifecycle;com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext){0§}[0] final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).com.arkivanov.essenty.lifecycle.coroutines/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle, com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines.flow/Flow<#A> // com.arkivanov.essenty.lifecycle.coroutines/withLifecycle|withLifecycle@kotlinx.coroutines.flow.Flow<0:0>(com.arkivanov.essenty.lifecycle.Lifecycle;com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext){0§}[0] final suspend fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction1) // com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle|repeatOnLifecycle@com.arkivanov.essenty.lifecycle.Lifecycle(com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] final suspend fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction1) // com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle|repeatOnLifecycle@com.arkivanov.essenty.lifecycle.LifecycleOwner(com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] ================================================ FILE: lifecycle-coroutines/build.gradle.kts ================================================ import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.lifecycle.coroutines" } kotlin { setupSourceSets { common.main.dependencies { implementation(project(":lifecycle")) implementation(deps.kotlinx.coroutinesCore) } common.test.dependencies { implementation(deps.kotlinx.coroutinesTest) } } } ================================================ FILE: lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner import com.arkivanov.essenty.lifecycle.doOnDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext /** * Creates and returns a new [CoroutineScope] with the specified [context]. * The returned [CoroutineScope] is automatically cancelled when the [Lifecycle] is destroyed. * * @param context a [CoroutineContext] to be used for creating the [CoroutineScope], default * is [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] * if available on the current platform, or [Dispatchers.Main] otherwise. */ fun LifecycleOwner.coroutineScope( context: CoroutineContext = Dispatchers.Main.immediateOrFallback, ): CoroutineScope = CoroutineScope(context = context).withLifecycle(lifecycle) /** * Automatically cancels this [CoroutineScope] when the specified [lifecycle] is destroyed. * * @return the same (this) [CoroutineScope]. */ fun CoroutineScope.withLifecycle(lifecycle: Lifecycle): CoroutineScope { lifecycle.doOnDestroy(::cancel) return this } ================================================ FILE: lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/DispatchersExt.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import kotlinx.coroutines.MainCoroutineDispatcher import kotlin.concurrent.Volatile @Volatile private var isImmediateSupported: Boolean = true internal val MainCoroutineDispatcher.immediateOrFallback: MainCoroutineDispatcher get() { if (isImmediateSupported) { try { return immediate } catch (ignored: UnsupportedOperationException) { } catch (ignored: NotImplementedError) { } isImmediateSupported = false } return this } ================================================ FILE: lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import com.arkivanov.essenty.lifecycle.Lifecycle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlin.coroutines.CoroutineContext /** * [Flow] operator that emits values from this upstream [Flow] when the [lifecycle] * is at least at [minActiveState] state. The emissions will be stopped when the * [lifecycle] state falls below [minActiveState] state. * * The [Flow] is collected on the specified [context], which defaults to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] * if available on the current platform, or to [Dispatchers.Main] otherwise. * * See the [AndroidX documentation](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(kotlinx.coroutines.flow.Flow).flowWithLifecycle(androidx.lifecycle.Lifecycle,androidx.lifecycle.Lifecycle.State)) * for more information. */ fun Flow.withLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED, context: CoroutineContext = Dispatchers.Main.immediateOrFallback, ): Flow = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState, context) { this@withLifecycle.collect { send(it) } } close() } @Deprecated( message = "Use 'withLifecycle' instead", replaceWith = ReplaceWith("withLifecycle(lifecycle, minActiveState)"), level = DeprecationLevel.ERROR ) fun Flow.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED, context: CoroutineContext = Dispatchers.Main.immediateOrFallback, ): Flow { return withLifecycle(lifecycle, minActiveState, context) } ================================================ FILE: lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext import kotlin.coroutines.resume /** * Convenience method for [Lifecycle.repeatOnLifecycle]. */ suspend fun LifecycleOwner.repeatOnLifecycle( minActiveState: Lifecycle.State = Lifecycle.State.STARTED, context: CoroutineContext = Dispatchers.Main.immediateOrFallback, block: suspend CoroutineScope.() -> Unit, ) { lifecycle.repeatOnLifecycle(minActiveState = minActiveState, context = context, block = block) } /** * Runs the given [block] in a new coroutine when this [Lifecycle] is at least at [minActiveState] and suspends * the execution until this [Lifecycle] is [Lifecycle.State.DESTROYED]. * * The [block] will cancel and re-launch as the [Lifecycle] moves in and out of the [minActiveState]. * * The [block] is called on the specified [context], which defaults to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] * if available on the current platform, or to [Dispatchers.Main] otherwise. * * See the [AndroidX documentation](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.Lifecycle).repeatOnLifecycle(androidx.lifecycle.Lifecycle.State,kotlin.coroutines.SuspendFunction1)) * for more information. */ suspend fun Lifecycle.repeatOnLifecycle( minActiveState: Lifecycle.State = Lifecycle.State.STARTED, context: CoroutineContext = Dispatchers.Main.immediateOrFallback, block: suspend CoroutineScope.() -> Unit ) { require(minActiveState != Lifecycle.State.INITIALIZED) { "repeatOnEssentyLifecycle cannot start work with the INITIALIZED lifecycle state." } if (this.state == Lifecycle.State.DESTROYED) { return } coroutineScope { withContext(context) { if (this@repeatOnLifecycle.state == Lifecycle.State.DESTROYED) { return@withContext } var callback: Lifecycle.Callbacks? = null var job: Job? = null val mutex = Mutex() try { suspendCancellableCoroutine { cont -> callback = createLifecycleAwareCallback( startState = minActiveState, onStateAppear = { job = launch { mutex.withLock { block() } } }, onStateDisappear = { job?.cancel() job = null }, onDestroy = { cont.resume(Unit) }, ) this@repeatOnLifecycle.subscribe(requireNotNull(callback)) } } finally { job?.cancel() job = null callback?.let { this@repeatOnLifecycle.unsubscribe(it) } callback = null } } } } /** * Creates lifecycle aware [Lifecycle.Callbacks] interface instance. * * @param startState [Lifecycle.State] that [onStateAppear] block must be called from * @param onStateAppear block of code that will be executed when the [Lifecycle.State] was equal [startState] * @param onStateDisappear block of code that will be executed when the [Lifecycle.State] was equal to opposite [startState] * @param onDestroy block of code that will be executed when the [Lifecycle.State] was equal [Lifecycle.State.DESTROYED] * * @return [Lifecycle.Callbacks] */ private fun createLifecycleAwareCallback( startState: Lifecycle.State, onStateAppear: () -> Unit, onStateDisappear: () -> Unit, onDestroy: () -> Unit, ): Lifecycle.Callbacks = object : Lifecycle.Callbacks { override fun onCreate() { launchIfState(Lifecycle.State.CREATED) } override fun onStart() { launchIfState(Lifecycle.State.STARTED) } override fun onResume() { launchIfState(Lifecycle.State.RESUMED) } override fun onPause() { closeIfState(Lifecycle.State.RESUMED) } override fun onStop() { closeIfState(Lifecycle.State.STARTED) } override fun onDestroy() { closeIfState(Lifecycle.State.CREATED) onDestroy() } private fun launchIfState(state: Lifecycle.State) { if (startState == state) { onStateAppear() } } private fun closeIfState(state: Lifecycle.State) { if (startState == state) { onStateDisappear() } } } ================================================ FILE: lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleTest.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.create import com.arkivanov.essenty.lifecycle.destroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive import kotlinx.coroutines.test.StandardTestDispatcher import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class CoroutineScopeWithLifecycleTest { @Test fun GIVEN_lifecycle_not_destroyed_WHEN_scope_created_THEN_scope_is_active() { val lifecycle = LifecycleRegistry() val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) assertTrue(scope.isActive) } @Test fun GIVEN_lifecycle_destroyed_WHEN_scope_created_THEN_scope_is_not_active() { val lifecycle = LifecycleRegistry() lifecycle.create() lifecycle.destroy() val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) assertFalse(scope.isActive) } @Test fun WHEN_lifecycle_destroyed_THEN_scope_is_not_active() { val lifecycle = LifecycleRegistry() val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) lifecycle.create() lifecycle.destroy() assertFalse(scope.isActive) } } ================================================ FILE: lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/DispatchersExtTest.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import kotlin.test.* @OptIn(ExperimentalCoroutinesApi::class) @Suppress("TestFunctionName") class DispatchersExtTest { @AfterTest fun after() { Dispatchers.resetMain() } @Test fun WHEN_immediateOrDefault_called_multiple_times_THEN_returns_same_dispatcher() { val dispatcher1 = Dispatchers.Main.immediateOrFallback val dispatcher2 = Dispatchers.Main.immediateOrFallback assertSame(dispatcher1, dispatcher2) } @Test fun GIVEN_Main_dispatcher_changed_WHEN_immediateOrDefault_called_THEN_returns_updated_dispatcher() { try { Dispatchers.Main.immediate } catch (e: NotImplementedError) { return // Only test on platforms where Main dispatcher is supported } val oldDispatcher = Dispatchers.Main.immediateOrFallback val testDispatcher = StandardTestDispatcher() Dispatchers.setMain(testDispatcher) val newDispatcher = Dispatchers.Main.immediateOrFallback assertNotSame(oldDispatcher, newDispatcher) } } ================================================ FILE: lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/LifecycleCoroutinesExtTest.kt ================================================ package com.arkivanov.essenty.lifecycle.coroutines import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleRegistry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import kotlinx.coroutines.yield import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class LifecycleCoroutinesExtTest { private val testDispatcher = StandardTestDispatcher() private val registry = LifecycleRegistry(initialState = Lifecycle.State.INITIALIZED) @BeforeTest fun beforeTesting() { Dispatchers.setMain(testDispatcher) } @AfterTest fun afterTesting() { Dispatchers.resetMain() } @Test fun test_passed_state_CREATED_must_be_trigger_block_once() = runTest { val state = Lifecycle.State.CREATED val expected = listOf(state) val actual = executeRepeatOnEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expected, actual) } @Test fun test_passed_state_STARTED_must_be_trigger_block_twice() = runTest { val state = Lifecycle.State.STARTED val expected = listOf(state, state) val actual = executeRepeatOnEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expected, actual) } @Test fun test_passed_state_RESUMED_must_be_trigger_block_twice() = runTest { val state = Lifecycle.State.RESUMED val expected = listOf(state, state) val actual = executeRepeatOnEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expected, actual) } @Test fun test_passed_state_DESTROYED_must_not_be_trigger_block() = runTest { val state = Lifecycle.State.DESTROYED val expected = emptyList() val actual = executeRepeatOnEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expected, actual) } @Test fun test_flow_passed_state_CREATED_must_be_trigger_block_once() = runTest { val state = Lifecycle.State.CREATED val expect = listOf(state, state) val actual = executeFlowWithEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expect, actual) } @Test fun test_flow_passed_state_STARTED_must_be_trigger_block_twice() = runTest { val state = Lifecycle.State.STARTED val expect = listOf(state, state, state, state) val actual = executeFlowWithEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expect, actual) } @Test fun test_flow_passed_state_RESUMED_must_be_trigger_block_twice() = runTest { val state = Lifecycle.State.RESUMED val expect = listOf(state, state, state, state) val actual = executeFlowWithEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expect, actual) } @Test fun test_flow_passed_state_DESTROYED_must_not_be_trigger_block() = runTest { val state = Lifecycle.State.DESTROYED val expect = emptyList() val actual = executeFlowWithEssentyLifecycleTest(state) advanceUntilIdle() assertEquals(expect, actual) } private suspend fun executeRepeatOnEssentyLifecycleTest( lifecycleState: Lifecycle.State ): List = coroutineScope { val events = ArrayList() launch { registry.repeatOnLifecycle( minActiveState = lifecycleState, context = testDispatcher ) { events.add(lifecycleState) } } registry.onCreate() yield() registry.onStart() yield() registry.onResume() yield() registry.onPause() yield() registry.onStop() yield() registry.onStart() yield() registry.onResume() yield() registry.onPause() yield() registry.onStop() yield() registry.onDestroy() return@coroutineScope events } private suspend fun executeFlowWithEssentyLifecycleTest( lifecycleState: Lifecycle.State ): List = coroutineScope { val actual = ArrayList() launch { flow { repeat(2) { emit(lifecycleState) } } .withLifecycle(registry, lifecycleState, testDispatcher) .collect { actual.add(it) } } registry.onCreate() yield() registry.onStart() yield() registry.onResume() yield() yield() registry.onPause() yield() registry.onStop() yield() registry.onStart() yield() registry.onResume() yield() registry.onPause() yield() registry.onStop() yield() registry.onDestroy() return@coroutineScope actual } } ================================================ FILE: lifecycle-reaktive/.gitignore ================================================ /build ================================================ FILE: lifecycle-reaktive/api/android/lifecycle-reaktive.api ================================================ public final class com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleKt { public static final fun disposableScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;)Lcom/badoo/reaktive/disposable/scope/DisposableScope; public static final fun withLifecycle (Lcom/badoo/reaktive/disposable/Disposable;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/badoo/reaktive/disposable/Disposable; } ================================================ FILE: lifecycle-reaktive/api/jvm/lifecycle-reaktive.api ================================================ public final class com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleKt { public static final fun disposableScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;)Lcom/badoo/reaktive/disposable/scope/DisposableScope; public static final fun withLifecycle (Lcom/badoo/reaktive/disposable/Disposable;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/badoo/reaktive/disposable/Disposable; } ================================================ FILE: lifecycle-reaktive/api/lifecycle-reaktive.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: final fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.reaktive/disposableScope(): com.badoo.reaktive.disposable.scope/DisposableScope // com.arkivanov.essenty.lifecycle.reaktive/disposableScope|disposableScope@com.arkivanov.essenty.lifecycle.LifecycleOwner(){}[0] final fun <#A: com.badoo.reaktive.disposable/Disposable> (#A).com.arkivanov.essenty.lifecycle.reaktive/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle): #A // com.arkivanov.essenty.lifecycle.reaktive/withLifecycle|withLifecycle@0:0(com.arkivanov.essenty.lifecycle.Lifecycle){0§}[0] ================================================ FILE: lifecycle-reaktive/build.gradle.kts ================================================ import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.lifecycle.reaktive" } kotlin { setupSourceSets { common.main.dependencies { implementation(project(":lifecycle")) implementation(deps.reaktive.reaktive) } } } ================================================ FILE: lifecycle-reaktive/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycle.kt ================================================ package com.arkivanov.essenty.lifecycle.reaktive import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner import com.arkivanov.essenty.lifecycle.doOnDestroy import com.badoo.reaktive.disposable.Disposable import com.badoo.reaktive.disposable.scope.DisposableScope /** * Creates and returns a new [DisposableScope], which is automatically * disposed when the [Lifecycle] is destroyed. */ fun LifecycleOwner.disposableScope(): DisposableScope = DisposableScope().withLifecycle(lifecycle) /** * Automatically disposes this [Disposable] when the specified [lifecycle] is destroyed. * * @return the same (this) [Disposable]. */ fun T.withLifecycle(lifecycle: Lifecycle): T { lifecycle.doOnDestroy(::dispose) return this } ================================================ FILE: lifecycle-reaktive/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleTest.kt ================================================ package com.arkivanov.essenty.lifecycle.reaktive import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.create import com.arkivanov.essenty.lifecycle.destroy import com.badoo.reaktive.disposable.Disposable import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @Suppress("TestFunctionName") class DisposableWithLifecycleTest { @Test fun GIVEN_lifecycle_not_destroyed_WHEN_disposable_created_THEN_disposable_is_not_disposed() { val lifecycle = LifecycleRegistry() val disposable = Disposable().withLifecycle(lifecycle) assertFalse(disposable.isDisposed) } @Test fun GIVEN_lifecycle_destroyed_WHEN_disposable_created_THEN_disposable_is_disposed() { val lifecycle = LifecycleRegistry() lifecycle.create() lifecycle.destroy() val scope = Disposable().withLifecycle(lifecycle) assertTrue(scope.isDisposed) } @Test fun WHEN_lifecycle_destroyed_THEN_disposable_is_disposed() { val lifecycle = LifecycleRegistry() val scope = Disposable().withLifecycle(lifecycle) lifecycle.create() lifecycle.destroy() assertTrue(scope.isDisposed) } } ================================================ FILE: settings.gradle.kts ================================================ dependencyResolutionManagement { versionCatalogs { create("deps") { from(files("deps.versions.toml")) } } } pluginManagement { repositories { gradlePluginPortal() maven("https://jitpack.io") } resolutionStrategy { eachPlugin { if (requested.id.toString() == "com.arkivanov.gradle.setup") { useModule("com.github.arkivanov:gradle-setup-plugin:4ae41e7b6a") } } } plugins { id("com.arkivanov.gradle.setup") } } if (!startParameter.projectProperties.containsKey("check_publication")) { include(":utils-internal") include(":lifecycle") include(":lifecycle-coroutines") include(":lifecycle-reaktive") include(":state-keeper") include(":state-keeper-benchmarks") include(":instance-keeper") include(":back-handler") } else { include(":tools:check-publication") } ================================================ FILE: state-keeper/.gitignore ================================================ /build ================================================ FILE: state-keeper/api/android/state-keeper.api ================================================ public final class com/arkivanov/essenty/statekeeper/AndroidExtKt { public static final fun StateKeeper (Landroidx/savedstate/SavedStateRegistry;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static final fun StateKeeper (Landroidx/savedstate/SavedStateRegistry;ZLkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static synthetic fun StateKeeper$default (Landroidx/savedstate/SavedStateRegistry;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static synthetic fun StateKeeper$default (Landroidx/savedstate/SavedStateRegistry;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static final fun stateKeeper (Landroidx/savedstate/SavedStateRegistryOwner;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static final fun stateKeeper (Landroidx/savedstate/SavedStateRegistryOwner;ZLkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static synthetic fun stateKeeper$default (Landroidx/savedstate/SavedStateRegistryOwner;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; public static synthetic fun stateKeeper$default (Landroidx/savedstate/SavedStateRegistryOwner;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeper; } public final class com/arkivanov/essenty/statekeeper/BundleExtKt { public static final fun getSerializable (Landroid/os/Bundle;Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public static final fun getSerializableContainer (Landroid/os/Bundle;Ljava/lang/String;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; public static final fun putSerializable (Landroid/os/Bundle;Ljava/lang/String;Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V public static final fun putSerializableContainer (Landroid/os/Bundle;Ljava/lang/String;Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)V } public abstract interface annotation class com/arkivanov/essenty/statekeeper/ExperimentalStateKeeperApi : java/lang/annotation/Annotation { } public final class com/arkivanov/essenty/statekeeper/PersistableBundleExtKt { public static final fun getSerializable (Landroid/os/PersistableBundle;Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public static final fun getSerializableContainer (Landroid/os/PersistableBundle;Ljava/lang/String;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; public static final fun putSerializable (Landroid/os/PersistableBundle;Ljava/lang/String;Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V public static final fun putSerializableContainer (Landroid/os/PersistableBundle;Ljava/lang/String;Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)V } public final class com/arkivanov/essenty/statekeeper/PolymorphicSerializerKt { public static final fun polymorphicSerializer (Lkotlin/reflect/KClass;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/KSerializer; } public final class com/arkivanov/essenty/statekeeper/SerializableContainer { public static final field Companion Lcom/arkivanov/essenty/statekeeper/SerializableContainer$Companion; public fun ()V public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun clear ()V public final fun consume (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public final fun set (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V } public final class com/arkivanov/essenty/statekeeper/SerializableContainer$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class com/arkivanov/essenty/statekeeper/SerializableContainerKt { public static final fun SerializableContainer (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; public static final fun consumeRequired (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeper { public abstract fun consume (Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public abstract fun isRegistered (Ljava/lang/String;)Z public abstract fun register (Ljava/lang/String;Lkotlinx/serialization/SerializationStrategy;Lkotlin/jvm/functions/Function0;)V public abstract fun unregister (Ljava/lang/String;)V } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperDispatcher : com/arkivanov/essenty/statekeeper/StateKeeper { public abstract fun save ()Lcom/arkivanov/essenty/statekeeper/SerializableContainer; } public final class com/arkivanov/essenty/statekeeper/StateKeeperDispatcherKt { public static final fun StateKeeperDispatcher (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; public static synthetic fun StateKeeperDispatcher$default (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; } public final class com/arkivanov/essenty/statekeeper/StateKeeperExtKt { public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperOwner { public abstract fun getStateKeeper ()Lcom/arkivanov/essenty/statekeeper/StateKeeper; } ================================================ FILE: state-keeper/api/jvm/state-keeper.api ================================================ public abstract interface annotation class com/arkivanov/essenty/statekeeper/ExperimentalStateKeeperApi : java/lang/annotation/Annotation { } public final class com/arkivanov/essenty/statekeeper/PolymorphicSerializerKt { public static final fun polymorphicSerializer (Lkotlin/reflect/KClass;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/KSerializer; } public final class com/arkivanov/essenty/statekeeper/SerializableContainer { public static final field Companion Lcom/arkivanov/essenty/statekeeper/SerializableContainer$Companion; public fun ()V public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun clear ()V public final fun consume (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public final fun set (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V } public final class com/arkivanov/essenty/statekeeper/SerializableContainer$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class com/arkivanov/essenty/statekeeper/SerializableContainerKt { public static final fun SerializableContainer (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; public static final fun consumeRequired (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeper { public abstract fun consume (Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; public abstract fun isRegistered (Ljava/lang/String;)Z public abstract fun register (Ljava/lang/String;Lkotlinx/serialization/SerializationStrategy;Lkotlin/jvm/functions/Function0;)V public abstract fun unregister (Ljava/lang/String;)V } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperDispatcher : com/arkivanov/essenty/statekeeper/StateKeeper { public abstract fun save ()Lcom/arkivanov/essenty/statekeeper/SerializableContainer; } public final class com/arkivanov/essenty/statekeeper/StateKeeperDispatcherKt { public static final fun StateKeeperDispatcher (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; public static synthetic fun StateKeeperDispatcher$default (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; } public final class com/arkivanov/essenty/statekeeper/StateKeeperExtKt { public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; } public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperOwner { public abstract fun getStateKeeper ()Lcom/arkivanov/essenty/statekeeper/StateKeeper; } ================================================ FILE: state-keeper/api/state-keeper.klib.api ================================================ // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true // - Show declarations: true // Library unique name: open annotation class com.arkivanov.essenty.statekeeper/ExperimentalStateKeeperApi : kotlin/Annotation { // com.arkivanov.essenty.statekeeper/ExperimentalStateKeeperApi|null[0] constructor () // com.arkivanov.essenty.statekeeper/ExperimentalStateKeeperApi.|(){}[0] } abstract interface com.arkivanov.essenty.statekeeper/StateKeeper { // com.arkivanov.essenty.statekeeper/StateKeeper|null[0] abstract fun <#A1: kotlin/Any> consume(kotlin/String, kotlinx.serialization/DeserializationStrategy<#A1>): #A1? // com.arkivanov.essenty.statekeeper/StateKeeper.consume|consume(kotlin.String;kotlinx.serialization.DeserializationStrategy<0:0>){0§}[0] abstract fun <#A1: kotlin/Any> register(kotlin/String, kotlinx.serialization/SerializationStrategy<#A1>, kotlin/Function0<#A1?>) // com.arkivanov.essenty.statekeeper/StateKeeper.register|register(kotlin.String;kotlinx.serialization.SerializationStrategy<0:0>;kotlin.Function0<0:0?>){0§}[0] abstract fun isRegistered(kotlin/String): kotlin/Boolean // com.arkivanov.essenty.statekeeper/StateKeeper.isRegistered|isRegistered(kotlin.String){}[0] abstract fun unregister(kotlin/String) // com.arkivanov.essenty.statekeeper/StateKeeper.unregister|unregister(kotlin.String){}[0] } abstract interface com.arkivanov.essenty.statekeeper/StateKeeperDispatcher : com.arkivanov.essenty.statekeeper/StateKeeper { // com.arkivanov.essenty.statekeeper/StateKeeperDispatcher|null[0] abstract fun save(): com.arkivanov.essenty.statekeeper/SerializableContainer // com.arkivanov.essenty.statekeeper/StateKeeperDispatcher.save|save(){}[0] } abstract interface com.arkivanov.essenty.statekeeper/StateKeeperOwner { // com.arkivanov.essenty.statekeeper/StateKeeperOwner|null[0] abstract val stateKeeper // com.arkivanov.essenty.statekeeper/StateKeeperOwner.stateKeeper|{}stateKeeper[0] abstract fun (): com.arkivanov.essenty.statekeeper/StateKeeper // com.arkivanov.essenty.statekeeper/StateKeeperOwner.stateKeeper.|(){}[0] } final class com.arkivanov.essenty.statekeeper/SerializableContainer { // com.arkivanov.essenty.statekeeper/SerializableContainer|null[0] constructor () // com.arkivanov.essenty.statekeeper/SerializableContainer.|(){}[0] final fun <#A1: kotlin/Any> consume(kotlinx.serialization/DeserializationStrategy<#A1>): #A1? // com.arkivanov.essenty.statekeeper/SerializableContainer.consume|consume(kotlinx.serialization.DeserializationStrategy<0:0>){0§}[0] final fun <#A1: kotlin/Any> set(#A1?, kotlinx.serialization/SerializationStrategy<#A1>) // com.arkivanov.essenty.statekeeper/SerializableContainer.set|set(0:0?;kotlinx.serialization.SerializationStrategy<0:0>){0§}[0] final fun clear() // com.arkivanov.essenty.statekeeper/SerializableContainer.clear|clear(){}[0] final object Companion { // com.arkivanov.essenty.statekeeper/SerializableContainer.Companion|null[0] final fun serializer(): kotlinx.serialization/KSerializer // com.arkivanov.essenty.statekeeper/SerializableContainer.Companion.serializer|serializer(){}[0] } } final fun <#A: kotlin/Any> (com.arkivanov.essenty.statekeeper/SerializableContainer).com.arkivanov.essenty.statekeeper/consumeRequired(kotlinx.serialization/DeserializationStrategy<#A>): #A // com.arkivanov.essenty.statekeeper/consumeRequired|consumeRequired@com.arkivanov.essenty.statekeeper.SerializableContainer(kotlinx.serialization.DeserializationStrategy<0:0>){0§}[0] final fun <#A: kotlin/Any> com.arkivanov.essenty.statekeeper/SerializableContainer(#A?, kotlinx.serialization/SerializationStrategy<#A>): com.arkivanov.essenty.statekeeper/SerializableContainer // com.arkivanov.essenty.statekeeper/SerializableContainer|SerializableContainer(0:0?;kotlinx.serialization.SerializationStrategy<0:0>){0§}[0] final fun <#A: kotlin/Any> com.arkivanov.essenty.statekeeper/polymorphicSerializer(kotlin.reflect/KClass<#A>, kotlinx.serialization.modules/SerializersModule): kotlinx.serialization/KSerializer<#A> // com.arkivanov.essenty.statekeeper/polymorphicSerializer|polymorphicSerializer(kotlin.reflect.KClass<0:0>;kotlinx.serialization.modules.SerializersModule){0§}[0] final fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, kotlin/Function1<#A, #B>, kotlin/String? = ..., kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] final fun <#A: kotlin/Any?, #B: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#B>, kotlin/Function1<#A, #B>, kotlin/String? = ..., kotlin/Function1<#B?, #A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:1>;kotlin.Function1<0:0,0:1>;kotlin.String?;kotlin.Function1<0:1?,0:0>){0§;1§}[0] final fun <#A: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeper).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeper(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] final fun <#A: kotlin/Any?> (com.arkivanov.essenty.statekeeper/StateKeeperOwner).com.arkivanov.essenty.statekeeper/saveable(kotlinx.serialization/KSerializer<#A>, kotlin/String? = ..., kotlin/Function0<#A>): kotlin.properties/PropertyDelegateProvider> // com.arkivanov.essenty.statekeeper/saveable|saveable@com.arkivanov.essenty.statekeeper.StateKeeperOwner(kotlinx.serialization.KSerializer<0:0>;kotlin.String?;kotlin.Function0<0:0>){0§}[0] final fun com.arkivanov.essenty.statekeeper/StateKeeperDispatcher(com.arkivanov.essenty.statekeeper/SerializableContainer? = ...): com.arkivanov.essenty.statekeeper/StateKeeperDispatcher // com.arkivanov.essenty.statekeeper/StateKeeperDispatcher|StateKeeperDispatcher(com.arkivanov.essenty.statekeeper.SerializableContainer?){}[0] final inline fun <#A: reified kotlin/Any> com.arkivanov.essenty.statekeeper/polymorphicSerializer(kotlinx.serialization.modules/SerializersModule): kotlinx.serialization/KSerializer<#A> // com.arkivanov.essenty.statekeeper/polymorphicSerializer|polymorphicSerializer(kotlinx.serialization.modules.SerializersModule){0§}[0] ================================================ FILE: state-keeper/build.gradle.kts ================================================ import com.arkivanov.gradle.bundle import com.arkivanov.gradle.dependsOn import com.arkivanov.gradle.setupBinaryCompatibilityValidator import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("kotlinx-serialization") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() setupBinaryCompatibilityValidator() android { namespace = "com.arkivanov.essenty.statekeeper" } kotlin { setupSourceSets { val java by bundle() val nonJava by bundle() val android by bundle() val macosArm64 by bundle() java dependsOn common javaSet dependsOn java nonJava dependsOn common (allSet - javaSet) dependsOn nonJava common.main.dependencies { implementation(project(":utils-internal")) api(deps.jetbrains.kotlinx.kotlinxSerializationCore) implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson) } android.main.dependencies { implementation(deps.androidx.savedstate.savedstateKtx) implementation(deps.androidx.lifecycle.lifecycleRuntime) } android.test.dependencies { implementation(deps.robolectric.robolectric) } } } ================================================ FILE: state-keeper/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/AndroidExt.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Bundle import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryOwner private const val KEY_STATE = "STATE_KEEPER_STATE" /** * Creates a new instance of [StateKeeper] and attaches it to the provided AndroidX [SavedStateRegistry]. * * @param savedStateRegistry a [SavedStateRegistry] to attach the returned [StateKeeper] to. * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, * default value is `false`. * @param isSavingAllowed called before saving the state. * When `true` then the state will be saved, otherwise it won't. Default value is `true`. */ fun StateKeeper( savedStateRegistry: SavedStateRegistry, discardSavedState: Boolean = false, isSavingAllowed: () -> Boolean = { true }, ): StateKeeper = StateKeeper( savedStateRegistry = savedStateRegistry, key = KEY_STATE, discardSavedState = discardSavedState, isSavingAllowed = isSavingAllowed, ) /** * Creates a new instance of [StateKeeper] and attaches it to the provided AndroidX [SavedStateRegistry]. * * @param savedStateRegistry a [SavedStateRegistry] to attach the returned [StateKeeper] to. * @param key a key to access the provided [SavedStateRegistry], to be used by the returned [StateKeeper]. * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, * default value is `false`. * @param isSavingAllowed called before saving the state. * When `true` then the state will be saved, otherwise it won't. Default value is `true`. */ fun StateKeeper( savedStateRegistry: SavedStateRegistry, key: String, discardSavedState: Boolean = false, isSavingAllowed: () -> Boolean = { true }, ): StateKeeper { val dispatcher = StateKeeperDispatcher( savedState = savedStateRegistry .consumeRestoredStateForKey(key = key) ?.getSerializableContainer(key = KEY_STATE) ?.takeUnless { discardSavedState }, ) savedStateRegistry.registerSavedStateProvider(key = key) { Bundle().apply { if (isSavingAllowed()) { putSerializableContainer(key = KEY_STATE, value = dispatcher.save()) } } } return dispatcher } /** * Creates a new instance of [StateKeeper] and attaches it to the AndroidX [SavedStateRegistry]. * * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, * default value is `false`. * @param isSavingAllowed called before saving the state. * When `true` then the state will be saved, otherwise it won't. Default value is `true`. */ fun SavedStateRegistryOwner.stateKeeper( discardSavedState: Boolean = false, isSavingAllowed: () -> Boolean = { true }, ): StateKeeper = stateKeeper( key = KEY_STATE, discardSavedState = discardSavedState, isSavingAllowed = isSavingAllowed, ) /** * Creates a new instance of [StateKeeper] and attaches it to the AndroidX [SavedStateRegistry]. * * @param key a key to access this [SavedStateRegistry], to be used by the returned [StateKeeper]. * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, * default value is `false`. * @param isSavingAllowed called before saving the state. * When `true` then the state will be saved, otherwise it won't. Default value is `true`. */ fun SavedStateRegistryOwner.stateKeeper( key: String, discardSavedState: Boolean = false, isSavingAllowed: () -> Boolean = { true }, ): StateKeeper = StateKeeper( savedStateRegistry = savedStateRegistry, key = key, discardSavedState = discardSavedState, isSavingAllowed = isSavingAllowed ) ================================================ FILE: state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Bundle import android.os.Parcel import android.os.Parcelable import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy /** * Inserts the provided `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] value * into this [Bundle], replacing any existing value for the given [key]. * Either [key] or [value] may be `null`. */ fun Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy) { putParcelable(key, ValueHolder(value = value, bytes = lazy { value?.serialize(strategy) })) } /** * Returns a `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] associated with * the given [key], or `null` if no mapping exists for the given [key] or a `null` value is explicitly * associated with the [key]. */ fun Bundle.getSerializable(key: String?, strategy: DeserializationStrategy): T? = getParcelableCompat>(key)?.let { holder -> holder.value ?: holder.bytes.value?.deserialize(strategy) } @Suppress("DEPRECATION") private inline fun Bundle.getParcelableCompat(key: String?): T? = classLoader.let { savedClassLoader -> try { classLoader = T::class.java.classLoader getParcelable(key) as T? } finally { classLoader = savedClassLoader } } /** * Inserts the provided [SerializableContainer] into this [Bundle], * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. */ fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?) { putSerializable(key = key, value = value, strategy = SerializableContainer.serializer()) } /** * Returns a [SerializableContainer] associated with the given [key], * or `null` if no mapping exists for the given [key] or a `null` value * is explicitly associated with the [key]. */ fun Bundle.getSerializableContainer(key: String?): SerializableContainer? = getSerializable(key = key, strategy = SerializableContainer.serializer()) private class ValueHolder( val value: T?, val bytes: Lazy, ) : Parcelable { override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeByteArray(bytes.value) } override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator> { override fun createFromParcel(parcel: Parcel): ValueHolder = ValueHolder(value = null, bytes = lazyOf(parcel.createByteArray())) override fun newArray(size: Int): Array?> = arrayOfNulls(size) } } ================================================ FILE: state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/PersistableBundleExt.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Build import android.os.Bundle import android.os.PersistableBundle import androidx.annotation.RequiresApi import com.arkivanov.essenty.statekeeper.base64.base64ToByteArray import com.arkivanov.essenty.statekeeper.base64.toBase64 import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy /** * Inserts the provided `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] value * into this [PersistableBundle], replacing any existing value for the given [key]. * Either [key] or [value] may be `null`. * * **Note:** unlike [Bundle.putSerializable], due to the specifics of [PersistableBundle] this function * serializes the [value] eagerly, which may degrade the performance for large payloads. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun PersistableBundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy) { putString(key, value?.serialize(strategy)?.toBase64()) } /** * Returns a `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] associated with * the given [key], or `null` if no mapping exists for the given [key] or a `null` value is explicitly * associated with the [key]. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun PersistableBundle.getSerializable(key: String?, strategy: DeserializationStrategy): T? = getString(key)?.base64ToByteArray()?.deserialize(strategy) /** * Inserts the provided [SerializableContainer] into this [Bundle], * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. * * **Note:** unlike [Bundle.putSerializableContainer], due to the specifics of [PersistableBundle] * this function serializes the [value] eagerly, which may degrade the performance for large payloads. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun PersistableBundle.putSerializableContainer(key: String?, value: SerializableContainer?) { putSerializable(key = key, value = value, strategy = SerializableContainer.serializer()) } /** * Returns a [SerializableContainer] associated with the given [key], * or `null` if no mapping exists for the given [key] or a `null` value * is explicitly associated with the [key]. */ @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun PersistableBundle.getSerializableContainer(key: String?): SerializableContainer? = getSerializable(key = key, strategy = SerializableContainer.serializer()) ================================================ FILE: state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Bundle import android.os.Parcel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleRegistry import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import kotlinx.serialization.builtins.serializer import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @Suppress("TestFunctionName") @RunWith(RobolectricTestRunner::class) class AndroidStateKeeperTest { @Test fun saves_and_restores_state_without_parcelling() { var savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(null) var stateKeeper = savedStateRegistryOwner.stateKeeper() stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } val bundle = Bundle() savedStateRegistryOwner.controller.performSave(bundle) savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(bundle) stateKeeper = StateKeeper(savedStateRegistry = savedStateRegistryOwner.savedStateRegistry) val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) assertEquals("data", restoredData) } @Test fun saves_and_restores_state_with_parcelling() { var savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(null) var stateKeeper = savedStateRegistryOwner.stateKeeper() stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } val bundle = Bundle() savedStateRegistryOwner.controller.performSave(bundle) savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(bundle.parcelize().deparcelize()) stateKeeper = StateKeeper(savedStateRegistry = savedStateRegistryOwner.savedStateRegistry) val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) assertEquals("data", restoredData) } @Test fun GIVEN_isSavingAllowed_is_false_on_save_THEN_state_not_saved() { val savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(null) val stateKeeper = savedStateRegistryOwner.stateKeeper(isSavingAllowed = { false }) stateKeeper.register(key = "key", strategy = String.serializer()) { throw IllegalStateException("Must not be called") } val bundle = Bundle() savedStateRegistryOwner.controller.performSave(bundle) } @Test fun GIVEN_discardSavedState_is_true_on_restore_THEN_discards_saved_state() { var savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(null) var stateKeeper = savedStateRegistryOwner.stateKeeper() stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } val bundle = Bundle() savedStateRegistryOwner.controller.performSave(bundle) savedStateRegistryOwner = TestSavedStateRegistryOwner() savedStateRegistryOwner.controller.performRestore(bundle.parcelize().deparcelize()) stateKeeper = savedStateRegistryOwner.stateKeeper(discardSavedState = true) val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) assertNull(restoredData) } private class TestSavedStateRegistryOwner : SavedStateRegistryOwner { val controller: SavedStateRegistryController = SavedStateRegistryController.create(this) override val lifecycle: Lifecycle = LifecycleRegistry(this) override val savedStateRegistry: SavedStateRegistry get() = controller.savedStateRegistry } } ================================================ FILE: state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Bundle import kotlinx.serialization.Serializable import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import kotlin.test.Test import kotlin.test.assertEquals @RunWith(RobolectricTestRunner::class) class BundleExtTest { @Test fun getSerializable_returns_same_value_after_putSerializable_without_serialization() { val value = Value(value = "123") val bundle = Bundle() bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) val newValue = bundle.getSerializable(key = "key", strategy = Value.serializer()) assertEquals(value, newValue) } @Test fun getSerializable_returns_same_value_after_putSerializable_with_serialization() { val value = Value(value = "123") val bundle = Bundle() bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) val newValue = bundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) assertEquals(value, newValue) } @Test fun getSerializable_returns_same_value_after_putSerializable_with_double_serialization() { val value = Value(value = "123") val bundle = Bundle() bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) bundle.putInt("int", 123) val newBundle = bundle.parcelize().deparcelize() newBundle.getInt("int") // Force partial deserialization of the Bundle val newValue = newBundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) assertEquals(value, newValue) } @Serializable data class Value(val value: String) } ================================================ FILE: state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt ================================================ package com.arkivanov.essenty.statekeeper import android.os.Bundle import android.os.Parcel internal fun Bundle.parcelize(): ByteArray { val parcel = Parcel.obtain() parcel.writeBundle(this) return parcel.marshall() } internal fun ByteArray.deparcelize(): Bundle { val parcel = Parcel.obtain() parcel.unmarshall(this, 0, size) parcel.setDataPosition(0) return requireNotNull(parcel.readBundle()) } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcher.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy internal class DefaultStateKeeperDispatcher( savedState: SerializableContainer?, ) : StateKeeperDispatcher { private val savedState: MutableMap? = savedState?.consume(strategy = SavedState.serializer())?.map private val suppliers = HashMap>() override fun save(): SerializableContainer { val map = savedState?.toMutableMap() ?: HashMap() suppliers.forEach { (key, supplier) -> supplier.toSerializableContainer()?.also { container -> map[key] = container } } return SerializableContainer(value = SavedState(map), strategy = SavedState.serializer()) } private fun Supplier.toSerializableContainer(): SerializableContainer? = supplier()?.let { value -> SerializableContainer(value = value, strategy = strategy) } override fun consume(key: String, strategy: DeserializationStrategy): T? = savedState ?.remove(key) ?.consume(strategy = strategy) override fun register(key: String, strategy: SerializationStrategy, supplier: () -> T?) { check(!isRegistered(key)) { "Another supplier is already registered with the key: $key" } suppliers[key] = Supplier(strategy = strategy, supplier = supplier) } override fun unregister(key: String) { check(isRegistered(key)) { "No supplier is registered with the key: $key" } suppliers -= key } override fun isRegistered(key: String): Boolean = key in suppliers private class Supplier( val strategy: SerializationStrategy, val supplier: () -> T?, ) @Serializable private class SavedState( val map: MutableMap ) } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/ExperimentalStateKeeperApi.kt ================================================ package com.arkivanov.essenty.statekeeper /** * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. */ @RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class ExperimentalStateKeeperApi ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/PolymorphicSerializer.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.decodeStructure import kotlinx.serialization.encoding.encodeStructure import kotlinx.serialization.modules.SerializersModule import kotlin.reflect.KClass /** * Creates a polymorphic [KSerializer] for the specified class of type [T] using the specified [module]. */ @ExperimentalStateKeeperApi @ExperimentalSerializationApi inline fun polymorphicSerializer(module: SerializersModule): KSerializer = polymorphicSerializer(baseClass = T::class, module = module) /** * Creates a polymorphic [KSerializer] for the specified [baseClass] class using the specified [module]. */ @ExperimentalStateKeeperApi @ExperimentalSerializationApi fun polymorphicSerializer(baseClass: KClass, module: SerializersModule): KSerializer = PolymorphicSerializer(baseClass = baseClass, module = module) @ExperimentalSerializationApi private class PolymorphicSerializer( private val baseClass: KClass, private val module: SerializersModule, ) : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("PolymorphicSerializer") { element("type") element("value", ContextualSerialDescriptor) } override fun serialize(encoder: Encoder, value: T) { val serializer = requireNotNull(module.getPolymorphic(baseClass, value)) encoder.encodeStructure(descriptor) { encodeStringElement(descriptor, 0, serializer.descriptor.serialName) encodeSerializableElement(descriptor, 1, serializer, value) } } override fun deserialize(decoder: Decoder): T = decoder.decodeStructure(descriptor) { var className: String? = null var value: T? = null while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> className = decodeStringElement(descriptor, index) 1 -> { val actualClassName = requireNotNull(className) val serializer = requireNotNull(module.getPolymorphic(baseClass, actualClassName)) value = decodeSerializableElement(descriptor, 1, serializer) } CompositeDecoder.DECODE_DONE -> break else -> error("Unsupported index: $index") } } requireNotNull(value) } private object ContextualSerialDescriptor : SerialDescriptor { override val elementsCount: Int = 0 override val kind: SerialKind = SerialKind.CONTEXTUAL override val serialName: String = "Value" override fun getElementAnnotations(index: Int): List = elementNotFoundError(index) override fun getElementDescriptor(index: Int): SerialDescriptor = elementNotFoundError(index) override fun getElementIndex(name: String): Int = CompositeDecoder.UNKNOWN_NAME override fun getElementName(index: Int): String = elementNotFoundError(index) override fun isElementOptional(index: Int): Boolean = elementNotFoundError(index) private fun elementNotFoundError(index: Int): Nothing { throw IndexOutOfBoundsException("Element at index $index not found") } } } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/SerializableContainer.kt ================================================ package com.arkivanov.essenty.statekeeper import com.arkivanov.essenty.statekeeper.base64.base64ToByteArray import com.arkivanov.essenty.statekeeper.base64.toBase64 import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder /** * Represents a lazy [Serializable][kotlinx.serialization.Serializable] container for a `Serializable` object. */ @Serializable(with = SerializableContainer.Serializer::class) class SerializableContainer private constructor( private var data: ByteArray?, ) { constructor() : this(data = null) private var holder: Holder<*>? = null /** * Deserializes and returns a previously stored [Serializable][kotlinx.serialization.Serializable] object. * * @param strategy a [DeserializationStrategy] for deserializing the object. */ fun consume(strategy: DeserializationStrategy): T? { val consumedValue: Any? = holder?.value ?: data?.deserialize(strategy) holder = null data = null @Suppress("UNCHECKED_CAST") return consumedValue as T? } /** * Stores a [Serializable][kotlinx.serialization.Serializable] object, replacing any previously stored object. * * @param value an object to be stored and serialized later when needed. * @param strategy a [SerializationStrategy] for serializing the value. */ fun set(value: T?, strategy: SerializationStrategy) { holder = Holder(value = value, strategy = strategy) data = null } /** * Clears any previously stored object. */ fun clear() { holder = null data = null } private class Holder( val value: T?, val strategy: SerializationStrategy, ) internal object Serializer : KSerializer { private const val NULL_MARKER = "." override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SerializableContainer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: SerializableContainer) { val bytes = value.holder?.serialize() ?: value.data encoder.encodeString(bytes?.toBase64() ?: NULL_MARKER) } private fun Holder.serialize(): ByteArray? = value?.serialize(strategy) override fun deserialize(decoder: Decoder): SerializableContainer = SerializableContainer(data = decoder.decodeString().takeUnless { it == NULL_MARKER }?.base64ToByteArray()) } } /** * Creates a new [SerializableContainer] and sets the provided [value] with the provided [strategy]. */ fun SerializableContainer( value: T?, strategy: SerializationStrategy ): SerializableContainer = SerializableContainer().apply { set(value = value, strategy = strategy) } /** * A convenience method for [SerializableContainer.consume]. Throws [IllegalStateException] * if the [SerializableContainer] is empty. */ fun SerializableContainer.consumeRequired(strategy: DeserializationStrategy): T = checkNotNull(consume(strategy)) ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeper.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy /** * A key-value storage, typically used to persist data after process death or Android configuration changes. */ interface StateKeeper { /** * Removes and returns a previously saved value for the given [key]. * * @param key a key to look up. * @param strategy a [DeserializationStrategy] for deserializing the value. * @return the value for the given [key] or `null` if no value is found. */ fun consume(key: String, strategy: DeserializationStrategy): T? /** * Registers the value [supplier] to be called when it's time to persist the data. * * @param key a key to be associated with the value. * @param strategy a [SerializationStrategy] for serializing the value. * @param supplier a supplier of the value. */ fun register(key: String, strategy: SerializationStrategy, supplier: () -> T?) /** * Unregisters a previously registered `supplier` for the given [key]. */ fun unregister(key: String) /** * Checks if a `supplier` is registered for the given [key]. */ fun isRegistered(key: String): Boolean } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperDispatcher.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlin.js.JsName /** * Represents a savable [StateKeeper]. */ interface StateKeeperDispatcher : StateKeeper { /** * Calls all registered `suppliers` and saves the data into a [SerializableContainer]. */ fun save(): SerializableContainer } /** * Creates a default implementation of [StateKeeperDispatcher] with the provided [savedState]. */ @JsName("stateKeeperDispatcher") fun StateKeeperDispatcher(savedState: SerializableContainer? = null): StateKeeperDispatcher = DefaultStateKeeperDispatcher(savedState) ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExt.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlin.concurrent.Volatile import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty /** * Helper function for creating a * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) holding an object * whose state is automatically saved and restored using [StateKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.statekeeper.StateKeeper * import com.arkivanov.essenty.statekeeper.saveable * import kotlinx.serialization.Serializable * * private class SomeLogic(stateKeeper: StateKeeper) { * * private val stateHolder by stateKeeper.saveable( * serializer = State.serializer(), * state = StateHolder::state, * ) { savedState -> * StateHolder(state = savedState ?: State()) * } * * private class StateHolder(var state: State) * * @Serializable * private class State(val someValue: Int = 0) * } * ``` * * @param serializer a [KSerializer] for serializing and deserializing the state. * @param state a function that selects a state [S] from the resulting * object [T] and returns it for saving. * @param key an optional key for saving and restoring the state. If not provided, then the * property name is used as a key. * @param init a function that accepts the previously saved state [S] (if any) and * returns an object of type [T]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi fun StateKeeper.saveable( serializer: KSerializer, state: (T) -> S, key: String? = null, init: (savedState: S?) -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val stateKey = key ?: "SAVEABLE_HOLDER_${property.name}" val holderSerializer = Holder.serializer(serializer) val result = init(consume(key = stateKey, strategy = holderSerializer)?.value) register(key = stateKey, strategy = holderSerializer) { Holder(state(result)) } ReadOnlyProperty { _, _ -> result } } /** * Helper function for creating a * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) holding an object * whose state is automatically saved and restored using [StateKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.statekeeper.StateKeeper * import com.arkivanov.essenty.statekeeper.saveable * import kotlinx.serialization.Serializable * * private class SomeLogic(override val stateKeeper: StateKeeper) : StateKeeperOwner { * * private val stateHolder by saveable( * serializer = State.serializer(), * state = StateHolder::state, * ) { savedState -> * StateHolder(state = savedState ?: State()) * } * * private class StateHolder(var state: State) * * @Serializable * private class State(val someValue: Int = 0) * } * ``` * * @param serializer a [KSerializer] for serializing and deserializing the state. * @param state a function that selects a state [S] from the resulting * object [T] and returns it for saving. * @param key an optional key for saving and restoring the state. If not provided, then the * property name is used as a key. * @param init a function that accepts the previously saved state [S] (if any) and * returns an object of type [T]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi fun StateKeeperOwner.saveable( serializer: KSerializer, state: (T) -> S, key: String? = null, init: (savedState: S?) -> T, ): PropertyDelegateProvider> = stateKeeper.saveable( serializer = serializer, state = state, key = key, init = init, ) /** * Helper function for creating a mutable * [delegated property](https://kotlinlang.org/docs/delegated-properties.html) whose value is * automatically saved and restored using [StateKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.statekeeper.StateKeeper * import com.arkivanov.essenty.statekeeper.saveable * import kotlinx.serialization.Serializable * * class SomeLogic(stateKeeper: StateKeeper) { * private var state: State by stateKeeper.saveable(serializer = State.serializer(), init = ::State) * * @Serializable * private class State(val someValue: Int = 0) * } * ``` * * @param serializer a [KSerializer] for serializing and deserializing values of type [T]. * @param key an optional key for saving and restoring the value. If not provided, then the * property name is used as a key. * @param init a function returning the initial value of type [T]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi fun StateKeeper.saveable( serializer: KSerializer, key: String? = null, init: () -> T, ): PropertyDelegateProvider> = PropertyDelegateProvider { _, property -> val stateKey = key ?: "SAVEABLE_${property.name}" val holderSerializer = Holder.serializer(serializer) val holder = consume(key = stateKey, strategy = holderSerializer) ?: Holder(init()) register(key = stateKey, strategy = holderSerializer) { Holder(holder.value) } holder } @Serializable private class Holder(@Volatile var value: T) : ReadWriteProperty { override fun getValue(thisRef: Any?, property: KProperty<*>): T = this.value override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value } } /** * Helper function for creating a mutable * [delegated property]((https://kotlinlang.org/docs/delegated-properties.html)) whose value is * automatically saved and restored using [StateKeeper]. * * Example: * * ``` * import com.arkivanov.essenty.statekeeper.StateKeeper * import com.arkivanov.essenty.statekeeper.saveable * import kotlinx.serialization.Serializable * * class SomeLogic(override val stateKeeper: StateKeeper) : StateKeeperOwner { * private var state: State by saveable(serializer = State.serializer(), init = ::State) * * @Serializable * private class State(val someValue: Int = 0) * } * ``` * * @param serializer a [KSerializer] for serializing and deserializing values of type [T]. * @param key an optional key for saving and restoring the value. If not provided, then the * property name is used as a key. * @param init a function returning the initial value of type [T]. * @return [PropertyDelegateProvider] of type [T], typically used to define a delegated property. */ @ExperimentalStateKeeperApi fun StateKeeperOwner.saveable( serializer: KSerializer, key: String? = null, init: () -> T, ): PropertyDelegateProvider> = stateKeeper.saveable( serializer = serializer, key = key, init = init, ) ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperOwner.kt ================================================ package com.arkivanov.essenty.statekeeper /** * Represents a holder of [StateKeeper]. */ interface StateKeeperOwner { val stateKeeper: StateKeeper } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json internal val essentyJson: Json = Json { allowStructuredMapKeys = true } internal expect fun T.serialize(strategy: SerializationStrategy): ByteArray internal expect fun ByteArray.deserialize(strategy: DeserializationStrategy): T ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Decoder.kt ================================================ package com.arkivanov.essenty.statekeeper.base64 internal fun String.base64ToByteArray(): ByteArray = decode(this) @Suppress("CognitiveComplexMethod", "LoopWithTooManyJumpStatements") // Keep the original internal fun decode(encoded: String): ByteArray { if (encoded.isBlank()) return ByteArray(0) val result = ByteArray(encoded.length) var resultSize = 0 val backDictionary = backDictionary var buffer = 0 var buffered = 0 var index = 0 while (index < encoded.length) { val ch = encoded[index++] if (ch <= ' ') continue if (ch == '=') { index-- break } val value = backDictionary.getOrElse(ch.code) { -1 } if (value == -1) error("Unexpected character $ch (${ch.code})) in $encoded") buffer = buffer shl 6 or value buffered++ if (buffered == 4) { result[resultSize] = (buffer shr 16).toByte() result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() result[resultSize + 2] = (buffer and 0xff).toByte() resultSize += 3 buffered = 0 buffer = 0 } } var padding = 0 while (index < encoded.length) { val ch = encoded[index++] if (ch <= ' ') continue check(ch == '=') padding++ buffer = buffer shl 6 buffered++ } if (buffered == 4) { result[resultSize] = (buffer shr 16).toByte() result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() result[resultSize + 2] = (buffer and 0xff).toByte() resultSize += 3 resultSize -= padding buffered = 0 } check(buffered == 0) { "buffered: $buffered" } return when { resultSize < result.size -> result.copyOf(resultSize) else -> result } } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Dictionaries.kt ================================================ package com.arkivanov.essenty.statekeeper.base64 internal val dictionary: CharArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() internal val backDictionary: IntArray = IntArray(0x80) { code -> dictionary.indexOf(code.toChar()) } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Encoder.kt ================================================ package com.arkivanov.essenty.statekeeper.base64 internal fun ByteArray.toBase64(): String = encode(this) internal fun encode(array: ByteArray): String = buildString(capacity = (array.size / 3) * 4 + 1) { var index = 0 while (index < array.size) { if (index + 3 > array.size) break val buffer = array[index].toInt() and 0xff shl 16 or (array[index + 1].toInt() and 0xff shl 8) or (array[index + 2].toInt() and 0xff shl 0) append(dictionary[buffer shr 18]) append(dictionary[buffer shr 12 and 0x3f]) append(dictionary[buffer shr 6 and 0x3f]) append(dictionary[buffer and 0x3f]) index += 3 } if (index < array.size) { var buffer = 0 while (index < array.size) { buffer = buffer shl 8 or (array[index].toInt() and 0xff) index++ } val padding = 3 - (index % 3) buffer = buffer shl (padding * 8) append(dictionary[buffer shr 18]) append(dictionary[buffer shr 12 and 0x3f]) val a = dictionary[buffer shr 6 and 0x3f] val b = dictionary[buffer and 0x3f] when (padding) { 0 -> { append(a) append(b) } 1 -> { append(a) append('=') } 2 -> { append("==") } } } } ================================================ FILE: state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md ================================================ The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonMain/src/kotlinx.serialization.base64/impl. Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/CodingTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlin.test.Test import kotlin.test.assertEquals class CodingTest { @Test fun serializes_and_deserializes() { val data = SerializableData() val newData = data.serialize(SerializableData.serializer()).deserialize(SerializableData.serializer()) assertEquals(data, newData) } } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcherTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.Serializable import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @Suppress("TestFunctionName") class DefaultStateKeeperDispatcherTest { @Test fun WHEN_save_recreate_consume_THEN_data_restored() { val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) val data1 = Data() val data2 = Data() dispatcher1.register(key = "key1", strategy = Data.serializer()) { data1 } dispatcher1.register(key = "key2", strategy = Data.serializer()) { data2 } dispatcher1.register(key = "key3", strategy = Data.serializer()) { null } val savedState = dispatcher1.save().serializeAndDeserialize() val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState) val restoredData1 = dispatcher2.consume(key = "key1", strategy = Data.serializer()) val restoredData2 = dispatcher2.consume(key = "key2", strategy = Data.serializer()) val restoredData3 = dispatcher2.consume(key = "key3", strategy = Data.serializer()) assertEquals(data1, restoredData1) assertEquals(data2, restoredData2) assertNull(restoredData3) } @Test fun WHEN_save_recreate_twice_consume_THEN_data_restored() { val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) val data1 = Data(value = "value1") val data2 = Data(value = "value2") val data3 = Data(value = "value3") dispatcher1.register(key = "key1", strategy = Data.serializer()) { data1 } dispatcher1.register(key = "key2", strategy = Data.serializer()) { data2 } val savedState1 = dispatcher1.save().serializeAndDeserialize() val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState1) dispatcher2.register(key = "key1", strategy = Data.serializer()) { data3 } val savedState2 = dispatcher2.save().serializeAndDeserialize() val dispatcher3 = DefaultStateKeeperDispatcher(savedState = savedState2) val restoredData1 = dispatcher3.consume(key = "key1", strategy = Data.serializer()) val restoredData2 = dispatcher3.consume(key = "key2", strategy = Data.serializer()) assertEquals(data3, restoredData1) assertEquals(data2, restoredData2) } @Test fun WHEN_consume_second_time_THEN_returns_null() { val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) dispatcher1.register(key = "key", strategy = Data.serializer()) { Data() } val savedState = dispatcher1.save().serializeAndDeserialize() val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState) dispatcher2.consume(key = "key", strategy = Data.serializer()) val restoredSerializable = dispatcher2.consume(key = "key", strategy = Data.serializer()) assertNull(restoredSerializable) } @Test fun GIVEN_not_registered_WHEN_isRegistered_THEN_returns_false() { val dispatcher = DefaultStateKeeperDispatcher(savedState = null) val result = dispatcher.isRegistered(key = "key") assertFalse(result) } @Test fun GIVEN_registered_with_one_key_WHEN_isRegistered_with_another_key_THEN_returns_false() { val dispatcher = DefaultStateKeeperDispatcher(savedState = null) dispatcher.register(key = "key1", strategy = Data.serializer()) { Data() } val result = dispatcher.isRegistered(key = "key2") assertFalse(result) } @Test fun GIVEN_registered_WHEN_isRegistered_with_same_key_THEN_returns_true() { val dispatcher = DefaultStateKeeperDispatcher(savedState = null) dispatcher.register(key = "key", strategy = Data.serializer()) { Data() } val result = dispatcher.isRegistered(key = "key") assertTrue(result) } @Serializable private data class Data( val value: String, ) { constructor() : this(value = "value") // To avoid default values in the primary constructor } } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/PolymorphicSerializerTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlin.test.Test import kotlin.test.assertEquals class PolymorphicSerializerTest { @Test fun serialize_and_deserialize() { val someListSerializer = ListSerializer(SomeSerializer) val originalSome = listOf(Some1(data = SerializableData()), Some2(data = SerializableData())) val newSome = originalSome.serialize(someListSerializer).deserialize(someListSerializer) assertEquals(originalSome, newSome) } private interface Some @Serializable private data class Some1(val data: SerializableData) : Some @Serializable private data class Some2(val data: SerializableData) : Some @OptIn(ExperimentalStateKeeperApi::class, ExperimentalSerializationApi::class) private object SomeSerializer : KSerializer by polymorphicSerializer( SerializersModule { polymorphic(Some::class) { subclass(Some1::class, Some1.serializer()) subclass(Some2::class, Some2.serializer()) } } ) } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/SerializableContainerTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.Serializable import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @Suppress("TestFunctionName") class SerializableContainerTest { @Test fun GIVEN_value_not_set_WHEN_consume_THEN_returns_null() { val container = SerializableContainer() val data = container.consume(SerializableData.serializer()) assertNull(data) } @Test fun GIVEN_value_set_WHEN_consume_THEN_returns_value() { val data = SerializableData() val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) val newData = container.consume(SerializableData.serializer()) assertEquals(data, newData) } @Test fun GIVEN_value_set_and_consumed_WHEN_consume_second_time_THEN_returns_null() { val container = SerializableContainer(value = SerializableData(), strategy = SerializableData.serializer()) container.consume(SerializableData.serializer()) val newData = container.consume(SerializableData.serializer()) assertNull(newData) } @Test fun serializes_and_deserializes_initial_data() { val data = SerializableData() val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) val newContainer = container.serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertEquals(data, newData) } @Test fun serializes_and_deserializes_set_data() { val data = SerializableData() val container = SerializableContainer() container.set(value = data, strategy = SerializableData.serializer()) val newContainer = container.serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertEquals(data, newData) } @Test fun serializes_and_deserializes_initial_data_twice() { val data = SerializableData() val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertEquals(data, newData) } @Test fun serializes_and_deserializes_set_data_twice() { val data = SerializableData() val container = SerializableContainer() container.set(value = data, strategy = SerializableData.serializer()) val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertEquals(data, newData) } @Test fun serializes_and_deserializes_initial_null() { val container = SerializableContainer() val newContainer = container.serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertNull(newData) } @Test fun serializes_and_deserializes_set_null() { val container = SerializableContainer() container.set(null, SerializableData.serializer()) val newContainer = container.serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertNull(newData) } @Test fun serializes_and_deserializes_initial_null_twice() { val container = SerializableContainer() val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertNull(newData) } @Test fun serializes_and_deserializes_set_null_twice() { val container = SerializableContainer() container.set(null, SerializableData.serializer()) val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() val newData = newContainer.consume(strategy = SerializableData.serializer()) assertNull(newData) } @Test fun serializes_and_deserializes_list() { val data = SerializableData() val containers = Containers( listOf( SerializableContainer(data, SerializableData.serializer()), SerializableContainer(), null, ) ) val newContainers = containers.serializeAndDeserialize(Containers.serializer()) val (container1, container2, container3) = newContainers.list assertNotNull(container1) assertNotNull(container2) assertNull(container3) val newData1 = container1.consume(SerializableData.serializer()) val newData2 = container2.consume(SerializableData.serializer()) assertEquals(newData1, data) assertNull(newData2) } @Test fun serializes_and_deserializes_list_twice() { val data = SerializableData() val containers = Containers( listOf( SerializableContainer(data, SerializableData.serializer()), SerializableContainer(), null, ) ) val newContainers = containers.serializeAndDeserialize(Containers.serializer()).serializeAndDeserialize(Containers.serializer()) val (container1, container2, container3) = newContainers.list assertNotNull(container1) assertNotNull(container2) assertNull(container3) val newData1 = container1.consume(SerializableData.serializer()) val newData2 = container2.consume(SerializableData.serializer()) assertEquals(newData1, data) assertNull(newData2) } @Serializable private class Containers( val list: List, ) } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/SerializableData.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @Suppress("DataClassPrivateConstructor") @ConsistentCopyVisibility @Serializable data class SerializableData private constructor( private val i1: Int, private val i2: Int?, private val i3: Int?, private val l1: Long, private val l2: Long?, private val l3: Long?, private val f1: Float, private val f2: Float?, private val f3: Float?, private val d1: Double, private val d2: Double?, private val d3: Double?, private val h1: Short, private val h2: Short?, private val h3: Short?, private val b1: Byte, private val b2: Byte?, private val b3: Byte?, private val c1: Char, private val c2: Char?, private val c3: Char?, private val z1: Boolean, private val z2: Boolean?, private val z3: Boolean?, private val s1: String, private val s2: String?, private val s3: String?, private val s4: String? = "", private val other1: Other, private val other2: Other?, private val other3: Other?, private val obj1: Obj, private val obj2: Obj?, private val obj3: Obj?, private val enum1: SomeEnum, private val enum2: SomeEnum?, private val enum3: SomeEnum?, @Serializable(with = NotSerializable1Serializer::class) private val notSerializable11: NotSerializable1, @Serializable(with = NotSerializable1Serializer::class) private val notSerializable12: NotSerializable1?, @Serializable(with = NotSerializable1Serializer::class) private val notSerializable13: NotSerializable1?, @Serializable(with = NotSerializable2Serializer::class) private val notSerializable21: NotSerializable2, @Serializable(with = NotSerializable2Serializer::class) private val notSerializable22: NotSerializable2?, @Serializable(with = NotSerializable2Serializer::class) private val notSerializable23: NotSerializable2?, private val intList1: List, private val intList2: List?, private val intList3: List?, private val intList4: List, private val intList5: List?, private val intList6: List?, private val longList1: List, private val longList2: List?, private val longList3: List?, private val longList4: List, private val longList5: List?, private val longList6: List?, private val shortList1: List, private val shortList2: List?, private val shortList3: List?, private val shortList4: List, private val shortList5: List?, private val shortList6: List?, private val byteList1: List, private val byteList2: List?, private val byteList3: List?, private val byteList4: List, private val byteList5: List?, private val byteList6: List?, private val charList1: List, private val charList2: List?, private val charList3: List?, private val charList4: List, private val charList5: List?, private val charList6: List?, private val floatList1: List, private val floatList2: List?, private val floatList3: List?, private val floatList4: List, private val floatList5: List?, private val floatList6: List?, private val doubleList1: List, private val doubleList2: List?, private val doubleList3: List?, private val doubleList4: List, private val doubleList5: List?, private val doubleList6: List?, private val booleanList1: List, private val booleanList2: List?, private val booleanList3: List?, private val booleanList4: List, private val booleanList5: List?, private val booleanList6: List?, private val stringList1: List, private val stringList2: List?, private val stringList3: List?, private val stringList4: List, private val stringList5: List?, private val stringList6: List?, private val serialzableList1: List, private val serialzableList2: List?, private val serialzableList3: List?, private val serialzableList4: List, private val serialzableList5: List?, private val serialzableList6: List?, private val intSet1: Set, private val intSet2: Set?, private val intSet3: Set?, private val intSet4: Set, private val intSet5: Set?, private val intSet6: Set?, private val longSet1: Set, private val longSet2: Set?, private val longSet3: Set?, private val longSet4: Set, private val longSet5: Set?, private val longSet6: Set?, private val shortSet1: Set, private val shortSet2: Set?, private val shortSet3: Set?, private val shortSet4: Set, private val shortSet5: Set?, private val shortSet6: Set?, private val byteSet1: Set, private val byteSet2: Set?, private val byteSet3: Set?, private val byteSet4: Set, private val byteSet5: Set?, private val byteSet6: Set?, private val charSet1: Set, private val charSet2: Set?, private val charSet3: Set?, private val charSet4: Set, private val charSet5: Set?, private val charSet6: Set?, private val floatSet1: Set, private val floatSet2: Set?, private val floatSet3: Set?, private val floatSet4: Set, private val floatSet5: Set?, private val floatSet6: Set?, private val doubleSet1: Set, private val doubleSet2: Set?, private val doubleSet3: Set?, private val doubleSet4: Set, private val doubleSet5: Set?, private val doubleSet6: Set?, private val booleanSet1: Set, private val booleanSet2: Set?, private val booleanSet3: Set?, private val booleanSet4: Set, private val booleanSet5: Set?, private val booleanSet6: Set?, private val stringSet1: Set, private val stringSet2: Set?, private val stringSet3: Set?, private val stringSet4: Set, private val stringSet5: Set?, private val stringSet6: Set?, private val serialzableSet1: Set, private val serialzableSet2: Set?, private val serialzableSet3: Set?, private val serialzableSet4: Set, private val serialzableSet5: Set?, private val serialzableSet6: Set?, private val intMap1: Map, private val intMap2: Map?, private val intMap3: Map?, private val intMap4: Map, private val intMap5: Map?, private val intMap6: Map?, private val longMap1: Map, private val longMap2: Map?, private val longMap3: Map?, private val longMap4: Map, private val longMap5: Map?, private val longMap6: Map?, private val shortMap1: Map, private val shortMap2: Map?, private val shortMap3: Map?, private val shortMap4: Map, private val shortMap5: Map?, private val shortMap6: Map?, private val byteMap1: Map, private val byteMap2: Map?, private val byteMap3: Map?, private val byteMap4: Map, private val byteMap5: Map?, private val byteMap6: Map?, private val charMap1: Map, private val charMap2: Map?, private val charMap3: Map?, private val charMap4: Map, private val charMap5: Map?, private val charMap6: Map?, private val floatMap1: Map, private val floatMap2: Map?, private val floatMap3: Map?, private val floatMap4: Map, private val floatMap5: Map?, private val floatMap6: Map?, private val doubleMap1: Map, private val doubleMap2: Map?, private val doubleMap3: Map?, private val doubleMap4: Map, private val doubleMap5: Map?, private val doubleMap6: Map?, private val booleanMap1: Map, private val booleanMap2: Map?, private val booleanMap3: Map?, private val booleanMap4: Map, private val booleanMap5: Map?, private val booleanMap6: Map?, private val stringMap1: Map, private val stringMap2: Map?, private val stringMap3: Map?, private val stringMap4: Map, private val stringMap5: Map?, private val stringMap6: Map?, private val serializableMap1: Map, private val serializableMap2: Map?, private val serializableMap3: Map?, private val serializableMap4: Map, private val serializableMap5: Map?, private val serializableMap6: Map?, ) { constructor() : this( i1 = 1, i2 = 2, i3 = null, l1 = 1L, l2 = 2L, l3 = null, f1 = 1F, f2 = 2F, f3 = null, d1 = 1.0, d2 = 2.0, d3 = null, h1 = 1, h2 = 2, h3 = null, b1 = 1, b2 = 2, b3 = null, c1 = 'a', c2 = 'b', c3 = null, z1 = false, z2 = true, z3 = null, s1 = "str", s2 = "str", s3 = null, s4 = null, other1 = Other(a = 1), other2 = Other(a = 2), other3 = null, obj1 = Obj, obj2 = Obj, obj3 = null, enum1 = SomeEnum.A, enum2 = SomeEnum.B, enum3 = null, notSerializable11 = NotSerializable1(value = 1), notSerializable12 = NotSerializable1(value = 2), notSerializable13 = null, notSerializable21 = NotSerializable2(value = 1), notSerializable22 = NotSerializable2(value = 2), notSerializable23 = null, intList1 = listOf(1, 2), intList2 = listOf(3, 4), intList3 = null, intList4 = listOf(5, null), intList5 = listOf(6, null), intList6 = null, longList1 = listOf(1L, 2L), longList2 = listOf(3L, 4L), longList3 = null, longList4 = listOf(5L, null), longList5 = listOf(6L, null), longList6 = null, shortList1 = listOf(1.toShort(), 2.toShort()), shortList2 = listOf(3.toShort(), 4.toShort()), shortList3 = null, shortList4 = listOf(5.toShort(), null), shortList5 = listOf(6.toShort(), null), shortList6 = null, byteList1 = listOf(1.toByte(), 2.toByte()), byteList2 = listOf(3.toByte(), 4.toByte()), byteList3 = null, byteList4 = listOf(5.toByte(), null), byteList5 = listOf(6.toByte(), null), byteList6 = null, charList1 = listOf('a', 'b'), charList2 = listOf('c', 'd'), charList3 = null, charList4 = listOf('e', null), charList5 = listOf('f', null), charList6 = null, floatList1 = listOf(1F, 2F), floatList2 = listOf(3F, 4F), floatList3 = null, floatList4 = listOf(5F, null), floatList5 = listOf(6F, null), floatList6 = null, doubleList1 = listOf(1.0, 2.0), doubleList2 = listOf(3.0, 4.0), doubleList3 = null, doubleList4 = listOf(5.0, null), doubleList5 = listOf(6.0, null), doubleList6 = null, booleanList1 = listOf(false, true), booleanList2 = listOf(true, false), booleanList3 = null, booleanList4 = listOf(false, null), booleanList5 = listOf(true, null), booleanList6 = null, stringList1 = listOf("a", "b"), stringList2 = listOf("c", "d"), stringList3 = null, stringList4 = listOf("e", null), stringList5 = listOf("f", null), stringList6 = null, serialzableList1 = listOf(Other(a = 1), Other(a = 2)), serialzableList2 = listOf(Other(a = 3), Other(a = 4)), serialzableList3 = null, serialzableList4 = listOf(Other(a = 5), null), serialzableList5 = listOf(Other(a = 6), null), serialzableList6 = null, intSet1 = setOf(1, 2), intSet2 = setOf(3, 4), intSet3 = null, intSet4 = setOf(5, null), intSet5 = setOf(6, null), intSet6 = null, longSet1 = setOf(1L, 2L), longSet2 = setOf(3L, 4L), longSet3 = null, longSet4 = setOf(5L, null), longSet5 = setOf(6L, null), longSet6 = null, shortSet1 = setOf(1.toShort(), 2.toShort()), shortSet2 = setOf(3.toShort(), 4.toShort()), shortSet3 = null, shortSet4 = setOf(5.toShort(), null), shortSet5 = setOf(6.toShort(), null), shortSet6 = null, byteSet1 = setOf(1.toByte(), 2.toByte()), byteSet2 = setOf(3.toByte(), 4.toByte()), byteSet3 = null, byteSet4 = setOf(5.toByte(), null), byteSet5 = setOf(6.toByte(), null), byteSet6 = null, charSet1 = setOf('a', 'b'), charSet2 = setOf('c', 'd'), charSet3 = null, charSet4 = setOf('e', null), charSet5 = setOf('f', null), charSet6 = null, floatSet1 = setOf(1F, 2F), floatSet2 = setOf(3F, 4F), floatSet3 = null, floatSet4 = setOf(5F, null), floatSet5 = setOf(6F, null), floatSet6 = null, doubleSet1 = setOf(1.0, 2.0), doubleSet2 = setOf(3.0, 4.0), doubleSet3 = null, doubleSet4 = setOf(5.0, null), doubleSet5 = setOf(6.0, null), doubleSet6 = null, booleanSet1 = setOf(false, true), booleanSet2 = setOf(true, false), booleanSet3 = null, booleanSet4 = setOf(false, null), booleanSet5 = setOf(true, null), booleanSet6 = null, stringSet1 = setOf("a", "b"), stringSet2 = setOf("c", "d"), stringSet3 = null, stringSet4 = setOf("e", null), stringSet5 = setOf("f", null), stringSet6 = null, serialzableSet1 = setOf(Other(a = 1), Other(a = 2)), serialzableSet2 = setOf(Other(a = 3), Other(a = 4)), serialzableSet3 = null, serialzableSet4 = setOf(Other(a = 5), null), serialzableSet5 = setOf(Other(a = 6), null), serialzableSet6 = null, intMap1 = mapOf(1 to 11, 2 to 22), intMap2 = mapOf(3 to 33, 4 to 44), intMap3 = null, intMap4 = mapOf(5 to 55, null to null), intMap5 = mapOf(6 to 66, null to null), intMap6 = null, longMap1 = mapOf(1L to 11L, 2L to 22L), longMap2 = mapOf(3L to 33L, 4L to 44L), longMap3 = null, longMap4 = mapOf(5L to 55L, null to null), longMap5 = mapOf(6L to 66L, null to null), longMap6 = null, shortMap1 = mapOf(1.toShort() to 11.toShort(), 2.toShort() to 22.toShort()), shortMap2 = mapOf(3.toShort() to 33.toShort(), 4.toShort() to 44.toShort()), shortMap3 = null, shortMap4 = mapOf(5.toShort() to 55.toShort(), null to null), shortMap5 = mapOf(6.toShort() to 66.toShort(), null to null), shortMap6 = null, byteMap1 = mapOf(1.toByte() to 11.toByte(), 2.toByte() to 22.toByte()), byteMap2 = mapOf(3.toByte() to 33.toByte(), 4.toByte() to 44.toByte()), byteMap3 = null, byteMap4 = mapOf(5.toByte() to 55.toByte(), null to null), byteMap5 = mapOf(6.toByte() to 66.toByte(), null to null), byteMap6 = null, charMap1 = mapOf('a' to 'A', 'b' to 'B'), charMap2 = mapOf('c' to 'C', 'd' to 'D'), charMap3 = null, charMap4 = mapOf('e' to 'E', null to null), charMap5 = mapOf('f' to 'F', null to null), charMap6 = null, floatMap1 = mapOf(1F to 11F, 2F to 22F), floatMap2 = mapOf(3F to 33F, 4F to 44F), floatMap3 = null, floatMap4 = mapOf(5F to 55F, null to null), floatMap5 = mapOf(6F to 66F, null to null), floatMap6 = null, doubleMap1 = mapOf(1.0 to 11.0, 2.0 to 22.0), doubleMap2 = mapOf(3.0 to 33.0, 4.0 to 44.0), doubleMap3 = null, doubleMap4 = mapOf(5.0 to 55.0, null to null), doubleMap5 = mapOf(6.0 to 66.0, null to null), doubleMap6 = null, booleanMap1 = mapOf(false to true, true to false), booleanMap2 = mapOf(true to false, false to true), booleanMap3 = null, booleanMap4 = mapOf(false to true, null to null), booleanMap5 = mapOf(true to false, null to null), booleanMap6 = null, stringMap1 = mapOf("a" to "A", "b" to "B"), stringMap2 = mapOf("c" to "C", "d" to "D"), stringMap3 = null, stringMap4 = mapOf("e" to "E", null to null), stringMap5 = mapOf("f" to "F", null to null), stringMap6 = null, serializableMap1 = mapOf(Other(a = 1) to Other(a = 11), Other(a = 2) to Other(a = 22)), serializableMap2 = mapOf(Other(a = 3) to Other(a = 33), Other(a = 4) to Other(a = 44)), serializableMap3 = null, serializableMap4 = mapOf(Other(a = 5) to Other(a = 55), null to null), serializableMap5 = mapOf(Other(a = 6) to Other(a = 66), null to null), serializableMap6 = null, ) } @Serializable private data class Other( val a: Int ) : SomeClass() @Serializable private data object Obj private enum class SomeEnum { A, B } private interface SomeInterface private abstract class SomeClass private data class NotSerializable1( val value: Int, ) private object NotSerializable1Serializer : KSerializer { override val descriptor: SerialDescriptor = Int.serializer().descriptor override fun serialize(encoder: Encoder, value: NotSerializable1) { encoder.encodeInt(value.value) } override fun deserialize(decoder: Decoder): NotSerializable1 = NotSerializable1(value = decoder.decodeInt()) } private data class NotSerializable2( val value: Int, ) private object NotSerializable2Serializer : KSerializer { override val descriptor: SerialDescriptor = Int.serializer().descriptor override fun serialize(encoder: Encoder, value: NotSerializable2) { encoder.encodeInt(value.value) } override fun deserialize(decoder: Decoder): NotSerializable2 = NotSerializable2(value = decoder.decodeInt()) } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @OptIn(ExperimentalStateKeeperApi::class) class StateKeeperExtTest { @Test fun saveable_holder_saves_and_restores_state() { val oldStateKeeper = StateKeeperDispatcher() val oldComponent = ComponentWithStateHolder(oldStateKeeper) oldComponent.holder.state++ val savedState = oldStateKeeper.save().serializeAndDeserialize() val newStateKeeper = StateKeeperDispatcher(savedState = savedState) val newComponent = ComponentWithStateHolder(newStateKeeper) assertEquals(1, newComponent.holder.state) } @Test fun saveable_holder_saves_and_restores_nullable_state() { val oldStateKeeper = StateKeeperDispatcher() val oldComponent = ComponentWithStateHolder(oldStateKeeper) oldComponent.nullableHolder.state = 1 val savedState = oldStateKeeper.save().serializeAndDeserialize() val newStateKeeper = StateKeeperDispatcher(savedState = savedState) val newComponent = ComponentWithStateHolder(newStateKeeper) assertEquals(1, newComponent.nullableHolder.state) } @Test fun saveable_property_saves_and_restores_state() { val oldStateKeeper = StateKeeperDispatcher() val oldComponent = ComponentWithState(oldStateKeeper) oldComponent.state++ val savedState = oldStateKeeper.save().serializeAndDeserialize() val newStateKeeper = StateKeeperDispatcher(savedState = savedState) val newComponent = ComponentWithState(newStateKeeper) assertEquals(1, newComponent.state) } @Test fun saveable_property_saves_and_restores_nullable_state_1() { val oldStateKeeper = StateKeeperDispatcher() val oldComponent = ComponentWithState(oldStateKeeper) oldComponent.nullableState1 = null val savedState = oldStateKeeper.save().serializeAndDeserialize() val newStateKeeper = StateKeeperDispatcher(savedState = savedState) val newComponent = ComponentWithState(newStateKeeper) assertNull(newComponent.nullableState1) } @Test fun saveable_property_saves_and_restores_nullable_state_2() { val oldStateKeeper = StateKeeperDispatcher() val oldComponent = ComponentWithState(oldStateKeeper) oldComponent.nullableState2 = 1 val savedState = oldStateKeeper.save().serializeAndDeserialize() val newStateKeeper = StateKeeperDispatcher(savedState = savedState) val newComponent = ComponentWithState(newStateKeeper) assertEquals(1, newComponent.nullableState2) } private class ComponentWithStateHolder(override val stateKeeper: StateKeeper) : StateKeeperOwner { val holder by saveable(serializer = Int.serializer(), state = Holder::state) { Holder(state = it ?: 0) } val nullableHolder by saveable(serializer = Int.serializer().nullable, state = NullableHolder::state) { NullableHolder(state = it) } } private class ComponentWithState(override val stateKeeper: StateKeeper) : StateKeeperOwner { var state: Int by saveable(serializer = Int.serializer()) { 0 } var nullableState1: Int? by saveable(serializer = Int.serializer().nullable) { 0 } var nullableState2: Int? by saveable(serializer = Int.serializer().nullable) { null } } private class Holder(var state: Int) private class NullableHolder(var state: Int?) } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.KSerializer internal fun T.serializeAndDeserialize(serializer: KSerializer): T = serialize(strategy = serializer) .deserialize(strategy = serializer) internal fun SerializableContainer.serializeAndDeserialize(): SerializableContainer = serializeAndDeserialize(SerializableContainer.serializer()) ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/Base64ImplTest.kt ================================================ package com.arkivanov.essenty.statekeeper.base64 import kotlin.test.Test import kotlin.test.assertEquals class Base64ImplTest { @Test fun encodeSmokeTests() { testEncode("123", "MTIz") testEncode("abcdef", "YWJjZGVm") testEncode("1", "MQ==") testEncode("2", "Mg==") testEncode("12", "MTI=") testEncode("abcd", "YWJjZA==") testEncode("abcde", "YWJjZGU=") // RFC's testcases testEncode("", "") testEncode("f", "Zg==") testEncode("fo", "Zm8=") testEncode("foo", "Zm9v") testEncode("foob", "Zm9vYg==") testEncode("fooba", "Zm9vYmE=") testEncode("foobar", "Zm9vYmFy") } @Test fun decodeSmokeTests() { testDecode("123", "MTIz") testDecode("abcdef", "YWJjZGVm") testDecode("1", "MQ==") testDecode("2", "Mg==") testDecode("12", "MTI=") testDecode("abcd", "YWJjZA==") testDecode("abcde", "YWJjZGU=") // RFC // RFC's testcases testDecode("", "") testDecode("f", "Zg==") testDecode("fo", "Zm8=") testDecode("foo", "Zm9v") testDecode("foob", "Zm9vYg==") testDecode("fooba", "Zm9vYmE=") testDecode("foobar", "Zm9vYmFy") } private fun testEncode(input: String, expected: String) { val result = encode(input.encodeToByteArray()) assertEquals(expected, result) } private fun testDecode(expected: String, encoded: String) { val result = decode(encoded).decodeToString() assertEquals(expected, result) } } ================================================ FILE: state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md ================================================ The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonTest/src/kotlinx/serialization/base64. Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. ================================================ FILE: state-keeper/src/javaMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.java.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream internal actual fun T.serialize(strategy: SerializationStrategy): ByteArray = ByteArrayOutputStream().use { output -> ZipOutputStream(output).use { zip -> zip.setLevel(7) zip.putNextEntry(ZipEntry("Entry")) zip.buffered().use { bufferedOutput -> @OptIn(ExperimentalSerializationApi::class) essentyJson.encodeToStream(serializer = strategy, value = this, stream = bufferedOutput) } } output.toByteArray() } internal actual fun ByteArray.deserialize(strategy: DeserializationStrategy): T = ZipInputStream(ByteArrayInputStream(this)).use { zip -> zip.nextEntry zip.buffered().use { bufferedInput -> @OptIn(ExperimentalSerializationApi::class) essentyJson.decodeFromStream(deserializer = strategy, stream = bufferedInput) } } ================================================ FILE: state-keeper/src/jsTest/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcherJsTest.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlin.test.Test import kotlin.test.assertTrue @Suppress("TestFunctionName") class DefaultStateKeeperDispatcherJsTest { // Verifies the workaround for https://youtrack.jetbrains.com/issue/KT-49186 @Test fun WHEN_save_THEN_returns_SerializableContainer() { val stateKeeper = DefaultStateKeeperDispatcher(null) val serializableContainer = stateKeeper.save() @Suppress("USELESS_IS_CHECK") assertTrue(serializableContainer is SerializableContainer) } } ================================================ FILE: state-keeper/src/nonJavaMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.kt ================================================ package com.arkivanov.essenty.statekeeper import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationStrategy internal actual fun T.serialize(strategy: SerializationStrategy): ByteArray = essentyJson.encodeToString(serializer = strategy, value = this).encodeToByteArray() internal actual fun ByteArray.deserialize(strategy: DeserializationStrategy): T = essentyJson.decodeFromString(deserializer = strategy, string = decodeToString()) ================================================ FILE: state-keeper-benchmarks/.gitignore ================================================ /build ================================================ FILE: state-keeper-benchmarks/build.gradle.kts ================================================ import com.arkivanov.gradle.setupAndroidLibrary plugins { id("kotlin-android") id("com.android.library") id("kotlinx-serialization") id("kotlin-parcelize") id("com.arkivanov.gradle.setup") } setupAndroidLibrary() android { namespace = "com.arkivanov.essenty.statekeeper.benchmarks" testOptions.unitTests.isIncludeAndroidResources = true } dependencies { implementation(project(":state-keeper")) implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson) testImplementation(kotlin("test")) testImplementation(deps.robolectric.robolectric) } ================================================ FILE: state-keeper-benchmarks/src/main/res/AndroidManifest.xml ================================================ ================================================ FILE: state-keeper-benchmarks/src/test/kotlin/com/arkivanov/essenty/statekeeper/benchmarks/Benchmarks.kt ================================================ @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package com.arkivanov.essenty.statekeeper.benchmarks import android.os.Build import android.os.Bundle import android.os.Parcel import android.os.Parcelable import com.arkivanov.essenty.statekeeper.deserialize import com.arkivanov.essenty.statekeeper.serialize import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import org.robolectric.annotation.Config import kotlin.test.assertEquals import kotlin.time.measureTime //@RunWith(RobolectricTestRunner::class) @Config(minSdk = Build.VERSION_CODES.TIRAMISU) class Benchmarks { // Manual run only // @Test fun size() { val data = getData() println("Parcelable size: ${data.getParcelizedSize()}") println("Serializable size: ${data.getSerializedSize()}") val newDataParcelable = data.parcelize().deparcelize() val newDataSerializable = data.serialize(Data.serializer()).deserialize(Data.serializer()) assertEquals(data, newDataParcelable) assertEquals(data, newDataSerializable) } // Manual run only // @Test fun performance() { val data = getData() repeat(100) { data.parcelize().deparcelize() data.serialize(Data.serializer()).deserialize(Data.serializer()) } val t1 = measureTime { repeat(100) { data.parcelize().deparcelize() } } val t2 = measureTime { repeat(100) { data.serialize(Data.serializer()).deserialize(Data.serializer()) } } println("Parcelize time: $t1") println("Serialize time: $t2") } private fun Data.getParcelizedSize(): Int = parcelize().size private fun Data.parcelize(): ByteArray { val bundle = Bundle() bundle.putParcelable("key", this) val parcel = Parcel.obtain() parcel.writeBundle(bundle) return parcel.marshall() } private fun ByteArray.deparcelize(): Data { val parcel = Parcel.obtain() parcel.unmarshall(this, 0, size) parcel.setDataPosition(0) return requireNotNull(parcel.readBundle()).getParcelable("key", Data::class.java)!! } private fun Data.getSerializedSize(): Int = serialize(Data.serializer()).size private fun getData(): Data = getInnerData( dataList = List(30) { getInnerData( dataList = List(10) { getInnerData() }, ) }, ) private fun getInnerData(dataList: List = emptyList()): Data = Data( booleanValue = true, byteValue = 64, shortValue = 8192, integerValue = 1234567, longValue = 12345678912345, floatValue = 100F, doubleValue = 100.0, charValue = 'a', stringValue = "Some string data goes here", intList = List(100) { it }, stringList = List(100) { "Some string data goes here" }, dataList = dataList, ) @Serializable @Parcelize data class Data( val booleanValue: Boolean, val byteValue: Byte, val shortValue: Short, val integerValue: Int, val longValue: Long, val floatValue: Float, val doubleValue: Double, val charValue: Char, val stringValue: String, val intList: List, val stringList: List, val dataList: List, ) : Parcelable } ================================================ FILE: tools/check-publication/.gitignore ================================================ *.iml .gradle /local.properties .idea /build ================================================ FILE: tools/check-publication/build.gradle.kts ================================================ import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupSourceSets plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() repositories { maven("https://s01.oss.sonatype.org/content/groups/staging/") { credentials { username = "arkivanov" password = System.getenv("SONATYPE_PASSWORD") } } } android { namespace = "com.arkivanov.essenty.tools.checkpublication" } kotlin { setupSourceSets { common.main.dependencies { val version = deps.versions.essenty.get() implementation("com.arkivanov.essenty:back-handler:$version") implementation("com.arkivanov.essenty:instance-keeper:$version") implementation("com.arkivanov.essenty:lifecycle:$version") implementation("com.arkivanov.essenty:lifecycle-coroutines:$version") implementation("com.arkivanov.essenty:lifecycle-reaktive:$version") implementation("com.arkivanov.essenty:state-keeper:$version") } } } ================================================ FILE: tools/check-publication/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: tools/check-publication/src/commonMain/kotlin/com/arkivanov/essenty/tools/checkpublication/Dummy.kt ================================================ package com.arkivanov.essenty.tools.checkpublication fun dummy() { // no-op } ================================================ FILE: utils-internal/.gitignore ================================================ /build ================================================ FILE: utils-internal/build.gradle.kts ================================================ import com.arkivanov.gradle.setupMultiplatform import com.arkivanov.gradle.setupPublication plugins { id("kotlin-multiplatform") id("com.android.library") id("com.arkivanov.gradle.setup") } setupMultiplatform() setupPublication() android { namespace = "com.arkivanov.essenty.utils.internal" } ================================================ FILE: utils-internal/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: utils-internal/src/commonMain/kotlin/com/arkivanov/essenty/utils/internal/ExperimentalEssentyApi.kt ================================================ package com.arkivanov.essenty.utils.internal /** * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. */ @RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class ExperimentalEssentyApi ================================================ FILE: utils-internal/src/commonMain/kotlin/com/arkivanov/essenty/utils/internal/InternalEssentyApi.kt ================================================ package com.arkivanov.essenty.utils.internal @RequiresOptIn(message = "This API is internal, please don't use it.", level = RequiresOptIn.Level.ERROR) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) annotation class InternalEssentyApi