Okay so I'm experimenting with Two-Way Data binding right now, logically I think everything is perfect in the code, but somehow I keep getting the same error whenever I run the app: "A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution" . Basically I'm trying to hide ImageView visibility if there is no data in my database, using Data Binding ofc.
fragment_list.xml (Binding Layout)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="listViewModel"
type="com.jovanovic.stefan.tododemo.fragments.list.ListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/listLayout"
... >
<ImageView
android:id="#+id/no_data_imageView"
emptyDatabase="#={listViewModel.emptyDatabase}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ListViewModel (ViewModel for my Fragment)
class ListViewModel: ViewModel() {
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData<Boolean>(true)
fun checkDatabase(toDoData: List<ToDoData>){
emptyDatabase.value = toDoData.isEmpty()
}
}
ListFragment
class ListFragment : Fragment(), SearchView.OnQueryTextListener {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val listViewModel = ViewModelProvider(this).get(ListViewModel::class.java)
val binding = FragmentListBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.listViewModel = listViewModel
val toDoViewModel= ViewModelProvider(this).get(ToDoViewModel::class.java)
// Observing LiveData object for Room DB which is reading all data
toDoViewModel.allData.observe(viewLifecycleOwner, Observer { data ->
// Using checkDatabase method from ListViewModel
listViewModel.checkDatabase(data)
})
return binding.root
}
BindingAdapter
#BindingAdapter("emptyDatabase")
#JvmStatic
fun emptyDatabase(view: View, emptyDatabase: MutableLiveData<Boolean>){
if(emptyDatabase.value == true){
view.visibility = View.VISIBLE
}else{
view.visibility = View.INVISIBLE
}
}
emptyDatabase="#={listViewModel.emptyDatabase}"
"=" is us only for android Model two way data binding."=" meaning if model update by get call its update view and if view update its update Model by set call .It's not applicable for databinding adaptor function
I remember that I had similar problems with kapt. Please clean your build and try again, this works for me. Also as per the scenario you described for emptyDatabase.value == true, view.visibility should be View.Insivisble.
After watching this video at (4:29) : https://youtu.be/TW9dSEgJIa8
I saw that I forgot to add this dependency for DataBinding:
//DataBinding
kapt "com.android.databinding:compiler:3.2.0-alpha10"
And even after I added that dependency, I still had an error, until I REMOVED "=" from "#={listViewModel.emptyDatabase}"
Related
The following code is from the project architecture-samples, you can see it here.
I know that I can use such as viewDataBinding.viewmodel to access layout control or data.
But in the following code, I find val view = activity?.findViewById<View>(R.id.menu_filter) ?: return is appear, it's a traditional code.
Is there a way to access Options menu with Databinding or Viewbinding technology ?
class TasksFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.menu_clear -> {
viewModel.clearCompletedTasks()
true
}
R.id.menu_filter -> {
showFilteringPopUpMenu()
true
}
R.id.menu_refresh -> {
viewModel.loadTasks(true)
true
}
else -> false
}
private fun showFilteringPopUpMenu() {
val view = activity?.findViewById<View>(R.id.menu_filter) ?: return
PopupMenu(requireContext(), view).run {
menuInflater.inflate(R.menu.filter_tasks, menu)
setOnMenuItemClickListener {
viewModel.setFiltering(
when (it.itemId) {
R.id.active -> TasksFilterType.ACTIVE_TASKS
R.id.completed -> TasksFilterType.COMPLETED_TASKS
else -> TasksFilterType.ALL_TASKS
}
)
true
}
show()
}
}
...
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" />
</data>
...
</layout>
As states in the docs:
View binding is a feature that allows you to more easily write code that interacts with views. Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views that have an ID in the corresponding layout.
In most cases, view binding replaces findViewById.
Look at the bold words, you notice that View Binding only works for XML layout (located in res/layout), whereas the menus are located in res/menu.
Also, View Binding uses findViewById, whereas menus use menu.findItem(R.id.menu_id), thus it is not possible.
I'm trying to use a shared viewmodel for an activity and a fragment displayed in the activity because both need to be updated by the viewmodel. The fragment is constantly updated using MutableLiveData, but the activity is not and I don't really understand why. For readability reasons I did cut out the layout parameters in the .xml files which are irrelevant to the problem.
My activity code looks like the following:
class MainActivity : AppCompatActivity() {
private lateinit var _binding : MainActivityBinding
private val _viewModel : MySharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = this
...
}
}
The lifecycle owner of the binding is set, yet MutableLiveData does not seem to update the activity.
In the activity layout file is the following:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.MaterialToolbar>
<TextView
android:id="#+id/totalTime"
android:text="#{viewModel.topBarText}"
.../>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.bottomnavigation.BottomNavigationView
.../>
<androidx.fragment.app.FragmentContainerView
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
So I have the topbar with the text view that always shall show the text, the bottom navigation view as tabbar and the fragment container view that contains the fragments.
The fragment code is:
class FirstFragment : Fragment() {
private val _viewModel : MySharedViewModel by viewModels()
private lateinit var _binding : FirstFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = viewLifecycleOwner
return _binding.root
}
}
Also here the lifecycleowner is set for the binding and in the fragment layout it is bound to the viewmodel:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
...
android:text="#{viewModel.fragmentText}" />
<Button
android:id="#+id/startButton"
android:onClick="#{() -> viewModel.startIteration()}"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my viewmodel I simply update texts that shall be displayed:
class MySharedViewModel : ViewModel() {
var topBarText : MutableLiveData<String> = MutableLiveData<String>("Hi")
var fragmentText : MutableLiveData<String> = MutableLiveData<String>("There")
private val stringsForTop = arrayOf("Hi","How", "You")
private val stringsForFragment = arrayOf("There","Are", "?")
private var index = 0
fun startIteration() {
kotlin.concurrent.fixedRateTimer(initialDelay = 1L, period = 1000L) {
viewModelScope.launch(Dispatchers.Main) {
topBarText.value = stringsForTop[index]
fragmentText.value = stringsForFragment[index]
index++
}
}
}
}
Now while the fragment text is updated, the text in the top bar is not updated and always just displays "Hi". I have the feeling, that the activity as lifecycleobserver for the view model is overwritten when the fragment is initialized and its viewLifecycleOwner is bound to the view model.
Am I missing something or is there another way to register both, activity and fragment with their lifecycleowners, at the viewmodel? Any help appreciated.
If you are using by viewmodels<>() in Fragment, it will create new instance of ViewModel coupled to Fragment life cycle. if you want same instance of ViewModel as it is Activity use by activityViewModels<>(), try below code in Fragment
private val _viewModel : MySharedViewModel by activityViewModels()
The following code is from the project architecture-samples, you can see it here.
I know that I can use such as viewDataBinding.viewmodel to access layout control or data.
But in the following code, I find val view = activity?.findViewById<View>(R.id.menu_filter) ?: return is appear, it's a traditional code.
Is there a way to access Options menu with Databinding or Viewbinding technology ?
class TasksFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.menu_clear -> {
viewModel.clearCompletedTasks()
true
}
R.id.menu_filter -> {
showFilteringPopUpMenu()
true
}
R.id.menu_refresh -> {
viewModel.loadTasks(true)
true
}
else -> false
}
private fun showFilteringPopUpMenu() {
val view = activity?.findViewById<View>(R.id.menu_filter) ?: return
PopupMenu(requireContext(), view).run {
menuInflater.inflate(R.menu.filter_tasks, menu)
setOnMenuItemClickListener {
viewModel.setFiltering(
when (it.itemId) {
R.id.active -> TasksFilterType.ACTIVE_TASKS
R.id.completed -> TasksFilterType.COMPLETED_TASKS
else -> TasksFilterType.ALL_TASKS
}
)
true
}
show()
}
}
...
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<import type="androidx.core.content.ContextCompat" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" />
</data>
...
</layout>
As states in the docs:
View binding is a feature that allows you to more easily write code that interacts with views. Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views that have an ID in the corresponding layout.
In most cases, view binding replaces findViewById.
Look at the bold words, you notice that View Binding only works for XML layout (located in res/layout), whereas the menus are located in res/menu.
Also, View Binding uses findViewById, whereas menus use menu.findItem(R.id.menu_id), thus it is not possible.
I'm trying to use Data Binding for setting onClick listeners for buttons in my fragment.
The function that I need to be called every time "next" button is pressed is in a View Model.
I managed to bind data from View Model to my layout XML but I am still unable to call functions from a view model :/
I'm getting this error when trying to call ViewModel functions:
C:\Users\Michal\git\fitness-fatality\app\build\generated\source\kapt\debug\com\example\fitnessfatality\DataBinderMapperImpl.java:10: error: cannot find symbol
import com.example.fitnessfatality.databinding.FragmentWorkoutLoggingBindingImpl;
^
symbol: class FragmentWorkoutLoggingBindingImpl
location: package com.example.fitnessfatality.databinding
I've also tried calling view model functions like this:
android:onClick="#{viewModel.incrementIndex()}"
However, if I bind the entire fragment, I am able to call its functions.
This is how I've tried implementing on click binding with view model:
<?xml version="1.0" encoding="utf-8"?>
<layout
android:id="#+id/main_linear_container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.fitnessfatality.ui.workoutTracking.viewModels.TrackingViewModel"/>
<import type="java.util.List"/>
<import type="com.example.fitnessfatality.ui.workoutTracking.TrackingFragment" />
<variable name="viewModel" type="TrackingViewModel" />
<variable name="fragment" type="TrackingFragment" />
</data>
<LinearLayout
android:orientation="vertical" android:layout_height="match_parent" android:layout_width="match_parent">
//More layouts
<Button
android:text="Next"
android:onClick="#{viewModel.incrementIndex}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="#+id/btn_next" android:layout_weight="1"/>
</LinearLayout>
</layout>
And in my fragment I have
private lateinit var trackingViewModel: TrackingViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
trackingViewModel = ViewModelProviders.of(this).get(TrackingViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding =
DataBindingUtil.inflate<FragmentWorkoutLoggingBinding>(
inflater,
R.layout.fragment_workout_logging,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = trackingViewModel
binding.fragment = this
return binding.root
}
And my ViewModel:
class TrackingViewModel(application: Application): BaseViewModel(application) {
val workoutExercises: LiveData<List<WorkoutExercisePojo>>
private val workoutExerciseRepository: WorkoutExerciseRepository
val currentIndex: MutableLiveData<Int> = MutableLiveData()
val index: LiveData<Int> = currentIndex
init {
val db = AppDatabase.getDatabase(application, scope)
workoutExerciseRepository = WorkoutExerciseRepository(db.workoutExerciseDao())
workoutExercises = workoutExerciseRepository.allWorkoutExercises
currentIndex.value = 0
}
fun incrementIndex() {
currentIndex.value = currentIndex.value!!.plus(1)
}
}
With the custom BindingAdapter:
#BindingAdapter("onClick")
fun onClick(view: View, onClick: () -> Unit) {
view.setOnClickListener {
onClick()
}
}
You should be able to directly bind a viewmodel function like
app:onClick="#{viewModel::forgotPasswordClicked}"
in your XML. This would then lead to a viewmodel function like:
fun forgotPasswordClicked() {
TODO("ForgotPasswordClicked")
}
This way, you also don't have to import unnecessary Android-Dependencies into your viewmodel.
Managed to solved. The problem was that incremenetIndex function in ViewModel did not accept View as a parameter.
So now, the function in ViewModel looks like this:
fun incrementIndex(view: View) {
currentIndex.value = currentIndex.value!!.plus(1)
}
I'm trying to learn how to implement databinding in an Android app. I have a small app I'm working with to learn this. And while I have databinding working for part of the app. I have hit a hiccup when trying to implement a recyclerview. I just cannot seem to get it. Been banging away at it for two or three days, and getting frustrated. Thought I'd ask you guys.
The app is super simple at this point.
The part i'm stuck on is accessing my recyclerview from an .xml layout from my MainFragment.kt
At first I was trying to use binding, but got frustrated and went back to just trying to use findViewById, but that is giving me issue too. I am beginning to think, I don't have as firm a grasp on databinding as I thought I did.
This is from the fragment that holds the recyclerView:
fragment_main.xml
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
android:id="#+id/job_recyclerView"/>
I have another small layout file that is using Cardview to show each individual item in the recyclerview
A super simple Model:
JobData.kt
data class JobData(val companyName: String, val location: String)
An Adapter:
JobAdapter.kt
class CustomAdapter(val userList: ArrayList<JobData>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
//Returning view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.job_item_layout, parent, false)
return ViewHolder(v)
}
//Binding the data on the list
override fun onBindViewHolder(holder: CustomAdapter.ViewHolder, position: Int) {
holder.bindItems(userList[position])
}
override fun getItemCount(): Int {
return userList.size
}
//Class holds the job list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(job: JobData) {
val textViewName = itemView.findViewById(R.id.tv_company_name) as TextView
val textViewAddress = itemView.findViewById(R.id.tv_Location) as TextView
textViewName.text = job.companyName
textViewAddress.text = job.location
}
}
}
And then the code in my MainFragment to handle it all, which it is not doing. I've tried everything, it was getting ugly. As you can see below. Binding is in place and working for my FloatingActionButton. But I for some reason cannot figure out how to access that recylerview. At the point the code is at below, I thought I'd just accessing using findViewById, but that is not working either.
MainFragment.kt
class MainFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentMainBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_main, container, false)
//Setting onClickListener for FAB(floating action button) using Navigation
binding.createNewJobFAB.setOnClickListener { v: View ->
v.findNavController().navigate(R.id.action_mainFragment_to_createNewJobFragment)
}
//getting recyclerview from xml
val recyclerView = findViewById(R.id.job_recyclerView) as RecyclerView
//adding a layoutmanager
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
//Arraylist to store jobs using the data class JobData
val jobs = ArrayList<JobData>()
//add dummy data to list
jobs.add(JobData("A Company", "Town A"))
jobs.add(JobData("B Company", "Town B"))
jobs.add(JobData("C Company", "Town C"))
jobs.add(JobData("D Company", "Town D"))
//creating adapter
val adapter = CustomAdapter(jobs)
//add adapter to recyclerView
recyclerView.adapter = adapter
return binding.root
}
}
The above fails to compile for two reasons:
findViewById shows as an "Unresolved Reference".
When adding the layoutManager, "this" shows as a "Type Mismatch"
Which I believe is due to the fact that Fragments do not have a context. Or so, I think anyway. But I don't know to resolve that? Maybe override some other method, but I can't seem to figure out which or how?
Oh and MainActivity looks like:
MainActivity.kt
class MainActivity : AppCompatActivity() {
//private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
#Suppress("UNUSED_VARIABLE")
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
//Ensures back button works as it should
override fun onSupportNavigateUp() = findNavController(this, R.id.navHostFragment).navigateUp()
}
Which is pointing to Nav_Graph for Android Navigation (part of JetPack). This bit is fine and working.
Adding gradle files to show that my dependencies were set correctly as suggested below.
app/gradle
android {
compileSdkVersion 28
dataBinding {
enabled = true
}
...
}
kapt {
generateStubs = true
correctErrorTypes = true
}
dependencies {
...
kapt "com.android.databinding:compiler:$gradle_version"
...
}
Encase your xml in <layout>..<layout/>
private lateinit var binding: FragmentXXXBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentXXXBinding.inflate(inflater)
return binding.root
}
Then you can call recyclerview by binding.jobRecyclerview
try to set all the click listeners etc on onViewCreated rather than onCreateView of fragment
It is wrong way to findViewById from Fragment(it is good technique for Activity):
val recyclerView = findViewById(R.id.job_recyclerView) as RecyclerView
First, fragment's layout have to be return by onCreateView() method.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
I personally like do all fragment's business logic inside onViewCreated()
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Now, we can use views by kotlinx
//val recyclerView = job_recyclerView
//Or old-fashioned way
val recyclerView = getView()!!.findViewById(R.id.job_recyclerView) as RecyclerView
}
RecylerView can be accessed from fragment's layout by having root view like: getView()!!.findViewById or by kotlinx inside onViewCreated(): job_recyclerView
Ok, so first of all you are getting error on findViewById because your fragment is unaware about the view that contains recyclerView
What you should do is, take an instance of view that you are inflating for this fragment (declare view as a global variable, replace your inflater line with this).
var rootView
// Inside onCreateView
var rootView = inflater?.inflate(R.layout.fragment, container, false)
Now replace, findViewById() with rootView.findViewById()
And the other error is because the fragment does not have any context of it's own so replace this with activity!!
By writing activity!! you are calling getActicity() method which returns context of parent activity.