├── 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 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /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 | ![Vector](images/logo-full-coloured.svg) 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 | [![Release](https://jitpack.io/v/haroldadmin/Vector.svg)](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 |