├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── sample_image_1.jpg
│ │ │ ├── sample_image_2.jpg
│ │ │ └── ic_launcher_background.xml
│ │ ├── 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
│ │ │ ├── attrs.xml
│ │ │ └── styles.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── main_activity.xml
│ │ │ ├── titled_picture_title_above.xml
│ │ │ ├── titled_picture_title_below.xml
│ │ │ ├── counter.xml
│ │ │ └── main_fragment.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── ru
│ │ │ └── impression
│ │ │ └── ui_generator_example
│ │ │ ├── GreetingStructure.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── ToastShower.kt
│ │ │ ├── Ext.kt
│ │ │ ├── TitledPicture.kt
│ │ │ ├── Counter.kt
│ │ │ └── MainFragment.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── jitpack.yml
├── ui-generator-base
├── consumer-rules.pro
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── ru
│ │ └── impression
│ │ └── ui_generator_base
│ │ ├── StateOwner.kt
│ │ ├── ComponentScheme.kt
│ │ ├── SavedViewState.kt
│ │ ├── ClearableCoroutineScope.kt
│ │ ├── ViewLifecycleOwner.kt
│ │ ├── SimpleLifecycle.kt
│ │ ├── Component.kt
│ │ ├── Hooks.kt
│ │ ├── ObservableEntity.kt
│ │ ├── CoroutineViewModel.kt
│ │ ├── Binders.kt
│ │ ├── StateDelegate.kt
│ │ ├── DataBindingManager.kt
│ │ ├── Ext.kt
│ │ └── ComponentViewModel.kt
├── proguard-rules.pro
└── build.gradle.kts
├── ui-generator-annotations
├── consumer-rules.pro
├── .gitignore
├── src
│ └── main
│ │ └── java
│ │ └── ru
│ │ └── impression
│ │ └── ui_generator_annotations
│ │ ├── MakeComponent.kt
│ │ ├── SharedViewModel.kt
│ │ └── Prop.kt
├── build.gradle.kts
└── proguard-rules.pro
├── ui-generator-processor
├── .gitignore
├── src
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ └── services
│ │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider
│ │ └── java
│ │ └── ru
│ │ └── impression
│ │ └── ui_generator_processor
│ │ ├── UIGeneratorProcessorProvider.kt
│ │ ├── Ext.kt
│ │ ├── UIGenerator.kt
│ │ ├── ComponentClassBuilder.kt
│ │ ├── FragmentComponentClassBuilder.kt
│ │ └── ViewComponentClassBuilder.kt
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk11
--------------------------------------------------------------------------------
/ui-generator-base/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui-generator-annotations/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui-generator-base/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ui-generator-annotations/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ui-generator-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/sample_image_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/drawable/sample_image_1.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/sample_image_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/drawable/sample_image_2.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArTemmey/ui-generator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea
5 | .DS_Store
6 | /build
7 | /buildSrc/build
8 | /captures
9 | .externalNativeBuild
10 | .cxx
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UI-generator
3 | %s, %s!
4 |
5 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/ui-generator-annotations/src/main/java/ru/impression/ui_generator_annotations/MakeComponent.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_annotations
2 |
3 | annotation class MakeComponent
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:
--------------------------------------------------------------------------------
1 | ru.impression.ui_generator_processor.UIGeneratorProcessorProvider
--------------------------------------------------------------------------------
/ui-generator-annotations/src/main/java/ru/impression/ui_generator_annotations/SharedViewModel.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_annotations
2 |
3 | annotation class SharedViewModel
--------------------------------------------------------------------------------
/ui-generator-annotations/src/main/java/ru/impression/ui_generator_annotations/Prop.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_annotations
2 |
3 | annotation class Prop(val twoWay: Boolean = false)
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(
2 | // ":app",
3 | ":ui-generator-annotations",
4 | ":ui-generator-processor",
5 | ":ui-generator-base",
6 | )
7 |
8 | rootProject.name = "UI-generator"
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateOwner.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | interface StateOwner {
4 | fun onStateChanged(renderImmediately: Boolean = false)
5 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentScheme.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | abstract class ComponentScheme(
4 | val render: (C.(viewModel: VM) -> Int?)? = null
5 | )
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 07 12:49:46 MSK 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-7.0.2-bin.zip
7 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/SavedViewState.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | class SavedViewState(val superState: Parcelable?, val viewModelState: Parcelable?): Parcelable
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/GreetingStructure.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import kotlinx.serialization.Serializable
4 | import ru.impression.ui_generator_base.ObservableEntity
5 | import ru.impression.ui_generator_base.ObservableEntityImpl
6 |
7 | @Serializable
8 | class GreetingStructure(private var greetingAddressee: String) :
9 | ObservableEntity by ObservableEntityImpl() {
10 |
11 | var greetingAddresseeState by state(greetingAddressee) { greetingAddressee = it }
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/UIGeneratorProcessorProvider.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_processor
2 |
3 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
4 | import com.google.devtools.ksp.processing.SymbolProcessorProvider
5 |
6 | class UIGeneratorProcessorProvider : SymbolProcessorProvider {
7 | override fun create(environment: SymbolProcessorEnvironment) =
8 | UIGenerator(environment.codeGenerator, environment.logger, environment.options["packageName"]!!)
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ClearableCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Job
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | interface ClearableCoroutineScope : CoroutineScope {
8 | fun clear()
9 | }
10 |
11 | class ClearableCoroutineScopeImpl(coroutineContext: CoroutineContext) :
12 | ClearableCoroutineScope {
13 |
14 | override var coroutineContext: CoroutineContext = coroutineContext + Job()
15 |
16 | override fun clear() {
17 | coroutineContext[Job]?.cancel()
18 | ?.also { coroutineContext = coroutineContext.minusKey(Job) + Job() }
19 | }
20 | }
--------------------------------------------------------------------------------
/ui-generator-annotations/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 |
4 | `maven-publish`
5 | }
6 |
7 | group = "com.github.ArtemiyDmtrvch"
8 |
9 | val sourcesJar by tasks.registering(Jar::class) {
10 | classifier = "sources"
11 | from(sourceSets.main.get().allSource)
12 | }
13 |
14 | publishing {
15 | repositories {
16 | maven(url = "https://jitpack.io")
17 | }
18 | publications {
19 | register("mavenJava", MavenPublication::class) {
20 | from(components["java"])
21 | artifact(sourcesJar.get())
22 |
23 | // groupId = "com.github.ArtemiyDmtrvch"
24 | // artifactId = "ui-generator-annotations"
25 | // version = "LOCAL"
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/Ext.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(KspExperimental::class)
2 |
3 | package ru.impression.ui_generator_processor
4 |
5 | import com.google.devtools.ksp.KspExperimental
6 | import com.google.devtools.ksp.getAnnotationsByType
7 | import com.google.devtools.ksp.symbol.KSPropertyDeclaration
8 |
9 | inline fun KSPropertyDeclaration.hasAnnotation() =
10 | (getAnnotationsByType(T::class).count() > 0)
11 |
12 | fun KSPropertyDeclaration.getParentTree(): List =
13 | findOverridee()?.let { listOf(it) + it.getParentTree() }.orEmpty()
14 |
15 | inline fun KSPropertyDeclaration.hasAnnotationInTree(): Boolean =
16 | getParentTree().none { it.hasAnnotation() } && hasAnnotation()
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/ui-generator-base/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 |
--------------------------------------------------------------------------------
/ui-generator-annotations/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 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 |
6 | class MainActivity : AppCompatActivity() {
7 |
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.main_activity)
11 | supportFragmentManager.findFragmentByTag(MainFragmentComponent::class.qualifiedName)
12 | ?: supportFragmentManager.beginTransaction().replace(
13 | R.id.container,
14 | MainFragmentComponent().apply {
15 | welcomeText = "Hello"; greetingStructure = GreetingStructure("world")
16 | },
17 | MainFragmentComponent::class.qualifiedName
18 | ).commit()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/ToastShower.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.view.View
4 | import android.widget.Toast
5 | import ru.impression.ui_generator_annotations.MakeComponent
6 | import ru.impression.ui_generator_annotations.Prop
7 | import ru.impression.ui_generator_annotations.SharedViewModel
8 | import ru.impression.ui_generator_base.ComponentScheme
9 | import ru.impression.ui_generator_base.ComponentViewModel
10 |
11 | @MakeComponent
12 | class ToastShower : ComponentScheme({ viewModel ->
13 | viewModel.toastMessage?.let {
14 | viewModel.toastMessage = null
15 | Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
16 | }
17 | null
18 | })
19 |
20 | @SharedViewModel
21 | class ToastShowerViewModel : ComponentViewModel() {
22 |
23 | var toastMessage by state(null)
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/Ext.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.view.View
4 | import androidx.core.view.isGone
5 | import androidx.core.view.isInvisible
6 | import androidx.core.view.isVisible
7 | import androidx.databinding.BindingAdapter
8 | import ru.impression.ui_generator_base.ComponentViewModel
9 |
10 | fun ComponentViewModel.showToast(toastMessage: String) {
11 | getSharedViewModel().toastMessage = toastMessage
12 | }
13 |
14 | object DataBindingExt {
15 |
16 | @JvmStatic
17 | @BindingAdapter("isInvisible")
18 | fun setIsInvisible(view: View, value: Boolean) {
19 | view.isInvisible = value
20 | }
21 |
22 | @JvmStatic
23 | @BindingAdapter("isVisible")
24 | fun setIsVisible(view: View, value: Boolean) {
25 | view.isVisible = value
26 | }
27 |
28 | @JvmStatic
29 | @BindingAdapter("isGone")
30 | fun setIsGone(view: View, value: Boolean) {
31 | view.isGone = value
32 | }
33 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ViewLifecycleOwner.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import android.view.View
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleOwner
6 |
7 | class ViewLifecycleOwner(private val parent: View) : LifecycleOwner {
8 |
9 | private val lifecycle = SimpleLifecycle(this)
10 |
11 | private val onAttachStateChangeListener = object : View.OnAttachStateChangeListener {
12 | override fun onViewAttachedToWindow(v: View?) {
13 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
14 | }
15 |
16 | override fun onViewDetachedFromWindow(v: View?) {
17 | lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
18 | }
19 | }
20 |
21 | init {
22 | parent.addOnAttachStateChangeListener(onAttachStateChangeListener)
23 | }
24 |
25 | override fun getLifecycle() = lifecycle
26 |
27 | fun destroy() {
28 | parent.removeOnAttachStateChangeListener(onAttachStateChangeListener)
29 | }
30 | }
--------------------------------------------------------------------------------
/ui-generator-processor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | `maven-publish`
4 | }
5 |
6 | group = "com.github.ArtemiyDmtrvch"
7 |
8 | dependencies {
9 | implementation(project(":ui-generator-annotations"))
10 | implementation("com.squareup:kotlinpoet:$kotlinpoet_version")
11 | implementation("com.squareup:kotlinpoet-ksp:$kotlinpoet_version")
12 | implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
13 | implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
14 | implementation("com.google.devtools.ksp:symbol-processing-api:$ksp_version")
15 | }
16 |
17 | val sourcesJar by tasks.registering(Jar::class) {
18 | classifier = "sources"
19 | from(sourceSets.main.get().allSource)
20 | }
21 |
22 | publishing {
23 | repositories {
24 | maven(url = "https://jitpack.io")
25 | }
26 | publications {
27 | register("mavenJava", MavenPublication::class) {
28 | from(components["java"])
29 | artifact(sourcesJar.get())
30 |
31 | // groupId = "com.github.ArtemiyDmtrvch"
32 | // artifactId = "ui-generator-processor"
33 | // version = "LOCAL"
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/titled_picture_title_above.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
17 |
18 |
23 |
24 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/titled_picture_title_below.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
17 |
18 |
24 |
25 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/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 |
23 | kapt.use.worker.api=true
--------------------------------------------------------------------------------
/app/src/main/res/layout/counter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
16 |
17 |
22 |
23 |
29 |
30 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/SimpleLifecycle.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleEventObserver
5 | import androidx.lifecycle.LifecycleObserver
6 | import androidx.lifecycle.LifecycleOwner
7 | import java.util.concurrent.ConcurrentSkipListSet
8 | import java.util.concurrent.CopyOnWriteArraySet
9 |
10 | class SimpleLifecycle(private val owner: LifecycleOwner) : Lifecycle() {
11 |
12 | @Volatile
13 | private var state = State.INITIALIZED
14 |
15 | private val observers = CopyOnWriteArraySet()
16 |
17 | override fun addObserver(observer: LifecycleObserver) {
18 | if (observer !is LifecycleEventObserver) return
19 | observers.add(observer)
20 | if (state > State.INITIALIZED)
21 | repeat(state.ordinal - 1) {
22 | observer.onStateChanged(owner, Event.upTo(State.values()[it + 1]) ?: return)
23 | }
24 | }
25 |
26 | override fun removeObserver(observer: LifecycleObserver) {
27 | observers.remove(observer as? LifecycleEventObserver ?: return)
28 | }
29 |
30 | override fun getCurrentState() = state
31 |
32 | fun handleLifecycleEvent(event: Event) {
33 | state = event.targetState
34 | observers.forEach { it.onStateChanged(owner, event) }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/TitledPicture.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.graphics.drawable.Drawable
4 | import android.widget.FrameLayout
5 | import ru.impression.ui_generator_annotations.MakeComponent
6 | import ru.impression.ui_generator_base.ComponentScheme
7 | import ru.impression.ui_generator_base.ComponentViewModel
8 |
9 | @MakeComponent
10 | class TitledPicture : ComponentScheme({ viewModel ->
11 | when (viewModel.titlePosition) {
12 | TitlePosition.BELOW_PICTURE -> R.layout.titled_picture_title_below
13 | TitlePosition.ABOVE_PICTURE -> R.layout.titled_picture_title_above
14 | }
15 | }) {
16 |
17 | enum class TitlePosition {
18 | BELOW_PICTURE,
19 | ABOVE_PICTURE,
20 | }
21 | }
22 |
23 | class TitledPictureViewModel : ComponentViewModel(attrs = R.styleable.TextAndImageBlockComponent) {
24 |
25 | var title by state(null, attr = R.styleable.TextAndImageBlockComponent_title)
26 |
27 | var titlePosition by state(
28 | TitledPicture.TitlePosition.BELOW_PICTURE,
29 | attr = R.styleable.TextAndImageBlockComponent_titlePosition
30 | )
31 |
32 | var picture by state(null, attr = R.styleable.TextAndImageBlockComponent_picture)
33 |
34 | fun showInfoToast() {
35 | showToast("Clicked on $title!")
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/Counter.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.util.Log
4 | import android.widget.FrameLayout
5 | import ru.impression.ui_generator_annotations.MakeComponent
6 | import ru.impression.ui_generator_annotations.Prop
7 | import ru.impression.ui_generator_base.ComponentScheme
8 | import ru.impression.ui_generator_base.ComponentViewModel
9 | import ru.impression.ui_generator_base.onInit
10 | import ru.impression.ui_generator_base.withLifecycle
11 | import kotlin.reflect.KClass
12 |
13 | @MakeComponent
14 | class Counter : ComponentScheme({
15 | onInit {
16 | Log.v(Counter::class.simpleName, "onInit")
17 | }
18 | withLifecycle {
19 | onCreate {
20 | Log.v(Counter::class.simpleName, "onCreate")
21 | }
22 | onDestroy {
23 | Log.v(Counter::class.simpleName, "onDestroy")
24 | }
25 | }
26 | R.layout.counter
27 | })
28 |
29 | open class CounterViewModel : ComponentViewModel() {
30 |
31 | @Prop(twoWay = true)
32 | open var count by state(0)
33 |
34 | // For checking generation
35 | @Prop
36 | var clazz by state?>(null)
37 |
38 | fun increment() {
39 | count++
40 | }
41 |
42 | fun decrement() {
43 | count--
44 | }
45 | }
46 |
47 | // Check override properties
48 | class CounterViewModelOverride: CounterViewModel() {
49 |
50 | @Prop(twoWay = true)
51 | override var count by state(2)
52 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.google.devtools.ksp") version ksp_version
3 | id("com.android.application")
4 |
5 | kotlin("plugin.serialization")
6 | kotlin("android")
7 | kotlin("kapt")
8 | }
9 |
10 | android {
11 | compileSdk = 31
12 | buildToolsVersion = "31.0.0"
13 |
14 | defaultConfig {
15 | applicationId = "ru.impression.ui_generator_example"
16 | minSdk = 17
17 | targetSdk = 31
18 | versionCode = 1
19 | versionName = "1.0"
20 |
21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | isMinifyEnabled = false
27 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
28 | }
29 | }
30 |
31 | dataBinding {
32 | addKtx = true
33 | isEnabled = true
34 | }
35 | kotlinOptions {
36 | jvmTarget = java_version.toString()
37 | }
38 | }
39 |
40 | java {
41 | sourceCompatibility = java_version
42 | targetCompatibility = java_version
43 | }
44 |
45 | kotlin {
46 | sourceSets.main {
47 | kotlin.srcDir("build/generated/ksp/debug/kotlin")
48 | }
49 | }
50 |
51 | ksp {
52 | arg("packageName","ru.impression.ui_generator_example")
53 | }
54 |
55 | dependencies {
56 | implementation(project(":ui-generator-base"))
57 | implementation(project(":ui-generator-annotations"))
58 | ksp(project(":ui-generator-processor"))
59 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version")
60 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
61 | implementation("androidx.appcompat:appcompat:1.4.0")
62 | implementation("androidx.core:core-ktx:1.7.0")
63 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
64 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
65 | }
66 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Component.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import android.view.View
4 | import androidx.databinding.ViewDataBinding
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.*
7 | import ru.impression.ui_generator_annotations.SharedViewModel
8 | import kotlin.reflect.KClass
9 | import kotlin.reflect.full.createInstance
10 | import kotlin.reflect.full.findAnnotation
11 |
12 | interface Component {
13 |
14 | val scheme: ComponentScheme
15 |
16 | val viewModel: VM
17 |
18 | val container: View?
19 |
20 | val boundLifecycleOwner: LifecycleOwner
21 |
22 | val dataBindingManager: DataBindingManager
23 |
24 | val hooks: Hooks
25 |
26 | fun createViewModel(viewModelClass: KClass, isSharedViewModel: Boolean): T {
27 | val activity = when (this) {
28 | is View -> activity
29 | is Fragment -> activity
30 | else -> null
31 | }
32 | return when {
33 | activity != null && isSharedViewModel ->
34 | ViewModelProvider(activity)[viewModelClass.java]
35 | activity != null && this is ViewModelStoreOwner ->
36 | ViewModelProvider(this)[viewModelClass.java]
37 | else -> viewModelClass.createInstance()
38 | }
39 | }
40 |
41 | fun onTwoWayPropChanged(propertyName: String) = Unit
42 |
43 | fun render(
44 | attachToContainer: Boolean = true,
45 | rebindViewModel: Boolean = true,
46 | executeBindingsImmediately: Boolean = true,
47 | ): ViewDataBinding? {
48 | viewModel.componentHasMissedStateChange = false
49 | viewModel.beforeRender()
50 | return dataBindingManager.updateBinding(
51 | scheme.render?.invoke(this as C, viewModel),
52 | attachToContainer,
53 | rebindViewModel,
54 | executeBindingsImmediately
55 | )
56 | }
57 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Hooks.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | class Hooks {
4 |
5 | private val initBlocks = ArrayList<() -> Unit>()
6 |
7 | private var initBlocksCalled = false
8 |
9 | fun addInitBlock(block: () -> Unit) {
10 | if (initBlocksCalled) return
11 | initBlocks.add(block)
12 | }
13 |
14 | fun callInitBlocks() {
15 | initBlocksCalled = true
16 | initBlocks.forEach { it() }
17 | initBlocks.clear()
18 | }
19 | }
20 |
21 | class LifecycleScope {
22 |
23 | private val onCreateBlocks = ArrayList<() -> Unit>()
24 | private val onStartBlocks = ArrayList<() -> Unit>()
25 | private val onResumeBlocks = ArrayList<() -> Unit>()
26 | private val onPauseBlocks = ArrayList<() -> Unit>()
27 | private val onStopBlocks = ArrayList<() -> Unit>()
28 | private val onDestroyBlocks = ArrayList<() -> Unit>()
29 |
30 | fun onCreate(block: () -> Unit) {
31 | onCreateBlocks.add(block)
32 | }
33 |
34 | internal fun callOnCreateBlocks() {
35 | onCreateBlocks.forEach { it() }
36 | }
37 |
38 | fun onStart(block: () -> Unit) {
39 | onStartBlocks.add(block)
40 | }
41 |
42 | internal fun callOnStartBlocks() {
43 | onStartBlocks.forEach { it() }
44 | }
45 |
46 | fun onResume(block: () -> Unit) {
47 | onResumeBlocks.add(block)
48 | }
49 |
50 | internal fun callOnResumeBlocks() {
51 | onResumeBlocks.forEach { it() }
52 | }
53 |
54 | fun onPause(block: () -> Unit) {
55 | onPauseBlocks.add(block)
56 | }
57 |
58 | internal fun callOnPauseBlocks() {
59 | onPauseBlocks.forEach { it() }
60 | }
61 |
62 | fun onStop(block: () -> Unit) {
63 | onStopBlocks.add(block)
64 | }
65 |
66 | internal fun callOnStopBlocks() {
67 | onStopBlocks.forEach { it() }
68 | }
69 |
70 | fun onDestroy(block: () -> Unit) {
71 | onDestroyBlocks.add(block)
72 | }
73 |
74 | internal fun callOnDestroyBlocks() {
75 | onDestroyBlocks.forEach { it() }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/ru/impression/ui_generator_example/MainFragment.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_example
2 |
3 | import android.util.Log
4 | import androidx.fragment.app.Fragment
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.coroutines.launch
9 | import ru.impression.ui_generator_annotations.MakeComponent
10 | import ru.impression.ui_generator_annotations.Prop
11 | import ru.impression.ui_generator_base.*
12 | import kotlin.random.Random
13 | import kotlin.random.nextInt
14 |
15 | @MakeComponent
16 | class MainFragment :
17 | ComponentScheme({
18 | onInit {
19 | Log.v(MainFragment::class.simpleName, "onInit")
20 | }
21 | withLifecycle {
22 | onCreate {
23 | Log.v(MainFragment::class.simpleName, "onCreate")
24 | }
25 | onStart {
26 | Log.v(MainFragment::class.simpleName, "onStart")
27 | }
28 | onResume {
29 | Log.v(MainFragment::class.simpleName, "onResume")
30 | }
31 | onDestroy {
32 | Log.v(MainFragment::class.simpleName, "onDestroy")
33 | }
34 | }
35 | R.layout.main_fragment
36 | })
37 |
38 | class MainFragmentViewModel : CoroutineViewModel() {
39 |
40 | var countDown by state(flow {
41 | delay(1000)
42 | emit(3)
43 | delay(1000)
44 | emit(2)
45 | delay(1000)
46 | emit(1)
47 | delay(1000)
48 | emit(0)
49 | })
50 |
51 | val countDownIsLoading get() = ::countDown.isLoading
52 |
53 |
54 | @Prop
55 | var welcomeText by state(null)
56 |
57 | @Prop
58 | var greetingStructure by state(null)
59 |
60 | fun changeGreetingAddressee() {
61 | greetingStructure?.greetingAddresseeState =
62 | arrayOf("world", "my friend", "universe").random()
63 | }
64 |
65 |
66 | var count by state(0) { showToast("Count is $it!") }
67 |
68 |
69 | var currentTime by state({
70 | delay(2000)
71 | System.currentTimeMillis().toString()
72 | })
73 |
74 | val currentTimeIsLoading get() = ::currentTime.isLoading
75 |
76 | fun reloadCurrentTime() {
77 | ::currentTime.reload()
78 | }
79 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ObservableEntity.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import ru.impression.singleton_entity.SingletonEntity
4 | import ru.impression.singleton_entity.SingletonEntityDelegate
5 | import ru.impression.singleton_entity.SingletonEntityParent
6 | import java.util.concurrent.CopyOnWriteArrayList
7 | import kotlin.properties.PropertyDelegateProvider
8 | import kotlin.reflect.KProperty
9 |
10 |
11 | interface ObservableEntity : StateOwner {
12 |
13 | fun state(
14 | initialValue: T,
15 | onChanged: ((T) -> Unit)? = null
16 | ): PropertyDelegateProvider>
17 |
18 | fun addStateOwner(stateOwner: StateOwner)
19 |
20 | fun removeStateOwner(stateOwner: StateOwner)
21 | }
22 |
23 | class ObservableEntityImpl : ObservableEntity {
24 | private val stateOwners = CopyOnWriteArrayList()
25 | private val delegates = ArrayList>()
26 | private val singletonEntityParent: SingletonEntityParent = object : SingletonEntityParent {
27 | override fun replace(oldEntity: SingletonEntity, newEntity: SingletonEntity) {
28 | delegates.forEach {
29 | if (it.value === oldEntity)
30 | (it as StateDelegate<*, SingletonEntity>).setValue(newEntity)
31 | }
32 | }
33 | }
34 |
35 | override fun state(initialValue: T, onChanged: ((T) -> Unit)?) =
36 | PropertyDelegateProvider> { _, property ->
37 | StateDelegate(
38 | parent = this@ObservableEntityImpl,
39 | singletonEntityParent = singletonEntityParent,
40 | initialValue = initialValue,
41 | onChanged = onChanged,
42 | property = property
43 | )
44 | .also { delegates.add(it) } as StateDelegate
45 | }
46 |
47 | override fun addStateOwner(stateOwner: StateOwner) {
48 |
49 | stateOwners.add(stateOwner)
50 | }
51 |
52 | override fun removeStateOwner(stateOwner: StateOwner) {
53 | stateOwners.remove(stateOwner)
54 | }
55 |
56 | override fun onStateChanged(renderImmediately: Boolean) {
57 | stateOwners.forEach { it?.onStateChanged(renderImmediately) }
58 | }
59 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/CoroutineViewModel.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import androidx.annotation.CallSuper
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlin.properties.PropertyDelegateProvider
8 |
9 | abstract class CoroutineViewModel(attrs: IntArray? = null) : ComponentViewModel(attrs),
10 | ClearableCoroutineScope by ClearableCoroutineScopeImpl(Dispatchers.IO) {
11 |
12 | protected fun state(loadValue: suspend () -> T, onChanged: ((T?) -> Unit)? = null) =
13 | PropertyDelegateProvider> { _, property ->
14 | StateDelegate(
15 | parent = this,
16 | singletonEntityParent = singletonEntityParent,
17 | initialValue = null,
18 | onChanged = onChanged,
19 | loadValue = loadValue,
20 | property = property
21 | )
22 | .also { delegates.add(it) }
23 | }
24 |
25 |
26 | protected fun state(flow: Flow, onChanged: ((T?) -> Unit)? = null) =
27 | PropertyDelegateProvider> { _, property ->
28 | StateDelegate(
29 | parent = this,
30 | singletonEntityParent = singletonEntityParent,
31 | initialValue = null,
32 | onChanged = onChanged,
33 | valueFlow = flow,
34 | property = property
35 | )
36 | .also { delegates.add(it) }
37 | }
38 |
39 |
40 | protected fun state(stateFlow: StateFlow, onChanged: ((T) -> Unit)? = null) =
41 | PropertyDelegateProvider> { _, property ->
42 | StateDelegate(
43 | parent = this,
44 | singletonEntityParent = singletonEntityParent,
45 | initialValue = stateFlow.value,
46 | onChanged = onChanged,
47 | valueFlow = stateFlow,
48 | property = property
49 | ).also { delegates.add(it) }
50 | }
51 |
52 |
53 | @CallSuper
54 | override fun onCleared() {
55 | clear()
56 | super.onCleared()
57 | }
58 | }
--------------------------------------------------------------------------------
/ui-generator-base/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("kotlin-parcelize")
4 |
5 | kotlin("plugin.serialization")
6 | kotlin("android")
7 |
8 | `maven-publish`
9 | }
10 |
11 | group = "com.github.ArtemiyDmtrvch"
12 |
13 | android {
14 | compileSdk = 31
15 | buildToolsVersion = "31.0.0"
16 |
17 | defaultConfig {
18 | minSdk = 17
19 | targetSdk = 31
20 |
21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | isMinifyEnabled = false
27 | proguardFiles(
28 | getDefaultProguardFile("proguard-android-optimize.txt"),
29 | "proguard-rules.pro"
30 | )
31 | }
32 | }
33 | dataBinding {
34 | addKtx = true
35 | isEnabled = true
36 | }
37 | kotlinOptions {
38 | jvmTarget = java_version.toString()
39 | }
40 | }
41 |
42 | java {
43 | sourceCompatibility = java_version
44 | targetCompatibility = java_version
45 | }
46 |
47 | val sourcesJar by tasks.creating(Jar::class) {
48 | archiveClassifier.set("sources")
49 | from(android.sourceSets.getByName("main").java.srcDirs)
50 | }
51 |
52 | afterEvaluate {
53 | publishing {
54 | repositories {
55 | maven(url = "https://jitpack.io")
56 | }
57 | publications {
58 | create("pub") {
59 | // Applies the component for the release build variant.
60 | from(components["release"])
61 | artifact(sourcesJar)
62 |
63 | // groupId = "com.github.ArtemiyDmtrvch"
64 | // artifactId = "ui-generator-base"
65 | // version = "LOCAL"
66 | }
67 | }
68 | }
69 | }
70 |
71 | dependencies {
72 | implementation(project(":ui-generator-annotations"))
73 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
74 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version")
75 | implementation("androidx.appcompat:appcompat:1.4.0")
76 | implementation("androidx.core:core-ktx:1.7.0")
77 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
78 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
79 | implementation("com.github.ArtemiyDmtrvch:kotlin-delegate-concatenator:cf5890d227")
80 | implementation("com.github.ArTemmey:singleton-entity:d990f02925")
81 | api("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
82 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Binders.kt:
--------------------------------------------------------------------------------
1 | @file:SuppressLint("RestrictedApi")
2 |
3 | package ru.impression.ui_generator_base
4 |
5 | import android.annotation.SuppressLint
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
9 | import android.widget.TextView
10 | import androidx.core.view.marginBottom
11 | import androidx.core.view.marginEnd
12 | import androidx.core.view.marginStart
13 | import androidx.core.view.marginTop
14 | import androidx.databinding.adapters.TextViewBindingAdapter
15 |
16 | fun TextView.bindText(text: CharSequence?) = TextViewBindingAdapter.setText(this, text)
17 |
18 | fun View.dp(value: Int) = value * context.resources.displayMetrics.density
19 |
20 | fun View.updateLayoutParams(
21 | width: Int? = null,
22 | height: Int? = null,
23 | marginStart: Int? = null,
24 | marginTop: Int? = null,
25 | marginEnd: Int? = null,
26 | marginBottom: Int? = null
27 | ) {
28 | layoutParams?.let {
29 | if ((width == null || width == it.width)
30 | && (height == null || height == it.height)
31 | && (marginStart == null || marginStart == this.marginStart)
32 | && (marginTop == null || marginTop == this.marginTop)
33 | && (marginEnd == null || marginEnd == this.marginEnd)
34 | && (marginBottom == null || marginBottom == this.marginBottom)
35 | ) return
36 | }
37 | layoutParams =
38 | if (marginStart != null || marginTop != null || marginEnd != null || marginBottom != null)
39 | (layoutParams as? ViewGroup.MarginLayoutParams
40 | ?: ViewGroup.MarginLayoutParams(width ?: WRAP_CONTENT, height ?: WRAP_CONTENT))
41 | .apply {
42 | marginStart?.let { leftMargin = it }
43 | marginTop?.let { topMargin = it }
44 | marginEnd?.let { rightMargin = it }
45 | marginBottom?.let { bottomMargin = it }
46 | }
47 | else
48 | layoutParams ?: ViewGroup.LayoutParams(width ?: WRAP_CONTENT, height ?: WRAP_CONTENT)
49 | }
50 |
51 | fun View.updateTag(
52 | key: Int,
53 | existenceCondition: Boolean = true,
54 | create: () -> T,
55 | update: ((T) -> Unit)? = null,
56 | onRemoved: ((T) -> Unit)? = null
57 | ) {
58 | val oldTag = getTag(key) as T?
59 | if (oldTag == null && existenceCondition) {
60 | setTag(key, create().also { update?.invoke(it) })
61 | } else if (oldTag != null) {
62 | if (!existenceCondition) {
63 | setTag(key, null)
64 | onRemoved?.invoke(oldTag)
65 | } else {
66 | update?.invoke(oldTag)
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/StateDelegate.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Job
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.collect
7 | import kotlinx.coroutines.launch
8 | import ru.impression.singleton_entity.SingletonEntity
9 | import ru.impression.singleton_entity.SingletonEntityParent
10 | import ru.impression.ui_generator_annotations.Prop
11 | import kotlin.properties.ReadWriteProperty
12 | import kotlin.reflect.KProperty
13 | import kotlin.reflect.full.findAnnotation
14 |
15 | class StateDelegate(
16 | val parent: R,
17 | private val singletonEntityParent: SingletonEntityParent,
18 | initialValue: T,
19 | private val onChanged: ((T) -> Unit)?,
20 | val loadValue: (suspend () -> T)? = null,
21 | val valueFlow: Flow? = null,
22 | val property: KProperty<*>
23 | ) : ReadWriteProperty {
24 |
25 | @Volatile
26 | var value = initialValue
27 | private set
28 |
29 | @Volatile
30 | var isLoading = false
31 | private set
32 |
33 | @Volatile
34 | private var loadJob: Job? = null
35 |
36 | init {
37 | observeValue()
38 | when {
39 | loadValue != null -> load(false)
40 | valueFlow != null -> collectValueFlow()
41 | }
42 | }
43 |
44 | @Synchronized
45 | fun load(notifyStateChangedBeforeLoading: Boolean): Job {
46 | loadJob?.cancel()
47 | isLoading = true
48 | if (notifyStateChangedBeforeLoading) parent.onStateChanged()
49 | return (parent as CoroutineScope).launch {
50 | val result = loadValue!!.invoke()
51 | isLoading = false
52 | setValue(result)
53 | loadJob = null
54 | }.also { loadJob = it }
55 | }
56 |
57 | private fun collectValueFlow() {
58 | fun dpCollect() = (parent as CoroutineScope).launch {
59 | valueFlow!!.collect {
60 | if (it === value) return@collect
61 | setValue(it)
62 | }
63 | }
64 | (parent as? ComponentViewModel)?.initSubscriptions(::dpCollect) ?: dpCollect()
65 | }
66 |
67 | override fun getValue(thisRef: R, property: KProperty<*>) = value
68 |
69 | @Synchronized
70 | override fun setValue(thisRef: R, property: KProperty<*>, value: T) {
71 | setValue(value)
72 | }
73 |
74 | @Synchronized
75 | fun setValue(
76 | value: T
77 | ) {
78 | stopObserveValue()
79 | this.value = value
80 | observeValue()
81 | parent.onStateChanged()
82 | if (property.findAnnotation()?.twoWay == true)
83 | (parent as? ComponentViewModel)?.notifyTwoWayPropChanged(property.name)
84 | onChanged?.invoke(value)
85 | }
86 |
87 | fun observeValue() {
88 | (this.value as? ObservableEntity)?.addStateOwner(parent)
89 | (this.value as? SingletonEntity)?.addParent(singletonEntityParent)
90 | }
91 |
92 | fun stopObserveValue() {
93 | (this.value as? ObservableEntity)?.removeStateOwner(parent)
94 | (this.value as? SingletonEntity)?.removeParent(singletonEntityParent)
95 | }
96 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/DataBindingManager.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.databinding.DataBindingUtil
9 | import androidx.databinding.ViewDataBinding
10 | import androidx.fragment.app.Fragment
11 |
12 | class DataBindingManager(private val component: Component<*, *>, private val viewModelVariableId: Int) {
13 |
14 | private val context by lazy {
15 | (component as? Fragment)?.context ?: (component as? View)?.context
16 | }
17 |
18 | var currentBinding: ViewDataBinding? = null
19 |
20 | private var currentLayoutResId: Int? = null
21 |
22 | private val bindingInitHandler = Handler(Looper.getMainLooper())
23 |
24 | internal fun updateBinding(
25 | newLayoutResId: Int?,
26 | attachToContainer: Boolean,
27 | rebindViewModel: Boolean = true,
28 | executeBindingsImmediately: Boolean
29 | ): ViewDataBinding? {
30 | currentBinding?.let {
31 | if (newLayoutResId != null && newLayoutResId == currentLayoutResId) {
32 | if (rebindViewModel) {
33 | it.setVariable(viewModelVariableId, component.viewModel)
34 | if (executeBindingsImmediately) it.executePendingBindings()
35 | }
36 | return it
37 | }
38 | (component.container as? ViewGroup)?.removeAllViews()
39 | }
40 | newLayoutResId?.let { inflateBinding(it, attachToContainer) }
41 | ?.apply { prepare(executeBindingsImmediately) }
42 | ?.also {
43 | currentBinding = it
44 | currentLayoutResId = newLayoutResId
45 | onBindingCreated(attachToContainer, executeBindingsImmediately)
46 | }
47 | return currentBinding
48 | }
49 |
50 |
51 | private fun inflateBinding(layoutResId: Int, attachToContainer: Boolean): ViewDataBinding? {
52 | return DataBindingUtil.inflate(
53 | LayoutInflater.from(context ?: return null),
54 | layoutResId,
55 | component.container as? ViewGroup,
56 | attachToContainer
57 | )
58 | }
59 |
60 | private fun ViewDataBinding.prepare(executeBindings: Boolean) {
61 | this.lifecycleOwner = component.boundLifecycleOwner
62 | setVariable(viewModelVariableId, component.viewModel)
63 | if (executeBindings) executePendingBindings()
64 | }
65 |
66 | private fun onBindingCreated(
67 | attachedToContainer: Boolean,
68 | executeBindingsImmediately: Boolean
69 | ) {
70 | if (attachedToContainer)
71 | component.render(
72 | rebindViewModel = false,
73 | executeBindingsImmediately = executeBindingsImmediately
74 | )
75 | else
76 | bindingInitHandler.post {
77 | component.render(
78 | rebindViewModel = false,
79 | executeBindingsImmediately = executeBindingsImmediately
80 | )
81 | }
82 | }
83 |
84 | fun releaseBinding() {
85 | currentBinding = null
86 | currentLayoutResId = null
87 | bindingInitHandler.removeCallbacksAndMessages(null)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
16 |
17 |
22 |
23 |
32 |
33 |
39 |
40 |
47 |
48 |
53 |
54 |
59 |
60 |
67 |
68 |
69 |
70 |
75 |
76 |
82 |
83 |
87 |
88 |
94 |
95 |
100 |
101 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/UIGenerator.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_processor
2 |
3 | import com.google.devtools.ksp.*
4 | import com.google.devtools.ksp.processing.*
5 | import com.google.devtools.ksp.symbol.KSAnnotated
6 | import com.google.devtools.ksp.symbol.KSClassDeclaration
7 | import com.google.devtools.ksp.symbol.KSVisitorVoid
8 | import com.squareup.kotlinpoet.FileSpec
9 | import com.squareup.kotlinpoet.TypeSpec
10 | import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
11 | import com.squareup.kotlinpoet.ksp.toClassName
12 | import com.squareup.kotlinpoet.ksp.toTypeName
13 | import com.squareup.kotlinpoet.ksp.toTypeParameterResolver
14 | import java.lang.StringBuilder
15 |
16 | @OptIn(KotlinPoetKspPreview::class)
17 | class UIGenerator(
18 | val codeGenerator: CodeGenerator,
19 | val logger: KSPLogger,
20 | val packageName: String
21 | ) : SymbolProcessor {
22 |
23 | override fun process(resolver: Resolver): List {
24 | val symbols =
25 | resolver.getSymbolsWithAnnotation("ru.impression.ui_generator_annotations.MakeComponent")
26 | val ret = symbols.filter { !it.validate() }.toList()
27 | symbols
28 | .filter { it is KSClassDeclaration && it.validate() }
29 | .forEach { it.accept(BuilderVisitor(), Unit) }
30 |
31 | return ret
32 | }
33 |
34 | inner class BuilderVisitor : KSVisitorVoid() {
35 |
36 |
37 | override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
38 | classDeclaration.primaryConstructor!!.accept(this, data)
39 |
40 | val resultClassName = "${classDeclaration.simpleName.asString()}Component"
41 | val resultClassPackageName = classDeclaration.containingFile!!.packageName.asString()
42 | val file = codeGenerator.createNewFile(
43 | Dependencies(true, classDeclaration.containingFile!!),
44 | resultClassPackageName,
45 | resultClassName
46 | )
47 | val typeArguments = classDeclaration.superTypes.first().element
48 | ?.typeArguments
49 | ?: return logger.error("Cannot find type arguments")
50 | val superClass = typeArguments.first()
51 | val viewModelClass = typeArguments[1].type!!.resolve().declaration as KSClassDeclaration
52 | var resultClass: TypeSpec? = null
53 | var downwardClass = superClass.type?.resolve()
54 |
55 | classIteration@ while (downwardClass != null) {
56 | when (downwardClass.toClassName().canonicalName) {
57 | "android.view.View" -> {
58 | resultClass = ViewComponentClassBuilder(
59 | logger,
60 | classDeclaration,
61 | resultClassName,
62 | resultClassPackageName,
63 | with(superClass.type!!.resolve()) {
64 | toTypeName(declaration.typeParameters.toTypeParameterResolver())
65 | },
66 | viewModelClass,
67 | packageName
68 | ).build()
69 | break@classIteration
70 | }
71 | "androidx.fragment.app.Fragment" -> {
72 | resultClass = FragmentComponentClassBuilder(
73 | logger,
74 | classDeclaration,
75 | resultClassName,
76 | resultClassPackageName,
77 | with(superClass.type!!.resolve()) {
78 | toTypeName(declaration.typeParameters.toTypeParameterResolver())
79 | },
80 | viewModelClass,
81 | packageName
82 | ).build()
83 | break@classIteration
84 | }
85 |
86 | else -> downwardClass =
87 | (downwardClass.declaration as KSClassDeclaration).superTypes.firstOrNull()
88 | ?.resolve()
89 | }
90 | }
91 |
92 | resultClass ?: return logger.error(
93 | "Illegal type of superclass for ${classDeclaration.toClassName().canonicalName}. Superclass must be either " +
94 | "out android.view.View or out " +
95 | "androidx.fragment.app.Fragment"
96 | )
97 |
98 | val resultContent = FileSpec.builder(resultClassPackageName, resultClassName)
99 | .addType(resultClass)
100 | .build()
101 |
102 | val resultText = StringBuilder().apply { resultContent.writeTo(this) }.toString()
103 |
104 | file.write(resultText.encodeToByteArray())
105 | file.close()
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/ComponentClassBuilder.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_processor
2 |
3 | import com.google.devtools.ksp.*
4 | import com.google.devtools.ksp.processing.KSPLogger
5 | import com.google.devtools.ksp.symbol.KSClassDeclaration
6 | import com.google.devtools.ksp.symbol.KSTypeReference
7 | import com.squareup.kotlinpoet.*
8 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
9 | import com.squareup.kotlinpoet.ksp.*
10 | import ru.impression.ui_generator_annotations.Prop
11 | import java.util.*
12 | import kotlin.collections.ArrayList
13 |
14 | @OptIn(KotlinPoetKspPreview::class)
15 | abstract class ComponentClassBuilder(
16 | protected val logger: KSPLogger,
17 | protected val scheme: KSClassDeclaration,
18 | protected val resultClassName: String,
19 | protected val resultClassPackage: String,
20 | protected val superclass: TypeName,
21 | protected val viewModelClass: KSClassDeclaration,
22 | protected val packageName: String
23 | ) {
24 |
25 | @OptIn(KspExperimental::class)
26 | protected val propProperties = ArrayList().apply {
27 | var downwardViewModelClass: KSClassDeclaration? = viewModelClass
28 |
29 | while (downwardViewModelClass?.qualifiedName?.asString() != "ru.impression.ui_generator_base.ComponentViewModel"
30 | && downwardViewModelClass?.qualifiedName?.asString() != "ru.impression.ui_generator_base.CoroutineViewModel"
31 | ) {
32 | val properties = downwardViewModelClass?.getAllProperties()
33 | ?.filter { it.hasAnnotationInTree() }
34 | ?: return@apply
35 |
36 | properties.forEach { viewModelElement ->
37 | logger.info("Expecting ${downwardViewModelClass?.qualifiedName?.asString()}")
38 |
39 | viewModelElement.getAnnotationsByType(Prop::class).firstOrNull()
40 | ?.let { annotation ->
41 | logger.info("Expecting ${viewModelElement.simpleName.asString()}")
42 |
43 | val propertyGetter = viewModelElement.getter
44 | val propertyName = propertyGetter.toString().substringBefore(".")
45 | val capitalizedPropertyName = propertyName
46 | .substring(0, 1)
47 | .uppercase(Locale.getDefault()) + propertyName.substring(1)
48 |
49 | add(
50 | PropProperty(
51 | propertyName,
52 | capitalizedPropertyName,
53 | propertyGetter!!.returnType!!,
54 | annotation.twoWay,
55 | "${propertyName}AttrChanged"
56 | )
57 | )
58 | }
59 | }
60 |
61 | downwardViewModelClass = downwardViewModelClass
62 | .getAllSuperTypes()
63 | .firstOrNull()
64 | ?.declaration as? KSClassDeclaration
65 | }
66 | }
67 |
68 | fun build() = with(TypeSpec.classBuilder(resultClassName)) {
69 | superclass(superclass)
70 | addSuperinterface(
71 |
72 | ClassName("ru.impression.ui_generator_base", "Component")
73 | .parameterizedBy(superclass, viewModelClass.toClassName())
74 | )
75 | addProperty(buildSchemeProperty())
76 | addProperty(buildViewModelProperty())
77 | addProperty(buildContainerProperty())
78 | addProperty(buildBoundLifecycleOwnerProperty())
79 | addProperty(buildDataBindingManagerProperty())
80 | addProperty(buildHooksProperty())
81 | addRestMembers()
82 | build()
83 | }
84 |
85 | private fun buildSchemeProperty() =
86 | with(PropertySpec.builder("scheme", scheme.toClassName())) {
87 | addModifiers(KModifier.OVERRIDE)
88 | initializer("%T()", scheme.toClassName())
89 | build()
90 | }
91 |
92 | protected abstract fun buildViewModelProperty(): PropertySpec
93 |
94 | protected abstract fun buildContainerProperty(): PropertySpec
95 |
96 | protected abstract fun buildBoundLifecycleOwnerProperty(): PropertySpec
97 |
98 | private fun buildDataBindingManagerProperty() = with(
99 | PropertySpec.builder(
100 | "dataBindingManager",
101 | ClassName("ru.impression.ui_generator_base", "DataBindingManager")
102 | )
103 | ) {
104 | addModifiers(KModifier.OVERRIDE)
105 | initializer("DataBindingManager(this, ${packageName}.BR.viewModel)")
106 | build()
107 | }
108 |
109 | private fun buildHooksProperty() = with(
110 | PropertySpec.builder(
111 | "hooks",
112 | ClassName("ru.impression.ui_generator_base", "Hooks")
113 | )
114 | ) {
115 | addModifiers(KModifier.OVERRIDE)
116 | initializer("Hooks()")
117 | build()
118 | }
119 |
120 | abstract fun TypeSpec.Builder.addRestMembers()
121 |
122 | @OptIn(KotlinPoetKspPreview::class)
123 | protected class PropProperty(
124 | val name: String,
125 | val capitalizedName: String,
126 | val type: KSTypeReference,
127 | val twoWay: Boolean,
128 | val attrChangedPropertyName: String
129 | ) {
130 | val kotlinType = type.toTypeName()
131 | }
132 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/Ext.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NonExhaustiveWhenStatementMigration")
2 |
3 | package ru.impression.ui_generator_base
4 |
5 | import android.content.ContextWrapper
6 | import android.graphics.drawable.Drawable
7 | import android.os.Bundle
8 | import android.os.Handler
9 | import android.os.Looper
10 | import android.util.AttributeSet
11 | import android.view.View
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.core.os.bundleOf
14 | import androidx.fragment.app.Fragment
15 | import androidx.lifecycle.Lifecycle
16 | import androidx.lifecycle.LifecycleEventObserver
17 | import androidx.lifecycle.LifecycleOwner
18 | import kotlinx.coroutines.Job
19 | import kotlinx.serialization.SerializationException
20 | import kotlinx.serialization.decodeFromString
21 | import kotlinx.serialization.encodeToString
22 | import kotlinx.serialization.json.Json
23 | import ru.impression.kotlin_delegate_concatenator.getDelegateFromSum
24 | import kotlin.reflect.KClass
25 | import kotlin.reflect.KMutableProperty0
26 | import kotlin.reflect.KMutableProperty1
27 | import kotlin.reflect.full.isSubclassOf
28 |
29 | val View.activity: AppCompatActivity?
30 | get() {
31 | var contextWrapper = (context as? ContextWrapper)
32 | while (contextWrapper !is AppCompatActivity) {
33 | contextWrapper =
34 | contextWrapper?.baseContext as ContextWrapper? ?: return null
35 | }
36 | return contextWrapper
37 | }
38 |
39 | @PublishedApi
40 | internal const val KEY_JSON_ARGS = "JSON_ARGS"
41 |
42 | inline fun Fragment.putArgument(key: String, value: T) {
43 | val arguments = arguments ?: Bundle().also { arguments = it }
44 | try {
45 | arguments.putAll(bundleOf(key to value))
46 | } catch (e: IllegalArgumentException) {
47 | e.printStackTrace()
48 | try {
49 | arguments.putString(key, Json.encodeToString(value))
50 | arguments.putStringArray(
51 | KEY_JSON_ARGS,
52 | (arguments.getStringArray(KEY_JSON_ARGS) ?: emptyArray()) + arrayOf(key)
53 | )
54 | } catch (e: SerializationException) {
55 | e.printStackTrace()
56 | }
57 | }
58 | }
59 |
60 |
61 | inline fun Fragment.getArgument(key: String): T? = arguments?.get(key)?.let {
62 | when {
63 | arguments?.getStringArray(KEY_JSON_ARGS)?.contains(key) == true -> try {
64 | Json.decodeFromString(it as? String ?: return@let null)
65 | } catch (e: SerializationException) {
66 | null
67 | }
68 | it is T -> it
69 | else -> null
70 | }
71 | }
72 |
73 |
74 | fun T.resolveAttrs(attrs: AttributeSet?) where T : Component<*, VM>, T : View {
75 | with(context.theme.obtainStyledAttributes(attrs, viewModel.attrs ?: return, 0, 0)) {
76 | try {
77 | for (delegateToAttr in viewModel.delegateToAttrs) {
78 | val property = delegateToAttr.key.property
79 | val classifier = property.returnType.classifier as? KClass<*> ?: continue
80 | property as KMutableProperty1
81 | val value = when (classifier) {
82 | Boolean::class -> getBoolean(
83 | delegateToAttr.value,
84 | property.get(viewModel) as Boolean? ?: false
85 | )
86 |
87 | Int::class -> getInt(
88 | delegateToAttr.value,
89 | property.get(viewModel) as Int? ?: 0
90 | )
91 |
92 | Float::class -> getFloat(
93 | delegateToAttr.value,
94 | property.get(viewModel) as Float? ?: 0f
95 | )
96 |
97 | String::class -> getString(delegateToAttr.value)
98 |
99 | Drawable::class -> getDrawable(delegateToAttr.value)
100 |
101 | else -> when {
102 | classifier.isSubclassOf(Enum::class) ->
103 | getInt(delegateToAttr.value, -1)
104 | .takeIf { it != -1 }
105 | ?.let { classifier.java.enumConstants?.get(it) }
106 | ?: continue
107 |
108 | else -> continue
109 | }
110 | }
111 | property.set(viewModel, value)
112 | }
113 | } finally {
114 | recycle()
115 | }
116 | }
117 | }
118 |
119 | val KMutableProperty0<*>.isLoading: Boolean
120 | get() = getDelegateFromSum>()?.isLoading == true
121 |
122 |
123 | fun KMutableProperty0<*>.reload(): Job =
124 | getDelegateFromSum>()!!.load(true)
125 |
126 | fun View.asLifecycleOwner() = ViewLifecycleOwner(this)
127 |
128 |
129 | fun View.onInit(block: () -> Unit) {
130 | (this as? Component<*, *>)?.onInit(block)
131 | }
132 |
133 | fun Fragment.onInit(block: () -> Unit) {
134 | (this as? Component<*, *>)?.onInit(block)
135 | }
136 |
137 | private fun Component<*, *>.onInit(block: () -> Unit) {
138 | hooks.addInitBlock(block)
139 | }
140 |
141 | fun View.withLifecycle(block: LifecycleScope.() -> Unit) {
142 | onInit {
143 | (this as? Component<*, *>)?.withLifecycle(block)
144 | }
145 | }
146 |
147 | fun Fragment.withLifecycle(block: LifecycleScope.() -> Unit) {
148 | onInit {
149 | Handler(Looper.getMainLooper()).post {
150 | (this as? Component<*, *>)?.withLifecycle(block)
151 | }
152 | }
153 | }
154 |
155 | private fun Component<*, *>.withLifecycle(block: LifecycleScope.() -> Unit) {
156 | val scope = LifecycleScope()
157 | block(scope)
158 | boundLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
159 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
160 | when (event) {
161 | Lifecycle.Event.ON_CREATE -> scope.callOnCreateBlocks()
162 | Lifecycle.Event.ON_START -> scope.callOnStartBlocks()
163 | Lifecycle.Event.ON_RESUME -> scope.callOnResumeBlocks()
164 | Lifecycle.Event.ON_PAUSE -> scope.callOnPauseBlocks()
165 | Lifecycle.Event.ON_STOP -> scope.callOnStopBlocks()
166 | Lifecycle.Event.ON_DESTROY -> scope.callOnDestroyBlocks()
167 | }
168 | }
169 |
170 | })
171 | }
--------------------------------------------------------------------------------
/ui-generator-base/src/main/java/ru/impression/ui_generator_base/ComponentViewModel.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_base
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.os.Parcelable
6 | import androidx.annotation.CallSuper
7 | import androidx.lifecycle.Lifecycle
8 | import androidx.lifecycle.LifecycleEventObserver
9 | import androidx.lifecycle.LifecycleOwner
10 | import androidx.lifecycle.ViewModel
11 | import ru.impression.kotlin_delegate_concatenator.getDelegateFromSum
12 | import ru.impression.singleton_entity.SingletonEntity
13 | import ru.impression.singleton_entity.SingletonEntityParent
14 | import ru.impression.ui_generator_annotations.SharedViewModel
15 | import kotlin.properties.PropertyDelegateProvider
16 | import kotlin.reflect.KMutableProperty0
17 | import kotlin.reflect.KProperty
18 | import kotlin.reflect.full.hasAnnotation
19 |
20 |
21 | abstract class ComponentViewModel(val attrs: IntArray? = null) : ViewModel(), StateOwner,
22 | LifecycleEventObserver {
23 |
24 | internal val delegates = ArrayList>()
25 |
26 | internal val delegateToAttrs = HashMap, Int>()
27 |
28 | @PublishedApi
29 | internal var _component: Component<*, *>? = null
30 |
31 | internal var componentHasMissedStateChange = false
32 |
33 | private val stateObservers = HashMap Unit>>()
34 |
35 | private val handler = Handler(Looper.getMainLooper())
36 |
37 | private val stateObserversNotifier = Runnable { notifyStateObservers(true) }
38 |
39 | private val subscriptionsInitializers = ArrayList<(() -> Unit)>()
40 |
41 | var propsAreSet = false
42 |
43 | internal val singletonEntityParent: SingletonEntityParent = object : SingletonEntityParent {
44 | override fun replace(oldEntity: SingletonEntity, newEntity: SingletonEntity) {
45 | delegates.forEach {
46 | if (it.value === oldEntity)
47 | (it as StateDelegate<*, SingletonEntity>).setValue(newEntity)
48 | }
49 | }
50 | }
51 |
52 | protected fun state(initialValue: T, attr: Int? = null, onChanged: ((T) -> Unit)? = null) =
53 | PropertyDelegateProvider> { _, property ->
54 | StateDelegate(
55 | parent = this,
56 | singletonEntityParent = singletonEntityParent,
57 | initialValue = initialValue,
58 | onChanged = onChanged,
59 | property = property
60 | ).also { delegate ->
61 | delegates.add(delegate)
62 | attr?.let { delegateToAttrs[delegate] = it }
63 | }
64 | }
65 |
66 |
67 | @CallSuper
68 | override fun onStateChanged(renderImmediately: Boolean) {
69 | notifyStateObservers(renderImmediately)
70 | }
71 |
72 | fun setComponent(component: Component<*, *>) {
73 | this._component = component
74 | component.boundLifecycleOwner.lifecycle.addObserver(this)
75 | if (componentHasMissedStateChange) {
76 | componentHasMissedStateChange = false
77 | onStateChanged()
78 | }
79 | }
80 |
81 | private fun unsetComponent() {
82 | _component?.boundLifecycleOwner?.lifecycle?.removeObserver(this)
83 | _component = null
84 | }
85 |
86 | fun initSubscriptions(block: () -> Unit) {
87 | block()
88 | subscriptionsInitializers.add(block)
89 | }
90 |
91 | fun restoreSubscriptions() {
92 | delegates.forEach { it.observeValue() }
93 | subscriptionsInitializers.forEach { it() }
94 | }
95 |
96 | fun addStateObserver(lifecycleOwner: LifecycleOwner, observer: () -> Unit) {
97 | fun addActual() {
98 | val set = stateObservers[lifecycleOwner]
99 | ?: HashSet<() -> Unit>().also { stateObservers[lifecycleOwner] = it }
100 | set.add(observer)
101 | lifecycleOwner.lifecycle.addObserver(this)
102 | }
103 | if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED))
104 | addActual()
105 | else
106 | lifecycleOwner.lifecycle.addObserver(
107 | object : LifecycleEventObserver {
108 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
109 | if (source == lifecycleOwner && event == Lifecycle.Event.ON_CREATE) {
110 | addActual()
111 | lifecycleOwner.lifecycle.removeObserver(this)
112 | }
113 | }
114 | }
115 | )
116 |
117 | }
118 |
119 | private fun removeStateObservers(lifecycleOwner: LifecycleOwner) {
120 | stateObservers.remove(lifecycleOwner)
121 | lifecycleOwner.lifecycle.removeObserver(this)
122 | }
123 |
124 | private fun notifyStateObservers(immediately: Boolean) {
125 | handler.removeCallbacks(stateObserversNotifier)
126 | if (immediately && Thread.currentThread() === Looper.getMainLooper().thread) {
127 | _component?.render(executeBindingsImmediately = true)
128 | ?: run { componentHasMissedStateChange = true }
129 | stateObservers.values.forEach { set -> set.forEach { it() } }
130 | } else {
131 | handler.post(stateObserversNotifier)
132 | }
133 | }
134 |
135 | internal fun notifyTwoWayPropChanged(propertyName: String) {
136 | _component?.onTwoWayPropChanged(propertyName)
137 | }
138 |
139 | inline fun getSharedViewModel(): T {
140 | val viewModelClass = T::class
141 | val component = _component
142 | return when {
143 | !viewModelClass.hasAnnotation() ->
144 | throw IllegalArgumentException("ViewModel must have SharedViewModel annotation")
145 | component == null ->
146 | throw IllegalStateException("Cannot get ViewModel when detached from component")
147 | else -> component.createViewModel(viewModelClass, true)
148 | }
149 | }
150 |
151 | final override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
152 | if (source === _component?.boundLifecycleOwner) onLifecycleEvent(event)
153 | if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
154 | if (source === _component?.boundLifecycleOwner) unsetComponent()
155 | removeStateObservers(source)
156 | }
157 | }
158 |
159 | open fun beforeRender() = Unit
160 |
161 | protected open fun onLifecycleEvent(event: Lifecycle.Event) = Unit
162 |
163 | open fun onSaveInstanceState(): Parcelable? = null
164 |
165 | open fun onRestoreInstanceState(savedInstanceState: Parcelable?) = Unit
166 |
167 | @CallSuper
168 | public override fun onCleared() {
169 | delegates.forEach { it.stopObserveValue() }
170 | }
171 |
172 | protected fun KMutableProperty0.set(value: T, renderImmediately: Boolean = false) {
173 | set(value)
174 | onStateChanged(renderImmediately)
175 | }
176 | }
--------------------------------------------------------------------------------
/ui-generator-processor/src/main/java/ru/impression/ui_generator_processor/FragmentComponentClassBuilder.kt:
--------------------------------------------------------------------------------
1 | package ru.impression.ui_generator_processor
2 |
3 | import com.google.devtools.ksp.KspExperimental
4 | import com.google.devtools.ksp.isAnnotationPresent
5 | import com.google.devtools.ksp.processing.KSPLogger
6 | import com.google.devtools.ksp.symbol.KSClassDeclaration
7 | import com.squareup.kotlinpoet.*
8 | import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
9 | import com.squareup.kotlinpoet.ksp.toClassName
10 | import com.squareup.kotlinpoet.ksp.toTypeName
11 | import ru.impression.ui_generator_annotations.SharedViewModel
12 |
13 | @OptIn(KotlinPoetKspPreview::class)
14 | class FragmentComponentClassBuilder(
15 | logger: KSPLogger,
16 | scheme: KSClassDeclaration,
17 | resultClassName: String,
18 | resultClassPackage: String,
19 | superclass: TypeName,
20 | viewModelClass: KSClassDeclaration,
21 | packageName: String
22 | ) : ComponentClassBuilder(
23 | logger,
24 | scheme,
25 | resultClassName,
26 | resultClassPackage,
27 | superclass,
28 | viewModelClass,
29 | packageName
30 | ) {
31 |
32 | @OptIn(KspExperimental::class)
33 | override fun buildViewModelProperty() =
34 | with(PropertySpec.builder("viewModel", viewModelClass.toClassName())) {
35 | addModifiers(KModifier.OVERRIDE)
36 | delegate(if (propProperties.isEmpty()) CodeBlock.of("lazy { createViewModel($viewModelClass::class, ${viewModelClass.isAnnotationPresent(SharedViewModel::class)}) } ") else
37 | with(CodeBlock.builder()) {
38 | add(
39 | """
40 | lazy {
41 | val viewModel = createViewModel($viewModelClass::class, ${viewModelClass.isAnnotationPresent(SharedViewModel::class)})
42 |
43 | """.trimIndent()
44 | )
45 | add("""
46 | if (!viewModel.propsAreSet) {
47 |
48 | """.trimIndent())
49 | propProperties.forEach { prop ->
50 | if (prop.kotlinType.isNullable) {
51 | add("""
52 | viewModel.${prop.name} = ${prop.name}
53 |
54 | """.trimIndent())
55 |
56 | } else {
57 | add("""
58 | ${prop.name}?.let { viewModel.${prop.name} = it }
59 |
60 | """.trimIndent())
61 | }
62 | }
63 | add("""
64 | viewModel.propsAreSet = true
65 | }
66 |
67 | """.trimIndent())
68 | add(
69 | """
70 | viewModel
71 | }
72 | """.trimIndent()
73 | )
74 | build()
75 | })
76 | build()
77 | }
78 |
79 | override fun buildContainerProperty() =
80 | with(PropertySpec.builder("container", ClassName("android.view", "View").copy(true))) {
81 | mutable(true)
82 | addModifiers(KModifier.OVERRIDE)
83 | initializer("null")
84 | build()
85 | }
86 |
87 | override fun buildBoundLifecycleOwnerProperty() = with(
88 | PropertySpec.builder(
89 | "boundLifecycleOwner",
90 | ClassName("androidx.lifecycle", "LifecycleOwner")
91 | )
92 | ) {
93 | addModifiers(KModifier.OVERRIDE)
94 | getter(FunSpec.getterBuilder().addCode("return viewLifecycleOwner").build())
95 | build()
96 | }
97 |
98 | override fun TypeSpec.Builder.addRestMembers() {
99 | propProperties.forEach { addProperty(buildPropWrapperProperty(it)) }
100 | addInitializerBlock(buildInitializerBlock())
101 | addFunction(buildOnCreateFunction())
102 | addFunction(buildOnCreateViewFunction())
103 | addFunction(buildOnActivityCreatedFunction())
104 | addFunction(buildOnSaveInstanceStateFunction())
105 | addFunction(buildOnDestroyViewFunction())
106 | }
107 |
108 | private fun buildPropWrapperProperty(propProperty: PropProperty) = with(
109 | PropertySpec.builder(
110 | propProperty.name,
111 | propProperty.type.toTypeName().copy(nullable = true)
112 | )
113 | ) {
114 | mutable(true)
115 | initializer("null")
116 | getter(
117 | FunSpec.getterBuilder()
118 | .addCode(
119 | """
120 | return field ?: %M("${propProperty.name}")
121 |
122 | """.trimIndent(),
123 | MemberName("ru.impression.ui_generator_base", "getArgument")
124 | )
125 | .build()
126 | )
127 | setter(
128 | FunSpec.setterBuilder().addParameter("value", propProperty.type.toTypeName().copy(nullable = true)).addCode(
129 | """
130 | field = value
131 | try {
132 | %M("${propProperty.name}", value)
133 | } catch (e: %T) {
134 | }
135 | """.trimIndent(),
136 | MemberName("ru.impression.ui_generator_base", "putArgument"),
137 | IllegalArgumentException::class.java
138 | ).build()
139 | )
140 | build()
141 | }
142 |
143 | private fun buildInitializerBlock() = CodeBlock.of(
144 | """
145 | hooks.callInitBlocks()
146 | """.trimIndent()
147 | )
148 |
149 | private fun buildOnCreateFunction() = with(FunSpec.builder("onCreate")) {
150 | addModifiers(KModifier.OVERRIDE)
151 | addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(true))
152 | addCode(
153 | """
154 | super.onCreate(savedInstanceState)
155 | viewModel.onRestoreInstanceState(savedInstanceState?.getParcelable("viewModelState"))""".trimIndent()
156 | )
157 | build()
158 | }
159 |
160 | private fun buildOnCreateViewFunction() = with(FunSpec.builder("onCreateView")) {
161 | addModifiers(KModifier.OVERRIDE)
162 | addParameter("inflater", ClassName("android.view", "LayoutInflater"))
163 | addParameter("container", ClassName("android.view", "ViewGroup").copy(true))
164 | addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(true))
165 | returns(ClassName("android.view", "View").copy(true))
166 | addCode(
167 | """
168 | this.container = container
169 | return render(attachToContainer = false, executeBindingsImmediately = false)?.root
170 | """.trimIndent()
171 | )
172 | build()
173 | }
174 |
175 | private fun buildOnActivityCreatedFunction() = with(FunSpec.builder("onActivityCreated")) {
176 | addModifiers(KModifier.OVERRIDE)
177 | addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(true))
178 | addCode(
179 | """
180 | super.onActivityCreated(savedInstanceState)
181 | viewModel.setComponent(this)
182 | """.trimIndent()
183 | )
184 | build()
185 | }
186 |
187 | private fun buildOnSaveInstanceStateFunction() = with(FunSpec.builder("onSaveInstanceState")) {
188 | addModifiers(KModifier.OVERRIDE)
189 | addParameter("outState", ClassName("android.os", "Bundle"))
190 | addCode(
191 | """
192 | super.onSaveInstanceState(outState)
193 | outState.putParcelable("viewModelState", viewModel.onSaveInstanceState())
194 | """.trimIndent()
195 | )
196 | build()
197 | }
198 |
199 | private fun buildOnDestroyViewFunction() = with(FunSpec.builder("onDestroyView")) {
200 | addModifiers(KModifier.OVERRIDE)
201 | addCode(
202 | """
203 | container = null
204 | dataBindingManager.releaseBinding()
205 | super.onDestroyView()
206 | """.trimIndent()
207 | )
208 | build()
209 | }
210 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **UI-generator is a framework that allows you to intuitively and quickly create UI** using the principle of reusable components. This principle is the most modern and effective in the field of UI development, and it underlies such frameworks as React and Flutter.
2 |
3 | ---
4 | **UI-generator is similar in functionality to [Jetpack Compose](https://developer.android.com/jetpack/compose)** and provides all its main features. But unlike the Jetpack Compose, UI-generator is fully available now and is compatible with the components of the Android support library - Fragments and Views, so you do not have to rewrite all your code to implement this framework. UI-generator works on annotation processing and generates code on top of Fragment and View classes.
5 |
6 | ## Installation
7 |
8 | In your root build.gradle.kts:
9 | ```kts
10 | allprojects {
11 | repositories {
12 | ...
13 | maven(url = "https://jitpack.io")
14 | }
15 | }
16 | ```
17 | In your app/build.gradle.kts:
18 | ```kts
19 | plugins {
20 | id("com.google.devtools.ksp") version ""
21 | }
22 |
23 | ksp {
24 | arg("packageName", "")
25 | }
26 |
27 | android {
28 | ...
29 | dataBinding {
30 | isEnabled = true
31 | }
32 | }
33 |
34 | dependencies {
35 | implementation("com.github.ArtemiyDmtrvch.ui-generator:ui-generator-base:+")
36 | implementation("com.github.ArtemiyDmtrvch.ui-generator:ui-generator-annotations:+")
37 | ksp("com.github.ArtemiyDmtrvch.ui-generator:ui-generator-processor:+")
38 | }
39 | ```
40 | ## Why do you need UI-generator
41 | - You will write ***at least 2 times less code*** than if you wrote using Android SDK and any architecture.
42 | - The entry threshold into your project will be minimal, because there are very few rules, and they are simple and universal for all situations
43 | - Your code will be a priori reusable, and you will never have a situation when you have a Fragment, but you need to display it in the RecyclerView
44 | - The principles laid down in UI-generator are the most promising for development for any platform, and soon they will become the standard for Android development
45 |
46 | ## Now let's see how this is all achieved
47 |
48 | ### 1. One rule for all components
49 |
50 | Suppose you have a Fragment in which an argument is passed, which is then displayed in the TextView. Here's how you do it:
51 | ```kotlin
52 | @MakeComponent
53 | class MyFragment : ComponentScheme({ R.layout.my_fragment })
54 |
55 | class MyFragmentViewModel : ComponentViewModel() {
56 |
57 | @Prop
58 | var myText by state(null)
59 | }
60 | ```
61 | ```xml
62 | // my_fragment.xml
63 |
64 |
65 |
68 |
69 |
73 |
74 | ```
75 | The annotation processor will generate a class **MyFragmentComponent** inherited from the Fragment with the ViewModel and the field `myText`. When this field is changed, an argument will be added to the Fragment, which will then be passed to the ViewModel and bound to the TextView using [Android data binding](https://developer.android.com/topic/libraries/data-binding). As a result, **this class can be used like this:**
76 | ```kotlin
77 | showFragment(MyFragmentComponent().apply { myText = "Hello world!" })
78 | ```
79 |
80 | Now let's imagine a similar example, but you will have a FrameLayout to which the text is bound, which is then displayed in the TextView. And here is how you do it:
81 | ```kotlin
82 | @MakeComponent
83 | class MyLayout : ComponentScheme({ R.layout.my_layout })
84 |
85 | class MyLayoutViewModel : ComponentViewModel() {
86 |
87 | @Prop
88 | var myText by state(null)
89 | }
90 | ```
91 | ```xml
92 | // my_layout.xml
93 |
94 |
95 |
98 |
99 |
103 |
104 | ```
105 | The annotation processor will generate a class **MyLayoutComponent** inherited from the FrameLayout with the ViewModel and the [binding adapter](https://developer.android.com/topic/libraries/data-binding/binding-adapters) for `myText` attribute, which will pass the value to the ViewModel. As a result, **this class can be used like this:**
106 | ```xml
107 |
111 | ```
112 |
113 | As you can see, the codes for the Fragment and for the View are completely identical. You do not need to write View and Fragment classes at all, they are generated automatically.
114 |
115 | **The single rule is:** create a class inherited from `ComponentScheme`, specify the super component as the first type argument, ViewModel as the second and mark this class with `MakeComponent` annotation. Then mark with `Prop` annotation those properties that may come from the parent component (the properties must be vars). An argument will be generated for the Fragment, and a binding adapter for the View. Then build the project and use generated classes.
116 |
117 | **Note:** you may need to build the project twice so that the binding adapters and component classes are generated correctly.
118 |
119 | Also, in the case of a View:
120 | - You can set `Prop.twoWay = true`, and then a two-way binding adapter will be generated for the View. It will send the value back when the annotated property changes.
121 | ```kotlin
122 | @Prop(twoWay = true)
123 | var twoWayText: String? = null //a two-way binding adapter will be generated
124 | ```
125 | - You can bind xml attribute to your state property:
126 | ```kotlin
127 | var picture by state(null, attr = R.styleable.MyViewComponent_picture)
128 | ```
129 | ```xml
130 |
132 | ```
133 |
134 | ### 2. Observable state
135 |
136 | First you create the `viewModel` variable in your layout.xml. Then you declare certain properties in the ViewModel by the `state` delegate. Each time one of these properties changes, data binding is performed. And this mechanism allows you to **forget about LiveData and ObservableFields.** Now the data for binding can be just vars.
137 |
138 | This mechanism optimally distributes the load on the main thread (data binding is placed at the end of the message queue of the main Looper). And if there are many consecutive state changes data binding will only be done once:
139 | ```kotlin
140 | property1 = "Hello world!"
141 | property2 = 123
142 | property3 = true
143 | //data binding will only be done once
144 | ```
145 |
146 | Data binding is performed at one time for all Views by replacing the old bound ViewModel with a new one. And this does not make the binding algorithm more complicated than using LiveData and ObservableFields, since all native data binding adapters and generated ones are not executed if the new value is the same as the old one.
147 |
148 | You can manually initiate data binding by calling `onStateChanged` function in ViewModel.
149 |
150 | **Note:** two-way data binding also works - changes in the view will change your state property
151 |
152 | ### 3. Functional rendering
153 |
154 | Suppose you need to display one or another layout, depending on the condition. Here's how you do it:
155 | ```kotlin
156 | @MakeComponent
157 | class MyScrollView : ComponentScheme({ viewModel ->
158 | if(viewModel.showFirstLayout)
159 | R.layout.first_layout
160 | else
161 | R.layout.second_layout
162 | })
163 | ```
164 | This lambda is called at the same time as data binding, that is, after a state change. In essence, this is also data binding, but to a super component. In addition to the ViewModel, a super component is passed to this lambda as `this`, and you can bind any data to it:
165 | ```kotlin
166 | @MakeComponent
167 | class MyButton : ComponentScheme