Search code examples
androidkotlinmvvmandroid-jetpack-composektor-client

Jetpack Compose: Displaying data in compose using MVVM


Need a bit of help on why data from viewmodel is not shown in the composable function MainContent. I tried to use MVVM style with coroutine but without DI which I think will be easier but somehow, I could not get it to work.

The viewmodel is working as the log.d is showing the correct data from server but somehow, I could not get it to display in

Text(text = viewModel.posts[it].phrase)

Any help will be greatly appreciated. The github link for this program is in https://github.com/somaria/LearnChnCompose

package com.gamecrawl.learnchncompose

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import com.gamecrawl.learnchncompose.ui.theme.LearnChnComposeTheme
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel: MainViewModel by viewModels()

        setContent {
            LearnChnComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainContent(viewModel)
                }
            }
        }
    }
}

class MainViewModel : ViewModel() {

    private var _posts = mutableListOf(Post("12", "test phrase", true))
    var posts get() = _posts; set(value) {
        _posts = value
    }

    init {
        CoroutineScope(Dispatchers.IO).launch {

            _posts = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
                header("Content-Type", "application/json")
            }

            Log.d("HomeViewModel", "init: ${_posts[1].phrase}")
            Log.d("HomeViewModel", "init: ${_posts[1].id}")

        }
    }

    fun addPost(post: Post) {
        CoroutineScope(Dispatchers.IO).launch {

            val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
                header("Content-Type", "application/json")
                body = post
            }
        }
    }


}

@Composable
fun MainContent(viewModel: MainViewModel) {


    Column {

        LazyColumn {
            items(viewModel.posts.size) {
                Text(text = viewModel.posts[it].phrase)
            }
        }
        Button(onClick = {
            viewModel.addPost(Post("test", "adding post 222", true))
        }) {
            Text(text = "Add Post")
        }
    }
}

@Serializable
data class Post(
    val id: String,
    val phrase: String,
    val published: Boolean
)


object KtorClient {

    val json = Json {
        encodeDefaults = true
        ignoreUnknownKeys = true
        isLenient = true
    }


    val httpClient = HttpClient(Android) {

        install(HttpTimeout) {
            socketTimeoutMillis = 200000
            requestTimeoutMillis = 200000
            connectTimeoutMillis = 200000
        }

        install(Logging) {

            logger = object : Logger {
                override fun log(message: String) {
                    Log.d("TAG", "log: $message")
                }
            }

        }

        install(JsonFeature) {
            serializer = KotlinxSerializer(json)
        }

        defaultRequest {
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
        }

    }

}

Solution

  • The data type of the posts is a MutableList<Post>. This means that changes to this variable will not cause the function to recompose. When the UI is loaded, then the variable does not have any data, since you fetch the data in an asynchronous coroutine. However, when the variable is updated, the UI is not recomposed.

    To fix this issue, you must declare _posts to be a MutableState<List<Post>> from the compose library instead. Reconfigure your ViewModel in the following way:

    import androidx.compose.runtime.State
    import androidx.compose.runtime.mutableStateOf
    import androidx.lifecycle.ViewModel
    
    class MainViewModel : ViewModel() {
        private val _posts = mutableStateOf(listOf<Post>()) // <- requires init value
        val posts: State<List<Post>> = _posts // <- keep both variables immutable 'val'
        /* always expose the immutable form of State */
    
        init {
            CoroutineScope(Dispatchers.IO).launch {
                /* _posts.value is used now due to the datatype change */
                _posts.value = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
                    header("Content-Type", "application/json")
                }
    
                Log.d("HomeViewModel", "init: ${_posts.value[1].phrase}")
                Log.d("HomeViewModel", "init: ${_posts.value[1].id}")
            }
        }
    
        fun addPost(post: Post) {
            CoroutineScope(Dispatchers.IO).launch {
                val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
                    header("Content-Type", "application/json")
                    body = post
                }
            }
        }
    }
    

    Now since your public posts variable is of type State<T>, you need to make changes to your composable function:

    @Composable
    fun MainContent(viewModel: MainViewModel) {
        val posts = viewModel.posts.value // <- grab the value of the state variable.
        /* The function will recompose whenever there's a change in posts */
        Column {
            LazyColumn {
                items(posts.size) {
                    Text(text = posts[it].phrase)
                }
            }
            Button(onClick = {
                viewModel.addPost(Post("test", "adding post 222", true))
            }) {
                Text(text = "Add Post")
            }
        }
    }
    

    This should help your issue.