DEV Community

Khoa Pham
Khoa Pham

Posted on

Communication between Fragment and Activity

Original post https://github.com/onmyway133/blog/issues/108

There's always need for communication, right 😉 Suppose we have OnboardingActivity that has several OnboardingFragment. Each Fragment has a startButton telling that the onboarding flow has finished, and only the last Fragment shows this button.

Here are several ways you can do that

1. EventBus 🙄

Nearly all articles I found propose this https://github.com/greenrobot/EventBus, but I personally don't like this idea because components are loosely coupled, every component and broadcast can listen to event from a singleton, which makes it very hard to reason when the project scales

data class OnboardingFinishEvent()
Enter fullscreen mode Exit fullscreen mode
class OnboardingActivity: AppCompatActivity() {
    override fun onStart() {
        super.onStart()

        EventBus.getDefault().register(this)
    }

    override fun onStop() {
        EventBus.getDefault().unregister(this)
        super.onStop()
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onOnboardingFinishEvent(event: OnboardingFinishEvent) {
        // finish
    }
}
Enter fullscreen mode Exit fullscreen mode
class OnboardingFragment: Fragment() {
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        startButton.onClick {
            EventBus.getDefault().post(OnboardingFinishEvent())
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Read more

2. Otto 🙄

This https://github.com/square/otto was deprecated in favor of RxJava and RxAndroid

3. RxJava 🙄

We can use simple PublishSubject to create our own RxBus

import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject

// Use object so we have a singleton instance
object RxBus {

    private val publisher = PublishSubject.create<Any>()

    fun publish(event: Any) {
        publisher.onNext(event)
    }

    // Listen should return an Observable and not the publisher
    // Using ofType we filter only events that match that class type
    fun <T> listen(eventType: Class<T>): Observable<T> = publisher.ofType(eventType)
}
Enter fullscreen mode Exit fullscreen mode
// OnboardingFragment.kt
startButton.onClick {
    RxBus.publish(OnboardingFinishEvent())
}
Enter fullscreen mode Exit fullscreen mode
// OnboardingActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    RxBus.listen(OnboardingFinishEvent::class.java).subscribe({
        // finish
    })
}
Enter fullscreen mode Exit fullscreen mode

4. Interface

This is advised here Communicating with Other Fragments. Basically you define an interface OnboardingFragmentDelegate that whoever conforms to that, can be informed by the Fragment of events. This is similar to Delegate pattern in iOS 😉

interface OnboardingFragmentDelegate {
    fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment)
}

class OnboardingFragment: Fragment() {
    var delegate: OnboardingFragmentDelegate? = null

    override fun onAttach(context: Context?) {
        super.onAttach(context)

        if (context is OnboardingFragmentDelegate) {
            delegate = context
        }
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        startButton.onClick {
            delegate?.onboardingFragmentDidClickStartButton(this)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
    override fun onboardingFragmentDidClickStartButton(fragment: OnboardingFragment) {
        onboardingService.hasShown = true
        startActivity<LoginActivity>()
    }
}
Enter fullscreen mode Exit fullscreen mode

5. ViewModel

We can learn from Share data between fragments to to communication between Fragment and Activity, by using a shared ViewModel that is scoped to the activity. This is a bit overkill

class OnboardingSharedViewModel: ViewModel() {
    val finish = MutableLiveData<Unit>()
}
Enter fullscreen mode Exit fullscreen mode
class OnboardingActivity: AppCompatActivity(), OnboardingFragmentDelegate {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModel = ViewModelProviders.of(this).get(OnboardingSharedViewModel::class.java)

        viewModel.finish.observe(this, Observer {
            startActivity<LoginActivity>()
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that we need to call ViewModelProviders.of(activity) to get the same ViewModel with the activity

class OnboardingFragment: Fragment() {
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewModel = ViewModelProviders.of(activity).get(OnboardingSharedViewModel::class.java)
        startButton.onClick({
            viewModel.finish.value = Unit
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Lambda

Create a lambda in Fragment, then set it on onAttachFragment. It does not work for now as there is no OnboardingFragment in onAttachFragment 😢

class OnboardingFragment: Fragment() {
    var didClickStartButton: (() -> Unit)? = null

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        startButton.onClick {
            didClickStartButton?.invoke()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
class OnboardingActivity: AppCompatActivity() {
    override fun onAttachFragment(fragment: Fragment?) {
        super.onAttachFragment(fragment)

        (fragment as? OnboardingFragment).let {
            it?.didClickStartButton = {
                // finish
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Listener in Bundle 🙄

Read more

Top comments (2)

Collapse
 
sindrenm profile image
Sindre Moen

If I had a direct reference to the OnboardingFragment somewhere in my activity, I suppose I would prefer the lambda. However, through onAttachFragment you'd need to check the type, which I feel looks kind of ugly.

Also, with the interface, I'd instead have a setter on my fragment instead of checking the type of context. That way, the listener doesn't necessarily need to be an Activity or Context, but anything that implements the interface.

Collapse
 
nottylerjames profile image
Tyler Healey

Have you found a clean way of implementing a Coordinator/Flow Controller pattern on Android? Seems like you'd have one Activity as the flow controller and multiple fragments that communicate back to the Activity. Since the Lambda doesn't work I suppose the delegate pattern would be best.

As a side note I should really get into Kotlin, it's so Swift-like that it took me a few seconds to realize this was an Android article.