I am getting started for using Mvvm architecture, but I am facing problem with my adapter -> It never called
I have an adapter here :
class LocationResultAdapter: RecyclerView.Adapter<LocationResultAdapter.ViewHolder>() {
private lateinit var resultList:List<ResultSearchBean>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: ItemPostBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.item_post, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(resultList[position])
}
override fun getItemCount(): Int {
return if (resultList.isNotEmpty()) {
resultList.size
} else {
0
}
}
fun updateList(list: List<ResultSearchBean>) {
resultList = list
notifyDataSetChanged()
}
class ViewHolder(private val binding: ItemPostBinding):RecyclerView.ViewHolder(binding.root){
private val viewModel = LocationViewModel()
fun bind(resultSearchBean: ResultSearchBean){
viewModel.bind(resultSearchBean)
binding.viewModel = viewModel
}
}
}
this adapter is called in my ViewModel
class LocationResultViewModel(private val resultSearchDao: ResultSearchDao): BaseViewModel() {
#Inject
lateinit var mFi9Api: Fi9Api
private lateinit var mSubscription: Disposable
var mSearchText: String = ""
val adapter: LocationResultAdapter = LocationResultAdapter()
override fun onCleared() {
super.onCleared()
mSubscription.dispose()
}
fun search(searchText: String) {
resultLocations(searchText)
}
private fun resultLocations(searchText: String) {
mSearchText = searchText
mSubscription = mFi9Api.getLocation(mSearchText)
.flatMap { resource ->
return#flatMap Observable.fromCallable {
resultSearchDao.deleteAll()
resultSearchDao.insertAll(resource.distinctBy { r -> r.label })
}
}
.concatMap {
return#concatMap resultSearchDao.all()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(GetLocationListSubscriber())
}
inner class GetLocationListSubscriber : ResourceObserver<List<ResultSearchBean>>() {
override fun onNext(#NonNull t: List<ResultSearchBean>) {
adapter.updateList(t)
}
override fun onError(#NonNull e: Throwable) {
}
override fun onComplete() {
// Nothing to do
}
}
In my activity I have this following code :
class LocationResultActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: LocationResultViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.resultList.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
viewModel = ViewModelProviders.of(this, ViewModelFactory(this)).get(LocationResultViewModel::class.java)
binding.viewModel = viewModel
search_btn.setOnClickListener {
viewModel.search(search_et.text.toString())
}
}
}
Only the method: adapter.updateList(t) is called.
I put some break point on onCreateViewHolder, OnBindViewHolder, getItemCount but it never called
What did I do that does not work?
Thank you for your help
I think you need to assign your adapter to your RecyclerView.
Try this:
binding.resultList.adapter = viewModel.adapter
Related
I have an editText and a button. When I click on the button the data from the edittext gets inserted into the database. On the save activity I have recyclerView which will show the items getting from the viewModel. I have put the observer to observer the data but it is not showing any item in the recuclerView.
class MainActivity : AppCompatActivity(), NoteAdapter.OnItemClicked {
private lateinit var recyclerView: RecyclerView
private lateinit var btnAdd: Button
private lateinit var etNote: EditText
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recyclerView)
btnAdd = findViewById(R.id.btn_add)
etNote = findViewById(R.id.et_note)
val mAdapter = NoteAdapter(this)
recyclerView.apply {
adapter = mAdapter
layoutManager = LinearLayoutManager(this#MainActivity)
}
viewModel = ViewModelProvider(this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application))[MyViewModel::class.java]
viewModel.allNotes.observe(this, Observer {note->
note?.let {
mAdapter.submitList(it)
}
})
btnAdd.setOnClickListener {
addNote()
}
}
override fun onClick(note: Notes) {
viewModel.delete(note)
}
private fun addNote(){
val note: String = etNote.text.toString()
if(note.isNotEmpty()) {
viewModel.insert(Notes(note))
Toast.makeText(this, "$note inserted", Toast.LENGTH_SHORT).show()
}
}
}
Adapter Class
class NoteAdapter(private val clickListener: OnItemClicked)
: ListAdapter<Notes, NoteAdapter.MyViewHolder>(NoteComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.note_item, parent, false)
val viewHolder = MyViewHolder(view)
viewHolder.ivDelete.setOnClickListener{
clickListener.onClick(getItem(viewHolder.adapterPosition))
}
return viewHolder
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentNote = getItem(position)
holder.apply {
tvNoteItem.text = currentNote.note
}
}
interface OnItemClicked{
fun onClick(note: Notes)
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvNoteItem: TextView = itemView.findViewById(R.id.tv_note)
val ivDelete: ImageView = itemView.findViewById(R.id.iv_delete)
}
class NoteComparator : DiffUtil.ItemCallback<Notes>(){
override fun areItemsTheSame(oldItem: Notes, newItem: Notes): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Notes, newItem: Notes): Boolean {
return oldItem == newItem
}
}
}
ViewModel Class
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NotesRepository
val allNotes : LiveData<List<Notes>>
init {
val dao = NoteDatabase.getDatabase(application).noteDao()
repository = NotesRepository(dao)
allNotes = repository.noteList
}
fun delete(note: Notes) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(note)
}
fun insert(note: Notes) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(note)
}
}
Heey Divyansh,
Analyzing your code I believe the problem is in your ViewModel. You are trying to change the property allNotes, which is a LiveData. You need mutable live data, so declare it like private because we can't expose a mutable property. And your allNotes property will receive the changes from this mutable property, like this:
class MyViewModel: ViewModel() {
private val _allNotes = MutableLiveData<List<Notes>>(emptyList())
val allNotes: LiveData<List<Notes>>
get() = _allNotes
}
Then, update the mutable live data:
class MyViewModel: ViewModel() {
// ...
init {
val dao = NoteDatabase.getDatabase(application).noteDao()
repository = NotesRepository(dao)
_allNotes.value = repository.noteList
}
}
I have found why it is not displaying the items because the viewModel is not inserting the the data as I have used Dispatcher.IO in the viewModel scope.
I removed it and now it is inserting and showing the items.
I am trying to use an intent in an onClick method,the intent does work...but it doesn't want to change to the other activity
im looking everywhere but nothing seems wrong.
note:i dont know why FragmentPagerAdapter is deprecated
code:
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = UserAdapter()
adapter.notifyDataSetChanged()
adapter.setOnItemClickCallback(object : UserAdapter.OnItemClickCallback{
override fun onItemClicked(data: User) {
Intent(this#MainActivity, DetailUserActivity::class.java).also{
intent.putExtra(DetailUserActivity.EXTRA_USERNAME, data.login)
startActivity(intent)
}
}
})
viewModel= ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[MainViewModel::class.java]
binding.apply {
rvUser.layoutManager = LinearLayoutManager ( this#MainActivity)
rvUser.setHasFixedSize(true)
rvUser.adapter = adapter
btnSearch.setOnClickListener{
searchUser()
}
etQuery.setOnKeyListener { _, KeyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && KeyCode == KeyEvent.KEYCODE_ENTER){
searchUser()
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
viewModel.getSearchUsers().observe(this) {
if (it != null) {
adapter.setList(it)
showLoading(false)
}
}
}
private fun searchUser(){
binding.apply {
val query = etQuery.text.toString()
if (query.isEmpty()) return
showLoading(true)
viewModel.setSearchUsers(query)
}
}
private fun showLoading(state: Boolean){
if (state){
binding.progressBar.visibility = View.VISIBLE
}else{
binding.progressBar.visibility = View.GONE
}
}
}
UserAdapter.kt
class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
private val list = ArrayList<User>()
private var onItemClickCallback: OnItemClickCallback? = null
fun setOnItemClickCallback (onItemClickCallback: OnItemClickCallback){
this.onItemClickCallback = onItemClickCallback
}
fun setList(users: ArrayList<User>){
list.clear()
list.addAll(users)
notifyDataSetChanged()
}
inner class UserViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(user: User){
binding.root.setOnClickListener{
onItemClickCallback?.onItemClicked(list[adapterPosition])
}
binding.apply {
Glide.with(itemView)
.load(user.avatar_url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.into(tvUser)
tvUsername.text = user.login
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return UserViewHolder((view))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(list[position])
}
override fun getItemCount(): Int = list.size
interface OnItemClickCallback{
fun onItemClicked(data: User)
}
}
DetailUserAdapter.kt
class DetailUserViewModel: ViewModel() {
val user= MutableLiveData<DetailUserResponse>()
fun setUserDetail(username: String){
RetrofitClient.apiInstance
.getUserDetail(username)
.enqueue(object : Callback<DetailUserResponse>{
override fun onResponse(
call: Call<DetailUserResponse>,
response: Response<DetailUserResponse>
) {
if (response.isSuccessful){
user.postValue(response.body())
}
}
override fun onFailure(call: Call<DetailUserResponse>, t: Throwable) {
t.message?.let { Log.d("Failure", it) }
}
})
}
fun getUserDetail(): LiveData<DetailUserResponse>{
return user
}
}
SectionPagerAdapter.kt
class SectionPagerAdapter(private val mCtx: Context, fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
#StringRes
private val TAB_TITLES = IntArray(R.string.tab_1)
override fun getCount(): Int = 2
override fun getItem(position: Int): Fragment {
var fragment: Fragment? = null
when(position){
0 -> fragment = FollowerFragment()
1 -> fragment = FollowingFragment()
}
return fragment as Fragment
}
override fun getPageTitle(position: Int): CharSequence? {
return mCtx.resources.getString(TAB_TITLES[position])
}
}
The code here has a minor issue:
Intent(this#MainActivity, DetailUserActivity::class.java).also{
intent.putExtra(DetailUserActivity.EXTRA_USERNAME, data.login)
startActivity(intent)
}
Here you are using .also, which passes the Intent you are creating as it however you are using intent for launching Activity which is the intent you get in your activity, the same you can get using getIntent(), this is causing this issue here.
Please change this to something similar to this and try:
Intent(this#MainActivity, DetailUserActivity::class.java).also{
it.putExtra(DetailUserActivity.EXTRA_USERNAME, data.login)
startActivity(it)
}
or may be similar to this:
Intent(this#MainActivity, DetailUserActivity::class.java).also{ launchIntent ->
launchIntent.putExtra(DetailUserActivity.EXTRA_USERNAME, data.login)
startActivity(launchIntent)
}
Thanks!
im trying to make an apps that use GitHub search user API with Retrofit.i want to make that when i type some username,it will show any github username related with it will be displayed on RecyclerView.is there something that i've missed or write wrongly?
since the apps run just fine,but when i try to search the username,it just show the 1 username type instead every username related to it.
here's my example to search username:
https://api.github.com/search/users?q=sidiqpermana
and heres some of my coding
RetrofitClient.kt
object RetrofitClient {
private const val BASE_URL = "https://api.github.com/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiInstance = retrofit.create(Api::class.java)
}
Api.kt
interface Api {
#GET("search/users")
#Headers("Authorization: token N7QNdhhICaLmxSTzAWr70V7nqf6yaK4B6Z15")
fun getSearchUsers(
#Query("q") query: String
):Call<UserResponse>
}
UserResponse.kt
data class UserResponse(
val items : ArrayList<User>
)
UserAdapter.kt
class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
private val list = ArrayList<User>()
fun setList(users: ArrayList<User>){
list.clear()
list.addAll(users)
notifyDataSetChanged()
}
inner class UserViewHolder(val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(user: User){
binding.apply {
Glide.with(itemView)
.load(user.avatar_url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.into(tvUser)
tvUsername.text = user.login
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return UserViewHolder((view))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(list[position])
}
override fun getItemCount(): Int = list.size
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainViewModel
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = UserAdapter()
adapter.notifyDataSetChanged()
viewModel= ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MainViewModel::class.java)
binding.apply {
rvUser.layoutManager = LinearLayoutManager ( this#MainActivity)
rvUser.setHasFixedSize(true)
rvUser.adapter = adapter
btnSearch.setOnClickListener{
searchUser()
}
etQuery.setOnKeyListener { _, KeyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && KeyCode == KeyEvent.KEYCODE_ENTER){
searchUser()
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
viewModel.getSearchUsers().observe(this,{
if (it!=null){
adapter.setList(it)
showLoading(false)
}
})
}
private fun searchUser(){
binding.apply {
val query = etQuery.text.toString()
if (query.isEmpty()) return
showLoading(true)
viewModel.setSearchUsers(query)
}
}
private fun showLoading(state: Boolean){
if (state){
binding.progressBar.visibility = View.VISIBLE
}else{
binding.progressBar.visibility = View.GONE
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
val listUsers = MutableLiveData<ArrayList<User>>()
fun setSearchUsers(query: String){
RetrofitClient.apiInstance
.getSearchUsers(query)
.enqueue(object : Callback<UserResponse>{
override fun onResponse(
call: Call<UserResponse>,
response: Response<UserResponse>
) {
if (response.isSuccessful){
listUsers.postValue(response.body()?.items)
}
}
override fun onFailure(call: Call<UserResponse>, t: Throwable) {
t.message?.let { Log.d("Failure", it) }
}
})
}
fun getSearchUsers(): LiveData<ArrayList<User>>{
return listUsers
}
}
I'm new to Kotlin. I have a MainActivity and a list view inside.
When an item is clicked in the list view, I want to update a textView inside MainActivity.
However because data should be passed from ListView to MainActivity, I did something weird:
MainActivity.kt:
data class Country(val imgRes:Int, val name:String)
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val data = arrayListOf<Country>()
val imgRes = intArrayOf()
val data1 = arrayOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
for(i in imgRes.indices) {
val country = Country(imgRes[i], data1[i])
data.add(country)
}
val adapter = RecyclerAdapter(data, object:RecyclerAdapter.OnItemClickListener {
override fun onItemClick(v: View, pos: Int) {
binding.textView.text = (v as TextView).text
}
})
binding.recycler1.adapter = adapter
binding.recycler1.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
}
}
ListView.kt:
class RecyclerAdapter(private val dataSet:List<Country>, private val listener:OnItemClickListener) : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
public interface OnItemClickListener {
fun onItemClick(v:View, pos:Int)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = RowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int {
return dataSet.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
inner class ViewHolder(val binding: RowBinding) : RecyclerView.ViewHolder(binding.root) {
val rowImageView = binding.rowImageView
val rowTextView = binding.rowTextView
init {
binding.root.setOnClickListener {
listener.onItemClick(rowTextView, adapterPosition)
}
}
fun bind(pos: Int) {
with(binding) {
rowImageView.setImageResource(dataSet[pos].imgRes)
rowTextView.text = dataSet[pos].name
}
}
}
}
I declared interface(OnItemClickListener) at ListView.kt and redefine it at MainActivity.kt.
It works fine but is there any better way to do this?
You can use higher order functions instead of interface.
Define this function top of your Adapter:
var onClick: ((text: String) -> Unit)? = null
in ViewHolder:
binding.root.setOnClickListener {
onClick?.invoke(dataSet[position].text)
}
in MainActivity:
adapter.onClick = { name ->
binding.textView.text = name
}
You can also find more information about higher orders in here
<include android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="#layout/yourlayout" />
Try this in your Adapter:
var itemClickListener: ((position: Int, name: String) -> Unit)? = null
Bindviewholder:
itemClickListner.invoke(1,"anyvalue")
in Activity:
adapter.itemClickListener = {
position, name ->
Toast.makeText(requireContext(),"position is $position name is $name ",Toast.LENGTH_SHORT).show()
}
in Adapter:
private var mListener: OnItemClickListener,
interface OnItemClickListener {
fun onClick(position: Int, routerArray: ArrayList<Router>)
}
binding.yourView.setOnClickListener {
mListener.onClick(adapterPosition, routerArray)
}
in Activity:
override fun onClick(position: Int, yourArray: ArrayList<Model>) {
binding.textView.text = yourArray[position].text
}
I use Callback pattern as a reaction of ClickListener, but I'm trying to use Coroutine at the moment.
Sometimes, so much of overlap of callback is hell.
When I click a item of RecyclerView, I want to know that position of item I clicked.
I made it with Channel and it works.
MainActivity.class
class MainActivity: Activity() {
private lateinit var binding: ActivityYoutubeFakeHomeBinding
private lateinit var myAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityYoutubeFakeHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setView()
}
private fun setView() = scope.launch {
val positionChannel = Channel<Int>(Channel.UNLIMITED)
myAdapter = MyAdapter(this#MainActivity, positionChannel)
launch {
while(true) {
dialogUtil.showEdit(positionChannel.receive())
}
}
binding.run {
recyclerView.apply {
adapter = myAdapter
layoutManager = LinearLayoutManager(context)
}
}
}
}
MyAdapter.class
class MyAdapter(private val context: Context,
private val channel: Channel<Int>): RecyclerView.Adapter< MyAdapter.HomeViewHolder>() {
private val fakeHomeList = mutableListOf<FakeVideo>()
inner class HomeViewHolder(private val binding: ItemBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(fakeVideo: FakeVideo) {
binding.run {
thumbnailImageView.setImageBitmap(fakeVideo.thumbnail)
titleTextView.text = fakeVideo.title
root.setOnClickListener {
CoroutineScope(Dispatchers.Default).launch {
channel.send(adapterPosition)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeViewHolder {
val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return HomeViewHolder(binding)
}
override fun onBindViewHolder(holder: HomeViewHolder, position: Int) {
holder.bind(fakeHomeList[position])
}
override fun getItemCount() = fakeHomeList.size
}
Yeah, it works. But I'm not sure whether this is the proper way.
Is there any better solution with Coroutine maybe like suspend function something?