Jetpack Compose AndroidView call Update - kotlin

Changing the state in the following way can work just fine
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
view.coordinator.selectedItem = selectedItem.value
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// I need to call some methods like below, but I can't get cropLayoutView
// cropLayoutView.flipImageHorizontally()
// cropLayoutView.flipImageVertically()
// cropLayoutView.rotateClockwise()
// val bitmap = cropLayoutView.croppedImage
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
// How to make a call here?
view.setCoordinator()
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state

Related

How to override SnackbarData in Jetpack Compose?

I need a custom parameter in the snackbar, but showSnackbar only allows passing three parameters (message, actionLabel and duration). I need to pass an Enum to decide which status to show.
#Composable
fun BaseScreen(
viewModel: BaseViewModel?,
content: #Composable () -> Unit
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope: CoroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
CustomSnackbar(
message = snackbarData.message,
// I can't get custom parameter
status = snackbarData.status
)
}
)
},
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
viewModel?.showSnackbar = { message ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
// I can't pass custom parameter
status = SnackbarStatusEnum.DANGER
)
}
}
content()
}
}
}
You can use something different.
Use a variable(state) to set the status and use a condition inside the Snackbar to change backgroundColor and actions.
Something like:
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { data ->
// custom snackbar with the custom colors
Snackbar(
backgroundColor = if (status) Color.Red else Color.Yellow,
//contentColor = ...,
snackbarData = data
)
}
},
)

Wait for result from Coroutine and then use it in Composable function

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?
You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

Coroutines not working in jetpack Compose

I use the following way to get network data.
I start a network request in a coroutine but it doesn't work, the pagination load is not called.
But if I call the network request through the init method in the ViewModel I can get the data successfully.
#Composable
fun HomeView() {
val viewModel = hiltViewModel<CountryViewModel>()
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
Scaffold {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 96.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier = Modifier.fillMaxSize()) {
items(pagingItems) { countryGroup ->
if (countryGroup == null) return#items
Text(text = "Hello", modifier = Modifier.height(100.dp))
}
}
}
}
#HiltViewModel
class CountryViewModel #Inject constructor() : ViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
init {
getCountryList() // can work
}
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
}
I don't understand what's the difference between the two calls, why doesn't it work?
Any helpful comments and answers are greatly appreciated.
I solved the problem, the coroutine was used twice in the code above, which caused network data to not be fetched.
A coroutine is used here:
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
Here is another coroutine:
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
current usage:
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
viewModel.getCountryList()
execute.value = false
}

Select all text of TextField in Jetpack Compose

I'm using TextField component in Jetpack Compose.
How to select all text when it receive focus?
In this case you should use TextFieldValue as state of your TextField, and when it receive focus, you set the selection using the TextFieldValue state.
val state = remember {
mutableStateOf(TextFieldValue(""))
}
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
)
Here's the result:
Notice that depending on you're touching the cursor goes to the touched position instead of select the entire text. You can try to figure it out if this is a bug or a feature :)
#nglauber solution doesn't seems to work anymore.
Debugging shows that onFocusChanged is called before onValueChange and within one view life cycle. A selection changed during onFocusChanged has no effect on TextField, since it is overridden during onValueChange.
Here's a possible workaround:
var state by remember {
mutableStateOf(TextFieldValue("1231"))
}
var keepWholeSelection by remember { mutableStateOf(false) }
if (keepWholeSelection) {
// in case onValueChange was not called immediately after onFocusChanged
// the selection will be transferred correctly, so we don't need to redefine it anymore
SideEffect {
keepWholeSelection = false
}
}
TextField(
value = state,
onValueChange = { newState ->
if (keepWholeSelection) {
keepWholeSelection = false
state = newState.copy(
selection = TextRange(0, newState.text.length)
)
} else {
state = newState
}
},
modifier = Modifier
.onFocusChanged { focusState ->
if (focusState.isFocused) {
val text = state.text
state = state.copy(
selection = TextRange(0, text.length)
)
keepWholeSelection = true
}
}
)
I think it should be possible to make it easier, so I created this question on Compose issue tracker.
I didn't have 100% success with #nglauber answer. You should add a small delay and it works great. For example:
val state = remember {
mutableStateOf(TextFieldValue(""))
}
// get coroutine scope from composable
val scope = rememberCoroutineScope()
TextField(
value = state.value,
onValueChange = { text -> state.value = text },
modifier = Modifier
.onFocusChanged {
if (it.hasFocus) {
// start coroutine
scope.launch {
// add your preferred delay
delay(10)
val text = state.value.text
state.value = state.value.copy(
selection = TextRange(0, text.length)
)
}
}
}
)
I wrote a Modifier extension function that works in spite of bug pointed out by #Pylyp
fun Modifier.onFocusSelectAll(textFieldValueState: MutableState<TextFieldValue>): Modifier =
composed(
inspectorInfo = debugInspectorInfo {
name = "textFieldValueState"
properties["textFieldValueState"] = textFieldValueState
}
) {
var triggerEffect by remember {
mutableStateOf<Boolean?>(null)
}
if (triggerEffect != null) {
LaunchedEffect(triggerEffect) {
val tfv = textFieldValueState.value
textFieldValueState.value = tfv.copy(selection = TextRange(0, tfv.text.length))
}
}
Modifier.onFocusChanged { focusState ->
if (focusState.isFocused) {
triggerEffect = triggerEffect?.let { bool ->
!bool
} ?: true
}
}
}
usage
#Composable
fun SelectAllOnFocusDemo() {
var tfvState = remember {
mutableStateOf(TextFieldValue("initial text"))
}
TextField(
modifier = Modifier.onFocusSelectAll(tfvState),
value = tfvState.value,
onValueChange = { tfvState.value = it },
)
}
I want to add to the Phil's answer I wanted to update the state dynamically and I ended up with this:
var state by remember(textVal) {
mutableStateOf(TextFieldValue(text = textVal, selection = TextRange(textVal.length)))
}
It does two things, first it updates the field if your textVal changes, also puts the cursor at the end.

How can I successfully pass my LiveData from my Repository to my Compose UI using a ViewModel?

I am trying to pass live events from a Broadcast Receiver to the title of my Homepage.
I am passing a String from the Broadcast Receiver to my Repository successfully, but in the end my title is always null. What am I missing?
My Repository looks like this:
object Repository {
fun getAndSendData (query: String): String{
return query
}
}
Then in my ViewModel I have:
private val _data = MutableLiveData<String>()
val repoData = _data.switchMap {
liveData {
emit(Repository.getAndSendData(it))
}
}
And finally in my Composable I have:
val repoData = viewModel.repoData.observeAsState()
topBar = {
TopAppBar(
title = { Text(text = if (repoData.value == null)"null" else repoData.value!!, style = typography.body1) },
navigationIcon = {
IconButton(onClick = { scaffoldState.drawerState.open() }) {
Icon(Icons.Rounded.Menu)
}
}
)
},
I don't think we can use Live Data from the compose(jetpack) because it can run from the Main thread. I used onCommit() {} with interface from the compose.
onCommit() {
viewModel.testCountriesList(object : NetworkCallBack {
override fun test(response: GeneralResponse) {
if(response.code == 200) {
counties = response.response
responseState.value = true
} else {
responseState.value = false
}
}
})
}