Hi folks, any recommendations to handle state on c...
# compose-ios
m
Hi folks, any recommendations to handle state on compose ios using flow? I've tried with both collectAsState and mutableStateOf and I don't see changes after the first try.
p
Shouldn't be a difference between platforms, collectAsState and mutableStateOf should work with no difference. Can you post the code, maybe there are missing details
m
Copy code
@Composable
fun Render(viewModel: MLSearchViewModel = MLSearchViewModel()){
    val time = getTimeMillis()
    val name = "start_${time}"
    EventTracer.instance.trace(name, "mainview", time)
    val searchTerms = remember { mutableStateOf<String>("") }
    val searchResult = viewModel.searchStateFlow.collectAsState()

    if(searchResult.value.success && searchTerms.value.isNotBlank()){
        EventTracer.instance.trace("search_${searchTerms.value}", "mainview", getTimeMillis())
    }

    MainView.MyApp( {
        if(it.isNotBlank()){
            EventTracer.instance.trace("search_$it", "mainview", getTimeMillis())
            viewModel.loadSearch(it)
        }
        searchTerms.value = it
    }, {
        MainView.ItemList(searchResult.value.data.results)
    })
    EventTracer.instance.trace(name, "mainview", getTimeMillis())
}
MyApp just renders an empty scaffold with a search bar:
Copy code
@Composable
fun MyApp(onSearch: (String) -> Unit, content: @Composable () -> Unit) {
    MaterialTheme {
        Scaffold(
            topBar = {
                TopBarWithSearch(onSearch)
            }
        ) { innerPadding ->
            content()
        }
    }
}
search bar:
Copy code
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TopBarWithSearch(onSearch: (String) -> Unit) {
    var text by remember { mutableStateOf("") }
    val keyboardController = LocalSoftwareKeyboardController.current
    val focusManager = LocalFocusManager.current
    TopAppBar(
        title = { Text("My App") },
        navigationIcon = {
            IconButton(onClick = {
                EventTracer.instance.write()
            }) {
                Icon(<http://Icons.Filled.Menu|Icons.Filled.Menu>, contentDescription = "Navigation Drawer Button")
            }
        },
        actions = {
            TextField(
                value = text,
                onValueChange = { text = it },
                label = { Text("Search") },
                leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
                modifier = Modifier.fillMaxWidth(),
                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
                keyboardActions = KeyboardActions(
                    onSearch = {
                        onSearch(text)
                        keyboardController?.hide()
                        focusManager.clearFocus()
                    }
                )
            )
        }
    )
}
Item list is a stateless lazy list:
Copy code
@Composable
fun ItemList(mlItems : List<Results>){
    LazyColumn(modifier = Modifier.padding(4.dp)) {
        items(mlItems, key = {it.id!!}){ item ->
            val bookmarked = remember { mutableStateOf(false) }
            Card(
                shape = RoundedCornerShape(4.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(4.dp)
            ) {
                val time = getTimeMillis()

                val args = mutableMapOf<String, String>().apply {
                    item.id?.let {
                        this["itemId"] = it
                    }
                }

                val safeId = if(item.id != null){
                    item.id!!
                } else {
                    ""
                }

                val categories = mutableListOf<String>("mainview", safeId)

                EventTracer.instance.trace("render${item.id}_$time", categories, time, 0, 0, args)
                Row(verticalAlignment = Alignment.CenterVertically) {
                        Box(modifier = Modifier.size(120.dp)) {

                        val firstImage = if(item.thumbnail != null){
                            item.thumbnail
                        } else {
                            ""
                        }
                        ImageLoader.getInstance()!!.load(firstImage)
                        FloatingActionButton(
                            onClick = {
                                bookmarked.value = !bookmarked.value
                            },
                            modifier = Modifier.align(Alignment.TopEnd).then(Modifier.size(20.dp, 20.dp))
                        ) {
                            if(bookmarked.value){
                                Icon(Icons.Filled.Favorite, contentDescription = null)
                            } else {
                                Icon(Icons.Outlined.FavoriteBorder, contentDescription = null)
                            }
                        }
                    }
                    Column(modifier = Modifier.padding(start = 8.dp)) {
                        Text(text = "${item.title}", style = MaterialTheme.typography.body1)
                        Spacer(modifier = Modifier.height(3.dp))
                        Text(text = "${item.price}", style = MaterialTheme.typography.h6)
                        Spacer(modifier = Modifier.height(2.dp))
                        Text(text = "${item.address?.cityName}", style = MaterialTheme.typography.body2)
                        Spacer(modifier = Modifier.height(1.dp))
                        Text(text = "${item.condition}", style = MaterialTheme.typography.body2)
                        Spacer(Modifier.weight(1f))
                        Text(text = "${item.installments?.amount} X ${item.installments?.quantity}", style = MaterialTheme.typography.body1)
                    }
                }
                EventTracer.instance.trace("render${item.id}_$time", categories, getTimeMillis(), 0 ,0, args)
            }
        }
    }
}
Copy code
ImageLoader.getInstance()!!.load(firstImage)
return an Image after downloading & creating an ImageBitmap
Copy code
@Composable
actual fun loadNetwork(imageUri: String, modifier: Modifier) {
    val bitmapState: MutableState<LoadedFile?> = remember { mutableStateOf(null) }
    if(bitmapState.value?.isAddressEqual(imageUri) != true){
        jobScope.getScope().launch {
            NSURL.URLWithString(imageUri)?.let { url ->
                val request = NSMutableURLRequest.requestWithURL(url)
                request.setHTTPMethod("GET")
                val task = NSURLSession.sharedSession.dataTaskWithRequest(request) { data , res, err ->
                    jobScope.getScope().launch {
                        data?.let {
                            val image = UIImage(it).toImageBitmap()
                            withContext(Dispatchers.Main) {
                                bitmapState.value = LoadedFile(imageUri).apply {
                                    this.bitmap = image
                                }
                            }
                        }
                    }
                }
                task.resume()
            }
        }
    }
    if(bitmapState.value?.bitmap!=null){
        Image(
            bitmap = bitmapState.value!!.bitmap!!,
            contentDescription = null,
            contentScale = ContentScale.Fit,
            modifier = modifier.then(Modifier.fillMaxSize())
        )
    }
}
I see the list render the first time I search, but then nothing. The flow is emitting, and the same code works on android. The only differences are the image loading process and the viewmodel base class
My guess right now would be that I need something else besides the flow to turn the ios viewmodel equivalent into an observable
p
Instead of
Copy code
@Composable
Render (viewModel: MLSearchViewModel = MLSearchViewModel())
try
Copy code
@Composable
Render (viewModel: MLSearchViewModel)
I don't see where you call Render() but you should host the ViewModel outside otherwise you will be creating a new instance on each recomposition.