How to add a button to the view dynamically? - kotlin

I'm new to Kotlin and TorandoFX. Maybe I'm missing something very basic in TornadoFX. I want to create from a list (which shoulde be mutable) buttons in the view. If the user clicks on the add button the list should get a new item and this should result in a new button in the view.
Thank you for your help.
I was thinking it should look like this:
import tornadofx.*
fun main(args: Array<String>) {
launch<MyApp>(args)
}
class MyApp: App(MainView::class)
class MainView: View("MainView") {
val values = ArrayList<Int>(listOf(1,2,3)).asObservable()
var count = 4
override val root = vbox {
values.forEach { x ->
button(x.toString())
}
button("add") {
action {
values.add(count)
println(values.toString())
count++
}
}
}
}
this code result in this view, but if I click the button the view doesnt refresh.
This code result in this view, but if I click the button the view doesnt refresh. I think I'm missing something about binding.

We figured out, I was right with the binding part. I just had to use bindChildren() function and give the function an observableArray and a function to for the conversion of the elements of the array as a parameter.
Thank you for the help.
import javafx.collections.FXCollections
import tornadofx.*
fun main(args: Array<String>) {
launch<MyApp>(args)
}
class MyApp: App(MainView::class)
class MainView: View("MainView") {
val values = FXCollections.observableArrayList<Int>(listOf(1,2,3))
var count = 4
override val root = vbox {
vbox {
bindChildren(values) { x ->
button(x.toString())
}
}
vbox() {
button("add") {
action {
values.add(count)
count++
}
}
}
}
}

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)
}
}

Jetpack Compose not updating / recomposing Flow List Values from Room DB when DB Data is getting changed

I'm trying to show a List of Items in my Android App. I'm using Jetpack Compose, Flows and RoomDB.
When launching the Activity all Items are shown without any problems, the Flow get's items collected and they are displayed.
But when I change some properties of the Item in the Database, the changes are not displayed. In my case I change the item to deleted, but it's still displayed as not deleted.
When I look at the Database Inspector, the value is changed in the database and set to deleted.
When I log collecting the flow, the change is getting emitted (It says the Item is set to deleted)
But Jetpack Compose is not recomposing the change.
If I delete an element from / add an element to the List (in the DB) the UI gets updated and recomposed.
So I can only assume that the problem must lie in the recomposition or handling of the flow.
Here my Code:
My Activity:
#AndroidEntryPoint
class StockTakingHistoryActivity : ComponentActivity() {
private val viewModel: StockTakingHistoryViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.stockList = ...
setContent {
LaunchedEffect(Unit) {
viewModel.getStockListItems(viewModel.stockList!!.uuid)
}
Surface(color = MaterialTheme.colors.background) {
Content(viewModel.stockListItems)
}
}
}
}
...
#Composable
private fun Content(items: List<StockListItem>) {
...
LazyColumn {
items(items) { item ->
HistoryItem(stockListItem = item)
}
}
}
}
...
#Composable
private fun HistoryItem(stockListItem: StockListItem) {
...
Text(text = stockListItem.deleted)
...
Button(onClick = {
viewModel.deleteItem(stockListItem)
}) {
Text(text = "Set to deleted!")
}
}
}
My ViewModel:
var stockListItems by mutableStateOf(emptyList<StockListItem>())
fun getStockListItems(uuid: String) {
viewModelScope.launch {
stockListItemRepository.findByUUID(uuid).collect { items ->
Log.d("StockTakingHistoryViewModel", "items changed! ${items.map { it.deleted }}")
stockListItems = items
}
}
}
fun deleteItem(stockListItem: StockListItem) {
viewModelScope.launch(Dispatchers.IO) {
stockListItemRepo.update(item.copy(deleted = true);
}
}
The Repository:
fun findByUUID(uuid: String): Flow<List<StockListItem>> {
return dao.findByUUID(uuid)
}
The Dao behind the Repository Request:
#Query("select * from StockListItem where stockListUUID = :uuid order by createdAt desc limit 30")
fun findByUUID(uuid: String): Flow<List<StockListItem>>
I would be very happy if someone could help me! Thank you!
Considering you can collect a flow as state (via collectAsState) I'd consider going that route for getting the list rather than calling collect in the viewModel and updating the stockListItems as there are fewer moving parts for things to go wrong.
For example something like the following:
setContent {
val stockListItems = viewModel.getStockListItemsFlow(uuid).collectAsState(initial = emptyList())
Surface(color = MaterialTheme.colors.background) {
Content(stockListItems)
}
}
Found the Problem: The equals() method of StockListItem only compared the primary key.

Why is data being shown when screen rotates in jetpack compose

I'm facing this issue where the data I'm retrieving from an API, https://randomuser.me/api/ at first compose it doesn't load.
But every time I rotate the screen the data updates.
First load
After screen rotation
View
class MainActivity : ComponentActivity() {
private val userViewModel : UserViewModel by viewModels()
private var userList: List<UserModel> = listOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userViewModel.userModel.observe(this, Observer {
userList = it
})
userViewModel.onCreate()
setContent {
ListUsers(userList = userList)
}
}
}
ViewModel
class UserViewModel : ViewModel() {
val userModel = MutableLiveData<List<UserModel>>()
var getRandomUsersUseCase = RandomUsersUseCase()
fun onCreate() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
}
Use State to ensure the data changes trigger recomposition of the Composable.
If you use another observable type such as LiveData in Compose, you
should convert it to State before reading it in a composable using
a composable extension function like LiveData.observeAsState().
Changes to your code would be,
val userListState by userViewModel.userModel.observeAsState()
setContent {
ListUsers(userList = userListState)
}
Why does it shows the data during rotation?
When rotating the screen or during any other configuration changes, the activity will be recreated.
More info on that here - Docs
In most cases, you would not require data to be changed when the screen rotates.
If you want to persist the data even after screen rotation, move the code inside onCreate() in your UserViewModel to the init block, like this.
init {
getData()
}
fun getData() {
viewModelScope.launch {
val result = getRandomUsersUseCase()
if(!result.isNullOrEmpty()){
userModel.postValue(result)
}
}
}
If you need to refresh the data on any other event like button click, swipe to refresh, etc, just call the getData() again on the event handler.
P.S: Check correct imports are added as required.
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue

How to make a search field in Workspace header?

As proposed in the tutorial, I've added a SearchView to Workspace header:
class AppWorkspace : Workspace() {
init {
add(SearchView::class)
}
}
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen { searchable }
}
}
So I need a property to enable that field in certain views:
val searchable = SimpleBooleanProperty(false)
Where I should define it and how to access it?
And how to implement onSearch or something?
I would suggest creating a Searchable interface:
interface Searchable {
fun onSearch(query: String)
}
Views that should enable the search field when they are docked would implement this interface. You can bind the enabled state of the search input by creating an observable boolean value to check for this:
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
Then you can make sure the search field action forwards to the onSearch function in the Searchable currently docked:
action {
(workspace.dockedComponent as Searchable).onSearch(text)
}
Now just implement onSearch in your Searchable view classes and your good to go. You could also clear the search field with an event:
object ClearSearch : FXEvent()
Listen to this event inside your search field:
subscribe<ClearSearch> { clear() }
Now your views can fire this event if they want to clear the search field:
override fun onSearch(query: String) {
println("Searching for $query...")
fire(ClearSearch)
}
For completeness, here is the SearchView:
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
action {
(workspace.dockedComponent as Searchable).onSearch(text)
}
subscribe<ClearSearch> { clear() }
}
}
And here is a class implementing Searchable:
class Editor1 : View("Editor 1"), Searchable {
override val root = borderpane {
center {
label("Nothing here yet")
}
}
override fun onSearch(query: String) {
println("Searching for $query...")
fire(ClearSearch)
}
}
Hope this helps :)
Following Edvin answer I get:
class MyApp: App(MyWorkspace::class)
class MyApp2: App(View3::class){
init{
FX.defaultWorkspace = MyWorkspace::class
}
}
class MyWorkspace: Workspace("My Workspace") {
override fun onDock() {
add(SearchView::class) //With this search view gets enable
}
init {
//add(SearchView::class) //With this search view doesn't get enable
root.setPrefSize(800.0, 600.0)
menubar {
menu("Windows"){
item("View1").action{dock<View1>()}
item("View2").action{dock<View2>()}
}
}
}
}
interface Searchable {
fun onSearch(consulta: String)
}
class SearchView : View() {
override val root = textfield {
promptText = "search"
enableWhen(workspace.dockedComponentProperty.booleanBinding { it is Searchable })
action {
(workspace.dockedComponent as? Searchable)?.onSearch(text)
}
subscribe<ClearSearch> { clear() }
}
}
object ClearSearch : FXEvent()
class View1 : View(), Searchable{
override val root = label("This is View 1")
override fun onSearch(consulta: String) {
println("I'm searching")
}
}
class View2 : View(){
override val root = label("This is View 2")
}
class View3 : View(){
override val root = button("Open Workspace"){
action{
workspace.openWindow()
}
}
}
The thing is when I put add(SearchView::class) in the init{} section and dock a Searchable view, the Search textfield doesn't get enable, if a put it in the onDock function it does get enable.
But if I open MyWorkspace from MyApp2, close it and open it again I get a error, since onDock is called again and try to add(SearchView::class) one more time. What the solution here so I can open MyWorkspace from some other window without any trouble?

TornadoFX key press listener issues

When I run the following code
fun main(args: Array<String>) {
Application.launch(HelloWorldApp::class.java, *args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
addEventFilter(KeyEvent.ANY) { event ->
println("pressed:"+event.character)
}
}
}
When I press any keys on my keyboard the println() is never called. Am I missing something?
Simply adding an HBox does not give it focus, and when it doesn't have focus it won't receive key events. You should override onDock and add the listener to the currentScene instead. If you really need to add the listener on the HBox, add the listener and request focus once the view has been docked:
fun main(args: Array<String>) {
launch<HelloWorldApp>(args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
addEventFilter(KeyEvent.ANY) { event ->
println("pressed:" + event.character)
}
}
override fun onDock() {
root.requestFocus()
}
}
Looking for a similar problem I came up with this, which looks simpler, but I do not yet understand any possible subtle differences between using the keyboard control versus explicit focus requesting.
import javafx.scene.input.KeyEvent
import tornadofx.*
fun main(args: Array<String>) {
launch<HelloWorldApp>(args)
}
class HelloWorldApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = hbox {
keyboard {
addEventHandler(KeyEvent.KEY_PRESSED) { println(it.code) }
}
}
}