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.
Related
I'm trying to figure out how Data Binding works with RecyclerView.
I've got Sound objects that have names. I bind these objects to ViewHolders and somehow, these names are displayed on the items of my RecyclerView list.
Here is the code (not mine, I took it from a book)
Layout File:
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="ru.vsevolod.zimin.beatbox.SoundViewModel"/>
</data>
<Button
android:layout_height="120dp"
android:text="#{viewModel.title}"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_width="match_parent"
tools:text="Sound Button"/>
</layout>
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var beatBox: BeatBox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
beatBox = BeatBox(assets)
beatBox.loadSounds()
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.recyclerViewie.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = SoundAdapter()
}
}
private inner class SoundHolder (val binding: ListItemSoundBinding):
RecyclerView.ViewHolder(binding.root) {
init {
binding.viewModel = SoundViewModel()
}
fun bind(sound: Sound) {
binding.viewModel?.sound = sound
}
}
private inner class SoundAdapter: RecyclerView.Adapter<SoundHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoundHolder {
val binding = DataBindingUtil.inflate<ListItemSoundBinding> (layoutInflater,
R.layout.list_item_sound, parent, false)
return SoundHolder(binding)
}
override fun onBindViewHolder(holder: SoundHolder, position: Int) {
val sound = beatBox.sounds[position]
holder.bind(sound)
}
override fun getItemCount(): Int {
return beatBox.sounds.size
}
}
}
BeatBox (creates and contains the list of Sound objects that are going to be bound to the ViewHolders):
private const val SOUNDS_FOLDER = "sample_sounds"
class BeatBox(private val assets: AssetManager) {
val sounds: List<Sound>
init {
sounds = loadSounds()
}
fun loadSounds(): List<Sound> {
val soundNames: Array<String>
try {
soundNames = assets.list(SOUNDS_FOLDER)!!
}catch(e: Exception){
Log.e(TAG, "Couldn't list assets",e)
return emptyList()
}
val sounds = mutableListOf<Sound>()
soundNames.forEach {
val sound = Sound(it)
sounds.add(sound)
}
return sounds
}
}
Sound (class for Sound objects):
class Sound(val name: String)
SoundViewModel:
class SoundViewModel: BaseObservable() {
var sound: Sound? = null
set(sound) {
field = sound
notifyChange()
}
#get : Bindable
var title: String? = null
get() = sound?.name
}
What I fail to understand is how exactly the titles of the Sound objects are hooked up to the ViewHolders. I also noticed that when I remove the getter from SoundViewModel like that:
Before:
#get : Bindable
var title: String? = null
get() = sound?.name
After:
#get : Bindable
var title = sound?.name
...the titles are no longer bound and I end up with a nameless list.
Could you please explain how this happens?
Thank you in advance!
First of all sorry for my bad English.
I'm trying to receive Data from my self-written Python Backend(REST-API) in my Android APP.
ApiService.kt:
private const val Base_URL = "http://192.168.178.93:5000/api/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(Base_URL)
.build()
interface TodoApiService{
#GET("todo")
suspend fun getToDo(): List<ToDo>
}
object ToDoApi{
val retrofitService : TodoApiService by lazy {
retrofit.create(TodoApiService::class.java)
}
}
MainActivityViewModel.kt:
class MainActivityViewModel : ViewModel() {
of the most recent request
private val _status = MutableLiveData<String>()
val status: LiveData<String> = _status
private val _toDo = MutableLiveData<List<ToDo>>()
val toDo: LiveData<List<ToDo>> = _toDo
init {
getToDo()
}
private fun getToDo() {
viewModelScope.launch{
try {
_toDo.value = ToDoApi.retrofitService.getToDo()
_status.value = "Success $_toDo"
}catch (e: Exception){
_status.value = "Failure ${e.message}"
}
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<androidx.recyclerview.widget.RecyclerView
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/recycler_view"
tools:listitem="#layout/todo_item"/>
</FrameLayout>
todo_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
android:id="#+id/text_view_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
TodoAdapter.kt
class TodoAdapter(private val context: Context, private val Items: List<ToDo>):RecyclerView. Adapter<TodoAdapter.TodoViewHolder>(){
class TodoViewHolder(private val view: View) : RecyclerView.ViewHolder(view){
val textView: TextView = view.findViewById(R.id.text_view_name)
}
override fun getItemCount() = Items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val adapterLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.todo_item, parent, false)
return TodoViewHolder(adapterLayout)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val ToDo = Items.get(position)
holder.textView.text = context.resources.getString(ToDo.Id.toInt())
}
}
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private val viewModel = MainActivityViewModel()
private lateinit var binding: ActivityMainBinding
private lateinit var linearLayoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
linearLayoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = linearLayoutManager
binding.recyclerView.adapter = TodoAdapter(this, viewModel.toDo.value)
}
}
I know that I need an adapter to connect my LiveData to my recyclerView. But I'm not able to implement it right. Android studio tells me I cant use my MutableLiveData<List> for my Adapter that just only needs a normal List(Required: List Found: List?. I cant Cast cause the data could be null.
Your use of LiveData is incorrect. You use it as a simple variable, passing its current value (which is null) to the adapter.
LiveData is intended for data stream and need to be observed.
I like to add a setter for the data on the Adapter class:
fun setData(data: List<ToDo>) {
Items = data
notifyDataSetChanged()
}
And in the Activity, observe the ViewModel's live data and update the adapter when new data arrives:
class MainActivity : AppCompatActivity() {
private lateinit var adapter: TodoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = TodoAdapter(this)
viewModel.todo.observe(this, { todos ->
adapter.setData(todos)
}
}
...
}
Now when you set value to your LiveData in the ViewModel, the adapter will be notified.
This is because you are sending a possible nullable list from the activity to the Adapter. You must do something like this:
binding.recyclerView.adapter = TodoAdapter(this, viewModel.toDo?.value ?: listOf())
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()
}
......
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.
The binding adapter has lost binding to the view model. However I have no idea what is the reason. The SpinnerTextView in the code is a textview popping an alert dialog for selecting value from a list. Setting the title will set The textview's text as the String value. The binding lost cause the textview does not show the new value, is there any solution?
I have put some breakpoints, and I found that the pickedQuantity = "0" worked and also pickedQuantity.value = quantities.value!![index] has been run too. However, in the BindingAdapter.kt only pickedQuantity = "0" triggered the setTitle function.
Therefore, my TextView will always shows 0 but not changing when I select value.
BindingAdapter.kt
#BindingAdapter("spinnerTitle")
fun<T> SpinnerTextView<T>.setTitle(str: String) {
title = str
}
#BindingAdapter("spinnerAlertTitle")
fun<T> SpinnerTextView<T>.setAlertTitle(str: String) {
alertTitle = str
}
#BindingAdapter("spinnerItems")
fun<T> SpinnerTextView<T>.setItems(list: List<T>) {
items = list
}
#BindingAdapter("spinnerItemHandler")
fun<T> SpinnerTextView<T>.setHandler(handler: (Int) -> Unit) {
valueChanged = handler
}
TicketTypeViewModel.kt
class TicketTypeViewModel : BaseViewModel() {
val ticketId = MutableLiveData<Int>()
val ticketName = MutableLiveData<String>()
val ticketPrice = MutableLiveData<String>()
val pickedQuantity = MutableLiveData<String>()
val quantities = MutableLiveData<List<String>>()
val spinnerTitle = MutableLiveData<String>()
val spinnerHandler = MutableLiveData<(Int) -> Unit>()
fun bind(ticket: TicketType, onClick: (Int) -> Unit) {
ticketId.value = ticket.ticketTypeId
ticketName.value = ticket.ticketTypeName
ticketPrice.value = "$" + ticket.price
pickedQuantity.value = "0"
spinnerTitle.value = ""
val temp = mutableListOf<String>()
for (i in 0 until ticket.quota) {
temp.add(i.toString())
}
quantities.value = temp.toList()
spinnerHandler.value = { index ->
pickedQuantity.value = quantities.value!![index]
onClick(index)
}
}
}
TicketTypeAdapter.kt
class TicketTypeAdapter(val onClick: (Int) -> Unit): RecyclerView.Adapter<TicketTypeAdapter.ViewHolder>() {
private var ticketList: MutableList<TicketType> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TicketTypeAdapter.ViewHolder {
val binding: EventTicketTypeListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.event_ticket_type_list_item, parent, false)
return ViewHolder(binding, onClick)
}
override fun onBindViewHolder(holder: TicketTypeAdapter.ViewHolder, position: Int) {
holder.bind(ticketList[position])
}
override fun getItemCount(): Int {
return ticketList.size
}
fun refreshTicketList(ticketList: List<TicketType>){
this.ticketList.clear()
this.ticketList.addAll(ticketList)
notifyDataSetChanged()
}
class ViewHolder(private val binding: EventTicketTypeListItemBinding, val onClick: (Int) -> Unit): RecyclerView.ViewHolder(binding.root){
private val viewModel = TicketTypeViewModel()
fun bind(ticket: TicketType){
viewModel.bind(ticket, onClick)
binding.ticketType = viewModel
}
}
}
In .xml
<com.cityline.component.SpinnerTextView
android:id="#+id/spinner_quantity"
spinnerAlertTitle="#{ticketType.spinnerTitle}"
spinnerItemHandler="#{ticketType.spinnerHandler}"
spinnerItems="#{ticketType.quantities}"
spinnerTitle="#{ticketType.pickedQuantity}"
android:layout_width="20dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="20dp"
android:background="#drawable/edit_text_bg_bottom_line"
android:gravity="center"
android:textSize="16sp" />
You're missing a call to binding.setLifecycleOwner(this)
Sets the LifecycleOwner that should be used for observing changes of
LiveData in this binding. If a LiveData is in one of the binding expressions
and no LifecycleOwner is set, the LiveData will not be observed and updates to it
will not be propagated to the UI.
So either set the lifecycle owner or use ObservableField instead, which is better fitting.
As adapters work differently in regards of data updates, you might want to propagate changes to the adapter data set instead and call notifyDataSetChanged() or a similar one to update the bindings.