You can try to create a custom wait action with both IdlingResource and ViewAction. The simplest way is to create an IdlingResource callback that listens to ViewTreeObserver.OnDrawListener and determines when the app is idling by matching with a Matcher<View>:
private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener {
    private lateinit var callback: IdlingResource.ResourceCallback
    private var matched = false
    override fun getName() = "View property change callback"
    override fun isIdleNow() = matched
    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
        this.callback = callback
    }
    override fun onDraw() {
        matched = matcher.matches(view)
        callback.onTransitionToIdle()
    }
}
Then create a custom ViewAction to wait for a match:
fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction {
    override fun getConstraints(): Matcher<View> {
        return any(View::class.java)
    }
    override fun getDescription(): String {
        return StringDescription().let {
            matcher.describeTo(it)
            "wait until: $it"
        }
    }
    override fun perform(uiController: UiController, view: View) {
        if (!matcher.matches(view)) {
            ViewPropertyChangeCallback(matcher, view).run {
                try {
                    IdlingRegistry.getInstance().register(this)
                    view.viewTreeObserver.addOnDrawListener(this)
                    uiController.loopMainThreadUntilIdle()
                } finally {
                    view.viewTreeObserver.removeOnDrawListener(this)
                    IdlingRegistry.getInstance().unregister(this)
                }
            }
        }
    }
}
And perform this action on a root view:
fun waitForElement(matcher: Matcher<View>) {
    onView(isRoot()).perform(waitUntil(hasDescendant(matcher))
}
...
waitForElement(allOf(withId(R.id.elementID), isDisplayed()))
Or if the view's property can change asynchronously, you can do:
onView(withId(R.id.elementID)).perform(waitUntil(withText("textChanged!")))
Also, this action may not be suitable for activity transitions.