Search code examples
androiduser-interfacekotlinmvvmandroid-viewmodel

Populate UI from a ViewModel in Kotlin


This is the first time I am using Android Architecture. I have decided to go with MVVM structure but I am stuck at the point where I have no idea how to set the textViews in my XML with the data I pull from the database.

Checking the logs, I have seen that the function calls to my database (Firestore) does retrieve the correct data documents. Do I set the UI elements from the Activity,ViewModel or the Fragment? (Please note that I'm using a Navigation bar and controller)

Please assist me, my code is as follows:

My Single Activity:

class HomeActivity : AppCompatActivity() {

    // Create the three objects for the fragments
    lateinit var homeFragment: HomeFragment
    lateinit var visitsFragment: VisitsFragment
    lateinit var profileFragment: ProfileFragment

    // ViewModels
    lateinit var customerViewModel: CustomerViewModel



    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_home)

        // Initialize the bottom nav bar and navigation controller and then merge them
        val bottomNavigationView = findViewById<BottomNavigationView>(R.id.btm_nav)
        val navigationController = findNavController(R.id.fragmentHost)
        bottomNavigationView.setupWithNavController(navigationController)
        // Create app bar config object so that you can rename the bar ontop with the tab name
        val appBarConfiguration = AppBarConfiguration(setOf(R.id.homeFragment,R.id.visitsFragment,R.id.profileFragment))
        setupActionBarWithNavController(navigationController,appBarConfiguration)

        // View Model
        customerViewModel = ViewModelProvider(this).get(CustomerViewModel::class.java)
        customerViewModel.retrieveCustomer().observe(this, Observer { it })
    }


    // This function creates the menu object by inflating it with the resource we gave it (menu resource).
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_main,menu);
        return true
    }
    // This function checks which menu item was selected and performs the task associated with the item selected.
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val id = item.itemId;

        // If Log Out menu item was selected
        if (id == R.id.menuLogOut){
            // Sign the user out
            FirebaseAuth.getInstance().signOut()
            // Finish this activity
            finish()
            // Start the initial activity
            val intent = Intent(this, MainActivity::class.java)
            startActivity(intent)
            // Display the message to the user
            Toast.makeText(this, "Successfully Signed Out", Toast.LENGTH_SHORT).show()
            return true
        }

        return super.onOptionsItemSelected(item)
    }
}

My View Model:

class CustomerViewModel: ViewModel() {

    val TAG = "CustomerViewModel"
    var db = Firebase.firestore
    var user = FirebaseAuth.getInstance().currentUser

    var liveData = MutableLiveData<List<Customer>>()
    var cusArray = arrayListOf<Customer>()
    var docRef = user?.uid

    fun retrieveCustomer(): MutableLiveData<List<Customer>>
    {
        db.collection("users").document(docRef.toString())
            .get()
            .addOnSuccessListener { document ->
                if (document != null)
                {
                    val data = document
                    // Set the data
                    val name = data.get("name") as String
                    val surname = data.get("surname") as String
                    val email = data.get("email") as String
                    val contactNo = data.get("contact no") as String

                    val customer = Customer(name, surname, email, contactNo)
                    cusArray.add(customer)

                    liveData.value = cusArray
                     Log.d(TAG, "DocumentSnapshot data: ${document.data}")
                }
                else
                {
                    Log.d(TAG, "No such document")
                }
            }
            .addOnFailureListener { exception ->
                Log.d(TAG, "get failed with " + exception.message, exception)
            }
        return liveData

    }

}

My Object Class

package com.CleanWheels.cleanwheels.DataClasses

data class Customer(
    val name: String?,
    val surname: String?,
    val email: String?,
    val contactNo: String?
)

My XML file (profile_fragment):

<?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"
    tools:context=".Fragments.ProfileFragment">

    <TextView
        android:id="@+id/banner"
        android:layout_width="499dp"
        android:layout_height="290dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_marginStart="-6dp"
        android:layout_marginTop="0dp"
        android:layout_marginEnd="0dp"
        android:background="@color/colorPrimary"
        android:layout_marginLeft="-6dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentLeft="true"
        android:layout_marginRight="0dp" />

    <ImageView
        android:id="@+id/image"
        android:layout_width="154dp"
        android:layout_height="159dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="53dp"
        android:layout_marginEnd="132dp"
        android:layout_marginRight="132dp"
        android:layout_marginBottom="78dp"
        android:src="@drawable/ic_action_profile" />

    <LinearLayout
        android:id="@+id/layout_1"
        android:layout_width="280dp"
        android:layout_height="40dp"
        android:layout_below="@id/banner"
        android:layout_marginLeft="80dp"
        android:layout_marginTop="100dp"
        android:layout_marginRight="20dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Name:"
            android:textSize="18sp"/>

        <TextView
            android:id="@+id/profileNameUI"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="18sp"/>

    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout_2"
        android:layout_width="280dp"
        android:layout_height="40dp"
        android:layout_below="@id/layout_1"
        android:layout_marginLeft="80dp"
        android:layout_marginTop="20dp"
        android:layout_marginRight="20dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Surname:"
            android:textSize="18sp"/>

        <TextView
            android:id="@+id/profileSurnameUI"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="18sp"/>

    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout_3"
        android:layout_width="280dp"
        android:layout_height="40dp"
        android:layout_below="@id/layout_2"
        android:layout_marginLeft="80dp"
        android:layout_marginTop="20dp"
        android:layout_marginRight="20dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Email Address:"
            android:textSize="18sp"/>

        <TextView
            android:id="@+id/profileEmailUI"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="18sp"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout_4"
        android:layout_width="280dp"
        android:layout_height="40dp"
        android:layout_below="@id/layout_3"
        android:layout_marginLeft="80dp"
        android:layout_marginBottom="50dp"
        android:layout_marginTop="20dp"
        android:layout_marginRight="20dp"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Contact No:"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/profileContactUI"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="18sp" />
    </LinearLayout>

<LinearLayout
    android:id="@+id/layout_5"
    android:layout_width="65dp"
    android:layout_height="220dp"
    android:layout_below="@id/banner"
    android:layout_alignParentStart="true"
    android:layout_alignParentLeft="true"
    android:layout_marginStart="0dp"
    android:layout_marginLeft="0dp"
    android:layout_marginTop="90dp"
    android:orientation="vertical">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="0dp"
        android:layout_marginBottom="5dp"
        android:layout_weight="1"
        android:src="@drawable/ic_action_profile_name_ui"/>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp"
        android:layout_weight="1"
        android:src="@drawable/ic_action_profile_surname_ui"/>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp"
        android:layout_weight="1"
        android:src="@drawable/ic_action_profile_email_ui"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="0dp"
        android:layout_weight="1"
        android:src="@drawable/ic_action_profile_contact_ui"/>


</LinearLayout>

The profile_fragment class file:

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup


// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [ProfileFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class ProfileFragment : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_profile, container, false)
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment ProfileFragment.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            ProfileFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

Then finally, the View I am trying to populate enter image description here


Solution

  • You need to move all the logic to ProfileFragment once you navigate to ProfileFragment data will be set. Example:

    ProfileFragment

    class ProfileFragment : Fragment() {
    
    private val customerViewModel: CustomerViewModel by viewModels()
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_profile, container, false)
        val name = view.findViewById<TextView>(R.id.name)
        val surname = view.findViewById<TextView>(R.id.surname)
        val email = view.findViewById<TextView>(R.id.email)
        val contact = view.findViewById<TextView>(R.id.contact)
    
        //calling initially here
        customerViewModel.retrieveCustomer()
        customerViewModel.liveData.observe(viewLifecycleOwner, Observer {
            //customer index at 0
            val customer = it[0]
            name.text = customer.name
            surname.text = customer.surname
            email.text = customer.email
            contact.text = customer.contactNo
        })
    
        return view
    }
    

    CustomerViewModel.kt

    class CustomerViewModel(application: Application) : AndroidViewModel(application) {
    var liveData = MutableLiveData<List<Customer>>()
    
    fun retrieveCustomer(){
        //Your logic to get data from Firebase or any remote or db
        val listOfCustomer = mutableListOf<Customer>()
        val customer = Customer("name", "surname","email", "contqct")
        listOfCustomer.add(customer)
        liveData.postValue(listOfCustomer)
    }
    

    }

    fragment_profile.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Name"
        android:padding="8dp"
        android:textSize="24dp"
        />
    <TextView
        android:id="@+id/surname"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Surname"
        android:padding="8dp"
        android:textSize="24dp"
        />
    <TextView
        android:id="@+id/email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Name"
        android:padding="8dp"
        android:textSize="24dp"
        />
    <TextView
        android:id="@+id/contact"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Name"
        android:padding="8dp"
        android:textSize="24dp"
        />
    </LinearLayout>