mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
364 Commits
Author | SHA1 | Date | |
---|---|---|---|
a24a335743 | |||
ef9af71960 | |||
925702d315 | |||
d50dffec8c | |||
cef2081a13 | |||
06c8bde7c9 | |||
c9bbfa3820 | |||
eed7cfdc42 | |||
bd9b0d16ab | |||
ea6c33b497 | |||
dc80ade2fb | |||
f6a06ee8ea | |||
2644f27975 | |||
3dc68a7b8b | |||
97fc1d6239 | |||
662f4d22a3 | |||
b70aa12be9 | |||
71f12f5f19 | |||
e10504eeeb | |||
2dea9f3bc0 | |||
35c9dda5bc | |||
e831f3949a | |||
b0b39cc693 | |||
fc03be3f73 | |||
b61f6b81f1 | |||
f5bc1c1fce | |||
a729f9568c | |||
5749e00377 | |||
ef73c24a0c | |||
94717ee351 | |||
9a18ded65b | |||
b23220f491 | |||
6e6bb03246 | |||
1ae6bae3b8 | |||
1239ca3256 | |||
57b7797ea4 | |||
5ee5bfd1d5 | |||
7229a3e198 | |||
bee083582f | |||
9d7f99f286 | |||
6ef403853c | |||
6ae7ccb9a1 | |||
dafc50c463 | |||
e89e2c931d | |||
43a67b99e4 | |||
46c48f4f31 | |||
bf0fe85aa6 | |||
42c5bd3a7f | |||
d170e86c8a | |||
e3078169b1 | |||
a33ad123f6 | |||
7e14fa2f5c | |||
ba698b41e1 | |||
e76215987e | |||
d1a247af8c | |||
2b7e9534f3 | |||
38521558a1 | |||
100f3d214b | |||
1309867611 | |||
611f64f2e1 | |||
f118ebce6e | |||
59fc90e556 | |||
fb9e4d57fb | |||
960c38b696 | |||
39895e58a6 | |||
b420d85be5 | |||
19ea2f340a | |||
11b0d059bf | |||
c8a25ce544 | |||
509583ea2e | |||
1c86f3f4bf | |||
6d999be590 | |||
e715772dbf | |||
63eb7b7ea8 | |||
b07683b815 | |||
96e97d1691 | |||
261d8827e3 | |||
c3156f2e41 | |||
8c08801460 | |||
aaf1299da7 | |||
a411355b4f | |||
eba41066b4 | |||
f295dff8a2 | |||
a16815143c | |||
6ff3f6ae42 | |||
84071881af | |||
7cccf7e56e | |||
2516d5e381 | |||
cdec8bac75 | |||
fa30aae194 | |||
eb959a3135 | |||
24033e0cac | |||
71f9a505e0 | |||
979b8f017b | |||
af78f01682 | |||
0b16d5c826 | |||
597e14bc7e | |||
04a95867e2 | |||
e0d5eb45b7 | |||
b90cab318e | |||
3252b61abe | |||
2a2da21ff3 | |||
04ef371337 | |||
623e0cd369 | |||
1f466747f0 | |||
2215462f99 | |||
ac4c0a2e4c | |||
f7496db5ac | |||
3028fe975d | |||
23a5034493 | |||
65e339f811 | |||
2020e48659 | |||
9566d6f81f | |||
a00d734712 | |||
27a3e8706a | |||
e601efcfc0 | |||
2bfad9f885 | |||
e78e984943 | |||
242f4b02d0 | |||
041be5a1d1 | |||
976ce056c1 | |||
00c23c73a8 | |||
9dd1848337 | |||
9b30efd9a2 | |||
5853f7cc49 | |||
7b00a06f3e | |||
9ef9be0f37 | |||
13ca419473 | |||
b80f1a0773 | |||
e85101c74e | |||
90668bdf63 | |||
e1a00079a5 | |||
e3add4df42 | |||
ced1a3bccb | |||
8d13a14343 | |||
7238e1ea8a | |||
bd0423f243 | |||
b0441c134c | |||
5860901c30 | |||
df4eaea4b9 | |||
e9223d5502 | |||
702c5a3e5d | |||
ccbed95cdc | |||
c4e2c06cf5 | |||
a6135738a3 | |||
ad1ea985b8 | |||
2058950d07 | |||
4dc27f4489 | |||
122daa3220 | |||
e30928e23d | |||
30292306bb | |||
2f9aa585f1 | |||
9e02c3e5ff | |||
7f813a519b | |||
b5072486b4 | |||
50c1cd8215 | |||
ed4812e6d8 | |||
59e0e751f1 | |||
a5ae5e6c2b | |||
a2b87e63c9 | |||
d95c283653 | |||
6a9ae0c148 | |||
040dd517d8 | |||
9663c1ca64 | |||
ffc2d23be7 | |||
49b009e59b | |||
778b6a555b | |||
b3730998e9 | |||
837758aebe | |||
6ac4149aa1 | |||
278584ae6a | |||
126f9d5f41 | |||
ce15ff4e0a | |||
382b956beb | |||
36deab4909 | |||
550fc59d9d | |||
9100a57458 | |||
74d9bbccd9 | |||
75e602a349 | |||
e73644db10 | |||
148e6bdae7 | |||
1b540199f0 | |||
30b70e9984 | |||
c1557cff27 | |||
2d662f91b3 | |||
c4a08e52e5 | |||
08c371c142 | |||
8e62dd460c | |||
1f9302dc94 | |||
16f445f699 | |||
b4abd564ec | |||
14ffafb0a7 | |||
e0cc780887 | |||
ab7d277167 | |||
8308c1df4d | |||
12c29f5180 | |||
2963098870 | |||
9f56b0a26d | |||
75851312fd | |||
3a3be138a5 | |||
fb7d1f18b0 | |||
d1c6c7696a | |||
5f38d9635d | |||
8185ea87b1 | |||
98bd07d025 | |||
c502c70a21 | |||
d933dc532b | |||
42594f0656 | |||
4b83ca19c3 | |||
bab13f5e83 | |||
14c5f5a26c | |||
b26a4f24d4 | |||
498ec673dc | |||
5c67ab6aec | |||
f78359b5d7 | |||
dbce612cb2 | |||
4322ffdb0a | |||
123b848d74 | |||
b120fbd2b1 | |||
d41c3c2de4 | |||
a3cabd7a9e | |||
3dde486126 | |||
5978122dda | |||
4b4806ce34 | |||
987c6cc709 | |||
012c7e9bdf | |||
d06bb265e5 | |||
e2fb8bf21e | |||
60dea2a518 | |||
3877f49278 | |||
a299a5b505 | |||
a03f7201d2 | |||
94e26ee8a0 | |||
2d8ad47083 | |||
e9aec238a1 | |||
844c33166c | |||
cfe9f2159f | |||
61bccd5f48 | |||
e5a608a315 | |||
9b4f35eea1 | |||
602ae2385b | |||
48a4246aac | |||
a4020cb484 | |||
d074f29b82 | |||
a67bef6853 | |||
14e666f18d | |||
cce914091c | |||
d7cd3db8e2 | |||
0d8f844314 | |||
abd0cb2031 | |||
79ef03ed0c | |||
a3087cb650 | |||
9acb9af338 | |||
0c9283eb87 | |||
493d838201 | |||
c0523469fa | |||
4f00eaa6d4 | |||
9706524572 | |||
3174b84367 | |||
6cccd5ff6c | |||
972857268d | |||
f3cd92cc5e | |||
502a49644c | |||
51719b5868 | |||
17821bd094 | |||
6a89ffaa8a | |||
d5a8d0f4d4 | |||
9739bd871e | |||
632d2545d4 | |||
ecfa273a81 | |||
a36828116e | |||
14aa9ca26c | |||
069e51f2ff | |||
a15cbdfb1a | |||
4af8114eda | |||
90dc84e900 | |||
67c595b440 | |||
830b7aee56 | |||
1890608cb3 | |||
bd396959a9 | |||
d5fe19f0a5 | |||
46bfb09415 | |||
a60cb596d1 | |||
6f9d5e2d5f | |||
80bc226ee1 | |||
12e37184e1 | |||
25e9345d02 | |||
ccc4d030c3 | |||
90c0817b6d | |||
527f7bbafe | |||
765a32729f | |||
de783f77a2 | |||
de4c8d104c | |||
88c8c28f45 | |||
57b36826d1 | |||
f81a2f309b | |||
5fc760f4a5 | |||
091cb38339 | |||
3ae9b3e576 | |||
b03b4cbeec | |||
f2c1b3c76a | |||
f3bec34882 | |||
d6aa9fe9c2 | |||
2d5304a770 | |||
88f2c16c82 | |||
490c318d1c | |||
8beaf61a08 | |||
8b61c984eb | |||
e38094df58 | |||
c25e3f5867 | |||
f78e81d175 | |||
3837ae237d | |||
2b6ef8b4ff | |||
6cc0eefb3e | |||
ab11e28bf7 | |||
26d5f5a5f5 | |||
74ae91cba6 | |||
70509c7edd | |||
5a69bd6c63 | |||
091bb1394f | |||
b82c3864a0 | |||
49ee38a936 | |||
c201866c51 | |||
023f841fb5 | |||
76102e9ab3 | |||
b2fc5e2a4d | |||
55aacb8753 | |||
8702846216 | |||
3347a6d189 | |||
47b3b42949 | |||
e985631621 | |||
e15034bfa4 | |||
e5dbcd25dd | |||
3714c02c12 | |||
fa636b4146 | |||
3d825aebc3 | |||
629884a396 | |||
e7a0fa4e8f | |||
1a41f37a9d | |||
07a65e0bb5 | |||
615f7f99c3 | |||
3de5558ed4 | |||
6bbe3a271f | |||
8bee29f683 | |||
c40f0fdcb9 | |||
30d1453a12 | |||
75fa88b00d | |||
0f817ad212 | |||
d7b46ae0d4 | |||
b6be14ecca | |||
33dbfc6f69 | |||
fcacdcd544 | |||
dd2fc5a86f | |||
0f8a6f6bde | |||
1efd94181d | |||
71ff0232aa | |||
63921cd984 | |||
051e03bed3 | |||
a051394f4f | |||
b872babe45 | |||
5a9cabc4bd | |||
3ba630684a | |||
498cd12f94 | |||
062848f2e4 |
12
.github/workflows/build.yml
vendored
Normal file
12
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Regular build
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build
|
21
.github/workflows/dokka_push.yml
vendored
Normal file
21
.github/workflows/dokka_push.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Publish KDocs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
publishing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew dokkaHtml
|
||||||
|
- name: Publish KDocs
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./dokka/build/dokka/html
|
||||||
|
publish_branch: kdocs
|
24
.github/workflows/packages_push.yml
vendored
Normal file
24
.github/workflows/packages_push.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
name: Publish package to GitHub Packages
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
publishing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Rewrite version
|
||||||
|
run: |
|
||||||
|
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||||
|
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||||
|
rm gradle.properties
|
||||||
|
mv gradle.properties.tmp gradle.properties
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build
|
||||||
|
- name: Publish
|
||||||
|
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||||
|
env:
|
||||||
|
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||||
|
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
8
.space.kts
Normal file
8
.space.kts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
job("Build and run tests") {
|
||||||
|
container(displayName = "Run gradle build", image = "openjdk:11") {
|
||||||
|
kotlinScript { api ->
|
||||||
|
// here can be your complex logic
|
||||||
|
api.gradlew("build")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
.travis.yml
26
.travis.yml
@@ -1,26 +0,0 @@
|
|||||||
language: android
|
|
||||||
install: true
|
|
||||||
|
|
||||||
os: linux
|
|
||||||
dist: trusty
|
|
||||||
jdk: oraclejdk8
|
|
||||||
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- tools
|
|
||||||
- platform-tools
|
|
||||||
- build-tools-30.0.2
|
|
||||||
- android-30
|
|
||||||
- add-on
|
|
||||||
- extra
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2"
|
|
||||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: build
|
|
||||||
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
|
||||||
- state: test
|
|
||||||
script: ./gradlew allTests
|
|
576
CHANGELOG.md
576
CHANGELOG.md
@@ -1,5 +1,579 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.5.25
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `UUID`: `0.3.0` -> `0.3.1`
|
||||||
|
* `Common`:
|
||||||
|
* New property `MPPFile#withoutSlashAtTheEnd`
|
||||||
|
* Extension `clamp` has been deprecated
|
||||||
|
* New extension `Iterable#diff`
|
||||||
|
* `Serialization`:
|
||||||
|
* New operators `TypedSerializer#plusAssign` and `TypedSerializer#minusAssign`
|
||||||
|
|
||||||
|
## 0.5.24
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.5.1` -> `1.5.2`
|
||||||
|
* `Klock`: `2.3.4` -> `2.4.1`
|
||||||
|
* `Coroutines`:
|
||||||
|
* New function `CoroutineScope` with safely exceptions handler as second parameter
|
||||||
|
|
||||||
|
## 0.5.23
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.33.1` -> `0.34.1`
|
||||||
|
* `Common`:
|
||||||
|
* New extensions `Iterable#joinTo` and `Array#joinTo`
|
||||||
|
|
||||||
|
## 0.5.22
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Kotlin`: `1.5.21` -> `1.5.30`
|
||||||
|
* `Klock`: `2.3.2` -> `2.3.4`
|
||||||
|
* `AppCompat`: `1.3.0` -> `1.3.1`
|
||||||
|
* `Ktor`: `1.6.2` -> `1.6.3`
|
||||||
|
|
||||||
|
## 0.5.21
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Klock`: `2.3.1` -> `2.3.2`
|
||||||
|
* `Serialization`
|
||||||
|
* `Typed Serializer`:
|
||||||
|
* `TypedSerializer` Descriptor serial name has been fixed
|
||||||
|
|
||||||
|
## 0.5.20
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`
|
||||||
|
* `Android`:
|
||||||
|
* `*OrNull` analogs of `Cursor.get*(String)` extensions have been added
|
||||||
|
* Extensions `Cursor.getFloat` and `Cursor.getFloatOrNull` have been added
|
||||||
|
|
||||||
|
## 0.5.19
|
||||||
|
|
||||||
|
* `LanguageCode`:
|
||||||
|
* `IetfLanguageCode` became as sealed class
|
||||||
|
* `IetfLanguageCode` now override `toString` and returns its code
|
||||||
|
|
||||||
|
## 0.5.18
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Kotlin Exposed`: `0.32.1` -> `0.33.1`
|
||||||
|
* `LanguageCode`:
|
||||||
|
* Module has been created
|
||||||
|
|
||||||
|
## 0.5.17
|
||||||
|
|
||||||
|
**SINCE THIS UPDATE JS PARTS WILL BE COMPILED WITH IR COMPILER ONLY**
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Kotlin`: `1.5.20` -> `1.5.21`
|
||||||
|
* `Ktor`: `1.6.1` -> `1.6.2`
|
||||||
|
* `Klock`: `2.2.0` -> `2.3.1`
|
||||||
|
* `CryptoJS`: `4.0.0` -> `4.1.1`
|
||||||
|
|
||||||
|
## 0.5.16
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Coroutines`: `1.5.0` -> `1.5.1`
|
||||||
|
* `Serialization`: `1.2.1` -> `1.2.2`
|
||||||
|
* `Ktor`: `1.6.0` -> `1.6.1`
|
||||||
|
* `Klock`: `2.1.2` -> `2.2.0`
|
||||||
|
* `Core KTX`: `1.5.0` -> `1.6.0`
|
||||||
|
|
||||||
|
## 0.5.15 HOTFIX FOR 0.5.14
|
||||||
|
|
||||||
|
* `Coroutines`
|
||||||
|
* Fixes in `subscribeAsync`
|
||||||
|
|
||||||
|
## 0.5.14 NOT RECOMMENDED
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Kotlin`: `1.5.10` -> `1.5.20`
|
||||||
|
* `Coroutines`
|
||||||
|
* `subscribeSafelyWithoutExceptions` got new parameter `onException` by analogue with `safelyWithoutExceptions`
|
||||||
|
* New extensions `Flow#subscribeAsync` and subsequent analogs of `subscribe` with opportunity to set up custom marker
|
||||||
|
|
||||||
|
## 0.5.13
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add functionality for multiplatform working with files:
|
||||||
|
* Main class for files `MPPFile`
|
||||||
|
* Inline class for filenames work encapsulation `FileName`
|
||||||
|
* `FSM`
|
||||||
|
* Module inited and in preview state
|
||||||
|
|
||||||
|
## 0.5.12
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* `Android`
|
||||||
|
* Extension `View#changeVisibility` has been fixed
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`
|
||||||
|
* Default adapter got `dataCountFlow` property
|
||||||
|
* New subtype of adapter based on `StateFlow`: `StateFlowBasedRecyclerViewAdapter`
|
||||||
|
|
||||||
|
## 0.5.11
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* Fixes in `WriteOneToManyRepo#add`
|
||||||
|
* `Exposed`:
|
||||||
|
* Fixes in `ExposedOneToManyKeyValueRepo#add`
|
||||||
|
|
||||||
|
## 0.5.10
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Core KTX`: `1.3.2` -> `1.5.0`
|
||||||
|
* `AndroidX Recycler`: `1.2.0` -> `1.2.1`
|
||||||
|
* `AppCompat`: `1.2.0` -> `1.3.0`
|
||||||
|
* `Android`
|
||||||
|
* `RecyclerView`:
|
||||||
|
* `data` of `RecyclerViewAdapter` became an abstract field
|
||||||
|
* New function `RecyclerViewAdapter`
|
||||||
|
* `Common`:
|
||||||
|
* New extension `View#changeVisibility`
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`:
|
||||||
|
* `WriteOneToManyRepo` got new function `clearWithValue`
|
||||||
|
* `Android`:
|
||||||
|
* New extension `SQLiteDatabase#selectDistinct`
|
||||||
|
* Fixes in `OneToManyAndroidRepo`
|
||||||
|
* `Ktor`
|
||||||
|
* `Server`
|
||||||
|
* All elements in configurators became a `fun interface`
|
||||||
|
* `Pagination`
|
||||||
|
* New function `doForAllWithCurrentPaging`
|
||||||
|
|
||||||
|
## 0.5.9
|
||||||
|
|
||||||
|
* `Repos`
|
||||||
|
* `Common`
|
||||||
|
* `OneToManyAndroidRepo` got new primary constructor
|
||||||
|
|
||||||
|
## 0.5.8
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* New extension `Iterable#firstNotNull`
|
||||||
|
* `Coroutines`
|
||||||
|
* New extension `Flow#firstNotNull`
|
||||||
|
* New extensions `CoroutineContext#LinkedSupervisorJob`, `CoroutineScope#LinkedSupervisorJob` and
|
||||||
|
`CoroutineScope#LinkedSupervisorScope`
|
||||||
|
|
||||||
|
## 0.5.7
|
||||||
|
|
||||||
|
* `Pagination`
|
||||||
|
* `Ktor`
|
||||||
|
* `Server`
|
||||||
|
* Fixes in extension `extractPagination`
|
||||||
|
* `Repos`
|
||||||
|
* `Cache`
|
||||||
|
* All standard cache repos have been separated to read and read/write repos
|
||||||
|
|
||||||
|
## 0.5.6
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Exposed`: `0.31.1` -> `0.32.1`
|
||||||
|
* `Coroutines`
|
||||||
|
* `JVM`
|
||||||
|
* `launchSynchronously` and subsequent functions got improved mechanism
|
||||||
|
* New method `safelyWithResult`
|
||||||
|
|
||||||
|
## 0.5.5
|
||||||
|
|
||||||
|
* `Versions`
|
||||||
|
* `Ktor`: `1.5.4` -> `1.6.0`
|
||||||
|
|
||||||
|
## 0.5.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.1.0` -> `2.1.2`
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.5.0` -> `1.5.10`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Extensions `doInUI` and `doInDefault` were replaced in common and available on any supported platform
|
||||||
|
* Extension `doInIO` replaced into `jvm` and available on any `JVM` platform
|
||||||
|
* Old extension `safelyWithouException` without `onException` has been replaced by its copy with `onException` and
|
||||||
|
default value
|
||||||
|
* New value `defaultSafelyWithoutExceptionHandlerWithNull` which is used in all `*WithoutExceptions` by default
|
||||||
|
* Analogs of `launch` and `async` for `safely` and `safelyWithoutExceptions` were added
|
||||||
|
* Analogs of `runCatching` for `safely` and `safelyWithoutExceptions` were added
|
||||||
|
|
||||||
|
## 0.5.2
|
||||||
|
|
||||||
|
* `Ktor`:
|
||||||
|
* `Client`:
|
||||||
|
* Fixes in `UnifiedRequester`
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin Serialization`: `1.2.0` -> `1.2.1`
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
**Notice**: This version is still depend on Kotlin
|
||||||
|
Exposed 0.31.1. That means that this version
|
||||||
|
may work improperly in modules based on Kotlin
|
||||||
|
Exposed
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin Exposed`: `0.30.2` -> `0.31.1`
|
||||||
|
* `Kotlin Coroutines`: `1.4.3` -> `1.5.0`
|
||||||
|
* `RecyclerView`: `1.1.0` -> `1.2.0`
|
||||||
|
* `Ktor`: `1.5.3` -> `1.5.4`
|
||||||
|
* `Klock`: `2.0.7` -> `2.1.0`
|
||||||
|
* `UUID`: `0.2.4` -> `0.3.0`
|
||||||
|
* **ALL DEPRECATIONS WERE REMOVED**
|
||||||
|
* `Android`:
|
||||||
|
* `Alerts`:
|
||||||
|
* `RecyclerView`:
|
||||||
|
* Classes `ActionViewHolder` and `ActionsRecyclerViewAdapter` became public
|
||||||
|
* `Coroutines`:
|
||||||
|
* New extension and function `doSynchronously` which are the same as `launchSynchronously`
|
||||||
|
* New extensions `doInDefault` and `doInIO`
|
||||||
|
|
||||||
|
## 0.4.36
|
||||||
|
|
||||||
|
* All `Android` targets inside common mpp modules now includes JVM code
|
||||||
|
|
||||||
|
## 0.4.35
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin Exposed`: `0.30.1` -> `0.30.2`
|
||||||
|
* `Serialization`:
|
||||||
|
* `TypedSerializer`:
|
||||||
|
* Project has been inited
|
||||||
|
|
||||||
|
## 0.4.34
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `uuid`: `0.2.3` -> `0.2.4`
|
||||||
|
* `Repos`:
|
||||||
|
* `AbstractExposedCRUDRepo` now implements `StandardCRUDRepo`
|
||||||
|
* `AbstractMutableAndroidCRUDRepo` now implements `StandardCRUDRepo`
|
||||||
|
|
||||||
|
## 0.4.33
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `1.5.2` -> `1.5.3`
|
||||||
|
* `Coroutines`
|
||||||
|
* Add `WeakJob` workaround:
|
||||||
|
* `CoroutineScope#weakLaunch`
|
||||||
|
* `CoroutineScope#weakAsync`
|
||||||
|
|
||||||
|
## 0.4.32
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin Exposed`: `0.29.1` -> `0.30.1`
|
||||||
|
|
||||||
|
## 0.4.31
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.31` -> `1.4.32`
|
||||||
|
* `Pagination`:
|
||||||
|
* New extensions `PaginationResult.changeResultsUnchecked` and `PaginationResult.changeResults` for mapping results
|
||||||
|
with the same parameters, but different data
|
||||||
|
* Extension `PaginationResult.thisPageIfNotEmpty` now is typed and will return `PaginationResult?` with the same
|
||||||
|
generic type as income `PaginationResult`
|
||||||
|
* New extension `PaginationResult.currentPageIfNotEmpty` - shortcut for `PaginationResult.thisPageIfNotEmpty`
|
||||||
|
* New common functions. They were created as replacements for currently available for more comfortable work
|
||||||
|
with repos pagination:
|
||||||
|
* `doForAll`
|
||||||
|
* `doForAllWithNextPaging`
|
||||||
|
* `doForAllWithCurrentPaging`
|
||||||
|
* `getAll`
|
||||||
|
* `getAllWithNextPaging`
|
||||||
|
* `getAllWithCurrentPaging`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Rewrite `subscribeSafelyWithoutExceptions`
|
||||||
|
* Now `subscribeSafelyWithoutExceptions` will use default handler instead of skipping
|
||||||
|
* New extension `subscribeSafelySkippingExceptions`
|
||||||
|
* `Repos`
|
||||||
|
* New subproject `repos.cache` - this subproject will contain repos with data caching mechanisms
|
||||||
|
* Most old `doForAll` methods have been deprecated
|
||||||
|
|
||||||
|
## 0.4.30
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.0.6` -> `2.0.7`
|
||||||
|
* `Pagination`:
|
||||||
|
* New variable `defaultPaginationPageSize` has been added to be able to change default pagination size
|
||||||
|
* Add new value `firstPageWithOneElementPagination`
|
||||||
|
|
||||||
|
## 0.4.29
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.4.2` -> `1.4.3`
|
||||||
|
* `Repos`:
|
||||||
|
* `Common`
|
||||||
|
* `Android`:
|
||||||
|
* New `blockingReadableTransaction`/`blockingWritableTransaction`
|
||||||
|
* Android databases realizations now use blocking transactions where it is possible
|
||||||
|
* Several improvements in transactions work
|
||||||
|
|
||||||
|
## 0.4.28
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.30` -> `1.4.31`
|
||||||
|
* `Ktor`: `1.5.1` -> `1.5.2`
|
||||||
|
* `Coroutines`
|
||||||
|
* Add `createActionsActor`/`createSafeActionsActor` and `doWithSuspending`
|
||||||
|
|
||||||
|
## 0.4.27
|
||||||
|
|
||||||
|
* `Repos`
|
||||||
|
* `Exposed`
|
||||||
|
* Fix in `AbstractExposedWriteCRUDRepo`
|
||||||
|
|
||||||
|
## 0.4.26
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Serialization`: `1.0.1` -> `1.1.0`
|
||||||
|
|
||||||
|
## 0.4.25
|
||||||
|
|
||||||
|
* `Matrix`:
|
||||||
|
* Subproject has been created
|
||||||
|
|
||||||
|
## 0.4.24
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.21` -> `1.4.30`
|
||||||
|
* `Klock`: `2.0.4` -> `2.0.6`
|
||||||
|
* `Coroutines`:
|
||||||
|
* New class `DoWithFirstBuilder`
|
||||||
|
* Several new extensions like `firstOf`/`first`/`invokeOnFirstOf`
|
||||||
|
|
||||||
|
## 0.4.23
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `1.5.0` -> `1.5.1`
|
||||||
|
* `Serialization`
|
||||||
|
* `Base64`
|
||||||
|
* New serializer `Base64BytesToFromStringSerializer` has been added
|
||||||
|
|
||||||
|
## 0.4.22
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Exposed`: `0.28.1` -> `0.29.1`
|
||||||
|
* `Klock`: `2.0.2` -> `2.0.4`
|
||||||
|
|
||||||
|
## 0.4.21
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* `JS`
|
||||||
|
* Extension `DataView#toByteArray` has been added
|
||||||
|
* Extension `ArrayBuffer#toByteArray` has been added
|
||||||
|
* Extension `ByteArray#toDataView` has been added
|
||||||
|
* Extension `ByteArray#toArrayBuffer` has been added
|
||||||
|
* `Coroutines`
|
||||||
|
* `JS`
|
||||||
|
* Extension `Blob#toByteArray` has been added
|
||||||
|
* `Crypto`
|
||||||
|
* Subproject has been created
|
||||||
|
* `Serialization`
|
||||||
|
* `Base64`
|
||||||
|
* Currently, it is not depended on `common` project and use `crypto` instead
|
||||||
|
|
||||||
|
## 0.4.20
|
||||||
|
|
||||||
|
* `Serialization`
|
||||||
|
* `Encapsulator`:
|
||||||
|
* Has been created
|
||||||
|
|
||||||
|
## 0.4.19
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* New extension `Iterable<Deferred>#awaitFirstWithDeferred` has been added to identify which of `Deferred`s was
|
||||||
|
normally completed
|
||||||
|
* New extensions `Iterable<Deferred<T>>#invokeOnFirst` and `Iterable<DeferredAction<*, O>>.invokeFirstOf` have been
|
||||||
|
added
|
||||||
|
|
||||||
|
## 0.4.18
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* New extension `Iterable<Deferred>#awaitFirst` has been added
|
||||||
|
* `Serialization`
|
||||||
|
* `Base 64`
|
||||||
|
* New `Base64ByteArraySerializer` has been added
|
||||||
|
|
||||||
|
## 0.4.17
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* Functionality for decode/encode base 64 to/from `ByteArray`/`String` have been added
|
||||||
|
* `Serialization`
|
||||||
|
* `Base 64`
|
||||||
|
* Project has been initiated
|
||||||
|
|
||||||
|
## 0.4.16
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* `safely`:
|
||||||
|
* New `safelyWithoutExceptions` function may accept `onException` parameter with nullable result
|
||||||
|
* Old `safelyWithoutExceptions` now using `defaultSafelyWithoutExceptionHandler` to handle exceptions "like in
|
||||||
|
`safely`", but it is expected that `defaultSafelyWithoutExceptionHandler` will not throw any exception
|
||||||
|
|
||||||
|
## 0.4.15
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* `safely`:
|
||||||
|
* `SafelyExceptionHandlerKey` has been deprecated
|
||||||
|
* `SafelyExceptionHandler` has been deprecated
|
||||||
|
* `ContextSafelyExceptionHandlerKey` has been added
|
||||||
|
* `ContextSafelyExceptionHandler` has been added
|
||||||
|
* `safelyWithContextExceptionHandler` has been added
|
||||||
|
|
||||||
|
## 0.4.14
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.20` -> `1.4.21`
|
||||||
|
* `Ktor`: `1.4.3` -> `1.5.0`
|
||||||
|
* `Klock`: `2.0.1` -> `2.0.2`
|
||||||
|
* `Coroutines`:
|
||||||
|
* Add global variable `defaultSafelyExceptionHandler`
|
||||||
|
* Add `SafelyExceptionHandlerKey` and `SafelyExceptionHandler` classes to be able to overwrite
|
||||||
|
`defaultSafelyExceptionHandler` using context of coroutine
|
||||||
|
|
||||||
|
## 0.4.13
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* `Android`
|
||||||
|
* Add expand/collapse functionality for horizontal expand/collapse
|
||||||
|
|
||||||
|
## 0.4.12
|
||||||
|
|
||||||
|
* `Coroutines`
|
||||||
|
* `JVM`
|
||||||
|
* Update `launchSynchronously` signature
|
||||||
|
* `Selector`
|
||||||
|
* Project created
|
||||||
|
|
||||||
|
## 0.4.11
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* Add `clamp` function
|
||||||
|
|
||||||
|
## 0.4.10
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `2.0.0` -> `2.0.1`
|
||||||
|
* `Repo`
|
||||||
|
* Repo `WriteStandardKeyValueRepo` got new method `unsetWithValues`
|
||||||
|
|
||||||
|
## 0.4.9
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `1.4.2` -> `1.4.3`
|
||||||
|
* `Coroutines`:
|
||||||
|
* `launchSynchronously` has been added in JVM
|
||||||
|
* `Repo`
|
||||||
|
* `Common`
|
||||||
|
* In repos different usages of `BroadcastChannel`s has been replaced with `MutableSharedFlow`
|
||||||
|
* `Exposed`
|
||||||
|
* `asObject` open fun has been added in CRUD realization
|
||||||
|
|
||||||
|
## 0.4.8
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Coroutines`: `1.4.1` -> `1.4.2`
|
||||||
|
* `UUID`: `0.2.2` -> `0.2.3`
|
||||||
|
* `Pagination`
|
||||||
|
* Add `PaginatedIterable` and `PaginatedIterator`
|
||||||
|
|
||||||
|
## 0.4.7
|
||||||
|
|
||||||
|
* `Ktor`
|
||||||
|
* `Client`
|
||||||
|
* New class `UnifiedRequester`
|
||||||
|
* `Server`
|
||||||
|
* New class `UnifiedRouter`
|
||||||
|
* `Repos`
|
||||||
|
* `Ktor`
|
||||||
|
* `Client`
|
||||||
|
* Rewriting of all clients on new `UnifiedRequester`
|
||||||
|
* `Server`
|
||||||
|
* Rewriting of all clients on new `UnifiedRouter`
|
||||||
|
|
||||||
|
## 0.4.6
|
||||||
|
|
||||||
|
* `Common`
|
||||||
|
* New annotation `Warning` has been added
|
||||||
|
* `Pagination`
|
||||||
|
* `Common`
|
||||||
|
* `Pagination` got new extension: `Pagination#isFirstPage`
|
||||||
|
* `Coroutines`:
|
||||||
|
* New extension `FlowCollector#invoke` has been added
|
||||||
|
* `Repos`
|
||||||
|
* `Common`
|
||||||
|
* `JVM` (and `Android` since `Android API 26`):
|
||||||
|
* `FileStandardKeyValueRepo` has been added
|
||||||
|
* Add several `typealias`es for each type of repos
|
||||||
|
|
||||||
|
## 0.4.5
|
||||||
|
|
||||||
|
* `Android`
|
||||||
|
* `Alerts`
|
||||||
|
* `Common`
|
||||||
|
* Project has been created
|
||||||
|
* `RecyclerView`
|
||||||
|
* Project has been created
|
||||||
|
* `Common`
|
||||||
|
* Annotation `PreviewFeature` has been added
|
||||||
|
* `Android`
|
||||||
|
* Added tools to work with visibility in more comfortable way
|
||||||
|
* Added tools to work with disabled/enabled state in more comfortable way
|
||||||
|
* Added tools to work with expanded/collapsed state in more comfortable way (in preview mode)
|
||||||
|
|
||||||
|
## 0.4.4
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Klock`: `1.12.1` -> `2.0.0`
|
||||||
|
* `Commons`:
|
||||||
|
* Update left items functionality to include work with `GridLayoutManager`
|
||||||
|
* `Repos`:
|
||||||
|
* Add interface `VersionsRepo`
|
||||||
|
* Add default realization of `VersionsRepo` named `StandardVersionsRepo` which use `StandardVersionsRepoProxy`
|
||||||
|
to get access to some end-store
|
||||||
|
* Add default realization of `StandardVersionsRepoProxy` based on `KeyValue` repos
|
||||||
|
* Add realizations of `StandardVersionsRepoProxy` for exposed and android (`SQL` and `SharedPreferences`)
|
||||||
|
* `Commons`:
|
||||||
|
* In Android fully reworked transactions functions
|
||||||
|
* Now `DatabaseCoroutineContext` is a shortcut for `Dispatchers.IO`
|
||||||
|
|
||||||
|
## 0.4.3
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.4.10` -> `1.4.20`
|
||||||
|
* `Common`:
|
||||||
|
* Two new extensions for Android:
|
||||||
|
* `Resources#getSp`
|
||||||
|
* `Resources#getDp`
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* Add `WriteOneToManyKeyValueRepo#set` function and extensions
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* Fixed error in `ExposedKeyValueRepo` related to negative size of shared flow
|
||||||
|
* Fixed error in `ExposedKeyValueRepo` related to avoiding of table initiation
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* `Repos`:
|
||||||
|
* `ReadOneToManyKeyValueRepo` got `keys` method with value parameter
|
||||||
|
* All implementations inside of this library has been updated
|
||||||
|
* `ReadStandardKeyValueRepo` got `keys` method with value parameter
|
||||||
|
* All implementations inside of this library has been updated
|
||||||
|
* New extensions `withMapper`
|
||||||
|
|
||||||
## 0.3.3
|
## 0.3.3
|
||||||
|
|
||||||
* `Coroutines`:
|
* `Coroutines`:
|
||||||
@@ -215,4 +789,4 @@ All deprecations has been removed
|
|||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
Inited :)
|
Inited :)
|
||||||
|
38
README.md
38
README.md
@@ -1 +1,37 @@
|
|||||||
# MicroUtils
|
# MicroUtils
|
||||||
|
|
||||||
|
This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies:
|
||||||
|
|
||||||
|
* [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines)
|
||||||
|
* [`Kotlin Serialization`](https://github.com/Kotlin/kotlinx.serialization)
|
||||||
|
* [`Kotlin Exposed`](https://github.com/JetBrains/Exposed)
|
||||||
|
* [`Ktor`](https://ktor.io)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>Android environment</b> </summary>
|
||||||
|
|
||||||
|
You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUtils/blob/master/gradle.properties#L24-L34">properties file</a> to get information about current project dependencies, compile and build tools for `Android` target.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
* `common` contains common tools for platform which usually are absent out-of-the-box when you starting project
|
||||||
|
* `selector` contains tools to use `Selector` interface with things like `RecyclerView` in android or other selection needs
|
||||||
|
* `coroutines` is a module for `Kotlin Coroutines` with different things like subscribing on flows (`onEach` + `launchIn` shortcut :) )
|
||||||
|
* `ktor` is a set of modules for `client`s and `server`s
|
||||||
|
* `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types
|
||||||
|
* `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination
|
||||||
|
* `serialization` is a collection of projects with serializers for `kotlinx.serialization`
|
||||||
|
* `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories
|
||||||
|
|
||||||
|
## Complex modules structure
|
||||||
|
|
||||||
|
Most of complex modules are built with next hierarchy:
|
||||||
|
|
||||||
|
* `common` submodule for `API` things which are common for all platforms
|
||||||
|
* `exposed` submodule contains realizations for exposed tables
|
||||||
|
* `ktor` submodule is usually unavailable directly, because it contains its own submodules for clients and servers
|
||||||
|
* `common` part contains routes which are common for clients and servers
|
||||||
|
* `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests
|
||||||
|
* `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object
|
||||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-cayman
|
17
android/alerts/common/build.gradle
Normal file
17
android/alerts/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppAndroidProjectPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
1
android/alerts/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.common"/>
|
@@ -0,0 +1,55 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
|
||||||
|
typealias AlertDialogCallback = (DialogInterface) -> Unit
|
||||||
|
|
||||||
|
inline fun Context.createAlertDialogTemplate(
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null
|
||||||
|
): AlertDialog.Builder {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
|
||||||
|
title ?.let {
|
||||||
|
builder.setTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
positivePair ?. let {
|
||||||
|
builder.setPositiveButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
negativePair ?. let {
|
||||||
|
builder.setNegativeButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
neutralPair ?. let {
|
||||||
|
builder.setNeutralButton(it.first) { di, _ -> it.second ?. invoke(di) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Context.createAlertDialogTemplateWithResources(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null
|
||||||
|
): AlertDialog.Builder = createAlertDialogTemplate(
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second }
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun AlertDialog.setDismissChecker(noinline checker: () -> Boolean) : AlertDialog {
|
||||||
|
setOnDismissListener {
|
||||||
|
if (!checker()) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
inline fun <T: View> Context.createCustomViewAlertDialog(
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
viewCreator: (Context) -> T
|
||||||
|
): AlertDialog = createAlertDialogTemplate(
|
||||||
|
title, positivePair, neutralPair, negativePair
|
||||||
|
).apply {
|
||||||
|
setView(viewCreator(this@createCustomViewAlertDialog))
|
||||||
|
}.create().apply {
|
||||||
|
if (show) show()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T: View> Context.createCustomViewAlertDialogWithResources(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
viewCreator: (Context) -> T
|
||||||
|
): AlertDialog = createCustomViewAlertDialog(
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second },
|
||||||
|
show,
|
||||||
|
viewCreator
|
||||||
|
)
|
@@ -0,0 +1,45 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.common
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
inline fun Context.createSimpleTextAlertDialog(
|
||||||
|
text: String,
|
||||||
|
title: String? = null,
|
||||||
|
positivePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<String, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog = createAlertDialogTemplate(
|
||||||
|
title,
|
||||||
|
positivePair,
|
||||||
|
neutralPair,
|
||||||
|
negativePair
|
||||||
|
).apply {
|
||||||
|
setMessage(text)
|
||||||
|
}.create().apply {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Context.createSimpleTextAlertDialog(
|
||||||
|
@StringRes
|
||||||
|
text: Int,
|
||||||
|
@StringRes
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog = createSimpleTextAlertDialog(
|
||||||
|
getString(text),
|
||||||
|
title ?.let { getString(it) },
|
||||||
|
positivePair ?.let { getString(it.first) to it.second },
|
||||||
|
neutralPair ?.let { getString(it.first) to it.second },
|
||||||
|
negativePair ?.let { getString(it.first) to it.second },
|
||||||
|
show
|
||||||
|
)
|
18
android/alerts/recyclerview/build.gradle
Normal file
18
android/alerts/recyclerview/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppAndroidProjectPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api internalProject("micro_utils.android.alerts.common")
|
||||||
|
api internalProject("micro_utils.android.recyclerview")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
1
android/alerts/recyclerview/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/>
|
@@ -0,0 +1,65 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||||
|
import dev.inmo.micro_utils.android.recyclerview.*
|
||||||
|
|
||||||
|
data class AlertAction(
|
||||||
|
val title: String,
|
||||||
|
val callback: (DialogInterface) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
class ActionViewHolder(
|
||||||
|
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||||
|
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||||
|
private lateinit var action: AlertAction
|
||||||
|
private val textView: TextView
|
||||||
|
get() = itemView.findViewById(android.R.id.text1)
|
||||||
|
|
||||||
|
init {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
action.callback(dialogInterfaceGetter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(item: AlertAction) {
|
||||||
|
action = item
|
||||||
|
textView.text = item.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionsRecyclerViewAdapter(
|
||||||
|
override val data: List<AlertAction>,
|
||||||
|
private val dialogInterfaceGetter: () -> DialogInterface
|
||||||
|
) : RecyclerViewAdapter<AlertAction>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||||
|
parent, dialogInterfaceGetter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.createActionsAlertDialog(
|
||||||
|
actions: List<AlertAction>,
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true
|
||||||
|
): AlertDialog {
|
||||||
|
lateinit var dialogInterface: DialogInterface
|
||||||
|
|
||||||
|
return createRecyclerViewDialog(
|
||||||
|
title, positivePair, neutralPair, negativePair, show
|
||||||
|
) {
|
||||||
|
ActionsRecyclerViewAdapter(
|
||||||
|
actions
|
||||||
|
) {
|
||||||
|
dialogInterface
|
||||||
|
}
|
||||||
|
}.also { dialogInterface = it }
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
package dev.inmo.micro_utils.android.alerts.recyclerview
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback
|
||||||
|
import dev.inmo.micro_utils.android.alerts.common.createCustomViewAlertDialogWithResources
|
||||||
|
|
||||||
|
fun Context.createRecyclerViewDialog(
|
||||||
|
title: Int? = null,
|
||||||
|
positivePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
neutralPair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
negativePair: Pair<Int, AlertDialogCallback?>? = null,
|
||||||
|
show: Boolean = true,
|
||||||
|
layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this),
|
||||||
|
marginOfRecyclerView: Int = 8, // dp
|
||||||
|
recyclerViewSetUp: RecyclerView.() -> Unit = {},
|
||||||
|
adapterFactory: () -> RecyclerView.Adapter<*>
|
||||||
|
): AlertDialog {
|
||||||
|
val recyclerView = RecyclerView(this).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
setMargins(marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView)
|
||||||
|
}
|
||||||
|
this.layoutManager = layoutManager
|
||||||
|
adapter = adapterFactory()
|
||||||
|
recyclerViewSetUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCustomViewAlertDialogWithResources(
|
||||||
|
title,
|
||||||
|
positivePair,
|
||||||
|
neutralPair,
|
||||||
|
negativePair,
|
||||||
|
show
|
||||||
|
) {
|
||||||
|
recyclerView
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppAndroidProjectPresetPath"
|
apply from: "$mppAndroidProjectPresetPath"
|
||||||
@@ -12,6 +11,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.*
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun RecyclerView.LayoutManager.findLastVisibleItemPositionGetter(): (() -> Int)? = when (this) {
|
||||||
|
is LinearLayoutManager -> ::findLastVisibleItemPosition
|
||||||
|
is GridLayoutManager -> ::findLastVisibleItemPosition
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
fun RecyclerView.lastVisibleItemFlow(
|
fun RecyclerView.lastVisibleItemFlow(
|
||||||
completingScope: CoroutineScope
|
completingScope: CoroutineScope
|
||||||
): Flow<Int> {
|
): Flow<Int> {
|
||||||
val lastVisibleElementFun: () -> Int = (layoutManager as? LinearLayoutManager) ?.let { it::findLastVisibleItemPosition } ?: error("Currently supported only linear layout manager")
|
val lastVisibleElementFun: () -> Int = layoutManager ?.findLastVisibleItemPositionGetter() ?: error("Currently supported only linear and grid layout manager")
|
||||||
val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
|
val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun())
|
||||||
addOnScrollListener(
|
addOnScrollListener(
|
||||||
object : RecyclerView.OnScrollListener() {
|
object : RecyclerView.OnScrollListener() {
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
|
||||||
abstract class RecyclerViewAdapter<T>(
|
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||||
val data: List<T>
|
protected abstract val data: List<T>
|
||||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
|
||||||
|
private val _dataCountState by lazy {
|
||||||
|
MutableStateFlow<Int>(data.size)
|
||||||
|
}
|
||||||
|
val dataCountState: StateFlow<Int> by lazy {
|
||||||
|
_dataCountState.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
var emptyView: View? = null
|
var emptyView: View? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
object : RecyclerView.AdapterDataObserver() {
|
object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount)
|
super.onItemRangeChanged(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
|
|
||||||
private fun checkEmpty() {
|
private fun checkEmpty() {
|
||||||
emptyView ?. let {
|
emptyView ?. let {
|
||||||
if (data.isEmpty()) {
|
if (dataCountState.value == 0) {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
@@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> RecyclerViewAdapter(
|
||||||
|
data: List<T>,
|
||||||
|
onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T>
|
||||||
|
) = object : RecyclerViewAdapter<T>() {
|
||||||
|
override val data: List<T> = data
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.common.Diff
|
||||||
|
import dev.inmo.micro_utils.common.PreviewFeature
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
@PreviewFeature("This feature in preview state and may contains different bugs. " +
|
||||||
|
"Besides, this feature can be changed in future in non-compatible way")
|
||||||
|
abstract class StateFlowBasedRecyclerViewAdapter<T>(
|
||||||
|
listeningScope: CoroutineScope,
|
||||||
|
dataState: StateFlow<List<T>>
|
||||||
|
) : RecyclerViewAdapter<T>() {
|
||||||
|
override var data: List<T> = emptyList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataState.onEach {
|
||||||
|
try {
|
||||||
|
val diffForRemoves = Diff(data, it)
|
||||||
|
val removedIndexes = diffForRemoves.removed.map { it.index }
|
||||||
|
val leftRemove = removedIndexes.toMutableList()
|
||||||
|
data = data.filterIndexed { i, _ ->
|
||||||
|
if (i in leftRemove) {
|
||||||
|
leftRemove.remove(i)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
removedIndexes.sortedDescending().forEach {
|
||||||
|
notifyItemRemoved(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val diffAddsAndReplaces = Diff(data, it)
|
||||||
|
data = it
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
diffAddsAndReplaces.replaced.forEach { (from, to) ->
|
||||||
|
notifyItemMoved(from.index, to.index)
|
||||||
|
}
|
||||||
|
diffAddsAndReplaces.added.forEach {
|
||||||
|
notifyItemInserted(it.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// currently do nothing
|
||||||
|
}
|
||||||
|
}.launchIn(listeningScope)
|
||||||
|
}
|
||||||
|
}
|
15
build.gradle
15
build.gradle
@@ -1,6 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
@@ -8,10 +7,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
|
|
||||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||||
@@ -21,10 +19,17 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
}
|
||||||
|
|
||||||
|
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||||
|
if (it != rootProject.findProject("docs")) {
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
|
||||||
|
task.enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,21 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
"It is possible, that behaviour of this thing will be changed or removed in future releases",
|
||||||
|
RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.TYPEALIAS
|
||||||
|
)
|
||||||
|
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
"This thing is marked as warned. See message of warn to get more info",
|
||||||
|
RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.TYPEALIAS
|
||||||
|
)
|
||||||
|
annotation class Warning(val message: String)
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
@Deprecated("Redundant", ReplaceWith("coerceIn(min, max)"))
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max)
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
package dev.inmo.micro_utils.common
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
private inline fun <T> getObject(
|
private inline fun <T> getObject(
|
||||||
additional: MutableList<T>,
|
additional: MutableList<T>,
|
||||||
iterator: Iterator<T>
|
iterator: Iterator<T>
|
||||||
@@ -27,8 +29,8 @@ data class Diff<T> internal constructor(
|
|||||||
|
|
||||||
private inline fun <T> performChanges(
|
private inline fun <T> performChanges(
|
||||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||||
additionalsInOld: MutableList<T>,
|
additionsInOld: MutableList<T>,
|
||||||
additionalsInNew: MutableList<T>,
|
additionsInNew: MutableList<T>,
|
||||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||||
removedList: MutableList<IndexedValue<T>>,
|
removedList: MutableList<IndexedValue<T>>,
|
||||||
addedList: MutableList<IndexedValue<T>>,
|
addedList: MutableList<IndexedValue<T>>,
|
||||||
@@ -52,20 +54,20 @@ private inline fun <T> performChanges(
|
|||||||
newPotentials.first().second ?.let { addedList.add(it) }
|
newPotentials.first().second ?.let { addedList.add(it) }
|
||||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||||
addedList.add(newOne!!)
|
addedList.add(newOne!!)
|
||||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
oldOne ?.let { additionsInOld.add(oldOne.value) }
|
||||||
}
|
}
|
||||||
if (newPotentials.size > 1) {
|
if (newPotentials.size > 1) {
|
||||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newOneEqualToOldObject -> {
|
newOneEqualToOldObject -> {
|
||||||
newPotentials.first().first ?.let { removedList.add(it) }
|
newPotentials.first().first ?.let { removedList.add(it) }
|
||||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||||
removedList.add(oldOne!!)
|
removedList.add(oldOne!!)
|
||||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
newOne ?.let { additionsInNew.add(newOne.value) }
|
||||||
}
|
}
|
||||||
if (newPotentials.size > 1) {
|
if (newPotentials.size > 1) {
|
||||||
newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) }
|
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +141,10 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
|
|
||||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||||
}
|
}
|
||||||
|
inline fun <T> Iterable<T>.diff(
|
||||||
|
other: Iterable<T>,
|
||||||
|
strictComparison: Boolean = false
|
||||||
|
): Diff<T> = calculateDiff(other, strictComparison)
|
||||||
|
|
||||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||||
|
@@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder
|
|||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
typealias ByteArrayAllocator = () -> ByteArray
|
typealias ByteArrayAllocator = () -> ByteArray
|
||||||
|
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
|
||||||
|
|
||||||
val ByteArray.asAllocator: ByteArrayAllocator
|
val ByteArray.asAllocator: ByteArrayAllocator
|
||||||
get() = { this }
|
get() = { this }
|
||||||
|
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = { this }
|
||||||
|
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = { this() }
|
||||||
|
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
|
||||||
|
return invoke().asAllocator
|
||||||
|
}
|
||||||
|
|
||||||
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||||
private val realSerializer = ByteArraySerializer()
|
private val realSerializer = ByteArraySerializer()
|
||||||
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
|||||||
|
|
||||||
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||||
val bytes = realSerializer.deserialize(decoder)
|
val bytes = realSerializer.deserialize(decoder)
|
||||||
return { bytes }
|
return bytes.asAllocator
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!
|
@@ -0,0 +1,59 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
inline fun <I, R> Iterable<I>.joinTo(
|
||||||
|
crossinline separatorFun: (I) -> R?,
|
||||||
|
prefix: R? = null,
|
||||||
|
postfix: R? = null,
|
||||||
|
crossinline transform: (I) -> R?
|
||||||
|
): List<R> {
|
||||||
|
val result = mutableListOf<R>()
|
||||||
|
val iterator = iterator()
|
||||||
|
|
||||||
|
prefix ?.let(result::add)
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val element = iterator.next()
|
||||||
|
result.add(transform(element) ?: continue)
|
||||||
|
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
result.add(separatorFun(element) ?: continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postfix ?.let(result::add)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <I, R> Iterable<I>.joinTo(
|
||||||
|
separator: R? = null,
|
||||||
|
prefix: R? = null,
|
||||||
|
postfix: R? = null,
|
||||||
|
crossinline transform: (I) -> R?
|
||||||
|
): List<R> = joinTo({ separator }, prefix, postfix, transform)
|
||||||
|
|
||||||
|
inline fun <I> Iterable<I>.joinTo(
|
||||||
|
crossinline separatorFun: (I) -> I?,
|
||||||
|
prefix: I? = null,
|
||||||
|
postfix: I? = null
|
||||||
|
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
|
||||||
|
|
||||||
|
inline fun <I> Iterable<I>.joinTo(
|
||||||
|
separator: I? = null,
|
||||||
|
prefix: I? = null,
|
||||||
|
postfix: I? = null
|
||||||
|
): List<I> = joinTo<I>({ separator }, prefix, postfix)
|
||||||
|
|
||||||
|
inline fun <I, reified R> Array<I>.joinTo(
|
||||||
|
crossinline separatorFun: (I) -> R?,
|
||||||
|
prefix: R? = null,
|
||||||
|
postfix: R? = null,
|
||||||
|
crossinline transform: (I) -> R?
|
||||||
|
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
|
||||||
|
|
||||||
|
inline fun <I, reified R> Array<I>.joinTo(
|
||||||
|
separator: R? = null,
|
||||||
|
prefix: R? = null,
|
||||||
|
postfix: R? = null,
|
||||||
|
crossinline transform: (I) -> R?
|
||||||
|
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
|
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@JvmInline
|
||||||
|
value class FileName(val string: String) {
|
||||||
|
val name: String
|
||||||
|
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' }
|
||||||
|
val extension: String
|
||||||
|
get() = name.takeLastWhile { it != '.' }
|
||||||
|
val nameWithoutExtension: String
|
||||||
|
get() {
|
||||||
|
val filename = name
|
||||||
|
return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let {
|
||||||
|
filename.substring(0, it)
|
||||||
|
} ?: filename
|
||||||
|
}
|
||||||
|
val withoutSlashAtTheEnd: String
|
||||||
|
get() = string.dropLastWhile { it == '/' }
|
||||||
|
override fun toString(): String = string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
expect class MPPFile
|
||||||
|
|
||||||
|
expect val MPPFile.filename: FileName
|
||||||
|
expect val MPPFile.filesize: Long
|
||||||
|
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||||
|
|
@@ -11,7 +11,7 @@ class DiffUtilsTests {
|
|||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
for ((i, v) in withIndex) {
|
for ((i, _) in withIndex) {
|
||||||
if (i + count > oldList.lastIndex) {
|
if (i + count > oldList.lastIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.khronos.webgl.*
|
||||||
|
|
||||||
|
fun DataView.toByteArray() = ByteArray(this.byteLength) {
|
||||||
|
getInt8(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ArrayBuffer.toByteArray() = Int8Array(this) as ByteArray
|
||||||
|
|
||||||
|
fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also {
|
||||||
|
forEachIndexed { i, byte -> it.setInt8(i, byte) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toArrayBuffer() = toDataView().buffer
|
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
|
import org.w3c.dom.ErrorEvent
|
||||||
|
import org.w3c.files.File
|
||||||
|
import org.w3c.files.FileReader
|
||||||
|
import kotlin.js.Promise
|
||||||
|
|
||||||
|
actual typealias MPPFile = File
|
||||||
|
|
||||||
|
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||||
|
val reader = FileReader()
|
||||||
|
reader.onload = {
|
||||||
|
success((reader.result as ArrayBuffer).toByteArray())
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
reader.onerror = {
|
||||||
|
failure(Exception((it as ErrorEvent).message))
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||||
|
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(name)
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = size.toLong()
|
||||||
|
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = ::dirtyReadBytes
|
@@ -0,0 +1,8 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
import kotlin.js.Promise
|
||||||
|
|
||||||
|
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||||
|
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.doInIO
|
||||||
|
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
actual typealias MPPFile = File
|
||||||
|
|
||||||
|
actual val MPPFile.filename: FileName
|
||||||
|
get() = FileName(name)
|
||||||
|
actual val MPPFile.filesize: Long
|
||||||
|
get() = length()
|
||||||
|
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||||
|
get() = {
|
||||||
|
doInIO {
|
||||||
|
doOutsideOfCoroutine {
|
||||||
|
readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
|
||||||
|
inline fun Resources.getSp(
|
||||||
|
resId: Int
|
||||||
|
) = getDimension(resId) / displayMetrics.scaledDensity
|
||||||
|
|
||||||
|
inline fun Resources.getDp(
|
||||||
|
resId: Int
|
||||||
|
) = getDimension(resId) * displayMetrics.density
|
@@ -0,0 +1,140 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.Transformation
|
||||||
|
|
||||||
|
private fun View.performExpand(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
onMeasured: View.() -> Unit,
|
||||||
|
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||||
|
) {
|
||||||
|
measure(targetWidth, targetHeight)
|
||||||
|
onMeasured()
|
||||||
|
show()
|
||||||
|
val a: Animation = object : Animation() {
|
||||||
|
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||||
|
super.applyTransformation(interpolatedTime, t)
|
||||||
|
onPerformAnimation(interpolatedTime, t)
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun willChangeBounds(): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
a.duration = duration
|
||||||
|
startAnimation(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.performCollapse(
|
||||||
|
duration: Long = 500,
|
||||||
|
onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit
|
||||||
|
) {
|
||||||
|
val a: Animation = object : Animation() {
|
||||||
|
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
|
||||||
|
if (interpolatedTime == 1f) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
onPerformAnimation(interpolatedTime, t)
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun willChangeBounds(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.duration = duration
|
||||||
|
|
||||||
|
startAnimation(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.expand(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
var measuredHeight = 0
|
||||||
|
performExpand(
|
||||||
|
duration,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
{
|
||||||
|
measuredHeight = this.measuredHeight
|
||||||
|
}
|
||||||
|
) { interpolatedTime, _ ->
|
||||||
|
layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.expandHorizontally(
|
||||||
|
duration: Long = 500,
|
||||||
|
targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
var measuredWidth = 0
|
||||||
|
performExpand(
|
||||||
|
duration,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
{
|
||||||
|
measuredWidth = this.measuredWidth
|
||||||
|
}
|
||||||
|
) { interpolatedTime, _ ->
|
||||||
|
layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.collapse(duration: Long = 500) {
|
||||||
|
val initialHeight: Int = measuredHeight
|
||||||
|
performCollapse(duration) { interpolatedTime, _ ->
|
||||||
|
layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.collapseHorizontally(duration: Long = 500) {
|
||||||
|
val initialWidth: Int = measuredWidth
|
||||||
|
performCollapse(duration) { interpolatedTime, _ ->
|
||||||
|
layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
inline val View.isCollapsed
|
||||||
|
get() = visibility == View.GONE
|
||||||
|
|
||||||
|
@PreviewFeature
|
||||||
|
inline val View.isExpanded
|
||||||
|
get() = !isCollapsed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true in case of expanding
|
||||||
|
*/
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||||
|
expand(duration)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
collapse(duration)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true in case of expanding
|
||||||
|
*/
|
||||||
|
@PreviewFeature
|
||||||
|
fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) {
|
||||||
|
expandHorizontally(duration)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
collapseHorizontally(duration)
|
||||||
|
false
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
inline val View.enabled
|
||||||
|
get() = isEnabled
|
||||||
|
|
||||||
|
inline val View.disabled
|
||||||
|
get() = !enabled
|
||||||
|
|
||||||
|
fun View.disable() {
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
(0 until childCount).forEach { getChildAt(it).disable() }
|
||||||
|
}
|
||||||
|
isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.enable() {
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
(0 until childCount).forEach { getChildAt(it).enable() }
|
||||||
|
}
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.toggleEnabledState(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,47 @@
|
|||||||
|
@file:Suppress("NOTHING_TO_INLINE", "unused")
|
||||||
|
|
||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
inline val View.gone
|
||||||
|
get() = visibility == View.GONE
|
||||||
|
inline fun View.gone() {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val View.hidden
|
||||||
|
get() = visibility == View.INVISIBLE
|
||||||
|
inline fun View.hide() {
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
inline val View.shown
|
||||||
|
get() = visibility == View.VISIBLE
|
||||||
|
inline fun View.show() {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
||||||
|
if (isShown) {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
@@ -20,4 +19,4 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
|
||||||
|
interface ActorAction<T> {
|
||||||
|
suspend operator fun invoke(): T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||||
|
*
|
||||||
|
* @see actor
|
||||||
|
*/
|
||||||
|
fun CoroutineScope.createActionsActor() = actor<suspend () -> Unit> {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||||
|
*
|
||||||
|
* @see safeActor
|
||||||
|
*/
|
||||||
|
inline fun CoroutineScope.createSafeActionsActor(
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler
|
||||||
|
) = safeActor<suspend () -> Unit>(Channel.UNLIMITED, onException) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be use with actor created by [createActionsActor] or [createSafeActionsActor]. Will send lambda which will
|
||||||
|
* execute [action] and return result.
|
||||||
|
*
|
||||||
|
* @see suspendCoroutine
|
||||||
|
* @see safely
|
||||||
|
*/
|
||||||
|
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
||||||
|
action: ActorAction<T>
|
||||||
|
) = suspendCoroutine<T> {
|
||||||
|
trySend {
|
||||||
|
safely({ e -> it.resumeWithException(e) }) {
|
||||||
|
it.resume(action())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -19,7 +19,7 @@ fun <T> CoroutineScope.actor(
|
|||||||
|
|
||||||
inline fun <T> CoroutineScope.safeActor(
|
inline fun <T> CoroutineScope.safeActor(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
noinline onException: ExceptionHandler<Unit> = {},
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
crossinline block: suspend (T) -> Unit
|
crossinline block: suspend (T) -> Unit
|
||||||
): Channel<T> = actor(
|
): Channel<T> = actor(
|
||||||
channelCapacity
|
channelCapacity
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
|
||||||
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||||
|
scope.launch(SupervisorJob()) {
|
||||||
|
val scope = this
|
||||||
|
forEach {
|
||||||
|
scope.launch {
|
||||||
|
continuation.resume(it to it.await())
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
if (cancelOnResult) {
|
||||||
|
forEach {
|
||||||
|
try {
|
||||||
|
it.cancel()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
): T = awaitFirstWithDeferred(scope, cancelOnResult).second
|
||||||
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirst(
|
||||||
|
cancelOthers: Boolean = true
|
||||||
|
): T = awaitFirst(CoroutineScope(coroutineContext), cancelOthers)
|
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
inline val UI
|
||||||
|
get() = Dispatchers.Main
|
||||||
|
inline val Default
|
||||||
|
get() = Dispatchers.Default
|
||||||
|
|
||||||
|
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||||
|
context,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
UI,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
Default,
|
||||||
|
block
|
||||||
|
)
|
@@ -0,0 +1,85 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
class DeferredAction<T, O>(
|
||||||
|
val deferred: Deferred<T>,
|
||||||
|
val callback: suspend (T) -> O
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke() = callback(deferred.await())
|
||||||
|
}
|
||||||
|
|
||||||
|
class DoWithFirstBuilder<T>(
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
private val deferreds = mutableListOf<Deferred<T>>()
|
||||||
|
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||||
|
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||||
|
}
|
||||||
|
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
|
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
|
|
||||||
|
fun build() = deferreds.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
|
||||||
|
|
||||||
|
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
): O {
|
||||||
|
return map { it.deferred }.awaitFirstWithDeferred(scope, cancelOnResult).let { result ->
|
||||||
|
first { it.deferred == result.first }.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <O> invokeFirstOf(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
vararg variants: DeferredAction<*, O>,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
): O = variants.toList().invokeFirstOf(scope, cancelOnResult)
|
||||||
|
|
||||||
|
suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
callback: suspend (T) -> O
|
||||||
|
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
|
||||||
|
|
||||||
|
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
block: DoWithFirstBuilder<T>.() -> Unit,
|
||||||
|
callback: suspend (T) -> O
|
||||||
|
) = firstOf(
|
||||||
|
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||||
|
cancelOnResult
|
||||||
|
).let { callback(it) }
|
||||||
|
|
||||||
|
suspend fun <T, O> invokeOnFirst(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
vararg variants: Deferred<T>,
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
callback: suspend (T) -> O
|
||||||
|
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
variants: Iterable<Deferred<T>>,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = variants.invokeOnFirst(this, cancelOnResult) { it }
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
block: DoWithFirstBuilder<T>.() -> Unit
|
||||||
|
) = firstOf(
|
||||||
|
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||||
|
cancelOnResult
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
vararg variants: Deferred<T>,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = firstOf(variants.toList(), cancelOnResult)
|
||||||
|
|
||||||
|
suspend fun <T> List<Deferred<T>>.first(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = scope.firstOf(this, cancelOnResult)
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
|
||||||
|
suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value)
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!
|
@@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||||
@@ -16,7 +18,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend
|
|||||||
*/
|
*/
|
||||||
inline fun <T> Flow<T>.subscribeSafely(
|
inline fun <T> Flow<T>.subscribeSafely(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
noinline onException: ExceptionHandler<Unit> = { throw it },
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
) = subscribe(scope) {
|
) = subscribe(scope) {
|
||||||
safely(onException) {
|
safely(onException) {
|
||||||
@@ -25,13 +27,26 @@ inline fun <T> Flow<T>.subscribeSafely(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||||
*/
|
*/
|
||||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
) = subscribeSafely(
|
) = subscribe(scope) {
|
||||||
scope,
|
safelyWithoutExceptions(onException) {
|
||||||
{},
|
block(it)
|
||||||
block
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||||
|
*/
|
||||||
|
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribe(scope) {
|
||||||
|
safelyWithoutExceptions({ /* skip exceptions */ }) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,118 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
private class SubscribeAsyncReceiver<T>(
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
||||||
|
) {
|
||||||
|
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
||||||
|
val channel: SendChannel<T>
|
||||||
|
get() = dataChannel
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launchSafelyWithoutExceptions {
|
||||||
|
for (data in dataChannel) {
|
||||||
|
output(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean = dataChannel.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface AsyncSubscriptionCommand<T, M> {
|
||||||
|
suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>)
|
||||||
|
}
|
||||||
|
private data class AsyncSubscriptionCommandData<T, M>(
|
||||||
|
val data: T,
|
||||||
|
val scope: CoroutineScope,
|
||||||
|
val markerFactory: suspend (T) -> M,
|
||||||
|
val block: suspend (T) -> Unit,
|
||||||
|
val onEmpty: suspend (M) -> Unit
|
||||||
|
) : AsyncSubscriptionCommand<T, M> {
|
||||||
|
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||||
|
val marker = markerFactory(data)
|
||||||
|
markersMap.getOrPut(marker) {
|
||||||
|
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) {
|
||||||
|
safelyWithoutExceptions { block(it) }
|
||||||
|
if (isEmpty()) {
|
||||||
|
onEmpty(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.channel.send(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AsyncSubscriptionCommandClearReceiver<T, M>(
|
||||||
|
val marker: M
|
||||||
|
) : AsyncSubscriptionCommand<T, M> {
|
||||||
|
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||||
|
val receiver = markersMap[marker]
|
||||||
|
if (receiver ?.isEmpty() == true) {
|
||||||
|
markersMap.remove(marker)
|
||||||
|
receiver.scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, M> Flow<T>.subscribeAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
markerFactory: suspend (T) -> M,
|
||||||
|
block: suspend (T) -> Unit
|
||||||
|
): Job {
|
||||||
|
val subscope = scope.LinkedSupervisorScope()
|
||||||
|
val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>()
|
||||||
|
val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) {
|
||||||
|
it.invoke(markersMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
|
||||||
|
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||||
|
actor.send(
|
||||||
|
AsyncSubscriptionCommandClearReceiver(marker)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actor.send(dataCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.invokeOnCompletion { if (subscope.isActive) subscope.cancel() }
|
||||||
|
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safely(onException) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safelyWithoutExceptions(onException) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline markerFactory: suspend (T) -> M,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribeAsync(scope, markerFactory) {
|
||||||
|
safelyWithoutExceptions({ /* do nothing */}) {
|
||||||
|
block(it)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,30 +1,156 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
typealias ExceptionHandler<T> = suspend (Throwable) -> T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This instance will be used in all calls of [safely] where exception handler has not been passed
|
||||||
|
*/
|
||||||
|
var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call
|
||||||
|
*/
|
||||||
|
var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
||||||
|
try {
|
||||||
|
defaultSafelyExceptionHandler(it)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||||
|
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||||
|
*
|
||||||
|
* @see safelyWithContextExceptionHandler
|
||||||
|
* @see ContextSafelyExceptionHandler
|
||||||
|
*/
|
||||||
|
object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls
|
||||||
|
*
|
||||||
|
* @see safelyWithContextExceptionHandler
|
||||||
|
* @see ContextSafelyExceptionHandlerKey
|
||||||
|
*/
|
||||||
|
class ContextSafelyExceptionHandler(
|
||||||
|
val handler: ExceptionHandler<Unit>
|
||||||
|
) : CoroutineContext.Element {
|
||||||
|
override val key: CoroutineContext.Key<*>
|
||||||
|
get() = ContextSafelyExceptionHandlerKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if
|
||||||
|
* exists
|
||||||
|
*
|
||||||
|
* @see ContextSafelyExceptionHandler
|
||||||
|
* @see ContextSafelyExceptionHandlerKey
|
||||||
|
*/
|
||||||
|
suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext]
|
||||||
|
* already contains [ContextSafelyExceptionHandler], [ContextSafelyExceptionHandler.handler] will be used BEFORE
|
||||||
|
* [contextExceptionHandler] in case of exception.
|
||||||
|
*
|
||||||
|
* After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call
|
||||||
|
* [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block
|
||||||
|
*/
|
||||||
|
suspend fun <T> safelyWithContextExceptionHandler(
|
||||||
|
contextExceptionHandler: ExceptionHandler<Unit>,
|
||||||
|
safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): T {
|
||||||
|
val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler ->
|
||||||
|
ContextSafelyExceptionHandler {
|
||||||
|
oldHandler(it)
|
||||||
|
contextExceptionHandler(it)
|
||||||
|
}
|
||||||
|
} ?: ContextSafelyExceptionHandler(contextExceptionHandler)
|
||||||
|
return withContext(contextSafelyExceptionHandler) {
|
||||||
|
safely(safelyExceptionHandler, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
* It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions
|
||||||
*
|
*
|
||||||
|
* Priorities of [ExceptionHandler]s:
|
||||||
|
*
|
||||||
|
* * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler])
|
||||||
|
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||||
|
* * [defaultSafelyExceptionHandler]
|
||||||
|
*
|
||||||
|
* Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will
|
||||||
|
* be called [onException]
|
||||||
|
*
|
||||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||||
* exception will be available for catching
|
* exception will be available for catching
|
||||||
|
*
|
||||||
|
* @see defaultSafelyExceptionHandler
|
||||||
|
* @see safelyWithoutExceptions
|
||||||
|
* @see safelyWithContextExceptionHandler
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> safely(
|
suspend inline fun <T> safely(
|
||||||
noinline onException: ExceptionHandler<T> = { throw it },
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T {
|
): T {
|
||||||
return try {
|
return try {
|
||||||
supervisorScope(block)
|
supervisorScope(block)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e)
|
||||||
onException(e)
|
onException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> runCatchingSafely(
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T> = runCatching {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> safelyWithResult(
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for [safely] without exception handler (instead of this you will receive null as a result)
|
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||||
|
* returning null at one time
|
||||||
|
*
|
||||||
|
* @see safelyWithoutExceptions
|
||||||
|
* @see launchSafelyWithoutExceptions
|
||||||
|
* @see asyncSafelyWithoutExceptions
|
||||||
|
*/
|
||||||
|
val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
||||||
|
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||||
|
* result from exception (instead of throwing it, by default always returns null)
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> safelyWithoutExceptions(
|
suspend inline fun <T> safelyWithoutExceptions(
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safely({ null }, block)
|
): T? = safely(onException, block)
|
||||||
|
|
||||||
|
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T?> = runCatching {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun CoroutineScope(
|
||||||
|
context: CoroutineContext,
|
||||||
|
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||||
|
) = CoroutineScope(
|
||||||
|
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||||
|
)
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
fun CoroutineContext.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
|
||||||
|
fun CoroutineScope.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||||
|
|
||||||
|
fun CoroutineScope.LinkedSupervisorScope(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = CoroutineScope(
|
||||||
|
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||||
|
)
|
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
|
||||||
|
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.weakLaunch(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job {
|
||||||
|
val scope = createWeakSubScope()
|
||||||
|
val job = scope.launch(context, start, block)
|
||||||
|
job.invokeOnCompletion { scope.cancel() }
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.weakAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): Deferred<T> {
|
||||||
|
val scope = createWeakSubScope()
|
||||||
|
val deferred = scope.async(context, start, block)
|
||||||
|
deferred.invokeOnCompletion { scope.cancel() }
|
||||||
|
return deferred
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.await
|
||||||
|
import org.khronos.webgl.Int8Array
|
||||||
|
import org.w3c.fetch.Response
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
|
||||||
|
suspend fun Blob.toByteArray() = Int8Array(
|
||||||
|
Response(this).arrayBuffer().await()
|
||||||
|
) as ByteArray
|
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
val IO
|
||||||
|
get() = Dispatchers.IO
|
||||||
|
|
||||||
|
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
IO,
|
||||||
|
block
|
||||||
|
)
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||||
|
var result: Result<T>? = null
|
||||||
|
val objectToSynchronize = Object()
|
||||||
|
synchronized(objectToSynchronize) {
|
||||||
|
launch {
|
||||||
|
result = safelyWithResult(block)
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
synchronized(objectToSynchronize) {
|
||||||
|
objectToSynchronize.notifyAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (result == null) {
|
||||||
|
objectToSynchronize.wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result!!.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||||
|
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
@@ -0,0 +1,34 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class AwaitFirstTests {
|
||||||
|
private fun CoroutineScope.createTestDeferred(value: Int, wait: Long = 100000) = async(start = CoroutineStart.LAZY) { delay(wait); value }
|
||||||
|
@Test
|
||||||
|
fun testThatAwaitFirstIsWorkingCorrectly() {
|
||||||
|
val baseScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
val resultDeferred = baseScope.createTestDeferred(-1, 0)
|
||||||
|
val deferreds = listOf(
|
||||||
|
baseScope.async { createTestDeferred(0) },
|
||||||
|
baseScope.async { createTestDeferred(1) },
|
||||||
|
baseScope.async { createTestDeferred(2) },
|
||||||
|
resultDeferred
|
||||||
|
)
|
||||||
|
val controlJob = baseScope.launch {
|
||||||
|
delay(1000000)
|
||||||
|
}
|
||||||
|
val result = baseScope.launchSynchronously {
|
||||||
|
val result = deferreds.awaitFirst(baseScope)
|
||||||
|
|
||||||
|
assertTrue(baseScope.isActive)
|
||||||
|
assertTrue(controlJob.isActive)
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
assertEquals(baseScope.launchSynchronously { resultDeferred.await() }, result)
|
||||||
|
assertTrue(deferreds.all { it == resultDeferred || it.isCancelled })
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class DoWithFirstTests {
|
||||||
|
@Test
|
||||||
|
fun testHandleOneOf() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
val happenedDeferreds = mutableListOf<Int>()
|
||||||
|
val deferredWhichMustHappen = (-1).asDeferred
|
||||||
|
scope.launchSynchronously {
|
||||||
|
scope.launch {
|
||||||
|
((0 until 100).map {
|
||||||
|
DeferredAction(
|
||||||
|
scope.async { delay(10000); it },
|
||||||
|
happenedDeferreds::add
|
||||||
|
)
|
||||||
|
} + DeferredAction(deferredWhichMustHappen, happenedDeferreds::add)).invokeFirstOf(scope)
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
|
assertEquals(1, happenedDeferreds.size)
|
||||||
|
assertEquals(scope.launchSynchronously { deferredWhichMustHappen.await() }, happenedDeferreds.first())
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class HandleSafelyCoroutineContextTest {
|
||||||
|
@Test
|
||||||
|
fun testHandleSafelyCoroutineContext() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
var contextHandlerHappen = false
|
||||||
|
var localHandlerHappen = false
|
||||||
|
var defaultHandlerHappen = false
|
||||||
|
defaultSafelyExceptionHandler = {
|
||||||
|
defaultHandlerHappen = true
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
val contextHandler: ExceptionHandler<Unit> = {
|
||||||
|
contextHandlerHappen = true
|
||||||
|
}
|
||||||
|
val checkJob = scope.launch {
|
||||||
|
safelyWithContextExceptionHandler(contextHandler) {
|
||||||
|
safely(
|
||||||
|
{
|
||||||
|
localHandlerHappen = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
error("That must happen :)")
|
||||||
|
}
|
||||||
|
println(coroutineContext)
|
||||||
|
error("That must happen too:)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launchSynchronously { checkJob.join() }
|
||||||
|
assert(contextHandlerHappen)
|
||||||
|
assert(localHandlerHappen)
|
||||||
|
assert(defaultHandlerHappen)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class LaunchSynchronouslyTest {
|
||||||
|
@Test
|
||||||
|
fun testRunInCoroutine() {
|
||||||
|
(0 .. 10000).forEach {
|
||||||
|
assertEquals(it, launchSynchronously { it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WeakJob {
|
||||||
|
@Test
|
||||||
|
fun `test that weak jobs works correctly`() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
lateinit var weakLaunchJob: Job
|
||||||
|
lateinit var weakAsyncJob: Job
|
||||||
|
scope.launchSynchronously {
|
||||||
|
val completeDeferred = Job()
|
||||||
|
coroutineScope {
|
||||||
|
weakLaunchJob = weakLaunch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weakAsyncJob = weakAsync {
|
||||||
|
while (isActive) {
|
||||||
|
delay(100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineContext.job.invokeOnCompletion {
|
||||||
|
scope.launch {
|
||||||
|
delay(1000L)
|
||||||
|
completeDeferred.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch { delay(1000L); cancel() }
|
||||||
|
}
|
||||||
|
completeDeferred.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!weakLaunchJob.isActive)
|
||||||
|
assert(!weakAsyncJob.isActive)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
|
||||||
Dispatchers.Main,
|
|
||||||
block
|
|
||||||
)
|
|
30
crypto/build.gradle
Normal file
30
crypto/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
api npm("crypto-js", "$crypto_js_version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java.srcDirs += [ "src/jvmMain/kotlin" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,77 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
private const val BASE64_MASK: Byte = 0x3f
|
||||||
|
private const val BASE64_PAD = '='
|
||||||
|
private val BASE64_INVERSE_ALPHABET = IntArray(256) {
|
||||||
|
BASE64_ALPHABET.indexOf(it.toChar())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
|
||||||
|
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
|
||||||
|
|
||||||
|
typealias EncodedBase64String = String
|
||||||
|
typealias EncodedByteArray = ByteArray
|
||||||
|
|
||||||
|
fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
||||||
|
fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
||||||
|
|
||||||
|
fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString {
|
||||||
|
var i = 0
|
||||||
|
while (this@encodeBase64String.size > i) {
|
||||||
|
val read = kotlin.math.min(3, this@encodeBase64String.size - i)
|
||||||
|
val data = ByteArray(3) {
|
||||||
|
if (it < read) {
|
||||||
|
this@encodeBase64String[it + i]
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val padSize = (data.size - read) * 8 / 6
|
||||||
|
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
|
||||||
|
((data[1].toInt() and 0xFF) shl 8) or
|
||||||
|
(data[2].toInt() and 0xFF)
|
||||||
|
|
||||||
|
for (index in data.size downTo padSize) {
|
||||||
|
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
|
||||||
|
append(char.toBase64())
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat(padSize) { append(BASE64_PAD) }
|
||||||
|
|
||||||
|
i += read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
||||||
|
|
||||||
|
fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
||||||
|
fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
||||||
|
|
||||||
|
fun EncodedByteArray.decodeBase64(): SourceBytes {
|
||||||
|
val result = mutableListOf<Byte>()
|
||||||
|
val data = ByteArray(4)
|
||||||
|
(0 until size step 4).forEach { i ->
|
||||||
|
var read = 0
|
||||||
|
for (j in 0 until 4) {
|
||||||
|
if (j + i < size) {
|
||||||
|
data[j] = get(j + i)
|
||||||
|
read++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chunk = data.foldIndexed(0) { index, result, current ->
|
||||||
|
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
|
||||||
|
}
|
||||||
|
for (index in data.size - 2 downTo (data.size - read)) {
|
||||||
|
val origin = (chunk shr (8 * index)) and 0xff
|
||||||
|
result.add(origin.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toByteArray()
|
||||||
|
}
|
||||||
|
fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString()
|
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This typealias shows that corresponding [ByteArray] is an raw data
|
||||||
|
*/
|
||||||
|
typealias SourceBytes = ByteArray
|
||||||
|
/**
|
||||||
|
* This typealias shows that corresponding [String] is an raw data
|
||||||
|
*/
|
||||||
|
typealias SourceString = String
|
||||||
|
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
typealias MD5 = String
|
||||||
|
|
||||||
|
expect fun SourceBytes.md5(): MD5
|
||||||
|
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Base64Text {
|
||||||
|
|
||||||
|
val testText = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"""
|
||||||
|
val base64Text = """TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpTZWQgdXQgcGVyc3BpY2lhdGlzIHVuZGUgb21uaXMgaXN0ZSBuYXR1cyBlcnJvciBzaXQgdm9sdXB0YXRlbSBhY2N1c2FudGl1bSBkb2xvcmVtcXVlIGxhdWRhbnRpdW0sIHRvdGFtIHJlbSBhcGVyaWFtLCBlYXF1ZSBpcHNhIHF1YWUgYWIgaWxsbyBpbnZlbnRvcmUgdmVyaXRhdGlzIGV0IHF1YXNpIGFyY2hpdGVjdG8gYmVhdGFlIHZpdGFlIGRpY3RhIHN1bnQgZXhwbGljYWJvLiBOZW1vIGVuaW0gaXBzYW0gdm9sdXB0YXRlbSBxdWlhIHZvbHVwdGFzIHNpdCBhc3Blcm5hdHVyIGF1dCBvZGl0IGF1dCBmdWdpdCwgc2VkIHF1aWEgY29uc2VxdXVudHVyIG1hZ25pIGRvbG9yZXMgZW9zIHF1aSByYXRpb25lIHZvbHVwdGF0ZW0gc2VxdWkgbmVzY2l1bnQuIE5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCwgcXVpIGRvbG9yZW0gaXBzdW0gcXVpYSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIsIGFkaXBpc2NpIHZlbGl0LCBzZWQgcXVpYSBub24gbnVtcXVhbSBlaXVzIG1vZGkgdGVtcG9yYSBpbmNpZHVudCB1dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hbSBhbGlxdWFtIHF1YWVyYXQgdm9sdXB0YXRlbS4gVXQgZW5pbSBhZCBtaW5pbWEgdmVuaWFtLCBxdWlzIG5vc3RydW0gZXhlcmNpdGF0aW9uZW0gdWxsYW0gY29ycG9yaXMgc3VzY2lwaXQgbGFib3Jpb3NhbSwgbmlzaSB1dCBhbGlxdWlkIGV4IGVhIGNvbW1vZGkgY29uc2VxdWF0dXI/IFF1aXMgYXV0ZW0gdmVsIGV1bSBpdXJlIHJlcHJlaGVuZGVyaXQgcXVpIGluIGVhIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIHF1YW0gbmloaWwgbW9sZXN0aWFlIGNvbnNlcXVhdHVyLCB2ZWwgaWxsdW0gcXVpIGRvbG9yZW0gZXVtIGZ1Z2lhdCBxdW8gdm9sdXB0YXMgbnVsbGEgcGFyaWF0dXI/"""
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeEncode() {
|
||||||
|
val encoded = testText.encodeBase64String()
|
||||||
|
assertEquals(base64Text, encoded)
|
||||||
|
val decoded = encoded.decodeBase64String()
|
||||||
|
assertEquals(testText, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
val urlTestText = "https://example.com?isitexample=1"
|
||||||
|
val urlTestBase64Text = "aHR0cHM6Ly9leGFtcGxlLmNvbT9pc2l0ZXhhbXBsZT0x"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodeEncodeUrl() {
|
||||||
|
val encoded = urlTestText.encodeBase64String()
|
||||||
|
assertEquals(urlTestBase64Text, encoded)
|
||||||
|
val decoded = encoded.decodeBase64String()
|
||||||
|
assertEquals(urlTestText, decoded)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class MD5Test {
|
||||||
|
val source = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||||
|
val expectedMD5 = "db89bb5ceab87f9c0fcc2ab36c189c2c"
|
||||||
|
@Test
|
||||||
|
fun testMD5() {
|
||||||
|
var i = 0
|
||||||
|
source.md5().forEach {
|
||||||
|
assertEquals(expectedMD5[i], it)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
external interface CryptoJs {
|
||||||
|
fun MD5(data: String): String
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsModule("crypto-js")
|
||||||
|
@JsNonModule
|
||||||
|
external val CryptoJS: CryptoJs
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.inmo.micro_utils.crypto
|
||||||
|
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||||
|
1,
|
||||||
|
MessageDigest.getInstance("MD5").digest(this)
|
||||||
|
).toString(16)
|
1
crypto/src/main/AndroidManifest.xml
Normal file
1
crypto/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.crypto"/>
|
@@ -1,3 +1,30 @@
|
|||||||
|
apply plugin: 'com.getkeepsafe.dexcount'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
jvmKotlinFolderFile = {
|
||||||
|
String sep = File.separator
|
||||||
|
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
enableIncludingJvmCodeInAndroidPart = {
|
||||||
|
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||||
|
if (jvmKotlinFolder.exists()) {
|
||||||
|
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disableIncludingJvmCodeInAndroidPart = {
|
||||||
|
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||||
|
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||||
|
android.sourceSets.main.java.srcDirs = []
|
||||||
|
for (oldDir in oldDirs) {
|
||||||
|
if (oldDir != jvmKotlinFolder.path) {
|
||||||
|
android.sourceSets.main.java.srcDirs += oldDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||||
buildToolsVersion "$android_buildToolsVersion"
|
buildToolsVersion "$android_buildToolsVersion"
|
||||||
@@ -33,6 +60,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
String sep = File.separator
|
||||||
|
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||||
|
enableIncludingJvmCodeInAndroidPart()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,20 +2,18 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
id "org.jetbrains.dokka"
|
id "org.jetbrains.dokka"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
js(BOTH) {
|
js(IR) {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
nodejs()
|
||||||
}
|
}
|
||||||
|
@@ -20,11 +20,11 @@ allprojects {
|
|||||||
|
|
||||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||||
|
|
||||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization"
|
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject"
|
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject"
|
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||||
|
|
||||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings"
|
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||||
|
|
||||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
||||||
|
17
fsm/common/build.gradle
Normal file
17
fsm/common/build.gradle
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.coroutines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
val context: Any
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class StateHandlerHolder<I : State>(
|
||||||
|
private val inputKlass: KClass<I>,
|
||||||
|
private val strict: Boolean = false,
|
||||||
|
private val delegateTo: StatesHandler<I>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
|
||||||
|
|
||||||
|
override suspend fun StatesMachine.handleState(state: State): State? {
|
||||||
|
return delegateTo.run { handleState(state as I) }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
fun interface StatesHandler<I : State> {
|
||||||
|
suspend fun StatesMachine.handleState(state: I): State?
|
||||||
|
}
|
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.coroutines.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
|
||||||
|
private suspend fun <I : State> StatesMachine.launchStateHandling(
|
||||||
|
state: State,
|
||||||
|
handlers: List<StateHandlerHolder<out I>>
|
||||||
|
): State? {
|
||||||
|
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||||
|
handleState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatesMachine (
|
||||||
|
private val statesManager: StatesManager,
|
||||||
|
private val handlers: List<StateHandlerHolder<*>>
|
||||||
|
) : StatesHandler<State> {
|
||||||
|
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
|
||||||
|
|
||||||
|
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||||
|
val statePerformer: suspend (State) -> Unit = { state: State ->
|
||||||
|
val newState = launchStateHandling(state, handlers)
|
||||||
|
if (newState != null) {
|
||||||
|
statesManager.update(state, newState)
|
||||||
|
} else {
|
||||||
|
statesManager.endChain(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
launch { statePerformer(it) }
|
||||||
|
}
|
||||||
|
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
launch { statePerformer(it.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
statesManager.getActiveStates().forEach {
|
||||||
|
launch { statePerformer(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startChain(state: State) {
|
||||||
|
statesManager.startChain(state)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
interface StatesManager {
|
||||||
|
val onChainStateUpdated: Flow<Pair<State, State>>
|
||||||
|
val onStartChain: Flow<State>
|
||||||
|
val onEndChain: Flow<State>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must set current set using [State.context]
|
||||||
|
*/
|
||||||
|
suspend fun update(old: State, new: State)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
|
||||||
|
* busy by the other [State]
|
||||||
|
*/
|
||||||
|
suspend fun startChain(state: State)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
|
||||||
|
* ignored
|
||||||
|
*/
|
||||||
|
suspend fun endChain(state: State)
|
||||||
|
|
||||||
|
suspend fun getActiveStates(): List<State>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
|
||||||
|
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
|
||||||
|
* new state by using [endChain] with that state
|
||||||
|
*/
|
||||||
|
class InMemoryStatesManager(
|
||||||
|
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||||
|
) : StatesManager {
|
||||||
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onStartChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
private val contextsToStates = mutableMapOf<Any, State>()
|
||||||
|
private val mapMutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun update(old: State, new: State) = mapMutex.withLock {
|
||||||
|
when {
|
||||||
|
contextsToStates[old.context] != old -> return@withLock
|
||||||
|
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
|
||||||
|
contextsToStates[old.context] = new
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val stateOnNewOneContext = contextsToStates.getValue(new.context)
|
||||||
|
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
|
endChainWithoutLock(stateOnNewOneContext)
|
||||||
|
contextsToStates.remove(old.context)
|
||||||
|
contextsToStates[new.context] = new
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startChain(state: State) = mapMutex.withLock {
|
||||||
|
if (!contextsToStates.containsKey(state.context)) {
|
||||||
|
contextsToStates[state.context] = state
|
||||||
|
_onStartChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun endChainWithoutLock(state: State) {
|
||||||
|
if (contextsToStates[state.context] == state) {
|
||||||
|
contextsToStates.remove(state.context)
|
||||||
|
_onEndChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun endChain(state: State) {
|
||||||
|
mapMutex.withLock {
|
||||||
|
endChainWithoutLock(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.common.dsl
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.fsm.common.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class FSMBuilder(
|
||||||
|
var statesManager: StatesManager = InMemoryStatesManager()
|
||||||
|
) {
|
||||||
|
private var states = mutableListOf<StateHandlerHolder<*>>()
|
||||||
|
|
||||||
|
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, false, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
|
||||||
|
states.add(StateHandlerHolder(kClass, true, handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build() = StatesMachine(
|
||||||
|
statesManager,
|
||||||
|
states.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
|
||||||
|
add(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
|
||||||
|
addStrict(I::class, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildFSM(
|
||||||
|
block: FSMBuilder.() -> Unit
|
||||||
|
): StatesMachine = FSMBuilder().apply(block).build()
|
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
53
fsm/common/src/jvmTest/kotlin/PlayableMain.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import dev.inmo.micro_utils.fsm.common.*
|
||||||
|
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
|
||||||
|
import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
sealed interface TrafficLightState : State {
|
||||||
|
val trafficLightNumber: Int
|
||||||
|
override val context: Int
|
||||||
|
get() = trafficLightNumber
|
||||||
|
}
|
||||||
|
data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState
|
||||||
|
|
||||||
|
class PlayableMain {
|
||||||
|
// @Test
|
||||||
|
fun test() {
|
||||||
|
runBlocking {
|
||||||
|
val countOfTrafficLights = 10
|
||||||
|
val initialStates = (0 until countOfTrafficLights).map {
|
||||||
|
when (0/*Random.nextInt(3)*/) {
|
||||||
|
0 -> GreenCommon(it)
|
||||||
|
1 -> YellowCommon(it)
|
||||||
|
else -> RedCommon(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statesManager = InMemoryStatesManager()
|
||||||
|
|
||||||
|
val machine = buildFSM {
|
||||||
|
strictlyOn<GreenCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
YellowCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<YellowCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
RedCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
strictlyOn<RedCommon> {
|
||||||
|
delay(1000L)
|
||||||
|
GreenCommon(it.context).also(::println)
|
||||||
|
}
|
||||||
|
this.statesManager = statesManager
|
||||||
|
}
|
||||||
|
|
||||||
|
initialStates.forEach { machine.startChain(it) }
|
||||||
|
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
machine.start(scope).join()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
fsm/common/src/main/AndroidManifest.xml
Normal file
1
fsm/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.common"/>
|
18
fsm/repos/common/build.gradle
Normal file
18
fsm/repos/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.fsm.common")
|
||||||
|
api project(":micro_utils.repos.common")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.inmo.micro_utils.fsm.repos.common
|
||||||
|
|
||||||
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
|
import dev.inmo.micro_utils.fsm.common.StatesManager
|
||||||
|
import dev.inmo.micro_utils.repos.*
|
||||||
|
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||||
|
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
class KeyValueBasedStatesManager(
|
||||||
|
private val keyValueRepo: KeyValueRepo<Any, State>,
|
||||||
|
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
|
||||||
|
) : StatesManager {
|
||||||
|
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
|
||||||
|
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
private val _onEndChain = MutableSharedFlow<State>(0)
|
||||||
|
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
|
||||||
|
|
||||||
|
override val onStartChain: Flow<State> = keyValueRepo.onNewValue.map { it.second }
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
override suspend fun update(old: State, new: State) {
|
||||||
|
mutex.withLock {
|
||||||
|
when {
|
||||||
|
keyValueRepo.get(old.context) != old -> return@withLock
|
||||||
|
old.context == new.context || !keyValueRepo.contains(new.context) -> {
|
||||||
|
keyValueRepo.set(old.context, new)
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val stateOnNewOneContext = keyValueRepo.get(new.context)!!
|
||||||
|
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
|
||||||
|
endChainWithoutLock(stateOnNewOneContext)
|
||||||
|
keyValueRepo.unset(old.context)
|
||||||
|
keyValueRepo.set(new.context, new)
|
||||||
|
_onChainStateUpdated.emit(old to new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun startChain(state: State) {
|
||||||
|
if (!keyValueRepo.contains(state.context)) {
|
||||||
|
keyValueRepo.set(state.context, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun endChainWithoutLock(state: State) {
|
||||||
|
if (keyValueRepo.get(state.context) == state) {
|
||||||
|
keyValueRepo.unset(state.context)
|
||||||
|
_onEndChain.emit(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun endChain(state: State) {
|
||||||
|
mutex.withLock { endChainWithoutLock(state) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getActiveStates(): List<State> {
|
||||||
|
return keyValueRepo.getAll { keys(it) }.map { it.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified TargetContextType, reified TargetStateType> createStatesManager(
|
||||||
|
targetKeyValueRepo: KeyValueRepo<TargetContextType, TargetStateType>,
|
||||||
|
noinline contextToOutTransformer: suspend Any.() -> TargetContextType,
|
||||||
|
noinline stateToOutTransformer: suspend State.() -> TargetStateType,
|
||||||
|
noinline outToContextTransformer: suspend TargetContextType.() -> Any,
|
||||||
|
noinline outToStateTransformer: suspend TargetStateType.() -> State,
|
||||||
|
) = KeyValueBasedStatesManager(
|
||||||
|
targetKeyValueRepo.withMapper<Any, State, TargetContextType, TargetStateType>(
|
||||||
|
contextToOutTransformer,
|
||||||
|
stateToOutTransformer,
|
||||||
|
outToContextTransformer,
|
||||||
|
outToStateTransformer
|
||||||
|
)
|
||||||
|
)
|
1
fsm/repos/common/src/main/AndroidManifest.xml
Normal file
1
fsm/repos/common/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="dev.inmo.micro_utils.fsm.repos.common"/>
|
@@ -5,41 +5,45 @@ kotlin.incremental=true
|
|||||||
kotlin.incremental.js=true
|
kotlin.incremental.js=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
org.gradle.jvmargs=-Xmx3072m
|
org.gradle.jvmargs=-Xmx2g
|
||||||
|
|
||||||
kotlin_version=1.4.10
|
kotlin_version=1.5.30
|
||||||
kotlin_coroutines_version=1.4.1
|
kotlin_coroutines_version=1.5.2
|
||||||
kotlin_serialisation_core_version=1.0.1
|
kotlin_serialisation_core_version=1.2.2
|
||||||
kotlin_exposed_version=0.28.1
|
kotlin_exposed_version=0.34.1
|
||||||
|
|
||||||
ktor_version=1.4.2
|
ktor_version=1.6.3
|
||||||
|
|
||||||
klockVersion=1.12.1
|
klockVersion=2.4.1
|
||||||
|
|
||||||
gradle_bintray_plugin_version=1.8.5
|
|
||||||
github_release_plugin_version=2.2.12
|
github_release_plugin_version=2.2.12
|
||||||
|
|
||||||
uuidVersion=0.2.2
|
uuidVersion=0.3.1
|
||||||
|
|
||||||
# ANDROID
|
# ANDROID
|
||||||
|
|
||||||
core_ktx_version=1.3.2
|
core_ktx_version=1.6.0
|
||||||
androidx_recycler_version=1.1.0
|
androidx_recycler_version=1.2.1
|
||||||
|
appcompat_version=1.3.1
|
||||||
|
|
||||||
android_minSdkVersion=19
|
android_minSdkVersion=19
|
||||||
android_compileSdkVersion=30
|
android_compileSdkVersion=30
|
||||||
android_buildToolsVersion=30.0.2
|
android_buildToolsVersion=30.0.3
|
||||||
dexcount_version=2.0.0-RC1
|
dexcount_version=3.0.0
|
||||||
junit_version=4.12
|
junit_version=4.12
|
||||||
test_ext_junit_version=1.1.2
|
test_ext_junit_version=1.1.2
|
||||||
espresso_core=3.3.0
|
espresso_core=3.3.0
|
||||||
|
|
||||||
|
# JS NPM
|
||||||
|
|
||||||
|
crypto_js_version=4.1.1
|
||||||
|
|
||||||
# Dokka
|
# Dokka
|
||||||
|
|
||||||
dokka_version=1.4.0
|
dokka_version=1.5.0
|
||||||
|
|
||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.3.3
|
version=0.5.25
|
||||||
android_code_version=3
|
android_code_version=66
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -25,7 +25,7 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
val producerScope = this@channelFlow
|
val producerScope = this@channelFlow
|
||||||
do {
|
do {
|
||||||
val reconnect = try {
|
val reconnect = try {
|
||||||
safely ({ throw it }) {
|
safely {
|
||||||
ws(correctedUrl) {
|
ws(correctedUrl) {
|
||||||
for (received in incoming) {
|
for (received in incoming) {
|
||||||
when (received) {
|
when (received) {
|
||||||
@@ -64,10 +64,11 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
|
|||||||
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
inline fun <T> HttpClient.createStandardWebsocketFlow(
|
||||||
url: String,
|
url: String,
|
||||||
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
deserializer: DeserializationStrategy<T>
|
deserializer: DeserializationStrategy<T>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = createStandardWebsocketFlow(
|
) = createStandardWebsocketFlow(
|
||||||
url,
|
url,
|
||||||
checkReconnection
|
checkReconnection
|
||||||
) {
|
) {
|
||||||
standardKtorSerialFormat.decodeDefault(deserializer, it)
|
serialFormat.decodeDefault(deserializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -4,22 +4,57 @@ import dev.inmo.micro_utils.ktor.common.*
|
|||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.SerializationStrategy
|
|
||||||
|
|
||||||
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
|
||||||
|
|
||||||
|
class UnifiedRequester(
|
||||||
|
private val client: HttpClient = HttpClient(),
|
||||||
|
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
) {
|
||||||
|
suspend fun <ResultType> uniget(
|
||||||
|
url: String,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
|
): ResultType = client.uniget(url, resultDeserializer, serialFormat)
|
||||||
|
|
||||||
|
fun <T> encodeUrlQueryValue(
|
||||||
|
serializationStrategy: SerializationStrategy<T>,
|
||||||
|
value: T
|
||||||
|
) = serializationStrategy.encodeUrlQueryValue(
|
||||||
|
value,
|
||||||
|
serialFormat
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <BodyType, ResultType> unipost(
|
||||||
|
url: String,
|
||||||
|
bodyInfo: BodyPair<BodyType>,
|
||||||
|
resultDeserializer: DeserializationStrategy<ResultType>
|
||||||
|
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
|
||||||
|
|
||||||
|
fun <T> createStandardWebsocketFlow(
|
||||||
|
url: String,
|
||||||
|
checkReconnection: (Throwable?) -> Boolean = { true },
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultRequester = UnifiedRequester()
|
||||||
|
|
||||||
suspend fun <ResultType> HttpClient.uniget(
|
suspend fun <ResultType> HttpClient.uniget(
|
||||||
url: String,
|
url: String,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = get<StandardKtorSerialInputData>(
|
) = get<StandardKtorSerialInputData>(
|
||||||
url
|
url
|
||||||
).let {
|
).let {
|
||||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = standardKtorSerialFormat.encodeHex(
|
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
|
||||||
|
value: T,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
|
) = serialFormat.encodeHex(
|
||||||
this,
|
this,
|
||||||
value
|
value
|
||||||
)
|
)
|
||||||
@@ -27,9 +62,10 @@ fun <T> SerializationStrategy<T>.encodeUrlQueryValue(value: T) = standardKtorSer
|
|||||||
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
suspend fun <BodyType, ResultType> HttpClient.unipost(
|
||||||
url: String,
|
url: String,
|
||||||
bodyInfo: BodyPair<BodyType>,
|
bodyInfo: BodyPair<BodyType>,
|
||||||
resultDeserializer: DeserializationStrategy<ResultType>
|
resultDeserializer: DeserializationStrategy<ResultType>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = post<StandardKtorSerialInputData>(url) {
|
) = post<StandardKtorSerialInputData>(url) {
|
||||||
body = standardKtorSerialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
|
||||||
}.let {
|
}.let {
|
||||||
standardKtorSerialFormat.decodeDefault(resultDeserializer, it)
|
serialFormat.decodeDefault(resultDeserializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "org.jetbrains.kotlin.plugin.serialization"
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
id "com.android.library"
|
id "com.android.library"
|
||||||
id "kotlin-android-extensions"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
apply from: "$mppProjectWithSerializationPresetPath"
|
||||||
|
@@ -3,16 +3,28 @@ package dev.inmo.micro_utils.ktor.common
|
|||||||
typealias QueryParam = Pair<String, String?>
|
typealias QueryParam = Pair<String, String?>
|
||||||
typealias QueryParams = Map<String, String?>
|
typealias QueryParams = Map<String, String?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with &
|
||||||
|
*/
|
||||||
val QueryParams.asUrlQuery: String
|
val QueryParams.asUrlQuery: String
|
||||||
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
|
get() = keys.joinToString("&") { "${it}${get(it) ?.let { value -> "=$value" }}" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with &
|
||||||
|
*/
|
||||||
val List<QueryParam>.asUrlQuery: String
|
val List<QueryParam>.asUrlQuery: String
|
||||||
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
|
get() = joinToString("&") { (key, value) -> "${key}${value ?.let { _ -> "=$value" }}" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||||
|
*/
|
||||||
fun String.includeQueryParams(
|
fun String.includeQueryParams(
|
||||||
queryParams: QueryParams
|
queryParams: QueryParams
|
||||||
): String = "$this${if(queryParams.isNotEmpty()) "${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" else ""}"
|
): String = "$this${if(queryParams.isNotEmpty()) "${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}" else ""}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create query part which includes key=value pairs separated with & and attach to receiver
|
||||||
|
*/
|
||||||
fun String.includeQueryParams(
|
fun String.includeQueryParams(
|
||||||
queryParams: List<QueryParam>
|
queryParams: List<QueryParam>
|
||||||
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
|
): String = "$this${if (contains("?")) "&" else "?"}${queryParams.asUrlQuery}"
|
||||||
|
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.serialization.SerializationStrategy
|
import kotlinx.serialization.SerializationStrategy
|
||||||
|
|
||||||
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
|
||||||
if (incoming.poll() != null) {
|
if (incoming.tryReceive() != null) {
|
||||||
close()
|
close()
|
||||||
throw CorrectCloseException
|
throw CorrectCloseException
|
||||||
}
|
}
|
||||||
@@ -33,10 +33,11 @@ fun <T> Route.includeWebsocketHandling(
|
|||||||
fun <T> Route.includeWebsocketHandling(
|
fun <T> Route.includeWebsocketHandling(
|
||||||
suburl: String,
|
suburl: String,
|
||||||
flow: Flow<T>,
|
flow: Flow<T>,
|
||||||
serializer: SerializationStrategy<T>
|
serializer: SerializationStrategy<T>,
|
||||||
|
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
|
||||||
) = includeWebsocketHandling(
|
) = includeWebsocketHandling(
|
||||||
suburl,
|
suburl,
|
||||||
flow
|
flow
|
||||||
) {
|
) {
|
||||||
standardKtorSerialFormat.encodeDefault(serializer, it)
|
serialFormat.encodeDefault(serializer, it)
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,87 @@ package dev.inmo.micro_utils.ktor.server
|
|||||||
import dev.inmo.micro_utils.coroutines.safely
|
import dev.inmo.micro_utils.coroutines.safely
|
||||||
import dev.inmo.micro_utils.ktor.common.*
|
import dev.inmo.micro_utils.ktor.common.*
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.request.receive
|
import io.ktor.request.receive
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondBytes
|
import io.ktor.response.respondBytes
|
||||||
import kotlinx.serialization.DeserializationStrategy
|
import io.ktor.routing.Route
|
||||||
import kotlinx.serialization.SerializationStrategy
|
import io.ktor.util.pipeline.PipelineContext
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
|
class UnifiedRouter(
|
||||||
|
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
|
||||||
|
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
|
||||||
|
) {
|
||||||
|
fun <T> Route.includeWebsocketHandling(
|
||||||
|
suburl: String,
|
||||||
|
flow: Flow<T>,
|
||||||
|
serializer: SerializationStrategy<T>
|
||||||
|
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
|
||||||
|
answerSerializer: SerializationStrategy<T>,
|
||||||
|
answer: T
|
||||||
|
) {
|
||||||
|
call.respondBytes (
|
||||||
|
serialFormat.encodeDefault(answerSerializer, answer),
|
||||||
|
serialFormatContentType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.uniload(
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = safely {
|
||||||
|
serialFormat.decodeDefault(
|
||||||
|
deserializer,
|
||||||
|
call.receive()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun PipelineContext<*, ApplicationCall>.getParameterOrSendError(
|
||||||
|
field: String
|
||||||
|
) = call.parameters[field].also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PipelineContext<*, ApplicationCall>.getQueryParameter(
|
||||||
|
field: String
|
||||||
|
) = call.request.queryParameters[field]
|
||||||
|
|
||||||
|
suspend fun PipelineContext<*, ApplicationCall>.getQueryParameterOrSendError(
|
||||||
|
field: String
|
||||||
|
) = getQueryParameter(field).also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValue(
|
||||||
|
field: String,
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = getQueryParameter(field) ?.let {
|
||||||
|
serialFormat.decodeHex(
|
||||||
|
deserializer,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> PipelineContext<*, ApplicationCall>.decodeUrlQueryValueOrSendError(
|
||||||
|
field: String,
|
||||||
|
deserializer: DeserializationStrategy<T>
|
||||||
|
) = decodeUrlQueryValue(field, deserializer).also {
|
||||||
|
if (it == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultUnifiedRouter = UnifiedRouter()
|
||||||
|
|
||||||
suspend fun <T> ApplicationCall.unianswer(
|
suspend fun <T> ApplicationCall.unianswer(
|
||||||
answerSerializer: SerializationStrategy<T>,
|
answerSerializer: SerializationStrategy<T>,
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
data class ApplicationCachingHeadersConfigurator(
|
data class ApplicationCachingHeadersConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(CachingHeaders) {
|
install(CachingHeaders) {
|
||||||
|
@@ -10,17 +10,18 @@ import kotlinx.serialization.Serializable
|
|||||||
class ApplicationRoutingConfigurator(
|
class ApplicationRoutingConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Route.invoke() }
|
fun interface Element { operator fun Route.invoke() }
|
||||||
|
private val rootInstaller = Element {
|
||||||
|
elements.forEach {
|
||||||
|
it.apply { invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
try {
|
featureOrNull(Routing) ?.apply {
|
||||||
feature(Routing)
|
rootInstaller.apply { invoke() }
|
||||||
} catch (e: IllegalStateException) {
|
} ?: install(Routing) {
|
||||||
install(Routing) {
|
rootInstaller.apply { invoke() }
|
||||||
elements.forEach {
|
|
||||||
it.apply { invoke() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class ApplicationSessionsConfigurator(
|
class ApplicationSessionsConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun Sessions.Configuration.invoke() }
|
fun interface Element { operator fun Sessions.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
|
@@ -8,7 +8,7 @@ import kotlinx.serialization.Contextual
|
|||||||
class StatusPagesConfigurator(
|
class StatusPagesConfigurator(
|
||||||
private val elements: List<@Contextual Element>
|
private val elements: List<@Contextual Element>
|
||||||
) : KtorApplicationConfigurator {
|
) : KtorApplicationConfigurator {
|
||||||
interface Element { operator fun StatusPages.Configuration.invoke() }
|
fun interface Element { operator fun StatusPages.Configuration.invoke() }
|
||||||
|
|
||||||
override fun Application.configure() {
|
override fun Application.configure() {
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user