Composables in Pager With Tabs Composed Too Quickly - tabs

Trying to implement accompanist pager with tabs to achieve something like instagram's page displaying followers, following and subscription - 3 tab menu with pager basically. This is the code I am using.
fun UsersPager(
myDBViewModel: MyDBViewModel
) {
val tabData = listOf(
"FOLLOWING" to Icons.Filled.PermIdentity,
"ALLUSERS" to Icons.Filled.PersonOutline,
"FOLLOWERS" to Icons.Filled.PersonOutline
)
val pagerState = rememberPagerState(
0
)
val tabIndex = pagerState.currentPage
val coroutineScope = rememberCoroutineScope()
Column {
TabRow(
selectedTabIndex = tabIndex,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
tabData.forEachIndexed { index, pair ->
Tab(
selected = tabIndex == index,
onClick = {
coroutineScope.launch {
Log.d("MP18", "click on Tab num: $index")
pagerState.animateScrollToPage(index)
}
},
text = {
Text(text = pair.first)
},
icon = {
Icon(imageVector = pair.second, contentDescription = null)
})
}
}
HorizontalPager(
state = pagerState,
itemSpacing = 1.dp,
modifier = Modifier
.weight(1f),
count = tabData.size
) { index ->
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (index) {
1 -> ShowMyFollowees(myDBViewModel = myDBViewModel)
2 -> ShowMyUsers(myDBViewModel = myDBViewModel)
3 -> ShowMyFollowers(myDBViewModel = myDBViewModel)
}
}
}
}
}
Then 3 composables follow this pattern to fetch data from API and display them:
#Composable
fun ShowMyUsers(
myDBViewModel: MyDBViewModel,
) {
val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = null)
val myUsersList by myDBViewModel.myUsersList.observeAsState(initial = emptyList())
val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)
if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!!) {
LaunchedEffect(key1 = Unit, block = {
Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
myDBViewModel.getFirstPageUsers()
})
}
ListMyUsers(myUsers = myUsersList, myDBViewModel = myDBViewModel)
}
#Composable
fun ListMyUsers(
myUsers: List<MyUser>,
myDBViewModel: MyDBViewModel
) {
val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = myDBViewModel.pageLoadedTimes.value)
val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)
Log.d(
"MP18",
"comp ShowMyUsers and pageLoadedTimes is: $pageLoadedTimes and loading is: $loading"
)
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp)
) {
itemsIndexed(
items = myUsers
) { index, user ->
myDBViewModel.onChangeProductScrollPosition(index)
val numRec = pageLoadedTimes?.times(PAGE_SIZE)
Log.d(
"MP188",
"in composable, page: $pageLoadedTimes, index: $index, loading: $loading, numRec: $numRec"
)
//we should query and display next page if this is true:
if ((index + 1) >= (pageLoadedTimes?.times(PAGE_SIZE)!!) && !loading!!) {
myDBViewModel.getNextPageUsers()
}
ShowSingleUser(
index = index,
pageLoadedTimes = pageLoadedTimes!!,
user = user,
myDBViewModel = myDBViewModel
)
}
}
}
}
In composables that are available, there's an API call (through ViewModel) which gets data from backend in order to populate some vars in viewModel. The problem I have is that when first tab is clicked, also the neighbouring composable gets composed and thus I'am making 2 API calls and "preparing" second tab data even if the user might never click on that tab. This is not what I want. I'd like to fetch data from tab2 and later tab3 only when there's a click on them. I hope I am clear in what's bothering me.

This is the expected behavior of the pager as the pager has been implemented by using LazyRow in accompanist pager. Basically, pager loads the second page before you scroll to it as LazyLayout is implemented in that way. If you want to cancel that you can do something like this, which I use in my code also:
// In anywhere of your composable
SideEffect {
if(currentShownItemIndex == pagerState.currentPage) {
// Make api call...
}
}
This should ensure that you are making your api call if and only if you are on the correct index
Edit: You can use Launched Effect if you want, I used SideEffect as it is easier to write and does not rely on any key and I needed a coroutine scope simply :d
Finally, this does not prevent the composition of the page in index+1 however prevents the unnecessary api call made by pager.

I found the solution for this. I added another variable in viewModel:
private val _pageInPager = MutableLiveData(0)
val pageInPager: LiveData<Int> = _pageInPager
fun setPageInPager(pageNum: Int) {
Log.d("MP188", "setPageInPager to: $pageNum")
_pageInPager.value = pageNum
}
Then in composable:
if user clicks on tab:
onClick = {
coroutineScope.launch {
Log.d("MP18", "click on Tab num: $index")
pagerState.animateScrollToPage(index)
myDBViewModel.setPageInPager(index)
}
},
or move the pager(slider):
myDBViewModel.setPageInPager(pagerState.currentPage)
I have the exact page in the variable: myDBViewModel.pageInPager, so I can add checker in LaunchedEffect before making an API call:
if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!! && pageInPager == 1) {
LaunchedEffect(key1 = Unit, block = {
Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
myDBViewModel.getFirstPageUsers()
})
I think this works ok now. Thank you #Subfly.

Related

How to view and bind an array with swing builder

Currently, this shows 2 variables on the screen. Whenever that variable changes, it is also shown on the GUI.
I would like to bind array indexes in a similar way. For example: x[1],
and whenever x[1] changes, so does the value shows on GUI
EDIT: array x doesn't update, I also don't want the whole array but each index on a new line.
import groovy.swing.SwingBuilder
import groovy.beans.Bindable
#Bindable
class controller{
boolean stop = false;
String status = "RUNNING";
String name = "";
String[] x= new String[5];
}
def ctrl = new controller();
def UI = new SwingBuilder().edt {
frame(title: 'Menu', size: [220, 150], locationRelativeTo: null, show: true, alwaysOnTop: true){
gridLayout(cols:1, rows: 5)
label(text: bind(source: ctrl, sourceProperty: 'status', converter: { v -> v? "Status: $v": ''}))
label(text: bind(source: ctrl, sourceProperty: 'name', converter: { v -> v? "$v": ''}))
label(text: bind(source: ctrl, sourceProperty: 'x', converter: { v -> v? "$v": ''}))
}
}
for(i = 0; i < 5 ; i++){
sleep(500);
ctrl.name+= "Hi there ";
ctrl.x[i] = "T"+i;
}
ctrl.status = "DONE";
sleep(1000);
UI.dispose();
You can't observe changes on an array using #Bindable in this way. The AST transformation is a convenience for registering methods that rely on PropertyChange and PropertyChangeListener.
Changes made to name and status properties work because you're replacing their value, thus a PropertyChangeEvent is triggered for every change. However for the x property you're not changing the array reference, but the elements within the array.
You have two options to make this work:
either observe changes on the index itself (add an Integer property to the controller class).
use an ObservableList instead of an array.
Personally I prefer option #1. You will have to use a converter such as
label(text: bind(source: ctrl, sourceProperty: 'index', converter: { v -> v != -1? ctrl.x[v]: ''}))
Notice the use of -1 to check for a selected index as a value of 0 would make the previous check (v?) fail due to Groovy Truth.
The controller class can be updated as follows
#Bindable
class controller{
boolean stop = false;
String status = "RUNNING";
String name = "";
String[] x= new String[5];
int index = -1;
}

Scala-Slick: only part of sequence of actions executed

Have a piece of code that adds/updates a Product and also associates one or more tags to it. Tags are actually added to a TagGroup and that is associated with the Product.
Issue I am facing is that only "part" of addOrUpdateProductWithTags() executes. Product is updated or created but Tags are not added. If I comment the last query (see comment) then everything works. Have turned on "" to confirm this.
lazy val pRetId = prods returning prods.map(_.id)
def addTags(keywords: Seq[String]) = {
for {
k <- keywords
} yield {
tags.filter(_.keyword === k).take(1).result.headOption.flatMap {
case Some(tag) => {
Logger.debug("Using existing tag: " + k)
DBIO.successful(tag.id)
}
case None => {
Logger.debug("Adding new tag: " + k)
tags.returning(tags.map(_.id)) += Tag(k, Some("DUMMY"))
}
}
}
}
def addOrUpdateProductWithTags(prod: Product, tagSet: Seq[String]): Future[Option[Long]] = {
// handle add or update product
val prodObject = prod.id match {
case 0L => pRetId += prod
case _ => prods.withFilter(_.id === prod.id).update(prod)
}
val action = for {
pid <- prodObject
tids <- DBIO.sequence(addTags(tagSet))
} yield (tids, pid)
val finalAction = action.flatMap {
case (tids, pid) => {
val prodId = if (prod.id > 0L) prod.id else pid.asInstanceOf[Number].longValue
val delAction = tagGroups.filter(_.prodId === prodId).delete
val tgAction = for {
tid <- tids
} yield {
tagGroups += TagGroup("Ignored-XX", prodId, tid)
}
delAction.flatMap { x => DBIO.sequence(tgAction) }
// IF LINE BELOW IS COMMENTED THEN TagGroup is created else even delete above doesn't happen
prods.filter(_.id === prodId).map(_.id).result.headOption
}
}
db.run(finalAction.transactionally)
}
This is the snippet in the controller where this method is called from. My suspicion is that caller doesn't wait long enough but not sure...
val prod = Prod(...)
val tagSet = generateTags(prod.tags)
val add = prodsService.addOrUpdateProductWithTags(prod, tagSet)
add.map { value =>
Redirect(controllers.www.routes.Dashboard.dashboard)
}.recover {
case th =>
InternalServerError("bad things happen in life: " + th)
}
Any clue what's wrong with the query ?
Stack: Scala 2.11.7, play version 2.5.4, play-slick 2.0.0 (slick 3.1)
Finally figured out a solution:
In place of the following 2 lines:
delAction.flatMap { x => DBIO.sequence(tgAction) }
prods.filter(_.id === prodId).map(_.id).result.headOption
I combined the actions with andThen operators as follows:
delAction >> DBIO.sequence(tgAction) >> prods.filter(_.id === prodId).map(_.id).result.headOption
Now the entire sequence gets executed. I still don't know what's wrong with the original solution but this works.

How to set focus on an element in Elm?

How can I set the focus on a Html element in Elm? I tried to set the autofocus attribute on the element and it only sets the focus on the page load.
The focus function in the elm-lang/dom package is used to set focus with a Task (without using any ports or JavaScript).
Internally it uses requestAnimationFrame to ensure any new DOM updates are rendered before it tries to find the DOM node to focus on.
An example use:
type Msg
= FocusOn String
| FocusResult (Result Dom.Error ())
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FocusOn id ->
( model, Dom.focus id |> Task.attempt FocusResult )
FocusResult result ->
-- handle success or failure here
case result of
Err (Dom.NotFound id) ->
-- unable to find dom 'id'
Ok () ->
-- successfully focus the dom
Full example on Ellie
A workaround for this is to use Mutation Observers. Insert this JavaScript either in your main HTML page or in the main view of your Elm code:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
handleAutofocus(mutation.addedNodes);
});
});
var target = document.querySelector('body > div');
var config = { childList: true, subtree: true };
observer.observe(target, config);
function handleAutofocus(nodeList) {
for (var i = 0; i < nodeList.length; i++) {
var node = nodeList[i];
if (node instanceof Element && node.hasAttribute('data-autofocus')) {
node.focus();
break;
} else {
handleAutofocus(node.childNodes);
}
}
}
Then create HTML elements by including Html.Attributes.attribute "data-autofocus" "".
With elm/html 0.19 you can set the Html.Attrbutes autofocus to True
input [ onInput Code, autofocus True ] []
I spent quite a bit of time exploring this recently. Unfortunately, I don't think it is possible with the existing elm-html library. However, I came up with a hack that utilizes css animations to trigger an event and embed that in pure js.
Here is my hack in Elm using a script node and a style node. It is very ugly in my opinion.
import Html exposing (div, button, text, input, node)
import Html.Events exposing (onClick)
import Html.Attributes exposing (type', class)
import StartApp.Simple
main =
StartApp.Simple.start { model = model, view = view, update = update }
model = []
view address model =
-- View now starts with a <style> and <script> (hacky)
(node "style" [] [ Html.text style ]) ::
(node "script" [] [Html.text script ]) ::
(button [ onClick address AddInput ] [ text "Add Input" ]) ::
model |>
div []
type Action = AddInput
update action model =
case action of
AddInput -> (Html.p [] [input [type' "text", class "focus"] []]) :: model
-- Use pure string css (hacky)
style = """
.focus {
animation-name: set-focus;
animation-duration: 0.001s;
-webkit-animation-name: set-focus;
-webkit-animation-duration: 0.001s;
}
#-webkit-keyframes set-focus {
0% {color: #fff}
}
#keyframes set-focus {
0% {color: #fff}
}
"""
-- Cheating by embedding pure javascript... (hacky)
script = """
var insertListener = function(event){
if (event.animationName == "set-focus") {
event.target.focus();
}
}
document.addEventListener("animationstart", insertListener, false); // standard + firefox
document.addEventListener("MSAnimationStart", insertListener, false); // IE
document.addEventListener("webkitAnimationStart", insertListener, false); // Chrome + Safari
"""
In Elm 0.19, use Browser.Dom.focus:
import Browser.Dom as Dom
import Task
type Msg
= NoOp
focusSearchBox : Cmd Msg
focusSearchBox =
Task.attempt (\_ -> NoOp) (Dom.focus "search-box")
You can choose to ignore if focusing fails like above or do something by triggering an update message.
Elm 0.19's Browser.Dom.focus is the modern solution
import Browser.Dom as Dom
import Task
type Msg
= NoOp
| Focus String
focusElement : String -> Cmd Msg
focusElement htmlId =
Task.attempt (\_ -> NoOp) (Dom.focus htmlId)
update : Msg -> Model -> (Model, Cmd Msg)
update msg =
case msg of
Focus htmlId ->
( model, focusElement htmlId )
NoOp ->
( model, Cmd.none )

Right Click on a Button / Scala

I'm currently trying to code a minesweeper using scala, but I can't find the way to listen to a right click on a button.
I've already searched on the Internet the way to do it, but I definitely was not able to find it.
If anyone could help me out, I would be really grateful :)
Thanks,
Schnipp
(Note: Scala is a new language to me and I am not a Java user, so I am sorry if my questions sound dumb)
EDIT:
I am trying to find (or implement) a function 'ButtonClickedRight' that could listen to a right-click on a button.
like this
import scala.swing._
import scala._
import scala.swing.event._
object Right extends MainFrame with App {
title = ""
visible = true
val b = new button("")
listenTo(b)
reactions += {
case ButtonClicked(`b`) => *code*
case ButtonClickedRight(`b`) => *code*
}
}
EDIT 2 --
I would like to know if the user has clicked on the Button "1" or not. The problem I have is that this code prints "Mouse clicked at " + e.point+" type "+e.modifiers when I click on the label but not on the button.
object App extends SimpleSwingApplication {
lazy val ui = new GridPanel(2,1) {
contents += new Button("1")
contents += new Label("2")
listenTo(mouse.clicks)
reactions += {
case e: MouseClicked =>
println("Mouse clicked at " + e.point+" type "+e.modifiers)
}
}
def top = new MainFrame {
contents = ui
visible = true
preferredSize = new Dimension(500,500)
}
}
Button events are fired through a specific publisher .mouse.clicks.
import scala.swing._
import scala.swing.event._
object App extends SimpleSwingApplication {
lazy val ui = new GridPanel(2,1) {
val button = new Button("1")
contents += button
contents += new Label("2")
listenTo(button.mouse.clicks) // !
reactions += {
case evt # MouseClicked(`button`, pt, _, _, _) =>
val which = evt.peer.getButton
if (which > 1) {
println(s"Mouse clicked at (${pt.x}; ${pt.y}) - button: $which")
}
}
}
lazy val top = new MainFrame {
contents = ui
size = new Dimension(500,500)
}
}
Note that at least on Linux my right button has number 3 not 2. You could also use the triggersPopup flag, but then you must ensure to monitor both MousePressed and MouseReleased, as this flag is platform-dependent.
I think that you are on the right path, for my understanding of scala swings I think that the problem is that you are not attaching the listener correctly. For one I would assign the button to a value and call listenTo only on it:
val button = new Button("1")
listenTo(button)
Then, in the reactions, I would write the pattern checking in the event that it comes from the button (probably redundant if you only call listenTo passing the button) and that it has the correct button:
case ButtonClicked(b) if b == button && b.peer.getButton == MouseEvent.BUTTON_2 => ...
So the code you provided in your edit would become:
object App extends SimpleSwingApplication {
lazy val ui = new GridPanel(2,1) {
val button = new Button("1")
contents += button
contents += new Label("2")
listenTo(button)
reactions += {
case evt # MouseClicked(b, pt, _, _, _) if b == button && evt.peer.getButton == java.awt.event.MouseEvent.BUTTON2 =>
println(s"Mouse clicked at (${pt.x}; ${pt.y}) - button: ${evt.peer.getButton}")
}
}
def top = new MainFrame {
contents = ui
visible = true
preferredSize = new Dimension(500,500)
}
}
The following works for me:
new Button {
listenTo(mouse.clicks)
reactions += {
case MouseClicked(_, _, c, _, _) => handleClick(c == 0)
}
}
def handleClick(isLeftClick: Boolean): Unit = {
//
}

Scala Swing ListView's Remove / Add Elements Events

With reference to this,
How do I change the contents of a ListView in Scala?
I could change ListView contents by changing listData. However, I couldn't get ListView to publish these events, ListElementsAdded, ListElementsAdded and ListChanged. From the looks of ListView source, it would only adds a listener to a read-only empty model. How do I go about this?
Thanks
Later on, I managed to figure out a way to have ListView published these events., please refer to the code.
Is the right to go about it? Is there a better way to do this? Please advise.
Thanks
** code borrowed and modified **
object ListViewTest extends SimpleSwingApplication
{
lazy val top = new MainFrame
{
title = "ListView Test"
contents = new BoxPanel(Orientation.Vertical)
{
border = Swing.EmptyBorder(2, 2, 2, 2)
val listModel = new DefaultListModel
List("First", "Second", "Third", "Fourth", "Fifth").map(listModel.addElement(_))
val myList = ListBuffer()
val listView = new ListView[String](myList)
{
selection.intervalMode = ListView.IntervalMode.Single
peer.setModel(listModel)
//listData = myList
}
listView.peer.getModel.addListDataListener(new ListDataListener {
def contentsChanged(e: ListDataEvent) { publish(ListChanged(listView)) }
def intervalRemoved(e: ListDataEvent) { publish(ListElementsRemoved(listView, e.getIndex0 to e.getIndex1)) }
def intervalAdded(e: ListDataEvent) { publish(ListElementsAdded(listView, e.getIndex0 to e.getIndex1)) }
})
contents += new ScrollPane(listView)
val label = new Label("No selection")
contents += label
val b = new Button("Remove")
contents += b
listenTo(listView.selection, listView, b)
reactions +=
{
case ListSelectionChanged(list, range, live) =>
label.text = "Selection: " + range
case e: ButtonClicked =>
if (listView.listData.isEmpty)
{
b.enabled = false
}
else
{
listView.peer.getModel.asInstanceOf[DefaultListModel].remove(listView.selection.anchorIndex)
}
case ListElementsRemoved(source, range) =>
println("Element at " + (range.start + 1) + " is removed.")
}
}
pack
}