I'm working with this network graph library.
I have a GraphSurfaceView in my layout.
This view displays graph by giving a NetworkGraph object to it, something like this:
graphSurface.init(networkGraph)
How can I bind networkGraph data to graphSurface in MVVM architecture?
I tried this code below and it didn't work, I should call init after creating my networkGraph:
BindingUtils.kt
#BindingAdapter("networkGraph")
#JvmStatic
fun setNetworkGraph(view: GraphSurfaceView, networkGraph: NetworkGraph) {
view.init(networkGraph)
}
myActivity.kt
relationActivityBinding = DataBindingUtil.setContentView(this, getLayoutId())
graphSurface = relationActivityBinding.graphSurface
viewModel.kt
fun getNetworkGraph() = this.graphManager?.getGraphNetwork() // returns my networkGraph
activity.xml
<giwi.org.networkgraph.GraphSurfaceView
android:id="#+id/graphSurface"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:networkGraph="#{viewModel.getNetworkGraph()}"/>
Related
I have 5 fragments and one parent fragment which takes care of the fragment navigation. All these fragments use a common (shared) view model to share data across all fragments. But when I try to call a method from XML on button's onClick, it doesn't get called. I added breakpoint but the debugger didn't stop there at all.
Moreover, I have some visibility conditions defined in the view which are also using viewmodel reference and those don't execute either.
Below is the code of button click
<androidx.appcompat.widget.AppCompatButton
android:id="#+id/previousBTN"
style="#style/buttonStyleWithFillColor"
android:layout_width="wrap_content"
android:layout_height="#dimen/common_44"
android:layout_marginEnd="#dimen/common_50"
android:layout_marginBottom="#dimen/common_15"
android:clickable="#{!viewModel.isHorizontalProgress()}"
android:fontFamily="#font/lato_bold_style"
android:gravity="center"
android:onClick="#{()->viewModel.previous()}"
android:paddingStart="#dimen/common_20"
android:paddingEnd="#dimen/common_20"
android:text="#string/submit"
android:visibility="#{viewModel.currentTabIndex==5? View.VISIBLE:View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
In the above code, you can see a method call on button click and a visibility condition . None of them works.
CurrentTabIndex is a mutable live data defined in viewModel
var currentTabIndex = MutableLiveData(1)
This is the viewModel class
class EmpGPViewModel #ViewModelInject constructor(application: Application): BaseViewModel<EmpGPNavigator>(application) {
var model=MutableLiveData<EmpGPModel>()
var currentTabIndex = MutableLiveData(1)
fun selectDate() {
navigator.selectDate()
}
fun selectOutTime() {
navigator.selectOutTime()
}
fun next() {
navigator.next()
}
fun previous() {
navigator.previous()
}
fun submit() {
navigator.submit()
}}
I am also using Hilt in this project for dependency injection. I also have the same code setup on a different screen only difference is that it doesn't use a shared view model and there all of this code works.
I have a base fragment class which provides an overridden method to instantiate viewModel.
This is how I do it.
override val viewModel: EmpGPViewModel by viewModels()
Base fragment class has calls below code
viewDataBinding?.lifecycleOwner = viewLifecycleOwner
viewDataBinding?.setVariable(bindingVariable, mViewModel)
viewDataBinding?.executePendingBindings()
I'm creating the app uses API and I have function inside the ViewModel which needs name of the city to fetch data from web. But it does not work. I have already worked with two way data binding in Java Android, but in Kotlin case something is wrong. In the CityViewModel I have an ObservableField which is binded with an Edit Text. This is place for name of city, then after button click is launching val currentWeatherByCity, but in logs is response error from API. If I set just string to the currentWeatherByCity as for example "London" API works, but if I want to use ObservableField app crashes.
CityViewModel:
class CityViewModel(
private val weatherRepository: WeatherRepository
): ViewModel() {
val city = ObservableField<String>()
private val metric: String = "metric"
val currentWeatherByCity by lazyDeferred {
weatherRepository.getCurrentWeatherByCity(city.get().toString(), metric)
}
}
CityActivity:
private fun bindUI() = launch(Dispatchers.Main) {
val cityWeather = cityViewModel.currentWeatherByCity.await()
cityWeather.observe(this#CityActivity, Observer {
if (it == null) return#Observer
})
ActivityCity xml:
<EditText android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center"
android:foregroundGravity="center_vertical"
android:textColor="#android:color/white"
android:textSize="20sp"
android:text="#={cityViewModel.city}"
android:hint="London/London,uk"
android:id="#+id/city_edit_text"
android:layout_marginEnd="50dp"
app:layout_constraintEnd_toStartOf="#+id/search_city_button"
android:inputType="text"
android:autofillHints="username"/>
I also checked the ObservableField in logs and city is null. If I set the String inside ObservableField then is right and API fetches data.
Android data binding does not observe kotlin's liveData builder
the following code will create a LiveData and it's supposed to be observed by data binding in XML but it doesn't work
val text =
liveData(Dispatchers.Default) {
emit("Hello")
}
on the other hand if it's gets observed in Kotlin it works fine
vm.text.observe(lifeCycleOwner,{
binding.texti.text = it
})
the xml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/texti"
android:text="#{viewModel.text}"
/>
and if i change the definition of live data to:
val text = MutableLiveData("Hello")
data binding automatically observes and sets the text
Am i doing it wrong or it's a bug?
Did you set lifecycle owner of the binding? The binding initialization should look like:
override fun onCreate(savedInstanceState: Bundle?) {
val binding = DataBindingUtil.setContentView<MyActivityBinding>(this, R.layout.my_activity)
binding.setLifecycleOwner(this)
binding.viewModel = viewModel.get()
...
}
Fore more details, see: Use LiveData to notify the UI about data changes
I have two variables inside my layout file :
<data>
<variable name="createExpenseViewModel" type="com.lionosur.dailyexpenses.viewModels.MainViewModel"/>
<variable name="createExpenseConverter" type="com.lionosur.dailyexpenses.converters.createExpenseActivityConverter.Companion"/>
</data>
My view model has an method to return the live data :
fun getAllExpenseItems(): LiveData<List<Expense>> {
return expenseRepository.getAllExpenseItems()
}
I need to observe this data and populate an spinner,
class createExpenseActivityConverter {
// contains all the static methods to convert the data for the ui
companion object {
fun getExpenseCategoryListFromSource(list:List<Source>):ArrayList<String> {
val categoryItems = ArrayList<String>()
categoryItems.addAll(list.map { it.sourceName })
return categoryItems
}
}
}
to populate a spinner I need to supply an array list of string
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="#+id/expense_category"
android:entries="#{()-> createExpenseViewModel.getAllSourceItems(1) }"
app:layout_constraintStart_toStartOf="#+id/textView"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="#+id/textView" app:layout_constraintWidth_percent="0.7"
/>
in android:entries I need to convert the observed data to array list of string, how do I pass the #{()-> createExpenseViewModel.getAllSourceItems(1) } result in to another static method createExpenseViewConverter.getExpenseCategoryListFromSource(sourceList) which would return a array list of string.
in my activity i have setup binding like this
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_expense)
val mainViewModel = DaggerExpenseComponent.builder()
.setContext(this)
.build()
.getExpenseViewModel()
binding.setLifecycleOwner(this)
binding.createExpenseViewModel = mainViewModel
You'll need to use below syntax for that :
android:entries="#{createExpenseConverter.getExpenseCategoryListFromSource(createExpenseViewModel.getAllSourceItems(1))}"
Here, what we've done is accessed your input from MainViewModel object createExpenseViewModel using getAllSourceItems() method;
And then passing it to another class createExpenseActivityConverter object createExpenseConverter using method getExpenseCategoryListFromSource() which returns you ArrayList<String> that your spinner requires.
Edit:
When you use LiveData in DataBinding, Data-binding Compiler takes care of refreshing data just like ObservableFields. All you need to do is provide your LifeCycleOwner to your databinding object.
For Example:
If your activity has ViewDataBinding let's say mActivityBinding using which you provide your ViewModel to set LiveData in xml binding, then after setting your ViewModel consider setting LifecycleOwner like below code :
//Some Activity having data-binding
... onCreate() method of activity
mActivityBinding.setViewModel(myViewModel);
mAcivityBinding.setLifecycleOwner(this); // Providing this line will help you observe LiveData changes from ViewModel in data-binding.
...
Refer here
I'm trying to use Kotlin extension methods inside Android's databinding. For example; calling an onclick handler. So I've made this code:
posttest_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<data>
<import type="android.view.View"/>
<import type="com.example.test.post.posttest.PostTestItemViewModelExtensionKt" />
<variable
name="viewModel"
type="com.example.test.post.posttest.PostTestItemViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:clickable="true"
android:onClick="#{(view) -> viewModel.clicked(view)}"
>
[...]
PostTestItemViewModel.kt
open class PostTestItemViewModel : ViewModel() {
val postTitle = MutableLiveData<String>()
val postBody = MutableLiveData<String>()
/**
* Binds the required properties/entities to this ViewModel
*/
fun bind(post: Post) {
postTitle.value = post.title
postBody.value = post.body
}
}
PostTestItemViewModelExtension.kt
fun PostTestItemViewModel.clicked(v: View) {
this.postTitle.value = "clicked"
}
So when I place the clicked method inside the viewmodel, it works perfectly the way it should be. However, when I create it as an extension method, I get the following error on compilation:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding errors.
cannot find method clicked(android.view.View) in class ...PostItemViewModel
I've tried different things already, such as changing the android:onclick tag to PostTestItemViewModelExtensionKt instead of viewModel. Unfortunately all the things don't seem to work. So it looks like the extension method is getting generated after the databinding takes place. Is there a way around this or am I still doing something wrong? Or is it just not possible to bind extension methods?
I'm using Kotlin version 1.2.71, gradle 3.2.0 and have the databinding { enabled = true } and kapt { generateStubs = true } added to my .gradle, and have the plugings kotlin-android, kotlin-android-extensions and kotlin-kapt defined.
Unfortunately you can't use extension methods as onClick callbacks.
Extension methods in Kotlin are created as Java static methods while the Android framework is expecting an instance method.
Note that in Android Studio you can decompile the Kotlin classes as Java to see the generated Java code.
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78