https://kotlinlang.org logo
Title
o

Olivier Patry

04/15/2021, 10:24 PM
Just for learnability, I was migrating a reaaaaaaaally old toy project/game I did few years ago. I tend to replace the AWT UI by Compose. It's a really simple game, no big deal here. The impl of the logic layer was using Java `Observer`/`Observable`, I try to keep it as is for now (keeping compatibility with old Java/AWT UI layer). How may I recompose what needs to be based on such impl?
I tried:
@Composable
fun GameScreen(rules: GameRules) {
    Column {
        LimitStatus(rules)
        var board by remember { mutableStateOf(rules.board) }

        rules.addObserver { _, arg ->
            if (arg == null) {
                board = rules.board
            }
        }
        GameBoard(board)
    }
}
Idea would be to update the
board
when observable ask me to do so (null
args
here means board update…). I'm not sure I'm 100% aware of the recomposition rules, I would have expected an update here but it doesn't happen (despite my observer is properly called). I guess, since the Java object ref doesn't change (board instance if the same, always coming from the rules object), nothing changes from Compose perspective. Any idea?
(⚠️ I should use
DisposableEffect
to add/remove observer I guess, otherwise I'll have too many of them at some point, but for now, it's never recomposed so…)
1
z

Zach Klippenstein (he/him) [MOD]

04/15/2021, 11:35 PM
You might want to look at the implementation of something like
Flow.collectAsState
to write yourself a generic extension
2
u

uli

04/16/2021, 6:55 AM
Our wrap your observable into a flow
But the underlying issue might be that the board reference does not change. Try assigning rules.board.cone()
o

Olivier Patry

04/16/2021, 8:06 AM
@Zach Klippenstein (he/him) [MOD] I'm not sure I understand how it's done. I checked the impl and I understand that it basically does the same as my snippet (except that it better hides observe/collect logic) 🤔
collectAsState
calls
produceState
which itself is a
remember { mutableStateOf(x) }
calling an effect to implement the observe logic. Did I understand correctly? Here is my updated impl (still not working)
@Composable
inline fun <reified T> GameRules.observeAsState(initialValue: T): State<T> {
    val state = remember { mutableStateOf(initialValue) }
    DisposableEffect(this) {
        val observer = Observer { observable, arg ->
            val rules = observable as GameRules
            when {
                initialValue is Board && arg == null -> {
                    state.value = rules.board.copy() as T
                }
                initialValue is GameRules.Limit && arg is GameRules.Limit -> {
                    state.value = rules.limit as T
                }
                initialValue is GameRules.GameStatus && arg is GameRules.GameStatus -> {
                    state.value = rules.status as T
                }
            }
        }
        addObserver(observer)
        onDispose {
            deleteObserver(observer)
        }
    }
    return state
}
and used as
val board by rules.observeAsState(rules.board)
@uli
copy()
doesn't seem to help, my problem might come from elsewhere
u

uli

04/16/2021, 8:19 AM
Did you already switch to 
DisposableEffect
 to add/remove observer?
Can you verify that your observer is being called?
o

Olivier Patry

04/16/2021, 8:52 AM
Everything is properly called, except the recomposition.
state.value =
is called when it should (following
Observer.update
)
I've also tried another approach, wrapping my legacy
Observable
GameRules
in a
GameRuleState
exposing properties as
mutableStateOf()
. It doesn't change, so I guess my problem comes from something else than state management 🤔, what do you think?
class GameRulesState(private val gameRules: GameRules) {
    val board = mutableStateOf(gameRules.board)
    val limit = mutableStateOf(gameRules.limit)
    val status = mutableStateOf(gameRules.status)
    private val onUpdate = Observer { observable, arg ->
        val rules = observable as GameRules
        when (arg) {
            null -> {
                board.value = rules.board.copy()
            }
            is GameRules.Limit -> {
                limit.value = rules.limit
            }
            is GameRules.GameStatus -> {
                status.value = rules.status
            }
        }
    }

    init {
        gameRules.addObserver(onUpdate)
    }
}
@Composable
fun GameScreen(rules: GameRulesState) {
    Column {
        LimitStatus(rules)
//        val board by rules.observeAsState(rules.board)
        val board = remember { rules.board }
u

uli

04/16/2021, 9:32 AM
That’s how it works on flow:
@Composable
fun <T : R, R> Flow<T>.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
    if (context == EmptyCoroutineContext) {
        collect { value = it }
    } else withContext(context) {
        collect { value = it }
    }
}
No
remember
but
produceState
.
collect {...}
would become your
val observer = Observer {…}
o

Olivier Patry

04/16/2021, 10:04 AM
the
produceState
impl is
@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1, key2) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}
So, I understand this as being similar to the remember call 🤔
----
I managed to have some updates from my
Observable
(primitive types in particular). What remains not recomposed is my "complex" board type
data class Board(val lineCount: Int, val columnCount: Int) {
    val game: Array<Array<Tile>>
...
}
So, I'll dig in that direction
It appears that it came from 2 different issues 😅 1. initially, not using
copy()
so
GameRules.board
being always the same object, modified "in place", the state value comparison always returned true (so nothing to do) 2. even using
copy()
it wasn't working since my
Board
contained a "complex object" initialization (
Array
of
Array
) that wasn't deep copied. So, I implemented a custom
copy()
implementation to deep copy the
Board
data and now it works. It wasn't really linked to recomposition at the end 🙂
👍 1
u

uli

04/16/2021, 6:27 PM
thanks for the update