Why not correct check "is"? - kotlin

Here Kotlin's snippet:
fun withBackgroundColorResId(#IdRes expectedId: Int): Matcher<Any> {
return object : BoundedMatcher<Any, Any>(Any::class.java) {
override fun matchesSafely(view: Any): Boolean {
if (view !is View || view !is ViewGroup) {
Log.w(TAG, "withBackgroundColorResId_incorrect_type, view = $view")
return false
}
val currenColor = (view.background.current as ColorDrawable).color
val expectedColor = ContextCompat.getColor(view.context, expectedId)
return currenColor == expectedColor
}
override fun describeTo(description: Description) {
description.appendText("textView with background color resId: ")
description.appendValue(context.getResources().getString(expectedId))
}
}
}
and usage:
onView(withRecyclerView(tradersRecyclerView).atPositionOnView(traderCheckPos, pauseTextView)).check(matches(withBackgroundColorResId(trade_not_running_color)))
and here logcat:
06-05 10:25:12.787 I/ViewInteraction(22053): Checking 'MatchesViewAssertion{viewMatcher=textView with background color resId: "#ffbde6ff"}' assertion on view RecyclerView with id: com.myproject.debug:id/tradersRecyclerView at position: 0
06-05 10:25:12.787 W/com.myproject.custom.matcher.CustomMatchers(22053): withBackgroundColorResId_incorrect_type, view = androidx.appcompat.widget.AppCompatTextView{a412c35 V.ED..C.. ........ 0,0-288,288 #7f0800c9 app:id/pauseTextView}
06-05 10:25:12.794 D/com.myproject.activity.TradersActivityTest(22053): afterEach
06-05 10:25:12.794 I/MockWebServer(22053): MockWebServer[8081] done accepting connections: Socket closed
06-05 10:25:12.825 D/LifecycleMonitor(22053): Lifecycle status change: com.myproject.ui.activity.TradersActivity#2964795 in: PAUSED
06-05 10:25:12.825 D/LifecycleMonitor(22053): running callback: androidx.test.rule.ActivityTestRule$LifecycleCallback#9705558
06-05 10:25:12.825 D/LifecycleMonitor(22053): callback completes: androidx.test.rule.ActivityTestRule$LifecycleCallback#9705558
as you can see the object "view" has type androidx.appcompat.widget.AppCompatTextView. As we known AppCompatTextView is extends from View
But why it print
withBackgroundColorResId_incorrect_type, view = androidx.appcompat.widget.AppCompatTextView
?

In Kotlin's documentation is states:
checks that a value has a certain type
So according to Your code, You ask in a way
is my view instance of X?
Following this line You get:
is view instanceof View? Answer is yes.
is view instanceof ViewGroup? Answer is no.
Single views extends mainly by View class. ViewGroups are mostly used in more complex layouts like LinearLayout,FrameLayout, etc.
Concluding the results, You have:
true||false = true

The problem is with your if condition, as the view is not a ViewGroup the condition is always true thus will print the LOG.
if (view is View) {
Log.w(TAG, "withBackgroundColorResId_incorrect_type, view = $view")
return false
}
Please, try with this one and see the outcome.

Related

Kotlin StateFlow multiple subscriptions after returning from another Fragment

I'm trying to implement StateFlow in my app with MVVM architecture between ViewModel and Fragment.
In ViewModel:
...
private val _eventDataState = MutableStateFlow<EventDataState>(EventDataState.Loading)
val eventDataState: StateFlow<EventDataState> = _eventDataState
...
In Fragment:
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.eventDataState.collect { eventDataState ->
when (eventDataState) {
is EventDataState.Loading -> {}
is EventDataState.Success -> onEventDataUpdated(data = eventDataState)
is EventDataState.Deleted -> onEventDataDeleted()
}
}
}
}
viewModel.getEventData()
}
...
All seems to work well:
2022-04-15 17:57:20.352 5251-5251/ ... E/EVENT: Data state: Success(...)
But when I switch to another Fragment through Activity's SupportFragmentManager and then hit Back button I get multiple responses from StateFlow:
2022-04-15 17:57:30.358 5251-5251/ ... E/EVENT: Data state: Success(...)
2022-04-15 17:57:30.362 5251-5251/ ... E/EVENT: Data state: Success(...)
So the more times I switch to another Fragment and back - the more copies of response I get from StateFlow (this is not initial StateFlow value).
My guess is that when another Fragment are called the first one destroys its view but subscription to Flow is still exists, and when returning back from another Fragment the first one calls its onViewCreated and so creating another copy of subscription to this Flow. Nevertheless I was unable to find any solution to my case on stackoverflow or any other resources.
What have I missed?
You should use viewLifecycleOwner.lifecycleScope.launch in fragments. viewLifecycleOwner.lifecycleScope and all its jobs will be cancelled when the view is destroyed.

how to have loading in Kotlin

my MainActivity contains a ViewPager that loads 4 fragments, each fragment should load lots of data from the server.
so when my app wants to be run for the first time, it almost takes more than 3 seconds and the other times(for example, if you exit the app but not clean it from your 'recently app' window and reopen it) it takes almost 1 second.
while it is loading, it shows a white screen.
is there any way instead of showing a white screen till data become ready, I show my own image?
something like the splash page?
If you do long-running actions on the main thread, you risk getting an ANR crash.
Your layout for each fragment should have a loading view that is initially visible, and your data view. Something like this:
(not code)
FrameLayout
loading_view (can show a progress spinner or something, size is match parent)
content_view (probably a RecyclerView, initial visibility=GONE, size is match parent)
/FrameLayout
You need to do your long running action on a background thread or coroutine, and then swap the visibility of these two views when the data is ready to show in the UI.
You should not be directly handling the loading of data in your Fragment code, as Fragment is a UI controller. The Android Jetpack libraries provide the ViewModel class for this purpose. You would set up your ViewModel something like this. In this example, MyData could be anything. In your case it's likely a List or Set of something.
class MyBigDataViewModel(application: Application): AndroidViewModel(application) {
private val _myBigLiveData = MutableLiveData<MyData>()
val myBigLiveData: LiveData<MyData>() = _myBigLiveData
init {
loadMyBigData()
}
private fun loadMyBigData() {
viewModelScope.launch { // start a coroutine in the main UI thread
val myData: MyData = withContext(Dispatchers.Default) {
// code in this block is done on background coroutine
// Calculate MyData here and return it from lambda
// If you have a big for-loop, you might want to call yield()
// inside the loop to allow this job to be cancelled early if
// the Activity is closed before loading was finished.
//...
return#withContext calculatedData
}
// LiveData can only be accessed from the main UI thread so
// we do it outside the withContext block
_myBigLiveData.value = myData
}
}
}
Then in your fragment, you observe the live data to update the UI when it is ready. The below uses the fragment-ktx library, which you need to add to your project. You definitely should read the documentation on ViewModel.
class MyFragment: Fragment() {
// ViewModels should not be instantiated directly, or they won't be scoped to the
// UI life cycle correctly. The activityViewModels delegate handles instantiation for us.
private val model: MyBigDataViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.myBigLiveData.observe(this, Observer<MyData> { myData ->
loading_view.visibility = View.GONE
content_view.visibility = View.VISIBLE
// use myData to update the view content
})
}
}

updating label with progress of http post not working. IllegalStateException

I am having trouble with binding a UI component to an observable that gets updated progress from a http post event. I get an IllegalStateException
As I understand it the issue is the bind update is not happening on the UI thread. The answers I have read say that I need to use runAsync and then specify a UI block to update the UI component, but I am at a loss for how to accomplish this.
// View class
private val controller: ZumController by inject()
item("_Upload") {
isMnemonicParsing = true
action {
controller.uploadToServer()
}
}
bottom = label() {
useMaxWidth = true
padding = Insets(5.0, 10.0, 5.0, 10.0)
this.bind(controller.progress)
}
// Controller class
var progress = SimpleStringProperty("Select images to upload")
fun uploadToServer() {
images.forEach{ p ->
Fuel.upload("http://127.0.0.1:8089")
.add {FileDataPart(File(p), name = "file")}
.progress { readBytes, totalBytes ->
progress.value = (readBytes.toFloat() / totalBytes.toFloat() * 100).toString()}
.response { _ -> }
}
}
How would I go about making sure the UI is updated during the application thread when I need progress before function call (uploadToServer()) returns? Sorry if this has already been answered, I still don't get exactly what is happening here.
I've solved my problem with the following changes. I pass the FXTask to function uploadToServer(). There I updateMessage() with the progress callback for the http POST request. I can't say its the best way but it works. feel free to update this answer with more clear and concise code
item("_Upload") {
isMnemonicParsing = true
action {
runAsync {
controller.uploadToServer(this)
} ui {
}
}
}
fun uploadToServer(task: FXTask<*>) {
images.forEach{ p ->
Fuel.upload("http://127.0.0.1:8089")
.add {FileDataPart(File(p), name = "file")}
.progress { readBytes, totalBytes ->
val perComplete = readBytes.toFloat() / totalBytes.toFloat() * 100
task.updateMessage("Uploading $p %.2f".format(perComplete).plus("%"))
}
.response { _ -> }
}
}
TornadoFX has a built in TaskStatus object which has properties for the progress of the task. You can bind one or more of the properties in the TaskStatus object to any UI element, and simply call updateProgress from within your controller function. You don't even need to pass in the TaskStatus object, as the default instance will be used if you don't.
There are a few test appa within the framework that does this:
https://github.com/edvin/tornadofx/blob/master/src/test/kotlin/tornadofx/testapps/AsyncProgressApp.kt
https://github.com/edvin/tornadofx/blob/master/src/test/kotlin/tornadofx/testapps/TaskStatusTest.kt
That said, a quick and dirty solution for updating the UI from any other thread is simply wrapping the UI manipulation code inside runLater {}. This will work equally well for just updating a label for example.

onApplyWindowInsets(WindowInsets insets) not getting called

I am currently using a custom view that extends a Constraint layout but I it does not trigger this overridden method in the view onApplyWindowInsets(WindowInsets insets) not sure what went missing.
class TestCustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
init {
}
//This method one not get called
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
return super.onApplyWindowInsets(insets)
val statusBarHeight = insets.systemWindowInsetTop
}
override fun fitSystemWindows(insets: Rect): Boolean {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
// Intentionally do not modify the bottom inset. For some reason,
// if the bottom inset is modified, window resizing stops working.
insets.left = 0
insets.top = 0
insets.right = 0
}
return super.fitSystemWindows(insets)
}
}
Once the insets are consumed, propagation down the hierarchy stops. It looks like something higher up is consuming what's available. See isConsumed() of WindowsInset.
Check if these insets have been fully consumed.
Insets are considered "consumed" if the applicable consume* methods have been called such that all insets have been set to zero. This affects propagation of insets through the view hierarchy; insets that have not been fully consumed will continue to propagate down to child views.
I found that without the android:windowTranslucentStatus property set to true, onApplyWindowInsets is never called. I discovered this on reading this unanswered question.
In styles.xml:
<style name="ExampleTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
</style>
I would love to understand why this is so, and if there is a way to have it called without requiring this attribute change.
I think if you wanna reset it's windowInsets callback state, you should change sysUiVisibility params to let fitsSystemWindows work normally. Because fitsSystemWindows is a key to consume the windowInsets. You can learn more from android source code, of course, you need time.

KeyDown actions in OSX?

I have the code all done for my keydown actions, but i dont know what to do with the first responder that every site i go to seems to skim over. Can anyone tell me how to set it up to recognise keydown actions in cocoa objectivec?
Thanks
First, keyDown: is an event message, not an action message. Note that its argument is an NSEvent, not a UI object of some sort (such as an NSControl or NSMenuItem).
Action messages go down the responder chain, in which case the “first responder” is not special. Each responder will hand any action message it doesn't know how to handle off to its next responder. This is the “responder chain”. The first responder is simply whatever responder is at the head of the responder chain—i.e., is first. You would simply need to be in that chain, behind anything that doesn't know how to respond to the action being passed down it.
But since this is an event message, things are different. You need to be the key view, which is the first responder.
And that's all there is to it. You need to respond to the keyDown: message (and possibly related ones) in a view, and that view needs to be the first responder to receive the message.
The NSResponder class reference and Cocoa Event-Handling Guide will tell you more.
Here is what I've done and it works well. (Swift 3 for macOS Sierra)
override func viewDidLoad() {
keyIsDown = false // variable defined in the NSViewController
NSEvent.addLocalMonitorForEvents(matching: .keyUp) { (aEvent) -> NSEvent? in
self.keyUp(with: aEvent)
return aEvent
}
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
self.keyDown(with: aEvent)
return aEvent
}
}
Now I also override these:
override var acceptsFirstResponder: Bool { return true }
override func becomeFirstResponder() -> Bool { return true }
override func resignFirstResponder() -> Bool { return true }
And these:
override func keyUp(with event: NSEvent) {
keyIsDown = false
if event.keyCode == 1 {
print("s key released")
}
}
override func keyDown(with event: NSEvent) {
if keyIsDown == true {
return
}
keyIsDown = true
// Whatever you'd like to do (check to see which key released, etc.)
}
That should get you started.