From here we now know that robolectric does not have a shadow object but we can create a custom shadow object for a snackbar.It's ashame they have one for toast but not for snackbar.
I am showing a snackbar in my code when there is no network connection. I'd like to know how can i write a unit test (with robolectric as the test runner) that can verify that a snackbar gets shown when there is no network connection.
Its a little hard because the snackbar is not in xml. So when i declare my actually Activity controller it doesn't have a snackbar at that time.
You know how to test a toast we have ShadowToast.getTextOfLatestToast() i want one for snackBar
im currently using org.robolectric:robolectric:3.0-rc2 and dont see ShadowSnackbar.class available.
It's actually explained in the blogpost how to add the ShadowToast class to enable testing.
Add the ShadowSnackbar to your test sources;
Add the Snackbar class as an instrumented class in your custom Gradle test runner;
Add the ShadowSnackbar as a Shadow in your test;
In your app's code, you'll call on the Snackbar when no internet connection is available. Because of the configuration (e.g. intercepting of) the Snackbar as Instrumented class, the Shadow-variant of the class will be used. You'll be able to evaluate the result at that moment.
I posted a much simpler answer
you can just do:
val textView: TextView? = rootView.findSnackbarTextView()
assertThat(textView, `is`(notNullValue()))
Implementation:
/**
* #return a TextView if a snackbar is shown anywhere in the view hierarchy.
*
* NOTE: calling Snackbar.make() does not create a snackbar. Only calling #show() will create it.
*
* If the textView is not-null you can check its text.
*/
fun View.findSnackbarTextView(): TextView? {
val possibleSnackbarContentLayout = findSnackbarLayout()?.getChildAt(0) as? SnackbarContentLayout
return possibleSnackbarContentLayout
?.getChildAt(0) as? TextView
}
private fun View.findSnackbarLayout(): Snackbar.SnackbarLayout? {
when (this) {
is Snackbar.SnackbarLayout -> return this
!is ViewGroup -> return null
}
// otherwise traverse the children
// the compiler needs an explicit assert that `this` is an instance of ViewGroup
this as ViewGroup
(0 until childCount).forEach { i ->
val possibleSnackbarLayout = getChildAt(i).findSnackbarLayout()
if (possibleSnackbarLayout != null) return possibleSnackbarLayout
}
return null
}
this is what worked for me, but it was a very simple use case
#Implements(Snackbar::class)
class CustomShadowSnackbar {
companion object {
val shownSnackbars = mutableListOf<Snackbar>()
fun Snackbar.getTextMessage(): String {
val view = (this.view as ViewGroup)
.children
.first { it is SnackbarContentLayout } as SnackbarContentLayout
return view.messageView.text.toString()
}
fun clear() {
shownSnackbars.clear()
}
}
#RealObject
lateinit var snackbar: Snackbar
#Implementation
fun show() {
shownSnackbars.add(snackbar)
}
#Implementation
fun __constructor__(
context: Context,
parent: ViewGroup,
content: View,
contentViewCallback: ContentViewCallback) {
Shadow.invokeConstructor(
Snackbar::class.java,
snackbar,
ReflectionHelpers.ClassParameter(Context::class.java, context),
ReflectionHelpers.ClassParameter(ViewGroup::class.java, parent),
ReflectionHelpers.ClassParameter(View::class.java, content),
ReflectionHelpers.ClassParameter(ContentViewCallback::class.java, contentViewCallback)
)
}
}
Related
I'm developing a huge section of my Android app in Jetpack Compose with the MVVM pattern.
I have a ViewModel father that is extended by all the other ViewModels. There, I have defined an open function which contains the initialization logic of each ViewModel that I need to call every time I enter in a new screen and to call again when something went wrong and the user clicks on the "try again" button.
abstract class MyFatherViewModel(): ViewModel() {
open fun myInitMethod() {}
fun onTryAgainClick() {
myInitMethod()
}
}
class MyScreen1ViewModel(): MyFatherViewModel() {
init {
myInitMethod()
}
override fun myInitMethod() {
super.myInitMethod()
// Do something
}
}
class MyScreen2ViewModel(): MyFatherViewModel() {
init {
myInitMethod()
}
override fun myInitMethod() {
super.myInitMethod()
// Do something
}
}
Is there a way I can call this method in the init function of MyFatherViewModel instead of doing it in all the children ViewModels? If I try to do that, it gives me the "Calling non-final function in constructor" warning and, of course, it doesn't work.
abstract class MyFatherViewModel(): ViewModel() {
open fun myInitMethod() {}
init {
myInitMethod()
}
fun onTryAgainClick() {
myInitMethod()
}
}
Is it possible to call a non-final function in constructor?
Technically yes, but you shouldn't. Kotlin is trying to protect you from problems here. If you call an open function from a constructor, it means you are running code from the child class before the parent class is completely initialized, and before the child class even started initializing. If the child implementation of the open function tries to access properties from the child class, unexpected things may happen. For instance, non-nullable properties could yield null (because not initialized yet), or primitive values could yield their type's default instead of the default value from their initializer:
fun main() {
Child()
}
open class Parent {
init {
initialize()
}
val id = 42
open fun initialize() = println("parent init")
}
class Child : Parent() {
val name = "Bob"
override fun initialize() = println("initializing $name, parent id=$id")
}
This prints the following output:
initializing null, parent id=0
I guess you can see why this is dangerous.
Maybe you should reconsider what you're trying to do with this try-again feature. Maybe a new view model should be instantiated instead (if try-again is to handle crashes, the state of the current view model may actually be bad enough to want to re-create it from scratch anyway).
I have a test that checks to see if a dialog is present or not.
#Test
fun dismissedWhenClicked() {
//dimiss dialog
onView(withText(R.string.simple)).inRoot(isDialog()).perform(click())
//check dialog
onView(isRoot()).inRoot(isDialog()).check(matches(not(isDisplayed())))
}
above is my best guess, but fails because Matcher 'is dialog' did not match any of the following roots
i have found 3 questions on here that address it but none seem to solve it.
Espresso check if no dialog is displayed - the comment works but it also passes when there is a dialog
Check the dialog is visible - Espresso - this doesn't check, instead it will just fail gracefully, i think.
espresso: Assert a Dialog is not shown - seems to have no answer.
I have solved this with a custom matcher modified slightly from here
#Test
fun dismissedWhenClicked() {
onView(withText(R.string.simple)).inRoot(isDialog()).perform(click())
onView(withId(R.id.fragment_layout)).inRoot(Utils.ActivityMatcher()).check(matches(isDisplayed()))
}
class ActivityMatcher : TypeSafeMatcher<Root>() {
override fun describeTo(description: Description) {
description.appendText("is activity")
}
public override fun matchesSafely(root: Root): Boolean {
val type: Int = root.windowLayoutParams.get().type
if (type == WindowManager.LayoutParams.TYPE_BASE_APPLICATION) {
val windowToken: IBinder = root.decorView.windowToken
val appToken: IBinder = root.decorView.applicationWindowToken
if (windowToken === appToken) {
//means this window isn't contained by any other windows.
return true
}
}
return false
}
}
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.