-
-
Notifications
You must be signed in to change notification settings - Fork 9
Experiment #2: Single Activity with standalone UI components
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 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:
- Inflates the root view (I'm using ViewBinding)
- 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
I have two categories of UI components:
- Screen: controls the app navigation (there's only one screen per app)
- 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
It's very simple to navigate between sections using a ViewFlipper
:
- Create an
enum
with all your sections (order is important here) - Create the extension functions
ViewFlipper.navigate()
andViewFlipper.currentSection
- 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.
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.
I really liked the result of this experiment, it looks very extensible so far. Can't wait to use it on a bigger project!