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
Related
I'm trying to rewrite my program and start using Kotlin Coroutines.
That is my function to retrieve a list of products for a given group. After debugging it looks like everything is correct.
class FirebaseRepository {
private val db = FirebaseFirestore.getInstance()
private val auth = FirebaseAuth.getInstance()
fun getCurrentUserId(): String{
return auth.currentUser!!.uid
}
suspend fun getLista(): MutableLiveData<List<Produkt>> {
val result = MutableLiveData<List<Produkt>>()
val lista = mutableListOf<Produkt>()
db.collection(Constants.GROUP)
.document("xGRWy21hwQ7yuBGIJtnA")
.collection("Przedmioty")
.orderBy("dataDodaniaProduktu", Query.Direction.DESCENDING)
.get().await().forEach {
val singleProdukt = it.toObject(Produkt::class.java)
singleProdukt.produktId = it.id
lista.add(singleProdukt)
result.postValue(lista)
}
return result
}
That is my ViewModel class:
class ListaViewModel: ViewModel() {
private val repository = FirebaseRepository()
var _produkty = MutableLiveData<List<Produkt>>()
val produkty : LiveData<List<Produkt>> = _produkty
init {
viewModelScope.launch {
_produkty = repository.getLista()
}
}
And finally in my fragment I'm trying to observe live data but looks like nothing is being passed to my adapter. What am I doing wrong?
class ListaFragment : Fragment(), ListaAdapter.OnItemClickListener {
private var _binding: FragmentListaBinding? = null
private val binding get() = _binding!!
private lateinit var recyclerView : RecyclerView
private lateinit var listAdapter : ListaAdapter
private val listaViewModel by viewModels<ListaViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentListaBinding.inflate(inflater, container, false)
recyclerView = binding.recyclerView
listAdapter = ListaAdapter(emptyList(), this)
recyclerView.adapter = listAdapter // Zapobiega "No adapter attached; skipping layout"
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
recyclerView.setHasFixedSize(true)
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
listAdapter = ListaAdapter(it, this)
}
return binding.root
}
Try replacing this:
val produkty : LiveData<List<Produkt>> = _produkty
with this
val produkty : LiveData<List<Produkt>> get() = _produkty
This way you'll have "getter" rather than "initializer". Initializer will compute its value once (to the empty live data) and after you reassign that var it won't change the value of your val.
The problem in your code lies in the fact that you're creating a new instance of your ListaAdapter class inside the observe() method, without notifying the adapter about the changes. That's the reason why you're getting no results in the adapter. To solve this, simply create a method inside your adapter class:
fun setProduktList(produktList: List<Produkt>) {
this.produktList = produktList
notifyDataSetChanged()
}
Then inside your observe() method, use the following line of code:
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
//listAdapter = ListaAdapter(it, this) //Removed
listAdapter.setProduktList(it) 👈
}
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.
The problem I am facing with the RecyclerView is the data is coming from Server and the API response is getting printed correctly in the console.
but when I am trying to set data in the adapter what is wrong or something is not going correctly with the flow that the data is not being updated on UI.
//This is my adapter class
class DashboardAdapter(val context: Context) : RecyclerView.Adapter<DashBoardHolder>() {
private var transactionList = ArrayList<DashboardData>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DashBoardHolder {
val inflator = LayoutInflater.from(parent.context)
val view = ActivityDashboardDataBinding.inflate(inflator, parent, false)
val viewHolder = DashBoardHolder(view)
return viewHolder
}
override fun onBindViewHolder(holder: DashBoardHolder, position: Int) {
val quickModel = transactionList[position]
holder.tvName.text = quickModel.bookingTitle
}
fun showListItems(dashboardlist: List<DashboardData>?, aboolean: Boolean) {
when {
aboolean -> transactionList.clear()
}
if (dashboardlist != null && !dashboardlist.isEmpty())
this.transactionList.addAll(dashboardlist)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return transactionList.size
}
}
MyHolderClass
class DashBoardHolder(val binding: ActivityDashboardDataBinding) :
RecyclerView.ViewHolder(binding.root) {
var tvName: TextView = binding.textViewGrandrukName
var tvTime: TextView = binding.tvGrandrukTripDetails
var tvPlace: ImageView = binding.btnGhandruk
var ivRectangle: ImageView = binding.imageView5
}
Similarly,I set the adapter in view section like this way:
//setting adapter in Presenter class
fun setAdapter() {
var layoutmanager: LinearLayoutManager? = LinearLayoutManager(appCompatActivity)
val firstVisiblePosition = layoutmanager!!.findFirstVisibleItemPosition()
binding!!.includesDashboardRecyclerview.rvBookingList.setHasFixedSize(true)
binding!!.includesDashboardRecyclerview.rvBookingList.layoutManager = layoutmanager
binding!!.includesDashboardRecyclerview.rvBookingList.adapter = dashboardAdapter
layoutmanager!!.scrollToPositionWithOffset(firstVisiblePosition, 0)
}
In Presenter Class, calling setAdapter class from presenter like this way
class DashboardPresenter(
private val dashboardView: DashboardView,
private val dashboardModel: DashboardModel
) {
fun onCreateView() {
onClick()
dashboardView.setAdapter()
getDashboardRequest()
}
//calling adpter function here
fun showList(termlist: List<DashboardData>?, aboolean: Boolean) {
(null as DashboardAdapter?)?.showListItems(termlist!!, aboolean)
}
}
I'm not able to understand what is getting wrong here.
(null as DashboardAdapter?)?.showListItems(termlist!!, aboolean)
You are calling showListItems() on the DashboardAdapter as a type not the instance dashboardAdapter. Assuming that dashboardAdapter is a local class field.
Also I guess this type casting is not necessary as you already using the optional ?
So, it can be simplified to:
dashboardAdapter?.showListItems(termlist!!, aboolean)
Assuming that this should be called whenever you retrieve the API response. So, showList() must be called when there's new API data.
I have implemented RecyclerView in my app with Kotlin using Refrofit, MVVM, DataBinding, Coroutines. The same code works fine in another fragment but not here.
*Note: The retrofit functions returns the commentsList successfully but only problem in displaying the list in a recyclerView.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val api = ApiRepository()
factory = CommentsViewModelFactory(api)
viewModel = ViewModelProvider(this, factory).get(CommentsViewModel::class.java)
viewModel.getComments(requireActivity())
viewModel.commentsList.observe(viewLifecycleOwner, Observer { comments ->
rvComment.also {
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
if (comments != null) {
it.adapter = HomeServicesCommentsAdapter(comments, this)
}
}
})
}
The ViewModel looks like this, i declared the comments as MutableLiveData, which returns the data successfully but the only issue is with the adapter attachment.
class CommentsViewModel(private val repository: ApiRepository) : ViewModel() {
var userComment: String? = null
private val comments = MutableLiveData<List<Comment>>()
private lateinit var job: Job
val commentsList: MutableLiveData<List<Comment>>
get() = comments
fun getComments(context: Context) {
job = CoroutinesIO.ioThenMain(
{
repository.getServices(context)
}, {
for (i in it!!.data.data)
comments.value = i.comments
}
)
}
Here is the adapter implementation
class HomeServicesCommentsAdapter(
private val comments: List<Comment>,
private val listenerService: RvListenerServiceComments
) : RecyclerView.Adapter<HomeServicesCommentsAdapter.ServicesViewHolder>() {
override fun getItemCount() = comments.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ServicesViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.custom_comment_layout,
parent,
false
)
)
override fun onBindViewHolder(holder: ServicesViewHolder, position: Int) {
holder.recyclerViewServicesBinding.comments = comments[position]
notifyDataSetChanged()
}
class ServicesViewHolder(
val recyclerViewServicesBinding: CustomCommentLayoutBinding
) : RecyclerView.ViewHolder(recyclerViewServicesBinding.root)
}
Let me know if you need the xml layout files.
Instead of giving layout manager at runtime while observing data ,
Define layoutmanager inside xml
eg:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvNews"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
tools:listitem="#layout/item_your_layout"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
Remove below lines from observer
it.layoutManager = LinearLayoutManager(requireContext())
EDIT:
Do not create instance of adapter while observing data because observing data is not on MainThread So make sure you set data on MainThread
val adapter = HomeServicesCommentsAdapter(arrayListOf(), this)
rvComment?.adapter = adapter
viewModel.getComments(requireActivity())
viewModel.commentsList.observe(viewLifecycleOwner, Observer { comments ->
comments?.let{adapter.setData(comments)}//define setData(list:ArrayList<Comments>) method in your adapter
})
HomeServicesCommentsAdapter.kt:
........
private var mObjects: MutableList<Comment>? = ArrayList()// top level declaration
fun setData(objects: List<Comment>?) {
this.mObjects = objects as MutableList<Comment>
this.notifyDataSetChanged()
}
......
In my application i have one list and into this this i have another list!(I know this is bad idea for UI but i should develop this!)
For show this lists i used RecyclerView and initialize other RecyclerView into previous RecyclerView adapter!
Activity codes:
class DashboardCardsActivity : BaseActivity(), DashboardCardsContracts.View {
#NonNull
private lateinit var presenter: DashboardCardsPresenterImpl
private var context: Context = this
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: DashboardCardsAdapter
private lateinit var layoutManager: LinearLayoutManager
private val modelList: MutableList<UserPackageOrderResponse.Res.UserPackageOrder> = mutableListOf()
private var isLoadingFlag = false
private var isHasLoadedAll = false
private var nextPage = 1
//Token
private var token = GoodPrefs.getInstance().getString(PrefsKey.USER_JWT_TOKEN.name, "")
override var layoutId: Int = R.layout.activity_dashboard_cards
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Initialize
context = this
presenter = DashboardCardsPresenterImpl(context, this)
layoutManager = LinearLayoutManager(context)
adapter = DashboardCardsAdapter(context, modelList)
//Init recyclerView
recyclerView = dashboardCards_pullList.recyclerView
recyclerView.initRecyclerView(layoutManager, adapter)
//Set image to loader
dashboardCards_pullList.setColorSchemeResources(R.color.colorAccent)
//Init toolbar
toolbarBase_toolbar.title = getString(R.string.basketCard)
toolbarBase_toolbar.setToolbarBackWithFinish(this)
//Call api
getLazyPullLoader()
}
I write below codes :
class DashboardListAdapter constructor(
val context: Context, val model: MutableList<UserPackageOrderResponse.Res.UserPackageOrder>
) : RecyclerView.Adapter<DashboardCardsAdapter.ViewHolder>() {
private var expansionlayout: ExpansionLayoutCollection = ExpansionLayoutCollection()
private lateinit var registerTimeUtil: TimeUtils
private var registerDateSplit: List<String> = emptyList()
private var registerDate: List<String> = emptyList()
private var orderState: String = ""
private var layoutManager: LinearLayoutManager? = null
private var adapter: DashboardCardsOrderAdapter
private val orderModelList: MutableList<UserPackageOrderResponse.Res.UserPackageOrder.Order> = mutableListOf()
init {
expansionlayout.openOnlyOne(true)
layoutManager = object : LinearLayoutManager(context) {
override fun canScrollVertically(): Boolean {
return false
}
}
adapter = DashboardCardsOrderAdapter(orderModelList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(inflater.inflate(R.layout.row_dashboard_card_list, parent, false))
}
override fun getItemCount(): Int {
return model.size
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
expansionlayout.add(holder.getExpansions())
//header
holder.orderNumber.text = model[position].hashcode
holder.rowNumber.text = "${position + 1}"
//Set date
registerDateSplit = model[position].createdAt.split(" ")
registerDate = registerDateSplit[0].split("-")
registerTimeUtil = TimeUtils(registerDate[0].toInt(), registerDate[1].toInt(), registerDate[2].toInt())
holder.orderRegisterDate.text = registerTimeUtil.getIranianDate()
//Order state
orderState = model[position].status
holder.setViewWithState(orderState, context)
//Content prices
holder.finalPrice.text = "${model[position].price.moneySeparating()} ${context.getString(R.string.toman)}"
holder.paymentPrice.text = "${model[position].price.moneySeparating()} ${context.getString(R.string.toman)}"
holder.postPrice.text = "${model[position].postFee.moneySeparating()} ${context.getString(R.string.toman)}"
holder.discountPrice.text = "${model[position].discount.moneySeparating()} ${context.getString(R.string.toman)}"
//init order list
orderModelList.clear()
orderModelList.addAll(model[position].orders)
layoutManager?.let {
holder.orderList.initRecyclerView(it, adapter)
}
}
}
But when running application show me ForceClose error and show me below message in logCat :
java.lang.IllegalArgumentException: LayoutManager com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsAdapter$1#d33c634 is already attached to a RecyclerView: androidx.recyclerview.widget.RecyclerView{5a76c5d VFED..... ......ID 0,0-682,178 #7f080089 app:id/dashboardCard_orderList}, adapter:com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsOrderAdapter#83ba9d2, layout:com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsAdapter$1#d33c634, context:com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsActivity#79c2f05
at androidx.recyclerview.widget.RecyclerView.setLayoutManager(RecyclerView.java:1340)
at com.app.android.utils.ExtensionsKt.initRecyclerView(Extensions.kt:74)
at com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsAdapter.onBindViewHolder(DashboardCardsAdapter.kt:77)
at com.app.android.ui.home.fragments.dashboard.activities.carts_list.DashboardCardsAdapter.onBindViewHolder(DashboardCardsAdapter.kt:22)
at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:6781)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:6823)
But after change my code with below case not show me any error, but scrolling is very slowly and show lag!
//init order list
orderModelList.clear()
orderModelList.addAll(model[position].orders)
layoutManager = object : LinearLayoutManager(context) {
override fun canScrollVertically(): Boolean {
return false
}
}
layoutManager?.let {
holder.orderList.initRecyclerView(it, adapter)
}
I initialized layoutManager in onBindView(), then not show me error but show me many lag when scrolling on items!
How can i fix this?
You have defined this:
private var layoutManager: LinearLayoutManager? = null
Define another one say,
private var layoutManager1: LinearLayoutManager? = null
Assign that to your second RecyclerView and run it. It should work.
P.S: Not familiar with Kotlin, in Java we use
recyclerview.setLayoutManager(new LinearLayoutManager(MyActivity.this));