fragment for flowpane in TornadoFX - tornadofx

The TornadoFX docs describe using the ListCellFragment to bind each cell in a list control to each item model in a list. I am wondering how to do something similar in a flowpane. I'd like to use that kind of class to render a bunch of controls and an SVG drawing in each cell. (So it would replace the button component in the example code below and somehow bind the shapeItem model to it).
class LibraryView : View("Current Library") {
val shapeLibraryViewModel : LibraryViewModel by inject()
override val root = anchorpane{
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty){
shapeItem -> button(shapeItem.nameProperty)
}
}
}
}
Since I don't see a pre-made class like the one for list view, perhaps I would need to create something similar to it...or maybe there's a more lightweight approach?

Using an ItemFragment is a bit overkill, since the itemProperty of the fragment will never change (a new fragment would be created for every item whenever the libraryItemsProperty change. However, if your view logic for each item is substantial, this approach will provide a clean way to separate and contain that logic, so it might be worth it. Here is a complete example you can use as a starting point.
class ShapeItemFragment : ItemFragment<ShapeItem>() {
val shapeModel = ShapeItemModel().bindTo(this)
override val root = stackpane {
label(shapeModel.name)
}
}
class ShapeItem(name: String) {
val nameProperty = SimpleStringProperty(name)
}
class ShapeItemModel : ItemViewModel<ShapeItem>() {
val name = bind(ShapeItem::nameProperty)
}
class LibraryViewModel : ViewModel() {
val libraryItemsProperty = SimpleListProperty<ShapeItem>(
listOf(
ShapeItem("Shape 1"),
ShapeItem("Shape 2")
).observable()
)
}
class LibraryView : View("Current Library") {
val shapeLibraryViewModel: LibraryViewModel by inject()
override val root = anchorpane {
flowpane {
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
val itemFragment = find<ShapeItemFragment>()
itemFragment.itemProperty.value = shapeItem
itemFragment.root
}
}
}
}
A slightly lighter version would be to pass the parameter into your fragment manually and just extend Fragment:
class ShapeItemFragment : Fragment() {
val item: ShapeItem by param()
override val root = stackpane {
label(item.nameProperty)
}
}
You can still bind to changes to properties inside the ShapeItem, since the underlying item won't change (as seen from the ItemFragment) anyway.
Your bindChildren statement would then look like this:
bindChildren(shapeLibraryViewModel.libraryItemsProperty) { shapeItem ->
find<ShapeItemFragment>(ShapeItemFragment::item to shapeItem).root
}

Related

How to add arguments to a org.gradle.api.Action?

I'm writing a custom Gradle plugin that registers an extension and uses Actions for configuring it so you can use it like
myExtension {
info {
someConfig("a") {
// config for someConfig["a"]
}
someConfig("b") {
// config for someConfig["b"]
}
}
}
Based on how much someConfig you define, the plugin created new Gradle tasks dynamically. I've seen some scenarios in which it'd useful to configure all someConfig with the same values, so instead of writing
myExtension {
info {
someConfig("a") {
someValue = x
}
someConfig("b") {
someValue = x
}
}
}
I was thinking in providing something like
myExtension {
info {
someConfig("a")
someConfig("b")
allConfigs { configName ->
someValue = x
}
}
}
When trying to code it, I've used an objectFactory for creating instances of Action so I can configure the extension values
val allMyConfigs = mutableMapOf<String, SomeConfig>()
fun someConfig(configName: String, init: Action<in EnvironmentInfo>) {
val myConfig = allMyConfigs.computeIfAbsent(env) {
objectFactory.newInstance(SomeConfig::class.java, env)
}
init.execute(environmentInfo)
}
but since Action is a (SomeConfig) -> Unit (i.e. a function receiving a SomeConfig instance) I'm unable to pass the config name (an String) as param
Is there a way to create an Action, so it's compatible with both build.gradle and build.gradle.kts files, that receives an extra parameter and maintain the SomeConfig as delegate?

Handling commands from the viewmodel to the UI

The peculiarity of this application is that every time a user does something (except common things like typing) the application must check with an authority that they are indeed allowed to perform that action.
For example, let us say that the user wishes to see their profile (which is on the top bar)
the Composable screen looks something like this:
#Composable
fun HomeScreen(
navController: NavController,
vm: HomeViewModel = hiltViewModel()
) {
val state = vm.state.value
val scaffoldState = rememberScaffoldState()
HomeScreen(state, scaffoldState, vm::process)
}
#Composable
fun HomeScreen(state: HomeState, scaffoldState: ScaffoldState, event: (HomeEvent) -> Unit) {
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = {
Text("Hello world")
},
actions = {
IconButton(onClick = {
event.invoke(HomeEvent.ShowProfile)
}) {
Icon(
painter = painterResource(id = R.drawable.ic_person),
contentDescription = stringResource(id = R.string.profile)
)
}
}
)
}
) {
}
}
the view model receives it like so:
#HiltViewModel
class HomeViewModel #Inject constructor(app: Application, private val checkAllowed: CheckAllowed): AndroidViewmodel(app) {
val state = mutableStateOf(HomeState.Idle)
fun process(event:HomeEvent) {
when(event) {
HomeEvent.ShowProfile -> {
state.value = HomeState.Loading
viewModelScope.launch {
try {
val allowed = checkAllowed(Permission.SeeProfile) //use case that checks if the action is allowed
if (allowed) {
} else {
}
} finally {
state.value = HomeState.Idle
}
}
}
}
}
}
I now have to send a command to the ui, to either show a snackbar with the error or navigate to the profile page.
I have read a number of articles saying that compose should have a state, and the correct way to do this is make a new state value, containing the response, and when the HomeScreen receives it , it will act appropriately and send a message back that it is ok
I assume something like this :
in the viewmodel
val command = mutableStateOf<HomeCommand>(HomeCommand.Idle)
fun commandExecuted() {
command.value = HomeCommand.Idle
}
and inside the HomeScreen
val command = vm.command.value
try {
when (command) {
is HomeCommand.ShowProfile -> navController.navigate("profile_screen")
is HomeCommand.ShowSnackbar -> scaffoldState.snackbarHostState.showSnackbar(command.message, "Dismiss", SnackbarDuration.Indefinite)
}
}finally {
vm.commandExecuted()
}
but the way I did it is using flows like so:
inside the viewmodel:
private val _commands = MutableSharedFlow<HomeCommand>(0, 10, BufferOverflow.DROP_LATEST)
val commands: Flow<HomeCommand> = _commands
and inside the HomeScreen:
LaunchedEffect(key1 = vm) {
this#ExecuteCommands.commands.collectLatest { command ->
when (command) {
is HomeCommand.ShowProfile -> navController.navigate("profile_screen")
is HomeCommand.ShowSnackbar -> scaffoldState.snackbarHostState.showSnackbar(command.message, "Dismiss", SnackbarDuration.Indefinite)
}
}
This seems to work, but I am afraid there may be a memory leak or something I'm missing that could cause problems
Is my approach correct? Should I change it to state as in the first example? can I make it better somehow?

How to change particular property value of a class in a LiveData<List<T>> (in my case LiveData<List<Item>>) using MediatorLiveData

The Item.kt class is
#Entity(tableName = "item")
class Item(
val id: Long,
val title: String,
) {
#Ignore
var selection: Boolean = false
}
Then i make a query to get all the items in the table ,it return
LiveData<List<Item>>
Then in the viewModel i want to apply selection(true) accordig to the Mutablelivedata selectionId, the selection id contain MutableLiveData<Long> (it contain an id in the LiveData<List<Item>>)
The MyViewModel.kt code is look like this
class MyViewModel(val repository: Repository) : ViewModel() {
..........
......
val selectionId: MutableLiveData<Long> by lazy {
MutableLiveData<Long>()
}
fun setSelectionId(id: Long) {
selectionId.postValue(id)
}
..........
......
val itemLiveList: LiveData<List<Item>> = liveData(Dispatchers.IO) {
emitSource(repository.getItems())
}
}
If it is an List<Item> i can do somethig like this
val ItemWithSelection: List<Item> = repository.getItems().apply {
this.forEach {
if (it.id == selectionId) {
it.selection = true
}
}
}
but i don't know how to achieve this using Mediator LiveData . Please help me
I don't understand everything in your code, for example I have never seen a function called liveData(CoroutineDispatcher). But do you mean you want something like this?
val listWithoutSelection = liveData(Dispatchers.IO) {
emitSource(repository.getItems())
}
val listWithSelection = MediatorLiveData<List<Item>>().apply {
addSource(listWithoutSelection) { updateListSelection() }
addSource(selectionId) { updateListSelection() }
}
fun updateListSelection() {
listWithSelection.value = listWithoutSelection.value?.map {
if (it.id == selectionId.value)
it.copyWithSelection(true)
else
it
}
}
The copyWithSelection could be easily done with Kotlin data classes. It is not needed dependent on whether you want to modify the object you get from the database. If you only use that object here, you could just always reset the selection of the others to false and then you can keep the object and you don't need a copy.

How to pass data from the navigationdrawer to Activity using safe-args?

Let's say we have a project like this one:
class MainActivity : AppCompatActivity() {
private lateinit var drawerLayout: DrawerLayout
private lateinit var appBarConfiguration : AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
drawerLayout = binding.drawerLayout
val navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)
// prevent nav gesture if not on start destination
navController.addOnDestinationChangedListener { nc: NavController, nd: NavDestination, bundle: Bundle? ->
if (nd.id == nc.graph.startDestination) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
}
NavigationUI.setupWithNavController(binding.navView, navController)
}
Link to a simple project: https://github.com/udacity/andfun-kotlin-android-trivia/blob/Step.11-Solution-Adding-Animation/app/src/main/java/com/example/android/navigation/MainActivity.kt
My file for navGraph contains fragments and also one Activity where I want to go if user select its title from the navigation drawer. I want to send some data to this Activity. How can I do it using safe-args?
I'm using:
apply plugin: "androidx.navigation.safeargs"
and
implementation 'androidx.navigation:navigation-fragment:2.0.0'
implementation 'androidx.navigation:navigation-ui:2.0.0'
Firstly, you'll have to go to the navigation graph and specify an argument for the activity destination. This can be done via the design UI or in XML like this;
<navigation>
<activity android:id="#+id/someActivity">
<argument
android:name="isEditMode"
app:argType="boolean"
android:defaultValue="false" />
</activity>
</navigation>
This snippet assumes you are passing a boolean to the activity.
At this point, you can build the project so that all required files are generated.
Then in the onClick of whatever Navigation menu item responsible for starting the activity, you pass in the data;
override boolean onNavigationItemSelected(menuItem: menuItem) {
val id = menuItem.itemId
when (id) {
R.id.openActivity -> {
val bundle = bundleOf("isEditMode" to false)
findNavController().navigate(R.id.someActivity, bundle)
}
}
return true
}
Then in your activity, get the pass data safely as;
val safeArguments: MyActivityArgs by navArgs()
val isEditMode = safeArgs.isEditMode)
Inside your onCreate method in MainActivity use this:
val navController = this.findNavController(R.id.myNavHostFragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->
when(destination.id) {
R.id.caixaFragment -> {
var myVar = someMethodThatBringsMyVarValue()
val argument = NavArgument.Builder().setDefaultValue(mayVar).build()
destination.addArgument("idMes", argument)
}
}
}
I got it from Ayxan Haqverdili in this question

How can I clear a 'textfield'

I'm trying to write text to a textfield, clear the text and the write a new text. I can't get rid of the old text. The new is written on the old so I see them both. I'm using choosefile and trying to show the selected file in a textfield so I can confirm the selection.
class TestView : View("My View") {
var tf: TextField by singleAssign()
override val root = BorderPane()
init {
with(root) {
center = form {
fieldset("Main") {
field("File") {
vbox {
tf = textfield()
tf.text("678")
tf.clear()
tf.text("999")
}
}
}
}
}
}
}
I expected to to see '999' in the textfield but I see both 678 and 999 at the same place.
You are actually calling the text() builder on the textfield, so you're basically creating two text elements inside the text field. I think you should take a step back and read the guide before you go further :) Here is a cleaned up version of your code:
class TestView : View("My View") {
val filename = SimpleStringProperty()
override val root = borderpane {
center {
form {
fieldset("Main") {
field("File") {
textfield(filename)
button("Browse...").action {
val filters = arrayOf(FileChooser.ExtensionFilter("All Files", "*.*"))
chooseFile("Choose file...", filters).firstOrNull()?.let {
filename.value = it.path
}
}
}
}
}
}
}
}

Resources