How to implement the MutableState interface in a custom class / make object observable for Jetpack Compose?
Asked Answered
S

1

6

I have a more complex object that I want to be observable for Jetpack Compose. However I can't use

// in my view model
var observableProperty by mutableStateOf(myObject)

because I need myObject to keep it's reference. So my plan is to use

// in my view model
var observableProperty = myObject

and make myObject itself implement the MutableState interface. However I don't understand how one can implement this in a custom class, especially how to raise a change notification to the observer (Jetpack Compose)?

Reduced example of what I currently have:

class MyComplexObject(){
    private val map = mutableMapOf<String,Any>()

    fun setValue(key: String, value: Any) {
        map[key] = value
        
        // how to notify about a change? <================
    }

    fun getValue(key: String) : Any? {
        return map[key]
    }
}
Stannary answered 22/8, 2022 at 19:38 Comment(2)
This question is already present at : https://mcmap.net/q/1777304/-create-custom-mutablestate-lt-t-gt-holders/15880865Moller
hmm, the linked question doesn't have an answer and it's also not that much focused as mine I think, where I have included a code example to make the issue more clear. So I hope that's a good reason for my own question.Stannary
M
6

You don't have to make your complex object implement MutableState. You can create a complex object that contains mutableStates and other objects that doesn't need to trigger recomposition on change. It's a common pattern as in JetSnack app.

@Stable
class SearchState(
    query: TextFieldValue,
    focused: Boolean,
    searching: Boolean,
    categories: List<SearchCategoryCollection>,
    suggestions: List<SearchSuggestionGroup>,
    filters: List<Filter>,
    searchResults: List<Snack>
) {
    var query by mutableStateOf(query)
    var focused by mutableStateOf(focused)
    var searching by mutableStateOf(searching)
    var categories by mutableStateOf(categories)
    var suggestions by mutableStateOf(suggestions)
    var filters by mutableStateOf(filters)
    var searchResults by mutableStateOf(searchResults)
    val searchDisplay: SearchDisplay
        get() = when {
            !focused && query.text.isEmpty() -> SearchDisplay.Categories
            focused && query.text.isEmpty() -> SearchDisplay.Suggestions
            searchResults.isEmpty() -> SearchDisplay.NoResults
            else -> SearchDisplay.Results
        }
}

And you wrap this object with remember to prevent to not instantiate on each recomposition

@Composable
private fun rememberSearchState(
    query: TextFieldValue = TextFieldValue(""),
    focused: Boolean = false,
    searching: Boolean = false,
    categories: List<SearchCategoryCollection> = SearchRepo.getCategories(),
    suggestions: List<SearchSuggestionGroup> = SearchRepo.getSuggestions(),
    filters: List<Filter> = SnackRepo.getFilters(),
    searchResults: List<Snack> = emptyList()
): SearchState {
    return remember {
        SearchState(
            query = query,
            focused = focused,
            searching = searching,
            categories = categories,
            suggestions = suggestions,
            filters = filters,
            searchResults = searchResults
        )
    }
}

Any changes in any of the MutableStates will trigger recomposition in Composable scopes these values are read.

remember functions that are commonly used such as rememberScrollState and some others also use this approach either.

@Stable
class ScrollState(initial: Int) : ScrollableState {

    /**
     * current scroll position value in pixels
     */
    var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
        private set

    /**
     * maximum bound for [value], or [Int.MAX_VALUE] if still unknown
     */
    var maxValue: Int
        get() = _maxValueState.value
        internal set(newMax) {
            _maxValueState.value = newMax
            if (value > newMax) {
                value = newMax
            }
        }

    /**
     * [InteractionSource] that will be used to dispatch drag events when this
     * list is being dragged. If you want to know whether the fling (or smooth scroll) is in
     * progress, use [isScrollInProgress].
     */
    val interactionSource: InteractionSource get() = internalInteractionSource

    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()

    private var _maxValueState = mutableStateOf(Int.MAX_VALUE, structuralEqualityPolicy())
 // Rest of the code
}
Monocarpic answered 22/8, 2022 at 20:5 Comment(3)
thank you very much for that reply and pattern idea. Yet I would prefer to directly implement the interface (for syntactic sugar reasons an ducriousity how it can be done)Stannary
I posted this as an alternative. Another alternative would be overriding structuralEqualityPolicy to determine how recomposition should be triggered instead of using default one which triggers recomposition when new instance is set as value of MutableState. For an example how to implement MutableState you can check out SnapshotMutableStateImpl in source code.Monocarpic
Unfortunately, trying to put MutableStates to my class properties, I get "Type 'MutableState<TypeVariable(T)>' has no method 'getValue(RowsOfWords, KProperty<*>)' and thus it cannot serve as a delegate"Libation

© 2022 - 2024 — McMap. All rights reserved.