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()
}
......
Related
I have a BluetoothService Class which offers BLE Services like scanning and holding a MutableList of known devices. When scanning it adds new devices to the list and posts them with the postValue() function to notify about the change.
My goal is to bring this data to the frontend and listing the found devices. I tried following the android widgets-sample and almost have the same code as there except for replacing the ViewModelFactory as its deprecated.
I checked my MutableLiveData List in the debugger and they are in fact up to date. I suspect the observer not being registered correctly, as it fires.
My Recycler View looks like this:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/listFoundDevices"
android:layout_width="347dp"
android:layout_height="352dp"
android:layout_marginTop="28dp"
android:background="#CD3131"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/recycler_view_item" />
recycler_view_item:
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="66dp"
android:layout_marginTop="3dp"
android:textColor="#android:color/black"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="#tools:sample/full_names" />
MainActivity shortened:
private val mainActivityViewModel by viewModels<MainActivityViewModel>()
private val bluetoothService = BluetoothService()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val peripheralAdapter = PeripheralAdapter {peripheral -> adapterOnClick(peripheral)}
val recyclerView: RecyclerView = findViewById(R.id.listFoundDevices)
recyclerView.adapter = peripheralAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
setSupportActionBar(binding.toolbar)
mainActivityViewModel.scanLiveData.observe(this) {
it?.let {
//Never fires the message despite updating the liveData with postValue()
println("Got an Update!")
peripheralAdapter.submitList(it as MutableList<MyPeripheral>)
}
}
}
private fun adapterOnClick(peripheral: MyPeripheral){
print("clicked on item")
}
ViewModel:
class MainActivityViewModel (private var bluetoothService: BluetoothService): ViewModel() {
private val repository by lazy { bluetoothService.scanLiveData }
val scanLiveData = MutableLiveData(repository.value)
}
PeripheralAdapter:
class PeripheralAdapter(private val onClick: (MyPeripheral) -> Unit) :
ListAdapter<MyPeripheral, PeripheralAdapter.PeripheralViewHolder>(PeripheralDiffCallback) {
class PeripheralViewHolder(itemView: View, val onClick: (MyPeripheral) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private val peripheralTextView: TextView = itemView.findViewById(R.id.textView)
private var currentPeripheral: MyPeripheral? = null
init {
itemView.setOnClickListener {
currentPeripheral?.let {
onClick(it)
}
}
}
fun bind(peripheral: MyPeripheral) {
currentPeripheral = peripheral
peripheralTextView.text = peripheral.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeripheralViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recycler_view_item, parent, false)
return PeripheralViewHolder(view, onClick)
}
override fun onBindViewHolder(holder: PeripheralViewHolder, position: Int) {
val peripheral = getItem(position)
holder.bind(peripheral)
}
}
BluetoothService:
var knownPeripherals = ArrayList<MyPeripheral>()
var scanLiveData = MutableLiveData(knownPeripherals)
fun handleDevice(result: ScanResult, data: ByteArray?) {
val peripheral = knownPeripherals.find { it.serialNumberAdv == foundSN }
if (peripheral == null){
knownPeripherals.add(MyPeripheral(this, result))
updateMutableData()
}
}
private fun updateMutableData(){
scanLiveData.postValue(knownPeripherals)
}
A hint why it's not working out would be appreciated.
I finally figured it out and I had several flaws in my code I'd like to walk you through.
private val mainActivityViewModel by viewModels<MainActivityViewModel>() only works if the ViewModel has a zero arguments-constructor, which I didn't have. I'm not sure how to handle this as the ViewModelFactory is deprecated. In my case it didn't matter. The ViewModel looks now like this:
class MainActivityViewModel : ViewModel() {
val bluetoothService = BluetoothService()
}
With that, my ViewModel can be instantiated the way I did it in the code above and it registers the observers correctly, which it didn't before. Oddly enough it didn't throw me any error at first, but after trying to call mainActivityViewModel.bluetoothService.liveData.hasActiveObservers()
Secondly, the adapter doesn't work with a mutable list, as it seems like doesn't check for the contents of the list. Instead it just checks if its the same list. Thats the case if you always give your list with changed content into it.
My answer to it is the following in the onCreate of my MainActivity
mainActivityViewModel.scanLiveData.observe(this) {
it?.let {
//now it fires
println("Got an Update!")
peripheralAdapter.submitList(it.toMutableList())
}
}
It now updates correctly.
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'm learning Room with the sample project RoomWordsSample at https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin.
The following code are from the project.
In my mind, the LiveDate will update UI automatically when the data changed if it was observed.
But in the file WordListAdapter.kt, I find notifyDataSetChanged() is added to the function setWords(words: List<Word>), it's seems that it must notify UI manually when data changed.
Why do it still need launch notifyDataSetChanged() when I have used LiveData ?
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
words?.let { adapter.setWords(it) }
})
}
}
WordViewModel.kt
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
WordListAdapter.kt
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Actually, livedata will give you updated data in your activity. But now, it is your activity's job to update the ui. So, whenever live data gives you updated data, you will have to tell the ui to update the data. Hence, notifyDataSetChanged().
notifyDataSetChanged has nothing to do with LiveData, it's part of RecyclerView api.
LiveData - is way of receiving data in lifecycle-aware way, RecyclerView simply displays views.
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
I am implementing a RecyclerView in a fragment. The XML should be correct since I tried it with my hard-coded data, and the API call does return the correct json data from the server according to the Log in the console. The problem is that the RecyclerView adapter does not get any data from my Observable. Here is my implementation
In PostDataService interface I used Retrofit to get an Observable>
interface PostDataService {
#GET(".")
fun getPosts(
#Query(value = "offset") offset: Long = 0,
#Query(value = "limit") limit: Long = 10,
#Query(value = "subscribedOnly") subscribedOnly: Boolean = false
): Observable<List<Post>>
companion object {
val retrofit: PostDataService = Retrofit.Builder()
.baseUrl("http:aws/api/post/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
.create(PostDataService::class.java)
}
}
In PostListRepository, I used RxJava operators to get the LiveData
class PostListRepository {
private val postListLiveData: MutableLiveData<List<Post>> = MutableLiveData()
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
fun getPostListLiveData(): MutableLiveData<List<Post>> {
val postList: MutableList<Post> = ArrayList()
val retrofitInstance = PostDataService.retrofit
val postListObservable = retrofitInstance.getPosts()
compositeDisposable.add(
postListObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMapIterable { it }
.subscribeWith(object : DisposableObserver<Post>() {
override fun onError(e: Throwable) {
// if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(post: Post) {
postList.add(post)
}
override fun onComplete() {
postListLiveData.postValue(postList)
}
})
)
return postListLiveData
}
fun clear() {
compositeDisposable.clear()
}
}
In PostListViewModel, I passed the LiveData from the repository into this ViewModel.
class PostListViewModel : ViewModel() {
private var postListRepository: PostListRepository = PostListRepository()
fun getPostList(): MutableLiveData<List<Post>> {
return postListRepository.getPostListLiveData()
}
fun clear() {
postListRepository.clear()
}
}
Here is the Fragment that contains the RecyclerView. I think the .oberserve function in getPostList() is not called since I tried Log it but got nothing.
class PostListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var swipeLayout: SwipeRefreshLayout
private lateinit var postListViewModel: PostListViewModel
private val postListAdapter = PostRecyclerViewAdapter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.view_post_list, container, false)
recyclerView = rootView.findViewById(R.id.postRecyclerView)
recyclerView.apply {
setHasFixedSize(true)
addItemDecoration(VerticalSpaceItemDecoration(36))
layoutManager = LinearLayoutManager(context)
adapter = postListAdapter
}
postListViewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
getPostList()
swipeLayout = rootView.findViewById(R.id.swipeLayout)
swipeLayout.setColorSchemeResources(R.color.colorPrimary)
swipeLayout.setOnRefreshListener {
getPostList()
swipeLayout.isRefreshing = false
}
return rootView
}
override fun onDestroy() {
super.onDestroy()
postListViewModel.clear() // to avoid memory leak
}
private fun getPostList() {
postListViewModel.getPostList().observe(this, Observer<List<Post>> { resource ->
postListAdapter.setPostList(resource)
postListAdapter.notifyDataSetChanged()
})
}
}
Here is the adapter for the RecyclerView:
class PostRecyclerViewAdapter : RecyclerView.Adapter<PostViewHolder>() {
private var postList: List<Post> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
// create a new view
val postView = PostView(parent.context)
// set the view's size, margins, paddings and layout parameters
return PostViewHolder.from(postView)
}
override fun getItemCount(): Int = postList.size
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val curPost = postList[position]
holder.postView.apply {
setPostOwnerDisplayName(curPost.content.userDisplayedName)
setPostOwnerRole(curPost.content.role)
setPostOwnerAvatar(R.mipmap.ic_launcher_round)
setPostText(curPost.content.text)
setPostImage(curPost.content.smallMediaPaths[0])
setLikeState(curPost.liked)
setBookmarkState(curPost.bookmarked)
}
}
fun setPostList(postList: List<Post>) {
this.postList = postList
}
}
As I mentioned above, I think the .oberserve function in getPostList() in PostListFragment is not called since I tried Log it but got nothing, so there is no data passed into the RecyclerView. Can anyone help me find the reason why it's not being called, or why it's not getting the data from the ViewModel?
I wouldn't think of this is related to your issue, but your code has potential problems.
To move observe part to onActivityCreated would be better to ensure view is created.
when your fragment view is re-created, a new Observer will be added, while previous one still alive, because your Observer is anonymous. So, you have to manage the observers to prevent it.
I just found out that I forgot to catch the exception in RxJava onNext() in case to get the moshi serialization error. After getting that, I got some moshi conversion errors.
Posted it in case anyone carelessly forgot to catch the moshi error.
Thanks!