I am trying to override View.setRotation() method in Kotlin.
Since AndroidKTX already provided property extension "rotation", caller's can simply call
viewObject.rotation = 90.0f
to rotate the view.
However, I want to add some additional operation when user change the rotation, like
override fun setRotation(newRotation: Float) {
if (rotation == newRotation)
return
rotation = newRotation
doSomethingElse()
}
This will crash because of StackOverflow error.
So, I have to add some additional code to achieve the goal:
private var _rotation: Float = 0.0f
override fun setRotation(newRotation: Float) {
if (_rotation == newRotation) {
return
}
_rotation = newRotation
updateRotationInternally()
}
private fun updateRotationInternally() {
super.setRotation(_rotation)
doSomethingElse()
}
This works, but I wonder if there is some other more elegant way of doing this, like "override the property extension setter"?
I disagree with one aspect of your implementation: your return shortcut. You are assuming that calling setRotation() with the existing rotation value has no effect. It would not surprise me if that is true in the official Google version of View, but for all we know, that is not a safe assumption on some Oppo device running their modified version of Android 8.0. Try not to assume the behavior of stuff that you didn't write. If you want to skip doSomethingElse() when the old and new rotations are equal, that's fine.
I am guessing that your setRotation() functions are in some subclass of View. If so, and taking my above complaint into account, here's the simplest that I could come up with:
class Bar : View() {
override fun setRotation(f: Float) {
val needSomething = getRotation() != f
super.setRotation(f)
if (needSomething) doSomethingElse()
}
fun doSomethingElse() {
println("got here!")
}
}
My overall test code was done in a Kotlin scratchpad (outside of Android), so I tested with a fake View implementation and fake rotation extension property:
open class View {
private var r: Float = 0.0f
open fun setRotation(f: Float) {
r = f
}
fun getRotation() = r
}
var View.rotation: Float
get() = getRotation()
set(value) = setRotation(value)
class Bar : View() {
override fun setRotation(f: Float) {
val needSomething = getRotation() != f
super.setRotation(f)
if (needSomething) doSomethingElse()
}
fun doSomethingElse() {
println("got here!")
}
}
fun main() {
val bar = Bar()
bar.rotation = 15.0f
bar.rotation = 15.0f
}
If you run this in that scratchpad, you will see got here printed on the console once, showing that while we successfully updated the extension property twice, we skipped getSomethingElse() on the second call, as the old and new rotation were the same.
Related
There is a fun method0:
private fun method0() {
println("method0 fun")
}
And a var method0 :
var method0 = {
println("method0")
}
It seems they are used the same:
method0()
I found that both occur at the same time, and the fun function has a higher priority when the code calls.
Other than that, is there any difference between them?
The var way of doing it results in a functional object. The lambda content is wrapped as a functional object so it can be passed around like any other instance of a class. It can directly be used as a function parameter, for instance.
var method0 = {
println("method0")
}
fun doSomethingTwice(action: ()->Unit) {
repeat(2) { action() }
}
fun main() {
doSomethingTwice(method0)
}
And since it's marked as a var you can swap it out for a different function:
fun main() {
method0 = { println("hello, world!") }
doSomethingTwice(method0)
}
Note that this way of specifying a function is a little bit heavier since it is wrapping the function in another class instance.
And you can still wrap any "regular" function into a functional object at any time by using :: to avoid doing it until it's necessary.
fun method0() {
println("method0")
}
fun main() {
doSomethingTwice(::method0)
}
What is a proper way to communicate between the ViewModel and the View, Google architecture components give use LiveData in which the view subscribes to the changes and update itself accordingly, but this communication not suitable for single events, for example show message, show progress, hide progress etc.
There are some hacks like SingleLiveEvent in Googles example but it work only for 1 observer.
Some developers using EventBus but i think it can quickly get out of control when the project grows.
Is there a convenience and correct way to implement it, how do you implement it?
(Java examples welcome too)
Yeah I agree, SingleLiveEvent is a hacky solution and EventBus (in my experience) always lead to trouble.
I found a class called ConsumableValue a while back when reading the Google CodeLabs for Kotlin Coroutines, and I found it to be a good, clean solution that has served me well (ConsumableValue.kt):
class ConsumableValue<T>(private val data: T) {
private var consumed = false
/**
* Process this event, will only be called once
*/
#UiThread
fun handle(block: ConsumableValue<T>.(T) -> Unit) {
val wasConsumed = consumed
consumed = true
if (!wasConsumed) {
this.block(data)
}
}
/**
* Inside a handle lambda, you may call this if you discover that you cannot handle
* the event right now. It will mark the event as available to be handled by another handler.
*/
#UiThread
fun ConsumableValue<T>.markUnhandled() {
consumed = false
}
}
class MyViewModel : ViewModel {
private val _oneShotEvent = MutableLiveData<ConsumableValue<String>>()
val oneShotEvent: LiveData<ConsumableValue<String>>() = _oneShotData
fun fireEvent(msg: String) {
_oneShotEvent.value = ConsumableValue(msg)
}
}
// In Fragment or Activity
viewModel.oneShotEvent.observe(this, Observer { value ->
value?.handle { Log("TAG", "Message:$it")}
})
In short, the handle {...} block will only be called once, so there's no need for clearing the value if you return to a screen.
What about using Kotlin Flow?
I do not believe they have the same behavior that LiveData has where it would alway give you the latest value. Its just a subscription similar to the workaround SingleLiveEvent for LiveData.
Here is a video explaining the difference that I think you will find interesting and answer your questions
https://youtu.be/B8ppnjGPAGE?t=535
try this:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
And wrapper it into LiveData
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
And observe
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
link reference: Use an Event wrapper
For showing/hiding progress dialogs and showing error messages from a failed network call on loading of the screen, you can use a wrapper that encapsulates the LiveData that the View is observing.
Details about this method are in the addendum to app architecture:
https://developer.android.com/jetpack/docs/guide#addendum
Define a Resource:
data class Resource<out T> constructor(
val state: ResourceState,
val data: T? = null,
val message: String? = null
)
And a ResourceState:
sealed class ResourceState {
object LOADING : ResourceState()
object SUCCESS : ResourceState()
object ERROR : ResourceState()
}
In the ViewModel, define your LiveData with the model wrapped in a Resource:
val exampleLiveData = MutableLiveData<Resource<ExampleModel>>()
Also in the ViewModel, define the method that makes the API call to load the data for the current screen:
fun loadDataForView() = compositeDisposable.add(
exampleUseCase.exampleApiCall()
.doOnSubscribe {
exampleLiveData.setLoading()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
exampleLiveData.setSuccess(it)
},
{
exampleLiveData.setError(it.message)
}
)
)
In the View, set up the Observer on creation:
viewModel.exampleLiveData.observe(this, Observer {
updateResponse(it)
})
Here is the example updateResponse() method, showing/hiding progress, and showing an error if appropriate:
private fun updateResponse(resource: Resource<ExampleModel>?) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
showProgress()
}
ResourceState.SUCCESS -> {
hideProgress()
// Use data to populate data on screen
// it.data will have the data of type ExampleModel
}
ResourceState.ERROR -> {
hideProgress()
// Show error message
// it.message will have the error message
}
}
}
}
You can easily achieve this by not using LiveData, and instead using Event-Emitter library that I wrote specifically to solve this problem without relying on LiveData (which is an anti-pattern outlined by Google, and I am not aware of any other relevant alternatives).
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
implementation 'com.github.Zhuinden:event-emitter:1.0.0'
If you also copy the LiveEvent class , then now you can do
private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter
fun doSomething() {
emitter.emit("hello")
}
And
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = getViewModel<MyViewModel>()
viewModel.events.observe(viewLifecycleOwner) { event ->
// ...
}
}
// inline fun <reified T: ViewModel> Fragment.getViewModel(): T = ViewModelProviders.of(this).get(T::class.java)
For rationale, you can check out my article I wrote to explain why the alternatives aren't as valid approaches.
You can however nowadays also use a Channel(UNLIMITED) and expose it as a flow using asFlow(). That wasn't really applicable back in 2019.
I'm using the Android spring animation in my project (see here). However, these animations are getting in the way of my espresso tests.
I already tried to disable these animations using the developer options in the phone, but they seem to not be affected by these settings.
Is there any way how I can disable them just for tests?
After struggling with a flaky test due to SpringAnimations I came up with three solutions:
Solution 1: Add a function that wraps creating your SpringAnimations
This is the most invasive in terms of changing existing code, but least complex method to follow:
You can check if animations are disabled at runtime:
fun animationsDisabled() =
Settings.Global.getFloat(
contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f,
) == 0.0f
Then selectively return a dummy animation that immediately finishes while also setting the value to it's final state:
fun <K : View?> createAnimation(
target: K,
property: FloatPropertyCompat<K>,
finalValue: Float
) = if (animationsDisabled() == false) {
SpringAnimation(target, property, finalValue).apply {
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
}
} else {
property.setValue(target, finalValue)
SpringAnimation(FloatValueHolder(0f)).apply{
spring = SpringForce(100f)
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
addUpdateListener { _, _, _ -> skipToEnd() }
}
}
}
Solution 2: Create an IdlingResource that tells Espresso if a DynamicAnimation is running
SpringAnimation and FlingAnimation both extend from DynamicAnimation, the class which is ignoring the system Animation Scale and causing issues here.
This solution isn't the prettiest as it uses reflection, but the implementation details it relies on haven't changed since DynamicAnimation was introduced.
Based on DataBindingIdlingResource:
import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID
// An espresso idling resource implementation that reports idle status for all DynamicAnimation instances
class DynamicAnimationIdlingResource(private val activityScenarioRule: ActivityScenarioRule<*>) :
IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
override fun getName() = "DynamicAnimation $id"
override fun isIdleNow(): Boolean {
val idle = !getDynamicAnimations().any { it.isRunning }
#Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
activityScenarioRule.scenario.onActivity {
it.findViewById<View>(android.R.id.content)
.postDelayed({ isIdleNow }, 16)
}
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getDynamicAnimations(): List<DynamicAnimation<*>> {
val dynamicAnimations = mutableListOf<DynamicAnimation<*>>()
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
if (it is DynamicAnimation<*>) {
dynamicAnimations.add(it)
}
}
return dynamicAnimations
}
}
For convenience a matching test rule:
/**
* A JUnit rule that registers an idling resource for all animations that use DynamicAnimations.
*/
class DynamicAnimationIdlingResourceRule(activityScenarioRule: ActivityScenarioRule<*>) : TestWatcher() {
private val idlingResource = DynamicAnimationIdlingResource(activityScenarioRule)
override fun finished(description: Description?) {
IdlingRegistry.getInstance().unregister(idlingResource)
super.finished(description)
}
override fun starting(description: Description?) {
IdlingRegistry.getInstance().register(idlingResource)
super.starting(description)
}
}
This isn't the perfect solution since it will still cause your tests to wait for animations despite changing the animation scale globally
If you have infinite animations based on SpringAnimations (by setting Damping to zero), this won't work as it'll always report to Espresso that an animation is running. You could work around that by casting the DynamicAnimation to a SpringAnimation and checking if Damping was set, but I felt like that's a rare enough case to not complicate things.
Solution 3: Force all SpringAnimations to skip to their last frame
Another reflection based solution, but this one completely disables the SpringAnimations. The trade-off is that theoretically Espresso can still try to interact in the 1 frame window between a SpringAnimation being asked to end, and it actually ending.
In practice I had to rerun the test hundreds of times in a row to get this to happen, at which point the animation may not even be the source of flakiness. So the trade-off is probably worth it if the animations are dragging down how long your tests take to complete:
private fun disableSpringAnimations() {
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
CoroutineScope(Dispatchers.IO).launch {
while (true) {
withContext(Dispatchers.Main) {
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
val animation = it as? SpringAnimation
if (animation?.isRunning == true && animation.canSkipToEnd()) {
animation.skipToEnd()
animation.doAnimationFrame(100000L)
}
}
}
delay(16L)
}
}
}
Call this method in your #Before annotated function to have it run before each test.
In the SpringAnimation implementation, skipToEnd sets a flag that is not checked until the next call to doAnimationFrame, hence the animation.doAnimationFrame(100000L) call.
I have tried the new BottomSheetBehaviour with design library 23.0.2 but i think it too limited. When I change state with setState() method, the bottomsheet use ad animation to move to the new state.
How can I change state immediately, without animation? I don't see a public method to do that.
Unfortunately it looks like you can't. Invocation of BottomSheetBehavior's setState ends with synchronous or asynchronous call of startSettlingAnimation(child, state). And there is no way to override these methods behavior cause setState is final and startSettlingAnimation has package visible modifier. Check the sources for more information.
I have problems with the same, but in a bit different way - my UI state changes setHideable to false before that settling animation invokes, so I'm getting IllegalStateException there. I will consider usage of BottomSheetCallback to manage this properly.
If you want to remove the show/close animation you can use dialog.window?.setWindowAnimations(-1). For instance:
class MyDialog(): BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setDimAmount(0f) // for removing the dimm
dialog.window?.setWindowAnimations(-1) // for removing the animation
return dialog
}
}
If you really need it, then you can resort to reflection:
fun BottomSheetBehavior.getViewDragHelper(): ViewDragHelper? = BottomSheetBehavior::class.java
.getDeclaredField("viewDragHelper")
.apply { isAccessible = true }
.let { field -> field.get(this) as? ViewDragHelper? }
fun ViewDragHelper.getScroller(): OverScroller? = ViewDragHelper::class.java
.getDeclaredField("mScroller")
.apply { isAccessible = true }
.let { field -> field.get(this) as? OverScroller? }
Then you can use these extension methods when the state changes:
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onSlide(view: View, offset: Float) {}
override fun onStateChanged(view: View, state: Int) {
if (state == STATE_SETTLING) {
try {
bottomSheetBehavior.getViewDragHelper()?.getScroller()?.abortAnimation()
} catch(e: Throwable) {}
}
}
})
I will add that the code is not perfect, getting fields every time the state changes is not efficient, and this is done for the sake of simplicity.
is there somebody who can explain me what "with" function is used for?
Signature
public inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
Doc
Calls the specified function f with the given receiver as its receiver and returns its result.
And I found its using on this project Antonio Leiva. It was using for moving view :
fun View.animateTranslationY(translationY: Int, interpolator: Interpolator) {
with(ObjectAnimator.ofFloat(this, "translationY", translationY.toFloat())) {
setDuration(context.resources.getInteger(R.integer.config_mediumAnimTime).toLong())
setInterpolator(interpolator)
start()
}
}
I was thinking that I know the meaning to I transfer it to
fun View.animateTranslationX(translationX: Int, interpolator: Interpolator) {
with(ObjectAnimator()) {
ofFloat(this, "translationX", translationX.toFloat())
setDuration(context.resources.getInteger(R.integer.config_mediumAnimTime).toLong())
setInterpolator(interpolator)
start()
}
}
but it doesn't compile ... But I think that ObjectAnimaton is receiver and it get everything what I will call in {} bracket. Can anybody explain the real meaning and provide a basic example - at least more basic than this? :D
The idea is the same as with keyword in Pascal.
Anyway, here are three samples with identical semantic:
with(x) {
bar()
foo()
}
with(x) {
this.bar()
this.foo()
}
x.bar()
x.foo()
I think that I understood what "with" do. Look at code:
class Dummy {
var TAG = "Dummy"
fun someFunciton(value: Int): Unit {
Log.d(TAG, "someFunciton" + value)
}
}
fun callingWith(): Unit {
var dummy = Dummy()
with(dummy, {
someFunciton(20)
})
with(dummy) {
someFunciton(30)
}
}
If I run this code I get one calling of someFunciton with 20 and then with 30 param.
So the code above can be tranfer to this :
fun View.animateTranslationX(translationX: Int, interpolator: Interpolator) {
var obj = ObjectAnimator()
with(obj) {
ofFloat(this, "translationX", translationX.toFloat())
setDuration(context.resources.getInteger(R.integer.config_mediumAnimTime).toLong())
setInterpolator(interpolator)
start()
}
}
and I should work - so we have to have var.