├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── BuildScriptExtensions.kt ├── config └── detekt │ └── detekt.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mapmemory-coroutines ├── api │ └── mapmemory-coroutines.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── CoroutinesAccessors.kt │ │ └── ReactiveMutableMap.kt │ └── test │ └── kotlin │ ├── CoroutinesAccessorsTest.kt │ └── ReactiveMutableMapTest.kt ├── mapmemory-kapt-bug-workaround ├── api │ └── mapmemory-kapt-bug-workaround.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── KaptBugWorkaround.kt │ └── test │ └── kotlin │ └── KaptBugWorkaroundTest.kt ├── mapmemory-rxjava2 ├── api │ └── mapmemory-rxjava2.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── ReactiveMutableMap.kt │ │ ├── RxAccessors.kt │ │ └── internal │ │ └── SubjectsVisibilityHack.kt │ └── test │ └── kotlin │ ├── ReactiveMutableMapTest.kt │ └── RxAccessorsTest.kt ├── mapmemory-rxjava3 ├── api │ └── mapmemory-rxjava3.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── ReactiveMutableMap.kt │ │ ├── RxAccessors.kt │ │ └── internal │ │ └── SubjectsVisibilityHack.kt │ └── test │ └── kotlin │ ├── ReactiveMutableMapTest.kt │ └── RxAccessorsTest.kt ├── mapmemory-test ├── api │ └── mapmemory-test.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── MapMemory.kt │ │ └── Scoped.kt │ └── test │ └── kotlin │ └── SopedTest.kt ├── mapmemory ├── api │ └── mapmemory.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ ├── MapMemory.kt │ │ ├── MemoryAccessors.kt │ │ ├── SharedMapMemoryProperty.kt │ │ └── internal │ │ ├── IntenalUtils.kt │ │ └── ReusableProperty.kt │ └── test │ └── kotlin │ ├── MapMemoryAccessorsTest.kt │ ├── MapMemoryTest.kt │ ├── ReusablePropertiesTest.kt │ └── SharedMapMemoryPropertyTest.kt └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | ij_visual_guides = 100 12 | 13 | ## General 14 | ij_continuation_indent_size = 8 15 | ij_smart_tabs = false 16 | ij_wrap_on_typing = false 17 | ij_any_keep_indents_on_empty_lines = false 18 | 19 | ## Formatter Control 20 | ij_formatter_tags_enabled = true 21 | ij_formatter_on_tag = @formatter:on 22 | ij_formatter_off_tag = @formatter:off 23 | 24 | [{*.kt, *.kts}] 25 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 26 | ij_continuation_indent_size = 4 # To match ktlint settings 27 | ij_kotlin_keep_indents_on_empty_lines = false 28 | 29 | ## Wrapping and Braces 30 | # Keep when reformatting 31 | ij_kotlin_keep_line_breaks = true 32 | ij_kotlin_keep_first_column_comment = true 33 | # Extends/implements list 34 | ij_kotlin_extends_list_wrap = normal 35 | ij_kotlin_align_multiline_extends_list = false 36 | ij_kotlin_continuation_indent_in_supertype_lists = false 37 | # Function declaration parameters 38 | ij_kotlin_method_parameters_wrap = on_every_item 39 | ij_kotlin_align_multiline_parameters = true 40 | ij_kotlin_method_parameters_new_line_after_left_paren = true 41 | ij_kotlin_method_parameters_right_paren_on_new_line = true 42 | ij_kotlin_continuation_indent_in_parameter_lists = false 43 | # Function call arguments 44 | ij_kotlin_call_parameters_wrap = on_every_item 45 | ij_kotlin_align_multiline_parameters_in_calls = false 46 | ij_kotlin_call_parameters_new_line_after_left_paren = true 47 | ij_kotlin_call_parameters_right_paren_on_new_line = true 48 | ij_kotlin_continuation_indent_in_argument_lists = false 49 | # Function parentheses 50 | ij_kotlin_align_multiline_method_parentheses = false 51 | # Chained function calls 52 | ij_kotlin_method_call_chain_wrap = normal 53 | ij_kotlin_wrap_first_method_in_call_chain = false 54 | ij_kotlin_continuation_indent_for_chained_calls = false 55 | # 'if()' statement 56 | ij_kotlin_else_on_new_line = false 57 | ij_kotlin_if_rparen_on_new_line = true 58 | ij_kotlin_continuation_indent_in_if_conditions = false 59 | # 'do ... while()' statement 60 | ij_kotlin_while_on_new_line = false 61 | # 'try' statement 62 | ij_kotlin_catch_on_new_line = false 63 | ij_kotlin_finally_on_new_line = false 64 | # Binary expressions 65 | ij_kotlin_align_multiline_binary_operation = false 66 | # Wraps 67 | ij_kotlin_assignment_wrap = normal 68 | ij_kotlin_enum_constants_wrap = off 69 | ij_kotlin_class_annotation_wrap = split_into_lines 70 | ij_kotlin_method_annotation_wrap = split_into_lines 71 | ij_kotlin_field_annotation_wrap = split_into_lines 72 | ij_kotlin_parameter_annotation_wrap = off 73 | ij_kotlin_variable_annotation_wrap = off 74 | # 'when' statements 75 | ij_kotlin_align_in_columns_case_branch = false 76 | # Braces placement 77 | ij_kotlin_lbrace_on_next_line = false 78 | # Expression body functions 79 | ij_kotlin_wrap_expression_body_functions = 1 80 | ij_kotlin_continuation_indent_for_expression_bodies = false 81 | # Elvis expressions 82 | ij_kotlin_wrap_elvis_expressions = 1 83 | ij_kotlin_continuation_indent_in_elvis = false 84 | 85 | ## Spaces 86 | # Before Parentheses 87 | ij_kotlin_space_before_if_parentheses = true 88 | ij_kotlin_space_before_for_parentheses = true 89 | ij_kotlin_space_before_while_parentheses = true 90 | ij_kotlin_space_before_catch_parentheses = true 91 | ij_kotlin_space_before_when_parentheses = true 92 | # Around Operators 93 | ij_kotlin_spaces_around_assignment_operators = true 94 | ij_kotlin_spaces_around_logical_operators = true 95 | ij_kotlin_spaces_around_equality_operators = true 96 | ij_kotlin_spaces_around_relational_operators = true 97 | ij_kotlin_spaces_around_additive_operators = true 98 | ij_kotlin_spaces_around_multiplicative_operators = true 99 | ij_kotlin_spaces_around_unary_operator = false 100 | ij_kotlin_spaces_around_range = false 101 | # Other 102 | ij_kotlin_space_before_comma = false 103 | ij_kotlin_space_after_comma = true 104 | ij_kotlin_space_before_type_colon = false 105 | ij_kotlin_space_after_type_colon = true 106 | ij_kotlin_space_after_extend_colon = true 107 | ij_kotlin_space_before_extend_colon = true 108 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true 109 | ij_kotlin_spaces_around_function_type_arrow = true 110 | ij_kotlin_spaces_around_when_arrow = true 111 | ij_kotlin_space_before_lambda_arrow = true 112 | 113 | ## Blank Lines 114 | # Keep Maximum Blank Lines 115 | ij_kotlin_keep_blank_lines_in_declarations = 1 116 | ij_kotlin_keep_blank_lines_in_code = 1 117 | ij_kotlin_keep_blank_lines_before_right_brace = 0 118 | # Minimum Blank Lines 119 | ij_kotlin_blank_lines_after_class_header = 0 120 | ij_kotlin_blank_lines_around_block_when_branches = 1 121 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 122 | 123 | ## Imports 124 | ij_kotlin_name_count_to_use_star_import = 5 125 | ij_kotlin_name_count_to_use_star_import_for_members = 3 126 | ij_kotlin_import_nested_classes = false 127 | ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.** 128 | ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ 129 | 130 | ## Other 131 | ij_kotlin_allow_trailing_comma = true 132 | ij_kotlin_allow_trailing_comma_on_call_site = true 133 | 134 | ## Code generation 135 | ij_kotlin_line_comment_at_first_column = true 136 | ij_kotlin_line_comment_add_space = false 137 | ij_kotlin_block_comment_at_first_column = true 138 | 139 | [*.md] 140 | trim_trailing_whitespace = false 141 | 142 | [{*.yaml, *.yml}] 143 | indent_size = 2 144 | ij_yaml_spaces_within_brackets = false 145 | ij_yaml_keep_indents_on_empty_lines = false 146 | ij_yaml_keep_line_breaks = true 147 | 148 | [{*.bash, *.sh, *.zsh}] 149 | indent_size = 2 150 | tab_width = 2 151 | 152 | [*.bat] 153 | end_of_line = crlf 154 | 155 | [*.properties] 156 | ij_properties_keep_blank_lines = true 157 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | check: 11 | name: Check and publish 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Java 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: 11 22 | distribution: temurin 23 | 24 | - name: Run Check 25 | uses: eskatos/gradle-command-action@v2 26 | with: 27 | arguments: detektMainAll check -xdetekt 28 | 29 | - name: Publish 30 | uses: eskatos/gradle-command-action@v2 31 | if: github.ref == 'refs/heads/main' && success() 32 | with: 33 | arguments: publish 34 | env: 35 | ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} 36 | ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ## JetBrains IDEs 4 | /.idea/** 5 | *.iml 6 | 7 | ## Gradle 8 | # Ignore Gradle project-specific cache directory 9 | .gradle 10 | 11 | # Ignore Gradle build output directory 12 | build 13 | 14 | # Ignore local properties 15 | local.properties 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [2.1] (2023-05-06) 4 | 5 | ### Reusable properties 6 | 7 | Reusable properties introduced to address the problem described in issue #14 8 | 9 | Reusable properties survive `MapMemory.clear()` call. It will not be removed from cache, but will be cleared instead. Reusable properties are especially useful for reactive types like `Flow`. You don't need to resubscribe to flow after MapMemory was cleared. 10 | 11 | ```kotlin 12 | val flow by memory.sharedFlow() 13 | 14 | // Subscribe to flow 15 | flow.onEach(::println).launchIn(coroutineScope) 16 | // And then clear MapMemory 17 | memory.crear() 18 | 19 | // Emitted value will be printed because the flow 20 | // is the same as before memory clear 21 | flow.emit(1) 22 | ``` 23 | 24 | You can create reusable property using operator `invoke` with `clear` lambda: 25 | 26 | ```kotlin 27 | class Counter { 28 | fun reset() = { /*...*/ } 29 | } 30 | 31 | val counter: Counter by memory(clear = { it.reset() }) { Counter() } 32 | ``` 33 | 34 | Many of default accessors are already turned into reusable: `mutableList`, `mutableMap`, `reactiveMutableMap`, `stateFlow`, `sharedFlow`, `behaviorSubject`, `publishSubject`. 35 | 36 | ### Changes 37 | 38 | - :warning: Deprecated functions removed: `MapMemory.nullable`, `MapMemory.reactiveMap`, `ReactiveMutableMap.getStream`, `ReactiveMutableMap.getAll`, `ReactiveMutableMap.getAllStream` 39 | - Added parameter `defaultValue` to most of reusable properties: `mutableList`, `mutableMap`, `reactiveMutableMap`, `behaviorSubject`. 40 | - :warning: **mapmemory-rxjava:** `ReplayStrategy` was removed. Please let us know if this change affected you: https://github.com/RedMadRobot/mapmemory/discussions/20 41 | 42 | ### Fixes 43 | 44 | - ReactiveMutableMap: initial empty map is not emitted (#15) 45 | 46 | ### Dependencies 47 | 48 | - **mapmemory:** Kotlin `1.4.30` → `1.8.20` 49 | - **mapmemory-coroutines:** kotlinx.coroutines `1.4.2` → `1.6.4` 50 | - **mapmemory-rxjava3:** RxJava `3.0.11` → `3.1.6` 51 | 52 | ### Housekeeping 53 | 54 | - infrastructure `0.8.2` → `0.18.1` 55 | - detekt `1.16.0` → `1.22.0` 56 | - Gradle `6.8.3` → `8.1.1` 57 | - binary-compatibility-validator `0.5.0` → `0.13.1` 58 | 59 | ## [2.0] (2021-05-01) 60 | 61 | ### ReactiveMap refactored 62 | 63 | `ReactiveMap` renamed to `ReactiveMutableMap`. 64 | There are two type parameters `K` (for keys), `V` (for values) instead of one `T` (for values), so you can use keys with a type different from `String`. 65 | 66 | Now `ReactiveMutableMap` implements interface `MutableMap`. 67 | Also, you can wrap `Map` with `ReactiveMutableMap`, using constructor. 68 | 69 | #### Naming changes 70 | 71 | > Old versions of methods are marked with `@Deprecated` to help migrate to new naming. 72 | 73 | Methods `getAll` replaced with field `values` to match `Map` interface. 74 | 75 | Word `stream` in methods names replaced with implementation-specific words to make API clearer. 76 | 77 | Coroutines: 78 | - `getStream` -> `getFlow` and `getValueFlow` 79 | - `getAllStream` -> `valuesFlow` 80 | - New field `flow` with the whole map 81 | 82 | RxJava: 83 | - `getStream` -> `getValueObservable` 84 | - `getAllStream` -> `valuesObservable` 85 | - New field `observable` with the whole map 86 | 87 | ### KAPT: 'IllegalStateException: Couldn't find declaration file' on delegate with inline getValue operator 88 | 89 | There is a bug in Kotlin Compiler that affects MapMemory if you create subclasses - [KT-46317](https://youtrack.jetbrains.com/issue/KT-46317). 90 | You can use module `mapmemory-kapt-bug-workaround` as a workaround: 91 | 92 | ```kotlin 93 | dependencies { 94 | implementation("com.redmadrobot.mapmemory:mapmemory-kapt-bug-workaround:[latest-version]") 95 | } 96 | ``` 97 | 98 | ```diff 99 | - val someValue: String by memory 100 | + val someValue: String by memory.value() 101 | ``` 102 | 103 | ## [2.0-rc1] (2021-03-14) 104 | 105 | ### Scoped and shared values (#1) 106 | 107 | Now there are two types of memory values: scoped (to class) and shared. 108 | 109 | All memory values by default are scoped to the class where it's declared. 110 | Scoping prevents from unintended sharing of properties with the same name between classes. 111 | This snippet demonstrates the problem: 112 | 113 | ```kotlin 114 | package com.example 115 | 116 | class StringsStorage(memory: MapMemory) { 117 | var values: MutableList by memory.list() 118 | } 119 | 120 | class IntsStorage(memory: MapMemory) { 121 | var values: MutableList by memory.list() // The same field name as in StringsStorage 122 | } 123 | 124 | val strings = StringsStorage(memory) 125 | val ints = IntsStorage(memory) 126 | 127 | strings.values.add("A") 128 | ints.values.add(1) 129 | 130 | println(memory) 131 | ``` 132 | 133 | Output: 134 | 135 | ``` 136 | For unscoped fields (old behaviour): 137 | {values: [A, 1]} 138 | 139 | For scoped fields (new behavior): 140 | {com.example.StringsStorage#values: [A], com.example.IntsStorage#values: [1]} 141 | ``` 142 | 143 | You can make memory field **shared** using extension `shared(key: String)`: 144 | 145 | ```kotlin 146 | // It is recommended to create constants for shared properties keys 147 | const val KEY_SERVER_HOST = "serverHost" 148 | 149 | class ServerConfig(memory: MapMemory) { 150 | var host: String by memory.shared(KEY_SERVER_HOST) 151 | } 152 | 153 | class DebugPanelConfig(memory: MapMemory) { 154 | var serverHost: String by memory.shared(KEY_SERVER_HOST) 155 | } 156 | ``` 157 | 158 | ### Removed `.nullable()` and `.withDefault { ... }` 159 | 160 | Accessor `nullable()` is not needed now. 161 | You can just declare a nullable field: 162 | ```diff 163 | -val selectedOption: String? by memory.nullable() 164 | +val selectedOption: String? by memory 165 | ``` 166 | 167 | `withDefault` is no more compatible with MapMemory, so you should use the operator `invoke` instead: 168 | ```diff 169 | -var counter: Int by memory.withDefault { 0 } 170 | +var counter: Int by memory { 0 } 171 | ``` 172 | 173 | ### Mutable collections accessors 174 | > **BREAKING CHANGE** 175 | 176 | Now accessors `map` and `list` return delegates to access immutable collections. 177 | You should use `mutableMap` and `mutableList` for mutable versions of collections. 178 | 179 | ### Added 180 | 181 | - Copying constructor for `MapMemory`. 182 | Now you can initialize memory with specified content on creation. 183 | - New module `mapmemory-test`. 184 | Contains utilities helping to test code that uses `MapMemory`. 185 | - New module `mapmemory-rxjava3` with accessors for RxJava3. 186 | 187 | ### Changed 188 | 189 | - `MapMemory` now is `MutableMap` instead of `MutableMap`. 190 | It is made to prevent NPEs because `ConcurrentHashMap` not supports nullable values. 191 | You still can store nullable values in memory using a delegate. 192 | 193 | ## [1.1] (2021-02-26) 194 | 195 | ### Added 196 | 197 | - Method `ReactiveMap.getValue` (#2) 198 | 199 | ### Changed 200 | 201 | - ReactiveMap constructor now is public (#4) 202 | 203 | ### Housekeeping 204 | 205 | - Updated Kotlin to 1.4.30 206 | - Updated Gradle to 6.8 207 | - Updated Detekt to 1.15.0 208 | - Migrated from JCenter to Maven Central (#5) 209 | 210 | ## 1.0 211 | 212 | Public release 213 | 214 | [unreleased]: https://github.com/RedMadRobot/mapmemory/compare/v2.1...main 215 | [2.1]: https://github.com/RedMadRobot/mapmemory/compare/v2.0...v2.1 216 | [2.0]: https://github.com/RedMadRobot/mapmemory/compare/v2.0-rc1..v2.0 217 | [2.0-rc1]: https://github.com/RedMadRobot/mapmemory/compare/v1.1...v2.0-rc1 218 | [1.1]: https://github.com/RedMadRobot/mapmemory/compare/v1.0...v1.1 219 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redmadrobot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MapMemory 2 | 3 | [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.mapmemory/mapmemory?style=flat-square)][mavenCentral] 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/RedMadRobot/mapmemory/main.yml?branch=main&style=flat-square)][ci] 5 | [![License](https://img.shields.io/github/license/RedMadRobot/mapmemory?style=flat-square)][license] 6 | 7 | Simple in-memory cache conception built on `Map`. 8 | 9 | --- 10 | 11 | 12 | 13 | - [Installation](#installation) 14 | - [Conception](#conception) 15 | - [Usage](#usage) 16 | - [Collections](#collections) 17 | - [Reusable properties](#reusable-properties) 18 | - [Scoped and Shared values](#scoped-and-shared-values) 19 | - [Reactive Style](#reactive-style) 20 | - [Advanced usage](#advanced-usage) 21 | - [MapMemory Lifetime](#mapmemory-lifetime) 22 | - [Testing](#testing) 23 | - [Migration Guide](#migration-guide) 24 | - [Upgrading from v1.1](#upgrading-from-v11) 25 | - [Contributing](#contributing) 26 | - [License](#license) 27 | 28 | 29 | 30 | ### Installation 31 | 32 | Add dependencies: 33 | 34 | ```kotlin 35 | repositories { 36 | mavenCentral() 37 | } 38 | 39 | dependencies { 40 | implementation("com.redmadrobot.mapmemory:mapmemory:2.1") 41 | 42 | // or if you want to work with MapMemory in reactive style, add one of 43 | implementation("com.redmadrobot.mapmemory:mapmemory-coroutines:2.1") 44 | implementation("com.redmadrobot.mapmemory:mapmemory-rxjava2:2.1") 45 | implementation("com.redmadrobot.mapmemory:mapmemory-rxjava3:2.1") 46 | 47 | // if you want to test code that uses MapMemory 48 | testImplementation("com.redmadrobot.mapmemory:mapmemory-test:2.1") 49 | } 50 | ``` 51 | 52 | ### Conception 53 | 54 | Kotlin provides delegates to access values in a map: 55 | 56 | ```kotlin 57 | val map = mapOf("answer" to 42) 58 | val answer: Int by map 59 | println(answer) // 42 60 | ``` 61 | 62 | This library uses this idea to implement in-memory storage. 63 | 64 | There are two simple principles: 65 | 66 | - **MapMemory** is a singleton, and it is shared between many consumers 67 | - **MapMemory** holds data but doesn't know **what** data it holds 68 | 69 | ### Usage 70 | 71 | >[!TIP] 72 | > 73 | >If you use any kind of DI framework, you should provide `MapMemory` with the desired scope. 74 | > For example, if you want your data to live forever, use singleton scope: 75 | > 76 | > ```kotlin 77 | > @Provides 78 | > @Singleton 79 | > fun provideMapMemory(): MapMemory = MapMemory() 80 | > ``` 81 | > 82 | > If you don't use any DI framework, you should take care of the `MapMemory` lifetime. 83 | 84 | Imagine, you have `UsersRepository` used to get users' information from API. 85 | You want to remember the last requested user. 86 | Let's store it in a `MapMemory`: 87 | 88 | ```kotlin 89 | class UsersRepository( 90 | private val api: Api, 91 | memory: MapMemory, // (1) Inject MapMemory into the constructor 92 | ) { 93 | 94 | var lastUser: User? by memory // (2) Declare in-memory property using delegate 95 | private set 96 | 97 | suspend fun getUser(email: String): User { 98 | return api.getUser(email) 99 | .also { lastUser = it } // (3) Use the property 100 | } 101 | } 102 | ``` 103 | 104 | `MapMemory` is a singleton, but `UsersRepository` is not. 105 | Property `lastUser` is tied to `MapMemory` lifetime, so it will survive `UsersRepository` recreation. 106 | 107 | You can specify the default value that will be used when the value you're trying to read is not set. 108 | For example, we don't want a nullable `User`, but want to get placeholder object `User.EMPTY` instead: 109 | 110 | ```kotlin 111 | var lastUser: User by memory { User.EMPTY } 112 | ``` 113 | 114 | #### Collections 115 | 116 | You can write the following code to store a mutable list in `MapMemory`: 117 | 118 | ```kotlin 119 | val users: MutableList by memory { mutableListOf() } 120 | ``` 121 | 122 | Boilerplate. 123 | Fortunately, there are shorthand accessors to store lists and maps: 124 | 125 | ```kotlin 126 | val users by memory.mutableList() 127 | ``` 128 | 129 | Accessors `mutableList` and `mutableMap` use concurrent collections under the hood. 130 | 131 | | Accessor | Default value | Description | 132 | |-----------------|--------------------|-----------------------| 133 | | `map()` | Empty map | Store map | 134 | | `mutableMap()` | Empty mutable map | Store values in map | 135 | | `list()` | Empty list | Store list | 136 | | `mutableList()` | Empty mutable list | Store values in list | 137 | 138 | Feel free to create your accessors if needed. 139 | 140 | #### Reusable properties 141 | 142 | If you don't want some value to be removed from memory on [MapMemory.clear] and want to clear the value instead, you can create a reusable property. 143 | Such properties use the given `clear` lambda to clear the current value. 144 | 145 | ```kotlin 146 | class Counter { 147 | fun reset() { /*...*/ 148 | } 149 | } 150 | 151 | val counter: Counter by memory(clear = { it.reset() }) { Counter() } 152 | ``` 153 | 154 | Reusable properties are especially useful for reactive types like `Flow` because you don't need to re-subscribe to `Flow` after `MapMemory` is cleared. 155 | 156 | > [!NOTE] 157 | > 158 | > Many of the default accessors already return reusable properties. 159 | > See the accessor's description to check if it returns reusable property. 160 | 161 | #### Scoped and Shared values 162 | 163 | Let's look at how MapMemory works under the hood. 164 | We have a class with an in-memory property declared using delegate: 165 | 166 | ```kotlin 167 | package com.example 168 | 169 | class TokenStorage(memory: MapMemory) { 170 | var authToken: String by memory 171 | } 172 | ``` 173 | 174 | `MapMemory` is `MutableMap`. 175 | Delegate accesses map value by a key retrieved from the property name. 176 | This behavior differs for two types of in-memory property delegates: 177 | - **Scoped** to the class where the property is declared. 178 | Property key is a combination of class and property name: `com.example.TokenStorage#authToken` 179 | - **Shared** between all classes by the specified key. 180 | All properties are scoped by default, you can share it with the function `shared`. 181 | 182 | Property `authToken` is scoped to class `TokenStorage`, but we can share it: 183 | 184 | ```kotlin 185 | // It is a good practice to declare constants for shared keys. 186 | const val KEY_AUTH_TOKEN = "authToken" 187 | 188 | class TokenStorage(memory: MapMemory) { 189 | var authToken: String by memory.shared(KEY_AUTH_TOKEN) 190 | } 191 | 192 | class Authenticator(memory: MapMemory) { 193 | // Property name may be different 194 | var savedToken: String by memory.shared(KEY_AUTH_TOKEN) 195 | } 196 | ``` 197 | 198 | Both `TokenStorage` and `Authenticator` will use the same value. 199 | 200 | > [!Warning] 201 | > 202 | > Keep in mind that this is just an example. 203 | > In real code, it may be more reasonable to inject `TokenStorage` into `Authenticator` instead of sharing in-memory property by key. 204 | 205 | #### Reactive Style 206 | 207 | Reactive subscription to values is useful to keep data shared between several screens up to date. 208 | 209 | To use MapMemory in reactive style, replace dependency `mapmemory` with one of the following: 210 | 211 | - `mapmemory-coroutines` 212 | - `mapmemory-rxjava2` 213 | - `mapmemory-rxjava3` 214 | 215 | These modules provide accessors for reactive types: 216 | 217 | ```kotlin 218 | // with coroutines 219 | val selectedOption: MutableStateFlow