In this post I will show a couple of samples of how to use a Flow to update UI in Android Compose.
StateFlows have always been by default go to when dealing with state and data updates. I started to do some research on how to more efficiently create a way to address API called and UI updates. I saw how Flows were mostly used in this process so I decided to start working with them.
Out of the box, I didn't want to dive into a Retrofit project to start learning how to use a flow to update UI. The best way I found to learn new implementations, when possible, is the base UI. Meaning text, buttons and colors, keeping it as simple as possible to learn.
I derived the below code from the following post that explains a bit more and has a better working example.
https://www.geeksforgeeks.org/kotlin-flow-in-android-with-example
My code below is kind of sloppy. I didn't organize the different classes. I put them all in one file and a separate for the ViewModel. To make it more neat, you may be better to separate the different classes.
Code
ColorApiState
This is the working functionality of the Flow. The design is to change the screen color and text every sec.
data class BGColor (
val color : Color?,
val message : String?
)
data class ColorApiState<out T>(val status: Status, val data: T?) {
companion object {
fun <T> success(data: T?): ColorApiState<T> {
return ColorApiState(Status.SUCCESS, data)
}
fun <T> loading(data: T?): ColorApiState<T> {
return ColorApiState(Status.LOADING, data)
}
fun <T> error(data: T?): ColorApiState<T> {
return ColorApiState(Status.ERROR, data)
}
}
}
enum class Status {
SUCCESS,
LOADING,
ERROR
}
class ApiTest {
private val list = listOf(Color.Red,Color.Blue,Color.Black,Color.White,Color.Yellow)
fun getColor() : BGColor {
return BGColor(list[Random.nextInt(0, list.size)], "")
}
}
class ColorRepository(private val apiTest: ApiTest) {
suspend fun getColor(): Flow<ColorApiState<BGColor>> {
return flow {
val color = apiTest.getColor()
Log.d("ColorAPiState", "${color.color}")
when(color.color){
Color.White -> { emit(ColorApiState.loading(color.copy(message = "Loading"))) }
Color.Black -> { emit(ColorApiState.error(color.copy(message = "Error"))) }
Color.Red -> { emit(ColorApiState.error(color.copy(message = "Error")))}
Color.Blue -> { emit(ColorApiState.success(color.copy(message = "Success"))) }
Color.Yellow -> { emit(ColorApiState.success(color.copy(message = "Success"))) }
}
}.flowOn(Dispatchers.IO)
}
}
ViewModel
Now the ViewModel. Here I will post the entire file.
class ViewModel : ViewModel() {
private val colorRepository = ColorRepository(ApiTest())
val colorState = MutableStateFlow(
ColorApiState(
Status.LOADING,
BGColor(null, null))
)
init {
getColor()
}
fun getColor(){
viewModelScope.launch {
while(true) {
colorRepository.getColor()
.collect {
Log.d("ViewModel", "Setting: ${it.data?.color}")
when(it.status){
Status.SUCCESS -> {
colorState.value = ColorApiState.success(
it.data
)
}
Status.LOADING -> {
colorState.value = ColorApiState.loading(
it.data
)
}
Status.ERROR -> {
colorState.value = ColorApiState.error(
it.data
)
}
}
}
Log.d("ColorAPiState", "Looping...")
delay(1000)
}
}
}
}
class ViewModelFactory() : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if(modelClass.isAssignableFrom(com.itgeek25.sampleflows.viewmodel.ViewModel::class.java)){
com.itgeek25.sampleflows.viewmodel.ViewModel() as T
} else {
throw IllegalArgumentException("ViewModel Not Found!!")
}
}
}
MainActivity
Finally the MainActivity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val factory = ViewModelFactory()
val viewModel = ViewModelProvider(
this,
factory
)[ViewModel::class.java]
setContent {
var color by remember { mutableStateOf(BGColor(Color.White, "Hello")) }
LaunchedEffect(Unit) {
viewModel.colorState.collect {
it.data?.let {
color = it
Log.i("In Activity", "$color")
}
}
}
SampleFlowsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Test(
modifier = Modifier.padding(innerPadding),
color
)
}
}
}
}
}
@Composable
fun Test(
modifier: Modifier,
color: BGColor?
) {
val newColor = if(color != null) color.color else Color.Yellow
val message = if(color != null) color.message else "Problem..."
Column(
modifier = modifier
.fillMaxSize()
.background(newColor!!),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = message!!, color = if(newColor == Color.Black) Color.Red else Color.Black, fontSize = 50.sp)
}
}
Simple, short and sweet. Hope this helps and gives better context of implementation. It helped me laying it out as simple as possible for practice without needing to add in any network operations for now.

BONUS
Below I decided to post a more practical example actually using Retrofit.
The project I started to build this from way a JWT project so disregard the Token naming convention. You can name it whatever you want.
The code will be a bit sloppy but it gets the point across and looks very easy to read. Hope it is useful for the next person.
Token
TokenRepository | TokenApiState | TokenStatus | Token
This is the Token classes, sloppy but I sandwiched it all together. Separating it could help with better readability.
class TokenRepository(val context : Context){
suspend fun getJWT() : Flow<TokenApiState<Any>> {
return flow {
emit(TokenApiState.loading("Starting to load..."))
delay(300)
val service = RetrofitHelper.getInstance(context).create(AccessApi::class.java)
val response : Response<Token> = service.altertestJWT("Taco Tuesday is now on Wednesday!")
if(response.isSuccessful){
val result = response.body()
if(result?.token == null){
emit(TokenApiState.expired("Sloppy coding"))
} else {
emit(TokenApiState.success(result))
}
} else {
emit(TokenApiState.expired("Sloppy coding"))
}
}.flowOn(Dispatchers.IO)
}
}
data class TokenApiState<out T>(val status : TokenStatus, val data : T?) {
companion object {
fun <T> success(data: T?): TokenApiState<T> {
return TokenApiState(TokenStatus.VALID, data)
}
fun <T> loading(message : T?): TokenApiState<T> {
return TokenApiState(TokenStatus.LOADING, message)
}
fun <T> expired(message : T?): TokenApiState<T> {
return TokenApiState(TokenStatus.EXPIRED, message)
}
}
}
enum class TokenStatus {
LOADING,
VALID,
EXPIRED
}
data class Token (
val token : String?
)
AccessAPI
Now in the AccessAPI, again I was starting a JWT project so disregard naming convention. The $Header call is important on the PHP server side though.
interface AccessApi {
@POST("test_jwt.php")
suspend fun altertestJWT(@Header("Authorization") message : String): Response<Token>
}
ViewModel
Now the ViewModel.
class ViewModel(val applicationContext : Context) : ViewModel() {
private val tokenRepository = TokenRepository(applicationContext)
val tokenState = MutableStateFlow(
TokenApiState<Token>(TokenStatus.LOADING, Token(null))
)
fun getToken(){
viewModelScope.launch {
tokenRepository.getJWT().collect{
Log.i("Collect", it.status.name + " : " + it.data)
when(it.status){
TokenStatus.VALID -> { tokenState.value = TokenApiState.success(it.data as Token) }
TokenStatus.LOADING -> { tokenState.value = TokenApiState.loading(Token(it.data as String)) }
TokenStatus.EXPIRED -> { tokenState.value = TokenApiState.expired(Token(it.data as String)) }
}
}
}
}
MainActivity
Finally a simple UI, terrible looking but functions.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val factory = ViewModelFactory(applicationContext)
val viewModel = ViewModelProvider(this, factory)[ViewModel::class.java]
enableEdgeToEdge()
setContent {
SampleJWTTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
var token by remember { mutableStateOf(Token("")) }
LaunchedEffect(Unit) {
viewModel.tokenState.collect {
it.data?.let {
token = it
Log.i("In Activity", "$token")
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.background(Color.Gray),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
token.token?.let { Text(text = it) }
Button(onClick = {
viewModel.getToken()
}) {
Text(text = "Update")
}
}
}
}
}
}
}
PHP
This is what I have on the PHP side to receive the call and post results.
<?php
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
echo json_encode(array(
'token' => $_SERVER["HTTP_AUTHORIZATION"] // or type in a message like ---> "Type in your message here"
));
exit;
} else {
echo json_encode(array(
'id' => 1,
'message' => "Problem"
));
}
?>