I am struck at the data binding part. I have a simple app where I am using mvvm pattern to show a list. But the problem is how to bind recycler view with the id recycler_view with activity
// data class
data class MovieItem(
val imageUrl: String,
val name: String
)
// repository
class MovieRepository(private val api: Api) {
suspend fun getAllMovies() = api.getMovieList()
}
// view Model
class MovieViewModel(private val repository: MovieRepository):ViewModel() {
var listMovie = MutableLiveData<List<MovieItem>>()
var job: Job? = null
fun makeApiCall(){
job= CoroutineScope(Dispatchers.IO).launch {
var response = repository.getAllMovies()
withContext(Dispatchers.Main){
if (response != null){
listMovie.postValue(response.body())
}
}
}
}
}
// adapter
class MovieItemAdapter:RecyclerView.Adapter<MovieItemAdapter.MovieViewHolder>() {
var listMovie = mutableListOf<MovieItem>()
private val context:Context ? = null
fun setMovieItem(data : List<MovieItem>){
this.listMovie = data.toMutableList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val binding = DataBindingUtil.inflate<MovieItemBinding>(LayoutInflater.from(parent.context),
R.layout.movie_item,parent,
false)
return MovieViewHolder(binding)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) =
override fun getItemCount(): Int {
return listMovie.size
}
class MovieViewHolder(val binding: MovieItemBinding):RecyclerView.ViewHolder(binding.root){
fun bind(item : MovieItem){
binding.model = item
}
}
}
// activity
class MainActivity : AppCompatActivity() {
lateinit var movieadapter: MovieItemAdapter
lateinit var api: Api
lateinit var repository: MovieRepository
lateinit var movieViewModel: MovieViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
movieadapter = MovieItemAdapter()
recyclerView.adapter = movieadapter
api = Api.getInstance()
repository = MovieRepository(api)
movieViewModel = ViewModelProvider(this, MovieFactory(repository))
.get(MovieViewModel::class.java)
movieViewModel.makeApiCall()
movieViewModel.listMovie.observe(this, Observer {
movieadapter.setMovieItem(it)
})
}
}
// adapter layout
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.example.databindingpractice.MovieItem" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:padding="3dp"
android:orientation="horizontal">
<ImageView
android:layout_width="70dp"
android:layout_height="70dp"
android:id="#+id/img_view" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#color/black"
android:textSize="18sp"
android:text="#{model.name}"
android:layout_marginLeft="5dp"
android:id="#+id/tv_view"
android:layout_gravity="center" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<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="activity"
type="com.example.databindingpractice.MainActivity" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/recycler_view" />
</LinearLayout>
In onCreate method of Activity, replace
setContentView(R.layout.activity_main)
With
val binding : MainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
Then you can use
binding.recyclerView.layoutManager = LinearLayoutManager(this)
movieadapter = MovieItemAdapter()
binding.recyclerView.adapter = movieadapter
Also remove this from activity_main.xml
<data>
<variable
name="activity"
type="com.example.databindingpractice.MainActivity" />
</data>
Use this for reference
Update:
Binding adapter can be defined as follows:
#BindingAdapter("moviesList")
fun bindRecyclerView(recyclerView: RecyclerView, list: List<MovieItem>?){
val adapter: MoviesAdapter = recyclerView.adapter as MoviesAdapter
adapter.submitList(list)
}
In activity_main.xml
<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="viewModel"
type="com.example.databindingpractice.MovieViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/recycler_view"
app:movieList="#{viewModel.listMovie}"/>
</LinearLayout>
Related
I am now developing an application where I use MVVM pattern for UI and repository interaction. In other words, I receive live data object with a list of models from my ROOM data base query via repository, then assign it to my live data variable in viewmodel. After that, this data should be populated to my xml layout recycler view via data binding, but It happens only once fragment is initialised. In other cases recycler view is void
DAO code :
#Query("SELECT * FROM note WHERE content LIKE :query ORDER BY isStarred")
fun searchAllNotes(query : String?) : Flow<List<Note>>
ViewModel code:
#HiltViewModel
class HomeViewModel #Inject constructor (
private val noteRepository : NoteRepository
) : ViewModel() {
private var _notesLiveData : LiveData<List<Note>> = noteRepository.getAllNotes().asLiveData()
val notesLiveData get()= _notesLiveData
fun searchNotes(query : String){
viewModelScope.launch(Dispatchers.IO){
_notesLiveData = noteRepository.searchAllNotes(query).asLiveData()
}
}
fun deleteNote(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.deleteNote(note)
}
}
fun updateNoteChecked(note : Note){
viewModelScope.launch(Dispatchers.IO){
noteRepository.updateNoteChecked(note.id, note.isStarred)
}
}
Fragment code
#AndroidEntryPoint
class HomeFragment : Fragment(),
NoteCardAdapter.NoteTouchListener,
SearchView.OnQueryTextListener{
private var _binding : FragmentHomeBinding? = null
val binding get() = _binding!!
private val adapter by lazy {
NoteCardAdapter(this as NoteCardAdapter.NoteTouchListener)
}
private val vm : HomeViewModel by viewModels()
private val noteSharedViewModel : NoteSharedViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.adapter = adapter
binding.vm = vm
binding.apply {
lifecycleOwner = viewLifecycleOwner
homeRecyclerView.isNestedScrollingEnabled = false
homeRecyclerView.layoutManager = LinearLayoutManager(view.context)
homeRecyclerView.itemAnimator= NotesItemAnimator()
}
binding.homeSearchView.isSubmitButtonEnabled = true
binding.homeSearchView.setOnQueryTextListener(this as SearchView.OnQueryTextListener)
binding.addNoteButton.setOnClickListener{
val note = Note(
"","","",false, activity?.getDate(), folderId = -1
)
noteSharedViewModel.selectNote(note)
val action = HomeFragmentDirections.actionHomeFragmentToSingleNoteFragment(
isNew = true
)
findNavController().navigate(action)
}
binding.foldersButton.setOnClickListener{
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToFoldersFragment())
}
}
xml layout code :
<?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="vm"
type="com.example.leonidsnotesapplication.presentation.notes_feature.viewmodels.HomeViewModel" />
<variable
name="adapter"
type="com.example.leonidsnotesapplication.presentation.notes_feature.adapters.NoteCardAdapter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:background="#color/note_background_color_3"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presentation.notes_feature.fragments.HomeFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/homeSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:theme="#style/AppSearchView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="#+id/tvRecentTitle"
android:textStyle="bold"
android:textSize="25sp"
android:textColor="#color/button_color_2"
android:text="#string/recent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
app:layout_constraintTop_toBottomOf="#id/homeSearchView"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:background="#drawable/ic_circle_arrow"
app:layout_constraintStart_toEndOf="#id/tvRecentTitle"
app:layout_constraintTop_toBottomOf="#id/homeSearchView" />
<androidx.recyclerview.widget.RecyclerView
tools:listitem="#layout/note_card_view"
android:scrollbars="vertical"
android:scrollbarStyle="outsideInset"
android:id="#+id/homeRecyclerView"
android:layout_width="match_parent"
android:requiresFadingEdge="vertical"
android:fadingEdge="vertical"
android:fadingEdgeLength="15dp"
android:layout_height="500dp"
android:layout_marginTop="30dp"
app:setNoteAdapter="#{adapter}"
app:submitNoteList="#{vm.notesLiveData}"
app:layout_constraintTop_toBottomOf="#id/tvRecentTitle"
app:layout_constraintStart_toStartOf="parent"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/add_note_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:backgroundTint="#color/button_color_1"
android:id="#+id/folders_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="#drawable/ic_folders_stack"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Data binding adapter :
#BindingAdapter("submitNoteList")
fun submitNoteList(recyclerView: RecyclerView, data : List<Note>?){
val adapter = recyclerView.adapter as NoteCardAdapter
adapter.setData((data as ArrayList<Note>? ?: arrayListOf()))
}
#BindingAdapter("setNoteAdapter")
fun setNoteAdapter(recyclerView: RecyclerView, adapter: NoteCardAdapter){
adapter.let {
recyclerView.adapter = it
}
}
You should observe your live data. Example code is:
vm.notesLiveData.observe(viewLifecycleOwner) { adapter.submitList(it) }
I have one relative layout and one ImageView. I want to set visibility based on Image loading like if image loads successfully then imageview is visible and if some error occurs relative layout is visible. How can I manage this scenario in data binding using BindingAdapter ?
Your question is not clear so I don't know if it will help you.
These are steps to implement
1: Create parameters in BindingAdapter.kt
#BindingAdapter("showLoading")
fun View.showLoading(loading: Boolean) {
if (loading) {
visible()
} else {
gone()
}
}
#BindingAdapter("showError")
fun View.showError(error: Boolean) {
if (error) {
visible()
} else {
gone()
}
}
fun View.gone() {
this.visibility = View.GONE
}
fun View.visible() {
this.visibility = View.VISIBLE
}
2: Use parameters [app:showLoading="#{viewModel.showLoading}"] and [app:showError="#{viewModel.showError}"] created in file XML
<?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">
<data>
<variable
name="viewModel"
type="com.xxx.xxx.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EAEAE2"
android:orientation="vertical">
<Button
android:id="#+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ShowLoading" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:showLoading="#{viewModel.showLoading}" />
<RelativeLayout
android:id="#+id/viewError"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#color/colorPrimary"
app:showError="#{viewModel.showError}" />
</LinearLayout>
</layout>
3: Create showError and showLoading variables in Viewmodel.
And assign the viewModel variable to the binding.
class MainViewModel: ViewModel() {
val showError: MutableLiveData<Boolean> = MutableLiveData()
val showLoading: MutableLiveData<Boolean> = MutableLiveData()
init {
showError.postValue(false)
showLoading.postValue(true)
}
}
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
binding.viewModel = viewModel
initViews()
}
private fun initViews() {
binding.btnAdd.setOnClickListener {
viewModel.showError.postValue(true)
viewModel.showLoading.postValue(false)
}
}
}
I set up BindingAdpater at recyclerview items. But when I get from data, it doesn't not work.
as you see below, myItem is working. But not working at items that is in parameter. I don't understand why items(in parameter) is not working.
BindingAdapter
#BindingAdapter("app:items")
fun setItems(view: RecyclerView, items: List<LaunchListQuery.Launch>?) {
var myItem = arrayListOf<LaunchListQuery.Launch?>()
myItem.add(LaunchListQuery.Launch("","Id : "+1,"Site : "+1,null))
myItem.add(LaunchListQuery.Launch("","Id : "+2,"Site : "+2,null))
myItem.add(LaunchListQuery.Launch("","Id : "+3,"Site : "+3,null))
(view.adapter as MainAdapter).submitList(items)
}
xml
<?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:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.haii.graphqldemo.main.MainViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="#+id/main_layout">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:items="#{viewmodel.items}"/>
</LinearLayout>
</layout>
ViewModel
class MainViewModel #Inject constructor(
private val repository: DefaultRepository
) : ViewModel() {
private var _items :MutableLiveData<List<LaunchListQuery.Launch?>> = MutableLiveData()
val items : LiveData<List<LaunchListQuery.Launch?>> = _items
fun getItems(){
viewModelScope.launch {
val items = repository.getLaunchList()
_items.value = items.launches
}
}
}
I solved this by adding
onCreate at Activity
binding.lifecycleOwner = this
onActivityCreated at Fragment
binding.lifecycleOwner = this.viewLifecycleOwner
I'm dev-ing using SwipeRefreshLayout in mvvm. so I wrote the codes at viewmodel and xml. when I run this, the progressbar is running but nothing happens. the log doesn't show anything. please let me know what should I fix it. I used this link "AndroidX SwipeRefreshLayout with DataBinding"
xml
<?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">
<data>
<variable
name="viewmodel"
type="com.haii.schedulemanager.schedule.ScheduleViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swipe_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:refreshing="#{viewmodel.isLoading}"
app:onRefreshListener="#{viewmodel::onRefresh}">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/scheduleListView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
</layout>
viewmodel
class ScheduleViewModel #Inject constructor(
private val scheduleRepository: ScheduleRepository
) : ViewModel() {
private val _item = MutableLiveData<List<ScheduleItem>>()
val item: LiveData<List<ScheduleItem>> = _item
private val _oneSchedule = MutableLiveData<ScheduleItem>()
val oneSchedule: LiveData<ScheduleItem> = _oneSchedule
lateinit var mOneSchedule : ScheduleItem
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
fun getWeek(groupName : String){
//_isLoading.value = false
viewModelScope.launch {
val itemList = scheduleRepository.getWeek(groupName)
if(itemList.isEmpty()){
Log.d("TAG","Null")
_item.value=null
}else{
_item.value = itemList
}
//_isLoading.value = true
}
}
fun onRefresh(){
Log.d("TAG","onRefresh")
getWeek("Haii")
}
}
don't forget to clear list when you refresh the page.
I am using databinding in my application but button is not working. where am i making mistake in this code. i tried many solution but no luck. but if make button in activity_login.xml then button click works. i think i am not able to pass the view model to the included view.
Here is my code
activity_login.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">
<data>
<variable
name ="loginViewModel"
type="com.innowi.checoutrestaurantdashboard.view.main.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/login_constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/login_background_burgerstack"
android:isScrollContainer="false"
android:paddingEnd="32dp"
android:paddingStart="32dp"
tools:context=".view.main.LoginActivity">
<include
android:id="#+id/login_layout"
layout="#layout/layout_login"
android:layout_width="0dp"
android:layout_height="0dp"
bind:loginViewModel="#{loginViewModel}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
layout_login.xml
<?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">
<data>
<variable
name ="loginViewModel"
type="com.innowi.checoutrestaurantdashboard.view.main.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
style="#style/Login"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="false"
android:padding="32dp">
<Button
android:id="#+id/login_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="#drawable/button_login_drawable"
android:enabled="false"
android:gravity="center"
android:paddingBottom="16dp"
android:paddingTop="16dp"
android:text="#string/button_sign_in"
android:textColor="#color/colorWhite"
android:textSize="24sp"
android:onClick="#{loginViewModel::onLoginButtonClick}"
app:layout_constraintLeft_toLeftOf="#+id/login_username"
app:layout_constraintRight_toRightOf="#+id/login_username"
app:layout_constraintTop_toBottomOf="#+id/login_password" />
.
.
.
.
</android.support.constraint.ConstraintLayout>
</layout>
MainViewModel.kt
class MainViewModel #Inject constructor(
private val loginRepository: LoginRepository
) : BaseViewModel() {
fun onLoginButtonClick(view : View){
performLogin()
}
.
.
.
}
LoginActivity.kt
class LoginActivity : AppBaseActivity() {
private lateinit var loginViewBinding: ActivityLoginBinding
private lateinit var viewModel: MainViewModel
private val TAG = LoginActivity::class.simpleName
override fun initViewModel(viewModelProvider: ViewModelProvider): BaseViewModel? {
viewModel = viewModelProvider.get(MainViewModel::class.java)
return viewModel
}
override fun render(state: ViewState) {
when (state) {
is LoadingState -> {
// loading
val loading = state.loading
Log.d(TAG,"Loading State")
}
is DefaultState -> {
// render Data
val data = state.data
Log.d(TAG,"Data State")
}
is ErrorState -> {
// show error
val error = state.error
Log.d(TAG,"Loading State")
}
}
}
override fun setContentView() {
loginViewBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginViewBinding.loginViewModel = viewModel
loginViewBinding.executePendingBindings()
}
}