I'm just beginning to learn kotlin and my app is vey basic but this basic functionality has been giving me trouble the past couple of days.
I have one activity with two fragments (A and B) in my app. Fragment A is used to display the data in recyclerview. Fragment B is used to add data using edit texts/add button etc. That all works fine. Then when user selects an item in the recyclerview in Fragment A I want to navigate back to Fragment B passing an argument to say the user is now editing now adding and populate the edit texts with the selected items fields so the user can edit.
My Adapter:
class MoviesAdapter constructor(private var movies: List<MovieModel>)
: RecyclerView.Adapter<MoviesAdapter.MainHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainHolder {
return MainHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.card_movie,
parent,
false
)
)
}
override fun onBindViewHolder(holder: MainHolder, position: Int) {
val movie = movies[holder.adapterPosition]
holder.bind(movie)
}
override fun getItemCount(): Int = movies.size
class MainHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(movie: MovieModel) {
itemView.movie_view_Title.text = movie.title
itemView.movie_view_director.text = movie.director
itemView.movie_view_releaseDate.text = movie.releaseDate.toString()
itemView.movie_view_ratingBar.rating = movie.rating.toFloat()
itemView.movie_view_Image.setImageBitmap(readImageFromPath(itemView.context,movie.image))
itemView.setOnClickListener{
//Pass the movie item to Fragment B to edit & pass argument to say we are editing not adding
}
}
}
}
add context in constructor
class MoviesAdapter constructor (mCTX:Context,private var movies: List<MovieModel>){
.....
}
start Second Activity or Fragment using context in Adapter
Pass data using Android Bundle()
or
Implement a Interface
Related
I'm starting using Kotlin (i'm a web dev) to maintain the mobile app of my current job. To practice my learning, I'm creating a basic app which is displaying a list of France departments (using a REST Api), and I need to allow the user to click on a list item to get more info on the selected item.
I'm trying to build this with databinding, Koin as dependency injection, and Room as db layer.
My issue is that I created a RecyclerView custom Adapter, and used the databinding to give it the datas. But now I want to implement the onClick behaviour, which should launch another activity to display item details. My problem is: I don't know how to do this in a clean way.
I was thinking about creating a viewModel to link to my Adapter, but can't really find how to do it well. And even if I did, how to start another activity in a viewModel ? (don't have access to the context and startActivity function). So I finally dropped that solution that doesn't seems to fit.
So I'm currently thinking of passing directly from my adapter the onClick function, but can't find a way to bind this function in my xml file. Here is my files:
MainActivity:
class MainActivity : AppCompatActivity() {
private val mViewModel: DepartmentsViewModel by viewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.toolbar.title = "Liste des départements"
val adapter = DepartmentListAdaptater()
binding.recyclerview.adapter = adapter
binding.recyclerview.layoutManager = LinearLayoutManager(this)
mViewModel.allDepartments.observe(this, Observer { data -> adapter.submitList(data) })
}
}
RecyclerView.Adapter:
class DepartmentListAdaptater : RecyclerView.Adapter<DepartmentListAdaptater.ViewHolder>() {
private var dataSet: List<Department>? = null
inner class ViewHolder(private val binding: DepartmentListRowBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(department: Department?) {
binding.department = department
}
}
fun submitList(list: List<Department>) {
dataSet = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DepartmentListRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int = dataSet?.size ?: 0
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(dataSet?.get(position))
}
}
The XML View:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="department" type="com.navalex.francemap.data.entity.Department" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="72dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="#drawable/list_item_bg"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:clickable="true"
tools:ignore="UselessParent">
<TextView
android:id="#+id/textView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingRight="16dp"
android:text="#{department.nom}"
android:paddingLeft="16dp"/>
</LinearLayout>
</RelativeLayout>
</layout>
First I want to say that it's really impressive that you are a web developer and you already have a lot of knowledge about things like dependency injection and keep the state of the view on ViewModel, congrats. Now, let's talk about your problem... I'll start with some suggestions that will improve the code clarity and performance.
For the Adapter implementation, always prefer to use ListAdapter, because this implementation have a more efficient way to compare the current list with the new list and update it. You can follow this example:
class MyAdapter: ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(
private val binding: FragmentFirstBinding
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// Here you can get the item values to put this values on your view
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ItemModel>() {
override fun areItemsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compare like this below
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ItemModel, newItem: ItemModel): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
In your fragment, you have a observer, that observe the value you want to sent to the adapter, right? When a update happen, you call the submitList sending the updated list and when the adapter receive this new list, the adapter will be responsible to update just the items that changed, because of your DIFF_CALLBACK implementation.
About the onClick item, you can wait for a callback on your adapter. Doing this:
class MyAdapter(
private val onItemClicked: (item: ItemModel) -> Unit
): ListAdapter<ItemModel, MyAdapter.MyViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = FragmentFirstBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding, onItemClicked)
}
// ...
class MyViewHolder(
private val binding: FragmentFirstBinding,
private val onItemClicked: (item: ItemModel) -> Unit
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: ItemModel) {
// ...
// Here you set the callback to a listener
binding.root.setOnClickListener {
onItemClicked.invoke(item)
}
}
}
// ...
}
As you can see, we will receive the callback on the Adapter constructor, then we send to the ViewHolder by constructor too. And on the ViewHolder bind we set the callback to a click listener.
On you fragment, you will have something like this:
class MyFragment: Fragment() {
private lateinit var adapter: MyAdapter
private val onItemClicked: (itemModel: ItemModel) -> Unit = { itemModel ->
// do something here when the item is clicked, like redirect to another activity
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = MyAdapter(onItemClicked)
}
}
I hope it helps you. Please, let me know if you need something more. I really appreciate helping.
I don't know about data binding specifically, but a typical way to do it is to let the Activity handle details like app navigation, and let the Adapter trigger that logic. A listener function is an easy way to do this:
// in your Adapter
var clickListener: ((YourData) -> ())? = null
// in your ViewHolder (make it an inner class so it can access the Adapter's
// fields, like the listener object and the stored data)
init {
clickableView.setOnClickListener {
// pass back whatever data here, if the listener needs to know
// what's been clicked. I'm just doing a lookup and passing
// the data item currently being displayed
clickListener?.invoke(
adapterData[bindingAdapterPosition]
)
}
}
// in your Activity, when setting up the adapter
adapter.clickListener = { whateverData ->
// do what you need to do in response to the click
}
So the Activity itself is defining that logic about actions that should be taken when a click happens - it's basically wiring up different parts of the app, so the Adapter doesn't need to be concerned with anything except taking data, displaying it, and informing a listener when specific interactions take place. That listener code (defined by the Activity) could navigate somewhere else, or update a database, or pass it to a networking component... the adapter doesn't need to know about that.
(The non-Kotlin way to do this would be to create an interface and have the Activity implement that, and pass itself as the listener/callback object, that kind of thing)
I have an Adapter:
class TripsAdapter(
onClickButtonAction: (TripId : Int) -> Unit,
): ListAdapter<Trip, TripssViewHolder>(TripsDiffCallBack()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TripssViewHolder {
return TripsViewHolder(
ItemsTripsBinding.inflate(
LayoutInflater.from(parent.context), parent, false), parent.context)
}
override fun onBindViewHolder(holder: TripsViewHolder, position: Int) {
holder.bind(getItem(position), onClickButtonAction ) <-- here I don't know how pass this function to ViewHolder
}
}
And here is bind function in ViewHolder:
fun bind(trip: Trip, onClickButtonAction : (Trip: Int) -> Unit) <- here is wrong code I think{
// And here I want to reaact on button click and pass ID to fragment like this:
binding.button.setOnClickListener{
onClickButtonAction.invoke()
}
}
And I want to receive it in Fragment like this:
TripsAdapter = TripsAdapter(onClickButtonAction = { id ->
do magic :P
})
Is it posible to pass this ID like funciton? I dont want to use interface. I want to pass Id on click from ViewHolder to Fragment.
You can pass a method as lambda as a callback . And since its a callback it should be Global so you do not have to pass it to ViewHolder further. Also u need to declare the lambda as val so that u can access it in the class.
I have created an example below .
class TripAdapter(val onClickButtonAction: (trip : Trip, view:View?) -> Unit) : ListAdapter<Trip, TripAdapter.TripsViewHolder>(TripsDiffCallBack) {
inner class TripsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(trip: Trip) {
binding.button.setOnClickListener {
onClickButtonAction(trip, it)
}
}
}
}
this is how your lambda will look i have changed the parameters just in case . passing whole object of Trip and also passing the clicked View this can be helpful for handling clicks on multiple views.
Now wherever u are using this Adapter you create its as below :-
val adapter = TripAdapter(::onTripClickListener)
private fun onTripClickListener(trip:Trip, view: View?){
}
I have a recyclerView in my kotlin app that shows a list of items, when i click on one my app navigates to other fragment that shows the details of that recyclerview item, my problem is when I filter the results, it uses the adapterPosition that in this case, its different from the position of the data in the json.
When I filter the data with searchView, I submit to the adapter the new list with the filters applied.
Fragment where the recyclerview is:
(Here i would like to send as a string one of the fields shown in the recyclerview item i click)
private var museumsListAdapter=MuseumsListAdapter{ it ->
val bundle = Bundle().apply {
putInt(INDICE,it)
}
findNavController().navigate(R.id.action_ListMuseumsFragment_to_detailsMuseumFragment, bundle)
}
Adapter of the recyclerView:
class MuseumsListAdapter(private val onMuseumSelected: (Int) -> Unit):
ListAdapter<MuseumsFieldsItem, MuseumsListViewHolder>(MuseumsFieldsItem.DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MuseumsListViewHolder {
val itemView=MuseumListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MuseumsListViewHolder(itemView.root){
onMuseumSelected(it)
}
}
override fun onBindViewHolder(holder: MuseumsListViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
ViewHolder of the recyclerview:
lass MuseumsListViewHolder(itemView: View, private val onItemClicked: (Int) ->Unit) : RecyclerView.ViewHolder(itemView) {
val itemBinding=MuseumListItemBinding.bind(itemView)
init {
itemView.setOnClickListener{
onItemClicked(adapterPosition)
}
}
And in the other fragment (details one) I get the value "INDICE" of the bundle.
class MuseumsListAdapter(private val onMuseumSelected: (Int) -> Unit):
this is a callback which takes in an int, so just don't use int :)
class MuseumsListAdapter(private val onMuseumSelected: (Foo) -> Unit):
where Foo represents whatever you model is, make use of Museum in your case
inside your:
itemView.setOnClickListener{
onMuseumSelected(adapterPosition)
}
you now need an instance of your Museum class.
by using private val onMuseumSelected: (Foo) -> Unit you'll get back a complete model to your fragment/activity, so you can use whatever field you need
how can I save information about the position of the clicked recyclerview element? I need to highlight the clicked recyclerview element and save this selection after closing the app. I tried to do this by creating a booleanArray and changing the value to true by clicking on the element. But how can I save this array so that after re-entering the application, the values are not reset?
Adaptor
class UsersAdapter(private val videoTitles: List<String>,
private val Trening: List<String>,
private val clickListener: onClickRecyclerViewItem,
private val array: BooleanArray
): RecyclerView.Adapter<UsersAdapter.CustomViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val cellForRow = layoutInflater.inflate(R.layout.recyclerview_item, parent, false)
return CustomViewHolder(cellForRow)
}
#SuppressLint("ShowToast")
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val videoTitle = videoTitles[position]
val Trening1 = Trening[position]
holder.view.name.text= videoTitle
holder.view.quantity.text= Trening1
holder.view.setOnClickListener {
clickListener.onClickItemListener()
array[position] = true }
}
override fun getItemCount(): Int {
return videoTitles.size
}
#SuppressLint("ShowToast")
class CustomViewHolder(val view: View): RecyclerView.ViewHolder(view){
}
}
You can save your item clicked in the sharepreferences or in room database and when you come back you can get the data from these and use as you wants.
This link show you how to use sharepreferences and this show how to implements room database
When closing the app, all information aka fragments / activities etc. are destroyed. If you want to save a specific value after closing the app, you can either create a room-database (which is kinda overkill for this purpose), or save the value in the shared-preferences. I advise you to look here to know more about saving data in android.
I have a RecyclerView which was build using an Arraylist. That Arraylist consists of User defined objects named ListItem.
Each recyclerview has a card view. Each CardView holds each ListItem.
I have removed one CardView from that RecyclerView.
When I rotate the screen , A new Activity is created which results in showing the old data. But I want the recyclerview to hold only updated list and should retain the scrolled position.
ListItem class :
class ListItem(var title: String, var info: String, val imageResource: Int) {
}
MainActivity class :
class MainActivity : AppCompatActivity() {
private lateinit var mSportsData: ArrayList<ListItem>
private lateinit var mAdapter: MyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val gridColumnCount = resources.getInteger(R.integer.grid_column_count)
recycler_view.layoutManager = GridLayoutManager(this,gridColumnCount)
mSportsData = ArrayList()
recycler_view.setHasFixedSize(true)
initializeData()
recycler_view.adapter = mAdapter
var swipeDirs = 0
if (gridColumnCount <= 1) {
swipeDirs = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
}
val helper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN,swipeDirs) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val from = viewHolder.adapterPosition
val to = target.adapterPosition
Collections.swap(mSportsData,from,to)
mAdapter.notifyItemMoved(from,to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
mSportsData.removeAt(viewHolder.adapterPosition)
mAdapter.notifyItemRemoved(viewHolder.adapterPosition)
}
})
helper.attachToRecyclerView(recycler_view)
}
private fun initializeData() {
val sportsList : Array<String> = resources.getStringArray(R.array.sports_titles)
Log.d("Printing","$sportsList")
val sportsInfo : Array<String> = resources.getStringArray(R.array.sports_info)
val sportsImageResources : TypedArray = resources.obtainTypedArray(R.array.sports_images)
mSportsData.clear()
for (i in sportsList.indices-1) {
Log.d("Printing","${sportsList[i]},${sportsInfo[i]},${sportsImageResources.getResourceId(i,0)}")
mSportsData.add(ListItem(sportsList[i], sportsInfo[i], sportsImageResources.getResourceId(i, 0)))
}
sportsImageResources.recycle()
mAdapter = MyAdapter(mSportsData,this)
mAdapter.notifyDataSetChanged()
}
fun resetSports(view: View) {
initializeData()
}
}
MyAdapter class :
class MyAdapter(var mSportsData: ArrayList<ListItem>, var context: Context) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.wordlist_item,parent,false))
}
override fun getItemCount() = mSportsData.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val listItem = mSportsData.get(position)
holder.bindTo(listItem)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val currentSport = mSportsData.get(adapterPosition)
val detailIntent = Intent(context, DetailActivity::class.java)
detailIntent.putExtra("title", currentSport.title)
detailIntent.putExtra("image_resource", currentSport.imageResource)
context.startActivity(detailIntent)
}
fun bindTo(currentSport : ListItem){
itemView.heading_textview.setText(currentSport.title)
itemView.description_textview.setText(currentSport.info)
Glide.with(context).load(currentSport.imageResource).into(itemView.image_view)
}
}
}
You can restrict activity restarting in your Manifest if you have same layout for Portrait and Landscape mode.
Add this to your activity in the manifest.
<activity android:name=".activity.YourActivity"
android:label="#string/app_name"
android:configChanges="orientation|screenSize"/>
If you don't want to restrict screen orientation changes, then you can use OnSaveInstanceState method to save your older data when orientation changed. Whatever data you save via this method you will receive it in your OnCreate Method in bundle. Here is the helping link. So here as you have ArrayList of your own class type you also need to use Serializable or Parcelable to put your ArrayList in your Bundle.
Except these making ArrayList as public static is always a solution, But its not a good solution in Object Oriented paratime. It can also give you NullPointerException or loss of data, in case of low memory conditions.
It looks like initializeData is called twice since onCreate is called again on orientation change, you could use some boolean to check if data has been already initialized then skip initializing
What you are doing is you are deleting the values that are passed down to the recyclerview but when the orientation changes the recyclerview reloads from activity and the original data from activity is passed down again and nothing changes, so if you want to save the changes in recyclerview you have to change the original data in the activity so that if the view reloads the data is the same.
I think u initialize adapter in oncreate method in which the whole adapter will be recreated and all datas is also newly created when configuration changes. Because u init data in oncreate method. Try something globally maintain the list and also delete the item in the list in activity when u delete in adapter also. Or try something like view model architecture
Use MVVM pattern in the project. It will manage the orientation state.
MVVM RecyclerView example:
https://medium.com/#Varnit/android-data-binding-with-recycler-views-and-mvvm-a-clean-coding-approach-c5eaf3cf3d72