https://kotlinlang.org logo
#compose
Title
# compose
j

Joost Klitsie

07/06/2020, 2:04 PM
The
BaseComponent
in itself basically is a lifecycleOwner, observes the parent's lifecycleowner (so copies those and will call on_destroy when onDispose is called on itself) and creates the dependency graph (by extending the parent's dependency graph). Does anyone have any pointers here? 🙂
f

Foso

07/06/2020, 2:36 PM
Hi, i tried Compose with react like Components https://github.com/Foso/ComposeReact
👍 1
j

Joost Klitsie

07/06/2020, 2:41 PM
@Foso I like the example 🙂 But in your case, is every time a new class instantiated? (Like on every pass through)
l

Leland Richardson [G]

07/06/2020, 4:46 PM
what is the motivation here? Is there something you are trying to do but can’t? Compose is embracing functions as the unit of abstraction. Classes don’t match the mental model quite as well (and they don’t for react either, btw, and react is moving away from classes as well)
I’d like to understand what is pushing you in the direction of wanting this though, and if it’s just familiarity or something more fundamental
managing the lifecycle of the class correctly is possible but it seems impossible to do and preserve a clean calling syntax or conventionally going for something in a companion object like in your example
j

Joost Klitsie

07/06/2020, 4:54 PM
Well my problem is mostly: How should I structure my app? What architecture can I use? I didn't see any example thusfar, so if you can point me at something that'd be great. I see for example this article: https://medium.com/better-programming/create-a-component-based-architecture-in-android-jetpack-compose-96980c191351 Or the Jet News Application But they do not provide any information how to structure an app. They only provide information for mainly 1 screen. There is no dependency injection happening. The app in the article only covers 1 screen, the Jet news is just passing on a bunch of repositories as arguments with the functions, that is not how I would structure a bigger app.
@Leland Richardson [G] So how would you architect an actual app?
written with compose
do you know any resources for me to check out? 🙂
l

Leland Richardson [G]

07/06/2020, 4:57 PM
we are working on more broadly focused resources/content that help answer a question like this, but i don’t think i can do so one-off in this slack thread, sorry 😕 is there a specific problem that you’re having (ie, you mention dependency injection?) that I can maybe provide a more specific answer for?
j

Joost Klitsie

07/06/2020, 5:11 PM
Well lets say I have an app with a structure like this:
Copy code
-Login
--Welcome
--Registration
--Username/Password
-MainOverview
--News Feed
---Article
----Comment
-Friends
--List
---Messaging
---Details
-User settings
--Name change
--Logout
Usually I would make my navgraphs resemble that, with nested graphs. So I have the following: Every screen here is a fragment. I inject a ViewModel for every screen. The screens do not know anything about their position, only their parent component controls them. Basically any dependency injection will inherit from the parent component the dependencies and that is how we can inject things in every screen. So how would I achieve an architecture in compose? I cannot just inject all the viewmodels into my single activity and then pass all of that as arguments down the composable graph. (or should I indeed do this? Expose the entire viewtree through 1 app state). Like if I turn them into classes that are components in themself, I can do injection per component, have 1 ViewModel per component etc. But how do I achieve a tidy architecture with compose if we do not have these components where we can inject things in between?
Perhaps I should just rethink the way how I should architect an app 🙂 But I don't know where to start
l

Leland Richardson [G]

07/06/2020, 5:15 PM
so it sounds like this is mostly a question around dependency injection
is there a particular DI lib you are using?
j

Joost Klitsie

07/06/2020, 5:17 PM
kodein
Like my plan was to make a multiplatform POC app with react for JS and compose for Android, and kodein is multiplatform. I wanna see how much code I can actually reuse and still get a full native experience, and compose and react might help as they are (in a very loose sense 🙂 ) kinda similar (one day I also wanna add swift ui to the POC)
l

Leland Richardson [G]

07/06/2020, 5:26 PM
sounds neat
so give me a sec and i’ll provide an example for both contained and global
❤️ 1
ok so it took me a while to figure out what the names of these types are in kodein’s documentation
there would be lots of ways to do this since there appear to be lots of ways to use Kodein
this mimics the
newInstance
method of injecting with kodein
if you use kodein in a different way usually, let me know if it’s not clear how to create something equivalent for that approach
j

Joost Klitsie

07/06/2020, 5:56 PM
Thank you very much, I'll have a look soon 🙂
Okay I got it to run, indeed seems to solve my issue! I guess most important is the scoping of the binding, the remember doesn't seem to do a lot
Copy code
val ParentDI = ambientOf<DI> { DI {} }

val DI = ambientOf<DI> { DI {} }

@Composable
inline fun <reified T : Any> inject(): DIProperty<T> = DI.current.instance()

@Composable
fun myModule() = subDI(ParentDI.current) {
    bind() from singleton {
        Controller()
    }
}

val RootKodeinContext = DI {
    bind() from singleton {
        ParentController()
    }
}

class ParentController() {

    val state = MutableStateFlow(Random.nextInt())

}

class Controller() {

    val state = MutableStateFlow("I am magic")

    init {
        CoroutineScope(Dispatchers.Main).launch {
            while (true) {
                state.value = System.currentTimeMillis().toString().takeLast(4)
                delay(1000)
            }
        }
    }
}

// Provide a context at the root.... or anywhere in the tree you want
@Composable
fun App() {
    Providers(
        DI provides RootKodeinContext
    ) {
        Column {
            val parentController by inject<ParentController>()
            parentController.state.collectAsState().value.let {
                Text(text = it.toString())
            }
            Providers(ParentDI provides DI.current) {
                Child()
            }
        }

    }
}

@Composable
fun Child() {
    // constructs and remembers a Controller based on current kodein context
    Providers(
        DI provides myModule()
    ) {
        val parentController by inject<ParentController>()
        parentController.state.collectAsState().value.let {
            Text(text = it.toString())
        }
        val controller by inject<Controller>()
        controller.state.collectAsState().value.let {
            // use controller
            Text(it)
        }

    }
}
It probably needs some finetuning 😅
But I will figure it out 🙂 thanks @Leland Richardson [G]!
l

Leland Richardson [G]

07/06/2020, 9:23 PM
no problem
can you help me understand the goal of
myModule()
in your example?
j

Joost Klitsie

07/06/2020, 9:27 PM
yeah so preferably the things are nested
so I wanted to make a function to return a module that is a subDI of the parent
the naming is off there as it is not a module
it is a di container
which is a sub container of the parent
but I wanted to see how to build such a container, and I am guessing the ambient of the parentDI only lives in a composable function
so therefore I used a function so I can make it a sub container of any given parent
I think the original parent can be this: `
Copy code
val ParentDI = ambientOf<DI> { (ContextAmbient.current.applicationContext as DIAware).di }
`
and then all the other DI containers will be whatever child of how it is nested
l

Leland Richardson [G]

07/06/2020, 9:37 PM
gotch
you could make something like this
Copy code
@Composable
fun subContainer(
  bind: KodeinScope.() -> Unit,
  content: @Composable () -> Unit
) = Providers(
  ParentDI provides subDI(ParentDI.current, bind),
  content
)
I don’t know the kodein library that well though… you might want to be careful if
subDI
produces a new object every time then it will recompose all of the composables that use
inject
you might want to implement a custom
areEquivalent
method in the
ambientOf
declaration
j

Joost Klitsie

07/07/2020, 6:30 AM
Thanks for your help! Yes I will check if it actually recomposes (would a simple println in the right places show that? :) ) I also think I can simply use 1 di object that always extends and after that replaces the previous one, but I think that is also in your code snippet happening now :)
Okay to me it looks like the Providers(...) is only called once. @Leland Richardson [G] do you know how or when that piece of code is called?
Copy code
@Composable
fun subContainer(
    bind: DI.MainBuilder.() -> Unit,
    content: @Composable () -> Unit
) = Providers(
    DI provides subDI(DI.current, init = bind),
    children = content
)
If it is only called 1 time, I do not need to make a comparison with for example the context (because I'd have to maintain those myself but I'd rather not)
Copy code
ambientOf<DI>({ old, new ->....
In theory (tm) the DI object is only build 1 time per "component" and shouldn't change during that component is alive. (so if I would remove and add a "component" from/to the graph then of course it gets reinitialized, but that is fine) Then it'll look something like this:
Copy code
val DI = ambientOf { DI {} }

@Composable
inline fun <reified T : Any> inject(): DIProperty<T> = DI.current.instance()

@Composable
fun composeSubDI(
    bind: DI.MainBuilder.() -> Unit,
    content: @Composable () -> Unit
) = Providers(
    DI provides subDI(DI.current, init = bind),
    children = content
)

// Provide a context at the root.... or anywhere in the tree you want
@Composable
fun App() {
    Providers(
        // Inherit from application
        DI provides subDI((ContextAmbient.current.applicationContext as DIAware).di) { // We could move out the contents of this somewhere else
            bind() from singleton {
                ParentController()
            }
        }
    ) {
        Column {
            val parentController by inject<ParentController>()
            parentController.state.collectAsState().value.let {
                Text(text = it.toString())
            }
            Child()
        }

    }
}

val myDIBuilder : DI.MainBuilder.() -> Unit =  {
    bind() from singleton {
        Controller("Some amazing text", false)
    }
}

@Composable
fun Child() { // <-- My amazing child component
    composeSubDI(bind = myDIBuilder) { // <-- should take care of its own DI stuff
        val controller by inject<Controller>()
        controller.state.collectAsState().value.let {
            Text(it)
        }
        SomeOtherChild()
    }
}
I think it is important to make it singletons (in kodein a singleton is only a singleton in the scope, not for the entire application) so it will just use the DI object to return the same instance all the time. I didn't see the DI object changing during redrawing the text because of state changes, so I guess this would be doable for now like this.