Skip to content

Commit 97048f2

Browse files
committed
Initial impl
1 parent 3303e03 commit 97048f2

File tree

49 files changed

+698
-572
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+698
-572
lines changed

gradle/libs.versions.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,33 @@ kotlin = "1.9.0"
1111
ktor = "2.3.9"
1212
kamel = "0.9.4"
1313
voyager = "1.0.0"
14+
kotlin-coroutines = "1.6.4"
1415
nexus-publish = "2.0.0-rc-1"
16+
kotlinx-serialization = "1.6.3"
17+
kotlinx-atomicfu = "0.24.0"
1518

1619
[libraries]
1720
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
1821
nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin", version.ref = "nexus-publish" }
1922
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "jetpack-compose-bom" }
2023
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
2124
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
25+
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" }
2226
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
2327
kamel = { module = "media.kamel:kamel-image", version.ref = "kamel" }
2428
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
29+
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
2530
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
2631
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
2732
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
2833
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
2934
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
3035
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
3136
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
37+
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
38+
kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
39+
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
40+
kotlinx-atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu", version.ref = "kotlinx-atomicfu" }
3241

3342
[plugins]
3443
androidLibrary = { id = "com.android.library", version.ref = "agp" }

nodal/build.gradle.kts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
2+
13
plugins {
24
alias(libs.plugins.kotlinMultiplatform)
35
alias(libs.plugins.androidLibrary)
6+
alias(libs.plugins.jetbrainsCompose)
7+
alias(libs.plugins.kotlinxSerialization)
48
id("module.publication")
59
}
610

711
kotlin {
12+
explicitApi()
13+
explicitApi = ExplicitApiMode.Strict
14+
815
targetHierarchy.default()
916
jvm()
1017
androidTarget {
@@ -18,12 +25,30 @@ kotlin {
1825
iosX64()
1926
iosArm64()
2027
iosSimulatorArm64()
21-
linuxX64()
28+
// linuxX64()
2229

2330
sourceSets {
31+
val androidMain by getting {
32+
dependencies {
33+
implementation(project.dependencies.platform(libs.compose.bom))
34+
implementation(libs.androidx.activity.compose)
35+
implementation(libs.androidx.appcompat)
36+
implementation(libs.compose.ui.tooling.preview)
37+
}
38+
}
2439
val commonMain by getting {
2540
dependencies {
26-
//put your multiplatform dependencies here
41+
implementation(libs.koin.core)
42+
implementation(libs.kotlin.coroutines)
43+
implementation(libs.kotlinx.serialization.core)
44+
implementation(libs.kotlinx.serialization.json)
45+
implementation(libs.kotlinx.atomicfu)
46+
47+
implementation(compose.runtime)
48+
implementation(compose.foundation)
49+
implementation(compose.material)
50+
implementation(compose.components.resources)
51+
2752
}
2853
}
2954
val commonTest by getting {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package dev.omkartenkale.nodal
2+
3+
import android.widget.FrameLayout
4+
import androidx.activity.OnBackPressedDispatcher
5+
import androidx.activity.compose.setContent
6+
import androidx.annotation.CallSuper
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.compose.ui.platform.ComposeView
9+
import androidx.lifecycle.lifecycleScope
10+
import dev.omkartenkale.nodal.Node.Companion.createRootNode
11+
import dev.omkartenkale.nodal.compose.UI
12+
import dev.omkartenkale.nodal.misc.RemovalRequest
13+
import kotlinx.coroutines.launch
14+
15+
public abstract class NodalActivity : AppCompatActivity() {
16+
17+
public lateinit var ui: UI
18+
public lateinit var contentNode: Node
19+
protected inline fun <reified T : Node> setContentNode(
20+
container: FrameLayout = findViewById(android.R.id.content),
21+
noinline dependencyDeclaration: DependencyDeclaration = {},
22+
) {
23+
// val finalNodeConfigBuilder: NodeConfigBuilder = {
24+
// param<RemovalRequest>(RemovalRequest{ finish() })
25+
//// param(localsharedpref)
26+
//// param(savedinstancestate)
27+
//// param(Unit)
28+
// apply(nodeConfigBuilder)
29+
// }
30+
31+
contentNode = createRootNode<T>{
32+
// provides<Timber> { Timber() }
33+
// provides<TreeVisualiser> { TreeVisualiser() }
34+
// provides<StringProvider> { StringProvider() }
35+
// provides<AnalyticsLogger> { AnalyticsLogger() }
36+
// provides<KoroutineDispatcher> { KoroutineDispatcher() }
37+
// provides<FileManager> { FileManager() }
38+
//
39+
// provides<SesionInfo> { SesionInfo() }
40+
// provides<RideInfo> { RideInfo() }
41+
// provides<SelectedCountry> { SelectedCountry() }
42+
// provides<DeeplinkResolver> { DeeplinkResolver() }
43+
// provides{ SystemNotificationManager() }
44+
// provides{ SystemEventListener() }
45+
// provides{ BuildVarient() }
46+
// provides{ NodalConfig() }
47+
//
48+
// //only to children and not subchildren
49+
// provides<UIContainer> { UIContainer() }
50+
// provides<SomeDeeplink> { SomeDeeplink() }
51+
// provides { ProductDetails() }
52+
//
53+
// providesSelf<RemovalRequest>(RemovalRequest{ finish() })
54+
55+
provides<NodalConfig>{ NodalConfig(true) }
56+
provides<OnBackPressedDispatcher>{ onBackPressedDispatcher }
57+
provides<UI>{ UI().also {
58+
ui = it
59+
container.addView(ComposeView(this@NodalActivity).also { setContent { ui.drawLayers() } })
60+
} }
61+
providesSelf<RemovalRequest> {
62+
RemovalRequest {
63+
finish()
64+
}
65+
}
66+
include(dependencyDeclaration)
67+
}.apply { dispatchAdded() }
68+
container.addView(ComposeView(this).also { setContent { ui.drawLayers() } })
69+
}
70+
71+
@CallSuper
72+
override fun onResume() {
73+
super.onResume()
74+
if (::ui.isInitialized) {
75+
lifecycleScope.launch {
76+
ui.dispatchFocusChanged(isFocused = true)
77+
}
78+
}
79+
}
80+
81+
@CallSuper
82+
override fun onPause() {
83+
super.onPause()
84+
if (::ui.isInitialized) {
85+
lifecycleScope.launch {
86+
ui.dispatchFocusChanged(isFocused = false)
87+
}
88+
}
89+
}
90+
91+
@CallSuper
92+
override fun onDestroy() {
93+
super.onDestroy()
94+
if (::contentNode.isInitialized) {
95+
contentNode.dispatchRemoved()
96+
}
97+
}
98+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.omkartenkale.nodal.misc
2+
3+
import dev.omkartenkale.nodal.Node
4+
import kotlin.reflect.KClass
5+
6+
public actual fun <T : Node> KClass<T>.instantiate(): T {
7+
return java.newInstance()
8+
}

nodal/src/androidMain/kotlin/fibiprops.android.kt

Lines changed: 0 additions & 2 deletions
This file was deleted.

nodal/src/androidUnitTest/kotlin/AndroidFibiTest.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.omkartenkale.nodal.misc
2+
3+
import dev.omkartenkale.nodal.Node
4+
import kotlin.reflect.KClass
5+
6+
public actual fun <T : Node> KClass<T>.instantiate(): T {
7+
TODO("Not yet implemented")
8+
}

nodal/src/commonMain/kotlin/CustomFibi.kt

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package dev.omkartenkale.nodal
2+
3+
public data class NodalConfig(val createEagerInstances: Boolean = false)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package dev.omkartenkale.nodal
2+
3+
import dev.omkartenkale.nodal.exceptions.DisallowedNodeAdditionException
4+
import dev.omkartenkale.nodal.exceptions.NodeCreationException
5+
import dev.omkartenkale.nodal.lifecycle.ChildChangedEvent
6+
import dev.omkartenkale.nodal.misc.RemovalRequest
7+
import dev.omkartenkale.nodal.misc.instantiate
8+
import kotlinx.atomicfu.atomic
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.SupervisorJob
12+
import kotlinx.coroutines.cancel
13+
import kotlinx.coroutines.flow.MutableSharedFlow
14+
import kotlinx.coroutines.flow.MutableStateFlow
15+
import kotlinx.coroutines.flow.filter
16+
import kotlinx.coroutines.flow.launchIn
17+
import kotlinx.coroutines.flow.onEach
18+
import kotlinx.coroutines.launch
19+
import kotlin.reflect.KClass
20+
21+
internal val nodeId = atomic(0)
22+
23+
public open class Node {
24+
25+
private lateinit var _scope: Scope
26+
public val dependencies: Scope
27+
get() = if (::_scope.isInitialized) {
28+
_scope
29+
} else error("Accessing dependencies in init block is not supported")
30+
31+
public companion object {
32+
private fun instantiateNode(
33+
klass: KClass<out Node>,
34+
parentScope: Scope,
35+
dependencyDeclaration: DependencyDeclaration
36+
): Node = try {
37+
val newNodeId = nodeId.getAndIncrement()
38+
val nodeScope = Scope("${klass.simpleName}#$newNodeId", parentScope)
39+
klass.instantiate().also {
40+
it.init(nodeScope)
41+
DependencyDeclarationDSL(nodeScope).apply(dependencyDeclaration)
42+
.apply(it.providesDependencies)
43+
}
44+
} catch (t: Throwable) {
45+
throw NodeCreationException(klass, t)
46+
}
47+
48+
public inline fun <reified T : Node> createRootNode(
49+
noinline dependencyDeclaration: DependencyDeclaration
50+
): T = createRootNode(T::class, dependencyDeclaration) as T
51+
52+
public fun createRootNode(
53+
klass: KClass<out Node>, dependencyDeclaration: DependencyDeclaration
54+
): Node = instantiateNode(klass, createRootScope(), dependencyDeclaration)
55+
}
56+
57+
private fun init(scope: Scope) {
58+
_scope = scope
59+
}
60+
61+
private val removalRequest: RemovalRequest by dependencies<RemovalRequest>()
62+
63+
public open val providesDependencies: DependencyDeclaration = {}
64+
public val stateChangedEvents: MutableStateFlow<NodeLifecycleState> =
65+
MutableStateFlow(NodeLifecycleState.INITIALIZED)
66+
public val childChangedEvents: MutableSharedFlow<ChildChangedEvent> = MutableSharedFlow()
67+
68+
protected var isAdded: Boolean = false
69+
set(value) {
70+
field = value
71+
stateChangedEvents.value =
72+
if (value) NodeLifecycleState.ADDED else NodeLifecycleState.REMOVED
73+
}
74+
75+
protected inline fun <reified T : Any> dependencies(): Lazy<T> = lazy {
76+
dependencies.get<T>()
77+
}
78+
// .also {
79+
// if (dependencies.get<NodalConfig>().createEagerInstances) {
80+
// it.value
81+
// }
82+
// }
83+
84+
private val _children = mutableListOf<Node>()
85+
public val children: List<Node> = _children
86+
87+
protected var coroutineScope: CoroutineScope =
88+
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
89+
90+
public inline fun <reified T : Node> addChild(noinline dependencyDeclaration: DependencyDeclaration = {}): T =
91+
addChild(T::class, dependencyDeclaration) as T
92+
93+
public fun addChild(
94+
klass: KClass<out Node>, dependencyDeclaration: DependencyDeclaration
95+
): Node {
96+
if (stateChangedEvents.value == NodeLifecycleState.REMOVED) {
97+
throw DisallowedNodeAdditionException(this::class, klass)
98+
}
99+
val dependencyDeclaration: DependencyDeclaration = {
100+
providesSelf<RemovalRequest> {
101+
RemovalRequest {
102+
removeChild(children.first())
103+
}
104+
}
105+
include(dependencyDeclaration)
106+
}
107+
val node = instantiateNode(klass, dependencies, dependencyDeclaration)
108+
_children.add(node)
109+
node.dispatchAdded()
110+
coroutineScope.launch { childChangedEvents.emit(ChildChangedEvent.NodeAdded(node)) }
111+
return node
112+
}
113+
114+
public fun removeChild(node: Node) {
115+
if (children.contains(node)) {
116+
node.dispatchRemoved()
117+
_children.remove(node)
118+
coroutineScope.launch { childChangedEvents.emit(ChildChangedEvent.NodeRemoved(node)) }
119+
} else error("Cant remove node as parent does not contain child. \n\tParent: ${this::class.simpleName} (${this::class.qualifiedName}), \n\tChild: ${node::class.simpleName} (${node::class.qualifiedName})")
120+
}
121+
122+
public open fun handleDetachRequest(node: Node): Boolean {
123+
removeChild(node)
124+
return true
125+
}
126+
127+
public fun dispatchAdded() {
128+
onAdded()
129+
isAdded = true
130+
}
131+
132+
public open fun onAdded(): Unit = Unit
133+
134+
internal fun dispatchRemoved() {
135+
children.forEach { it.dispatchRemoved() }
136+
isAdded = false
137+
onRemoved()
138+
coroutineScope.cancel()
139+
dependencies.close()
140+
}
141+
142+
public open fun onRemoved(): Unit = Unit
143+
protected fun removeSelf(): Unit = removalRequest.invoke()
144+
145+
public fun Node.onAdded(block: () -> Unit) {
146+
stateChangedEvents.filter { it == NodeLifecycleState.ADDED }.onEach { block() }
147+
.launchIn(coroutineScope)
148+
}
149+
150+
public fun Node.onRemoved(block: () -> Unit) {
151+
stateChangedEvents.filter { it == NodeLifecycleState.REMOVED }.onEach { block() }
152+
.launchIn(coroutineScope)
153+
}
154+
}

0 commit comments

Comments
 (0)