LiveData always returns LiveData<Object>? - kotlin

I'm trying to put a recyclerview which get its data from room. My function getAllHomework returns LiveData<List<Homework>>, but when I tried to set the return value to the recyclerview adapter, it will always return this error
Type Mismatch.
Required: List<Homework>
Found: List<Homework>?
Here's my HomeworkViewModel class which has the function getAllHomework looks like:
class HomeworkViewModel : ViewModel() {
private var matrixNumber: String? = null
private var schoolID: Int = 0
lateinit var listAllHomework: LiveData<List<Homework>>
lateinit var homeworkRepository: HomeworkRepository
fun init(params: Map<String, String>) {
schoolID = Integer.parseInt(params["schoolID"])
homeworkRepository = HomeworkRepository()
listAllHomework = homeworkRepository.getAllHomework(1, "2018")
}
fun getAllHomework(): LiveData<List<Homework>>{
return listAllHomework
}
}
And below is the part in my Homework activity that tries to set the value into recyclerview adapter but will always return the type mismatch error.
class Homework : AppCompatActivity(), LifecycleOwner {
lateinit var linearLayoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.homework)
linearLayoutManager = LinearLayoutManager(this)
rvHomeworks.layoutManager = linearLayoutManager
var adapter = AdapterHomework(this)
rvHomeworks.adapter = adapter
var homeworkViewModel = ViewModelProviders.of(this).get(HomeworkViewModel::class.java)
homeworkViewModel.init(params)
homeworkViewModel.getAllHomework().observe(this, Observer {
allHomework -> adapter.setHomeworkList(allHomework)
})
}
}
The line allHomework -> adapter.setHomeworkList(allHomework) above will show the Type Mismatch error I mentioned above.
Here's how my AdapterHomework looks like:
class AdapterHomework(context: Context): RecyclerView.Adapter<AdapterHomework.ViewHolder>() {
lateinit var homeworkList: List<Homework>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder{
val v = LayoutInflater.from(parent.context).inflate(R.layout.rv_homeworks, parent, false)
return ViewHolder(v);
}
#JvmName("functionToSetTheHomeworkList")
fun setHomeworkList(myHomeworkList: List<Homework>){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
I could not find where in my code did I ever return List<Homework>? instead of List<Homework>.

This has actually nothing to do with Room, but with how LiveData was designed - specifically Observer class:
public interface Observer<T> {
void onChanged(#Nullable T t);
}
as you can see T (in your case List<Homework>) is marked as #Nullable therefore you will always get it's Kotlin equivalent List<Homework>? as a parameter of onChanged in your Observer implementation.
I would recommend changing setHomeworkList to something like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
if(myHomeworkList != null){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
You can also use let function for that like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
myHomeworkList?.let {
homeworkList = it
notifyDataSetChanged()
}
}

Related

Getting list from viewmodel in observe event -MVVM

I have a issue in getting a list returned in observe event in my activity. i am developing a login screen in MVVM. viewmodel is as follows.
my problem is i can get returned data in observe call back into a UI control. but same data returned assign into a list variable is empty. in other words, list returned unable to pass into a a list variable in an activity.
class LoginViewModel #Inject internal constructor (private val loginRepository: LoginRepository,private val usersRepository: UsersRepository): ViewModel() {
private var _userEmail:MutableLiveData<String>
private var _userPassword:MutableLiveData<String>
private var _userLoginData:MutableLiveData<UserLoginData>
private var allUsers:MutableLiveData<List<Users>>
private var findUser:MutableLiveData<List<Users>>
init{
_userEmail= MutableLiveData()
_userPassword= MutableLiveData()
_userLoginData= MutableLiveData()
allUsers= MutableLiveData()
findUser= MutableLiveData()
}
fun getEmail():LiveData<String>{
return _userEmail
}
fun getPassword():MutableLiveData<String>{
return _userPassword
}
fun userLogin(userEmail:String,userPassword:String):MutableLiveData<UserLoginData>{
_userEmail.postValue(userEmail)
_userPassword.postValue(userPassword)
viewModelScope.launch(Dispatchers.IO) {
var userlogindata:UserLoginData=loginRepository.userLogin(userEmail,userPassword)
_userLoginData.postValue(userlogindata)
}
return _userLoginData
}
fun getAllUsers():MutableLiveData<List<Users>>{
//lateinit var _allUsers:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _allUsers:List<Users> =usersRepository.getUsers()
allUsers.postValue(_allUsers)
}
return allUsers
}
fun findUser(userEmail:String):MutableLiveData<List<Users>>{
//lateinit var finduser:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _findUser:List<Users> =usersRepository.findUser(userEmail)
findUser.postValue(_findUser)
}
return findUser
}
}
in an activity i am observing the users list and getting the list into a list variable in the activity. code in the activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var loginViewModel: LoginViewModel
lateinit var loginData:UserLoginData
var users:List<Users> = emptyList()
var findUser:List<Users> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
/*observe users list*/
loginViewModel.getAllUsers().observe(this, {It->
users=It
binding.textView.text=It[0].email.toString()
})
loginViewModel.findUser(binding.loginEditTextTextEmailAddressTxt.toString().trim()).observe(this,{it->
findUser=it
})
This program failed if i use data in the users or findUser lists.
Kindly help me to find the best practice in getting the changed data from viewmodel into an activity
ViewModel:
data class User(
var name: String
)
private val _allUsers = MutableLiveData<List<User>>()
private val allUsers: LiveData<List<User>> get() = _allUsers
fun fetchAllUsers(): LiveData<List<User>> {
viewModelScope.launch {
//delay is simulating network request delay
delay(1000)
//listOf is simulating usersRepository.getUsers()
_allUsers.value = listOf(User("name1"), User("name2"), User("name3"))
}
return allUsers
}
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchAllUsers().observe(viewLifecycleOwner) { userList ->
userList.forEach {
Log.d("user", it.name)
}
}
You can try this way but I do not prefer returning liveData with function because you have to observe liveData once. You need to be careful observe once.

Problems using VIewHolder fun and variable

I'm having trouble binding my ViewHolder, and I've got two warning that I believe are related. I am trying to use Hilt to create a clickable ViewHolder, so in my SessionAdapter I am using an inner class to bind my SessionViewHolder to my RecyclerView.
First, I am struggling to understand what to return for the inner class SessionViewHolder fun bind(session: Session) { ...}. Android Studio is telling me function "bind" is never used, but I thought I used it in my onBindViewHolder?
Secondly, in my override onBindViewHolder I don't understand how I should use val session?
#AndroidEntryPoint
class SessionFragment : Fragment() {
var adapter: SessionAdapter = SessionAdapter()
private val sessionAdapter = SessionListAdapter(this::onSessionClicked)
private fun onSessionClicked(session: Session): Session {
return(session)
}
class SessionAdapter {
fun setOnClickListener() {
return(addSessionToItinerary())
}
private fun addSessionToItinerary() {
return addSessionToItinerary()
}
}
class SessionListAdapter(
private val onSessionCLicked: (Session) -> Unit,
) : ListAdapter<Session, SessionListAdapter.SessionViewHolder>(SessionItemCallback) {
inner class SessionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind() {
val textView = itemView.findViewById<TextView>(0)
fun bind(session: Session) {
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
return#setOnClickListener
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val itemView = layoutInflater.inflate(R.layout.fragment_session_list, parent, false)
return SessionViewHolder(itemView)
}
override fun onBindViewHolder(holder: SessionViewHolder, position: Int) {
val session = getItem(position)
holder.bind()
}
}
Thank you in advance for you help. I have gotten myself confused with the recurrence of bind and session throughout my adapter.
See here in this code, you have defined two bind functions, but one is nested inside the other so it is unusable:
fun bind() {
val textView = itemView.findViewById<TextView>(0)
fun bind(session: Session) {
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
return#setOnClickListener
}
}
}
The outer bind() function is the one you are calling, and it doesn't make sense to bind nothing. This function gets a reference to a TextView, and it creates a function that is never used.
Another problem is that you are passing 0 to findViewById. There is never going to be a view with an ID of 0. You need to pass R.id.whateverYourTextViewIsNamedInYourXml.
Side note, return#onClickListener is unnecessary. If a function doesn't return anything, putting a return statement on the last line doesn't do anything.
To make it work, you should replace the above code with something like this, but replace the name of the text view with whatever ID you assigned it in your XML:
fun bind(session: Session) {
val textView = itemView.findViewById<TextView>(R.id.myTextView)
textView.text = session.title
itemView.setOnClickListener {
onSessionCLicked(session)
}
}
and then pass the session to this function when you call it.
Side note, your SessionAdapter class doesn't make any sense at all, but you're not using it for anything anyway. I would delete that.

RecyclerView in Fragment not populating

So I've been wrestling with this for days and I need some help. I've made this code work in an activity, but then I move it to a fragment it doesn't work. Everything else is the same between the two.
Using the debugger with the working Activity, the line
apiService = retrofit.create<HomeJsonApiService>(HomeJsonApiService::class.java)
goes to getItemCount(). However in the fragment it goes directly to onCreateView in the Fragment. I've attached my code below. Thanks in advance for the help! And be gentle. I'm still new to this :)
First is my fragment:
class TabHomeActivity : Fragment() {
val itemList = ArrayList<HomeCards>()
lateinit var adapter: HomeCardsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var binding = FragmentTabHomeActivityBinding.inflate(layoutInflater)
adapter = HomeCardsAdapter()
var rv = binding.rvHomeCards
rv.adapter = adapter
loadData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.cards_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
private fun loadData() {
ApiManager.getInstance().service.listHeroes()
.enqueue(object : Callback<ResponseData<List<HomeCards>>> {
override fun onResponse(
call: Call<ResponseData<List<HomeCards>>>,
response: Response<ResponseData<List<HomeCards>>>
) {
val listData: List<HomeCards> = response.body()!!.data
// updating data from network to adapter
itemList.clear()
itemList.addAll(listData)
adapter.updateData(itemList)
adapter.notifyDataSetChanged()
}
override fun onFailure(call: Call<ResponseData<List<HomeCards>>>, t: Throwable) {
}
})
}
}
The HTTP request:
data class ResponseData<T> (
val code: Int,
val data: T
)
interface HomeJsonApiService {
#GET("marvel-heroes.asp?h=2")
fun listHeroes(): retrofit2.Call<ResponseData<List<HomeCards>>>
}
class ApiManager {
private var apiService: HomeJsonApiService? = null
init {
createService()
}
val service: HomeJsonApiService get() = apiService!!
private fun createService() {
val loggingInterceptor =
HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.i("Retrofit", message)
}
})
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://www.mywebsite.com/jsonfolder/JSON/")
.addConverterFactory(GsonConverterFactory.create())
.build()
apiService = retrofit.create(HomeJsonApiService::class.java)
}
companion object {
private var instance: ApiManager? = null
fun getInstance(): ApiManager {
return instance ?: synchronized(this) {
ApiManager().also { instance = it }
}
}
}
}
And my adapter:
class HomeCardsAdapter() : RecyclerView.Adapter<HomeCardsAdapter.ViewHolder>() {
private lateinit var itemList: List<HomeCards>
lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
context = parent.context
val view = LayoutInflater.from(context).inflate(R.layout.cards_home, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return if (::itemList.isInitialized) itemList.size else 0
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind()
}
fun updateData(list: List<HomeCards>) {
itemList = list;
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
//var binding = ActivityMainBinding(layoutInflater(inf))
fun bind() {
val item = itemList.get(adapterPosition)
ViewHolder(itemView).itemView.findViewById<TextView>(R.id.cardHomeTitle).text = item.name
ViewHolder(itemView).itemView.findViewById<TextView>(R.id.cardHomeTitle).text = item.superheroName
Glide.with(context)
.load(item.photo)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.into(ViewHolder(itemView).itemView.findViewById<ImageView>(R.id.cardHomeIcon))
}
}
}
class HomeCards {
#SerializedName("superhero_name")
var superheroName: String = ""
var name: String = ""
var photo: String = ""
}
The main problem is:
var binding = FragmentTabHomeActivityBinding.inflate(layoutInflater)
That is inside on onCreate but onCreateView is returning another view inflater.inflate(R.layout.cards_home, container, false)
So you are applying the adapter to a recycler that is on the binding, but the view on the screen is inflated from the layout. Change it to this:
private lateinit var binding: FragmentTabHomeActivityBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
binding = FragmentTabHomeActivityBinding.inflate(layoutInflater, container, false)
return binding.root
}
And move the code from from onCreate to onViewCreated but make sure to use the lateinit binding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = HomeCardsAdapter()
var rv = binding.rvHomeCards
rv.adapter = adapter
loadData()
}
After that there is a problem in your adapter: private lateinit var itemList: List<HomeCards> very specifically List<HomeCards>. The method notifyDataSetChanged doesn't work by changing or updating the reference of the data structure but when the collection is modified. Change it to this:
private val list = mutableListOf<HomeCards>()
override fun getItemCount(): Int {
return list.size()
}
fun updateData(list: List<HomeCards>) {
this.itemList.clear()
this.itemList.addAll(list)
notifyDataSetChanged()
}
If onResponse() gets called and provides response, verify that code updating UI is running on main/ui thread. Common source of issue when working with network (other threads).
activity?.runOnUiThread {
itemList.clear()
itemList.addAll(listData)
adapter.updateData(itemList)
adapter.notifyDataSetChanged()
}

this weird run time exception keep coming up

This is the runtime exception: "lateinit property name Session has not been initialized" that keeps coming up.
class GalasatyActivity : BaseActivity() {
private lateinit var binding: ActivityGalasatyBinding
private val list = ArrayList<Post>()
private lateinit var readingTypes: String
private lateinit var student :String
private lateinit var statue :String
private var sessionType by Delegates.notNull<Int>()
private lateinit var nameSession:String
private lateinit var mSharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGalasatyBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpActionBar()
setupGalasatyList()
mSharedPreferences = getSharedPreferences(Constants.PREFERENCE_NAME, Context.MODE_PRIVATE)
//nameSession = findViewById<TextView>(R.id.tv_galsa).toString()
readingTypes = findViewById<TextView>(R.id.tv_reading_type).toString()
student = findViewById<TextView>(R.id.tv_student).toString()
statue = findViewById<TextView>(R.id.tv_statue).toString()
sessionType = findViewById<TextView>(R.id.tv_session_type).toString().toInt()
nameSession = findViewById<TextView>(R.id.tv_galsa).toString()
}
private fun setUpActionBar(){
setSupportActionBar(binding.toolBarGalasaty)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolBarGalasaty.setNavigationOnClickListener {
doubleBackToExit()
}
}
private fun setupGalasatyList(){
hideProgressBar()
//getGalasatDetails(name,readingTypes,student,statue,sessionType)
DataRetrofit.instance.getSessions(nameSession,readingTypes,student,statue,sessionType).enqueue(object :Callback<Post?>{
override fun onResponse(call: Call<Post?>, response: Response<Post?>) {
binding.rvGalasatyList.layoutManager = LinearLayoutManager(this#GalasatyActivity,LinearLayoutManager.VERTICAL,false)
binding.rvGalasatyList.setHasFixedSize(true)
val galsaAdapter = GalasatyItemListAdapter(this#GalasatyActivity, list)
binding.rvGalasatyList.adapter = galsaAdapter
galsaAdapter.setOnClickListener(object : GalasatyItemListAdapter.OnClickListener{
override fun onClick(position: Int, model: Post) {
startActivity(Intent(this#GalasatyActivity,GalsaActivity::class.java))
}
})
}
override fun onFailure(call: Call<Post?>, t: Throwable) {
Toast.makeText(this#GalasatyActivity, t.message, Toast.LENGTH_SHORT).show()
}
})
}
private fun getGalasatDetails(name:String, readingTypes: String, student:String, statement:String, sessionType:Int){
if (Constants.isInternetAvailable(this)){
val retrofit: retrofit2.Retrofit = retrofit2.Retrofit.Builder().baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create()).build()
val service: DataAPI = retrofit.create(DataAPI::class.java)
val listCall: Call<Post> = service.getSessions(name,readingTypes,student,statement,sessionType)
showProgressDialog(resources.getString(R.string.please_wait))
listCall.enqueue(object :Callback<Post?> {
#RequiresApi(Build.VERSION_CODES.N)
override fun onResponse(call: Call<Post?>, response: Response<Post?>) {
if (response.isSuccessful){
hideProgressBar()
val galasatList: Post? = response.body()
val sessionResponseJsonString = Gson().toJson(galasatList)
val editor = mSharedPreferences.edit()
editor.putString(Constants.SESSIONS_RESPONSE_DATA, sessionResponseJsonString)
editor.apply()
setupUI()
Log.i("Response result","$galasatList")
}else{
when(response.code()){
400->{
Log.e("Error 400","Bad connection")
}404->{
Log.e("Error 404","Not found")
}else->{
Log.e("Error","Generic error")
}
}
}
}
override fun onFailure(call: Call<Post?>, t: Throwable) {
hideProgressBar()
Log.e("erorrr", t.message.toString())
}
})
}else{
Toast.makeText(this,"No internet connection", Toast.LENGTH_SHORT).show()
}
}
private fun setupUI(){
val galasatResponseJsonString = mSharedPreferences.getString(Constants.SESSIONS_RESPONSE_DATA,"")
if (!galasatResponseJsonString.isNullOrEmpty()) {
val galasatList = Gson().fromJson(galasatResponseJsonString, Post::class.java)
for (i in Post.toString()) {
readingTypes = galasatList.readingType
student = galasatList.student
statue = galasatList.student
nameSession = galasatList.name
}
}
}
}
You're initializing a view into a string which is outrightly wrong.
See, here you've declared a variable as lateinit var nameSession: String and then you're initializing it with a view as nameSession = findViewById<TextView>(R.id.tv_galsa).toString() , that too as String which actually doesn't make sense.
Second, the error is about nameSession which as I said earlier is used first in the line no. 64 - DataRetrofit.instance.getSessions(nameSession,..., so the code breaks at this very point and you think that it will work with others, just not this which is again not the case because your code is wrong.
See, There are two cases, Either you want to pass the text value of the TextView nameSession and others, or the view in the getSessions().
To passs the view, declare it as lateinit var nameSession: TextView, initialize it as nameSession = findViewById<TextView>(R.id.tv_galsa) and pass it as DataRetrofit.instance.getSessions(nameSession,...).
To pass the text value of the TextView, declare and initialize it exactly as my first point, just pass it as DataRetrofit.instance.getSessions(nameSession.text,...).
Do the 2nd point for others as well. You DON'T convert the view to String, you convert the value of the view to String, and to get the value, you use the .text property of the TextView as I've used in the 2nd point.(setText() and getText() in Java).
So, convert all your variables as Views, and then pass their value using the .text property. One more of doing it will be nameSession = binding.tv_galsa.text.

Why does private lateinit var mCustomAdapter CustomAdapter cause Property getter or setter expected in Kotlin?

The code mRecyclerView.adapter= CustomAdapter(allList) works well, I hope to define a private var mCustomAdapter, and assign value late.
But the code private lateinit var mCustomAdapter CustomAdapter cause error, how can I fixed it? Thanks!
Code A
class UIMain : AppCompatActivity() {
private lateinit var mCustomAdapter CustomAdapter //Error
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
...
mRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
mRecyclerView.adapter= CustomAdapter(allList) //OK
}
Code B
class CustomAdapter (val backupItemList: List<MSetting>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private var mSelectedItem = -1
//this method is returning the view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_recyclerview, parent, false)
return ViewHolder(v)
}
fun getSelectedItem():Int{
return mSelectedItem
}
//this method is binding the data on the list
override fun onBindViewHolder(holder: CustomAdapter.ViewHolder, position: Int) {
holder.bindItems(backupItemList[position])
holder.itemView.radioButton.setChecked(position == mSelectedItem);
}
//this method is giving the size of the list
override fun getItemCount(): Int {
return backupItemList.size
}
//the class is hodling the list view
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(aMSetting: MSetting) {
//itemView.radioButton.isChecked=false
itemView.radioButton.tag=aMSetting._id
itemView.textViewUsername.text=aMSetting.createdDate.toString()
itemView.textViewAddress.text=aMSetting.description
itemView.radioButton.setOnClickListener {
mSelectedItem=getAdapterPosition()
notifyDataSetChanged();
}
}
}
}
You are missing : at the end of mCustomAdapter variable
Try this:
private lateinit var mCustomAdapter: CustomAdapter
See more: https://kotlinlang.org/docs/reference/basic-syntax.html#defining-variables