Here is my first time to apply MVVM concept to my Android Application. I follow the steps at the referenced article
https://medium.com/swlh/realtime-firestore-pagination-on-android-with-mvvm-b5e30cea437
And I am managed to load data successfully. When it comes to implementing the onclick event at the row of my RecyclerView List, it comes out that there has no onlick response .
Would you please suggest the better method to implement the onCLick method given that I have applied PageListAdapter?
When I study the PageListAdapter documentation on Android, it seems no clue for me to implement the onclick method.
class MovieViewModel(movieRepository: MovieRepository) : ViewModel() {
private val viewModelJob = SupervisorJob()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var selected: MutableLiveData<RealtimeMovie>? = null
private val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.setPageSize(20)
.build()
val records: LiveData<PagedList<RealtimeMovie>> =
LivePagedListBuilder<String, RealtimeMovie>(
MovieDataSource.Factory(movieRepository, uiScope),
config
).build()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here is my adapter:
class MovieAdapter : PagedListAdapter<RealtimeMovie, MovieAdapter.MovieViewHolder>(
object : DiffUtil.ItemCallback<RealtimeMovie>() {
override fun areItemsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old.id == new.id
override fun areContentsTheSame(old: RealtimeMovie, new: RealtimeMovie): Boolean =
old == new
}
) {
private lateinit var onItemClick: (movie: RealtimeMovie) -> Unit
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.list_movie,
parent,
false
)
return MovieViewHolder(view)
}
infix fun setOnItemClick(onClick: (movie: RealtimeMovie) -> Unit) {
this.onItemClick = onClick
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val record = getItem(position)
holder.bind(record)
holder.itemView.setOnClickListener { onItemClick(record!!) }
}
override fun onViewRecycled(holder: MovieViewHolder) {
super.onViewRecycled(holder)
holder.apply {
txtRecordName.text = ""
crdRecord.isEnabled = true
crdRecord.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
android.R.color.white
)
)
viewHolderDisposables.clear()
}
}
inner class MovieViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val viewHolderDisposables = CompositeDisposable()
val crdRecord by lazy { view.findViewById<MaterialCardView>(R.id.crd_record) }
val txtRecordName by lazy { view.findViewById<TextView>(R.id.txt_record_name) }
fun bind(RealtimeMovie: RealtimeMovie?) {
RealtimeMovie?.let {
it.record
.subscribeBy(
onNext = {
txtRecordName.text = it.title
},
onError = {
// Handle error here
// Record maybe deleted
}
)
.addTo(viewHolderDisposables)
}
}
}
}
Here is my fragment :
..
viewModel = ViewModelProviders.of(this, factory).get(MovieViewModel::class.java)
viewModel.records.observe(this, Observer {
swpRecords.isRefreshing = false
recordsAdapter.submitList(it)
recordsAdapter.setOnItemClick {
print("movie : ${it.id}" )
print("movie : ${it.record}" )
}
})
Related
I have an API which give me the list of doctors. On it's last page only 1 item is there and other items are null like this:
After this i have used paging library for pagination
my pagingSource code: `
class DocPagingSource(val docRepository: DocRepository): PagingSource<Int, Data>() {
override fun getRefreshKey(state: PagingState<Int, Data>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
return try {
val currentPage = params.key?: 1
val city: String = ""
val response = docRepository.getDoctors(city, currentPage)
val page = Math.ceil(response.body()!!.total.toDouble()/5).toInt()
val data = response.body()!!.data
val responseData = mutableListOf<Data>()
responseData.addAll(data)
LoadResult.Page(
data = responseData,
prevKey = if(currentPage==1) null else -1,
nextKey = if (currentPage== page) null else currentPage.plus(1)
)
}catch (e: HttpException){
LoadResult.Error(e)
}catch (e: Exception){
LoadResult.Error(e)
}
}
`
My paging Adapter Code:
class DocAdapter(val context: Context): PagingDataAdapter<Data, DocAdapter.DocViewHolder>(DiffUtil()) {
private lateinit var binding: ItemDoctorsBinding
inner class DocViewHolder : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Data?) {
binding.apply {
txtDocCity.text = item?.city
txtDocName.text = item?.docName
txtDocFees.text = item?.docConsultationFee
txtDocYOE.text = item?.docYoE
txtDocSpecialisation.text = item?.docSpecialisation
Glide.with(context)
.load(item?.docProfileImgUrl)
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(docPhoto)
}
}
}
override fun onBindViewHolder(holder: DocViewHolder, position: Int) {
val item = getItem(position)
if (item!=null){
holder.bind(getItem(position)!!)
holder.setIsRecyclable(false)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DocViewHolder {
val inflater = LayoutInflater.from(context)
binding = ItemDoctorsBinding.inflate(inflater, parent, false)
return DocViewHolder()
}
class DiffUtil: androidx.recyclerview.widget.DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.docId == newItem.docId
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem==newItem
}
}}
what I am getting after reaching my 16th item in doctor list on last page it should show entry till 16th item but after that it also shows again like this:
Also if i dont use holder.setIsRecyclable(false) in pagingAdapter then this android icon not shown but then list is populated with previous doctors:
on the top DR. Sixteen is shown like this:
and in between it again shows like this:
My doctorViewModel Class:
class DocViewModel(val repository: DocRepository): ViewModel() {
val loading = MutableLiveData<Boolean>()
val docList = Pager(PagingConfig(5, maxSize = 100)){
DocPagingSource(repository)
}.flow.cachedIn(viewModelScope)}
My main Activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var docViewModel: DocViewModel
private lateinit var docAdapter: DocAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val docListRepository = DocRepository()
val docFactory = DocViewModelFactory(docListRepository)
docViewModel = ViewModelProvider(this, docFactory).get(DocViewModel::class.java)
docAdapter = DocAdapter(this)
lifecycleScope.launchWhenCreated {
docViewModel.docList.collect{
docAdapter.submitData(it)
}
}
binding.docRecyclerView.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = docAdapter
setHasFixedSize(true)
}
}}
I have solved this error by applying a condition in my paging source code
This is my main activity of my app. I'm using nav controller.
class MainActivity : AppCompatActivity() {
lateinit var viewModel: NewsViewModel
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val newRepository = NewsRepository(ArticleDatabase(this))
val viewModelProviderFactory = NewsViewModelProviderFactory(newRepository)
viewModel = ViewModelProvider(this , viewModelProviderFactory).get(NewsViewModel::class.java)
// bottomNavigationView.setupWithNavController(newsNavHostFragment.findNavController())
val navHostFragment= supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
navController= navHostFragment.navController
bottomNavigationView.setupWithNavController(navController)
}
This the breaking news fragment. The function is same in other fragments so only posting this one
class BreakingNewsFragment : Fragment (R.layout.fragment_breaking_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as MainActivity).viewModel
setUpRecyclerView()
newsAdapter!!.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article" , it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
viewModel.breakingNews.observe(viewLifecycleOwner, Observer { response ->
when(response) {
is Resource.Success -> {
hideProgressBar()
response.data?.let { newsResponse ->
newsAdapter.differ.submitList(newsResponse.articles)
}
}
is Resource.Error -> {
hideProgressBar()
response.message?.let { message ->
Log.e("Breaking Fragment", "An error occured: $message")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
}
private fun hideProgressBar() {
paginationProgressBar.visibility = View.INVISIBLE
}
private fun showProgressBar() {
paginationProgressBar.visibility = View.VISIBLE
}
fun setUpRecyclerView() {
newsAdapter = NewsAdapter()
rvBreakingNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
}
}
This is made from Youtuber Philip lackner's tutorial. Here's the the link of original project
https://github.com/philipplackner/MVVMNewsApp . His version of fragment is deprecated so i used new fragment view in xml file. so there is an issue in using nav controller.
EDIT -> Forgot to put errors. Here it is
2022-10-17 01:08:09.796 3971-3971/com.arpit.newsapp20 E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arpit.newsapp20, PID: 3971
java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.hashCode()' on a null object reference
at com.arpit.newsapp20.models.Article.hashCode(Unknown Source:15)
at androidx.navigation.NavBackStackEntry.hashCode(NavBackStackEntry.kt:256)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at androidx.navigation.NavController.linkChildToParent(NavController.kt:143)
at androidx.navigation.NavController.addEntryToBackStack(NavController.kt:1918)
at androidx.navigation.NavController.addEntryToBackStack$default(NavController.kt:1813)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1721)
at androidx.navigation.NavController$navigate$4.invoke(NavController.kt:1719)
at androidx.navigation.NavController$NavControllerNavigatorState.push(NavController.kt:287)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:198)
at androidx.navigation.fragment.FragmentNavigator.navigate(FragmentNavigator.kt:164)
at androidx.navigation.NavController.navigateInternal(NavController.kt:260)
at androidx.navigation.NavController.navigate(NavController.kt:1719)
at androidx.navigation.NavController.navigate(NavController.kt:1545)
at androidx.navigation.NavController.navigate(NavController.kt:1472)
at androidx.navigation.NavController.navigate(NavController.kt:1454)
at com.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:31)
at com.arpit.newsapp20.ui.BreakingNewsFragment$onViewCreated$1.invoke(BreakingNewsFragment.kt:27)
at com.arpit.newsapp20.adapters.NewsAdapter.onBindViewHolder$lambda-2$lambda-1(NewsAdapter.kt:56)
at com.arpit.newsapp20.adapters.NewsAdapter.$r8$lambda$FmTlKYZBcoLQp02jU2NS9dL1z-k(Unknown Source:0)
at com.arpit.newsapp20.adapters.NewsAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
at android.view.View.performClick(View.java:7441)
at android.view.View.performClickInternal(View.java:7418)
at android.view.View.access$3700(View.java:835)
at android.view.View$PerformClick.run(View.java:28676)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7839)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Here is the article class
#Entity(
tableName = "articles"
)
data class Article(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val author: String,
val content: String?,
val description: String,
val publishedAt: String,
val source: Source,
val title: String,
val url: String,
val urlToImage: String
) : Serializable
Here is the news adapter class
class NewsAdapter : RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
private val differCallback = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article_preview,
parent,
false
)
)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
private var onItemClickListener: ((Article) -> Unit)? = null
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.itemView.apply {
Glide.with(this).load(article.urlToImage).into(ivArticleImage)
tvSource.text = article.source.name
tvTitle.text = article.title
tvDescription.text = article.description
tvPublishedAt.text = article.publishedAt
setOnClickListener {
onItemClickListener?.let { it(article) }
}
}
}
fun setOnItemClickListener(listener: (Article) -> Unit) {
onItemClickListener = listener
}
}
You look like to want to pass the clicked news item to the article fragment. But in your click listener you don't receive the clicked item. Instead you pass something else in "it". I think that's why you get a null pointer exception.
You need something like following:
newsAdapter!!.setOnItemClickListener {
val newsItem = // get clicked news item from adapter
newsItem?.let {
val bundle = Bundle().apply {
putSerializable("article" , it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment2 , bundle
)
}
}
I'll give you some hint of my app , maybe help with my issue .
I have an Api an with retrofit I'll get this data . after that insert all data in roomdatabase .
In my main screen with a recyclerview I display all this product (data) .
Near each of this product I have a button to send product to the cart fragment .also have a value(textview) to save each button is pressed to show how many items gone to cart fragment .
I use a upsert(transaction) for insert or update the product to cart fragment .
and after that use a obvserver livedata to display the amount value of products that is send to cart .
First i need A way to implement the observe live data in my main screen instead of adapter / and also is my upsert query is correct ? sometimes when click on products button the other products value changes .
Here is my code :
Dao :
// this is for Maintable
#Query("SELECT * FROM main")
fun getalldata(): LiveData<List<Roomtable>>
// cart Table
#Query("SELECT * FROM Cart")
fun getAllFromCart(): LiveData<List<CartTable>>
#Query("SELECT * FROM Cart WHERE id = :int")
fun GetAllFromCart(int: Int): LiveData<List<CartTable>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertToCart(model: CartTable): Long
#Query("UPDATE cart SET amount = amount+1 WHERE id = :int")
fun updateCart(int: Int)
#Transaction
fun upsert(model: CartTable) {
val id = insertToCart(model)
if (id == -1L) {
model.id?.let {
updateCart(it)
}
MainAdapter :
class RecyclerAdapterMain(
private val product: List<Roomtable>,
val context: Context,
private val viewlifecyclerOwner: LifecycleOwner
) :
RecyclerView.Adapter<RecyclerAdapterMain.ViewHolder>() {
val viewModel: ViewModelRoom by lazy {
ViewModelProvider.AndroidViewModelFactory(Application()).create(ViewModelRoom::class.java)
}
inner class ViewHolder(itemview: View) :
RecyclerView.ViewHolder(itemview) {
val title: TextView = itemview.product_txt
val price: TextView = itemview.price_product
val imageproduct: ImageView = itemview.product_image
val btn_add_product: Button = itemview.btn_add_product
var amount_value: TextView = itemview.amount_value
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutview =
LayoutInflater.from(parent.context).inflate(R.layout.product_items, parent, false)
return ViewHolder(layoutview)
}
override fun getItemCount(): Int = product.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
var products = product[position]
holder.title.text = products.title
holder.price.text = products.price
Picasso.get().load(products.image).into(holder.imageproduct)
holder.btn_add_product.setOnClickListener {
products.amount++
viewModel.upsert(
CartTable(
holder.adapterPosition,
products.title,
products.price,
products.image,
products.amount
)
)
viewModel.GetallFromCart(holder.adapterPosition).observe(viewlifecyclerOwner, Observer {
if (it != null) {
for (item in it) {
holder.amount_value.text = item.amount.toString()
}
}
})
}
Main Activity :
class HomeActivity : AppCompatActivity(){
val viewModel: ViewModelRoom by lazy {
ViewModelProvider(this).get(ViewModelRoom::class.java)
}
#RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
loadProduct()
#RequiresApi(Build.VERSION_CODES.M)
private fun loadProduct() {
swipeRefreshMain.isRefreshing = true
if (hasNetworkAvilable(applicationContext)) {
viewModel.setup()
viewModel.products.observe(this, Observer {
loadrecycler(it)
})
} else {
Toast.makeText(
applicationContext,
"برای بروز رسانی محصولات اینترنت خود را روشن کنید",
Toast.LENGTH_LONG
).show()
viewModel.getalldata().observe(this, Observer {
if (!it.isNullOrEmpty()) {
loadrecycler(it)
} else {
val builder = AlertDialog.Builder(this)
.setView(R.layout.customalertdialog)
.setPositiveButton("Ok", null)
.create()
.show()
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workmanager: WorkManager = WorkManager.getInstance(this)
val workRequest = OneTimeWorkRequest.Builder(UploadWorkerClass::class.java)
.setConstraints(constraint)
.build()
workmanager.enqueue(workRequest)
}
})
}
}
fun loadrecycler(product: List<Roomtable>) {
val swipeRefreshLayout: SwipeRefreshLayout = findViewById(R.id.swipeRefreshMain)
val recycler: RecyclerView = findViewById(R.id.recycler_main)
recycler.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(
product, this#HomeActivity, this#HomeActivity )
Handler(Looper.getMainLooper()).postDelayed({
swipeRefreshLayout.isRefreshing = false
}, randomInRange(1, 3) * 1000.toLong())
}
}
}
I'm using the JetPack paging library with a network call (no database).
I am able to scroll down smoothly and load new pages of data, BUT, when scrolling up it stutters and quickly jumps to the top of the list. I am unable to scroll up smoothly.
Here is a video showing the problem: https://imgur.com/a/bRoelyF
What I've Tried:
Enabling retrofit caching
Using a LinearLayoutManager instead of GridLayoutManager
Following old and newer tutorials with versions 1.0.1 and 2.1.2 of the library
Here is my code:
MovieDataSource.kt:
private val movieDbApi: TheMovieDbApi
) : PageKeyedDataSource<Int, Movie>() {
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {}
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Movie>) {
movieDbApi.getTopRatedMovies(BuildConfig.MOVIE_DATA_BASE_API, FIRST_PAGE).subscribe(
{
it?.let { callback.onResult(it.results, null, FIRST_PAGE + 1) }
}, {}
)
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
movieDbApi.getTopRatedMovies(BuildConfig.MOVIE_DATA_BASE_API, params.key).subscribe(
{
val key = params.key + 1
it?.let {callback.onResult(it.results, key)
}
},{}
)
}
MovieDataSourceFactory.kt:
class MovieDataSourceFactory(private val movieDbApi: TheMovieDbApi) :
DataSource.Factory<Int, Movie>() {
// Is this where the MovieDataSource callBacks are sent?
val movieLiveDataSource = MutableLiveData<MovieDataSource>()
override fun create(): DataSource<Int, Movie> {
val movieDataSource = MovieDataSource(movieDbApi)
movieLiveDataSource.postValue(movieDataSource)
return movieDataSource
}
}
HomeViewModel.kt:
class HomeViewModel #Inject constructor(
theMovieDbApi: TheMovieDbApi
) : DisposingViewModel() {
var moviePagedList: LiveData<PagedList<Movie>>
private var liveDataSource: LiveData<MovieDataSource>
init {
val movieDataSourceFactory = MovieDataSourceFactory(theMovieDbApi)
liveDataSource = movieDataSourceFactory.movieLiveDataSource
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(MovieDataSource.PAGE_SIZE)
.build()
moviePagedList = LivePagedListBuilder(movieDataSourceFactory, config)
.build()
}
}
HomeViewModel.kt:
class HomeActivity : AppCompatActivity() {
#Inject
internal lateinit var viewModelFactory: ViewModelFactory<HomeViewModel>
private lateinit var viewModel: HomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
AndroidInjection.inject(this)
val adapter = HomeAdapter()
movie_recycler_view.setHasFixedSize(false)
movie_recycler_view.layoutManager = LinearLayoutManager(this)
val viewModel = ViewModelProvider(this, viewModelFactory).get(HomeViewModel::class.java)
viewModel.moviePagedList.observe(this, Observer {
adapter.submitList(it)
})
movie_recycler_view.adapter = adapter
}
}
HomeAdapter.kt:
class HomeAdapter : PagedListAdapter<Movie, HomeAdapter.MovieViewHolder>(USER_COMPARATOR) {
override fun getItemCount(): Int {
return super.getItemCount()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
movie?.let { holder.bind(it) }
}
class MovieViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(movie: Movie) {
Picasso.get().load(BASE_IMAGE_URL + movie.poster_path).into(itemView.movie_image)
}
}
companion object {
private val USER_COMPARATOR = 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
}
}
}
If anyone has a solution or spots a problem I'd love to hear it!
I solved the problem.
It's because I didn't add a placeholder image to Picasso in the adapter.
Before:
Picasso.get()
.load(BASE_IMAGE_URL + movie.poster_path)
.into(itemView.movie_image)
After:
Picasso.get()
.load(BASE_IMAGE_URL + movie.poster_path)
.placeholder(R.drawable.placeholder)
.into(itemView.movie_image)
Now it loads well.
Another consideration is the size of the image, it takes a while to load a larger image especially if you are loading many of them within an infinite scroll.
I'm working on a Grid RecyclerView for Scores App. Scores will be updated every 5 secs from API Call...
I'm using this library for my Recyclerview Adapter.
I'm using Observable Fields & List.
I need to update only the scores (textviews) but now the RecyclerView is getting updated every 5 secs.
Please Guide me in right direction. Thank you!
Full Code in Gist
class DashboardFragment : Fragment() {
private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var binding: FragmentDashboardBinding
lateinit var retrofit: Retrofit
lateinit var apiService: APIService
lateinit var disposable: Disposable
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dashboardViewModel = ViewModelProvider(this).get(DashboardViewModel::class.java)
fetchData()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil
.inflate(inflater, R.layout.fragment_dashboard, container, false)
binding.viewModel = dashboardViewModel
return binding.root
}
fun fetchData() {
val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val gson = GsonBuilder()
.setLenient()
.create()
retrofit = Retrofit.Builder()
.baseUrl(APIService.BASE_URL)
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
apiService = this.retrofit.create(APIService::class.java)
callIndicesEndpoint(null)
disposable = Observable.interval(1000, 2000, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ aLong: Long? -> this.refreshIndices(aLong)})
{ throwable: Throwable -> this.onError(throwable) }
}
#SuppressLint("CheckResult")
private fun callIndicesEndpoint(aLong: Long?) {
val observable =
apiService.indices
observable.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
.map { result: ObservableArrayList<Indices> -> result }
.subscribe(
{ data: ObservableArrayList<Indices> ->
this.handleResults(data)
}
) { t: Throwable ->
this.handleError(t)
}
}
#SuppressLint("CheckResult")
private fun refreshIndices(aLong: Long?) {
val observable =
apiService.indices
observable.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
.map { result: ObservableArrayList<Indices> -> result }
.subscribe({ data: ObservableArrayList<Indices> -> this.refreshResults(data)})
{ t: Throwable ->this.handleError(t)}
}
private fun handleResults(data: ObservableArrayList<Indices>) {
dashboardViewModel.populate(data)
}
private fun refreshResults(data: ObservableArrayList<Indices>) {
dashboardViewModel.refresh(data)
}
private fun onError(throwable: Throwable) {
Log.e(">>>", "ERROR")
}
private fun handleError(t: Throwable) {
Log.e("> >", t.localizedMessage + " - Err - " + t.cause)
//Add your error here.
}
override fun onPause() {
super.onPause()
disposable.dispose()
}
}
VIEWMODEL
class DashboardViewModel : ViewModel() {
val indices: ObservableList<Indices> = ObservableArrayList<Indices>()
fun populate(data: ArrayList<Indices>) {
indices.clear()
indices.addAll(data)
}
fun refresh(data: ArrayList<Indices>) {
// How to refresh only Items without recreating the List
}
val itemIds: ItemIds<Any> =
ItemIds { position, item -> position.toLong() }
val indicesItem: ItemBinding<Indices> =
ItemBinding.of<Indices>(BR.item, R.layout.item_futures)
}
You can use broadcast receiver functionality for update particular items from recyclerview .
I think what you need is to use DiffUtil, it'll be of great help as
DiffUtil figures out what has changed, RecyclerView can use that information to update only the items that were changed, added, removed, or moved, which is much more efficient than redoing the entire list.
Here is a course you can use to get a hand of it: https://codelabs.developers.google.com/codelabs/kotlin-android-training-diffutil-databinding/#3
When I had something like that I used the DiffUtil.
I had to update rates every X seconds and not change the whole list.
So the adapter should be something like this:
class RatesAdapter :
ListAdapter<Rate, RecyclerView.ViewHolder>(RateDiffCallback()) {
private val baseRateView = 0
private val rateView = 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == rateView) {
RateHolder(
RateItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
} else {
BaseRateHolder(
BaseRateLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) {
baseRateView
} else rateView
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val rate = getItem(position)
if (holder.itemViewType == rateView) {
(holder as RateHolder).bind(rate)
holder.itemView.setOnClickListener {
swapper.itemSwap(rate)
}
} else {
(holder as BaseRateHolder).bind(rate)
}
}
class RateHolder(
private val binding: RateItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Rate) {
binding.apply {
rate = item
executePendingBindings()
}
}
}
class BaseRateHolder(
private val binding: BaseRateLayoutBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Rate) {
binding.apply {
rate = item
executePendingBindings()
}
}
val value = binding.value
}
}
private class RateDiffCallback : DiffUtil.ItemCallback<Rate>() {
override fun areItemsTheSame(oldItem: Rate, newItem: Rate): Boolean {
return oldItem.currencyCode == newItem.currencyCode
}
override fun areContentsTheSame(oldItem: Rate, newItem: Rate): Boolean {
return oldItem.rate == newItem.rate
}
}
Here I used binding in the adapter, and if you are not familiar with it, I will be more than happy to explain as well
In the Activity:
Before the onCreate:
private val ratesAdapter = RatesAdapter()
In the onCreate:
ratesList.adapter = ratesAdapter
Whenever you need to update the adapter including the first time you need to call it:
ratesAdapter.submitList(rates)
Rate is the model class that I am using
rates is the mutable list of < Rate >
ratesList is the recyclerview
try this : notifyItemChanged
with this:
listView.setItemAnimator(null);