Calling composable funciton from mouseclickable - kotlin

I need to show a cursorDropDownMenu if there was a click with the secondary button on an Item.
My code which calls the function is the following:
.mouseClickable {
if(buttons.isSecondaryPressed){
showContextmenu()
}else {
//same as in clickable
}
}
my showcontextmenu function is the following:
#Composable
fun showContextmenu(){
println("rightclick detected")
CursorDropdownMenu(expanded = true, onDismissRequest = {/*todo implement? */ }){
DropdownMenuItem({/*onclick: todo get data and forward it to render the gui tree */}){
Text("render")
}
}
}
But the compile error is that #Composable invocations can only happen from the context of a #composable function.
Some help would be very nice.

Use boolean state var to show/hide menu.
var isContextMenuVisible by remember {mutableStateOf(false)}
//...
//your button code
.mouseClickable {
if(buttons.isSecondaryPressed){
isContextMenuVisible = true
}else {
//same as in clickable
}
}
//...
if (isContextMenuVisible) {
ShowContextmenu()
}
//...
And:
#Composable
fun ShowContextmenu(){
println("rightclick detected")
CursorDropdownMenu(expanded = true, onDismissRequest = {isContextMenuVisible = false}){
DropdownMenuItem({/*onclick: todo get data and forward it to render the gui tree */}){
Text("render")
}
}
}

Related

Kotlin: Edit icon dashboard of icons between fragments

I'm trying to figure out the most efficient way to structure this problem..
I'd like to click on the 'EDIT' icon in the dashboard of the MainFragment, display a DialogFragment, allow user to select/deselect up to 5 icons, save the selection, close the DialogFragment, and update the MainFragment.
Should I use MutableLiveData/Observer from a ViewModel? Or is there a better approach? I currently cannot figure out how to use the ViewModel approach correctly...
So far, this is the code I have:
MainFragment: https://i.stack.imgur.com/5fRt2.png
DialogFragment: https://i.stack.imgur.com/ZvW3d.png
ViewModel Class:
class IconDashboardViewModel() : ViewModel(){
var liveDataDashIcons: MutableLiveData<MutableList<String>> = MutableLiveData()
var liveItemData: MutableLiveData<String> = MutableLiveData()
// Observer for live list
fun getLiveDataObserver(): MutableLiveData<MutableList<String>> {
return liveDataDashIcons
}
// Observer for each icon
fun getLiveItemObserver(): MutableLiveData<String> {
return liveItemData
}
// Set icon list
fun setLiveDashIconsList(iconList: MutableLiveData<MutableList<String>>) {
liveDataDashIcons.value = iconList.value
}
// Set data for data
fun setItemData(icon : MutableLiveData<String>) {
liveItemData.value = icon.toString()
}
var iconList = mutableListOf<String>()
}
MainFragment:
private fun populateIconList() : MutableLiveData<MutableList> {
var iconList = viewModel.liveDataDashIcons
// Roster icon
if (roster_dash_layout.visibility == View.VISIBLE) {
iconList.value!!.add(getString(R.string.roster))
} else {
if (iconList.value!!.contains(getString(R.string.roster))) {
iconList.value!!.remove(getString(R.string.roster))
}
}
}
DialogFragment:
private fun setIconList(iconList: MutableList){
var iconList = viewModel.iconList
Log.d(TAG, "viewModel iconList = " + iconList)
if (iconList.contains(getString(R.string.roster))) {
binding.radioButtonRosterPick.setBackgroundResource(R.drawable.icon_helmet_blue_bg)
}
}

Navigate back to previous composable screen Lifecycle.Event.ON_CREATE event call again

My question is that when i navigate back/popup to previous composable screen Lifecycle.Event.ON_CREATE event call again. For example i have two composable screen, first show list of item and send one is detail screen of specific item. When i navigate back to list item screen. List item screen load(network call) again. Below is code test sample
Navigation Logic
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home") {
RememberLifecycleEvent(event = {
Log.i("check","home event")
// API Call
})
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { navController.navigate("blur") }) {
Text(text = "Blur")
}
}
}
composable("blur") {
RememberLifecycleEvent(event = {
Log.i("check","blur event")
})
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { navController.navigate("home") }) {
Text(text = "Home")
}
}
}
}
Lifecycle Event Logic
#Composable
fun RememberLifecycleEvent(
event: () -> Unit,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
) {
val state by rememberUpdatedState(newValue = event)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { owner, event ->
if (event == Lifecycle.Event.ON_CREATE) {
state()
Log.i("check","event = $event")
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
I want to call api only first time in Lifecycle.Event.ON_CREATE event
This is happening because when you navigate from A to B, the onDispose is called in A. Then, when you return to A from B, the DisposableEffect is called again and since the Activity is already in "resumed" state, the ON_CREATE event is sent again.
My suggestion is controlling this call in your view model since it is kept alive after you go to B from A.
There are a few possibilities depending if you want to call the API once on every 'forward' navigation to your first screen or if you want to call the API just once based on some other criteria.
If former, you can either use a ViewModel and call the API from it when the ViewModel is created. If you use Hilt and call hiltViewModel() inside your Composable the ViewModel will be scoped to the lifecycle of the NavBackStackEntry of your NavHost.
But the same scope can also be achieved by simply using a rememberSaveable, since this will use the saveStateHandle from the NavBackStackEntry of your NavHost.
Another advantage is that both of the above options also ensure that the API won't be called again on orientation change and other configuration changes (when they are enabled).
// Just a sample (suspend) call
suspend fun someApi(): String {
// ...
return "some result"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home") {
var apiCalled by rememberSaveable { mutableStateOf(false) }
if (!apiCalled) {
apiCalled = true
// key = Unit is okay here, we only want to launch once when entering the composition
LaunchedEffect(Unit) {
val result = runCatching {
someApi()
}
if (result.isFailure) {
// retry the api call? or report the error
}
}
}
// rest of your code ...
}
}

How to implement javascript interface with Webview, when using Jetpack Compose?

How to implement the WebAppInterface (javascript interface for webview) when using Jetpack Compose?
I'm following this documentation
This is how far I've got so far, but showToast() is not called. Adding #Composable to showToast() didn't help.
/** Instantiate the interface and set the context */
class WebAppInterface(private val mContext: Context) {
/** Show a toast from the web page */
#JavascriptInterface
fun showToast(toast: String) {
Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
}
}
#SuppressLint("SetJavaScriptEnabled")
#Composable
fun WebPageScreen(urlToRender: String) {
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = WebViewClient()
addJavascriptInterface(WebAppInterface(getContext()), "Android")
loadUrl(urlToRender)
}
}, update = {
it.loadUrl(urlToRender)
})
}
HTML/JS code from Android docs:
<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />
<script type="text/javascript">
function showAndroidToast(toast) {
Android.showToast(toast);
}
</script>
You missed one step in the documentation you refer to:
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = WebViewClient()
settings.javaScriptEnabled = true // <-- This line
addJavascriptInterface(WebAppInterface(getContext()), "Android")
}

Any(better) approach to pass compose desktop's `onClick` through `callbackFow`?

I was wondering if it is possible to pass onClik [handling onClick] through callbackFlow as I saw from this this post.
I was having hard time implementing since the onClick Callback was inside parameter also Button is function so wasn`t able to implement extension function
anyways I tried something like
lateinit var buttonListener :Flow<Unit>
fun <T >offers(t: T) = callbackFlow {
offer(t)
awaitClose { null }
}
CoroutineScope(IO).launch {
if(::buttonListener.itInitalized){
buttonListener.collect {
println("it => Kotlin.Unit")
}
}
}
MaterialTheme {
Button(
onClick = {
println("buttonClicked")
buttonListener = offers(Unit)
} //...
) { /** designs */}
}
which is callable only 1 times on every runtime
buttonClicked <--\
Kotlin.Unit => Kotlin.Unit <--/\__first click
buttonClicked
buttonClicked
buttonClicked
yet expecting someting like
buttonClicked
Kotlin.Unit => Kotlin.Unit
buttonClicked
Kotlin.Unit => Kotlin.Unit
buttonClicked
Kotlin.Unit => Kotlin.Unit
You can use coroutine Channel instead of Flow to receive events from outside the coroutine. Then convert it to Flow using consumeAsFlow() method.
Now the flow operators like collect can be called on this Flow.
It can receive multiple onClick events from the button composable.
var buttonListener = Channel<Unit>()
CoroutineScope(Dispatchers.IO).launch {
buttonListener.consumeAsFlow().collect {
Log.d(TAG, "onCreate: $it => Kotlin.Unit")
}
}
MaterialTheme {
Button(
onClick = {
Log.d(TAG, "onCreate: buttonClicked")
buttonListener.offer(Unit)
}
){
Text(text = "Button")
}
}

How to subscribe to StateFlow in kotlin-react useEffect

I'm trying to create a small counter example for kotlin-react with functionalComponent with kotlin 1.4-M2.
The example should use kotlinx.coroutines.flow. I'm struggling at collecting the values from the store in reacts useEffect hook.
Store:
object CounterModel { // Modified sample from kotlin StateFlow doc
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter: StateFlow<Int> get() = _counter // publicly exposed as read-only state flow
fun inc() { _counter.value++ }
}
Component:
val counter = functionalComponent<RProps> {
val (counterState, setCounter) = useState(CounterModel.counter.value)
useEffect(listOf()) {
// This does not work
GlobalScope.launch { CounterModel.counter.collect { setCounter(it) } }
}
div {
h1 {
+"Counter: $counterState"
}
button {
attrs.onClickFunction = { CounterModel.inc() }
}
}
}
When I directly call CounterModel.counter.collect { setCounter(it) } it complains about Suspend function 'collect' should be called only from a coroutine or another suspend function.
How would you implement this useEffect hook?
And once the subscription works, how would you unsubscribe from it (use useEffectWithCleanup instead of useEffect)?
Finally found a solution. We can use onEach to do an action for every new value and then 'subscribe' with launchIn. This returns a job that can be canceled for cleanup:
object CounterStore {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter
fun inc() { _counter.value++ }
}
val welcome = functionalComponent<RProps> {
val (counter, setCounter) = useState(CounterStore.counter.value)
useEffectWithCleanup(listOf()) {
val job = CounterStore.counter.onEach { setCounter(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
div {
+"Counter: $counter"
}
button {
attrs.onClickFunction = { CounterStore.inc() }
+"Increment"
}
}
We can extract this StateFlow logic to a custom react hook:
fun <T> useStateFlow(flow: StateFlow<T>): T {
val (state, setState) = useState(flow.value)
useEffectWithCleanup(listOf()) {
val job = flow.onEach { setState(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
return state
}
And use it like this in our component:
val counter = useStateFlow(CounterStore.counter)
The complete project can be found here.
The Flow-Api is very experimental so this might not be the final solution :)
if's very important to check that the value hasn't changed,
before calling setState, otherwise the rendering happens twice
external interface ViewModelProps : RProps {
var viewModel : MyViewModel
}
val App = functionalComponent<ViewModelProps> { props ->
val model = props.viewModel
val (state, setState) = useState(model.stateFlow.value)
useEffectWithCleanup {
val job = model.stateFlow.onEach {
if (it != state) {
setState(it)
}
}.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
}