Convert MainActivity with actionbar/toolbar and floatingaction button to Anko - kotlin

I am trying to learn how to use Kotlin/Anko.
I have gone thru the examples here and also cloned the template project and can understand how to do some basic stuff, but as an exercise I wanted to convert this simple activity (generated from a blank activity in Android Studio and converted to Kotlin) to use Anko as well. There are not a lot of examples around for Anko, most are just copies of what is on the above referenced github page.
Can someone demonstrate how to go about and convert the following into Anko DSL?
MainActivity.kt
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolBar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolBar)
val fab = findViewById(R.id.fab) as FloatingActionButton
fab.setOnClickListener { view -> Snackbar.make(view, "Replace this with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show() }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == R.id.action_settings) {
println("settings clicked on ")
return true
}
return super.onOptionsItemSelected(item)
}
}
main_activity.xml
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="#layout/content_main" />
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="#dimen/fab_margin"
android:src="#android:drawable/ic_dialog_email" />
</android.support.design.widget.CoordinatorLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context="com.gmail.npnster.mykotlinfirstproject.MainActivity"
tools:showIn="#layout/activity_main">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:id="#+id/hello"
/>
</RelativeLayout>
menu_main.xml
<menu 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"
tools:context="com.gmail.npnster.mykotlinfirstproject.MainActivity">
<item
android:id="#+id/action_settings"
android:orderInCategory="100"
android:title="#string/action_settings"
app:showAsAction="never" />
</menu>

You can use ankoView method to create Views without DSL methods inside DSL context.
For example, to create a NavigationView one can use
ankoView({ NavigationView(it) }) {
lparams(width = wrapContent, height = matchParent, gravity = Gravity.START)
// more initialization follows
}
This way you can instantiate FloatingActionButton and AppBarLayout, just call their constructors inside ankoView's first argument function. For your convenience, you can make yourself DSL-like functions like in the manual:
fun floatingActionButton(init: FloatingActionButton.() -> Unit) = ankoView({ FloatingActionButton(it) }, init)
Creating a Toolbar is even easier: there is a DSL toolbar method in org.jetbrains.anko.appcompat.v7.
When using Anko DSL, to include another layout, as you did with content_main, one can either use Anko include function or just write a function which will fill in a ViewGroup. You can use this template:
fun ViewGroup.myLayout() {
textView("123")
// more DSL code here
}
Then just call myLayout() inside some ViewGroup initializer.

I know it's a bit late answer, but I hope it helps someone. I did the layout this way (of course some styling is still needed):
class MainUI(val adapter: MainUIAdapter) : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
coordinatorLayout {
fitsSystemWindows = true
appBarLayout {
toolbar {
setTitleTextColor(Color.WHITE) // so far still needed
id = R.id.toolbar
}.lparams(width = matchParent, height = matchParent)
}.lparams(width = matchParent)
relativeLayout {
id = R.id.container
recyclerView { // just an example
id = R.id.recycler_view
adapter = this#MainUI.adapter
layoutManager = LinearLayoutManager(ctx)
}
}.lparams(width = matchParent, height = matchParent) {
behavior = ScrollingViewBehavior()
}
floatingActionButton {
onClick { doSomething() }
imageResource = R.drawable.ic_add_white_24dp // the plus sign
}.lparams {
gravity = Gravity.BOTTOM or Gravity.END
margin = dip(16)
}
}
}
}
and used in MainActivity like that:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainUI(MainUIAdapter(people)).setContentView(this)
toolbar = find<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
}

Related

RecyclerView view doesn't change using background_selector

I'm Trying to toggle item background color upon item selection.
I can see it's logic is working yet the background wouldn't change.
The log prints show the selctions indeed work, yet the background not.
From background_selector:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#android:color/holo_green_light" android:state_pressed="false" android:state_selected="true" />
<item android:drawable="#android:color/holo_purple" android:state_selected="false" />
</selector>
from task_row.xml:
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="3dp"
app:cardElevation="3dp"
android:id="#+id/tasksCardView"
android:background="#drawable/background_selector"
app:cardUseCompatPadding="true">
From Adapter code:
class MyTasksAdapter (
val arrayList: ArrayList<Task>,
val selectedItemsList: ArrayList<Task>,
val deletedItemsList: ArrayList<String>) :
RecyclerView.Adapter<MyTasksAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private var cardView: CardView = itemView.tasksCardView
private val selectedItems = SparseBooleanArray()
override fun onClick(view: View) {
if (selectedItems[adapterPosition, false]) {
selectedItems.delete(adapterPosition)
cardView.isSelected = false
Log.d("Item selected AA", "position: $position")
}
else {
selectedItems.put(adapterPosition, true)
view.isSelected = true
Log.d("Item selected BB", "position: $position")
}
}
init {
itemView.setOnClickListener(this)
}
Tried also changing cardview to view, but it didn't help either.
class MyTasksAdapter (
val arrayList: ArrayList<Task>,
val selectedItemsList: ArrayList<Task>,
val deletedItemsList: ArrayList<String>) :
RecyclerView.Adapter<MyTasksAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
//private lateinit var item: ClipData.Item
private var cardView: CardView = itemView.tasksCardView
private val selectedItems = SparseBooleanArray()
override fun onClick(view: View) {
if (selectedItems[adapterPosition, false]) {
selectedItems.delete(adapterPosition)
view.isSelected = false
Log.d("Item selected AA", "position: $position")
}
else {
selectedItems.put(adapterPosition, true)
view.isSelected = true
Log.d("Item selected BB", "position: $position")
}
}
init {
itemView.setOnClickListener(this)
}
Appreciate your kind help.
Working now.
Moved it to the relative_layout header: instead of cardview_layout header
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="3dp"
app:cardElevation="3dp"
android:id="#+id/tasksCardView"
android:hapticFeedbackEnabled="true"
app:cardUseCompatPadding="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
**android:background="#drawable/background_selector"**
android:padding="16dp">

Kotlin : Recyclerview on Fragment ,how to make the default total price is the first item multiply 1

I got stuck in the situation ..
The idea is ,make the display total price with the first item selected multiply the first value of popup menu which is 1 ,I was trying to make the first item as the initial price ,but the total price became the first item price * selected value of popup menu ....
I have no idea what happend ..
Could you please help me check my code ,thank you so much in advance :
Product.kt
package com.gearsrun.recyclerviewfragmentapplication
data class Product(var price : String)
ProductAdapter.kt
package com.gearsrun.recyclerviewfragmentapplication
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_layout.view.*
class ProductAdapter(private val productList:List<Product>): RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {
private var selectedItemPosition :Int = 0
private var mlistener : onItemClickListener ?=null
fun interface onItemClickListener{
fun onItemClick(position: Int)
}
fun setOnItemClickListener(listener: onItemClickListener){
mlistener = listener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_layout,parent,false)
return ProductViewHolder(itemView,mlistener)
}
override fun onBindViewHolder(holder: ProductAdapter.ProductViewHolder, position: Int) {
val currentItem = productList[position]
holder.price.text = currentItem.price
holder.itemView.setOnClickListener {
selectedItemPosition = position
notifyDataSetChanged()
}
if(selectedItemPosition == position){
holder.itemView.cardView.setBackgroundColor(Color.parseColor("#FAFAD2"))
}else{
holder.itemView.cardView.setBackgroundColor(Color.parseColor("#FFFFFF"))
}
}
override fun getItemCount() = productList.size
class ProductViewHolder(itemView: View,listener: onItemClickListener):RecyclerView.ViewHolder(itemView) {
val price : TextView = itemView.price
init {
itemView.setOnClickListener {
listener?.onItemClick(absoluteAdapterPosition)
}
}
}
}
fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.HomeFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<!--Recycler View-->
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/giftRecycleView"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_marginTop="30dp"
/>
<!--Selected option will display here-->
<LinearLayout
android:layout_width="match_parent"
android:gravity="center"
android:padding="16dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Select amount : "
android:textColor="#color/black"
android:textSize="18sp"/>
<TextView
android:id="#+id/selectedTv"
android:layout_width="100dp"
android:layout_height="60dp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginLeft="10dp"
android:background="#color/black"
android:textColor="#color/white"
android:textSize="20sp"
android:text="1" />
</LinearLayout>
<!--Total price-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total : "
android:textColor="#color/black"
android:textSize="18sp"/>
<TextView
android:id="#+id/price_t"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/black"
android:textSize="18sp"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
HomeFragment.kt
package com.gearsrun.recyclerviewfragmentapplication.fragment
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.gearsrun.recyclerviewfragmentapplication.Product
import com.gearsrun.recyclerviewfragmentapplication.ProductAdapter
import com.gearsrun.recyclerviewfragmentapplication.R
import kotlinx.android.synthetic.main.fragment_home.*
import kotlin.properties.Delegates
class HomeFragment : Fragment(R.layout.fragment_home) {
//recyclerview
private val productList = generateProduct()
private var select_price = 0 // gift price
private var select_num = 1 //popup menu value
private fun refreshOutput(){
price_t.text = (select_num*select_price).toString()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ProductAdapter(productList)
giftRecycleView.adapter = adapter
adapter.setOnItemClickListener{position : Int->
select_price = productList[position].price.toInt()
refreshOutput()
}
giftRecycleView.layoutManager = LinearLayoutManager(context,LinearLayoutManager.HORIZONTAL,false)
//popup menu
val popupMenu = PopupMenu(
context,
selectedTv
)
for(i in 0..5){
popupMenu.menu.add(Menu.NONE,i,i,i.toString())
}
//handle menu clicks
popupMenu.setOnMenuItemClickListener { menuItem ->
val i = menuItem.itemId+1
selectedTv.text = i.toString()
select_num = i
refreshOutput()
true
}
//handle menu click to show menu
selectedTv.setOnClickListener {
popupMenu.show()
}
//calculate the total price
refreshOutput()
}
private fun generateProduct(): List<Product>{
val list = ArrayList<Product>()
list.add(Product("5"))
list.add(Product("6"))
list.add(Product("7"))
return list
}
}
If I understood correctly the popup has a value other than textView and this value is used for the calculation. And the problem comes before selecting amout.
You can set a default popup value like here
How to set a default selected option in Android popup menu?
First I have to say something about this piece of code. If you find yourself copy-pasting code like this, you need to step back and simplify it. Your setup of the popup menu could be cut down to this:
//add menu items to popup menu
for (i in 0..4) {
popupMenu.menu.add(Menu.NONE, i, i, (i + 1).toString())
}
//handle menu clicks
popupMenu.setOnMenuItemClickListener { menuItem ->
val i = menuItem.itemId + 1
selectedTv.text = i.toString()
select_num = i
total_price = select_num * select_price
price_t.text = total_price.toString()
true
}
And here you can see the problem. You are only updating the calculated value when the popup menu is clicked, but not when a different view is selected in the adapter. You should create a function that updates the calculation and puts it in the text view, and call them from both listeners (adapter's item click listener and the popup menu listener).
It's kind of weird to do this with local variables and more unusual to define a function inside onViewCreated(). You should promote them to private properties so the function goes outside onViewCreated(). Eliminate the total_price variable because you never use it in a useful way. It will always be out of date once something else changes, so it is not helping you at all. So your code will end up looking like:
private var select_price = 0
private var select_num = 1
private fun refreshOutput() {
price_t.text = (select_num * select_price).toString()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ProductAdapter(productList)
giftRecycleView.adapter = adapter
adapter.setOnItemClickListener(object :ProductAdapter.onItemClickListener{
override fun onItemClick(position: Int) {
select_price = productList[position].price.toInt()
refreshOutput()
}
})
giftRecycleView.layoutManager = LinearLayoutManager(context,LinearLayoutManager.HORIZONTAL,false)
//popup menu
val popupMenu = PopupMenu(
context,
selectedTv
)
for (i in 0..5) {
popupMenu.menu.add(Menu.NONE, i, i, i.toString())
}
//handle menu clicks
popupMenu.setOnMenuItemClickListener { menuItem ->
val i = menuItem.itemId + 1
selectedTv.text = i.toString()
select_num = i
refreshOutput()
true
}
//handle menu click to show menu
selectedTv.setOnClickListener {
popupMenu.show()
}
//calculate the total price
refreshOutput() // show initial value
}
And a couple of tips about Kotlin. You are misusing lateinit for your listener. lateinit is for properties that are guaranteed to be initialized before they are accessed anywhere else in your code. This is mostly only applicable for classes that are instantiated by reflection, and the subclass's code's first entry point is somewhere other than the constructor, like in an Activity's onCreate() or Fragment's onCreateView()/onViewCreated(). This is not true for your Adapter, so by marking the property lateinit, you are only using the keyword to subvert null-safety. The property should simply be nullable, and a null-safe call should be used with it in the one place where you actually use it.
Also, if you define your interface as a fun interface, you can take advantage of lambda syntax.
private var mlistener : onItemClickListener? = null
fun interface onItemClickListener{
fun onItemClick(position: Int)
}
fun setOnItemClickListener(listener: onItemClickListener){
mlistener = listener
}
//...
// In product view holder:
init {
itemView.setOnClickListener {
listener?.onItemClick(absoluteAdapterPosition)
}
}
// In Fragment:
adapter.setOnItemClickListener { position: Int ->
select_price = productList[position].price.toInt()
refreshOutput()
}
And finally, my answer is just explaining how to get your current code working. Really, you should convert the select_price and select_num into LiveDatas or StateFlows in a ViewModel. Then you would use these values to set up the state of your UI elements, and they will persist correctly if the screen is rotated. The way it is now, when the screen rotates, your currently selected price and number will be lost.

How to write the adapter class for the following recycler view?

I am trying to make a Todo app and I have done the Room part and able to store data now I want to display the data in the form of Recycler View but I am not getting how to write the adapter class corresponding to it. I looked for it in different sites I never got any satisfying answer.
**TodoFragViewModel.kt""
class TodofragViewModel(
val database: TodoDao, applicaltion: Application
): AndroidViewModel(applicaltion) {
// TODO: Implement the ViewModel
/**
* viewModelJob allows us to cancel all coroutines started by this ViewModel.
*/
private var viewModelJob = Job()
/**All coroutines can be cancelled by viewmodelJob.cancel() and Dispatcher.main is byDefault choice
*/
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val currenctTodo = MutableLiveData<Todo?>()
private val allTodo = database.getAllTodo()
init{
intializeThisTodo()
}
private fun intializeThisTodo(){
uiScope.launch {
currenctTodo.value=getFromDatabase()
}
}
private suspend fun getFromDatabase(): Todo? {
return withContext(Dispatchers.IO){
val info =database.getCurrentTodo()
info
}
}
private suspend fun insert(thisTodo: Todo) {
withContext(Dispatchers.IO) {
database.insert(thisTodo)
Log.i("Database","${database.getCurrentTodo()?.description} and ${database.getCurrentTodo()?.time}")
}
}
fun onAdded(time:String,description:String) {
uiScope.launch {
val thisTodo = Todo(time,description)
insert(thisTodo)
currenctTodo.value=getFromDatabase()
}
}
/**
* Called when the ViewModel is dismantled.
* At this point, we want to cancel all coroutines;
* otherwise we end up with processes that have nowhere to return to
* using memory and resources.
*/
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
todo_recycler_view
<androidx.cardview.widget.CardView 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="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/date_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/todo_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/date_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
TodoFrag.kt
class todofrag : Fragment() {
companion object {
fun newInstance() = todofrag()
}
private lateinit var viewModel: TodofragViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.todofrag_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val application = requireNotNull(this.activity).application
val dataSource= TodoDatabase.getInstance(application)?.InformationDatabaseDao
val viewModelFactory = dataSource?.let { TodoViewModelFactory(it, application) }
val viewModel=ViewModelProviders.of(this,viewModelFactory).get(TodofragViewModel::class.java)
add_button.setOnClickListener{
val currentDate: String = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(Date())
val currentTime: String = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
val time:String="${currentDate} \n ${currentTime}"
viewModel.onAdded(time,todo_text.text.toString())
}
}
}
Please let me know if any other files are added. By the way, I tried to use card view so that it looked good.
The developer documentation explains it pretty well.
This might not be perfectly suited for what you need, but it should be a good start. Specifically, I don't know all the fields for your Todo class, so make sure you account for those in this code.
Basically, you'll want to have a ViewHolder that represents your CardView
class TodoViewHolder(convertView: View) : RecyclerView.ViewHolder(convertView) {
val dateText = convertView.findViewById(R.id.date_text)
val description = convertView.findViewById(R.id.todo_description)
// whatever else you need access to
}
And you'll want to use DiffUtil for a better user experience. This allows for some animations when things in the list change, such as removing an item, editing an item, or adding an item.
private class TodoDiffCallback : DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.dateText == newItem.dateText && oldItem.description == newItem.description
}
You'll want to extend ListAdapter and override its methods. onCreateViewHolder creates an instance of your TodoViewHolder for each view that is seen and onBindViewHolder allows you to add behavior to each item in the list. It is worth noting that you can pass parameter into the adapter in case you need to.
class MyListAdapter : ListAdapter<Todo, TodoViewHolder>(TodoDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TodoViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.todo_recycler_view, parent, false))
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val todo = getItem(position)
holder.dateText = todo.dateText
holder.description = todo.description
// add whatever click listener and other stuff you need
}
}
In your fragment, when you access your RecyclerView, just add an instance of the adapter if it's null.
if (recyclerView.adapter == null) {
recyclerView.adapter = TotoListAdapter()
}
And when you want to add data (that you have retrieved from Room or your API) to the adapter (in the fragment/activity), just do the following:
(recyclerView.adapter as? TodoListAdapter)?.submitList(data)
On a side note, make sure to clean up your style (you can use the Reformat Code command in the Code menu), and you would want to rename the todo_recycler_view to something like todo_view. You'll want to have a RecyclerView layout in your fragment layout.

Kotlin read and display data from Google Sheets

Can somebody help me accomplish what I am trying to. I have spent hours watching videos and surfing Google to do it myself. But, I couldn't as there's no help I could find to read just one cell value from Google Sheets and I couldn't also find code for Kotlin.
All what I want is to display the value in cell B2 in my Google sheets in a textview on MainActivity.
I am new to Android app development.
Thanks in advance.
i have code for to get the value from google sheets let me share it to you.
First of all you need to create one spreadsheet and you need to create your api key to get access to spreadsheet api.
MY SpreadSheet data
--> When you create spread sheet in file options -> publish to web. and it will generate a link.
-> get the sheets id from that link. it will be like
https://docs.google.com/spreadsheets/d/your-id-//edit#gid=0
--> On given url you have to put your sheets id sheet name and the api key that you have generated from google console
From generated url you can get the json format of your data into spreadsheet.
Your url will look like this.
https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/YOUR_SHEET_NAME?alt=json&key=YOUR_API_KEY
Open this url into browser and get the json format of your data from spreadsheet.
After getting the data
Here is the code to retrive the data from spreadsheet.
first you need a model class to get the data from json.
class model {
// variables for our first name,
// last name, email and avatar
private var first_name: String? = null
private var last_name: String? = null
private var email: String? = null
constructor(first_name: String, last_name: String, email: String){
this.first_name = first_name;
this.last_name = last_name;
this.email = email;
}
fun getFirst_name(): String? {
return first_name
}
fun setFirst_name(first_name: String?) {
this.first_name = first_name
}
fun getLast_name(): String? {
return last_name
}
fun setLast_name(last_name: String?) {
this.last_name = last_name
}
fun getEmail(): String? {
return email
}
fun setEmail(email: String?) {
this.email = email
}
}
BaseModel.kt
class BaseModel {
#SerializedName("range")
#Expose
private var range: String? = null
#SerializedName("majorDimension")
#Expose
private var majorDimension: String? = null
#SerializedName("values")
#Expose
private var values: List<List<model?>?>? = null
fun getRange(): String? {
return range
}
fun setRange(range: String?) {
this.range = range
}
fun getMajorDimension(): String? {
return majorDimension
}
fun setMajorDimension(majorDimension: String?) {
this.majorDimension = majorDimension
}
fun getValues(): List<List<model?>?>? {
return values
}
fun setValues(values: List<List<model?>?>?) {
this.values = values
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="#+id/idRVUsers"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/user_rv_item" />
<!--we are adding progress bar for thepurpose of loading-->
<ProgressBar
android:id="#+id/idPBLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
user_rv_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="8dp"
app:cardCornerRadius="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp">
<!--image view for displaying user image-->
<ImageView
android:id="#+id/idIVUser"
android:layout_width="100dp"
android:src="#drawable/ic_launcher_foreground"
android:layout_height="100dp"
android:layout_margin="10dp" />
<!--text view for displaying first name-->
<TextView
android:id="#+id/idTVFirstName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_toEndOf="#id/idIVUser"
android:layout_toRightOf="#id/idIVUser"
android:text="First Name"
android:textColor="#color/black"
android:textSize="15sp" />
<!--text view for displaying last name-->
<TextView
android:id="#+id/idTVLastName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/idTVFirstName"
android:layout_marginTop="10dp"
android:layout_toEndOf="#id/idIVUser"
android:layout_toRightOf="#id/idIVUser"
android:text="Last Name"
android:textColor="#color/black"
android:textSize="15sp" />
<!--text view for displaying user email-->
<TextView
android:id="#+id/idTVEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/idTVLastName"
android:layout_marginTop="10dp"
android:layout_toEndOf="#id/idIVUser"
android:layout_toRightOf="#id/idIVUser"
android:text="Email"
android:textColor="#color/black"
android:textSize="15sp" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
rv_adapter.kt
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class UserRVAdapter(modelArraylist: ArrayList<model>) :
RecyclerView.Adapter<UserRVAdapter.ViewHolder>() {
// variable for our array list and context.
private val userModalArrayList: ArrayList<model> = modelArraylist
private val context: Context? = null
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val firstNameTV: TextView? = itemView.findViewById(R.id.idTVFirstName)
var lastNameTV: TextView? = itemView.findViewById(R.id.idTVLastName)
var emailTV: TextView? = itemView.findViewById(R.id.idTVEmail)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view: View =
LayoutInflater.from(parent.context).inflate(R.layout.user_rv_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// getting data from our array list in our modal class.
// getting data from our array list in our modal class.
val userModal: model = userModalArrayList[position]
// on the below line we are setting data to our text view.
// on the below line we are setting data to our text view.
holder.firstNameTV?.setText(userModal.getFirst_name())
holder.lastNameTV?.setText(userModal.getLast_name())
holder.emailTV?.setText(userModal.getEmail())
}
override fun getItemCount(): Int {
return userModalArrayList.size
}
}
MainActivity.kt
import android.os.Bundle
import android.view.View
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import org.json.JSONException
import org.json.JSONObject
class MainActivity : AppCompatActivity() {
private var userModalArrayList: ArrayList<model>? = null
private var userRVAdapter: UserRVAdapter? = null
private var userRV: RecyclerView? = null
private var loadingPB: ProgressBar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
userModalArrayList = ArrayList()
userRV = findViewById(R.id.idRVUsers)
loadingPB = findViewById(R.id.idPBLoading)
getDataFromAPI()
}
private fun getDataFromAPI() {
val url=
"https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/YOUR_SHEET_NAME?alt=json&key=YOUR_API_KEY"
val queue = Volley.newRequestQueue(this#MainActivity)
val jsonObjectRequest =
JsonObjectRequest(
Request.Method.GET,
url,
null,
object : Response.Listener<JSONObject> {
override fun onResponse(response: JSONObject) {
loadingPB!!.visibility = View.GONE
try {
// val feedObj = response.getJSONObject("")
val entryArray = response.getJSONArray("values")
for (i in 1 until entryArray.length()) {
val entryObj = entryArray.getJSONArray(i)
val firstName =
entryObj[2].toString()
val lastName = entryObj[3].toString()
// entryObj.getJSONObject("gsx\$lastname").getString("\$t")
val email = entryObj[1].toString()
userModalArrayList!!.add(model(firstName, lastName, email))
// passing array list to our adapter class.
userRVAdapter = UserRVAdapter(userModalArrayList!!)
// setting layout manager to our recycler view.
userRV!!.layoutManager = LinearLayoutManager(this#MainActivity)
// setting adapter to our recycler view.
userRV!!.adapter = userRVAdapter
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
},
object : Response.ErrorListener {
override fun onErrorResponse(error: VolleyError?) {
// handline on error listener method.
Toast.makeText(this#MainActivity, "Fail to get data..", Toast.LENGTH_SHORT)
.show()
}
})
queue.add(jsonObjectRequest);
}
}
You can use this code to get data from your whole google spread sheet.
I suggest making google sheet public, then you can access it for example as csv or in other formats, but csv is simplest. You can construct url based on sheet id:
val url = "https://docs.google.com/spreadsheets/d/e/${id}/pub?output=csv"
You can get contents for example using ktor:
suspend fun getUrlAsString(url: String): String {
val client = HttpClient(Android) {
}
return client.get<String>(url)
}
Example on how to parse csv, but you can use some library or parse text yourself, it's pretty simple:
fun parseCsv(text: String): List<Map<String, String>> {
val reader = CSVReaderBuilder(StringReader(text))
.build()
val lines = reader.readAll()
val firstLine = lines.removeAt(0)
val result = lines.map { line ->
firstLine.mapIndexed { index, label ->
label to line[index]
}.toMap()
}
if (result.size > 4)
println(result[4])
return result
}

Passing arguments in BottomNavigationView with navigation Component

I'm currently writing an app with the new (to me) navigation component. I've got the basics down with a single navigation graph to navigate around my app, I've got a fragment with a BottomNavigationView in which has 3 seperate fragments, I've managed to update this to use the navigation component (as far as my problem) using the menu with ids that match the navigation items. My fragments which all previously used newInstance methods to pass a bundle to the onCreate are obviously now not used but I still need to pass a bundle to my fragments.
I haven't been able to find any examples of this being done, as the fragments are implicitly created.
My code is structured as ClientFragment which is the host fragment for the navigation drawer etc which is;
class ClientFragment : Fragment() {
private val viewModel: ClientViewModel by viewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_client, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.client = arguments?.getParcelable(ARG_CLIENT)!!
toolbar_client.title = viewModel.client.name
toolbar_client.setNavigationOnClickListener { Navigation.findNavController(view).navigateUp() }
}
}
This class previously held on onclick listener to my fragments, with a newInstance method which tool viewModel.client.
My fragments in the nav_graph are all similar. The first fragment;
class ClientDetailsFragment : Fragment() {
private val viewModel: ClientViewModel by viewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_client_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// viewModel.client = arguments?.getParcelable(ARG_CLIENT)!!
initClientDetails()
}
private fun initClientDetails() {
// text_client_details_name.text = viewModel.client.name
// text_client_details_account_number.text = viewModel.client.accountNumber
// text_client_details_mobile_number.text = viewModel.client.mobileNumber
// text_client_details_landline_number.text = viewModel.client.landlineNumber
// text_client_details_email.text = viewModel.client.email
// text_client_details_address.text = "NOT YET IMPLEMENTED"
//
// text_client_description_body.text = viewModel.client.description
// text_client_system_details_body.text = viewModel.client.systemDetails
}
}
The app crashes on the commented out line;
// viewModel.client = arguments?.getParcelable(ARG_CLIENT)!!
My navigation graph and menu are;
nav graph;
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="#+id/client_nav_graph"
app:startDestination="#id/clientDetailsFragment">
<fragment
android:id="#+id/clientCustomersFragment"
android:name="com.management.engineering.alarm.alarmengineermanagement.features.client.ClientCustomersFragment"
android:label="ClientCustomersFragment"
tools:layout="#layout/fragment_client_customers" />
<fragment
android:id="#+id/clientDetailsFragment"
android:name="com.management.engineering.alarm.alarmengineermanagement.features.client.ClientDetailsFragment"
android:label="ClientDetailsFragment"
tools:layout="#layout/fragment_client_details"/>
<fragment
android:id="#+id/clientJobHistoryFragment"
android:name="com.management.engineering.alarm.alarmengineermanagement.features.client.ClientJobHistoryFragment"
android:label="ClientJobHistoryFragment"
tools:layout="#layout/fragment_client_job_history" />
</navigation>
menu;
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/clientDetailsFragment"
android:icon="#drawable/ic_launcher_foreground"
android:title="Details"/>
<item
android:id="#+id/clientJobHistoryFragment"
android:icon="#drawable/ic_launcher_foreground"
android:title="Job History"/>
<item
android:id="#+id/clientCustomersFragment"
android:icon="#drawable/ic_launcher_foreground"
android:title="Customers"/>
</menu>
I have found that you can add arguments to the navigation graph, but have found nothing about where to put them for this specific scenario, I'm also aware of being able to manual add bundles when navigating using .navigate.
Is there a way for me to set in my ClientFragment the arguments for each of these fragments to be
viewModel.client
Update:
My argument issue was solved by using a view model that's shared between all of the fragments in the BottomNavigationView (I realised this as I was typing the issue out to my friend) and the navigation itself I added this to the ClientFragment;
bottom_nav_client.setupWithNavController(
Navigation.findNavController(
view.findViewById<View>(R.id.fl_client_nav_container)
)
)
and my xml for fragment_client;
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar_client"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="?attr/NavigationBackIconLight"
app:titleTextColor="#color/white" />
<fragment
android:id="#+id/fl_client_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#id/bottom_nav_client"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbar_client"
app:navGraph="#navigation/client_nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav_client"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?android:attr/windowBackground"
app:itemBackground="#color/colorPrimary"
app:itemIconTint="#drawable/bottom_nav_color"
app:itemTextColor="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/client_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
This is combined with the same navigation graph and menu as shown above.
The Codelabs referred to in the accepted answer don't mention passing arguments to fragments in the BottomNavigationView.
Override the OnNavigationItemSelectedListener set by the setupWithNavController() with a custom one:
val args = Bundle()
bottomNavigationView.setupWithNavController(navController)
bottomNavigationView.setOnNavigationItemSelectedListener { item ->
navController.navigate(item.itemId, args)
true
}
if you are using NavigationComponent :
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
NavigationUI.onNavDestinationSelected(menuItem, navController);
}
you use this function, inside onNavDestiationSelected function you can see that navigation component passes null to args, so if you wanna pass argument to bottomNavigation fragments, simply you can write navigation function byyourselft and the only change is pass your arguments. as follow:
create a package level function :
fun onNavDestinationSelected(item: MenuItem, navController: NavController, args: Bundle?): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = true
)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, args, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.hierarchy?.any { it.id == item.itemId } == true
} catch (e: IllegalArgumentException) {
false
}
}
and use it inside onNavigationItemSelected function as follow :
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
OnNavDestinationSelectKt.onNavDestinationSelected(menuItem, navController, yourArgument);
}
The Navigation Architecture Component documentation shows how to define destination arguments, in your concrete case you should create a custom Parcelable class (i.e. Client) and include it as an argument of the corresponding fragment.
<fragment
android:id="#+id/clientCustomersFragment"
android:name="com.management.engineering.alarm.alarmengineermanagement.features.client.ClientCustomersFragment"
android:label="ClientCustomersFragment"
tools:layout="#layout/fragment_client_customers" >
<action
android:id="#+id/client_to_details"
app:destination="#+id/clientDetailsFragment" />
</fragment>
<fragment
android:id="#+id/clientDetailsFragment"
android:name="com.management.engineering.alarm.alarmengineermanagement.features.client.ClientDetailsFragment"
android:label="ClientDetailsFragment"
tools:layout="#layout/fragment_client_details">
<argument
android:name="client"
app:argType="com.management.engineering.alarm.alarmengineermanagement.features.client.Client" />
</fragment>
The 'androidx.navigation.safeargs' gradle plugin will generate the classes ClientToDetails and ClientDetailsFragmentArgs which can be used to pass.retrieve the parameter client.
Source
val client: Client = TODO()
val navController: NavController = TODO()
navController.navigate(ClientToDetails(client))
Destination
val client = ClientDetailsFragmentArgs.fromBundle(arguments).client
This codelabs exactly what you want to do :
https://codelabs.developers.google.com/codelabs/android-navigation/index.html?index=..%2F..index#0
Docs : https://developer.android.com/topic/libraries/architecture/navigation/