How to Effectively Manage Data and Bind UI in Android Applications

How to Effectively Manage Data and Bind UI in Android Applications

Ultimate Guide to View Binding, Shared Preferences, and Data Sharing for Smooth Android Apps

View Binding in Android

View binding is a feature that makes it easier to interact with views.

  • generates a binding class for each XML layout present in the module, activity_main.xml to ActivityMainBinding

  • instance of a binding class contains a direct reference to all views that have an id in the layout.

  • replaces the findViewById method.

Enabling View Binding :

//app level gradle file android
android {
    ...
    buildFeatures {
        viewBinding = true
    }
}

If you want a layout file to be ignored while generating binding classes, add the tools:viewBindingIgnore="true" attribute to the root view of that layout file

<LinearLayout
        ...
        tools:viewBindingIgnore="true" >
    ...
</LinearLayout>

Implementing View Binding :

private lateinit var binding: ResultProfileBinding
//set up instance of the Binding class
//ResultProfile is the module name
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ResultProfileBinding.inflate(layoutInflater)
//inflate() method creates an instance of the binding class for the activity to use.
    val view = binding.root
//getting a reference to the root view
    setContentView(view)
//Pass the root view to setContentView() to present it on screen

    binding.name.text = "hey"
    binding.button.setOnClickListener { .. }
//accessing views by BindingObject.viewID
}

Implementing View Binding in Fragments :

as the fragment’s view lifecycle is different from the activity lifecycle, hence we need to handle the binding objects lifecycle carefully to avoid memory leaks.

//import..
class ExampleFragment : Fragment() {
    private var binding: FragmentExampleBinding? = null
//a Fragment's view lifecycle is shorter
//Declaring binding as nullable (FragmentExampleBinding?) allows us to 
//safely set it to null in onDestroyView() when the view is destroyed.
    private val bind get() = binding!! 
//Non-nullable reference to the binding object,
//instead of writing binding!! every time
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {
        binding = FragmentExampleBinding.inflate(inflater, container, false)
//initialization of binding variable
        return bind.root // Return the root view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Use binding to access views
        bind.textView.text = "Hello, View Binding in Fragment!"
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null // Clear the binding object to avoid memory leaks
    }
}

Shared Preferences & Data Saving in Android

Shared Preferences allows us to save & retrieve data in the form of key:value pair. Used to save small amounts of data that should persist across app sessions, like user preferences, settings, etc.

  • Data belonging to primitive data types(integer, float, boolean, string, long)

  • Shared Preferences files are managed by an Android Framework and can be accessed anywhere within the application to read and write data

Implementation -

//MainActivity.kt
//import..
class MainActivity : AppCompatActivity() {
    lateinit var userName : EditText
    lateinit var userCount : Button
    var name : String? = null
    var count : Int? = null
    var countNum = 0

    lateinit var sharedPreferences : SharedPreferences
    override fun onCreate(savedInstanceState: Bundle?) {
        //statements//
        userName = findViewById(R.id.nameInput)
        userCount = findViewById(R.id.countBtn)
        userCount.setOnClickListener{
            countNum++
            userCount.setText(""+countNum)
        }
    }

    override fun onPause() {
//onPause method is the best time to save the values 
        super.onPause()
        sharedPreferences = this.getSharedPreferences("saveData", Context.MODE_PRIVATE)
//getSharedPrefereces used to get the instance of SharedPreference
//where saveData is the name of the file
//MODE_PRIVATE makes the created file only accessible by the calling application
        name = userName.text.toString()
        count = userCount.text.toString().toInt()

        val editor : Editor = sharedPreferences.edit()
//Editor is used to edit the values in the SharedPreference
        editor.putString("NAME:",name)
        editor.putInt("COUNT:", count!!)
        editor.apply() 
//apply() saves the values

        Toast.makeText(this,"DATA SAVED",Toast.LENGTH_SHORT).show()
    }

    override fun onResume() {
//onResume is the best time to retreive data
//as onCreate isn't called when acitivity regains focus
        super.onResume()

        sharedPreferences = this.getSharedPreferences("saveData", Context.MODE_PRIVATE)

        name = sharedPreferences.getString("NAME:",null)
        count = sharedPreferences.getInt("COUNT:",0)

        userName.setText(name)
        userCount.setText(""+count)
    }
}

Sharing Data between Activities & Fragments

In this article, we will learn how to pass data between different mobile screens in Android.

Sharing Data b/w Activities :

Sharing Data with Intent used for small amounts of temporary data.

//SENDING DATA -> 
intent.putExtra("name",username) 
intent.putExtra("age",userage)
//parameter: (key, value)

//RECEIVING DATA ->
intent.getStringExtra(name)
intent.getIntExtra(age,0)
//parameter: (key, default value)

Sharing Data with Shared Preferences for simple, small data.

// Saving data in Activity A
val sharedPreferences = getSharedPreferences("MyPrefs", MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("KEY_NAME", "John Doe")
editor.apply()

// Retrieving data in Activity B
val sharedPreferences = getSharedPreferences("MyPrefs", MODE_PRIVATE)
val name = sharedPreferences.getString("KEY_NAME", "default_value")

Sharing Data with a Singleton Class for large, complex data.

//SessionManager.kt
object SessionManager {
    var userId: String? = null
    var authToken: String? = null
    fun isLoggedIn() = userId != null && authToken != null

    fun clearSession() {
        userId = null
        authToken = null
    }
}
//set session data : after login
SessionManager.userId = "12345"
SessionManager.authToken = "abcd1234"

//access session data
if (SessionManager.isLoggedIn()) {
    println("User ID: ${SessionManager.userId}")
} else {
    println("Not logged in")
}

ViewModel & LiveData provided by Jetpack Compose, persistent storage methods like Room Database(provided by Jetpack Library) or SQLite Database, using File Methods(JSON, text, etc) can also be used.

Persistent Storage Methods(Shared Preference, Files & Database) are used when data is needed across app restarts.

Sharing Data from Activity to Fragment :

Sharing Data with Bundle used for small amounts of data

//SENDING DATA ->
//needs to be executed before committing the fragment transaction
val fragment = ExampleFragment()
val bundle = Bundle() //creating an object of Bundle class
bundle.putInt("position",position) //passing the tag & data 
fragment.arguments = bundle //binding the bundle to the fragment
//begin fragment transaction

//RECEIVING DATA ->
arguments?.getInt("position",0) //tag & default value is passed

Sharing Data with Interface Callback used for Fragment to Activity Communication

//Define Interface in Fragment
class ExampleFragment : Fragment() {
    //Define an interface for the communication
    interface OnDataPassListener {
        fun onDataPass(data: String)
    }
    // Declare a variable to hold the listener reference
    // listener -> activity that implements interface
    private var dataPassListener: OnDataPassListener? = null

    //Attach the listener when the fragment is attached to the activity
    override fun onAttach(context: Context) {
        super.onAttach(context)
        dataPassListener = context as? OnDataPassListener
    }

    // Method to pass data to the activity
    fun passDataToActivity(data: String) {
        dataPassListener?.onDataPass(data) // Trigger the listener method
    }

    // Example of when you might call passDataToActivity()
    // You could call this method when the user clicks a button, etc.
    // Pass Data 
    fun someAction() {
        val data = "Hello from Fragment!"
        passDataToActivity(data) // Pass data to the activity
    }
}
//Implement Interface in Activity
class MainActivity : AppCompatActivity(), ExampleFragment.OnDataPassListener {
//inherit from Interface
    override fun onCreate(savedInstanceState: Bundle?) {
        //statements..
        // Add fragment dynamically
        if (savedInstanceState == null) {
            val fragment = ExampleFragment()
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, fragment)
                .commit()
        }
    }

    //Implement the onDataPass() method from the fragment's interface
    override fun onDataPass(data: String) {
        // Handle the data passed from the fragment
        Toast.makeText(this, "Data from Fragment: $data", Toast.LENGTH_SHORT).show()
    }
}

Sharing Data with Singleton used for shared data between multiple holders

object DataHolder {
    var sharedData: String? = null
}
//Activity
DataHolder.sharedData = "Hello Fragment"
//Fragment
val data = DataHolder.sharedData

Shared ViewModel provided by Jetpack Compose, Third Party Libraries like EventBus or LiveDataBus can be used.

Sharing Data from Fragment to Activity :

Sharing Data with Activity Object i.e. directly accessing the Activity from the Fragment.

//SENDING DATA from Fragment 
private var activityCallback: MainActivity? = null
override fun onAttach(context: Context) {
    super.onAttach(context)
    activityCallback = activity as? MainActivity
}
fun passDataToActivity() { //pass data to activity
    activityCallback?.gettingData("Hello Activity!", 21)
}

//(activity as MainActivity).gettingData(userName,userAge)

//RECEIVING DATA -> MainActivity.kt
//user-defined function of our activity class
fun gettingData(userName : String, userAge : Int){
//used to receive data from fragment
    val name : String = "Name: $userName"
    val age : String = "Age: $userAge"
}

Sharing Data with Interface Callback used for Fragment & Activity Communication

class ExampleFragment : Fragment() {
    // Define an interface for data communication
    interface OnDataPass {
        fun onDataPass(data: String)
    }

    var dataPasser: OnDataPass? = null
    override fun onAttach(context: Context) {
        super.onAttach(context)
        // Ensure the activity implements the interface
        dataPasser = context as? OnDataPass
    }
    fun passDataToActivity() {
        // Pass data to the activity
        dataPasser?.onDataPass("Hello Activity!")
    }
}
class MainActivity : AppCompatActivity(), ExampleFragment.OnDataPass {
    override fun onDataPass(data: String) {
        println("Received data: $data")
    }
}

Sharing Data b/w Fragments :

Sharing Data by using Activity as a Mediator which involves Fragment to Share Data to the Activity and then another Fragment to receive it from Activity. Helpful when switching layout to the ReceiverFragment with sharing the data.

class FragmentA : Fragment() { //Sender
    var dataPasser: DataPassListener? = null
    interface DataPassListener {
        fun passData(data: String)
    }
    override fun onAttach(context: Context) {
        super.onAttach(context)
        dataPasser = activity as? DataPassListener
    }

    fun sendDataToFragmentB() {
        dataPasser?.passData("Hello Fragment B")
    }
}

class FragmentB : Fragment() {    //Receiver
    private var receivedData: String? = null
    fun setReceivedData(data: String) {
        receivedData = data
    }
}
class MainActivity : AppCompatActivity(), FragmentA.DataPassListener {
    override fun passData(data: String) {
        val fragmentB = supportFragmentManager.findFragmentByTag("FragmentB") as? FragmentB
        fragmentB?.setReceivedData(data)
    }
}

Sharing Data by attaching Bundle during Fragment Call from another Fragment.

class FragmentA : Fragment() {    //sender
    fun passDataToFragmentB() {
        val fragmentB = FragmentB()
        val bundle = Bundle()
        bundle.putString("key", "Hello Fragment B")
        fragmentB.arguments = bundle
        fragmentManager?.beginTransaction()?
            .replace(R.id.fragment_container, fragmentB)?.commit()
    }
}

class FragmentB : Fragment() {    //receiver
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val data = arguments?.getString("key")
        println("Received data: $data")
    }
}

Use Singleton Method, if you don’t want the layout to switch while sharing the data.