How to show a loading screen and handle authorization at the same time?
Can I switch to LoadingView and return back to AuthView in AuthController or I need to move auth logic to LoadingController?
class AuthController : Controller() {
val authView : AuthView by inject()
val loadingView : LoadingView by inject()
fun tryAuth(login: String, password: String) {
runAsync {
login == "admin" && password == "admin"
} ui { successful ->
authView.replaceWith(loadingView, ViewTransition.Fade(0.5.seconds))
if (successful) {
// doesn't work
loadingView.replaceWith(MainView::class, ViewTransition.Metro(0.5.seconds))
} else {
// doesn't work
loadingView.replaceWith(AuthView::class, ViewTransition.Fade(0.5.seconds))
}
}
}
}
You are replacing the AuthView with the LoadingView and then the LoadingView with the MainView in the same pulse, so that's not going to give you what you want. Normally you'd want to change to the LoadingView on the UI thread before you evaluate the auth information. Using this approach, your code works, but it might not be what you want.
class AuthController : Controller() {
val authView : AuthView by inject()
val loadingView : LoadingView by inject()
fun tryAuth(login: String, password: String) {
authView.replaceWith(loadingView, ViewTransition.Fade(0.5.seconds))
runAsync {
// Simulate db access or http call
Thread.sleep(2000)
login == "admin" && password == "admin"
} ui { successful ->
if (successful) {
// doesn't work
loadingView.replaceWith(MainView::class, ViewTransition.Metro(0.5.seconds))
} else {
// doesn't work
loadingView.replaceWith(AuthView::class, ViewTransition.Fade(0.5.seconds))
}
}
}
}
class AuthView : View("Auth") {
val authController: AuthController by inject()
override val root = stackpane {
button(title).action {
authController.tryAuth("admin", "admin")
}
}
}
class LoadingView : View("Loading...") {
override val root = stackpane {
label(title)
}
}
class MainView : View("Main View") {
override val root = stackpane {
label(title)
}
}
You must keep in mind that replacing a view will not resize the window (though you could access the stage and ask it to resize to the current view), so you might be better off with opening each view in a separate window instead.
Related
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)
}
}
Inside ExamAcitivy :
passExamState always returns false even though its value changes to true after the setOnClickListener, when called in the adapter I need it to return the state acordingly. How do i propely pass the examState and its value to the adapter?
class ExamActivity : AppCompatActivity(){
private var examState = false
private val questionData = QuestionData()
private var questionAdapter = QuestionAdapter(questionData)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exam)
questionViewPager.adapter = questionAdapter
endExam.setOnClickListener {
examState=true
}
}
fun passExamState() : Boolean {
return examState
}
}
Inside My ViewPager2Adapter :
class QuestionAdapter(private val questionData: QuestionData) :
RecyclerView.Adapter<QuestionAdapter.QuestionViewPagerViewHolder>() {
inner class QuestionViewPagerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
//itemviews
init {
if (!ExamActivity().passExamState()) {
getthis()
}else{
getthat()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewPagerViewHolder {
return QuestionViewPagerViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_question, parent, false)
)
}
override fun onBindViewHolder(holder: QuestionViewPagerViewHolder, position: Int) {
//itemviews
if (!ExamActivity().passExamState()){
dothis()
}else{
dothat()
}
}
//itemcount
}
The problem is that you're creating a new ExamActivity instance and using that instance's property to check passExamState(). In this line, which appears twice in your code:
if (!ExamActivity().passExamState()) {
you are calling the ExamActivity constructor to construct a new Activity, so you are not checking the state of the actual Activity that is on the screen.
The proper, encapsulated way to do this is to create the appropriate Boolean property in your Adapter, and allow your Activity to modify that property. This way of organizing the code reduces inter-dependence. The Adapter doesn't have to know about the existence of the Activity. For example:
//In ExamActivity:
endExam.setOnClickListener {
examState = true // If you even need to track this property in the Activity.
// Maybe you can remove the property entirely.
questionAdapter.examState = true
}
// Adapter class:
class QuestionAdapter(private val questionData: QuestionData) :
RecyclerView.Adapter<QuestionAdapter.QuestionViewPagerViewHolder>() {
var examState = false
inner class QuestionViewPagerViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
init {
if (!examState) { // and the same in onBindViewHolder
getthis()
}else{
getthat()
}
}
}
//...
}
I'm using Android's Navigation component and I'm wondering how to setup AlertDialog from a fragment with a click listener.
MyFragment
fun MyFragment : Fragment(), MyAlertDailog.MyAlertDialogListener {
...
override fun onDialogPostiveCLick(dialog: DialogFragment) {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
// Here I would typically call setTargetFragment() and then show the dialog.
// but findnavcontroller doesn't have setTargetFragment()
findNavController.navigate(MyFragmentDirection.actionMyFragmentToMyAlertDialog())
}
}
MyAlertDialog
class MyAlertDialog : DialogFragment() {
...
internal lateinit var listener: MyAlertDialogListener
interface MyAlertDialogListener{
fun onDialogPostiveCLick(dialog: DialogFragment)
}
override fun onCreateDialog(savdInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage("My Dialog message")
.setPositiveButton("Positive", DialogInterface.OnClickListener {
listener = targetFragment as MyAlertDialogListener
listener.onDialogPositiveClick(this)
}
...
}
}
}
This currently receives a null point exception when initializing the listener in MyAlertDialog.
To use targetFragment, you have to set it first as you commented, unfortunately jetpack navigation does not do this for you (hence the null pointer exception). Check out this thread for alternative solution: https://stackoverflow.com/a/50752558/12321475
What I can offer you is an alternative. If the use-case is as simple as displaying a dialog above current fragment, then do:
import androidx.appcompat.app.AlertDialog
...
class MyFragment : Fragment() {
...
fun onDialogPostiveCLick() {
Log.i(TAG, "Listener returns a postive click")
}
fun launchMyAlertDialog() {
AlertDialog.Builder(activity)
.setMessage("My Dialog message")
.setPositiveButton("Positive") { _, _ -> onDialogPostiveCLick() }
.setCancellable(false)
.create().show()
}
}
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?
My application needs to permit additions to the listview. I've figured out how I can dynamically add to a listview by using observableArrayList. If I click on the button, an item gets added to the list and displayed.
Now I'm struggling to add a click handler (I want to handle the event that happens when someone clicks on any item within the list view). Where do I do this?
Here is my code.
package someapp
import javafx.collections.FXCollections
import javafx.geometry.Pos
import javafx.scene.layout.VBox
import javafx.scene.text.FontWeight
import tornadofx.*
class MyApp : App(HelloWorld::class) {
}
class HelloWorld : View() {
val leftSide: LeftSide by inject()
override val root = borderpane {
left = leftSide.root
}
}
class LeftSide: View() {
var requestView: RequestView by singleAssign()
override val root = VBox()
init {
with(root) {
requestView = RequestView()
this += requestView
this += button("Add Item") {
action {
requestView.responses.add( Request( "example.com",
"/foo/bar",
"{ \"foo\" : \"bar\"}".toByteArray()))
}
}
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>(
)
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray) {
}
To configure a callback when an item in a ListView is selected, use the onUserSelect callback:
onUserSelect {
information("You selected $it")
}
You can optionally pass how many clicks constitutes a select as well, default is 2:
onUserSelect(1) {
information("You selected $it")
}
You are using some outdated constructs in your code, here is an updated version converted to best practices :)
class MyApp : App(HelloWorld::class)
class HelloWorld : View() {
override val root = borderpane {
left(LeftSide::class)
}
}
class LeftSide : View() {
val requestView: RequestView by inject()
override val root = vbox {
add(requestView)
button("Add Item").action {
requestView.responses.add(Request("example.com",
"/foo/bar",
"""{ "foo" : "bar"}""".toByteArray()))
}
}
}
class RequestView : View() {
val responses = FXCollections.observableArrayList<Request>()
override val root = listview(responses) {
cellFormat {
graphic = cache {
form {
fieldset {
label(it.hostname) {
alignment = Pos.CENTER_RIGHT
style {
fontSize = 22.px
fontWeight = FontWeight.BOLD
}
}
field("Path") {
label(it.path)
}
}
}
}
}
onUserSelect(1) {
information("You selected $it")
}
}
}
class Request(val hostname: String, val path: String, val body: ByteArray)