Android SharedPreferences Tutorial and Example

Communicating between fragments with interface in Android

The Android framework gives developers a powerful way to compose screens using fragments, but the moment two fragments need to share data, things get tricky. New learners in Brisbane's growing mobile dev community often hit a wall when they realise fragments cannot talk to each other directly without violating good architecture principles.

A clean, time-tested pattern solves this problem using a plain interface implemented by the host activity. This approach decouples fragments, keeps them reusable, and survives configuration changes that trip up less thoughtful designs.

Across Sydney and Melbourne, where Android teams ship apps for surf-club platforms and fintech startups alike, this interface pattern is a staple of code reviews. It mirrors conventions in the wider Australian tech sector, making it easier to onboard junior engineers from local universities.

The rest of this article walks through the contract, the host activity, the sender, and the receiver, then explores how the same idea pairs with the android-viewpager-image-slideshow example and modern alternatives.

Why fragments need a mediator

Direct fragment-to-fragment references are a recipe for tight coupling. When fragment A holds a reference to fragment B, neither can be reused in a different screen, tested in isolation, or swapped out for a tablet layout. Australian teams that maintain legacy apps for clients in Adelaide and Perth know how painful these hidden dependencies become during refactors.

The host activity is always available during a fragment's lifetime, and it acts as a natural mediator. By defining a contract that the activity must implement, fragments can broadcast events upward without knowing who is listening. This is the same idea used in observer patterns across libraries maintained by the Sydney Google Developer Group.

It also keeps lifecycle bugs at bay. Fragments disappear and reappear during rotation, back-stack pops, and process restarts. An interface queried inside onAttach and released inside onDetach automatically follows the fragment's actual visibility, which is far more reliable than reaching for a sibling view that may have been destroyed.

Defining the contract interface

A contract is usually a small Java or Kotlin file with a single method signature. For a list-detail scenario, you might declare onItemSelected(String id) and let the activity decide what to do with the id. Keeping the method focused on a single piece of data avoids the god-listener anti-pattern that troubles older Australian banking apps.

Naming matters more than developers in Hobart or Canberra often realise. Use verbs that describe the event, not the action a specific fragment should take. The sender does not care whether the receiver opens a detail page, fires analytics, or updates a toolbar; the contract simply reports the event and trusts the activity to route it.

Place the interface in its own file, or alongside the receiving fragment as a nested type. Either way, it should not live inside the sending fragment, because doing so would invert the dependency and recreate the coupling the pattern is meant to remove.

Implementing the contract in the host activity

The activity declares that it implements the contract and overrides the method to act on incoming data. In a typical detail flow, the activity finds the receiving fragment by tag, calls a public method on it, or uses the FragmentManager to replace the current view. This routing step is where most of the real logic lives.

class MainActivity : AppCompatActivity(), OnItemSelectedListener {
    override fun onItemSelected(id: String) {
        val detail = supportFragmentManager.findFragmentById(R.id.detail_container) as? DetailFragment
        detail?.showItem(id) ?: addDetailFragment(id)
    }
}

The example assumes a phone layout with a single container; tablets with side-by-side panes simply call showItem on the already-attached fragment. This branching is invisible to the sender, which is the point. Localising the layout logic to the activity keeps the fragments portable across AU and NZ form factors and screen densities.

Wiring up the sending fragment

The sending fragment stores a nullable reference to the listener and assigns it in onAttach, clearing it in onDetach. The cast inside onAttach should throw a clear exception if the host activity does not implement the contract; this fail-fast behaviour saves hours of debugging when the fragment is reused in a new activity by mistake.

class ListFragment : Fragment() {
    private var listener: OnItemSelectedListener? = null

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = context as? OnItemSelectedListener
            ?: throw IllegalStateException("Host must implement OnItemSelectedListener")
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }
}

When the user taps a row, the fragment invokes the listener. No knowledge of fragments, activities, or navigation graphs leaks into this code, so the same ListFragment can be dropped into any screen in any Australian app, from a regional tourism guide to a Melbourne-based loyalty program.

Receiving data in the target fragment

The receiving fragment exposes a public method that the activity calls. That method updates the view, fetches data, or triggers a loader. Because the method is invoked from the activity, the receiving fragment never imports the sending fragment, which keeps the dependency graph acyclic.

Configuration changes are a common source of broken callbacks. If the activity recreates after rotation, the receiving fragment is also recreated, and the activity re-routes the latest event to the new instance. Developers in Queensland's tourism sector, where apps run on flaky regional 4G, often debug this path after a late-returning network call.

For a richer example, a paging scenario shows how multiple child fragments can listen for selection events from a shared parent. The pattern scales naturally to ViewPager and ViewPager2, as long as each page binds and unbinds its listener in the correct lifecycle methods.

Alternatives, pitfalls, and a quick decision guide

The interface pattern is not the only option. Shared ViewModels, owned by the activity, let fragments observe a LiveData or StateFlow stream. They shine when the same data must drive many observers, such as a filter panel updating a list and a map in tandem. Bundles passed via setArguments work for one-shot initialisation but are awkward for events that fire after the fragment is created. Event bus libraries decouple senders and receivers at the cost of indirection that is hard to trace. Direct activity calls are acceptable when a fragment is guaranteed to live only inside one parent.

Several issues catch developers repeatedly. Storing the listener in a field assigned during onCreate instead of onAttach causes nulls after rotation. Casting to a specific activity class defeats the purpose of the interface. Forgetting onDetach leaves a reference to a destroyed context, which is a memory leak that often shows up only on lower-end devices common in regional Australian markets.

Here is how the most common options compare and where they fit best.

Approach Coupling Lifecycle safety Best for
Interface callback Loose, activity-mediated Strong when wired in onAttach and onDetach Click events, user-driven messages
Shared ViewModel Loose, scoped to activity Strong with LiveData or StateFlow Multiple observers, reactive UI
Fragment Result API Loose, FragmentManager-backed Strong, survives back stack Fragment-to-fragment handoff
Event bus library Very loose Weak, hard to trace Legacy code, rapid prototyping
setArguments bundle Tight, parent-to-child Strong for init only One-time setup values

Habits that keep the pattern healthy

  • Bind in onAttach, unbind in onDetach
  • Keep the interface in its own file or alongside the receiver
  • Throw on missing implementation to fail fast
  • Pass data, not actions, across the boundary

Questions before adopting a third approach

  • Does the new method survive configuration changes out of the box?
  • Can the sender and receiver be unit-tested in isolation?
  • Does the pattern require extra libraries the rest of the codebase does not already use?
  • Is the team in Sydney, Melbourne, or offshore already familiar with the concept?