Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libraries/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
implementation(libs.androidx.lifecycle.java8)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.compose.foundation.layout)
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")

testImplementation(project(":libraries:testing-junit4"))
testImplementation(libs.androidx.arch.core.testing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityBoundary
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequestBoundary
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester
import com.bumble.appyx.viewmodel.IntegrationPointViewModel

open class ActivityIntegrationPoint(
private val activity: Activity,
private val activity: ComponentActivity,
savedInstanceState: Bundle?,
) : IntegrationPoint(savedInstanceState = savedInstanceState) {
private val activityBoundary = ActivityBoundary(activity, requestCodeRegistry)
Expand All @@ -23,6 +25,10 @@ open class ActivityIntegrationPoint(
override val permissionRequester: PermissionRequester
get() = permissionRequestBoundary

val viewModel = IntegrationPointViewModel.getInstance(activity)

fun isChangingConfigurations(): Boolean = activity.isChangingConfigurations

fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
activityBoundary.onActivityResult(requestCode, resultCode, data)
}
Expand Down
23 changes: 21 additions & 2 deletions libraries/core/src/main/kotlin/com/bumble/appyx/core/node/Node.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import com.bumble.appyx.Appyx
import com.bumble.appyx.core.BuildConfig
import com.bumble.appyx.core.integrationpoint.ActivityIntegrationPoint
import com.bumble.appyx.core.integrationpoint.IntegrationPoint
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient
Expand Down Expand Up @@ -44,7 +48,7 @@ open class Node(
buildContext: BuildContext,
val view: NodeView = EmptyNodeView,
plugins: List<Plugin> = emptyList()
) : NodeLifecycle, NodeView by view, RequestCodeClient {
) : NodeLifecycle, NodeView by view, RequestCodeClient, ViewModelStoreOwner {

@Suppress("LeakingThis") // Implemented in the same way as in androidx.Fragment
private val nodeLifecycle = NodeLifecycleImpl(this)
Expand Down Expand Up @@ -74,10 +78,14 @@ open class Node(
check(isRoot) { "Only a root Node can have an integration point" }
field = value
}

private var wasBuilt = false

val id = getNodeId(buildContext)
private val nodeViewModelStore by lazy {
(integrationPoint as ActivityIntegrationPoint).viewModel.getViewModelStoreForNode(
id
)
}

override val requestCodeClientId: String = id

Expand All @@ -90,6 +98,14 @@ open class Node(
if (!wasBuilt) error("onBuilt was not invoked for $this")
}
})

lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
if (!(integrationPoint as ActivityIntegrationPoint).isChangingConfigurations()) {
(integrationPoint as ActivityIntegrationPoint).viewModel.clear(id)
}
}
})
}

private fun getNodeId(buildContext: BuildContext): String {
Expand Down Expand Up @@ -129,6 +145,7 @@ open class Node(
CompositionLocalProvider(
LocalNode provides this,
LocalLifecycleOwner provides this,
LocalViewModelStoreOwner provides this
) {
HandleBackPress()
View(modifier)
Expand Down Expand Up @@ -243,4 +260,6 @@ open class Node(
}

}

override fun getViewModelStore(): ViewModelStore = nodeViewModelStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.bumble.appyx.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.get

class IntegrationPointViewModel : ViewModel() {
private val viewModelStores = mutableMapOf<String, ViewModelStore>()
fun clear(nodeId: String) {
val viewModelStore = viewModelStores.remove(nodeId)
viewModelStore?.clear()
}

override fun onCleared() {
super.onCleared()
viewModelStores.values.forEach { it.clear() }
viewModelStores.clear()
}

fun getViewModelStoreForNode(nodeId: String): ViewModelStore =
viewModelStores.getOrPut(nodeId) { ViewModelStore() }

companion object {
fun getInstance(viewModelStoreOwner: ViewModelStoreOwner): IntegrationPointViewModel {
return ViewModelProvider(viewModelStoreOwner).get()
}
}
}

Check warning

Code scanning / detekt

Checks whether files end with a line separator.

The file /home/runner/work/appyx/appyx/libraries/core/src/main/kotlin/com/bumble/appyx/viewmodel/IntegrationPointViewModel.kt is not ending with a new line.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.bumble.appyx.app.node.samples

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch

class MyViewModel() : ViewModel() {

Check warning

Code scanning / detekt

Empty block of code detected. As they serve no purpose they should be removed.

An empty default constructor can be removed.

private val stateFlow = MutableStateFlow(0)
val flow: Flow<Int> = stateFlow

init {
viewModelScope.launch {
while (true) {
stateFlow.getAndUpdate { value ->
value + 1
}
delay(1000)
}
}
}

override fun onCleared() {
super.onCleared()
}
}

Check warning

Code scanning / detekt

Checks whether files end with a line separator.

The file /home/runner/work/appyx/appyx/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/MyViewModel.kt is not ending with a new line.
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bumble.appyx.app.node.backstack.InsideTheBackStack
import com.bumble.appyx.app.node.cards.CardsExampleNode
import com.bumble.appyx.app.node.helper.screenNode
Expand All @@ -26,8 +29,8 @@ import com.bumble.appyx.core.navigation.EmptyNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.sample.navigtion.compose.ComposeNavigationRoot
import com.bumble.appyx.interactions.App
import com.bumble.appyx.sample.navigtion.compose.ComposeNavigationRoot
import kotlinx.parcelize.Parcelize

class SamplesSelectorNode(
Expand Down Expand Up @@ -97,6 +100,8 @@ class SamplesSelectorNode(

@Composable
override fun View(modifier: Modifier) {
val viewModel = viewModel<MyViewModel>()
val counter = viewModel.flow.collectAsState(initial = 0)
val decorator: @Composable (child: ChildRenderer) -> Unit = remember {
{ childRenderer ->
ScaledLayout {
Expand All @@ -110,6 +115,9 @@ class SamplesSelectorNode(
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top)

) {
item {
Text(text = "Counter: ${counter.value}")
}
item {
CardItem(decorator)
}
Expand Down