Search code examples
androidobservableandroid-jetpack-compose

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


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]
    }
}

Solution

  • 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
    }