It's been a year since I used DataBinding last time. It seems something has changed since then or I've made a stupid mistake. Let's look at my problem.
UPD: please, also check the first comment
Long story short
I have a simple ProgressBar in my project:
<data>
<import type="android.view.View" />
<variable
name="vm"
type="/path/to/.SignInViewModel" />
</data>
<layout>
<androidx.constraintlayout.motion.widget.MotionLayout
<ProgressBar
...
android:visibility="#{vm.signInProgress ? View.VISIBLE : View.GONE}" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</layout>
which is visible on the screen until I press the button:
binding.login.setOnClickListener {
viewModel.signInProgress.value = false
}
where signInProgress is just a MutableLiveData
val signInProgress: MutableLiveData<Boolean> = MutableLiveData()
That's all about my interaction with ProgressBar. BUT when I click on login button, my ProgressBar becomes invisible for only a second, then it appears again and begins to rotate. Whaaaaat?
Also I checked generated Binding file and it seems like everything is fine:
androidxDatabindingViewDataBindingSafeUnboxVmSignInProgressGetValue = androidx.databinding.ViewDataBinding.safeUnbox(vmSignInProgressGetValue);
if(androidxDatabindingViewDataBindingSafeUnboxVmSignInProgressGetValue) {
dirtyFlags |= 0x10L;
} else {
dirtyFlags |= 0x8L;
}
vmSignInProgressViewVISIBLEViewGONE = ((androidxDatabindingViewDataBindingSafeUnboxVmSignInProgressGetValue) ? (android.view.View.VISIBLE) : (android.view.View.GONE));
if ((dirtyFlags & 0x7L) != 0) {
this.loginProgress.setVisibility(vmSignInProgressViewVISIBLEViewGONE);
}
Fragment:
binding.lifecycleOwner = this
binding.vm = viewModel
Gradle
// app
android {
buildFeatures {
viewBinding true
dataBinding true
}
}
// project
classpath "com.android.tools.build:gradle:7.0.2"
Can you help me to understand, what's going on here? I can't understand, why it can't just hide? It's my personal project, so I can share a link if you want to help me :(
Ok, I found the cause of the problem, but I haven't find a solution yet. Problem in my MotionLayout (I created the same layout with ConstraintLayout), but I wonder why
Related
There is a large data-bound view, which may take several seconds to inflate. I would like to display the user a splash screen and inflate the main view a delayed action. Android studio throws an exception "Failed to call observer method".
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.screen_splash)
Handler(Looper.getMainLooper()).postDelayed({
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this,
R.layout.activity_main
)
binding.lifecycleOwner = this // this line throws exception
}, 1000)
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.ViewModel"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/map_list"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
</RelativeLayout>
Exception:
2021-12-05 13:42:56.638 23701-23701/com.example E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example, PID: 23701
java.lang.RuntimeException: Failed to call observer method
at androidx.lifecycle.ClassesInfoCache$MethodReference.invokeCallback(ClassesInfoCache.java:226)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeMethodsForEvent(ClassesInfoCache.java:194)
at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeCallbacks(ClassesInfoCache.java:185)
at androidx.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:37)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
at androidx.databinding.ViewDataBinding.setLifecycleOwner(ViewDataBinding.java:434)
at com.example.databinding.ActivityMainBindingImpl.setLifecycleOwner(ActivityMainBindingImpl.java:166)
at com.example.MainActivity.onCreate$lambda-3(MainActivity.kt:106)
at com.example.MainActivity.$r8$lambda$lffeScwTEbHi2B1isKEoQYU2po4(Unknown Source:0)
at com.example.MainActivity$$ExternalSyntheticLambda5.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:888)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:213)
at android.app.ActivityThread.main(ActivityThread.java:8178)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
Caused by: java.lang.NumberFormatException: s == null
at java.lang.Integer.parseInt(Integer.java:577)
at java.lang.Integer.valueOf(Integer.java:801)
at com.example.databinding.ControlPanelBindingImpl.executeBindings(ControlPanelBindingImpl.java:800)...
I am not sure about the structure of your application. In our case we had a similar requirement where we wanted to show a loader until the initial fragment is bound. So we created a viewStub in the activity. Then when the fragment is attached we set a liveData in the shared view model to SHOW which notifies the activity to inflate the viewStub. This way we inflate the view stub which hides the full screen displaying a splash image. Then once the view in the fragment is created and in the onViewCreated we again set the liveData in the shared view model to HIDE which hides the viewStub and the fragment is displayed.
Use fragmentContainerView inside your main activity. Show the view that you want to show in this container. Create a view in front of the container. Show splash message in this view. Make the visibility of the splash view gone when the main view is loaded. So the splash screen will use the activity life cycle and the main view will use the fragment lifecycle. This may be a solution for you.
Disclaimer: turns out that the issue is resolved in UPDATE 2 section
in the answer; the other sections are left if they could help future visitors in other potential issues
At first look, thought that Caused by: java.lang.NumberFormatException: s == null is related to the issue; although you told in comments that it's working synchronously.
And the exception java.lang.RuntimeException: Failed to call observer method won't help to know the error by tracing it in the code.
But your code successfully worked with me in simple layouts; probably the issue is related to the heavy layout that you try to load synchronously along while accessing the binding.lifecycleOwner; I guess the latter snippet requires a while before accessing the lifecycleOwner. So, you could post some delay in advance.
For that, I am going to use coroutines instead of posting a delay; as the code would be more linear and readable:
CoroutineScope(Main).launch {
delay(1000) // Original delay of yours
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this#MainActivity,
R.layout.activity_main
)
delay(1000) // try and error to manipulate this delay
binding.lifecycleOwner = this#MainActivity
}
If not already used, the coroutine dependency is
def coroutine_version = "1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
UPDATE
The posted delay in your code doesn't help in showing the splash/launch screen during that delay while the main activity is loading;
Handler(Looper.getMainLooper()).postDelayed({
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this,
R.layout.activity_main
) // This won't be called unless the 1000 sec is over
binding.lifecycleOwner = this
}, 1000)
What your code does:
A splash screen is shown
A delay is posted (still the main layout is not loading in here)
main layout is shown when the delay is over
So, the posted delay is just accumulating to the time of loading the main layout; this even make it more lagged. Furthermore this is not the recommended way of using splash screen (This medium post would help in that)
Instead, I think what you intend to do:
Show a splash screen
Load main layout
Post a delay so that the main layout takes time to load during the delay
Show the main layout when the delay is over
But, the problem is that the thing need to be loaded is UI which requires to do that in the main thread, not in a background thread. So, we instead of using two different layout and call setContentView() twice; you could instead create a single layout for your main layout, and add some view that represents the splash screen which will obscure the main layout entirely (be in front of it) until the layout is loaded (i.e. the delay is over); then remove this splash view then:
Demo:
splash_screen.xml (Any layout you want that must match parent to obscure it):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/splash_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/black"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#mipmap/ic_launcher" />
</LinearLayout>
Main activity:
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "LOG_TAG"
}
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "Start Inflating layout")
binding = DataBindingUtil.setContentView(
this#MainActivity,
R.layout.activity_main
)
// Show only the first time app launches, not in configuration changes
if (savedInstanceState == null) {
CoroutineScope(IO).launch {
Log.d(TAG, "Start of delay")
delay(1000)
Log.d(TAG, "End of delay")
withContext(Main) {
hideSplash()
}
}
showSplash()
}
binding.lifecycleOwner = this#MainActivity
Log.d(TAG, "End Inflating layout")
}
private fun showSplash() {
supportActionBar?.hide()
// Inflate splash screen layout
val splashLayout =
layoutInflater.inflate(
R.layout.splash_screen,
binding.rootLayout,
false
) as LinearLayout
binding.rootLayout.addView(
splashLayout
)
}
private fun hideSplash() {
supportActionBar?.show()
binding.rootLayout.removeView(
findViewById(R.id.splash_screen)
)
}
}
Logs
2021-12-11 21:59:18.349 20681-20681/ D/LOG_TAG: Start Inflating layout
2021-12-11 21:59:18.452 20681-20707/ D/LOG_TAG: Start of delay
2021-12-11 21:59:18.476 20681-20681/ D/LOG_TAG: End Inflating layout
2021-12-11 21:59:20.457 20681-20707/ D/LOG_TAG: End of delay
Now the delay is running along with inflating the layout; the splash screen shown while it loads; and ends when the delay is over.
UPDATE 2
It's definitely not going to work: databinding = ... line takes 2.5 seconds to complete, can't add a view to "databinding.root" before it's ready. It works in the presented code because your main view is tiny.
Now try to separate inflating the layout from setContentView() in dataBinding; still both requires to be in the main thread
setContentView(R.layout.screen_splash)
CoroutineScope(Main).launch {
// Inflate main screen layout asynchronously
binding = ActivityMainBinding.inflate(layoutInflater)
delay(2500) // 2.5 sec delay of loading the mainLayout before setContentView
setContentView(binding.root)
binding.lifecycleOwner = this#MainActivity
}
Finally found the problem and, in retrospect, it was too elementary for the question:
Must assign ViewModel before lifecycleOwner
binding.viewModel = myViewModer
binding.livecycleOwner = this#MainActivity
Just changing order of these lines fixed it.
I am having trouble getting data-binding to work properly on a BottomSheetDialog layout. Here are the details:
Definition and setting of var:
private lateinit var myDrawerBinding: MyDrawerBinding
myDrawerBinding = MyDrawerBinding.bind(myDrawerContent) // crashes on this line
and later it's set and shown this way (although it never gets to this point)
myDrawerBinding.viewModel = theViewModel
val bottomSheet = BottomSheetDialog(context)
bottomSheet.setContentView(myDrawerBinding.myDrawerContent)
bottomSheet.show()
Here is a snippet of the XML layout (my_drawer.xml):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable name="viewModel" type="path.to.my.viewModel"/>
</data>
<RelativeLayout
android:id="#+id/myDrawerContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<View
android:layout_width="50dp"
android:layout_height="3dp"
android:visibility="#{viewModel.shouldShowView() ? View.VISIBLE : View.GONE}"/>
....
The crash occurs when it calls the .bind() method above, and the error is:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.view.View.getTag()' on a null object reference
This same exact functionality works on a separate DrawerLayout that I am showing in the same Fragment, but for some reason the BottomSheetDialog layout is giving problems.
Finally found a fix for this. I had to treat this view a little differently from my DrawerLayout, although I have a feeling this approach may work for that view as well.
Here is the binding setup:
val myDrawerView = layoutInflater.inflate(R.layout.my_drawer, null)
val binding = MyDrawerBinding.inflate(layoutInflater, myDrawerView as ViewGroup, false)
binding.viewModel = theViewModel
And then to show the view:
val bottomSheetDialog = BottomSheetDialog(context)
bottomSheetDialog.setContentView(binding.myDrawerContent)
bottomSheetDialog.show()
Works like a charm now!
ConstraintLayout intermittently fails to layout correctly when a view is set from GONE to VISIBLE shortly after an activity is resumed:
<android.support.constraint.ConstraintLayout
android:id="#+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<TextView
android:id="#+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="#+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="#id/text1"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
override fun onResume() {
super.onResume()
text1.text = ""
text1.visibility = View.GONE
text2.text = ""
text2.visibility = View.GONE
text1.postDelayed({
text1.text = "Hello"
text1.visibility = View.VISIBLE
text2.text = "World"
text2.visibility = View.VISIBLE
}, 100
)
}
Full source code here
Instrumenting the TextView class reveals that the TextView instances are measured correctly but their width is set to 0 when they are laid out.
I wonder if the ConstraintLayout LinearSystem is non-deterministic. Are there maps that are iterated over where the iteration order is undefined? (I've seen this with Cassowary)
I'm looking to your statement in github page:
ConstraintLayout intermittently fails to layout correctly when a view is set from GONE to VISIBLE shortly after an activity is resumed
I've checked out your project and changed 100ms to 1000ms.
Here's the output:
It seems to me, that you expect that the moment you perform textview.setVisibility(View.GONE) you expect the view to be not visible. That's not the way android works. You are merely posting an event to the MessageQueue that would be handled later by Looper, and this 100ms is not enough for human eye to see those changes happening.
This was a bug in the ConstraintLayout fixed in constraint-layout:1.1.0-beta2 https://issuetracker.google.com/issues/65613481
After upgrading to Gradle 2.3. My project cannot compile. I'm having the log in the console
incompatible types: ObservableInt cannot be converted to int
Look at the generated file
android.databinding.ObservableInt viewModelLoadingVisibility;
this.vLoading.getRoot().setVisibility(viewModelLoadingVisibility);
In xml file
<android.support.v7.widget.RecyclerView
android:id="#+id/rvProducts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
android:visibility="#{viewModel.contentVisibility}"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
I tried in method in my binding class
#BindingAdapter("app:visibility")
public static void setViewVisible(View view, ObservableInt visible) {
int visibility = visible.get();
view.setVisibility(visibility);
}
and got log
warning: Application namespace for attribute app:visibility will be ignored.
public static void setViewVisible(View view, ObservableInt visible) {
warning: Use of ObservableField and primitive cousins directly as method parameters is deprecated and support will be removed soon. Use the contents as parameters instead in method public static void setViewVisible(android.view.View,android.databinding.ObservableInt)
public static void setViewVisible(View view, ObservableInt visible) {
Anyone encounters this?
This looks like a bug. Please file it. There are many tests and we don't expect this kind of regression. It is important that we get your specific example so we can be sure it is caught.
You can ignore the warnings for now.
The first is caused because of this:
#BindingAdapter("app:visibility")
You should use this instead:
#BindingAdapter("visibility")
The second is because we support ObservableInt as a parameter. You typically don't want to accept ObservableInt, but rather int instead. I'd love to see use cases where ObservableInt is necessary. We may just remove that warning and support it always or we may pull the plug on supporting ObservableInt as a parameter if there are no valid uses.
----- edit -----
I tested this with a little application and I didn't have any issue without any BindingAdapter. Here is the layout:
<layout>
<data>
<variable name="model" type="com.example.gmount.testobservableint.MyModel"/>
</data>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="#{model::clicked}"
tools:context="com.example.gmount.testobservableint.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="#{model.visibility}"
/>
</android.support.constraint.ConstraintLayout>
</layout>
Here is my model:
public class MyModel {
public final ObservableInt visibility = new ObservableInt(View.VISIBLE);
public void clicked(View view) {
int oldVisibility = visibility.get();
int newVisibility = oldVisibility == View.VISIBLE ? View.GONE : View.VISIBLE;
visibility.set(newVisibility);
}
}
Even when I used a BindingAdapter taking an ObservableInt, it worked. Here's my BindingAdapter:
#BindingAdapter("visiblity")
public static void setVisibility(View view, ObservableInt visibility) {
view.setVisibility(visibility.get());
}
And I changed the View's binding to be:
<TextView ...
app:visibility="#{model.visibility}"
/>
Is there something different about your viewModel?
You just have to add this to the bottom of your build.gradle dependencies:
apt 'com.android.databinding:compiler:2.3.0'
See this: https://stackoverflow.com/a/42711830/376829 regarding GoMobile update to version "+eb90329 Mar 7 2017" and GoBind plugin revert to version "0.2.6" (although the current version is "0.2.8")
android:visibility="#{viewModel.contentVisibility}"
remember this
dataBinding {
enabled = true
}
re-download the library from the Support repository in the Android SDK manager.
I'm having a tough time getting a basic MvvmCross Android example working where the BackgroundColor of the RelativeLayout is bound to the ViewModel.
The app runs, some text appears, and I'm expecting my background to turn Yellow. The background color, however, remains unchanged.
I have included the Hot Tuna starter pack in both my Core and Droid projects as well as the MvvmCross - Color Plugin. My Droid project was automatically given ColorPluginBootstrap.cs
Layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="BackgroundColor NativeColor(BackgroundColor)">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:gravity="center"
android:text="Text to make sure the layout inflates" />
</RelativeLayout>
ViewModel
public class ViewModel : MvxViewModel
{
RGBAColorConverter _rgbaConverter;
public ViewModel()
{
var color = "#ffedff00";
_rgbaConverter = new RGBAColorConverter();
BackgroundColor = _rgbaConverter.Convert(color);
}
private MvxColor _backgroundColor;
public MvxColor BackgroundColor
{
get { return _backgroundColor; }
set
{
_backgroundColor = value;
RaisePropertyChanged(() => BackgroundColor);
}
}
}
Binding works - I've tried making other ViewModel properties that were string to do simple text binding. All of that seems just fine.
I've placed debugging break points on the getter of the BackgroundColor ViewModel property and I can see the MvxColor as expected.
What am I missing for my color binding scenario?
I haven't done anything extra in the Setup.cs
I haven't created any other wiring up classes in my Droid project
I haven't created any Android-specific color converter implementations
I've just written a test app and it seems to work for me - using 3.0.14 nuget binaries.
Also, the ValueConverters test app seemed to work OK - https://github.com/MvvmCross/MvvmCross-Tutorials/tree/master/ValueConversion
Looking at your sample, the only thing I can think of is that maybe you are only testing transparent colours (RGBA #ffedff00 has Alpha=0)
If that isn't it, can you post more - perhaps a full sample somewhere?