Reputation: 3362
I have a RecyclerView activity and a ViewModel class. The activity calls a method in ViewModel which consumes a web-service, and it also observes a LiveData field. Every time the method gets back an an item from the web-service it sets it to the LiveData and so the observer in the Activity is notified and thus all items enter the RecyclerView. It is guaranteed that this flow works correctly as I can see so in the logs, and in the UI too.
The problem arises when I put a delay with Thread.slepp(500) in the method where the web-service is consumed.
Instead of putting an item in the RecyclerView then wait for 500millis and then put another, it waits 500 millis * numberOfItems and then draws them all together.
I can assure that there is no problem with the ViewModel and LiveData setup because the log works as intended meaning, it print the title of the item created, waits 500 millis, prints the next one. So the problem lies only with the adapter and how it's notified only after the method call is complete.
My question is how can I notify the adapter every-time the observer is called?
here is my implementation of the three classes:
RecipeList
class RecipeList : LifecycleActivity() {
var recipeList: MutableList<Recipe> = mutableListOf()
var adapter: RecipeAdapter? = null
var viewModel: RecipeViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recipe_list)
val ingredients = intent.getStringExtra("ingredients")
val term = intent.getStringExtra("term")
viewModel = ViewModelProviders.of(this).get(RecipeViewModel::class.java)
val url = "http://www.recipepuppy.com/api/?i=${ingredients}onions,garlic&q=${term}"
val layoutManager = LinearLayoutManager(this)
adapter = RecipeAdapter(this, recipeList)
rec_recycler_id.layoutManager = layoutManager
rec_recycler_id.adapter = adapter
subscribe()
viewModel?.getRecipe(url)
}
fun subscribe() {
val observer = Observer<Recipe> { recipe ->
if (recipe != null) {
Log.d("mike", "subscribe ${recipe?.title} ")
recipeList.add(recipe)
adapter?.notifyDataSetChanged()
}
}
viewModel?.mRecipe?.observe(this, observer)
}
}
RecipeViewModel
class RecipeViewModel(application: Application): AndroidViewModel(application) {
var recipes: MutableLiveData<MutableList<Recipe>>? = MutableLiveData<MutableList<Recipe>>()
var mRecipe: MutableLiveData<Recipe> = MutableLiveData()
fun getRecipe(url:String){
val requestQueue = Volley.newRequestQueue(this.getApplication())
val recipeRequest = JsonObjectRequest(Request.Method.GET,url,
Response.Listener {
response: JSONObject ->
try {
val results = response.getJSONArray("results")
for( i in 0..results.length()-1){
var recipeObj = results.getJSONObject(i)
var title = recipeObj.getString("title")
var link = recipeObj.getString("href")
var thumbnail = recipeObj.getString("thumbnail")
var ingredients = recipeObj.getString("ingredients")
var recipe = Recipe(title,ingredients,thumbnail,link)
mRecipe.value = recipe
Log.d("mike",title)
Thread.sleep(200)
}
}catch (e: JSONException){
e.printStackTrace()
}
},
Response.ErrorListener {
error: VolleyError? ->
try{
Log.d("error",error.toString())
}catch (e: JSONException){
e.printStackTrace()
}
})
requestQueue?.add(recipeRequest)
}
and the RecipeAdapter
class RecipeAdapter(val context: Context, var recipes: MutableList<Recipe>) : RecyclerView.Adapter<RecipeAdapter.ViewHolder>() {
override fun getItemCount(): Int = recipes.size
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.recipe_rec_row, null)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
holder?.bindViews(recipes[position])
}
inner class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
fun bindViews(recipe: Recipe) {
itemView.textView7.text = recipe.title
itemView.textView9.text = recipe.ingredients
itemView.button6.setOnClickListener() {
if(!recipe.link.trim().isEmpty())
context.startActivity<ShowLinkAct>("url" to recipe.link)
else
context.toast("No link available")
}
if (!recipe.thumbnail.isEmpty()) {
Picasso.with(context)
.load(recipe.thumbnail)
.placeholder(android.R.drawable.ic_menu_report_image)
.error(android.R.drawable.ic_menu_report_image)
.into(itemView.imageView)
} else {
Picasso.with(context).load(android.R.drawable.ic_menu_report_image).into(itemView.imageView)
}
}
}
}
I am looking forward for your suggestions, Thank you in advance
Upvotes: 1
Views: 399
Reputation: 8106
I reommend you to store the data for your adapter inside your adapter. If you'r using AAC you should also check the sample of the GithubBrowser. Here's a little (non tested) sample.
Warning: You should not use context operations inside your RecyclerView because you may get leaks.
BaseAdapter (all the adapters extend this adapter which has a DiffUtil)
abstract class DataBoundListAdapter<T, V : ViewDataBinding> : RecyclerView.Adapter<DataBoundViewHolder<V>>() {
val log = AnkoLogger(javaClass.simpleName)
private var items: List<T>? = null
private var dataVersion = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBoundViewHolder<V> {
val binding = createBinding(parent)
return DataBoundViewHolder(binding)
}
protected abstract fun createBinding(parent: ViewGroup): V
override fun onBindViewHolder(holder: DataBoundViewHolder<V>, position: Int) {
bind(holder.binding, items!![position])
holder.binding.executePendingBindings()
}
@SuppressLint("StaticFieldLeak")
@MainThread
fun replace(update: List<T>?) {
dataVersion++
if (items == null) {
if (update == null) {
return
}
items = update
notifyDataSetChanged()
} else if (update == null) {
val oldSize = items!!.size
items = null
notifyItemRangeRemoved(0, oldSize)
} else {
val startVersion = dataVersion
val oldItems = items
object : AsyncTask<Void, Void, DiffUtil.DiffResult>() {
override fun doInBackground(vararg voids: Void): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldItems!!.size
}
override fun getNewListSize(): Int {
return update.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems!![oldItemPosition]
val newItem = update[newItemPosition]
return [email protected](oldItem, newItem)
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems!![oldItemPosition]
val newItem = update[newItemPosition]
return [email protected](oldItem, newItem)
}
})
}
override fun onPostExecute(diffResult: DiffUtil.DiffResult) {
if (startVersion != dataVersion) {
// ignore update
return
}
items = update
diffResult.dispatchUpdatesTo(this@DataBoundListAdapter)
}
}.execute()
}
}
protected abstract fun bind(binding: V, item: T)
protected abstract fun areItemsTheSame(oldItem: T, newItem: T): Boolean
protected abstract fun areContentsTheSame(oldItem: T, newItem: T): Boolean
override fun getItemCount(): Int {
return if (items == null) 0 else items!!.size
}
}
Here's a sample adapter. You may want to use Databinding if you using AAC. I recommend that in your case! Take care that context operations shouldnt be in the RecyclerView since you may get a leak.
class RecipeAdapter(private val dataBindingComponent: DataBindingComponent,
private val yourVm: ViewModel, private val context: Context) : DataBoundListAdapter<Recipe, RecipeRecRowBinding>() {
override fun createBinding(parent: ViewGroup): RecipeRecRowBinding {
val binding = DataBindingUtil.inflate<RecipeRecRowBinding>(LayoutInflater.from(parent.context), R.layout.recipe_rec_row, parent, false, dataBindingComponent)
return binding
}
override fun bind(binding: RecipeRecRowBinding, recipe: Recipe) {
binding.model = recipe
binding.viewModel = yourVm
binding.itemView.textView7.text = recipe.title
binding.itemView.textView9.text = recipe.ingredients
binding.itemView.button6.setOnClickListener() {
if(!recipe.link.trim().isEmpty())
//ohoh, you shouldnt call something on your activity within your adapter
context.startActivity<ShowLinkAct>("url" to recipe.link)
else
//ohoh, you shouldnt call something on your activity within your adapter
context.toast("No link available")
}
if (!recipe.thumbnail.isEmpty()) {
Picasso.with(context)
.load(recipe.thumbnail)
.placeholder(android.R.drawable.ic_menu_report_image)
.error(android.R.drawable.ic_menu_report_image)
.into(itemView.imageView)
} else {
Picasso.with(context).load(android.R.drawable.ic_menu_report_image).into(itemView.imageView)
}
}
override fun areItemsTheSame(oldItem: Recipe, newItem: Recipe) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Recipe, newItem: Recipe) = oldItem.equals(newItem)
}
Finally your subscriber which push the data to your adapter and take care about changes (DiffUtil)
fun subscribe() {
val observer = Observer<Recipe> {
if (it!= null) {
// it cant be null since you validate it here
Log.d("mike", "subscribe ${it.title} ")
adapter.replace(it)
}
}
viewModel?.mRecipe?.observe(this, observer)
}
Upvotes: 1