Two-Way Communication Between Fragments in a Multi-Module Android Project while maintaining loose coupling

Digvijay Singh
DigDroid
5 min readJun 24, 2023

--

In Android development, fragments are self-contained, modular components that contribute their UI to an activity. They are independent and reusable parts that make our applications scalable and maintainable.

However, with the advent of complex, multi-module Android projects when dealing with multiple fragments, a typical challenge arises in maintaining “loose coupling” while allowing for communication between these fragments, which are unaware of each other.

A Modular Communication Strategy

In this article, we will explore a technique of abstracting communication channels to establish two-way communication between fragments by using abstract classes to define a standard contract for the interactions.

In our scenario, we can create an abstract CommunicatorFragment class, extending from the base Fragment class in Android, with defined methods for sending and receiving data.

Using CommunicatorFragment as a base class.

abstract class CommunicatorFragment : Fragment() {
abstract fun processReceivedData(key: String, value: Any)
abstract fun registerDataCallback(callback: (key: String, value: Any) -> Unit)
}

This CommunicatorFragment class lays the blueprint for our communication strategy. The processReceivedData method can receive data, while the registerDataCallback function can handle incoming data using a lambda function (callback) as an argument.

How does this work?

Any class that extends CommunicatorFragment must implement these two methods.

1. Process Received Data

processReceivedData(key: String, value: Any)

A function is implemented to receive data in a fragment. The way to send the data is not specified in the abstract class but must be provided by the subclasses.

2. Register Data Callbacks

registerDataCallback(callback: (key: String, value: Any) -> Unit)

This function accepts a lambda function (callback) which takes two parameters (key and value) and doesn’t return anything (Unit). This function is supposed to set a callback to be invoked when data is received.

Implementing Communication in Fragment

Given the scenario, we have two modules. Module A with fragmentA and Module B withfragmentB.

Both of these fragments are inflated in another Fragment or Activity, and fragmentAneeds to pass data to fragmentBupon some user action without knowing there is a fragmentB.

Both fragments extend the CommunicatorFragment class, which exists in a common module and implements the processReceivedData & registerDataCallback Methods.

Here is a simplified example of how fragmentA can implement these methods:

package com.example.moduleA
import com.example.utility.CommunicatorFragment

...

class FragmentA : CommunicatorFragment(){

private lateinit var dataCallback: (key: String, value: Any) -> Unit

override fun processReceivedData(key: String, value: Any) {
// do something with received Data
}


override fun registerDataCallback(callback: (key: String, value: Any) -> Unit) {
dataCallback = callback
}

...

// Invoke the callback function when ready
// For example, on some UI event like onClick
dataCallback.invoke(key, value)


}

Similarly in fragmentB

package com.example.moduleB
import com.example.utility.CommunicatorFragment

...

class FragmentB : CommunicatorFragment(){

private lateinit var dataCallback: (key: String, value: Any) -> Unit

override fun processReceivedData(key: String, value: Any) {
// do something with received Data
}

override fun registerDataCallback(callback: (key: String, value: Any) -> Unit) {
dataCallback = callback
}


...
}

Let’s see it in action

We will now see Different examples of how we can communicate one-way or two-way.

Example OneSend Data Directly

fragmentA.processReceivedData(key, value)

This line is directly calling processReceivedData function on fragmentA with the given key and value. It's a direct way of sending data to the fragmentA to be processed.

Example Two — Receive Data from a fragment

fragmentB.registerDataCallback { key, value ->
//do something with the data
}

Here, Activity/ Parent Fragmentregisters a callback on fragmentB using its registerDataCallback function. See dataCallback.invoke(key,value)in the code above, for fragmentA.

This means that the code inside the lambda function will be executed when it receives data and calls this callback. You could process the data right here in MainActivity.

Example Three — Communication from One Fragment to Another Directly

fragmentA.registerDataCallback { key, value ->
//pass the received data to fragmentB
fragmentB.processReceivedData(key, value)
}

This code registers a callback on fragmentA using the registerDataCallback function. When fragmentA receives data and calls this callback, it immediately sends this data to fragmentB by calling fragmentB's processReceivedData method. This is an example of directly passing data from one fragment to another.

Let’s Put Everything Together

This Kotlin code showcases different ways to facilitate communication between fragments within an Android application using Activity as a medium to handle interactions.

package com.example.moduleC
import com.example.moduleA.FragmentA
import com.example.moduleB.FragmentB

...

class MainActivity : AppCompatActivity() {

private lateinit var fragmentA : FragmentA
private lateinit var fragmentB : FragmentB
...

//Example one - Send data Directly
fragmentA.processReceivedData(key, value)

...

//Example two - recive data from fragmentB
fragmentB.registerDataCallback { key, value ->
//do something with the data
}

...

//Example three - recive data from one fragment and pass to another Directly
fragmentA.registerDataCallback { key, value ->
//pass the recived data to fragmentB
fragmentB.processReceivedData(key, value)
}

...

}

Get the Code

Here is a sample app on GitHub for you to try.

Sample App

https://github.com/digvijayrajat/PunBridge

Conclusion

This architecture promotes clean, maintainable, and scalable code. The fragments are unaware of each other, reducing tight coupling and improving code modularity.

Creating a two-way communication system between fragments that are not aware of each other might seem daunting at first, but with the right approach and tools, it can be achieved smoothly.

This approach also simplifies testing, as each fragment can be tested independently. When you need to add a new fragment, have it extend CommunicatorFragment and implement the methods. Your new fragment is ready to participate in the communication pipeline without changing existing fragments.

Remember that this is just a simple and quick guide. There are also other ways to achieve the same result, depending on the structure and requirements of your application. Always choose the solution that best fits best with your project’s needs.

--

--