Skip to content

Experiment #2: Single Activity with standalone UI components

Adriel Café edited this page Aug 25, 2020 · 6 revisions

I agree that Activities and Fragments aren't UI elements, and based on this nice series of posts about MVC I came up with the following rules:

  • Activities must not contain any UI logic
  • UI components must be standalone classes (not a Fragment or custom view)
  • UI components must be independent from each other
  • Reusability is not the focus here (see Adapter DSL with reusable UI components)

Single Activity app

Single Activity apps aren't the focus of this experiment, but are part of it. I'm using a ViewFlipper to switch between views and a BottomAppBar to control the navigation:

<androidx.coordinatorlayout.widget.CoordinatorLayout>

    <ViewFlipper>

        <include
            android:id="@+id/wallpapersSection"
            layout="@layout/section_wallpapers"/>

        <include
            android:id="@+id/settingsSection"
            layout="@layout/section_settings"/>

        <include
            android:id="@+id/aboutSection"
            layout="@layout/section_about"/>

    </ViewFlipper>

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/navigation"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Full implementation on screen_home.xml

My Activity is pretty simple, it just:

  1. Inflates the root view (I'm using ViewBinding)
  2. Initializes the UI components
class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ScreenHomeBinding.inflate(inflater).apply {
            setupScreen(this)
            setContentView(root)
        }
    }

    private fun setupScreen(binding: ScreenHomeBinding) {
        HomeScreen(binding)

        WallpapersSection(binding.wallpapersSection)
        SettingsSection(binding.settingsSection)
        AboutSection(binding.aboutSection)
    }
}

Full implementation on HomeActivity.kt

Standalone UI components

I have two categories of UI components:

  1. Screen: controls the app navigation (there's only one screen per app)
  2. Section: controls its own UI, they are located inside the ViewFlipper (we can have multiple sections per app)

My UI component is a standalone class that uses a ViewBinding instance to control the UI. I inject functionalities into it (composition), that way I can keep it small and easy to maintain.

class HomeScreen(private val binding: ScreenHomeBinding) {

    init {
        setupViews()
    }

    private fun setupViews() {
        binding.navigation.apply {
            // Setup
        }
    }
}

Full implementation on HomeScreen.kt

Navigation

It's very simple to navigate between sections using a ViewFlipper:

  1. Create an enum with all your sections (order is important here)
  2. Create the extension functions ViewFlipper.navigate() and ViewFlipper.currentSection
  3. There's no step #3, It's done!
enum class ScreenSection {
    WALLPAPERS,
    SETTINGS,
    ABOUT
}

val ViewFlipper.currentSection : ScreenSection
    get() = ScreenSection.values()[displayedChild]

fun ViewFlipper.navigate(section: ScreenSection) {
    if (displayedChild != section.ordinal) {
        displayedChild = section.ordinal
    }
}

class HomeScreen(private val binding: ScreenHomeBinding) {

    private fun navigateTo(section: ScreenSection) {
        binding.sectionContainer.navigate(section)
    }
}

Of course, this is a minimalist implementation, but it fits all my app needs. If you need more features (routes, backstack, deeplink) take a look at these great libraries: cicerone, simple-stack, scene, magellan.

Communication

I said before that UI components should be independent of each other, but they can still communicate indirectly. Some options are: Observer pattern (Custom Listeners, Observable) or Publish-Subscribe pattern (a.k.a EventBus). I'm using Broker for that:

class HomeViewModel(
    private val toggleFavorite: ToggleFavoriteInteractor,
    private val eventPublisher: BrokerPublisher
) : ViewModel() {
    
    fun toggleFavorite(wallpaper: Wallpaper) {
        val favorite = toggleFavorite(wallpaper)

        // HomeScreen's ViewModel publishes an event
        eventPublisher.publish(WallpaperEvent.FavoriteChanged(wallpaper, favorite))
    }
}

class WallpapersSection(private val binding: SectionWallpapersBinding) : KoinComponent {

    private val eventSubscriber by inject<BrokerSubscriber>()

    init {
        setupViews()
        setupListeners()
    }

    private fun setupListeners() {
        // WallpapersSection subscribes to that event
        eventSubscriber.subscribe<WallpaperEvent.FavoriteChanged>(binding.activity) { event ->
            // Do something
        }
    }
}

If you decide to use a Pub-Sub/EventBus like the example above, please read this great article on this powerful pattern.

Conclusion

I really liked the result of this experiment, it looks very extensible so far. Can't wait to use it on a bigger project!