I have a Dao with this method.
#Query("SELECT * FROM expense WHERE date BETWEEN :dateStart AND :dateEnd")
fun getExpensesBetweenTheDate(dateStart: Calendar, dateEnd: Calendar):
DataSource.Factory<Int, Expense>
My repository get Dao and create LiveData> object.
fun getExpensesBetweenTheDate(startDay: Calendar, endDay: Calendar): LiveData<PagedList<Expense>> {
val factory = expenseDao.getExpensesBetweenTheDate(startDay, endDay)
val config = PagedList.Config.Builder()
.setPageSize(30)
.setMaxSize(200)
.setEnablePlaceholders(true)
.build()
return LivePagedListBuilder(factory, config)
.build()
}
My ViewModel get repository and create a variable.
val expenses = repository.getExpensesBetweenTheDate(startCalendar, endCalendar)
Finally, MainActivity observes on LiveData.
viewModel.expenses.observe(this, Observer(simpleExpenseAdapter::submitList))
All working fine, but when I try to add a new record to the database, it appears there not immediately, but after restarting the application. Similar code without a paging library works well. Maybe i do something wrong. Just in case, I give below the code of the adapter, viewHolder and layout.
Adapter.
class ExpenseAdapter : PagedListAdapter<Expense, ExpenseViewHolder>(EXPENSE_COMPARATOR) {
companion object {
private val EXPENSE_COMPARATOR = object : DiffUtil.ItemCallback<Expense>() {
override fun areItemsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem.expenseId == newItem.expenseId
}
override fun areContentsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseViewHolder {
return ExpenseViewHolder.create(parent)
}
override fun onBindViewHolder(holder: ExpenseViewHolder, position: Int) {
val expenseItem = getItem(position)
if (expenseItem != null) holder.bind(expenseItem)
}
}
ViewHolder.
class ExpenseViewHolder(binding: ExpenseElementSimpleBinding) : RecyclerView.ViewHolder(binding.root) {
private val mBinding = binding
init {
mBinding.root.setOnClickListener {
val intent = Intent(it.context, ShowExpenseActivity::class.java)
intent.putExtra("expense", mBinding.expense)
it.context.startActivity(intent)
}
}
companion object {
fun create(parent: ViewGroup): ExpenseViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ExpenseElementSimpleBinding.inflate(inflater, parent, false)
return ExpenseViewHolder(binding)
}
}
fun bind(item: Expense) {
mBinding.apply {
expense = item
executePendingBindings()
}
}
}
Layout.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="expense"
type="com.example.budgetplanning.data.model.Expense"/>
</data>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:text="#{expense.description}"
tools:text="Gasoline"
android:padding="5dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<androidx.appcompat.widget.AppCompatTextView
android:text="#{String.valueOf(expense.amount)}"
tools:text="123"
android:padding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
You have to call the simpleExpenseAdapter.notifyDataSetChanged() after the the submitList
This is happening because when you are calling simpleExpenseAdapter::submitList is equivalent to call simpleExpenseAdapter:submitList() when the list diff is not called at this time. So, you have to notify that the list has changed.
Or so, you can pass the new list as a parameter like:
viewModel.expenses.observe(this, Observer<YourObjectListened> {
simpleExpenseAdapter.submitList(it)
})
try to use toLiveData with original example from Paging library overview
Related
For a particular Recycler View Item, If I select the Checkbox (tick it) then I need the text of its corresponding TextView to formatted as Strikethrough.
I am using Binding Adapters, Flow and Live Data.
But after selecting the checkbox, its corresponding TextView is not getting formatted.
But If I navigate to some other fragment and come back to here(FruitFragmnet) then the TextView data is formatted. (i.e. the database gets updated correctly on ticking checkbox but the live data emission is delayed to UI)
Possible Root Cause: My update to Room Database is happening immeialtey, but from database the LiveData is not flown to UI immediately.
I did lot of trial and errors, read multiple similar questions but I was unable to find the missing link and solution to this issue.
Please advice. Following is the code:
BindingAdapter
#BindingAdapter("markAsCompleted")
fun markAsCompleted(textView: TextView, completed: Boolean) {
if (completed) {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, items: List<Fruit>?) {
items?.let {
(view.adapter as SettingAdapter).submitList(items)
}
}
Fruit Fragment with Recycler View
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/fruits_list"
setItems="#{settingViewModel.allList}" // This is Binding Adapter
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</layout>
Above Fruit's Fragment Item View
<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>
<import type="android.widget.CompoundButton" />
<variable
name="fruit"
type="com.example.data.Fruit" />
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<CheckBox
android:id="#+id/fruit_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#{fruit.completed}"
android:onClick="#{(view) -> settingViewModel.completeFruit(fruit,((CompoundButton)view).isChecked())}"
/>
<TextView
android:id="#+id/fruit_name"
markAsCompleted="#{fruit.completed}" // This is Binding Adapter
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{fruit.fruit}" />
....
Fruit Fragment
class FruitFragment : Fragment() {
private lateinit var binding: FragmentFruitBinding
private lateinit var fruitAdapter: FruitAdapter
private val viewModel: SettingViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFruitBinding.inflate(layoutInflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
settingViewModel = viewModel
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fruitAdapter = FruitAdapter(viewModel)
binding.fruitslist.apply {
adapter = fruitAdapter
}
}
}
SettingViewModel
class SettingViewModel(application: Application) : AndroidViewModel(application) {
private val app = getApplication<Application>()
private val dao = Database.getDatabase(app.applicationContext).dao
val allList: LiveData<List<Fruit>> = dao.getFruits().asLiveData().distinctUntilChanged()
fun completeFruit(fruit: Fruit, completed: Boolean) {
viewModelScope.launch {
if (completed) {
dao.updateCompleted(fruit.id, completed)
} else {
dao.updateCompleted(fruit.id, completed)
}
}
}
....
}
DAO Class
#Dao
interface DatabaseDao {
#Query("SELECT * FROM fruit_table")
fun getFruits(): Flow<List<Fruit>>
#Query("UPDATE fruit_table SET completed = :completed WHERE id = :id")
suspend fun updateCompleted(id: Int, completed: Boolean)
}
Recycler View Adapter
class FruitAdapter(private val viewModel: SettingViewModel) : ListAdapter<Fruit, ViewHolder>(FruitDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, viewModel)
}
class ViewHolder private constructor(val binding: ContainerFruitBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Fruit, viewModel: SettingViewModel) {
binding.apply {
settingViewModel = viewModel
fruit = item
executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ContainerFruitBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class FruitDiffCallback : DiffUtil.ItemCallback<Fruit>() {
override fun areItemsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.fruit == newItem.fruit
}
}
Data Class
#Entity(tableName = "fruit_table")
data class Fruit(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var fruit: String,
var completed: Boolean = false
)
I guess you need to change the second parameter of setItems function to LiveData in BindingAdapter:
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, data: LiveData<List<Fruit>>) {
data.value?.let {
(view.adapter as SettingAdapter).submitList(it)
}
}
I am trying to use data binding with RecyclerView. I set up a simple application that shows the numbers 1 to 50, one on each row, in a RecyclerView. And I want to use data binding to each row.
Here is the code of my adapter (Note that I called dataBinding.executePendingBindings()):
class SimpleAdapter
: ListAdapter<Int, SimpleAdapter.SimpleAdapterViewHolder>(SimpleAdapterDiffUtil()) {
inner class SimpleAdapterViewHolder(
private val dataBinding: SimpleAdapterViewHolderBinding
) : RecyclerView.ViewHolder(dataBinding.root) {
fun bind(str: String) {
Log.e("MYTAG", "setting TextView: $str")
dataBinding.txtView.text = str
dataBinding.executePendingBindings()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleAdapterViewHolder {
val binding = SimpleAdapterViewHolderBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return SimpleAdapterViewHolder(binding)
}
override fun onBindViewHolder(holder: SimpleAdapterViewHolder, position: Int) {
val stringValue = getItem(position)
holder.bind(stringValue.toString())
}
}
private class SimpleAdapterDiffUtil: DiffUtil.ItemCallback<Int>() {
override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
}
And this is the view 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="text"
type="String" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txt_view"
android:text="#{text}"
android:layout_width="match_parent"
android:layout_height="36dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#color/black"
app:layout_constraintTop_toBottomOf="#id/txt_view"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And here is what it does:
As you can see, in the initial screen, none of the rows were not properly rendered. When I scrolled down, I found that it started rendering from row 23. Then if I scrolled back up, I can finally see the prior rows being filled.
If I change the code to use plain old views, rather than data bindings, it works as expected. Code here:
// This works fine!
class SimpleAdapter
: ListAdapter<Int, SimpleAdapter.SimpleAdapterViewHolder>(SimpleAdapterDiffUtil()) {
inner class SimpleAdapterViewHolder(
private val rootView: View
) : RecyclerView.ViewHolder(rootView) {
fun bind(str: String) {
Log.e("MYTAG", "setting TextView: $str")
rootView.findViewById<TextView>(R.id.txt_view).text = str
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleAdapterViewHolder {
val rootView = LayoutInflater.from(parent.context)
.inflate(R.layout.simple_adapter_view_holder, parent, false)
return SimpleAdapterViewHolder(rootView)
}
override fun onBindViewHolder(holder: SimpleAdapterViewHolder, position: Int) {
val stringValue = getItem(position)
holder.bind(stringValue.toString())
}
}
private class SimpleAdapterDiffUtil: DiffUtil.ItemCallback<Int>() {
override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
}
As I see in your xml, you're setting android: text="#{text}"
In other words, textView value is being set from binding.text
You set dataBinding.txtView.text = str and it will update the text. After that using executePendingDataBinding it's value will be reset to that dataBinding.text (aka android: text="#{text}") which is null or empty. So you shoud set
dataBinding.text = str
Instead of
dataBinding.txtView.text = str
I'm very new in Kotlin and Android programming. I tried to create a Fragment which populates a Recycler view, but somehow I get the following error: E/RecyclerView: No adapter attached; skipping layout
I don't really understand why I get this, since I binded everything. If somebody can explain what I'm doing wrong I would really appreciate it. My code is the following:
My class:
data class Movie(val id:Int, val posterPath:String, val vote:Double, val language:String,val releaseDate:String, val title:String) {}
My fragment:
class MovelistScreen : Fragment(R.layout.fragment_movelist_screen) {
#ExperimentalStdlibApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// View created, can be accessed
// val args = arguments ?: throw IllegalArgumentException("Use new instance method")
// val argValue = args.getString(ARG_NAME)
val binding = FragmentMovelistScreenBinding.inflate(layoutInflater)
val lst : List<Movie> = buildList {
add(Movie(id=1,posterPath="/asdasd",vote=7.3,language="Eng",releaseDate="2017",title="Test1"))
add(Movie(id=2,posterPath="/asdasd",vote=6.3,language="Hun",releaseDate="2013",title="Test2"))
}
val listAdapter = MovieListAdapter()
binding.itemList.adapter=listAdapter
listAdapter.submitList(lst)
}
companion object {
private const val ARG_NAME = "test_argument"
fun newInstance(testArg: String): DetailpageFragment = DetailpageFragment().apply {
arguments = Bundle().apply { putString(ARG_NAME, testArg) }
}
}
}
My adapter
class MovieListAdapter : ListAdapter<Movie, MovieViewHolder>(diffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
return MovieViewHolder(MovieItemBinding.inflate(layoutInflater,parent,false))
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val item:Movie=getItem(position)
val binding:MovieItemBinding = holder.binding
binding.movieTitle.text=item.title
binding.releaseYear.text=item.releaseDate
binding.language.text=item.language
binding.ratingtext.text=item.vote.toString()
binding.movieImage.load("https://i.postimg.cc/VLbN4hkz/the-hobbit-the-desolation-of-smaug.jpg")
}
}
private val diffUtil : DiffUtil.ItemCallback<Movie> = object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean = oldItem == newItem
}
class MovieViewHolder(val binding: MovieItemBinding):RecyclerView.ViewHolder(binding.root)
fragment_movelist_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/item_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"
android:padding="16dp">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
mainactivity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/fragmentmovielist"
android:name="com.example.ubbassignment2.MovelistScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="#layout/fragment_detailpage" />
</FrameLayout>
You're creating a new view and binding in onViewCreated and setting adapter to it (that view will be garbage collected) you should inflate your view in onCreateView and set the adapter to that recycler view instead of your temporary one.
Fragment:
override fun onCreateView(...): View {
val binding = FragmentMovelistScreenBinding.inflate(layoutInflater)
val lst : List<Movie> = buildList {
add(Movie(id=1,posterPath="/asdasd",vote=7.3,language="Eng",releaseDate="2017",title="Test1"))
add(Movie(id=2,posterPath="/asdasd",vote=6.3,language="Hun",releaseDate="2013",title="Test2"))
}
val listAdapter = MovieListAdapter()
binding.itemList.adapter=listAdapter
listAdapter.submitList(lst)
return binding.root
}
I want to get isChecked data from checkbox from recycler view in this I am using Binding Adapter but I am not getting how to do that. If anyone has a way to do that then please share it.
class ItemListAdapter(private val itemDeleteListener: ItemDeleteListener,
private val checkItemListener: CheckItemListener) :
ListAdapter<ListItemTable, ItemListAdapter.ViewHolder>(ListItemDiffCallBack()) {
class ViewHolder(private val binding: ShowItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listItemTable: ListItemTable, itemDeleteListener: ItemDeleteListener,
checkItemListener: CheckItemListener) {
binding.itemHistory = listItemTable
binding.itemDelete = itemDeleteListener
binding.checkItem = checkItemListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShowItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position), itemDeleteListener, checkItemListener)
getItem(position).itemCompleted
}
class ItemDeleteListener(val clickListener: (listId: Long) -> Unit) {
fun onClick(listItemTable: ListItemTable) = clickListener(listItemTable.itemId)
}
class CheckItemListener(val clickListener: (listId: Long) -> Unit){
fun onClick(listItemTable: ListItemTable) = clickListener(listItemTable.itemId)
}
class ListItemDiffCallBack : DiffUtil.ItemCallback<ListItemTable>() {
override fun areItemsTheSame(oldItem: ListItemTable, newItem: ListItemTable): Boolean {
return oldItem.itemId == newItem.itemId
}
override fun areContentsTheSame(oldItem: ListItemTable, newItem: ListItemTable): Boolean {
return oldItem == newItem
}
}
}
See: https://developer.android.com/topic/libraries/data-binding/two-way
If you're using data binding I think you should take a look at two-way data binding. It's even introduced through this checkbox problem inside the docs, so you're in luck!
Edit: To be a bit more specific you would implement this like so:
Create a binding adapter that sets the property on the view
<layout>
<data>
<variable
name="itemHistory"
type="your.package.ListItemTable" />
<variable
name="checkItem"
type="android.widget.CompoundButton.OnCheckedChangeListener" />
<!-...->
</data>
<CheckBox
android:id="#+id/rememberMeCheckBox"
android:checked="#{itemHistory.itemCompleted}"
android:onCheckedChanged="#{checkItem}"
/>
</layout>
So you have to use OnCheckedChangeListener instead of your own listener, and pass that to your xml.
You already have 2 "listeners" on your constructor of the adapter and you are passing them to the view holder by using the bind method.
class ItemListAdapter(private val itemDeleteListener: ItemDeleteListener, private val checkItemListener: CheckItemListener)
So you have to pass the correct one to your view holder and then set the checkbox listener. Is better to use the constructor of the view holder to avoid passing the same listener on every binding
class ViewHolder(private val binding: ShowItemBinding, private val itemDeleteListener: ItemDeleteListener, private val checkItemListener: CheckItemListener) {
fun bind(listItemTable: ListItemTable) {
binding.YOUR_CHECKBOX.setOnCheckedChangeListener(null)
binding.itemHistory = listItemTable
binding.itemDelete = itemDeleteListener
binding.checkItem = checkItemListener
binding.executePendingBindings()
binding.YOUR_CHECKBOX.setOnCheckedChangeListener { -, isChecked ->
//here call your "listener" by example
checkItemListener.theMethod(isChecked)
}
}
}
We set the checkbox listener to null first so we can stop listening and then set the view values, otherwise, anything changing the view will also trigger the listener, once the view values are set, then set the checkbox listener to trigger the callbacks.
I am working on the Movies app where I want to load movies and save as cache in local db. I was following the Udacity's "Building Andoid apps with Kotlin" course "Mars real estate" example. My app is using Live data, MVVM, databinding, Room and Retrofit. Each time new data is loaded, it is saved in db, and is the single source of 'truth'.
This is the main layout:
<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.moviesapp.presentation.main.MainViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.moviesapp.presentation.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/movies_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
android:clipToPadding="false"
android:background="#android:color/black"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:listData="#{viewModel.movies}"
app:spanCount="3"
tools:itemCount="16"
tools:listitem="#layout/grid_view_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment:
class MainFragment : DaggerFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
/**
// * Lazily initialize [MainViewModel].
// */
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this, providerFactory).get(MainViewModel::class.java)
}
#Inject
lateinit var providerFactory: ViewModelProviderFactory
#Inject
lateinit var sharedPreferences: SharedPreferences
override fun onResume() {
super.onResume()
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val binding = FragmentMainBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.moviesGrid.adapter = MainAdapter(MainAdapter.OnClickListener {
viewModel.displayMovieDetails(it)
})
viewModel.navigateToSelectedMovie.observe(this, Observer {
if (null != it) {
this.findNavController().navigate(MainFragmentDirections.actionShowDetail(it.id))
viewModel.displayMovieDetailsComplete()
}
})
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
this.findNavController().navigate(
MainFragmentDirections.actionShowSettings()
)
true
}
R.id.action_search -> {
this.findNavController().navigate(
MainFragmentDirections.actionMainFragmentToSearchFragment()
)
true
}
R.id.action_back -> {
viewModel.paginateBack()
true
}
R.id.action_forward -> {
viewModel.paginateForward()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
viewModel.onPreferencesChanged()
}
}
View Model:
class MainViewModel #Inject constructor(application: Application, private val getMoviesUseCase: GetMoviesUseCase) :
AndroidViewModel(application) {
private var currentPage = 1
private val totalPages: Int by lazy {
with (PreferenceManager.getDefaultSharedPreferences(application)) {
getInt(TOTAL_PAGES, 0)
}
}
// get movies saved in local db
val movies = getMoviesUseCase.allMovies()
init {
loadMovies()
}
private fun loadMovies() {
if (isInternetAvailable(getApplication()))
viewModelScope.launch {
getMoviesUseCase.refreshMovies(currentPage)
}
}
private val _navigateToSelectedMovie = MutableLiveData<Movie>()
val navigateToSelectedMovie: LiveData<Movie>
get() = _navigateToSelectedMovie
fun displayMovieDetails(movie: Movie?) {
if (movie != null) _navigateToSelectedMovie.value = movie
}
fun displayMovieDetailsComplete() {
_navigateToSelectedMovie.value = null
}
Adapter:
class MainAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Movie, MainAdapter.MovieViewHolder>(DiffCallback) {
class MovieViewHolder(private var binding: GridViewItemBinding):
RecyclerView.ViewHolder(binding.root ) {
fun bind(movie: Movie) {
binding.movie = movie
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
return MovieViewHolder(
GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)
)
)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(movie)
}
holder.bind(movie)
}
class OnClickListener(val clickListener: (movie: Movie) -> Unit) {
fun onClick(movie: Movie) = clickListener(movie)
}
}
Data binding adapters:
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<Movie>?) {
val adapter = recyclerView.adapter as MainAdapter
adapter.submitList(data)
}
#BindingAdapter("posterImageUrl")
fun bindPosterImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = (IMAGE_BASE_URL + POSTER_SIZE + imgUrl).toUri().buildUpon().build()
Glide.with(imgView.context)
.load(imgUri)
.apply(RequestOptions().placeholder(R.drawable.loading_animation).error(R.drawable.ic_broken_image))
.into(imgView)
}
}
I now data new Live data appears in my View model, but the Recyclerview doesn't get updated until I swipe to refresh. The viewmodel instance is passed to the recyclerview via databinding. I now in fragments we call the observe method on Live data, but don't know how its is done in this example. In the Mars real estate example it appears to be working. I cannot find where is the difference in my code.
Thanks in advance,
Armands
// in viewModel, is the type of movies LiveData<List<Movie>>?
val movies = getMoviesUseCase.allMovies()
If the type of movies returned from getMoviesUseCase.allMovies() is LivaData<List<Movie>> type, the observers(Recyclerview) can receive the changes.
If the type of movies is List<Movie>, the observers cannot receive the changes, because no one to notify the observers. You should, in this case, change it to the LiveData<List<Movie>>.
private val _movies = MutableLiveData<List<Movie>>().apply { value = emptyList() }
val movies: LiveData<List<Movie>> = _movies
and try to init the _movies like this:
viewModelScope.launch {
_movies.value = getMoviesUseCase.allMovies()
}