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.
Related
I'm now trying to implement data binding for a fragment in my android studio project. I followed all the guidelines provided in official android documentation.
It builds well, but when I launch it on my emulator It throw this compilation error
Cannot inherit from final 'com.example....databinding.FragmentFoldersBinding'
FragmentFoldersBinding()' has private access in 'com.example...databinding.FragmentFoldersBinding'`
`Cannot resolve symbol 'mAdapter'.
Cannot resolve symbol 'mViewModel'
Here is my fragment class code
#AndroidEntryPoint
class FoldersFragment : Fragment(), FoldersAdapter.FolderClickListener {
private lateinit var binding : FragmentFoldersBinding
private val vm : FoldersViewModel by activityViewModels()
private val adapter : FoldersAdapter by lazy { FoldersAdapter(this as FoldersAdapter.FolderClickListener) }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFoldersBinding.inflate(inflater)
binding.apply {
viewModel = vm
lifecycleOwner = this#FoldersFragment
adapter = adapter
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?){
super.onViewCreated(view, savedInstanceState)
binding.addFolderButton.setOnClickListener {
FolderAddDialog().show(childFragmentManager, null)
}
binding.foldersRecyclerView.isNestedScrollingEnabled = false
binding.foldersRecyclerView.layoutManager = LinearLayoutManager(view.context)
vm.foldersLiveData.observe(viewLifecycleOwner){
adapter.setData(it)
}
}
override fun onClickedFolder(folder : Folder) {
val action = FoldersFragmentDirections.actionFoldersFragmentToNotesFragment(folder)
findNavController().navigate(action)
}
override fun setUpOnItemSwiped(swipe: SwipeCallback) {
val itemTouchHelper = ItemTouchHelper(swipe)
itemTouchHelper.attachToRecyclerView(binding.foldersRecyclerView)
}
override fun onDeleteSwiped(folder: Folder) {
vm.deleteFolder(folder)
}
}
fragment xml part
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example....presentation.folders_feature.FoldersViewModel" />
<variable
name="adapter"
type="com.example...presentation.folders_feature.FoldersAdapter" />
</data>
I've tried multiple of different solutions suggested. Thank you for helping in advance!
I have a simple android application, In my application, there is one image icon, and when I click the icon, I want to show context menu. My code is debugging but when I click the image button, nothing change, so context menu not work, I do not know where is the problem, Any idea will be appreciated.
MenuFragment:
class MenuFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_menu, container, false)
popupMenu()
}
private fun popupMenu() {
val popupMenu = PopupMenu(requireContext(), menu)
popupMenu.inflate(R.menu.menu)
popupMenu.setOnMenuItemClickListener {
when(it.itemId) {
R.id.menu_one -> {
Toast.makeText(requireContext(), "menu1", Toast.LENGTH_SHORT).show()
true
}
R.id.menu_two -> {
Toast.makeText(requireContext(), "menu2", Toast.LENGTH_SHORT).show()
true
}
else -> true
}
}
menu.setOnLongClickListener {
try {
val popup = PopupMenu::class.java.getDeclaredField("mPopup")
popup.isAccessible = true
val menu = popup.get(popupMenu)
menu.javaClass
.getDeclaredMethod("setForceShowIcon",Boolean::class.java)
.invoke(menu, true)
}catch(e: Exception) {
e.printStackTrace()
}finally {
popupMenu.show()
}
true
}
}
}
fragment_menu:
<ImageView
android:id="#+id/menu"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:src="#drawable/ic_menu"
android:text="5:33 PM"
android:textSize="12sp"
/>
menu:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/menu_one"
android:title="Menu1"
android:icon="#drawable/ic_menu_one"
/>
<item
android:id="#+id/menu_two"
android:title="Menu2"
android:icon="#drawable/ic_menu_two"
/>
It seems you want to show the menu as soon as possible, since the function invocation is after the return statement then it never happen.
You have to leverage the fragment lifecycle. Override onViewCreated and call it there
override... onViewCreated(...) {
super....
popUpMenu()
}
The method onViewCreated happens immediately after the view is created. If you need it when the view is ready to interact onStart. In both cases, make sure backward navigation doesn't show it again by mistake.
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}"
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)
}