├── hello-vector
├── .gitignore
├── debug.keystore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ └── fragment_message.xml
│ │ │ └── drawable-v24
│ │ │ │ ├── ic_vector.xml
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── haroldadmin
│ │ │ │ └── hellovector
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── HelloState.kt
│ │ │ │ ├── HelloFragment.kt
│ │ │ │ └── HelloViewModel.kt
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── haroldadmin
│ │ └── hellovector
│ │ ├── HelloFragmentTest.kt
│ │ └── HelloViewModelTest.kt
├── proguard-rules.pro
└── build.gradle
├── sampleapp
├── .gitignore
├── debug.keystore
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── menu
│ │ │ └── menu_main.xml
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable
│ │ │ ├── ic_round_add_24px.xml
│ │ │ ├── ic_round_check_24px.xml
│ │ │ └── ic_vector.xml
│ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── item_entity.xml
│ │ │ ├── fragment_entities.xml
│ │ │ ├── fragment_about.xml
│ │ │ └── fragment_add_entity.xml
│ │ ├── navigation
│ │ │ └── nav_graph.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── com
│ │ │ └── haroldadmin
│ │ │ └── sampleapp
│ │ │ ├── entities
│ │ │ ├── Injector.kt
│ │ │ ├── EntitiesState.kt
│ │ │ ├── EntitiesComponent.kt
│ │ │ ├── EntitiesViewModel.kt
│ │ │ ├── EntitiesAdapter.kt
│ │ │ └── EntitiesFragment.kt
│ │ │ ├── addEditEntity
│ │ │ ├── Injector.kt
│ │ │ ├── AddEditEntityState.kt
│ │ │ ├── AddEditEntityComponent.kt
│ │ │ └── AddEditEntityFragment.kt
│ │ │ ├── about
│ │ │ ├── AboutState.kt
│ │ │ └── AboutFragment.kt
│ │ │ ├── RepositoryModule.kt
│ │ │ ├── utils
│ │ │ ├── ColourAdapter.kt
│ │ │ ├── Extensions.kt
│ │ │ └── Provider.kt
│ │ │ ├── repository
│ │ │ ├── Colour.kt
│ │ │ └── EntitiesRepository.kt
│ │ │ ├── EntityCounter.kt
│ │ │ ├── AppViewModel.kt
│ │ │ ├── AppComponent.kt
│ │ │ └── MainActivity.kt
│ │ ├── sqldelight
│ │ └── com
│ │ │ └── haroldadmin
│ │ │ └── sampleapp
│ │ │ └── CountingEntity.sq
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── vector
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── haroldadmin
│ │ │ └── vector
│ │ │ ├── Vector.kt
│ │ │ ├── VectorState.kt
│ │ │ ├── state
│ │ │ ├── StateStoreImpl.kt
│ │ │ ├── StateHolderFactory.kt
│ │ │ ├── StateStore.kt
│ │ │ ├── StateHolderImpl.kt
│ │ │ ├── StateProcessorFactory.kt
│ │ │ ├── StateHolder.kt
│ │ │ ├── StateStoreFactory.kt
│ │ │ └── StateProcessor.kt
│ │ │ ├── loggers
│ │ │ ├── SystemOutLogger.kt
│ │ │ ├── AndroidLogger.kt
│ │ │ └── Logger.kt
│ │ │ ├── VectorSavedStateViewModelFactory.kt
│ │ │ ├── vectorLazy.kt
│ │ │ ├── VectorViewModelFactory.kt
│ │ │ ├── SavedStateVectorViewModel.kt
│ │ │ ├── ReflectionExtensions.kt
│ │ │ ├── VectorStateFactory.kt
│ │ │ ├── Extensions.kt
│ │ │ ├── ViewModelOwner.kt
│ │ │ ├── VectorViewModel.kt
│ │ │ └── VectorFragment.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── haroldadmin
│ │ └── vector
│ │ ├── state
│ │ ├── CountingState.kt
│ │ ├── StateStoreTest.kt
│ │ └── StateHolderTest.kt
│ │ ├── VectorLazyTest.kt
│ │ ├── extensions
│ │ ├── CompletionExtensionTest.kt
│ │ ├── CompletionExtensions.kt
│ │ ├── ChannelComputeExtensionTest.kt
│ │ ├── ReflectionExtensionsTest.kt
│ │ └── WithStateExtensionTest.kt
│ │ ├── ViewModelOwnerTest.kt
│ │ ├── loggers
│ │ ├── StringLogger.kt
│ │ └── LoggersTest.kt
│ │ ├── VectorViewModelTest.kt
│ │ ├── VectorStateFactoryTest.kt
│ │ ├── SavedStateVectorViewModelTest.kt
│ │ ├── ViewModelExtensionsTest.kt
│ │ └── VectorFragmentTest.kt
├── proguard-rules.pro
├── consumer-rules.pro
└── build.gradle
├── benchmark
├── .gitignore
├── src
│ ├── main
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ ├── java
│ │ └── com
│ │ │ └── haroldadmin
│ │ │ └── vector
│ │ │ └── benchmark
│ │ │ ├── TestState.kt
│ │ │ ├── reflection
│ │ │ ├── NewInstanceCreation.kt
│ │ │ ├── PrimaryConstructorAccess.kt
│ │ │ ├── UnderlyingClassAccess.kt
│ │ │ └── SuperClassCheckBenchMark.kt
│ │ │ ├── StateFlowConflatedChannelBenchmark.kt
│ │ │ ├── regularStateStore
│ │ │ ├── StateStoreBenchmark.kt
│ │ │ ├── RegularStateStore.kt
│ │ │ └── RegularStateStoreImpl.kt
│ │ │ └── actorStateStore
│ │ │ ├── ActorsStateStore.kt
│ │ │ ├── StateStoreBenchmark.kt
│ │ │ └── ActorsStateStoreImpl.kt
│ │ └── AndroidManifest.xml
├── benchmark-proguard-rules.pro
└── build.gradle
├── assets
└── i-am-not-a-designer.sketch
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .github
├── workflows
│ ├── android.yml
│ ├── code-style.yml
│ └── release.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── requirements.txt
├── ktlint.gradle
├── docs
├── components
│ ├── logging.md
│ ├── vector-fragment.md
│ ├── saved-state-vectorviewmodel.md
│ └── vector-state.md
├── misc
│ ├── state-store-context.md
│ └── automatic-viewmodel-creation.md
├── index.md
└── images
│ └── logo-monochrome.svg
├── generateApiReference.py
├── gradle.properties
├── mkdocs.yml
├── .gitignore
├── gradlew.bat
└── README.md
/hello-vector/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sampleapp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | # VS Code
3 | .settings
4 | .project
5 | .classpath
--------------------------------------------------------------------------------
/vector/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | # VS Code
3 | .settings
4 | .project
5 | .classpath
--------------------------------------------------------------------------------
/benchmark/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 |
3 | # VS Code
4 | .settings
5 | .project
6 | .classpath
--------------------------------------------------------------------------------
/sampleapp/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/debug.keystore
--------------------------------------------------------------------------------
/hello-vector/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/debug.keystore
--------------------------------------------------------------------------------
/assets/i-am-not-a-designer.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/assets/i-am-not-a-designer.sketch
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/vector/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Vector
3 |
4 |
--------------------------------------------------------------------------------
/benchmark/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':vector'
2 | include ':sampleapp'
3 | include ':benchmark'
4 | include ':hello-vector'
5 | rootProject.name='Vector'
6 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/vector/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/sampleapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/haroldadmin/Vector/HEAD/hello-vector/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/hello-vector/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Hello Vector
3 | Get Message
4 |
5 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/TestState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark
2 |
3 | import com.haroldadmin.vector.VectorState
4 |
5 | internal data class TestState(val count: Int = 0) : VectorState
--------------------------------------------------------------------------------
/hello-vector/src/main/java/com/haroldadmin/hellovector/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 |
5 | class MainActivity : AppCompatActivity(R.layout.activity_main)
6 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Apr 27 01:15:04 IST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/Injector.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import com.haroldadmin.sampleapp.EntityCounter
4 |
5 | fun EntitiesFragment.inject() {
6 | (requireActivity().application as EntityCounter)
7 | .appComponent
8 | .inject(this)
9 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/addEditEntity/Injector.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.addEditEntity
2 |
3 | import com.haroldadmin.sampleapp.EntityCounter
4 |
5 | fun AddEditEntityFragment.inject() {
6 | (requireActivity().application as EntityCounter)
7 | .appComponent
8 | .inject(this)
9 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/state/CountingState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import android.os.Parcelable
4 | import com.haroldadmin.vector.VectorState
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @Parcelize
8 | internal data class CountingState(val count: Int = 0) : VectorState, Parcelable
9 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/EntitiesState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import com.haroldadmin.sampleapp.CountingEntity
4 | import com.haroldadmin.vector.VectorState
5 |
6 | data class EntitiesState(
7 | val entities: List? = null,
8 | val isLoading: Boolean = false
9 | ) : VectorState
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/about/AboutState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.about
2 |
3 | import com.haroldadmin.sampleapp.BuildConfig
4 | import com.haroldadmin.vector.VectorState
5 |
6 | data class AboutState(
7 | val appVersion: String = BuildConfig.VERSION_NAME,
8 | val libraryVersion: String = com.haroldadmin.vector.BuildConfig.VERSION_NAME
9 | ) : VectorState
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 |
11 | - name: set up JDK 1.8
12 | uses: actions/setup-java@v1
13 | with:
14 | java-version: 1.8
15 |
16 | - name: Build with Gradle
17 | run: ./gradlew testDebugUnitTest
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/code-style.yml:
--------------------------------------------------------------------------------
1 | name: Code-style checks
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 |
11 | - name: set up JDK 1.8
12 | uses: actions/setup-java@v1
13 | with:
14 | java-version: 1.8
15 |
16 | - name: Ktlint check
17 | run: ./gradlew ktlint
18 |
19 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | astroid==2.3.0
2 | Click==7.0
3 | htmlmin==0.1.12
4 | isort==4.3.21
5 | Jinja2==2.10.1
6 | jsmin==2.2.2
7 | lazy-object-proxy==1.4.2
8 | livereload==2.6.1
9 | Markdown==3.1.1
10 | MarkupSafe==1.1.1
11 | mccabe==0.6.1
12 | mkdocs==1.0.4
13 | mkdocs-material==4.4.2
14 | mkdocs-minify-plugin==0.2.1
15 | pep562==1.0
16 | Pygments==2.4.2
17 | pylint==2.4.1
18 | pymdown-extensions==6.1
19 | PyYAML==5.1.2
20 | six==1.12.0
21 | tornado==6.0.3
22 | typed-ast==1.4.0
23 | wrapt==1.11.2
24 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/Vector.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | /**
4 | * Singleton object to configure the library
5 | *
6 | */
7 | object Vector {
8 |
9 | /**
10 | * Enables/Disables logging based on its value
11 | *
12 | * If true, then all loggers shall write their logs.
13 | * If false, then no loggers shall write any logs.
14 | *
15 | * Can be changed at runtime.
16 | */
17 | var enableLogging: Boolean = false
18 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | /**
4 | * A state object contains all the necessary information to
5 | * render the view.
6 | *
7 | * Can be used with Kotlin data classes to allow for easy mutation
8 | * using the generated `copy` function.
9 | *
10 | * Example:
11 | * ```kotlin
12 | * data class NotesListState(
13 | * val notes: List = listOf()
14 | * ): VectorState
15 | * ```
16 | */
17 | interface VectorState
--------------------------------------------------------------------------------
/hello-vector/src/main/java/com/haroldadmin/hellovector/HelloState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import android.os.Parcelable
4 | import com.haroldadmin.vector.VectorState
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @Parcelize
8 | data class HelloState(
9 | val message: String = loadingMessage
10 | ) : VectorState, Parcelable {
11 | companion object {
12 | const val loadingMessage = "Loading..."
13 | const val helloMessage = "Hello, World!"
14 | }
15 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/drawable/ic_round_add_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/drawable/ic_round_check_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/ktlint.gradle:
--------------------------------------------------------------------------------
1 | repositories {
2 | jcenter()
3 | }
4 |
5 | configurations {
6 | ktlint
7 | }
8 |
9 | dependencies {
10 | ktlint "com.pinterest:ktlint:0.33.0"
11 | }
12 |
13 | task ktlint(type: JavaExec, group: "verification") {
14 | description = "Check Kotlin code style."
15 | classpath = configurations.ktlint
16 | main = "com.pinterest.ktlint.Main"
17 | args "src/**/*.kt"
18 | }
19 |
20 | task ktlintFormat(type: JavaExec, group: "formatting") {
21 | description = "Fix Kotlin code style deviations."
22 | classpath = configurations.ktlint
23 | main = "com.pinterest.ktlint.Main"
24 | args "-F", "src/**/*.kt"
25 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/VectorLazyTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import org.junit.Before
4 | import org.junit.Test
5 |
6 | class VectorLazyTest {
7 |
8 | lateinit var lazy: vectorLazy
9 |
10 | @Before
11 | fun setup() {
12 | lazy = vectorLazy { 42 }
13 | }
14 |
15 | @Test
16 | fun `should be instantiated lazily`() {
17 | assert(!lazy.isInitialized())
18 | lazy.value
19 | assert(lazy.isInitialized())
20 | }
21 |
22 | @Test
23 | fun `should hold correct value`() {
24 | val value = lazy.value
25 | assert(value == 42)
26 | }
27 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateStoreImpl.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.Logger
4 | import com.haroldadmin.vector.VectorState
5 | import com.haroldadmin.vector.loggers.logv
6 |
7 | /**
8 | * The default implementation of [StateStore]
9 | */
10 | internal class StateStoreImpl (
11 | holder: StateHolder,
12 | processor: StateProcessor,
13 | private val logger: Logger
14 | ) : StateStore(holder, processor) {
15 |
16 | override fun clear() {
17 | logger.logv { "Clearing State Store" }
18 | stateProcessor.clearProcessor()
19 | stateHolder.clearHolder()
20 | }
21 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/docs/components/logging.md:
--------------------------------------------------------------------------------
1 | # Logging
2 |
3 | Vector can log state related actions. It ships with two different loggers:
4 |
5 | * AndroidLogger: Writes log statements to Android log (`Log.*`). Can be created using the `androidLogger()` factory function.
6 | * SystemOutLogger: Writes log statements to STDOUT (`println`). Can be created using the `systemOutLogger()` factory function.
7 |
8 | By default, the `AndroidLogger` is used, but you can customize which ever one you want based on your needs. The `Logger` interface is very simple, and you can use it to create your custom implementations as well.
9 |
10 | ## Enable/Disable Logging
11 |
12 | You can enable or disable logging by setting `Vector.enableLogging`.
13 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
17 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Entity Counter
3 | Name
4 | Increase
5 | Decrease
6 | Entity Saved!
7 | This list is empty.\nAdd some entities by tapping the button below!
8 |
9 | Changes saved!
10 | Built with Vector
11 | Library logo
12 | "App Version: %1$s \nLibrary Version: %2$s
13 |
14 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/extensions/CompletionExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.extensions
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.launch
5 | import kotlinx.coroutines.runBlocking
6 | import org.junit.Test
7 |
8 | internal class CompletionExtensionTest {
9 |
10 | @Test
11 | fun `should finish executing only after complete has been called`() = runBlocking {
12 | var isCompleted = false
13 | awaitCompletion {
14 | launch(Dispatchers.Default) {
15 | launch(Dispatchers.Default) {
16 | isCompleted = true
17 | complete(Unit)
18 | }
19 | }
20 | }
21 | assert(isCompleted)
22 | }
23 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/EntitiesComponent.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.Subcomponent
6 | import dagger.android.AndroidInjector
7 | import dagger.multibindings.ClassKey
8 | import dagger.multibindings.IntoMap
9 |
10 | @Subcomponent
11 | interface EntitiesComponent : AndroidInjector {
12 | @Subcomponent.Factory
13 | interface Factory : AndroidInjector.Factory
14 | }
15 |
16 | @Module(subcomponents = [EntitiesComponent::class])
17 | interface EntitiesModule {
18 | @Binds
19 | @IntoMap
20 | @ClassKey(EntitiesFragment::class)
21 | fun bindEntitiesFragment(factory: EntitiesComponent.Factory): AndroidInjector.Factory<*>
22 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | If you can link to a sample project demonstrating this bug, please add it here
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Application Properties (please complete the following information):**
23 | - MinSDK, TargetSDK, CompileSDK
24 | - Device used
25 | - Android Version used for testing
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/generateApiReference.py:
--------------------------------------------------------------------------------
1 | import os, subprocess, shutil
2 | from pathlib import Path
3 |
4 | current_dir = Path(os.path.dirname(os.path.realpath(__file__)))
5 | api_ref_dir = current_dir.joinpath("docs/api/")
6 | gradlew_path = current_dir.joinpath("gradlew")
7 |
8 | def delete_existing_api_ref():
9 | print(f"API Reference directory: {api_ref_dir}")
10 | for path in api_ref_dir.glob("**/*"):
11 | print(f"Deleting {path}")
12 | if path.is_file():
13 | path.unlink()
14 | elif path.is_dir():
15 | shutil.rmtree(path)
16 |
17 | def generate_api_ref():
18 | print(f"Gradle wrapper at {gradlew_path}")
19 | print("Generating KDocs using Dokka...")
20 | subprocess.call([gradlew_path, ":vector:dokka"])
21 |
22 | if __name__ == "__main__":
23 | delete_existing_api_ref()
24 | generate_api_ref()
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateHolderFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.Logger
4 | import com.haroldadmin.vector.VectorState
5 |
6 | /**
7 | * A factory class to create instances of [StateHolder]
8 | */
9 | internal object StateHolderFactory {
10 |
11 | /**
12 | * Creates and returns a [StateHolder].
13 | *
14 | * @param initialState The initial state to be passed to the state holder
15 | * @param logger The logger to be used by the state holder for debug logs
16 | *
17 | * @return A class that implements the state holder interface
18 | */
19 | fun create(initialState: S, logger: Logger): StateHolder {
20 | return StateHolderImpl(initialState, logger)
21 | }
22 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/extensions/CompletionExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.extensions
2 |
3 | import kotlinx.coroutines.CompletableDeferred
4 | import kotlinx.coroutines.runBlocking
5 |
6 | /**
7 | * Takes in an [action] to be run, completes only after that action has completed and returns the result value.
8 | * The action is expected to call [complete] on the supplied [CompletableDeferred] with the an result value.
9 | *
10 | * Multiple invocations of complete have no effect, only the first call affects the result value.
11 | */
12 | internal fun awaitCompletion(action: suspend CompletableDeferred.() -> Unit): T = runBlocking {
13 | val completable = CompletableDeferred()
14 | action(completable)
15 | val result = completable.await()
16 | result
17 | }
18 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/loggers/SystemOutLogger.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.loggers
2 |
3 | import com.haroldadmin.vector.Vector
4 | import com.haroldadmin.vector.loggers.Logger.*
5 |
6 | /**
7 | * An implementation of [Logger] which writes logs to [System.out]
8 | *
9 | * Logs are only written if logging is enabled.
10 | */
11 | internal class SystemOutLogger(override val tag: String) : Logger {
12 |
13 | override fun log(message: String, level: Level) {
14 | if (!Vector.enableLogging) {
15 | return
16 | }
17 | when (level) {
18 | Level.DEBUG -> println("D/$tag: $message")
19 | Level.VERBOSE -> println("V/$tag: $message")
20 | }
21 | }
22 | }
23 |
24 | fun systemOutLogger(tag: String = "Vector"): Logger = SystemOutLogger(tag)
--------------------------------------------------------------------------------
/docs/misc/state-store-context.md:
--------------------------------------------------------------------------------
1 | # Coroutine Context for the State Store
2 |
3 | Every `VectorViewModel` has a backing `StateHolder` and a `StateStore`. The `StateHolder` is responsible for holding the current state, and the `StateStore` is responsible for processing state access/mutation blocks.
4 |
5 | All state related actions are processed off the main thread, in a sequential manner. The coroutine context for processing these actions can be customized using the ViewModel.
6 | Just pass in the desired context to the ViewModel's constructor.
7 |
8 | ```kotlin
9 | abstract class VectorViewModel(
10 | initialState: S?,
11 | stateStoreContext: CoroutineContext = Dispatchers.Default + Job(), // <- Change this parameter in your own implementations
12 | protected val logger: Logger = androidLogger()
13 | )
14 | ```
--------------------------------------------------------------------------------
/hello-vector/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/addEditEntity/AddEditEntityState.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.addEditEntity
2 |
3 | import android.os.Parcelable
4 | import com.haroldadmin.vector.VectorState
5 | import kotlinx.android.parcel.Parcelize
6 | import java.util.*
7 |
8 | sealed class AddEditEntityState : VectorState, Parcelable {
9 |
10 | @Parcelize
11 | data class AddEntity(
12 | val id: String = UUID.randomUUID().toString(),
13 | val name: String = "",
14 | val count: Long = 0,
15 | val isSaved: Boolean = false
16 | ) : AddEditEntityState()
17 |
18 | @Parcelize
19 | data class EditEntity(
20 | val id: String,
21 | val name: String = "",
22 | val count: Long = 0,
23 | val isSaved: Boolean = false
24 | ) : AddEditEntityState()
25 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/addEditEntity/AddEditEntityComponent.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.addEditEntity
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.Subcomponent
6 | import dagger.android.AndroidInjector
7 | import dagger.multibindings.ClassKey
8 | import dagger.multibindings.IntoMap
9 |
10 | @Subcomponent
11 | interface AddEditEntityComponent : AndroidInjector {
12 | @Subcomponent.Factory
13 | interface Factory : AndroidInjector.Factory
14 | }
15 |
16 | @Module(subcomponents = [AddEditEntityComponent::class])
17 | interface AddEditEntityModule {
18 | @Binds
19 | @IntoMap
20 | @ClassKey(AddEditEntityFragment::class)
21 | fun bindAddEditEntityFragment(factory: AddEditEntityComponent.Factory): AndroidInjector.Factory<*>
22 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/ViewModelOwnerTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.os.Bundle
4 | import org.junit.Test
5 |
6 | class ViewModelOwnerTest {
7 |
8 | @Test
9 | fun `Activity as a ViewModelOwner should contain the activity passed to it`() {
10 | val activity = TestActivity()
11 | val viewModelOwner = activity.activityViewModelOwner()
12 | viewModelOwner.activity()
13 | }
14 |
15 | @Test
16 | fun `Fragment as a ViewModelOwner should contain the fragment passed to it`() {
17 | val fragment = TestFragment().apply { arguments = Bundle.EMPTY }
18 | val viewModelOwner = fragment.fragmentViewModelOwner()
19 | viewModelOwner.fragment()
20 | assert(viewModelOwner.args() == Bundle.EMPTY)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp
2 |
3 | import android.content.Context
4 | import com.haroldadmin.sampleapp.utils.ColourAdapter
5 | import com.squareup.sqldelight.android.AndroidSqliteDriver
6 | import dagger.Module
7 | import dagger.Provides
8 |
9 | @Module
10 | object RepositoryModule {
11 | @JvmStatic
12 | @Provides
13 | fun database(context: Context): Database {
14 | val driver = AndroidSqliteDriver(Database.Schema, context, "countingEntities.db")
15 | val adapter = CountingEntity.Adapter(ColourAdapter)
16 | return Database.invoke(driver, adapter)
17 | }
18 |
19 | @JvmStatic
20 | @Provides
21 | fun countingQueries(database: Database): CountingEntityQueries {
22 | return database.countingEntityQueries
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/utils/ColourAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.utils
2 |
3 | import com.haroldadmin.sampleapp.repository.Colour
4 | import com.squareup.sqldelight.ColumnAdapter
5 |
6 | object ColourAdapter : ColumnAdapter {
7 | override fun decode(databaseValue: String): Colour {
8 | return when (databaseValue) {
9 | Colour.BLUE.toString() -> Colour.BLUE
10 | Colour.RED.toString() -> Colour.RED
11 | Colour.GREEN.toString() -> Colour.GREEN
12 | Colour.PINK.toString() -> Colour.PINK
13 | Colour.YELLOW.toString() -> Colour.YELLOW
14 | else -> throw IllegalArgumentException("Unknown colour value requested")
15 | }
16 | }
17 |
18 | override fun encode(value: Colour): String = value.toString()
19 | }
20 |
--------------------------------------------------------------------------------
/vector/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/loggers/StringLogger.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.loggers
2 |
3 | import com.haroldadmin.vector.Vector
4 | import com.haroldadmin.vector.loggers.Logger.*
5 | import java.lang.StringBuilder
6 |
7 | internal class StringLogger : Logger {
8 |
9 | private var logBuilder = StringBuilder()
10 |
11 | override val tag: String = "StringLogger"
12 |
13 | override fun log(message: String, level: Level) {
14 | if (!Vector.enableLogging) {
15 | return
16 | }
17 | when (level) {
18 | Level.DEBUG -> logBuilder.append("D/$tag: $message").append("\n")
19 | Level.VERBOSE -> logBuilder.append("V/$tag: $message").append("\n")
20 | }
21 | }
22 |
23 | fun clear() = logBuilder.clear()
24 |
25 | fun getLog() = logBuilder.toString()
26 | }
--------------------------------------------------------------------------------
/hello-vector/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/sampleapp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/repository/Colour.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.repository
2 |
3 | import kotlin.random.Random
4 | import kotlin.random.nextInt
5 |
6 | enum class Colour {
7 | RED {
8 | override fun toString(): String = "#ff1744"
9 | },
10 | BLUE {
11 | override fun toString(): String = "#42a5f5"
12 | },
13 | GREEN {
14 | override fun toString(): String = "#66bb6a"
15 | },
16 | YELLOW {
17 | override fun toString(): String = "#fbc02d"
18 | },
19 | PINK {
20 | override fun toString(): String = "#ec407a"
21 | }
22 | }
23 |
24 | fun getRandomColour() = when (Random(System.currentTimeMillis()).nextInt(1..5)) {
25 | 1 -> Colour.RED
26 | 2 -> Colour.BLUE
27 | 3 -> Colour.GREEN
28 | 4 -> Colour.YELLOW
29 | else -> Colour.PINK
30 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateStore.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.VectorState
4 |
5 | /**
6 | * A class which can hold current state as well as handle actions to be performed on it.
7 | *
8 | * @param stateHolder The delegate to handle [StateHolder] functions
9 | * @param stateProcessor The delegate to handle [StateProcessor] functions
10 | */
11 | abstract class StateStore(
12 | protected open val stateHolder: StateHolder,
13 | protected open val stateProcessor: StateProcessor
14 | ) : StateHolder by stateHolder, StateProcessor by stateProcessor {
15 |
16 | /**
17 | * Clear any resources held by this state store.
18 | * Implementations should also forward the call to [stateHolder] and [stateProcessor]
19 | */
20 | abstract fun clear()
21 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/loggers/AndroidLogger.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.loggers
2 |
3 | import android.util.Log
4 | import com.haroldadmin.vector.Vector
5 | import com.haroldadmin.vector.loggers.Logger.*
6 |
7 | /**
8 | * An implementation of [Logger] which writes out to the standard Android Log.
9 | *
10 | * Logs are only written if logging is enabled.
11 | */
12 | internal class AndroidLogger(override val tag: String) : Logger {
13 |
14 | override fun log(message: String, level: Level) {
15 | if (!Vector.enableLogging) {
16 | return
17 | }
18 | when (level) {
19 | Level.DEBUG -> Log.d(tag, message)
20 | Level.VERBOSE -> Log.v(tag, message)
21 | }
22 | }
23 | }
24 |
25 | /**
26 | * A utility function to create instances of [AndroidLogger]
27 | */
28 | fun androidLogger(tag: String): Logger = AndroidLogger(tag)
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/EntityCounter.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp
2 |
3 | import android.app.Application
4 | import com.haroldadmin.vector.Vector
5 | import dagger.android.AndroidInjector
6 | import dagger.android.DispatchingAndroidInjector
7 | import dagger.android.HasAndroidInjector
8 | import javax.inject.Inject
9 |
10 | class EntityCounter : Application(), HasAndroidInjector {
11 |
12 | @Inject lateinit var androidInjector: DispatchingAndroidInjector
13 |
14 | lateinit var appComponent: AppComponent
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | Vector.enableLogging = true
19 | appComponent = DaggerAppComponent
20 | .factory()
21 | .create(this)
22 | .also { comp -> comp.inject(this) }
23 | }
24 |
25 | override fun androidInjector(): AndroidInjector {
26 | return androidInjector
27 | }
28 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/sqldelight/com/haroldadmin/sampleapp/CountingEntity.sq:
--------------------------------------------------------------------------------
1 | import com.haroldadmin.sampleapp.repository.Colour;
2 |
3 | CREATE TABLE countingEntity (
4 | id TEXT NOT NULL,
5 | name TEXT NOT NULL,
6 | counter INTEGER NOT NULL,
7 | colour TEXT AS Colour NOT NULL,
8 | PRIMARY KEY(id)
9 | );
10 |
11 | getEntity:
12 | SELECT *
13 | FROM countingEntity
14 | WHERE id = :id;
15 |
16 | getCounterForEntity:
17 | SELECT counter
18 | FROM countingEntity
19 | WHERE id = :id;
20 |
21 | getAll:
22 | SELECT *
23 | FROM countingEntity;
24 |
25 | insert:
26 | INSERT OR REPLACE
27 | INTO countingEntity(id, name, counter, colour)
28 | VALUES(:id, :name, :counter, :colour);
29 |
30 | update:
31 | UPDATE countingEntity
32 | SET counter = :counter, name = :name, colour = :colour
33 | WHERE id = :id;
34 |
35 | delete:
36 | DELETE FROM countingEntity
37 | WHERE id = :id;
38 |
39 | getNumberOfEntities:
40 | SELECT COUNT(*)
41 | FROM countingEntity;
--------------------------------------------------------------------------------
/hello-vector/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
--------------------------------------------------------------------------------
/sampleapp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorSavedStateViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.os.Bundle
4 | import androidx.lifecycle.AbstractSavedStateViewModelFactory
5 | import androidx.lifecycle.SavedStateHandle
6 | import androidx.lifecycle.ViewModel
7 | import androidx.savedstate.SavedStateRegistryOwner
8 |
9 | /**
10 | * A class extending [AbstractSavedStateViewModelFactory] which takes in an initializer block
11 | * to create the requested viewmodel.
12 | */
13 | internal class VectorSavedStateViewModelFactory(
14 | owner: SavedStateRegistryOwner,
15 | defaultArgs: Bundle?,
16 | private val factory: (Class<*>, SavedStateHandle) -> V
17 | ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
18 | @Suppress("UNCHECKED_CAST")
19 | override fun create(
20 | key: String,
21 | modelClass: Class,
22 | handle: SavedStateHandle
23 | ) = factory(modelClass, handle) as T
24 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/loggers/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.loggers
2 |
3 | /**
4 | * Used to write debug logs.
5 | *
6 | * Implementations of this interface can be used to provide different output sources for log
7 | * statements, such as Android's built in Logging class, or STDOUT.
8 | */
9 | interface Logger {
10 |
11 | enum class Level {
12 | DEBUG, VERBOSE
13 | }
14 |
15 | /**
16 | * A name tag associated with this logger for identification and filtering
17 | */
18 | val tag: String
19 |
20 | /**
21 | * Logs the given message to the associated output
22 | *
23 | * @param message The message to be logged
24 | */
25 | fun log(message: String, level: Level = Level.DEBUG)
26 | }
27 |
28 | inline fun Logger.logd(crossinline messageProducer: () -> String) {
29 | log(messageProducer(), Logger.Level.DEBUG)
30 | }
31 |
32 | inline fun Logger.logv(crossinline messageProducer: () -> String) {
33 | log(messageProducer(), Logger.Level.VERBOSE)
34 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/EntitiesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.haroldadmin.sampleapp.repository.EntitiesRepository
5 | import com.haroldadmin.vector.VectorViewModel
6 | import com.squareup.inject.assisted.Assisted
7 | import com.squareup.inject.assisted.AssistedInject
8 | import kotlinx.coroutines.launch
9 |
10 | class EntitiesViewModel @AssistedInject constructor(
11 | @Assisted initialState: EntitiesState,
12 | private val repository: EntitiesRepository
13 | ) : VectorViewModel(initialState) {
14 |
15 | fun getAllEntities() = viewModelScope.launch {
16 | val entities = repository.getAllEntities()
17 | setState {
18 | val newState = copy(entities = entities, isLoading = false)
19 | newState
20 | }
21 | }
22 |
23 | @AssistedInject.Factory
24 | interface Factory {
25 | fun create(initialState: EntitiesState): EntitiesViewModel
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/state/StateStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.StringLogger
4 | import io.mockk.spyk
5 | import io.mockk.verify
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.test.TestCoroutineScope
8 | import org.junit.Test
9 |
10 | class StateStoreTest {
11 |
12 | private val testScope = TestCoroutineScope()
13 | private val initState = CountingState()
14 | private val logger = StringLogger()
15 |
16 | @Test
17 | fun `Clearing state store should also clear StateHolder and StateProcessor`() {
18 | val holder = spyk(StateHolderFactory.create(initState, logger))
19 | val processor = spyk(StateProcessorFactory.create(holder, logger, testScope.coroutineContext + Job()))
20 | val stateStore = StateStoreFactory.create(holder, processor, logger)
21 |
22 | stateStore.clear()
23 |
24 | verify(exactly = 1) { holder.clearHolder() }
25 | verify(exactly = 1) { processor.clearProcessor() }
26 | }
27 | }
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/reflection/NewInstanceCreation.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.reflection
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import org.junit.Ignore
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import kotlin.reflect.full.createInstance
11 |
12 | @RunWith(AndroidJUnit4::class)
13 | @Ignore("Don't run benchmarks with regular builds")
14 | internal class NewInstanceCreation {
15 |
16 | @get:Rule val benchmarkRule = BenchmarkRule()
17 |
18 | @Test
19 | fun kotlinReflectionNewInstanceCreation() = benchmarkRule.measureRepeated {
20 | val instance = ImplementingClass::class.createInstance()
21 | }
22 |
23 | @Test
24 | fun javaReflectionNewInstanceCreation() = benchmarkRule.measureRepeated {
25 | val instance = ImplementingClass::class.java.newInstance()
26 | }
27 |
28 | // Java Reflection version is 6x-7x faster than the kotlin version
29 | }
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/reflection/PrimaryConstructorAccess.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.reflection
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import org.junit.Ignore
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import kotlin.reflect.full.primaryConstructor
11 |
12 | @RunWith(AndroidJUnit4::class)
13 | @Ignore("Don't run benchmarks with regular builds")
14 | internal class PrimaryConstructorAccess {
15 |
16 | @get:Rule
17 | val benchmarkRule = BenchmarkRule()
18 |
19 | @Test
20 | fun accessThroughPrimaryConstructorProperty() = benchmarkRule.measureRepeated {
21 | val constructor = this::class.primaryConstructor
22 | }
23 |
24 | @Test
25 | fun accessThroughConstructorsListDotFirst() = benchmarkRule.measureRepeated {
26 | val constructor = this::class.constructors.first()
27 | }
28 |
29 | // Access through constructors list is faster
30 | }
31 |
--------------------------------------------------------------------------------
/hello-vector/src/main/java/com/haroldadmin/hellovector/HelloFragment.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import com.haroldadmin.vector.fragmentViewModel
9 | import com.haroldadmin.vector.renderState
10 | import kotlinx.android.synthetic.main.fragment_message.view.messageButton
11 | import kotlinx.android.synthetic.main.fragment_message.view.messageTextView
12 |
13 | class HelloFragment : Fragment() {
14 |
15 | private val viewModel: HelloViewModel by fragmentViewModel()
16 |
17 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
18 | val root = inflater.inflate(R.layout.fragment_message, container, false)
19 |
20 | root.messageButton.setOnClickListener {
21 | viewModel.getMessage()
22 | }
23 |
24 | renderState(viewModel) { state ->
25 | root.messageTextView.text = state.message
26 | }
27 |
28 | return root
29 | }
30 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/AppViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.haroldadmin.sampleapp.repository.EntitiesRepository
5 | import com.haroldadmin.vector.VectorState
6 | import com.haroldadmin.vector.VectorViewModel
7 | import com.squareup.inject.assisted.Assisted
8 | import com.squareup.inject.assisted.AssistedInject
9 | import kotlinx.coroutines.launch
10 |
11 | data class AppState(val numberOfEntities: Long = 0) : VectorState
12 |
13 | class AppViewModel @AssistedInject constructor(
14 | @Assisted initialState: AppState,
15 | private val repository: EntitiesRepository
16 | ) : VectorViewModel(initialState) {
17 |
18 | fun updateNumberOfEntities() = viewModelScope.launch {
19 | println("${this@AppViewModel}: Updating number of entities")
20 | val numberOfEntities = repository.getNumberOfEntities()
21 | setState { copy(numberOfEntities = numberOfEntities) }
22 | }
23 |
24 | @AssistedInject.Factory
25 | interface Factory {
26 | fun create(initialState: AppState): AppViewModel
27 | }
28 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateHolderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.Logger
4 | import com.haroldadmin.vector.VectorState
5 | import com.haroldadmin.vector.loggers.logv
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 |
9 | /**
10 | * The default implementation of [StateHolder]
11 | *
12 | * @param initialState The initial state value to put in the [stateObservable]
13 | * @param logger A logger which can be used to record debug logs
14 | */
15 | internal class StateHolderImpl(
16 | initialState: S,
17 | private val logger: Logger
18 | ) : StateHolder {
19 |
20 | private val _stateObservable = MutableStateFlow(initialState)
21 |
22 | override val stateObservable: StateFlow
23 | get() = _stateObservable
24 |
25 | override fun updateState(state: S) {
26 | _stateObservable.value = state
27 | }
28 |
29 | override fun clearHolder() {
30 | logger.logv { "Clearing State Holder" }
31 | // StateFlow does not need to be closed
32 | }
33 | }
--------------------------------------------------------------------------------
/hello-vector/src/main/java/com/haroldadmin/hellovector/HelloViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.viewModelScope
5 | import com.haroldadmin.vector.SavedStateVectorViewModel
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.delay
9 | import kotlinx.coroutines.launch
10 | import kotlin.coroutines.CoroutineContext
11 |
12 | class HelloViewModel(
13 | initialState: HelloState,
14 | coroutineContext: CoroutineContext = Dispatchers.Default + Job(),
15 | savedStateHandle: SavedStateHandle
16 | ) : SavedStateVectorViewModel(initialState, coroutineContext, savedStateHandle) {
17 |
18 | companion object {
19 | const val defaultDelay = 1000L
20 | }
21 |
22 | init {
23 | getMessage()
24 | }
25 |
26 | fun getMessage(delayDuration: Long = defaultDelay) = viewModelScope.launch {
27 | setStateAndPersist { copy(message = HelloState.loadingMessage) }
28 | delay(delayDuration)
29 | setStateAndPersist { copy(message = HelloState.helloMessage) }
30 | }
31 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp
2 |
3 | import android.content.Context
4 | import com.haroldadmin.sampleapp.addEditEntity.AddEditEntityFragment
5 | import com.haroldadmin.sampleapp.addEditEntity.AddEditEntityModule
6 | import com.haroldadmin.sampleapp.entities.EntitiesFragment
7 | import com.haroldadmin.sampleapp.entities.EntitiesModule
8 | import com.squareup.inject.assisted.dagger2.AssistedModule
9 | import dagger.BindsInstance
10 | import dagger.Component
11 | import dagger.Module
12 | import dagger.android.AndroidInjectionModule
13 |
14 | @Component(modules = [AppModule::class, EntitiesModule::class, AddEditEntityModule::class, RepositoryModule::class, AndroidInjectionModule::class])
15 | interface AppComponent {
16 |
17 | fun inject(application: EntityCounter)
18 | fun inject(entitiesFragment: EntitiesFragment)
19 | fun inject(addEditEntityFragment: AddEditEntityFragment)
20 |
21 | @Component.Factory
22 | interface Factory {
23 | fun create(@BindsInstance context: Context): AppComponent
24 | }
25 | }
26 |
27 | @AssistedModule
28 | @Module(includes = [AssistedInject_AppModule::class])
29 | object AppModule
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateProcessorFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.Logger
4 | import com.haroldadmin.vector.VectorState
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | /**
8 | * A Factory which produces instances of [StateProcessor]
9 | */
10 | internal object StateProcessorFactory {
11 |
12 | /**
13 | * Create and return an instance of [StateProcessor]
14 | *
15 | * @param S The state type to be associated with this processor
16 | * @param logger A logger to be supplied to the state processor
17 | * @param coroutineContext The context of execution of the state processor
18 | *
19 | * @return A class implementing StateProcessor
20 | */
21 | fun create(
22 | stateHolder: StateHolder,
23 | logger: Logger,
24 | coroutineContext: CoroutineContext
25 | ): StateProcessor {
26 | return SelectBasedStateProcessor(
27 | shouldStartImmediately = true,
28 | stateHolder = stateHolder,
29 | logger = logger,
30 | coroutineContext = coroutineContext
31 | )
32 | }
33 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/loggers/LoggersTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.loggers
2 |
3 | import com.haroldadmin.vector.Vector
4 | import org.junit.Test
5 |
6 | internal class LoggersTest {
7 | private val logger = StringLogger()
8 |
9 | @Test
10 | fun `Logging a string when logging disabled should produce no log`() {
11 | Vector.enableLogging = false
12 |
13 | val message = "Goodbye, world!"
14 | logger.log(message)
15 |
16 | assert(logger.getLog().isBlank())
17 | }
18 |
19 | @Test
20 | fun `Logging a string produces an output log`() {
21 | Vector.enableLogging = true
22 |
23 | val message = "Hello, world!"
24 | logger.log(message)
25 |
26 | assert(logger.getLog().contains(message))
27 | }
28 |
29 | @Test
30 | fun `Logging level test`() {
31 | Vector.enableLogging = true
32 | val message = "Hello, world!"
33 | logger.logd { message }
34 | assert(logger.getLog().contains(Regex("""^D/${logger.tag}.+""")))
35 |
36 | logger.clear()
37 |
38 | logger.logv { message }
39 | assert(logger.getLog().contains(Regex("""^V/${logger.tag}.+""")))
40 | }
41 | }
--------------------------------------------------------------------------------
/hello-vector/src/test/java/com/haroldadmin/hellovector/HelloFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import android.os.Build
4 | import android.widget.TextView
5 | import androidx.fragment.app.testing.launchFragmentInContainer
6 | import androidx.test.espresso.Espresso.onView
7 | import androidx.test.espresso.matcher.ViewMatchers.withId
8 | import org.junit.Ignore
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.RobolectricTestRunner
12 | import org.robolectric.annotation.Config
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | @Config(sdk = [Build.VERSION_CODES.P])
16 | class HelloFragmentTest {
17 |
18 | @Test
19 | @Ignore("Test passes on local device, but fails on CI for some reason")
20 | fun shouldFetchMessageWhenLaunched() {
21 | val scenario = launchFragmentInContainer()
22 | scenario.onFragment { fragment ->
23 | onView(withId(R.id.messageTextView)).check { view, _ ->
24 | view as TextView
25 | assert(view.text == HelloState.loadingMessage) {
26 | "Expected loading text, found ${view.text}"
27 | }
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/extensions/ChannelComputeExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.extensions
2 |
3 | import com.haroldadmin.vector.compute
4 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
5 | import org.junit.After
6 | import org.junit.Before
7 | import org.junit.Test
8 |
9 | class ChannelComputeExtensionTest {
10 |
11 | private val initialValue = 0
12 | lateinit var channel: ConflatedBroadcastChannel
13 |
14 | @Before
15 | fun setup() {
16 | channel = ConflatedBroadcastChannel(initialValue)
17 | }
18 |
19 | @Test
20 | fun `computing new value should replace old value`() {
21 | val isSuccessful = channel.compute { currentValue ->
22 | currentValue + 1
23 | }
24 |
25 | assert(isSuccessful)
26 | assert(channel.value == initialValue + 1)
27 | }
28 |
29 | @Test
30 | fun `computing new value should do nothing if the channel is closed`() {
31 | channel.close()
32 | val isSuccessful = channel.compute { currentValue -> currentValue + 1 }
33 |
34 | assert(!isSuccessful)
35 | }
36 |
37 | @After
38 | fun teardown() {
39 | channel.close()
40 | }
41 | }
--------------------------------------------------------------------------------
/benchmark/benchmark-proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontobfuscate
24 |
25 | -ignorewarnings
26 |
27 | -keepattributes *Annotation*
28 |
29 | -dontnote junit.framework.**
30 | -dontnote junit.runner.**
31 |
32 | -dontwarn androidx.test.**
33 | -dontwarn org.junit.**
34 | -dontwarn org.hamcrest.**
35 | -dontwarn com.squareup.javawriter.JavaWriter
36 |
37 | -keepclasseswithmembers @org.junit.runner.RunWith public class *
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/docs/components/vector-fragment.md:
--------------------------------------------------------------------------------
1 | # Vector Fragment
2 |
3 | Vector Fragment is an abstract class that extends the AndroidX `Fragment` class.
4 |
5 | `VectorFragment` class also has an abstract `renderState(state: S, renderer: (S) -> Unit)` method.
6 | It is supposed to be the place where Views update themselves according to the state update provided by the ViewModel.
7 |
8 | You are free to choose your own implementation and not extend from `VectorFragment` at all.
9 |
10 | An example of how the `renderState` function should look like:
11 |
12 | ```kotlin
13 | class UsersListFragment: VectorFragment() {
14 |
15 | private val viewModel: UserViewModel by viewModel()
16 |
17 | override fun onViewCreated(...) {
18 | renderState(viewModel) { state ->
19 | // Update your views here
20 | }
21 | }
22 | }
23 | ```
24 |
25 | The `renderState` method internally launches a coroutine in a Coroutine Scope scoped to fragment's view-lifecycle.
26 |
27 | Vector is not opinionated about what should be used as Views in an app, so please feel free to use whatever you like. However, the `VectorViewModel` class exposes the current state observable as a `Kotlin Flow` object, so it helps if your View object is a `CoroutineScope`.
28 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/reflection/UnderlyingClassAccess.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.reflection
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import org.junit.Ignore
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 |
11 | @Ignore("Don't run benchmarks with regular builds")
12 | @RunWith(AndroidJUnit4::class)
13 | class UnderlyingClassAccess {
14 |
15 | @get:Rule val benchmarkRule = BenchmarkRule()
16 |
17 | @Test
18 | fun kotlinClassAccess() {
19 | benchmarkRule.measureRepeated {
20 | val klass = ImplementingClass::class
21 | }
22 | }
23 |
24 | @Test
25 | fun javaClassAccess() {
26 | benchmarkRule.measureRepeated {
27 | val clazz = ImplementingClass::class.java
28 | }
29 | }
30 |
31 | @Test
32 | fun javaClassToKotlin() {
33 | benchmarkRule.measureRepeated {
34 | val klass = ImplementingClass::class.java.kotlin
35 | }
36 | }
37 |
38 | // Accessing the underlying java class or kotlin class is equivalent in performance
39 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.utils
2 |
3 | import android.text.Editable
4 | import android.text.TextWatcher
5 | import android.view.View
6 | import android.widget.EditText
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.flow.*
9 |
10 | fun View.hide() {
11 | this.visibility = View.GONE
12 | }
13 |
14 | fun View.show() {
15 | this.visibility = View.VISIBLE
16 | }
17 |
18 | fun EditText.debouncedTextChanges(time: Long = 200): Flow {
19 | val channel = Channel(capacity = Channel.UNLIMITED)
20 |
21 | val textWatcher = object : TextWatcher {
22 | override fun afterTextChanged(text: Editable) {}
23 | override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
24 | override fun onTextChanged(text: CharSequence, p1: Int, p2: Int, p3: Int) {
25 | channel.offer(text)
26 | }
27 | }
28 |
29 | this.addTextChangedListener(textWatcher)
30 |
31 | return channel
32 | .consumeAsFlow()
33 | .debounce(time)
34 | .onCompletion {
35 | this@debouncedTextChanges.removeTextChangedListener(textWatcher)
36 | channel.close()
37 | }
38 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/vectorLazy.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.annotation.RestrictTo
4 |
5 | /**
6 | * A lazy delegate class, which takes in an initializer to initialize its underlying
7 | * property. Implements the double-checked locking algorithm.
8 | *
9 | * @property initializer The method which creates an instance of the requested type
10 | */
11 | @Suppress("ClassName")
12 | @RestrictTo(RestrictTo.Scope.LIBRARY)
13 | class vectorLazy(private val initializer: () -> T) : Lazy {
14 |
15 | @Volatile
16 | private var _value: T? = null
17 |
18 | private val lock = this
19 |
20 | override val value: T
21 | @Suppress("UNCHECKED_CAST")
22 | get() {
23 | if (_value != null) {
24 | return _value as T
25 | } else {
26 | synchronized(lock) {
27 | return if (_value != null) {
28 | _value as T
29 | } else {
30 | val initializedValue = initializer()
31 | _value = initializedValue
32 | initializedValue
33 | }
34 | }
35 | }
36 | }
37 |
38 | override fun isInitialized(): Boolean = _value != null
39 | }
40 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateHolder.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.VectorState
4 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
5 | import kotlinx.coroutines.flow.StateFlow
6 |
7 | /**
8 | * Holds the current state value and provides access to it. A [ConflatedBroadcastChannel] is used
9 | * to hold the current state value. [clear] should be called when this state holder is no longer
10 | * in use.
11 | *
12 | * @param S The state type implementing [VectorState]
13 | */
14 | interface StateHolder {
15 |
16 | /**
17 | * A [StateFlow] to expose the state as an observable entity.
18 | * This flow is conflated, so only the latest state value is present in it
19 | *
20 | * To be notified of every state update, use the [kotlinx.coroutines.flow.buffer] operator.
21 | */
22 | val stateObservable: StateFlow
23 |
24 | /**
25 | * A convenient way to access the current state value in the [stateObservable]
26 | */
27 | val state: S
28 | get() = stateObservable.value
29 |
30 | /**
31 | * Updates the state contained in this state holder
32 | */
33 | fun updateState(newState: S)
34 |
35 | /**
36 | * This method is expected to be called when this state holder is no longer being used
37 | */
38 | fun clearHolder()
39 | }
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/StateFlowConflatedChannelBenchmark.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 |
12 | /**
13 | * benchmark: 429 ns StateFlowConflatedChannelBenchmark.conflatedBroadcastChannelUpdates
14 | * benchmark: 238 ns StateFlowConflatedChannelBenchmark.stateFlowUpdates
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class StateFlowConflatedChannelBenchmark {
18 |
19 | @get:Rule
20 | val benchmarkRule = BenchmarkRule()
21 |
22 | @Test
23 | fun conflatedBroadcastChannelUpdates() {
24 | val channel = ConflatedBroadcastChannel(TestState())
25 | val newState = TestState(42)
26 | benchmarkRule.measureRepeated {
27 | channel.offer(newState)
28 | }
29 | }
30 |
31 | @Test
32 | fun stateFlowUpdates() {
33 | val flow = MutableStateFlow(TestState())
34 | val newState = TestState(42)
35 | benchmarkRule.measureRepeated {
36 | flow.value = newState
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/about/AboutFragment.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.about
2 |
3 | import android.animation.ObjectAnimator
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.view.animation.LinearInterpolator
9 | import androidx.fragment.app.Fragment
10 | import com.haroldadmin.sampleapp.R
11 | import com.haroldadmin.sampleapp.databinding.FragmentAboutBinding
12 | import com.haroldadmin.vector.renderState
13 |
14 | class AboutFragment : Fragment() {
15 |
16 | private lateinit var binding: FragmentAboutBinding
17 |
18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
19 | binding = FragmentAboutBinding.inflate(inflater, container, false)
20 |
21 | renderState(AboutState()) { state ->
22 | ObjectAnimator
23 | .ofFloat(binding.logo, View.ROTATION, 0f, 360f)
24 | .apply {
25 | duration = 3000
26 | repeatCount = ObjectAnimator.INFINITE
27 | interpolator = LinearInterpolator()
28 | }
29 | .also { it.start() }
30 |
31 | binding.debugInfo.text = getString(R.string.debugInformation, state.appVersion, state.libraryVersion)
32 | }
33 |
34 | return binding.root
35 | }
36 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.navigation.NavController
7 | import androidx.navigation.fragment.NavHostFragment
8 | import androidx.navigation.ui.AppBarConfiguration
9 | import androidx.navigation.ui.onNavDestinationSelected
10 | import androidx.navigation.ui.setupWithNavController
11 | import com.haroldadmin.sampleapp.databinding.ActivityMainBinding
12 |
13 | class MainActivity : AppCompatActivity() {
14 |
15 | private lateinit var binding: ActivityMainBinding
16 | private lateinit var navController: NavController
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
21 |
22 | val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
23 | navController = navHostFragment.navController
24 |
25 | val appBarConfig = AppBarConfiguration(navController.graph)
26 | binding.toolbar.apply {
27 | setupWithNavController(navController, appBarConfig)
28 | inflateMenu(R.menu.menu_main)
29 | setOnMenuItemClickListener { item -> item.onNavDestinationSelected(navController) }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/drawable/ic_vector.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
19 |
26 |
27 |
--------------------------------------------------------------------------------
/benchmark/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'androidx.benchmark'
3 | apply plugin: 'kotlin-android'
4 | apply plugin: 'kotlin-android-extensions'
5 | android {
6 | compileSdkVersion buildConfig.compileSdk
7 | defaultConfig {
8 | minSdkVersion buildConfig.minSdk
9 | targetSdkVersion buildConfig.targetSdk
10 | versionCode buildConfig.versionCode
11 | versionName buildConfig.versionName
12 | testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
13 | }
14 |
15 | buildTypes {
16 | debug {
17 | minifyEnabled true
18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'benchmark-proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility 1.8
23 | targetCompatibility 1.8
24 | }
25 | kotlinOptions {
26 | jvmTarget = "1.8"
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: 'libs', include: ['*.jar'])
32 |
33 | implementation project(path: ':vector')
34 |
35 | implementation libs.kotlinStdLib
36 | implementation libs.kotlinReflect
37 | implementation libs.coroutinesCore
38 | implementation libs.coroutinesAndroid
39 | androidTestImplementation libs.androidxTestRunner
40 | androidTestImplementation libs.androidxTestExt
41 | androidTestImplementation libs.junit
42 | androidTestImplementation "androidx.benchmark:benchmark-junit4:1.0.0-alpha06"
43 | }
44 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/drawable-v24/ic_vector.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
19 |
26 |
27 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/reflection/SuperClassCheckBenchMark.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.reflection
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import org.junit.Ignore
7 | import org.junit.Rule
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import kotlin.reflect.full.isSuperclassOf
11 |
12 | internal interface SuperTypeInterface
13 | internal class ImplementingClass : SuperTypeInterface
14 |
15 | @Ignore("Don't run benchmarks for regular builds")
16 | @RunWith(AndroidJUnit4::class)
17 | internal class SuperClassCheckBenchMark {
18 |
19 | @get:Rule
20 | val benchmarkRule = BenchmarkRule()
21 |
22 | @Test
23 | fun kotlinReflectionSuperClassCheck() {
24 | benchmarkRule.measureRepeated {
25 | val superClass = SuperTypeInterface::class
26 | val subClass = ImplementingClass::class
27 | superClass.isSuperclassOf(subClass)
28 | }
29 | }
30 |
31 | @Test
32 | fun javaReflectionSuperClassCheck() {
33 | benchmarkRule.measureRepeated {
34 | val superClass = SuperTypeInterface::class.java
35 | val subClass = ImplementingClass::class.java
36 | superClass.isAssignableFrom(subClass)
37 | }
38 | }
39 |
40 | // Result is that Kotlin Reflection is around 150x-200x slower than Java Reflection for this particular check
41 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/repository/EntitiesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.repository
2 |
3 | import com.haroldadmin.sampleapp.CountingEntity
4 | import com.haroldadmin.sampleapp.CountingEntityQueries
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 |
9 | class EntitiesRepository @Inject constructor(private val dao: CountingEntityQueries) {
10 |
11 | suspend fun getAllEntities(): List = withContext(Dispatchers.IO) {
12 | dao.getAll().executeAsList()
13 | }
14 |
15 | suspend fun getEntity(id: String): CountingEntity = withContext(Dispatchers.IO) {
16 | dao.getEntity(id).executeAsOne()
17 | }
18 |
19 | suspend fun getCounterForEntity(id: String): Long = withContext(Dispatchers.IO) {
20 | dao.getCounterForEntity(id).executeAsOne()
21 | }
22 |
23 | suspend fun saveNewEntity(entity: CountingEntity) = withContext(Dispatchers.IO) {
24 | dao.insert(entity.id, entity.name, entity.counter, entity.colour)
25 | }
26 |
27 | suspend fun deleteEntity(entity: CountingEntity) = withContext(Dispatchers.IO) {
28 | dao.delete(entity.id)
29 | }
30 |
31 | suspend fun updateEntity(entity: CountingEntity) = withContext(Dispatchers.IO) {
32 | dao.update(entity.counter, entity.name, entity.colour, entity.id)
33 | }
34 |
35 | suspend fun getNumberOfEntities(): Long = withContext(Dispatchers.IO) {
36 | dao.getNumberOfEntities().executeAsOne()
37 | }
38 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/extensions/ReflectionExtensionsTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.extensions
2 |
3 | import com.haroldadmin.vector.DoesNotImplementVectorVMFactoryException
4 | import com.haroldadmin.vector.TestViewModel
5 | import com.haroldadmin.vector.TestViewModelWithFactory
6 | import com.haroldadmin.vector.companionObject
7 | import com.haroldadmin.vector.factoryCompanion
8 | import com.haroldadmin.vector.instance
9 | import com.haroldadmin.vector.state.CountingState
10 | import org.junit.Test
11 |
12 | class ReflectionExtensionsTest {
13 |
14 | @Test
15 | fun `Class newInstance test`() {
16 | val instance = CountingState::class.java.instance(42)
17 | instance as CountingState
18 | assert(instance.count == CountingState(42).count)
19 | }
20 |
21 | @Test
22 | fun `Class Companion object test`() {
23 | val companion = ClassWithCompanion::class.java.companionObject()
24 | assert(companion != null)
25 | }
26 |
27 | @Test
28 | fun `ViewModelFactory Companion test when factory exists`() {
29 | // Test should fail because this operation throws an error when there's no companion
30 | TestViewModelWithFactory::class.java.factoryCompanion()
31 | }
32 |
33 | @Test(expected = DoesNotImplementVectorVMFactoryException::class)
34 | fun `ViewModelFactory Companion test when factory does not exist`() {
35 | TestViewModel::class.java.factoryCompanion()
36 | }
37 | }
38 |
39 | private class ClassWithCompanion {
40 | companion object
41 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateStoreFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.Logger
4 | import com.haroldadmin.vector.VectorState
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | /**
8 | * A factory to create instances of [StateStore]
9 | */
10 | internal object StateStoreFactory {
11 |
12 | fun create(
13 | initialState: S,
14 | logger: Logger,
15 | coroutineContext: CoroutineContext
16 | ): StateStore {
17 | val stateHolder = StateHolderFactory.create(initialState, logger)
18 | val stateProcessor = StateProcessorFactory.create(stateHolder, logger, coroutineContext)
19 | return create(stateHolder, stateProcessor, logger)
20 | }
21 |
22 | fun create(
23 | stateHolder: StateHolder,
24 | stateProcessor: StateProcessor,
25 | logger: Logger
26 | ): StateStore {
27 | return StateStoreImpl(stateHolder, stateProcessor, logger)
28 | }
29 |
30 | fun create(
31 | initialState: S,
32 | logger: Logger,
33 | stateHolderFactory: StateHolderFactory,
34 | stateProcessorFactory: StateProcessorFactory,
35 | coroutineContext: CoroutineContext
36 | ): StateStore {
37 | val holder = stateHolderFactory.create(initialState, logger)
38 | val processor = stateProcessorFactory.create(holder, logger, coroutineContext)
39 | return create(holder, processor, logger)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/utils/Provider.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.utils
2 |
3 | import android.content.Context
4 | import com.haroldadmin.sampleapp.CountingEntity
5 | import com.haroldadmin.sampleapp.Database
6 | import com.haroldadmin.sampleapp.repository.Colour
7 | import com.squareup.sqldelight.ColumnAdapter
8 | import com.squareup.sqldelight.android.AndroidSqliteDriver
9 | import com.squareup.sqldelight.db.SqlDriver
10 |
11 | class Provider(context: Context) {
12 |
13 | private val sqlDelightDriver: SqlDriver = AndroidSqliteDriver(Database.Schema, context, "countingEntities.db")
14 |
15 | private val colourAdapter = object : ColumnAdapter {
16 | override fun decode(databaseValue: String): Colour {
17 | return when (databaseValue) {
18 | Colour.BLUE.toString() -> Colour.BLUE
19 | Colour.RED.toString() -> Colour.RED
20 | Colour.GREEN.toString() -> Colour.GREEN
21 | Colour.PINK.toString() -> Colour.PINK
22 | Colour.YELLOW.toString() -> Colour.YELLOW
23 | else -> throw IllegalArgumentException("Unknown colour value requested")
24 | }
25 | }
26 |
27 | override fun encode(value: Colour): String = value.toString()
28 | }
29 |
30 | val database: Database by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
31 | Database(
32 | driver = sqlDelightDriver,
33 | countingEntityAdapter = CountingEntity.Adapter(colourAdapter)
34 | )
35 | }
36 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/VectorViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import com.haroldadmin.vector.state.CountingState
4 | import kotlinx.coroutines.Job
5 | import kotlinx.coroutines.test.TestCoroutineScope
6 | import org.junit.Before
7 | import org.junit.Test
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | internal class VectorViewModelTest {
11 |
12 | private val testScope = TestCoroutineScope()
13 | private lateinit var viewModel: TestVectorViewModel
14 | private val initialState = CountingState()
15 | private val job = Job()
16 |
17 | @Before
18 | fun setup() {
19 | viewModel = TestVectorViewModel(initialState, testScope.coroutineContext + job)
20 | }
21 |
22 | @Test
23 | fun setStateTest() {
24 | viewModel.incrementCount()
25 | withState(viewModel) { state -> assert(state.count == initialState.count + 1) }
26 |
27 | viewModel.decrementCount()
28 | withState(viewModel) { state -> assert(state.count == initialState.count) }
29 | }
30 |
31 | @Test
32 | fun clearTest() {
33 | viewModel.clear()
34 | assert(job.isCancelled)
35 | }
36 | }
37 |
38 | private class TestVectorViewModel(
39 | initialState: CountingState,
40 | stateStoreContext: CoroutineContext
41 | ) : VectorViewModel(initialState, stateStoreContext) {
42 |
43 | fun incrementCount() = setState { copy(count = this.count + 1) }
44 | fun decrementCount() = setState { copy(count = this.count - 1) }
45 | fun clear() {
46 | onCleared()
47 | }
48 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
19 |
20 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/item_entity.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
19 |
20 |
29 |
30 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: 'Vector'
2 | site_description: 'Kotlin Coroutines based MVI Architecture Library'
3 | site_author: 'Kshitij Chauhan'
4 | site_url: 'https://haroldadmin.github.io/vector/'
5 | remote_branch: gh-pages
6 |
7 | repo_name: 'Vector'
8 | repo_url: 'https://github.com/haroldadmin/Vector'
9 |
10 | nav:
11 | - 'Overview': 'index.md'
12 | - 'Introduction': 'introduction.md'
13 | - 'Basics': 'usage/basics.md'
14 | - 'Advanced Usage': 'usage/advanced.md'
15 | - 'Components':
16 | - 'Vector State': 'components/vector-state.md'
17 | - 'Vector Fragment': 'components/vector-fragment.md'
18 | - 'Vector ViewModel': 'components/vector-viewmodel.md'
19 | - 'SavedState VectorViewModel': 'components/saved-state-vectorviewmodel.md'
20 | - 'Logging': 'components/logging.md'
21 | - 'Miscellaneous':
22 | - 'Automatic ViewModel Creation': 'misc/automatic-viewmodel-creation.md'
23 | - 'Coroutine Context for State Store': 'misc/state-store-context.md'
24 | - 'API Reference': 'api/vector/index.md'
25 |
26 | theme:
27 | name: 'material'
28 | palette:
29 | primary: 'teal'
30 | accent: 'blue'
31 | font:
32 | text: 'Roboto'
33 | code: 'Roboto Mono'
34 | logo: 'images/logo-monochrome.svg'
35 | favicon: 'images/logo-monochrome.svg'
36 |
37 | extra:
38 | social:
39 | - type: 'github'
40 | link: 'https://github.com/haroldadmin'
41 | - type: 'twitter'
42 | link: 'https://twitter.com/haroldadmin'
43 | - type: 'reddit'
44 | link: 'https://reddit.com/u/theharolddev'
45 |
46 | markdown_extensions:
47 | - admonition
48 | - codehilite:
49 | guess_lang: false
50 | - toc:
51 | permalink: true
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.VectorState
4 |
5 | /**
6 | * An entity that manages any action on state.
7 | *
8 | * @param S The state type implementing [VectorState]
9 | *
10 | * A [reducer] is be processed before any existing [action] in the queue
11 | * A [action] is given the latest state value as it's parameter
12 | */
13 | interface StateProcessor {
14 |
15 | /**
16 | * Offer a [reducer] to this processor. This action will be processed as soon as
17 | * possible, before all existing [action] waiting in the queue, if any.
18 | *
19 | * @param reducer The action to be offered
20 | */
21 | fun offerSetAction(reducer: reducer)
22 |
23 | /**
24 | * Offer a [action] to this processor. The state parameter supplied to this action
25 | * shall be the latest state value at the time of processing this action.
26 | *
27 | * These actions are treated as side effects. When such an action is received, a separate coroutine is launched
28 | * to process it. This means that when there are multiple such actions waiting in the queue, they will be launched
29 | * in order, but their completion depends on how long it takes to process them. They will be processed in the
30 | * coroutine context of their state processor.
31 | */
32 | fun offerGetAction(action: action)
33 |
34 | /**
35 | * Cleanup any resources held by this processor.
36 | */
37 | fun clearProcessor()
38 | }
39 |
40 | internal typealias reducer = suspend S.() -> S
41 |
42 | internal typealias action = suspend (S) -> Unit
--------------------------------------------------------------------------------
/hello-vector/src/test/java/com/haroldadmin/hellovector/HelloViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.hellovector
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.haroldadmin.vector.withState
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.asCoroutineDispatcher
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.coroutines.test.resetMain
11 | import kotlinx.coroutines.test.setMain
12 | import org.junit.After
13 | import org.junit.Before
14 | import org.junit.Test
15 | import java.util.concurrent.Executors
16 | import kotlin.coroutines.CoroutineContext
17 |
18 | @ExperimentalCoroutinesApi
19 | class HelloViewModelTest {
20 |
21 | private lateinit var viewModel: HelloViewModel
22 | private lateinit var testContext: CoroutineContext
23 |
24 | @Before
25 | fun setup() {
26 | val mainThreadSurrogate = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
27 | testContext = mainThreadSurrogate + Job()
28 | Dispatchers.setMain(mainThreadSurrogate)
29 | viewModel = HelloViewModel(HelloState(), testContext, SavedStateHandle())
30 | }
31 |
32 | @Test
33 | fun `should fetch message when initialized`() = runBlocking(testContext) {
34 | val expectedMessage = "Hello, World!"
35 | viewModel.getMessage(delayDuration = 0).join()
36 | withState(viewModel) { state ->
37 | assert(state.message == expectedMessage) {
38 | "Expected $expectedMessage, got ${state.message}"
39 | }
40 | }
41 | }
42 |
43 | @After
44 | fun cleanup() {
45 | Dispatchers.resetMain()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 | *.aab
5 |
6 | # Files for the ART/Dalvik VM
7 | *.dex
8 |
9 | # Java class files
10 | *.class
11 |
12 | # Generated files
13 | bin/
14 | gen/
15 | out/
16 |
17 | # Gradle files
18 | .gradle/
19 | build/
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | # Android Studio captures folder
34 | captures/
35 |
36 | # IntelliJ
37 | *.iml
38 | .idea/workspace.xml
39 | .idea/tasks.xml
40 | .idea/gradle.xml
41 | .idea/assetWizardSettings.xml
42 | .idea/dictionaries
43 | .idea/libraries
44 | .idea/caches
45 | # Android Studio 3 in .gitignore file.
46 | .idea/caches/build_file_checksums.ser
47 | .idea/modules.xml
48 |
49 | # Keystore files
50 | # Uncomment the following lines if you do not want to check your keystore files in.
51 | #*.jks
52 | #*.keystore
53 |
54 | # External native build folder generated in Android Studio 2.2 and later
55 | .externalNativeBuild
56 |
57 | # Google Services (e.g. APIs or Firebase)
58 | # google-services.json
59 |
60 | # Freeline
61 | freeline.py
62 | freeline/
63 | freeline_project_description.json
64 |
65 | # fastlane
66 | fastlane/report.xml
67 | fastlane/Preview.html
68 | fastlane/screenshots
69 | fastlane/test_output
70 | fastlane/readme.md
71 |
72 | # Version control
73 | vcs.xml
74 |
75 | # lint
76 | lint/intermediates/
77 | lint/generated/
78 | lint/outputs/
79 | lint/tmp/
80 | # lint/reports/
81 |
82 | # Release files
83 | app/release
84 |
85 | # IntelliJ Idea
86 | .idea
87 |
88 | .kotlintest
89 | .DS_Store
90 |
91 | # VS Code
92 | .settings
93 | .project
94 | .classpath
95 |
96 | bundleToApk.py
97 | *.apks
98 | site/
99 | docs/api/
--------------------------------------------------------------------------------
/hello-vector/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Generate sample apps' APKs and documentation
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build:
10 | name: Generate Documentation
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v1
18 | with:
19 | python-version: '3.7'
20 | architecture: 'x64'
21 |
22 | - name: Install python dependencies
23 | run: |
24 | pip3 install --upgrade pip
25 | pip3 install -r requirements.txt
26 |
27 | - name: Generate API Reference
28 | run: python3 ./generateApiReference.py
29 |
30 | - name: Build MkDocs
31 | run: mkdocs build
32 |
33 | - name: Deploy
34 | uses: peaceiris/actions-gh-pages@v2.4.0
35 | env:
36 | ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
37 | PUBLISH_BRANCH: gh-pages
38 | PUBLISH_DIR: ./site
39 |
40 | apk:
41 | name: Generate APKs
42 | runs-on: ubuntu-latest
43 |
44 | steps:
45 | - uses: actions/checkout@v1
46 | - name: set up JDK 1.8
47 | uses: actions/setup-java@v1
48 | with:
49 | java-version: 1.8
50 |
51 | - name: Build Hello-Vector APK
52 | run: ./gradlew :hello-vector:assembleDebug
53 |
54 | - name: Build Sample App APK
55 | run: ./gradlew :sampleapp:assembleDebug
56 |
57 | - name: Upload Hello-Vector APK
58 | uses: actions/upload-artifact@v1
59 | with:
60 | name: hello-vector
61 | path: hello-vector/build/outputs/apk/debug/hello-vector-debug.apk
62 |
63 | - name: Upload Sample App APK
64 | uses: actions/upload-artifact@v1
65 | with:
66 | name: sample-app
67 | path: sampleapp/build/outputs/apk/debug/sampleapp-debug.apk
68 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | 
4 |
5 | Vector is a Kotlin Coroutines based MVI Architecture library for Android.
6 |
7 | It is inspired from [MvRx](https://www.github.com/airbnb/mvrx) and [Roxie](https://github.com/ww-tech/roxie), but unlike them it is built completely using Kotlin Coroutines instead of RxJava. As such, it internally only uses Coroutine primitives, and has extensive support for Suspending functions.
8 |
9 | Vector works well with Android Architecture Components. It is 100% Kotlin, and is intended for use with Kotlin only.
10 |
11 | ## Installation Instructions
12 |
13 | Add the Jitpack repository to your top level `build.gradle` file.
14 |
15 | ```groovy
16 | allprojects {
17 | repositories {
18 | ...
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 | ```
23 |
24 | And then add the following dependency in your module's `build.gradle` file:
25 |
26 | ```groovy
27 | dependencies {
28 | implementation "com.github.haroldadmin:Vector:(latest-version)"
29 | }
30 | ```
31 |
32 | [](https://jitpack.io/#haroldadmin/Vector)
33 |
34 | ## R8/Proguard Config
35 |
36 | The library ships with consumer proguard rules, so no additional configuration should be required.
37 |
38 | ## License
39 |
40 | ```
41 | Copyright 2019 Vector Contributors
42 |
43 | Licensed under the Apache License, Version 2.0 (the "License");
44 | you may not use this file except in compliance with the License.
45 | You may obtain a copy of the License at
46 |
47 | https://www.apache.org/licenses/LICENSE-2.0
48 |
49 | Unless required by applicable law or agreed to in writing, software
50 | distributed under the License is distributed on an "AS IS" BASIS,
51 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52 | See the License for the specific language governing permissions and
53 | limitations under the License.
54 | ```
55 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/VectorStateFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import io.mockk.mockk
4 | import org.junit.Before
5 | import org.junit.Test
6 |
7 | internal class VectorStateFactoryTest {
8 |
9 | private lateinit var factory: VectorStateFactory
10 |
11 | @Before
12 | fun setup() {
13 | factory = RealStateFactory()
14 | }
15 |
16 | @Test(expected = UnInstantiableStateClassException::class)
17 | fun `should fail to create state when there's no ViewModel Companion Factory or Default params`() {
18 | val state = factory.createInitialState(
19 | TestViewModel::class,
20 | TestStates.TestState::class,
21 | mockk(),
22 | mockk()
23 | )
24 | assert(state.count == 42)
25 | }
26 |
27 | @Test
28 | fun `should create state using default parameters when there is no Companion Factory`() {
29 | val state = factory.createInitialState(
30 | TestViewModel::class,
31 | TestStates.TestStateWithDefaults::class,
32 | mockk(),
33 | mockk()
34 | )
35 | assert(state.count == 42)
36 | }
37 |
38 | @Test
39 | fun `should use companion factory to create state when it is present`() {
40 | val state = factory.createInitialState(
41 | TestViewModelWithFactory::class,
42 | TestStates.TestState::class,
43 | mockk(),
44 | mockk()
45 | )
46 | assert(state.count == 0)
47 | }
48 |
49 | @Test
50 | fun `should prefer companion factory over default params when creating state`() {
51 | val state = factory.createInitialState(
52 | TestViewModelWithFactoryAndDefaults::class,
53 | TestStates.TestStateWithDefaults::class,
54 | mockk(),
55 | mockk()
56 | )
57 | assert(state.count == 0)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/hello-vector/src/main/res/layout/fragment_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
24 |
25 |
39 |
40 |
--------------------------------------------------------------------------------
/vector/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | # Kotlin reflection sometimes throws errors when Metadata is absent
2 | -dontwarn org.jetbrains.annotations.**
3 | -keep class kotlin.Metadata { *; }
4 |
5 | # Keep the Companion object of classes extending VectorViewModel
6 | -keepclassmembers class ** extends com.haroldadmin.vector.VectorViewModel {
7 | ** Companion;
8 | }
9 |
10 | # Classes extending VectorViewModel are recreated using reflection, which assumes that a one argument
11 | # constructor accepting a data class holding the state exists. Need to make sure to keep the constructor
12 | # around. `create` and `initialState` methods are here in case the companion object is marked with
13 | # @JvmStatic
14 | -keepclassmembers class ** extends com.haroldadmin.vector.VectorViewModel {
15 | public (...);
16 | public static *** create(...);
17 | public static *** initialState(...);
18 | }
19 |
20 | # If a VectorViewModel is used without JvmStatic, keep create and initalState methods which
21 | # are accessed via reflection.
22 | -keepclassmembers class ** implements com.haroldadmin.vector.VectorViewModelFactory {
23 | public (...);
24 | public *** create(...);
25 | public *** initialState(...);
26 | }
27 |
28 | # VectorViewModelFactory is referenced via reflection using the Companion class name.
29 | -keepnames class * implements com.haroldadmin.vector.VectorViewModelFactory
30 |
31 |
32 | # Members of the Kotlin data classes used as the state in Vector are read via Kotlin reflection which cause trouble
33 | # with Proguard if they are not kept.
34 | -keepclassmembers,includedescriptorclasses class ** implements com.haroldadmin.vector.VectorState {
35 | *;
36 | }
37 |
38 | # The MvRxState object and the names classes that implement the MvRxState interfrace need to be
39 | # kept as they are accessed via reflection.
40 | -keepnames class com.haroldadmin.vector.VectorState
41 | -keepnames class * implements com.haroldadmin.vector.VectorState
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/EntitiesAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.haroldadmin.sampleapp.CountingEntity
9 | import com.haroldadmin.sampleapp.databinding.ItemEntityBinding
10 |
11 | class EntitiesDiffCallback : DiffUtil.ItemCallback() {
12 | override fun areItemsTheSame(oldItem: CountingEntity, newItem: CountingEntity): Boolean =
13 | oldItem.name == newItem.name
14 |
15 | override fun areContentsTheSame(oldItem: CountingEntity, newItem: CountingEntity): Boolean {
16 | return oldItem.name == newItem.name &&
17 | oldItem.counter == newItem.counter &&
18 | oldItem.colour == newItem.colour
19 | }
20 | }
21 |
22 | class EntityViewHolder(private val binding: ItemEntityBinding) :
23 | RecyclerView.ViewHolder(binding.root) {
24 | fun bind(countingEntity: CountingEntity, onEntityClick: (CountingEntity) -> Unit) {
25 | binding.apply {
26 | entity = countingEntity
27 | root.setOnClickListener { onEntityClick(countingEntity) }
28 | binding.executePendingBindings()
29 | }
30 | }
31 | }
32 |
33 | class EntitiesAdapter(diffCallback: EntitiesDiffCallback, val onEntityClick: (CountingEntity) -> Unit) :
34 | ListAdapter(diffCallback) {
35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntityViewHolder {
36 | val binding = ItemEntityBinding.inflate(LayoutInflater.from(parent.context), parent, false)
37 | return EntityViewHolder(binding)
38 | }
39 |
40 | override fun onBindViewHolder(holder: EntityViewHolder, position: Int) {
41 | holder.bind(getItem(position), onEntityClick)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/SavedStateVectorViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.os.Build
4 | import androidx.activity.ComponentActivity
5 | import androidx.lifecycle.SavedStateHandle
6 | import com.haroldadmin.vector.state.CountingState
7 | import kotlinx.coroutines.Job
8 | import kotlinx.coroutines.test.TestCoroutineScope
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.Robolectric
12 | import org.robolectric.RobolectricTestRunner
13 | import org.robolectric.annotation.Config
14 | import kotlin.coroutines.CoroutineContext
15 |
16 | @RunWith(RobolectricTestRunner::class)
17 | @Config(sdk = [Build.VERSION_CODES.P])
18 | internal class SavedStateVectorViewModelTest {
19 |
20 | private val testScope = TestCoroutineScope()
21 | private val job = Job()
22 | private val activity: ComponentActivity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
23 | private val viewModel: TestSavedStateVM by activity.viewModel { initialState, handle ->
24 | TestSavedStateVM(initialState, testScope.coroutineContext + job, handle)
25 | }
26 |
27 | @Test
28 | fun setStateAndPersistTest() {
29 | viewModel.apply {
30 | incrementCount()
31 | val savedState = getSavedState()!!
32 | withState(viewModel) { state -> assert(state == savedState) }
33 | }
34 | }
35 | }
36 |
37 | private class TestSavedStateVM(
38 | initialState: CountingState,
39 | stateStoreContext: CoroutineContext,
40 | savedStateHandle: SavedStateHandle
41 | ) : SavedStateVectorViewModel(initialState, stateStoreContext, savedStateHandle) {
42 |
43 | fun incrementCount() = setStateAndPersist {
44 | copy(count = this.count + 1)
45 | }
46 |
47 | fun getSavedState(): CountingState? {
48 | return savedStateHandle.get(KEY_SAVED_STATE)
49 | }
50 | }
51 |
52 | private class SavedStateVMActivity : ComponentActivity()
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/regularStateStore/StateStoreBenchmark.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.regularStateStore
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import com.haroldadmin.vector.VectorState
7 | import kotlinx.coroutines.runBlocking
8 | import org.junit.Ignore
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | data class TestState(val count: Int = 0) : VectorState
14 |
15 | @Ignore("We don't want benchmarks to run with regular builds")
16 | @RunWith(AndroidJUnit4::class)
17 | class StateStoreBenchmark {
18 |
19 | @get:Rule
20 | val benchmarkRule = BenchmarkRule()
21 |
22 | private val stateStore =
23 | RegularStateStoreImpl(TestState())
24 |
25 | @Test
26 | fun setStateTest() {
27 | benchmarkRule.measureRepeated {
28 | runBlocking {
29 | stateStore.set { copy(count = this.count + 1) }
30 | }
31 | }
32 | }
33 |
34 | @Test
35 | fun simpleRecursiveCall() {
36 | benchmarkRule.measureRepeated {
37 | runBlocking {
38 | stateStore.set {
39 | stateStore.get { state ->
40 | // Do nothing
41 | }
42 | this
43 | }
44 | }
45 | }
46 | }
47 |
48 | @Test
49 | fun complexRecursiveCall() {
50 | benchmarkRule.measureRepeated {
51 | runBlocking {
52 | stateStore.get {
53 | stateStore.set {
54 | stateStore.set { this }
55 | stateStore.get {
56 | stateStore.set { this }
57 | }
58 | this
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
19 |
22 |
23 |
24 |
29 |
34 |
39 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/docs/images/logo-monochrome.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 |
5 | /**
6 | * A Factory meant to be implemented using the Companion object of a [VectorViewModel] to provide
7 | * ways to create the initial state, as well as the creation of the ViewModel itself.
8 | *
9 | * @param VM The type of the [VectorViewModel] being created by this factory
10 | * @param S The state class bound to the given [VectorViewModel] type
11 | *
12 | */
13 | interface VectorViewModelFactory, S : VectorState> {
14 |
15 | /**
16 | * Used to create the initial state, if this method is implemented.
17 | *
18 | * @param handle Can be used to retrieve last saved state before process death,
19 | * using [SavedStateVectorViewModel.KEY_SAVED_STATE] or some other mechanism.
20 | * @param owner The [ViewModelOwner] for this ViewModel. Can be used to access fragment arguments,
21 | * context etc.
22 | *
23 | * @return Initial state to be used for creation of the ViewModel
24 | *
25 | */
26 | fun initialState(handle: SavedStateHandle, owner: ViewModelOwner): S? { return null }
27 |
28 | /**
29 | * Used to create the requested ViewModel. This method needs to be implemented if your ViewModel
30 | * has dependencies other than those of a [VectorViewModel] or a [SavedStateVectorViewModel]. However, if you are
31 | * using a different kind of factory to create your ViewModel, you might skip implementing this method.
32 | *
33 | * @param initialState The initial state to be given to the ViewModel
34 | * @param owner The [ViewModelOwner] for this ViewModel. Can be used to access context, dependency graph, etc.
35 | * @param handle The saved state handle to be given to the ViewModel, if needed.
36 | *
37 | * @return The ViewModel to be created using this function.
38 | *
39 | */
40 | fun create(initialState: S, owner: ViewModelOwner, handle: SavedStateHandle): VM? { return null }
41 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/SavedStateVectorViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.haroldadmin.vector.loggers.logd
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.Job
7 | import kotlin.coroutines.CoroutineContext
8 |
9 | /**
10 | * A Subclass of [VectorViewModel] that has access to a [SavedStateHandle] to easily
11 | * persist state properties in case of process death
12 | *
13 | * @param initialState The initial state for this ViewModel
14 | * @param stateStoreContext The [CoroutineContext] to be used with the contained State Store
15 | * @param savedStateHandle The [SavedStateHandle] to be used for persisting state across process deaths
16 | */
17 | abstract class SavedStateVectorViewModel(
18 | initialState: S,
19 | stateStoreContext: CoroutineContext = Dispatchers.Default + Job(),
20 | protected val savedStateHandle: SavedStateHandle
21 | ) : VectorViewModel(initialState, stateStoreContext) {
22 |
23 | companion object {
24 | /**
25 | * A predefined key which can be used to persist a valid [VectorState] class into the
26 | * [savedStateHandle]
27 | */
28 | const val KEY_SAVED_STATE = "vector:saved-state"
29 | }
30 |
31 | /**
32 | * A convenience wrapper around the [setState] function which runs the given reducer, and then
33 | * persists the newly created state
34 | *
35 | * @param reducer The state reducer to create a new state from the current state
36 | *
37 | */
38 | protected fun setStateAndPersist(reducer: suspend S.() -> S) {
39 | setState(reducer)
40 | persistState()
41 | }
42 |
43 | /**
44 | * Saves the current state into [savedStateHandle] using [KEY_SAVED_STATE]
45 | * Subclasses can override this method for custom behaviour.
46 | */
47 | protected open fun persistState() = withState { state ->
48 | logger.logd { "Persisting state: $state" }
49 | savedStateHandle.set(KEY_SAVED_STATE, state)
50 | }
51 | }
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/extensions/WithStateExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.extensions
2 |
3 | import com.haroldadmin.vector.VectorViewModel
4 | import com.haroldadmin.vector.state.CountingState
5 | import com.haroldadmin.vector.withState
6 | import kotlinx.coroutines.CompletableDeferred
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.newSingleThreadContext
10 | import kotlinx.coroutines.test.TestCoroutineScope
11 | import kotlinx.coroutines.test.resetMain
12 | import kotlinx.coroutines.test.runBlockingTest
13 | import kotlinx.coroutines.test.setMain
14 | import org.junit.After
15 | import org.junit.Before
16 | import org.junit.Test
17 |
18 | class WithStateExtensionTest {
19 |
20 | private val testScope = TestCoroutineScope()
21 | private val mainThreadDispatcher = newSingleThreadContext("Main thread")
22 |
23 | @Before
24 | fun setup() {
25 | Dispatchers.setMain(mainThreadDispatcher)
26 | }
27 |
28 | @Test
29 | fun withStateTest() = testScope.runBlockingTest {
30 |
31 | val deferred = CompletableDeferred()
32 | val initState = CountingState()
33 |
34 | val viewModel = object : VectorViewModel(
35 | initialState = initState,
36 | stateStoreContext = testScope.coroutineContext + Job()
37 | ) {
38 | fun incrementCount() = setState {
39 | val newState = copy(count = this.count + 1)
40 | deferred.complete(Unit)
41 | newState
42 | }
43 | }
44 |
45 | withState(viewModel) { state ->
46 | assert(initState == state)
47 | }
48 |
49 | viewModel.incrementCount()
50 | deferred.await()
51 |
52 | withState(viewModel) { state ->
53 | assert(state.count == initState.count + 1)
54 | }
55 | }
56 |
57 | @After
58 | fun cleanup() {
59 | testScope.cleanupTestCoroutines()
60 | Dispatchers.resetMain()
61 | mainThreadDispatcher.close()
62 | }
63 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/actorStateStore/ActorsStateStore.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.actorStateStore
2 |
3 | import com.haroldadmin.vector.VectorState
4 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
5 |
6 | /**
7 | * Manages synchronized accesses and mutations to state, so that functions trying to access state
8 | * always receive the latest state, and functions trying to set state execute only after all previous
9 | * state mutations have been processed.
10 | *
11 | * This class is expected to be owned by a [VectorViewModel] which calls [cleanup] when it is cleared.
12 | *
13 | * @param S The subclass of [VectorState] on which this class is based. For convenience, use a Kotlin data class
14 | */
15 | interface ActorsStateStore {
16 |
17 | /**
18 | * A convenient way to access the current state value in the [stateChannel]
19 | */
20 | val state: S
21 |
22 | /**
23 | * Takes in a state reducer, adds it to a queue.
24 | * This is used to mutate state by sending in a reducer
25 | *
26 | * Example:
27 | * stateStore.set { copy(count = counter + 1) }
28 | */
29 | fun set(action: suspend S.() -> S)
30 |
31 | /**
32 | * Takes in a block that needs to access state and perform some action with it
33 | *
34 | * The supplied state parameter is guaranteed to always be the latest state,
35 | * even if there are other state mutation blocks in the queue
36 | */
37 | fun get(block: suspend (S) -> Unit)
38 |
39 | /**
40 | * A [ConflatedBroadcastChannel] to expose the state as an observable entity.
41 | * Any new state produced by the reducers given to [set] is passed to this channel.
42 | *
43 | * This channel is conflated, so only the latest state value is present in it
44 | */
45 | val stateChannel: ConflatedBroadcastChannel
46 |
47 | /**
48 | * This method is expected to be called by the owning ViewModel of this class
49 | * to cleanup the resources, and close all channels in this State Store
50 | */
51 | fun cleanup()
52 | }
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/regularStateStore/RegularStateStore.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.regularStateStore
2 |
3 | import com.haroldadmin.vector.VectorState
4 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
5 |
6 | /**
7 | * Manages synchronized accesses and mutations to state, so that functions trying to access state
8 | * always receive the latest state, and functions trying to set state execute only after all previous
9 | * state mutations have been processed.
10 | *
11 | * This class is expected to be owned by a [VectorViewModel] which calls [cleanup] when it is cleared.
12 | *
13 | * @param S The subclass of [VectorState] on which this class is based. For convenience, use a Kotlin data class
14 | */
15 | interface RegularStateStore {
16 |
17 | /**
18 | * A convenient way to access the current state value in the [stateChannel]
19 | */
20 | val state: S
21 |
22 | /**
23 | * Takes in a state reducer, adds it to a queue.
24 | * This is used to mutate state by sending in a reducer
25 | *
26 | * Example:
27 | * stateStore.set { copy(count = counter + 1) }
28 | */
29 | suspend fun set(action: suspend S.() -> S)
30 |
31 | /**
32 | * Takes in a block that needs to access state and perform some action with it
33 | *
34 | * The supplied state parameter is guaranteed to always be the latest state,
35 | * even if there are other state mutation blocks in the queue
36 | */
37 | suspend fun get(block: suspend (S) -> Unit)
38 |
39 | /**
40 | * A [ConflatedBroadcastChannel] to expose the state as an observable entity.
41 | * Any new state produced by the reducers given to [set] is passed to this channel.
42 | *
43 | * This channel is conflated, so only the latest state value is present in it
44 | */
45 | val stateChannel: ConflatedBroadcastChannel
46 |
47 | /**
48 | * This method is expected to be called by the owning ViewModel of this class
49 | * to cleanup the resources, and close all channels in this State Store
50 | */
51 | fun cleanup()
52 | }
--------------------------------------------------------------------------------
/docs/components/saved-state-vectorviewmodel.md:
--------------------------------------------------------------------------------
1 | # SavedState VectorViewModel
2 |
3 | A subclass of [`VectorViewModel`](vector-viewmodel.md) which provides easier state persistence across process deaths, by providing access to a `SavedStateHandle` from [ViewModel SavedState](https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate) library in Android Jetpack.
4 |
5 | ## Making state persistable
6 |
7 | The easiest way to persist your UI state is to simply save the entire state object using the saved state handle. However, you state class needs to implementing `Parcelable` for this, which is a tedious and error prone process.
8 |
9 | Luckily, Kotlin comes with an [Android Extension](https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate) which helps use make our classes `Parcelable` with just a single annotation.
10 |
11 | ```kotlin
12 | import kotlinx.android.parcel.Parcelize
13 |
14 | @Parcelize
15 | data class UserState(
16 | val userId: Long = -1,
17 | val user: User? = null,
18 | val isUserPremium: Boolean = false
19 | ): Parcelable
20 | ```
21 |
22 | Make sure that you have turned on the experimental flag in your `build.gradle` file to be able to access this feature:
23 |
24 | ```groovy
25 | androidExtensions {
26 | experimental = true
27 | }
28 | ```
29 |
30 | ## Persisting State
31 |
32 | The `SavedStateVectorViewModel` class has a `setStateAndPersist` method which is the same as the regular `setState` method, except that it also persists the new state.
33 |
34 | ```kotlin
35 | fun greetUser() = setStateAndPersist {
36 | copy(greeting = "Hello!")
37 | }
38 | ```
39 |
40 | If you want to exclude some properties in your state object from being persisted, you must annotate them with `@Transient`.
41 |
42 | ```kotlin
43 | @Parcelize
44 | data class UserState(
45 | val userId: Long = -1,
46 | @Transient val user: User? = null,
47 | val isUserPremium: Boolean = false
48 | ): Parcelable
49 | ```
50 |
51 | This method by default tries to persist your entire state object using `KEY_SAVED_STATE` key defined in this class. If you need to customize this behaviour, you should override the `persistState` method.
52 |
53 | ```kotlin
54 | override fun persistState() = withState { state ->
55 | // Your custom implementation
56 | }
57 | ```
58 |
--------------------------------------------------------------------------------
/docs/misc/automatic-viewmodel-creation.md:
--------------------------------------------------------------------------------
1 | # Automatic ViewModel creation
2 |
3 | Vector ships with some lazy delegates for instantiating ViewModels automatically.
4 |
5 | ```kotlin
6 | val viewModel by fragmentViewModel()
7 | by activityViewModel()
8 | by viewModel()
9 |
10 | val viewModel by fragmentViewModel { initialState, savedStateHandle -> ... }
11 | by activityViewModel { initialState, savedStateHandle -> ... }
12 | by viewModel { initialState, savedStateHandle -> ... }
13 | ```
14 |
15 | These delegates use Reflection to instantiate your ViewModels. The process goes as follows:
16 |
17 | * First, we try to create the initial state for your ViewModel using either the ViewModel factory or using the constructor.
18 | * If the ViewModel implements `VectorViewModelFactory` in its companion object, we attempt to create initial state using it
19 | * If the ViewModel does not implement that interface, or it does not override the `initialState()` method, then we attempt to create initial state using the state class constructor. For this to succeed, all properties in your state class must have default values.
20 | * If both the strategies fail, we throw an exception and crash.
21 |
22 | * Then, we try to create the ViewModel.
23 | * If the delegate has been supplied a trailing lambda which tells us how to produce the ViewModel, we invoke it, register the ViewModel with the `ViewModelProvider` for the calling Activity/Fragment and return it.
24 | * Otherwise, we check if the ViewModel implements `VectorViewModelFactory` in its companion object. If so, we attempt to create the ViewModel using its `create` method.
25 | * If the ViewModel does not implement that interface or if the returned ViewModel is null, we try to create the ViewModel using its constructor. For this to succeed, the ViewModel must have one of the following constructors:
26 | 1. ViewModel()
27 | 2. ViewModel(initialState)
28 | 3. ViewModel(initialState, savedStateHandle)
29 | 4. ViewModel(initialState, stateStoreContext, savedStateHandle)
30 |
31 | * If these conditions can not be met, we throw an exception and crash.
32 |
33 | Therefore, the ViewModel's `VectorViewModelFactory` is given priority for both tasks if it is implemented. Otherwise, we resort to constructor invocations.
34 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/state/StateHolderTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.state
2 |
3 | import com.haroldadmin.vector.loggers.StringLogger
4 | import kotlinx.coroutines.delay
5 | import kotlinx.coroutines.flow.collect
6 | import kotlinx.coroutines.flow.takeWhile
7 | import kotlinx.coroutines.launch
8 | import kotlinx.coroutines.runBlocking
9 | import org.junit.Test
10 |
11 | class StateHolderTest {
12 |
13 | @Test
14 | fun `StateHolderFactory creates correctly configured StateHolder instance`() {
15 |
16 | val initState = CountingState()
17 |
18 | val stateHolder = StateHolderFactory.create(
19 | initialState = initState,
20 | logger = StringLogger()
21 | )
22 |
23 | assert(stateHolder.state == initState)
24 | }
25 |
26 | @Test
27 | fun `state property contains the latest state`() {
28 | val initState = CountingState()
29 |
30 | val stateHolder = StateHolderFactory.create(
31 | initialState = initState,
32 | logger = StringLogger()
33 | )
34 |
35 | stateHolder.updateState(CountingState(count = 42))
36 | assert(stateHolder.state.count == 42) {
37 | "Expected current count to be 42, got ${stateHolder.state.count}"
38 | }
39 | }
40 |
41 | @Test
42 | fun `state updates are conflated`() = runBlocking {
43 | val initState = CountingState()
44 |
45 | val stateHolder = StateHolderFactory.create(initState, StringLogger())
46 |
47 | val numberOfUpdates = 10
48 | // Fast producer
49 | launch {
50 | for (i in 1..10) {
51 | val currentState = stateHolder.state
52 | val newState = currentState.copy(count = i)
53 | stateHolder.updateState(newState)
54 | }
55 | stateHolder.clearHolder()
56 | }
57 |
58 | var collectedUpdates = 0
59 | // Slow consumer
60 | stateHolder
61 | .stateObservable
62 | .takeWhile { it.count < numberOfUpdates }
63 | .collect {
64 | collectedUpdates++
65 | delay(1)
66 | }
67 |
68 | assert(collectedUpdates < numberOfUpdates) {
69 | "StateUpdates were not conflated, received as many updates as were produced"
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/vector/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.github.dcendents.android-maven'
5 | apply plugin: 'kotlin-kapt'
6 | apply plugin: 'org.jetbrains.dokka-android'
7 |
8 | group = "com.github.haroldadmin"
9 |
10 | android {
11 | compileSdkVersion buildConfig.compileSdk
12 |
13 | defaultConfig {
14 | minSdkVersion buildConfig.minSdk
15 | targetSdkVersion buildConfig.targetSdk
16 | versionCode buildConfig.versionCode
17 | versionName buildConfig.versionName
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | consumerProguardFiles 'consumer-rules.pro'
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility 1.8
30 | targetCompatibility 1.8
31 | }
32 | kotlinOptions {
33 | jvmTarget = "1.8"
34 | }
35 | testOptions {
36 | unitTests {
37 | includeAndroidResources = true
38 | }
39 | }
40 | androidExtensions {
41 | experimental = true
42 | }
43 | }
44 |
45 | dokka {
46 | outputFormat = "gfm"
47 | outputDirectory = "$rootDir/docs/api/"
48 | externalDocumentationLink {
49 | url = new URL("https://developer.android.com/reference/")
50 | packageListUrl = new URL("https://developer.android.com/reference/androidx/package-list")
51 | }
52 | }
53 |
54 | dependencies {
55 | implementation fileTree(dir: 'libs', include: ['*.jar'])
56 |
57 | api libs.kotlinStdLib
58 | api libs.coroutinesCore
59 | api libs.coroutinesAndroid
60 | implementation libs.kotlinReflect
61 |
62 | api libs.fragmentKtx
63 | api libs.vmSavedState
64 | api libs.viewModel
65 | implementation libs.appCompat
66 | implementation libs.lifecycleRuntime
67 |
68 | testImplementation libs.junit
69 | testImplementation libs.coroutinesTest
70 | testImplementation libs.mockk
71 | testImplementation libs.robolectric
72 | debugImplementation libs.fragmentTest
73 |
74 | androidTestImplementation libs.androidxTestCore
75 | androidTestImplementation libs.espressoCore
76 | }
77 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/actorStateStore/StateStoreBenchmark.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.actorStateStore
2 |
3 | import androidx.benchmark.junit4.BenchmarkRule
4 | import androidx.benchmark.junit4.measureRepeated
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import com.haroldadmin.vector.benchmark.TestState
7 | import org.junit.Ignore
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 |
12 | @Ignore("We don't want benchmarks to run with regular builds")
13 | @RunWith(AndroidJUnit4::class)
14 | class StateStoreBenchmark {
15 |
16 | @get:Rule
17 | val benchmarkRule = BenchmarkRule()
18 |
19 | private val stateStore =
20 | ActorsStateStoreImpl(TestState())
21 |
22 | @Test
23 | fun setStateTest() {
24 | benchmarkRule.measureRepeated {
25 | stateStore.set { copy(count = this.count + 1) }
26 | }
27 | }
28 |
29 | @Test
30 | fun simpleRecursiveCall() {
31 | benchmarkRule.measureRepeated {
32 | stateStore.set {
33 | stateStore.get {
34 | // Do nothing
35 | }
36 | this
37 | }
38 | }
39 | }
40 |
41 | @Test
42 | fun complexRecursiveCall() {
43 | benchmarkRule.measureRepeated {
44 | stateStore.get {
45 | stateStore.set {
46 | stateStore.set { this }
47 | stateStore.get {
48 | stateStore.set { this }
49 | }
50 | this
51 | }
52 | }
53 | }
54 | }
55 |
56 | @Test
57 | fun multiLevelStateOperation() {
58 | benchmarkRule.measureRepeated {
59 | stateStore.get { state1 ->
60 | if (state1.count % 2 == 0) {
61 | stateStore.get { state2 ->
62 | if (state2.count % 4 == 0) {
63 | stateStore.set { copy(count = count + 2) }
64 | } else {
65 | stateStore.set { copy(count = count + 1) }
66 | }
67 | }
68 | } else {
69 | stateStore.set { copy(count = count + 2) }
70 | }
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/hello-vector/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion buildConfig.compileSdk
7 |
8 | defaultConfig {
9 | minSdkVersion buildConfig.minSdk
10 | targetSdkVersion buildConfig.targetSdk
11 | versionCode buildConfig.versionCode
12 | versionName buildConfig.versionName
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles 'consumer-rules.pro'
15 | vectorDrawables.useSupportLibrary = true
16 | }
17 |
18 | signingConfigs {
19 | debug {
20 | storeFile file('debug.keystore')
21 | storePassword 'vector'
22 | keyAlias 'vector'
23 | keyPassword 'vector'
24 | }
25 | }
26 |
27 | buildTypes {
28 | release {
29 | signingConfig signingConfigs.debug
30 | minifyEnabled true
31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
32 | }
33 | }
34 |
35 | androidExtensions {
36 | experimental = true
37 | }
38 |
39 | testOptions {
40 | unitTests {
41 | includeAndroidResources = true
42 | }
43 | }
44 |
45 | compileOptions {
46 | sourceCompatibility 1.8
47 | targetCompatibility 1.8
48 | }
49 |
50 | kotlinOptions {
51 | jvmTarget = "1.8"
52 | }
53 | }
54 |
55 | dependencies {
56 | implementation fileTree(dir: 'libs', include: ['*.jar'])
57 |
58 | implementation project(path: ':vector')
59 |
60 | implementation libs.kotlinStdLib
61 | implementation libs.coroutinesCore
62 | implementation libs.coroutinesAndroid
63 |
64 | implementation libs.appCompat
65 | implementation libs.lifecycle
66 | implementation libs.lifecycleRuntime
67 | implementation libs.coreKtx
68 | implementation libs.fragmentKtx
69 | implementation libs.constraintLayout
70 | implementation libs.vmSavedState
71 |
72 | testImplementation libs.junit
73 | testImplementation libs.coroutinesTest
74 | testImplementation libs.mockk
75 | testImplementation libs.robolectric
76 | debugImplementation libs.fragmentTest
77 | testImplementation libs.espressoCore
78 |
79 | androidTestImplementation libs.androidxTestExt
80 | androidTestImplementation libs.androidxTestCore
81 | }
82 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/fragment_entities.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
19 |
20 |
29 |
30 |
39 |
40 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/ReflectionExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import kotlin.reflect.KClass
4 | import kotlin.reflect.full.declaredFunctions
5 | import kotlin.reflect.full.memberFunctions
6 |
7 | /**
8 | * Tries to find the companion object of a class that implements [VectorViewModelFactory] and
9 | * returns it. If no such companion object is found, it throws [DoesNotImplementVectorVMFactoryException]
10 | */
11 | internal fun Class<*>.factoryCompanion(): Class<*> {
12 | return companionObject()?.let { clazz ->
13 | if (clazz doesImplement VectorViewModelFactory::class.java) {
14 | clazz
15 | } else {
16 | null
17 | }
18 | } ?: throw DoesNotImplementVectorVMFactoryException()
19 | }
20 |
21 | /**
22 | * Overloaded version of java class factoryCompanion method
23 | */
24 | internal fun KClass<*>.factoryCompanion(): Class<*> {
25 | return this.java.factoryCompanion()
26 | }
27 |
28 | internal fun KClass<*>.factoryKompanion(): KClass<*> {
29 | return this.factoryCompanion().kotlin
30 | }
31 |
32 | /**
33 | * Tries to find the companion object of the given class, and returns it. If the class does not
34 | * have a companion object, returns null
35 | */
36 | internal fun Class<*>.companionObject(): Class<*>? {
37 | return try {
38 | Class.forName("$name\$Companion")
39 | } catch (ex: ClassNotFoundException) {
40 | null
41 | }
42 | }
43 |
44 | /**
45 | * Creates a new instance of the given class using the constructor having one parameter only.
46 | * If no such constructor exists, returns null.
47 | */
48 | internal fun Class<*>.instance(initArg: Any? = null): Any? {
49 | return declaredConstructors.firstOrNull { it.parameterTypes.size == 1 }?.newInstance(initArg)
50 | }
51 |
52 | /**
53 | * Syntactic sugar for [Class.isAssignableFrom] method
54 | */
55 | internal infix fun Class<*>.doesImplement(other: Class<*>): Boolean {
56 | return other.isAssignableFrom(this)
57 | }
58 |
59 | internal infix fun KClass<*>.doesImplement(other: KClass<*>): Boolean {
60 | return other.java.isAssignableFrom(this.java)
61 | }
62 |
63 | /**
64 | * Kotlin interfaces with default methods don't create equivalent Java interfaces with default methods.
65 | * https://youtrack.jetbrains.com/issue/KT-4779
66 | *
67 | * Therefore we can't use general java reflection to find out if a class has overridden a default interface method.
68 | * This extension allows us to do it though, using Kotlin reflection.
69 | */
70 | internal infix fun KClass<*>.doesOverride(methodName: String): Boolean {
71 | return this.memberFunctions.first { it.name == methodName } in this.declaredFunctions
72 | }
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/actorStateStore/ActorsStateStoreImpl.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.actorStateStore
2 |
3 | import com.haroldadmin.vector.VectorState
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.cancel
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
9 | import kotlinx.coroutines.channels.actor
10 | import kotlinx.coroutines.channels.consumeEach
11 | import java.util.ArrayDeque
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | private interface Action
15 | private inline class SetStateAction(val reducer: suspend S.() -> S) : Action
16 | private inline class GetStateAction(val block: suspend (S) -> Unit) : Action
17 |
18 | /**
19 | * An Implementation of [StateStore] interface. This class is expected to be owned by a
20 | * [VectorViewModel] which calls [cleanup] when it is cleared
21 | *
22 | * @param initialState The initial state object with which the owning ViewModel was created
23 | */
24 | internal class ActorsStateStoreImpl(
25 | initialState: S,
26 | override val coroutineContext: CoroutineContext = Dispatchers.Default
27 | ) : ActorsStateStore, CoroutineScope {
28 |
29 | /**
30 | * A [ConflatedBroadcastChannel] to expose the latest value of state to its
31 | * subscribers
32 | */
33 | override val stateChannel = ConflatedBroadcastChannel(initialState)
34 |
35 | override val state: S
36 | get() = stateChannel.value
37 |
38 | private val actionsActor = actor>(capacity = Channel.UNLIMITED) {
39 |
40 | val getStateQueue = ArrayDeque Unit>()
41 |
42 | consumeEach { action ->
43 | when (action) {
44 | is SetStateAction -> {
45 | val newState = action.reducer(state)
46 | stateChannel.offer(newState)
47 | }
48 | is GetStateAction -> {
49 | getStateQueue.offer(action.block)
50 | }
51 | }
52 |
53 | getStateQueue
54 | .takeWhile { channel.isEmpty }
55 | .map { block -> block(state) }
56 | }
57 | }
58 |
59 | override fun set(action: suspend S.() -> S) {
60 | actionsActor.offer(SetStateAction(action))
61 | }
62 |
63 | override fun get(block: suspend (S) -> Unit) {
64 | actionsActor.offer(GetStateAction(block))
65 | }
66 |
67 | override fun cleanup() {
68 | actionsActor.close()
69 | stateChannel.close()
70 | this.cancel() // Cancel coroutine scope
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/benchmark/src/androidTest/java/com/haroldadmin/vector/benchmark/regularStateStore/RegularStateStoreImpl.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector.benchmark.regularStateStore
2 |
3 | import com.haroldadmin.vector.VectorState
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.asCoroutineDispatcher
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
9 | import kotlinx.coroutines.withContext
10 | import java.util.concurrent.Executors
11 |
12 | /**
13 | * An Implementation of [StateStore] interface. This class is expected to be owned by a
14 | * [VectorViewModel] which calls [cleanup] when it is cleared
15 | *
16 | * @param initialState The initial state object with which the owning ViewModel was created
17 | */
18 | internal class RegularStateStoreImpl(
19 | initialState: S
20 | ) : RegularStateStore {
21 |
22 | private val executor = Executors.newSingleThreadExecutor()
23 | private val job = Job()
24 | private val stateStoreContext = executor.asCoroutineDispatcher() + job
25 | private val stateStoreScope = CoroutineScope(stateStoreContext)
26 |
27 | /**
28 | * A [ConflatedBroadcastChannel] to expose the latest value of state to its
29 | * subscribers
30 | */
31 | override val stateChannel = ConflatedBroadcastChannel(initialState)
32 |
33 | override val state: S
34 | get() = stateChannel.value
35 |
36 | override suspend fun set(action: suspend S.() -> S) = withContext(stateStoreContext) {
37 | setStateQueue.offer(action)
38 | flushQueues()
39 | }
40 |
41 | override suspend fun get(block: suspend (S) -> Unit) = withContext(stateStoreContext) {
42 | getStateQueue.offer(block)
43 | flushQueues()
44 | }
45 |
46 | private val setStateQueue: Channel S> = Channel(capacity = Channel.UNLIMITED)
47 | private val getStateQueue: Channel Any> = Channel(capacity = Channel.UNLIMITED)
48 |
49 | private suspend fun flushQueues(): Unit = withContext(stateStoreContext) {
50 | flushSetStateQueue()
51 | getStateQueue.poll()?.invoke(state) ?: return@withContext
52 | flushQueues()
53 | }
54 |
55 | private suspend fun flushSetStateQueue(): Unit = withContext(stateStoreContext) {
56 | val stateReducer = setStateQueue.poll()
57 | if (stateReducer != null) {
58 | val newState = state.stateReducer()
59 | stateChannel.offer(newState)
60 | } else {
61 | return@withContext
62 | }
63 | flushSetStateQueue()
64 | }
65 |
66 | override fun cleanup() {
67 | job.cancel()
68 | stateChannel.close()
69 | setStateQueue.close()
70 | getStateQueue.close()
71 | executor.shutdownNow()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/sampleapp/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.squareup.sqldelight'
5 | apply plugin: "androidx.navigation.safeargs.kotlin"
6 | apply plugin: 'kotlin-kapt'
7 |
8 | android {
9 | compileSdkVersion buildConfig.compileSdk
10 |
11 | defaultConfig {
12 | minSdkVersion buildConfig.minSdk
13 | targetSdkVersion buildConfig.targetSdk
14 | versionCode buildConfig.versionCode
15 | versionName buildConfig.versionName
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | consumerProguardFiles 'consumer-rules.pro'
18 | vectorDrawables.useSupportLibrary = true
19 | }
20 |
21 | signingConfigs {
22 | debug {
23 | storeFile file('debug.keystore')
24 | storePassword 'vector'
25 | keyAlias 'vector'
26 | keyPassword 'vector'
27 | }
28 | }
29 |
30 | buildTypes {
31 | release {
32 | signingConfig signingConfigs.debug
33 | minifyEnabled true
34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35 | }
36 | }
37 |
38 | dataBinding {
39 | enabled = true
40 | }
41 |
42 | androidExtensions {
43 | experimental = true
44 | }
45 |
46 | compileOptions {
47 | sourceCompatibility 1.8
48 | targetCompatibility 1.8
49 | }
50 |
51 | kotlinOptions {
52 | jvmTarget = "1.8"
53 | }
54 | }
55 |
56 | dependencies {
57 | implementation fileTree(dir: 'libs', include: ['*.jar'])
58 |
59 | implementation project(path: ':vector')
60 |
61 | implementation libs.kotlinStdLib
62 | implementation libs.coroutinesCore
63 | implementation libs.coroutinesAndroid
64 |
65 | implementation libs.appCompat
66 | implementation libs.lifecycle
67 | implementation libs.lifecycleRuntime
68 | implementation libs.coreKtx
69 | implementation libs.fragmentKtx
70 | implementation libs.constraintLayout
71 | implementation libs.recyclerView
72 | implementation libs.materialComponents
73 | implementation libs.navigation
74 | implementation libs.navigationUi
75 | implementation libs.vmSavedState
76 |
77 | implementation libs.dagger.dagger
78 | implementation libs.dagger.daggerAndroid
79 | implementation libs.dagger.daggerAndroidSupport
80 | implementation libs.dagger.assistedInject
81 | kapt libs.dagger.compiler
82 | kapt libs.dagger.androidCompiler
83 | kapt libs.dagger.assistedInjectCompiler
84 |
85 | implementation libs.sqldelightDriver
86 |
87 | testImplementation libs.junit
88 |
89 | androidTestImplementation libs.androidxTestCore
90 | androidTestImplementation libs.androidxTestExt
91 | androidTestImplementation libs.espressoCore
92 | }
93 |
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/fragment_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
26 |
27 |
44 |
45 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/ViewModelExtensionsTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.os.Build
4 | import androidx.fragment.app.Fragment
5 | import androidx.fragment.app.FragmentActivity
6 | import com.haroldadmin.vector.state.CountingState
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.Robolectric
11 | import org.robolectric.RobolectricTestRunner
12 | import org.robolectric.annotation.Config
13 |
14 | @RunWith(RobolectricTestRunner::class)
15 | @Config(sdk = [Build.VERSION_CODES.P])
16 | internal class ViewModelExtensionsTest {
17 |
18 | private lateinit var activity: DelegateTestActivity
19 | private lateinit var fragmentOne: DelegateTestActivity.DelegateTestFragmentOne
20 | private lateinit var fragmentTwo: DelegateTestActivity.DelegateTestFragmentTwo
21 |
22 | @Before
23 | fun setup() {
24 | activity = Robolectric.buildActivity(DelegateTestActivity::class.java).setup().get()
25 | fragmentOne = DelegateTestActivity.DelegateTestFragmentOne()
26 | fragmentTwo = DelegateTestActivity.DelegateTestFragmentTwo()
27 |
28 | activity.apply {
29 | addFragment(fragmentOne)
30 | addFragment(fragmentTwo)
31 | }
32 | }
33 |
34 | @Test
35 | fun fragmentViewModelTest() {
36 | withState(fragmentOne.unsharedViewModel) { state ->
37 | assert(state == CountingState())
38 | }
39 |
40 | withState(fragmentTwo.unsharedViewModel) { state ->
41 | assert(state == CountingState())
42 | }
43 |
44 | assert(fragmentOne.unsharedViewModel !== fragmentTwo.unsharedViewModel)
45 | }
46 |
47 | @Test
48 | fun activityViewModelDelegateTest() {
49 | assert(fragmentOne.sharedViewModel === fragmentTwo.sharedViewModel)
50 | assert(fragmentOne.sharedViewModel.currentState === fragmentTwo.sharedViewModel.currentState)
51 | }
52 |
53 | private class DelegateTestActivity : FragmentActivity() {
54 |
55 | fun addFragment(fragment: Fragment) {
56 | supportFragmentManager
57 | .beginTransaction()
58 | .add(fragment, null)
59 | .commitNow()
60 | }
61 |
62 | /**
63 | * Making these fragments as nested classes inside a private class instead of standalone private classes to
64 | * stop fragment manager from throwing an error for non public fragment classes.
65 | */
66 | class DelegateTestFragmentOne : Fragment() {
67 | val unsharedViewModel: DelegateTestViewModel by fragmentViewModel()
68 | val sharedViewModel: DelegateTestSharedViewModel by activityViewModel()
69 | }
70 |
71 | class DelegateTestFragmentTwo : Fragment() {
72 | val unsharedViewModel: DelegateTestViewModel by fragmentViewModel()
73 | val sharedViewModel: DelegateTestSharedViewModel by activityViewModel()
74 | }
75 | }
76 |
77 | private class DelegateTestViewModel : VectorViewModel(CountingState())
78 |
79 | private class DelegateTestSharedViewModel : VectorViewModel(CountingState())
80 | }
81 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorStateFactory.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModelStoreOwner
5 | import kotlin.reflect.KClass
6 |
7 | /**
8 | * Creates an initial state for a ViewModel using either the [VectorViewModelFactory] or using
9 | * the default constructor
10 | */
11 | internal interface VectorStateFactory {
12 |
13 | fun createInitialState(
14 | vmClass: KClass<*>,
15 | stateClass: KClass,
16 | handle: SavedStateHandle,
17 | owner: ViewModelOwner
18 | ): S
19 | }
20 |
21 | internal class RealStateFactory : VectorStateFactory {
22 |
23 | override fun createInitialState(
24 | vmClass: KClass<*>,
25 | stateClass: KClass,
26 | handle: SavedStateHandle,
27 | owner: ViewModelOwner
28 | ): S {
29 | return getStateFromVectorVMFactory(vmClass, handle, owner)
30 | ?: getDefaultStateFromConstructor(stateClass) //
31 | ?: throw UnInstantiableStateClassException(stateClass.java.simpleName)
32 | }
33 |
34 | /**
35 | * Checks if the ViewModel implements a [VectorViewModelFactory] in its companion object, and if so, uses it
36 | * to create the initial state if the factory implements the corresponding function. If any of these conditions are not met,
37 | * returns null
38 | */
39 | private fun getStateFromVectorVMFactory(
40 | vmClass: KClass<*>,
41 | handle: SavedStateHandle,
42 | owner: ViewModelOwner
43 | ): S? {
44 |
45 | val factoryClass = try {
46 | vmClass.factoryCompanion()
47 | } catch (ex: DoesNotImplementVectorVMFactoryException) {
48 | null
49 | }
50 |
51 | return factoryClass?.let { clazz ->
52 | try {
53 | @Suppress("UNCHECKED_CAST")
54 | clazz.getMethod("initialState", SavedStateHandle::class.java, ViewModelOwner::class.java)
55 | .invoke(factoryClass.instance(), handle, owner) as S?
56 | } catch (ex: NoSuchMethodException) {
57 | // Look for JvmStatic method
58 | @Suppress("UNCHECKED_CAST")
59 | clazz.getMethod("initialState", SavedStateHandle::class.java, ViewModelStoreOwner::class.java)
60 | .invoke(null, handle, owner) as S?
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * Creates the initial state using the default constructor of the state class.
67 | */
68 | private fun getDefaultStateFromConstructor(stateClass: KClass): S? {
69 | return stateClass.let {
70 | try {
71 | // Use Java reflection version for new instance creation as it is faster
72 | @Suppress("UNCHECKED_CAST")
73 | stateClass.java.newInstance()
74 | } catch (e: NoSuchMethodException) {
75 | null
76 | } catch (e: InstantiationException) {
77 | null
78 | }
79 | }
80 | }
81 | }
82 |
83 | internal class UnInstantiableStateClassException(
84 | className: String
85 | ) : IllegalArgumentException("$className could not be instantiated without a VectorViewModelFactory")
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/entities/EntitiesFragment.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.entities
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.navigation.fragment.findNavController
10 | import androidx.recyclerview.widget.DividerItemDecoration
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import com.haroldadmin.sampleapp.AppViewModel
13 | import com.haroldadmin.sampleapp.databinding.FragmentEntitiesBinding
14 | import com.haroldadmin.sampleapp.utils.hide
15 | import com.haroldadmin.sampleapp.utils.show
16 | import com.haroldadmin.vector.activityViewModel
17 | import com.haroldadmin.vector.fragmentViewModel
18 | import com.haroldadmin.vector.renderState
19 | import dagger.android.support.AndroidSupportInjection
20 | import javax.inject.Inject
21 |
22 | class EntitiesFragment : Fragment() {
23 |
24 | @Inject lateinit var viewModelFactory: EntitiesViewModel.Factory
25 | @Inject lateinit var appViewModelFactory: AppViewModel.Factory
26 |
27 | private lateinit var binding: FragmentEntitiesBinding
28 |
29 | private val viewModel: EntitiesViewModel by fragmentViewModel { initialState, _ ->
30 | viewModelFactory.create(initialState)
31 | }
32 |
33 | private val appViewModel: AppViewModel by activityViewModel { initialState, _ ->
34 | appViewModelFactory.create(initialState)
35 | }
36 |
37 | private val entitiesAdapter = EntitiesAdapter(EntitiesDiffCallback()) { entity ->
38 | findNavController().navigate(
39 | EntitiesFragmentDirections.editEntity(
40 | entity.id,
41 | entity.name,
42 | entity.counter
43 | )
44 | )
45 | }
46 |
47 | override fun onAttach(context: Context) {
48 | AndroidSupportInjection.inject(this)
49 | super.onAttach(context)
50 | }
51 |
52 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
53 | binding = FragmentEntitiesBinding.inflate(inflater, container, false)
54 |
55 | binding.rvEntities.apply {
56 | layoutManager = LinearLayoutManager(requireContext())
57 | adapter = entitiesAdapter
58 | addItemDecoration(DividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL))
59 | }
60 |
61 | binding.addEntity.setOnClickListener {
62 | findNavController().navigate(EntitiesFragmentDirections.addEntity())
63 | }
64 |
65 | viewModel.getAllEntities()
66 |
67 | renderState(viewModel) { state ->
68 | entitiesAdapter.submitList(state.entities)
69 | if (state.entities.isNullOrEmpty()) {
70 | binding.emptyListMessage.show()
71 | binding.pbLoading.hide()
72 | } else {
73 | binding.emptyListMessage.hide()
74 | if (state.isLoading) {
75 | binding.pbLoading.show()
76 | } else {
77 | binding.pbLoading.hide()
78 | }
79 | }
80 | appViewModel.updateNumberOfEntities()
81 | }
82 |
83 | return binding.root
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/vector/src/test/java/com/haroldadmin/vector/VectorFragmentTest.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.testing.launchFragmentInContainer
9 | import androidx.lifecycle.Lifecycle
10 | import com.haroldadmin.vector.state.CountingState
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.runBlocking
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import org.robolectric.RobolectricTestRunner
16 | import org.robolectric.annotation.Config
17 | import java.util.Timer
18 | import kotlin.concurrent.fixedRateTimer
19 |
20 | @RunWith(RobolectricTestRunner::class)
21 | @Config(sdk = [Build.VERSION_CODES.P])
22 | internal class VectorFragmentTest {
23 |
24 | @Test
25 | fun `renderState should stop collecting state updates after view has been destroyed`() {
26 | // Store fragment instance in a variable to make assertions after it has destroyed
27 | var fragmentInstance: RendererTestFragment? = null
28 |
29 | val scenario = launchFragmentInContainer()
30 | scenario.onFragment { fragment ->
31 | fragmentInstance = fragment
32 | runBlocking {
33 | delay(RendererTestViewModel.initialDelay) // Wait for first state update
34 | assert(fragment.lastUpdatedState != null) {
35 | "State updates are not being collected even after view has been created"
36 | }
37 | }
38 | }
39 |
40 | scenario.moveToState(Lifecycle.State.DESTROYED) // Cancel the viewLifecycle coroutine scope
41 |
42 | fragmentInstance!!.let { fragment ->
43 | runBlocking {
44 | val currentCount = fragment.lastUpdatedState!!.count
45 | delay(RendererTestViewModel.period) // Wait for next state update
46 | val nextCount = fragment.lastUpdatedState!!.count
47 | assert(nextCount == currentCount) {
48 | "State updates are being collected even after the fragment's view has been destroyed"
49 | }
50 | }
51 | }
52 |
53 | fragmentInstance == null
54 | }
55 |
56 | internal class RendererTestFragment : VectorFragment() {
57 |
58 | private val viewModel: RendererTestViewModel by fragmentViewModel()
59 | var lastUpdatedState: CountingState? = null
60 |
61 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
62 | renderState(viewModel) { state -> lastUpdatedState = state }
63 | return View(requireContext())
64 | }
65 | }
66 |
67 | internal class RendererTestViewModel : VectorViewModel(CountingState()) {
68 |
69 | companion object {
70 | const val initialDelay = 100L
71 | const val period = 100L
72 | }
73 |
74 | val timer: Timer = fixedRateTimer(
75 | name = "countdown",
76 | daemon = true,
77 | initialDelay = initialDelay,
78 | period = period
79 | ) {
80 | setState {
81 | copy(count = count + 1)
82 | }
83 | }
84 |
85 | override fun onCleared() {
86 | super.onCleared()
87 | timer.cancel()
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/docs/components/vector-state.md:
--------------------------------------------------------------------------------
1 | # Vector State
2 |
3 | Vector recommends using immutable [Kotlin Data Classes](https://kotlinlang.org/docs/reference/data-classes.html?q=&p=0#data-classes) to represent your UI Model classes. Such classes should implement the `VectorState` interface, because the `VectorViewModel` class is generic on a subtype of this interface.
4 |
5 | It is recommended to keep your state classes immutable, otherwise you risk your UI model getting into inconsistent states when there are multiple sources producing state updates concurrently.
6 |
7 | ## Example
8 |
9 | ```kotlin
10 | data class ProfilePageState(
11 | val user: User,
12 | val isLoading: Boolean,
13 | val isError: Boolean
14 | ): VectorState
15 | ```
16 |
17 | Using a Data Class provides the benefit of the automatically generated `copy()` method. It allows you to mutate the state very easily, you just need to provide the values that have actually changed, and the others will be kept the same. For example, if the User Profile page in the above example starts in the `Loading` state, the initial state model would look like this:
18 |
19 | ```kotlin
20 | val initialState = ProfilePageState(
21 | user = cachedUser,
22 | isLoading = true,
23 | isError = false
24 | )
25 | ```
26 |
27 | When the loading completes, the state can be mutated easily like this:
28 |
29 | ```kotlin
30 | val newState = initialState.copy(
31 | user = userRetrievedFromNetwork,
32 | isLoading = false
33 | )
34 | ```
35 |
36 | Since the `isError` variable value remains the same (false), we do not need to supply it in the `copy` method.
37 |
38 | ## Sealed Class based Model Classes
39 |
40 | A great way to represent all possible states a screen can be in is using Kotlin's Sealed Classes.
41 |
42 | Continuing the profile page example, suppose we want to show a different types of information based on whether the user is a premium user or not. We can do it this way:
43 |
44 | ```kotlin
45 | sealed class ProfilePageState: VectorState {
46 | data class PremiumProfilePage(
47 | val user: User,
48 | val accountPerks: List,
49 | val isLoading: Boolean,
50 | val isError: Boolean
51 | ): ProfilePageState()
52 |
53 | data class StandardProfilePage(
54 | val user: User,
55 | val isLoading: Boolean,
56 | val isError: Boolean
57 | ): ProfilePageState()
58 | }
59 | ```
60 |
61 | This allows you to separate similar, but related states of a screen. It increases verbosity though, and you lose direct access to convenient data class methods, unless you type cast the state object into one of its subclasses.
62 |
63 | ## Persistable state
64 |
65 | [Kotlin Extensions for Android](https://kotlinlang.org/docs/tutorials/android-plugin.html) have the ability to automatically generate `Parcelable` implementations of data classes with the `@Parcelize` annotation. This can be leveraged to easily persist state classes when needed.
66 |
67 | ```kotlin
68 | @Parcelize
69 | data class ProfilePageState(
70 | ...
71 | ): Parcelable
72 | ```
73 |
74 | When needed, this state can be directly put as a `Serializable` into a `SavedInstanceState` bundle or a `SavedStateHandle` in a ViewModel.
75 |
76 | ## Automatic State Restoration
77 |
78 | If you use the lazy ViewModel delegates shipped with Vector, you must ensure that either:
79 |
80 | * Your state class has default values for every property, or
81 | * Your ViewModel class implements the `VectorViewModelFactory` interface along with its `initialState` method
82 |
83 | This is to ensure that we can create an instance of your state class automatically.
84 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.lifecycleScope
5 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel
6 | import kotlinx.coroutines.flow.collect
7 |
8 | /**
9 | * A convenience function to access current state and execute an action on it.
10 | *
11 | * @param S The type of the current state
12 | * @param block The action to be performed using the current state
13 | *
14 | * Example:
15 | *
16 | * class MyViewModel(): VectorViewModel()
17 | *
18 | * class MyFragment(): VectorFragment {
19 | * onViewCreated(...) {
20 | * withState(viewModel) { state ->
21 | * if (state.isPremiumUser) {
22 | * premiumFeature.enable()
23 | * }
24 | * }
25 | * }
26 | * }
27 | *
28 | * Note: The state provided to the block is not guaranteed to be the latest state, because there
29 | * might be other state mutation blocks in the State Store's queue
30 | *
31 | * Warning: This WILL cause your app to crash if you create your ViewModels without initial state
32 | * and fail to provide it later, before calling this function.
33 | */
34 | inline fun withState(
35 | viewModel: VectorViewModel,
36 | crossinline block: (S) -> Unit
37 | ) {
38 | block(viewModel.currentState)
39 | }
40 |
41 | /**
42 | * Renders the UI based on the given [state] parameter using the [renderer] block. If your fragment is tied to a
43 | * [VectorViewModel] then consider using the overloaded version of the method which takes in a viewModel as an
44 | * input parameter.
45 | *
46 | * @param state The state instance using which the UI should be rendered
47 | * @param renderer The method which updates the UI state
48 | */
49 | @Suppress("unused")
50 | inline fun Fragment.renderState(state: S, renderer: (S) -> Unit) {
51 | renderer(state)
52 | }
53 |
54 | /**
55 | * Renders the UI based on emitted state updates from the given [viewModel] using the [renderer]
56 | * block.
57 | *
58 | * Launches a coroutine in the view's lifecycle scope which collects state updates from the given
59 | * [viewModel] and calls the [renderer] method on it. The renderer method interacts with the Fragment's views, and
60 | * therefore must only be called within the lifecycle of the view. As such, use it in or after [Fragment.onCreateView].
61 | *
62 | * The [renderer] parameter is a suspending function
63 | * It can be used to safely run coroutines which affect the UI.
64 | *
65 | * @param viewModel The ViewModel whose [VectorViewModel.state] flow is used to receive state updates and
66 | * render the UI
67 | * @param renderer The method which updates the UI
68 | */
69 | inline fun Fragment.renderState(
70 | viewModel: VectorViewModel,
71 | crossinline renderer: suspend (S) -> Unit
72 | ) {
73 |
74 | viewLifecycleOwner.lifecycleScope.launchWhenCreated {
75 | viewModel.state.collect { state ->
76 | renderer(state)
77 | }
78 | }
79 | }
80 |
81 | /**
82 | * A convenience function to update the value present in the Receiving [ConflatedBroadcastChannel]
83 | *
84 | * Takes in a function to calculate a new value based on the current value in the channel,
85 | * and then sets the new value to the channel.
86 | */
87 | internal inline fun ConflatedBroadcastChannel.compute(crossinline newValueProvider: (T) -> T): Boolean {
88 |
89 | if (this.isClosedForSend) return false
90 |
91 | val newValue = newValueProvider.invoke(this.value)
92 | this.offer(newValue)
93 | return true
94 | }
95 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/ViewModelOwner.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.fragment.app.Fragment
7 | import androidx.fragment.app.FragmentActivity
8 |
9 | /**
10 | * [ViewModelOwner] wraps the owner of a [VectorViewModel].
11 | *
12 | * Use it to get access to your object graph, arguments and saved instance state during the
13 | * creation of the ViewModel.
14 | *
15 | * **DO NOT STORE A REFERENCE TO THIS IN YOUR VIEWMODEL**
16 | */
17 | sealed class ViewModelOwner
18 |
19 | /**
20 | * A [ViewModelOwner] wrapping an Activity
21 | *
22 | * @property activity The underlying activity of this [ViewModelOwner]
23 | * @constructor Creates a [ViewModelOwner] wrapping the given Activity
24 | *
25 | */
26 | class ActivityViewModelOwner(
27 | val activity: ComponentActivity
28 | ) : ViewModelOwner() {
29 |
30 | /**
31 | * Get a type-casted version of the wrapped activity
32 | *
33 | * @param A The type of your Activity
34 | * @throws ClassCastException If the wrapped activity can not be casted to given activity type
35 | *
36 | */
37 | @Suppress("UNCHECKED_CAST")
38 | fun activity(): A = activity as A
39 | }
40 |
41 | /**
42 | * A [ViewModelOwner] which wraps the parent fragment of a ViewModel
43 | *
44 | * @property fragment The parent fragment wrapped in this class
45 | * @property args The supplied arguments to be used for a ViewModel creation
46 | * @constructor Creates a [ViewModelOwner] wrapping the given Fragment
47 | *
48 | */
49 | class FragmentViewModelOwner(
50 | val fragment: Fragment,
51 | private val args: Bundle? = fragment.arguments
52 | ) : ViewModelOwner() {
53 |
54 | /**
55 | * Get a type-casted version of the parent activity of the wrapped fragment
56 | *
57 | * @param A The type of the parent activity
58 | *
59 | */
60 | @Suppress("UNCHECKED_CAST")
61 | fun activity(): A = fragment.activity as A
62 |
63 | /**
64 | * Get a type-casted version of the parent fragment wrapped in this class
65 | *
66 | * @param F The type of the parent activity
67 | *
68 | */
69 | @Suppress("UNCHECKED_CAST")
70 | fun fragment(): F = fragment as F
71 |
72 | /**
73 | * Get the arguments bundle wrapped in this class
74 | */
75 | fun args(): Bundle? = args
76 | }
77 |
78 | /**
79 | * Get access to [Context] from a [ViewModelOwner].
80 | *
81 | * When this is an [ActivityViewModelOwner], it returns the wrapped activity itself.
82 | * When this is a [FragmentViewModelOwner], returns the context from the wrapped fragment.
83 | */
84 | fun ViewModelOwner.context(): Context {
85 | return when (this) {
86 | is ActivityViewModelOwner -> activity
87 | is FragmentViewModelOwner -> fragment.requireContext()
88 | }
89 | }
90 |
91 | /**
92 | * Creates a [ViewModelOwner] from this Fragment
93 | */
94 | fun Fragment.fragmentViewModelOwner(): FragmentViewModelOwner {
95 | return FragmentViewModelOwner(this)
96 | }
97 |
98 | /**
99 | * Creates a [ViewModelOwner] from the parent activity of this Fragment
100 | */
101 | fun Fragment.activityViewModelOwner(): ActivityViewModelOwner {
102 | return ActivityViewModelOwner(requireActivity())
103 | }
104 |
105 | /**
106 | * Creates a [ViewModelOwner] from this activity
107 | */
108 | fun ComponentActivity.activityViewModelOwner(): ActivityViewModelOwner {
109 | return ActivityViewModelOwner(this)
110 | }
111 |
--------------------------------------------------------------------------------
/sampleapp/src/main/java/com/haroldadmin/sampleapp/addEditEntity/AddEditEntityFragment.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.sampleapp.addEditEntity
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.lifecycleScope
10 | import com.google.android.material.snackbar.Snackbar
11 | import com.haroldadmin.sampleapp.AppViewModel
12 | import com.haroldadmin.sampleapp.R
13 | import com.haroldadmin.sampleapp.databinding.FragmentAddEntityBinding
14 | import com.haroldadmin.sampleapp.utils.debouncedTextChanges
15 | import com.haroldadmin.vector.activityViewModel
16 | import com.haroldadmin.vector.fragmentViewModel
17 | import com.haroldadmin.vector.renderState
18 | import dagger.android.support.AndroidSupportInjection
19 | import kotlinx.coroutines.flow.collect
20 | import kotlinx.coroutines.launch
21 | import javax.inject.Inject
22 |
23 | class AddEditEntityFragment : Fragment() {
24 |
25 | @Inject lateinit var viewModelFactory: AddEditEntityViewModel.Factory
26 | @Inject lateinit var appViewModelFactory: AppViewModel.Factory
27 |
28 | private lateinit var binding: FragmentAddEntityBinding
29 |
30 | private val viewModel: AddEditEntityViewModel by fragmentViewModel { initialState, handle ->
31 | viewModelFactory.create(initialState, handle)
32 | }
33 |
34 | private val appViewModel: AppViewModel by activityViewModel { initialState, _ ->
35 | appViewModelFactory.create(initialState)
36 | }
37 |
38 | override fun onAttach(context: Context) {
39 | AndroidSupportInjection.inject(this)
40 | super.onAttach(context)
41 | }
42 |
43 | override fun onCreateView(
44 | inflater: LayoutInflater,
45 | container: ViewGroup?,
46 | savedInstanceState: Bundle?
47 | ): View? {
48 | binding = FragmentAddEntityBinding.inflate(inflater, container, false)
49 |
50 | binding.apply {
51 | btIncrease.setOnClickListener {
52 | viewModel.incrementCount()
53 | }
54 |
55 | btDecrease.setOnClickListener {
56 | viewModel.decrementCount()
57 | }
58 |
59 | saveEntity.setOnClickListener {
60 | viewModel.saveEntity()
61 | appViewModel.updateNumberOfEntities()
62 | }
63 |
64 | lifecycleScope.launch {
65 | name.debouncedTextChanges(200)
66 | .collect { name ->
67 | viewModel.setName(name.toString())
68 | }
69 | }
70 | }
71 |
72 | renderState(viewModel) { state ->
73 | when (state) {
74 | is AddEditEntityState.AddEntity -> {
75 | with(binding) {
76 | count.text = state.count.toString()
77 | }
78 |
79 | if (state.isSaved) {
80 | Snackbar
81 | .make(binding.root, R.string.entitySavedMessage, Snackbar.LENGTH_SHORT)
82 | .show()
83 | }
84 | }
85 |
86 | is AddEditEntityState.EditEntity -> {
87 | with(binding) {
88 | count.text = state.count.toString()
89 | if (name.text.toString() != state.name) {
90 | name.setText(state.name)
91 | }
92 | }
93 |
94 | if (state.isSaved) {
95 | Snackbar
96 | .make(binding.root, R.string.entitySavedMessage, Snackbar.LENGTH_SHORT)
97 | .show()
98 | }
99 | }
100 | }
101 | }
102 |
103 | return binding.root
104 | }
105 | }
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.annotation.CallSuper
4 | import androidx.lifecycle.ViewModel
5 | import com.haroldadmin.vector.loggers.Logger
6 | import com.haroldadmin.vector.loggers.androidLogger
7 | import com.haroldadmin.vector.loggers.logd
8 | import com.haroldadmin.vector.loggers.logv
9 | import com.haroldadmin.vector.state.StateStoreFactory
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.onEach
14 | import kotlin.coroutines.CoroutineContext
15 |
16 | /**
17 | * The Base ViewModel class your ViewModel should inherit from
18 | *
19 | * @param S The state class for this ViewModel implementing [VectorState]
20 | * @param initialState The initial state for this ViewModel
21 | * @param stateStoreContext The [CoroutineContext] to be used with the state store
22 | *
23 | * A [VectorViewModel] can implement the [VectorViewModelFactory] in its Companion object
24 | * to provide ways to create the initial state, as well as the ViewModel itself.
25 | */
26 | abstract class VectorViewModel(
27 | initialState: S,
28 | stateStoreContext: CoroutineContext = Dispatchers.Default + Job()
29 | ) : ViewModel() {
30 |
31 | protected val logger: Logger by lazy { androidLogger(tag = this::class.java.simpleName) }
32 |
33 | /**
34 | * The state store associated with this ViewModel
35 | */
36 | protected open val stateStore = StateStoreFactory.create(
37 | initialState,
38 | androidLogger(this::class.java.simpleName + "StateStore"),
39 | stateStoreContext
40 | )
41 |
42 | /**
43 | * A [kotlinx.coroutines.flow.Flow] of [VectorState] which can be observed by external classes to respond to changes in state.
44 | */
45 | val state: Flow = stateStore
46 | .stateObservable
47 | .onEach { state ->
48 | logger.logd { "New state: $state" }
49 | }
50 |
51 | /**
52 | * Access the current value of state stored in the [stateStore].
53 | *
54 | * **THIS VALUE OF STATE IS NOT GUARANTEED TO BE UP TO DATE**
55 | * This property is only meant to be used by external classes who need to get hold of the current state
56 | * without having to subscribe to it. For use cases where the current state is needed to be accessed inside the
57 | * ViewModel, the [withState] method should be used
58 | */
59 | val currentState: S
60 | get() = stateStore.state
61 |
62 | /**
63 | * The only method through which state mutation is allowed in subclasses.
64 | *
65 | * Dispatches an action the [stateStore]. This action shall be processed as soon as possible in
66 | * the state store, but not necessarily immediately
67 | *
68 | * @param action The state reducer to create a new state from the current state
69 | *
70 | */
71 | protected fun setState(action: suspend S.() -> S) {
72 | stateStore.offerSetAction(action)
73 | }
74 |
75 | /**
76 | * Dispatch the given action the [stateStore]. This action shall be processed as soon as all existing
77 | * state reducers have been processed. The state parameter supplied to this action should be the
78 | * latest value at the time of processing of this action.
79 | *
80 | * These actions are treated as side effects. A new coroutine is launched for each such action, so that the state
81 | * processor does not get blocked if a particular action takes too long to finish.
82 | *
83 | * @param action The action to be performed with the current state
84 | *
85 | */
86 | protected fun withState(action: suspend (S) -> Unit) {
87 | stateStore.offerGetAction(action)
88 | }
89 |
90 | /**
91 | * Clears this ViewModel as well as its [stateStore].
92 | */
93 | @CallSuper
94 | override fun onCleared() {
95 | logger.logv { "Clearing ViewModel" }
96 | super.onCleared()
97 | stateStore.clear()
98 | }
99 | }
--------------------------------------------------------------------------------
/sampleapp/src/main/res/layout/fragment_add_entity.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
19 |
20 |
24 |
25 |
26 |
38 |
39 |
54 |
55 |
70 |
71 |
72 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vector
2 |
3 | 
4 |
5 | 
6 |
7 | Vector is an Android library to help implement the MVI architecture pattern.
8 |
9 | It is inspired from [MvRx](https://www.github.com/airbnb/mvrx) and [Roxie](https://github.com/ww-tech/roxie), but unlike them it is **built completely using Kotlin Coroutines** instead of RxJava. As such, it internally only uses Coroutine primitives, and has extensive support for Suspending functions.
10 |
11 | Vector works well with Android Architecture Components. It is 100% Kotlin, and is intended for use with Kotlin only.
12 |
13 | ## Building Blocks
14 |
15 | Vector is based primarily around three classes: `VectorViewModel`, `VectorState`, and `VectorFragment`.
16 |
17 | * **VectorViewModel**
18 |
19 | The Vector ViewModel class is the heart of any screen built with Vector. It is an abstract class extending the Android Architecture Components ViewModel class, and therefore survives configuration changes. It is generic on a class implementing the `VectorState` interface. It is also the only class which can mutate state.
20 |
21 | It exposes the current state through a `Kotlin Flow`.
22 |
23 | * **VectorState**
24 |
25 | VectorState is an interface denoting a model class representing the view's state. We recommend using Kotlin data classes to represent view state in the interest of keeping state immutable. Use the generated `copy()` method to create new state objects.
26 |
27 | * **VectorFragment**
28 |
29 | Vector provides an abstract `VectorFragment` class extending from AndroidX's Fragment class. A `VectorFragment` has a convenient coroutine scope, which can be used to easily launch Coroutines from a Fragment.
30 |
31 | ## Example
32 |
33 | Here's a contrived example to show how an app written in Vector looks like.
34 |
35 | > VectorState
36 |
37 | ```kotlin
38 | data class MyState(val message: String): VectorState
39 | ```
40 |
41 | > VectorFragment
42 |
43 | ```kotlin
44 | class MyFragment: VectorFragment() {
45 |
46 | private val myViewModel: MyViewModel by fragmentViewModel()
47 |
48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
49 | renderState(viewModel) { state ->
50 | toast(state.message)
51 | }
52 | }
53 | }
54 | ```
55 |
56 | > VectorViewModel
57 |
58 | ```kotlin
59 | class MyViewModel(initState: MyState): VectorViewModel(initState) {
60 |
61 | init {
62 | getMessage()
63 | }
64 |
65 | fun getMessage() = setState {
66 | copy(message = "Hello, world!")
67 | }
68 | }
69 | ```
70 |
71 | When the `setState()` function is given a state reducer, it internally enqueues it to a Kotlin `Actor`. The reducers passed to this actor are processed sequentially to avoid race conditions.
72 |
73 | ## Documentation
74 |
75 | The docs can be found at the project's [documentation website](https://haroldadmin.github.io/Vector).
76 |
77 | ## Projects using Vector
78 |
79 | * You can find a [sample app](https://github.com/haroldadmin/Vector/tree/master/sampleapp) along with the library in this repository.
80 | * [MoonShot](https://www.github.com/haroldadmin/MoonShot) is another project of mine. It's an app to help you keep up with SpaceX launches, and is built with Vector.
81 |
82 | If you would like your project using Vector to be featured here, please open an Issue on the repository. I shall take a look at it and add your project to the list.
83 |
84 | ## Installation Instructions
85 |
86 | Add the Jitpack repository to your top level `build.gradle` file.
87 |
88 | ```groovy
89 | allprojects {
90 | repositories {
91 | ...
92 | maven { url 'https://jitpack.io' }
93 | }
94 | }
95 | ```
96 |
97 | And then add the following dependency in your module's `build.gradle` file:
98 |
99 | ```groovy
100 | dependencies {
101 | implementation "com.github.haroldadmin:Vector:(latest-version)"
102 | }
103 | ```
104 |
105 | Find the latest **stable release** version on the [Releases](https://github.com/haroldadmin/Vector/releases) page.
106 |
107 | Latest release (stable/unstable):
108 | [](https://jitpack.io/#haroldadmin/Vector)
109 |
110 | ## Contributing
111 |
112 | If you like this project, or are using it in your app, consider starring the repository to show your support.
113 | Contributions from the community are very welcome.
114 |
--------------------------------------------------------------------------------
/vector/src/main/java/com/haroldadmin/vector/VectorFragment.kt:
--------------------------------------------------------------------------------
1 | package com.haroldadmin.vector
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.lifecycleScope
5 | import com.haroldadmin.vector.loggers.androidLogger
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.flow.collect
8 |
9 | /**
10 | * A Fragment which has a convenient fragmentScope property
11 | * to easily launch coroutines in it.
12 | */
13 | @Deprecated(
14 | message = "All the utilities provided by this class can be accessed through extension methods instead",
15 | replaceWith = ReplaceWith(
16 | expression = "Fragment",
17 | imports = ["androidx.fragment.app.Fragment"]
18 | )
19 | )
20 | abstract class VectorFragment : Fragment() {
21 |
22 | /**
23 | * A [CoroutineScope] associated with the lifecycle of this fragment. The scope is cancelled when
24 | * [onDestroy] of this Fragment has been called.
25 | */
26 | @Deprecated(
27 | message = "Use the AndroidX provided lifecycleScope extension instead",
28 | replaceWith = ReplaceWith(
29 | "lifecycleScope",
30 | "androidx.lifecycle.lifecycleScope"
31 | )
32 | )
33 | protected open val fragmentScope: CoroutineScope
34 | get() = lifecycleScope
35 |
36 | /**
37 | * A [CoroutineScope] associated with the view-lifecycle of this fragment. The scope is cancelled
38 | * when [onDestroyView] of this Fragment has been called, and created when [onCreateView] is called.
39 | *
40 | * This is deprecated, and simply delegates to the lifecycle library's [androidx.lifecycle.lifecycleScope] property
41 | */
42 | @Deprecated(
43 | message = "Use the AndroidX provided lifecycle scope extension instead",
44 | replaceWith = ReplaceWith(
45 | "viewLifecycleOwner.lifecycleScope",
46 | "androidx.lifecycle.lifecycleScope"
47 | )
48 | )
49 | protected open val viewScope: CoroutineScope
50 | get() = viewLifecycleOwner.lifecycleScope
51 |
52 | /**
53 | * Renders the UI based on the given [state] parameter using the [renderer] block. If your fragment is tied to a
54 | * [VectorViewModel] then consider using the overloaded version of the method which takes in a viewModel as an
55 | * input parameter.
56 | *
57 | * @param state The state instance using which the UI should be rendered
58 | * @param renderer The method which updates the UI state
59 | */
60 | @Deprecated(
61 | message = "Use the renderState extension method instead",
62 | replaceWith = ReplaceWith(
63 | "renderState",
64 | imports = ["com.haroldadmin.vector.renderState"]
65 | )
66 | )
67 | protected inline fun renderState(state: S, renderer: (S) -> Unit) {
68 | renderer(state)
69 | }
70 |
71 | /**
72 | * Renders the UI based on emitted state updates from the given [viewModel] using the [renderer]
73 | * block.
74 | *
75 | * Launches a coroutine in the view's lifecycle scope which collects state updates from the given
76 | * [viewModel] and calls the [renderer] method on it. The renderer method interacts with the Fragment's views, and
77 | * therefore must only be called within the lifecycle of the view. As such, use it in or after [onCreateView].
78 | *
79 | * The [renderer] parameter is a suspending function with a CoroutineScope of the Fragment's view lifecycle.
80 | * It can be used to safely run coroutines which affect the UI.
81 | *
82 | * @param viewModel The ViewModel whose [VectorViewModel.state] flow is used to receive state updates and
83 | * render the UI
84 | * @param renderer The method which updates the UI
85 | */
86 | @Deprecated(
87 | message = "Use the renderState extension method instead",
88 | replaceWith = ReplaceWith(
89 | "renderState",
90 | imports = ["com.haroldadmin.vector.renderState"]
91 | )
92 | )
93 | protected inline fun renderState(
94 | viewModel: VectorViewModel,
95 | crossinline renderer: suspend CoroutineScope.(S) -> Unit
96 | ) {
97 | viewLifecycleOwner.lifecycleScope.launchWhenCreated {
98 | viewModel.state.collect { state ->
99 | renderer(state)
100 | }
101 | }
102 | }
103 |
104 | protected open val logger by lazy { androidLogger(this::class.java.simpleName) }
105 | }
106 |
--------------------------------------------------------------------------------