Custom adapter in coroutine - android

When I try to fill a ListView using a custom adapter, I get an empty list, I can't figure out what the error is? Why is everything loaded and displayed when using the default adapter?
HeroesAdapter.kt
class HeroesAdapter(context: Context, heroes: List<TestHero>): BaseAdapter() {
private val context = context
private val heroes = heroes
override fun getCount(): Int {
return heroes.count()
}
override fun getItem(position: Int): Any {
return heroes[position]
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
// categoryView = LayoutInflater.from(context).inflate(R.layout.activity_heroes, null)
val categoryView = LayoutInflater.from(context).inflate(R.layout.activity_heroes, parent, false)
// val categoryImage: ImageView = categoryView.findViewById(R.id.heroesImageView)
val categoryText: TextView = categoryView.findViewById(R.id.textHeroView)
val category = heroes[position]
categoryText.text = category.global.name
return categoryView
}
}
HeroesActivity
class HeroesActivity : AppCompatActivity() {
lateinit var adapter : ArrayAdapter<TestHero>
lateinit var adapt: ArrayAdapter<String>
lateinit var heroesAdapt : HeroesAdapter
var listHero = ArrayList<TestHero>()
private val TAG = "HeroesActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_heroes)
// val adapterr = ArrayAdapter(this, android.R.layout.simple_list_item_1,)
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1,
LinkedList<TestHero>())
// heroesListView.adapter = adapter
adapt = ArrayAdapter(this, android.R.layout.simple_list_item_1,
LinkedList<String>())
//heroesListView.adapter = adapt
getCurrentData()
heroesAdapt = HeroesAdapter(this, listHero)
heroesListView.adapter = heroesAdapt
}
private fun getCurrentData() {
val api = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiRequest::class.java)
GlobalScope.launch(Dispatchers.IO) {
val response = api.herList().awaitResponse()
if (response.isSuccessful) {
val data = response.body()!!
Log.d(TAG, data.toString())
withContext(Dispatchers.Main){
// adapter.add(data.global.name)
adapt.add(data.global.platform)
listHero.add(data)
}
}
}
}
}

Looks like you add data to the backing list of the adapter (listHero.add(data)), but you never inform the adapter that its backing data has changed (heroesAdapt.notifyDataSetChanged()).
As an aside, there are some issues with your coroutine. You should not use GlobalScope. Use lifecycleScope instead to avoid leaking network calls and copies of your Activity. Really, you should fetch data in a ViewModel using the ViewModel's scope and expose it via LiveData or SharedFlow so the network call doesn't have to restart if the phone rotates.

Related

null cannot be cast to non-null type RecyclerView with stateFlow

I got some categories from an api and trying to show them on a recycler view but it doesn't work for some reason.
Although the data appears correctly in the logcat, it is sent as null to the Category adapter.
This is the Main Activity (where I'm trying to show the data):
`
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "MEALZ"
private lateinit var binding: ActivityMainBinding
private val viewModel:MealsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = CategoryAdapter(this)
binding.categoriesRv.adapter = adapter
viewModel.getMeals()
lifecycleScope.launch {
viewModel.categories.collect {
adapter.setData(it?.categories as List<Category>)
Log.d(TAG, "onCreate: ${it?.categories}")
}
}
}
}
`
This is Recycler Category Adapter :
`
class CategoryAdapter(private val context: Context?) :
RecyclerView.Adapter<CategoryAdapter.CategoryViewHolder>() {
private var categoryList: MutableList<Category?> = mutableListOf<Category?>()
inner class CategoryViewHolder(itemView: CategoryLayoutBinding) :
RecyclerView.ViewHolder(itemView.root) {
val name = itemView.categoryNameTv
val img = itemView.categoryIv
val des = itemView.categoryDesTv
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder {
val binding = CategoryLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
return CategoryViewHolder(binding)
}
override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
var category = categoryList[position]
holder.name.text = category?.strCategory
holder.des.text = category?.strCategoryDescription
Glide.with(context as Context).load(category?.strCategoryThumb).into(holder.img)
}
override fun getItemCount(): Int {
return categoryList.size
}
fun setData(CategoryList: List<Category>) {
this.categoryList.addAll(CategoryList)
notifyDataSetChanged() //to notify adapter that new data change has been happened to adapt it
}
}
`
This is the View Model class:
#HiltViewModel
class MealsViewModel #Inject constructor(private val getMealsUseCase: GetMeals): ViewModel() {
private val TAG = "MealsViewModel"
private val _categories: MutableStateFlow<CategoryResponse?> = MutableStateFlow(null)
val categories: StateFlow<CategoryResponse?> = _categories
fun getMeals() = viewModelScope.launch {
try {
_categories.value = getMealsUseCase()
} catch (e: Exception) {
Log.d(TAG, "getMeals: ${e.message.toString()}")
}
}
}
you create your _categories with null as initial value, so first value of categories flow will be null and only second one will contain fetched data. As a workaround, you can check that data is not null:
viewModel.categories.collect {
if (it != null) {
adapter.setData(it?.categories as List<Category>)
Log.d(TAG, "onCreate: ${it?.categories}")
}
}
or introduce some kind of "loading" state

RecyclerView only displaying correct data when using breakpoints

I am trying to make my RecylerView display data from a database, however the recyclerView displays nothing, unless i walk through the code with breakpoints, in which case it displays as expected. In the EssayPlanDialogFragment i inisialised an empty arrayList to store the essay lists which then has essayParagrpahs added to it either if the dialogFragemnt is called from the previous essayFragment, in which case the fillIn() function is called to turn database data into paragraphs or if the add paragraph is pressed to add a new empty paragraph. The breakpoint that seem to make it work is on the var paragraphList = ArrayList() and triggers 8 times before displaying the data
RecyclerView Adapter
class EssayPlanAdapter(): RecyclerView.Adapter<EssayPlanAdapter.ViewHolder>() {
var paragraphList = ArrayList<essayParagraph>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
var view = LayoutInflater.from(parent.context).inflate(R.layout.essay_paragraph_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
///code
}
override fun getItemCount(): Int {
return paragraphList.size
}
fun setData(paragraph: ArrayList<essayParagraph>) {
this.paragraphList = paragraph
notifyDataSetChanged()
}
//more code ViewHolder Class and code help with adding essayPlan to databse
}
EssayPlanDialogFragment
class essayPlayDialogFragment(questionId:Int,fromEssay:Int): DialogFragment() {
var paragraphs = ArrayList<essayParagraph>()
var fromEssay = fromEssay
lateinit var introduction: LinearLayout
lateinit var title:EditText
var questionId = questionId
override fun onCreateView(){
//code
}
override fun onViewCreated(view: View, #Nullable savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = EssayPlanAdapter()
title = view.findViewById(R.id.essayTitle)
introduction = view.findViewById(R.id.introduction)
val viewModle = ViewModelProvider(this).get(essayViewModle::class.java)
val paragraphRecycler: RecyclerView = view.findViewById(R.id.essayPlanRecycler)
val closeBtn: ImageButton = view.findViewById(R.id.closeButton)
val finishBtn:ImageButton = view.findViewById(R.id.finnishButton)
//if Dialog fragment opened from previousEssays prafment fromEssay == 1, if from makeNewEssayPragment == 0
if (fromEssay == 1){
val essay = viewModle.checkId(questionId) //returns essay with aprriate essayId
paragraphRecycler.adapter = adapter
paragraphRecycler.layoutManager = LinearLayoutManager(requireContext())
var tempEssays = fillIn(essay,introduction)
adapter.setData(tempEssay)
}
else{
paragraphRecycler.adapter = adapter
paragraphRecycler.layoutManager = LinearLayoutManager(requireContext())
adapter.setData(paragraphs)
}
val button: Button = view.findViewById(R.id.addParagraph)
button.setOnClickListener(){
paragraphs.add(essayParagraph("","","",""))
adapter.setData(paragraphs)
}
finishBtn.setOnClickListener {
//add essay to database
}
fun constructEssayPlanData(adapter: EssayPlanAdapter,intro:LinearLayout,ID:Int):EssayPlan{
//make essayPlan to be added to database
}
fun fillIn(essay:EssayPlan,intro: LinearLayout): ArrayList<essayParagraph>{
intro.lineOfThought.setText(essay.LOT.toString())
intro.relaventPlot.setText(essay.intro.toString())
title.setText("TestTest")
var topicSentences = essay.topicSentences.split("+").toMutableList()
topicSentences.removeAt(0)
var firstQuotes = essay.firstQuotes.split("+").toMutableList()
firstQuotes.removeAt(0)
var secondQuotes = essay.SecondQuotes.split("+").toMutableList()
secondQuotes.removeAt(0)
var thirdQuotes = essay.ThirdQuotes.split("+").toMutableList()
thirdQuotes.removeAt(0)
for(i in 0..(essay.numParagraph -1)){
var essayTemp = essayParagraph(topicSentences[i],firstQuotes[i],secondQuotes[i],thirdQuotes[i])
paragraphs.add(essayTemp)
}
return paragraphs
}
}
ViewModel
fun checkId(id:Int):EssayPlan{
var essay = EssayPlan(id,"","","","","","","",0)
viewModelScope.launch(Dispatchers.IO) {
essay = repository.check(id)
}
return essay
}
repository
fun check(id:Int):EssayPlan{
var essayPlans: EssayPlan = essayPlanDao.checkExist(id)
return essayPlans
}
DAO
#Query("SELECT * FROM plans WHERE id == :id LIMIT 1")
fun checkExist(id:Int):EssayPlan

Kotlin] I want to send some data from One class to Other class by using Intent

I just want to say sorry to my English skill
I've studied the Android Studio and Kotlin these days.
but I'd got a problem on RecyclerViewer and Adapter, for Intent method
work flow chart
this image, this is what i want to do
so i coded the three classes
ShoppingAppActivity.kt, MyAdapter.kt, CartActivity.kt
At ShoppingAppActivity, If I click the itemId ( in the Red box texts)
I make it move to other context(CartActivity)
ShoppingAppActivity working
if i clicked the red box then
cartStatus
go to cart Activity
it worked but already I said, I just want to send only send itemID
covert to String (i will use toString())
SO i tried to use Intent method in ShoppingAppActivity.kt
///PROBLEM PART
adapter?.setOnItemClickListener{
val nextIntent = Intent(this, CartActivity::class.java)
//nextIntent.putExtra("itemID", )
startActivity(nextIntent)
}
///PROBLEM PART
like this but the problem is I don't know what am i have to put the parameter in
nextIntent.putExtra("itemID", )
what should i do?
I think, I should fix MyAdaptor.kt or ShopingAppActivity.kt for this problem.
But in my knowledge, this is my limit. :-(
below
Full Codes of ShoppingAppActivity.kt, MyAdapter.kt, CartActivity.kt
ShoppingAppActivity.kt
class ShoppingAppActivity : AppCompatActivity() {
lateinit var binding: ActivityShoppingAppBinding
private var adapter: MyAdapter? = null
private val db : FirebaseFirestore = Firebase.firestore
private val itemsCollectionRef = db.collection("items")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityShoppingAppBinding.inflate(layoutInflater)
setContentView(binding.root)
updateList()
binding.recyclerViewItems.layoutManager = LinearLayoutManager(this)
adapter = MyAdapter(this, emptyList())
binding.recyclerViewItems.adapter = adapter
///PROBLEM PART
adapter?.setOnItemClickListener{
val nextIntent = Intent(this, CartActivity::class.java)
//nextIntent.putExtra("itemID", )
startActivity(nextIntent)
}
///PROBLEM PART
}
private fun updateList() {
itemsCollectionRef.get().addOnSuccessListener {
val items = mutableListOf<Item>()
for (doc in it) {
items.add(Item(doc))
}
adapter?.updateList(items)
}
}
}
MyAdapter.kt
data class Item(val id: String, val name: String, val price: Int, val cart: Boolean) {
constructor(doc: QueryDocumentSnapshot) :
this(doc.id, doc["name"].toString(), doc["price"].toString().toIntOrNull() ?: 0, doc["cart"].toString().toBoolean() ?: false)
constructor(key: String, map: Map<*, *>) :
this(key, map["name"].toString(), map["price"].toString().toIntOrNull() ?: 0, map["cart"].toString().toBoolean() ?: false)
}
class MyViewHolder(val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root)
class MyAdapter(private val context: Context, private var items: List<Item>)
: RecyclerView.Adapter<MyViewHolder>() {
fun interface OnItemClickListener {
fun onItemClick(student_id: String)
}
private var itemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(listener: OnItemClickListener) {
itemClickListener = listener
}
fun updateList(newList: List<Item>) {
items = newList
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ItemBinding = ItemBinding.inflate(inflater, parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = items[position]
val itemID : String
holder.binding.textID.text = item.id
holder.binding.textName.text = item.name
if(item.cart)
{
holder.binding.textCart.text = "in Cart"
}
else
{
holder.binding.textCart.text = ""
}
holder.binding.textID.setOnClickListener {
AlertDialog.Builder(context).setMessage("You clicked ${item.id}.").show()
itemClickListener?.onItemClick(item.id)
}
holder.binding.textName.setOnClickListener {
//AlertDialog.Builder(context).setMessage("You clicked ${student.name}.").show()
itemClickListener?.onItemClick(item.id)
}
//return item.id.toString()
}
override fun getItemCount() = items.size
}
CartActivity.kt
class CartActivity : AppCompatActivity() {
lateinit var binding: ActivityCartBinding
private val db: FirebaseFirestore = Firebase.firestore
private val itemsCollectionRef = db.collection("items")
private var adapter: MyAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root)
updateList()
//binding.recyclerViewItems.layoutManager = LinearLayoutManager(this)
//adapter = MyAdapter(this, emptyList())
//binding.recyclerViewItems.adapter = adapter
binding.changeCartStatus.setOnClickListener{
//change the button's text if the itemID is corrected
//if(){
// binding.changeCartStatus.text = ""
//}
}
}
private fun updateList() {
itemsCollectionRef.get().addOnSuccessListener {
val items = mutableListOf<Item>()
for (doc in it) {
items.add(Item(doc))
}
adapter?.updateList(items)
}
}
}
You just need to implement listener to your activity
class ShoppingAppActivity : AppCompatActivity() ,MyAdapter.OnItemClickListener {
In oncreate add below line after adapter
adapter?.setOnItemClickListener(this)
Then override its method
override fun onItemClick(id: String){
val nextIntent = Intent(this, CartActivity::class.java)
nextIntent.putExtra("itemID",id )
startActivity(nextIntent)
}

RecyclerView make endless scrolling with JSON

I'm trying to make my Android App (I'm only experienced in iOS).
I created a RecyclerView that gets the data from a web. I tried everything to implement endless scrolling to load more items, but when I call the function to get the items, the entire RecyclerView loads again and no attach the new results on the bottom.
This is my code:
ConversationUser.kt
data class ConversationUser(
val message_nickname: String,
val message_image_thumb: String,
val message_large_thumb: String,
val message_modified: String,
val message_status: String,
val message_unread: Int,
val conv_id: String,
val message_dest: String) {
}
ConversacionesActivity.kt
class ConversacionesActivity : AppCompatActivity() {
// MARK: Variables
var user_token = ""
var user_id = ""
override fun onCreate(savedInstanceState: Bundle?) {
// User Defaults
val sharedPreferences = getSharedPreferences("Preferences", Context.MODE_PRIVATE)
user_token = sharedPreferences.getString("user_token", "")!!
user_id = sharedPreferences.getString("user_id", "")!!
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_conversaciones)
recyclerConv.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
getConversationsData()
recyclerConv.setLoadingListener(object : LoadingListener {
override fun onRefresh() {
//refresh data here
}
override fun onLoadMore() {
// load more data here
getConversationsData()
}
})
}
fun getConversationsData() {
val httpAsync = "https://mywebsite.com/conversations/${user_token}"
.httpPost()
.responseString { request, response, result ->
when (result) {
is Result.Failure -> {
val ex = result.getException()
println(ex)
}
is Result.Success -> {
val data = result.get()
runOnUiThread {
val conversaciones = processJson(data)
show(conversaciones)
return#runOnUiThread
}
}
}
}
httpAsync.join()
}
fun processJson(json: String): List<ConversationUser> {
val gson: Gson = GsonBuilder().create()
val conversaciones: List<ConversationUser> = gson.fromJson(
json,
Array<ConversationUser>::class.java
).toList()
return conversaciones
}
fun show(conversaciones: List<ConversationUser>) {
recyclerConv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerConv.adapter = AdaptadorConv(conversaciones, this, user_token, user_id)
}
AdaptadorConv.kt
class AdaptadorConv(
val conversaciones: List<ConversationUser> = ArrayList(),
val context: Context,
val user_token: String,
val user_id: String) : RecyclerView.Adapter<AdaptadorConv.ConvViewHolder>() {
override fun onBindViewHolder(holder: ConvViewHolder, position: Int) {
holder.convName.text = conversaciones[position].message_nickname
holder.convTime.text = conversaciones[position].message_modified
}
override fun getItemCount(): Int {
return conversaciones.size - 1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConvViewHolder {
val view: View = LayoutInflater.from(parent.context).inflate(
R.layout.conversaciones,
parent,
false
)
return ConvViewHolder(view)
}
class ConvViewHolder(vista: View): RecyclerView.ViewHolder(vista) {
val convImg: ImageView = itemView.findViewById(R.id.convImg)
val convStatus: ImageView = itemView.findViewById(R.id.convStatus)
val convName: TextView = itemView.findViewById(R.id.convName)
val convUnread: TextView = itemView.findViewById(R.id.convUnread)
val convTime: TextView = itemView.findViewById(R.id.convTime)
}
Thanks for any help or hint.
Please check your show () method, you are creating new Adapter every time with the new dataset. You have to append the new items to the adapter's list and adapter should be set to list once. Helpful tutorial can be found at here.

Kotlin: No adapter attached; skipping layout :RecyclerView

I'm trying to implement Recyclerview in my kotlin code....
And I'm using Retrofit getting data from webservice and plot it into recycler view
MainActivity.class
class MainActivity : AppCompatActivity() {
internal lateinit var jsonApi:MyAPI
private val compositeDisposable = CompositeDisposable()
lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recycler_drivers)
// init API
val retrofitt = RetrofitClient.instance
if (retrofitt != null) {
jsonApi = retrofitt.create(MyAPI::class.java)
}
//View
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this)
fetchData()
}
private fun fetchData() {
compositeDisposable.add(jsonApi.drivers
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe{drivers->displayData(drivers)}
)
}
private fun displayData(drivers: List<Driver>?) {
val adapter = DriverAdapter(this,drivers!!)
recycler_drivers.adapter = adapter
}
}
Adapter.class
class DriverAdapter(internal var contex:Context, internal var driverList:List<Driver>): RecyclerView.Adapter<DriverViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.driver_layout, parent, false)
return DriverViewHolder(itemView)
}
override fun getItemCount(): Int {
return driverList.size
}
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) {
holder.txt_driver_number.text = driverList[position].driver_number
holder.txt_first_name.text = driverList[position].first_name
holder.txt_ph_number.text = driverList[position].ph_number.toString()
}
}
ViewHolder.class
class DriverViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val txt_driver_number = itemView.txt_driver_number
val txt_first_name = itemView.txt_first_name
val txt_ph_number = itemView.txt_ph_number
}
This is the API interface
interface MyAPI {
#get:GET("data")
val drivers:Observable<List<Driver>>
}
RetrofitClient Object
object RetrofitClient {
private var ourInstance : Retrofit? = null
var instance: Retrofit? = null
get(){
if(ourInstance == null){
ourInstance = Retrofit.Builder()
.baseUrl("http://localhost/BSProject/public/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
return ourInstance!!
}
}
and this is the Model class which is basically the data coming form my localhost server
class Driver {
var driver_number: String = ""
var first_name: String = ""
var ph_number: Int = 0
}
As you can see I have attached an adapter for Recycleview. so why do I keep getting this error?
I have read other questions related to same problem, but none helps.
Either build the recyclerView inside your displayData()
private fun displayData(drivers: MutableList<Driver>?) {
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = DriverAdapter(this,drivers!!)
recycler_drivers.adapter = adapter
}
Or do what Gabriele Suggested where you attach your adapter to the recyclerviewin onCreate() and add your response data to your adapter after having made the call. This is the ideal approach
class MainActivity: {
lateinit var driverAdapter: DriverAdapter
protected void onCreate() {
...
recyclerView = findViewById(R.id.recycler_drivers)
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = DriverAdapter(this)
recycler_drivers.adapter = adapter
}
private fun displayData(drivers: List<Driver>?) {
driverAdapter.setDrivers(drivers)
}
And you'd expose a method in your adapter to set the data setDrivers()
class DriverAdapter(internal var contex:Context):
RecyclerView.Adapter<DriverViewHolder>()
{
val drivers = mutableListOf()
...
fun setDrivers(drivers: MutableList<Driver>) {
this.drivers = drivers
notifyDataSetChanged()
}
}
This will get rid of your No adapter attached; skipping layout :RecyclerView error
I think you are seeing this issue because of the asynchronous nature of querying the web service through retrofit. You don't actually assign the RecyclerView.Adapter until after onCreate exits.
Try changing the visiblility of the RecyclerView to Gone until the adapter is applied in displayData, then set it to Visible

Categories

Resources