Best Practices for Reusing ViewModel Logic Across Multiple Android Screens
This article examines common challenges of sharing user interaction logic across multiple Android screens using ViewModel, compares four reuse strategies—including base class inheritance, helper classes, Kotlin class delegation, and interface default functions—and recommends the interface default‑function approach as the most flexible and maintainable solution.
Introduction
In projects that use ViewModel, it is common to encounter a situation where multiple pages need to display the same user component, requiring each page's ViewModel to handle identical user interactions (e.g., like, comment, share in a social app). Writing the same code in each ViewModel quickly leads to duplication, making the codebase hard to maintain and scale.
Problem Background: Shared User Interaction Across Pages
Assume a social app where several pages need to handle actions on a post (click, like, share) using an MVI architecture. A sealed interface defines these actions:
sealed interface PostAction {
data class Clicked(val id: String) : PostAction
data class LikeClicked(val id: String) : PostAction
data class ShareClicked(val id: String) : PostAction
}There is also a BaseViewModel class that provides common functionality for all ViewModels:
abstract class BaseViewModel : ViewModel() {
var showShackBar by mutableStateOf("")
var showBottomSheet by mutableStateOf("")
fun navigate() { /* Implementation */ }
fun showSnackbar(message: String) { showSnackBar = message }
}When multiple pages need to share PostAction logic, how should we think about reusing that logic?
Solution 1: Add a ViewModel Base Class
The simplest idea is to create a new base class under BaseViewModel that handles PostAction :
open class BasePostViewModel : BaseViewModel() {
fun handleAction(action: PostAction) = when (action) {
is PostAction.Clicked -> navigate()
is PostAction.LikeClicked -> showSnackBar("Liked")
is PostAction.ShareClicked -> { /* Implementation */ }
}
}Each page ViewModel that needs PostAction can inherit from BasePostViewModel . This works for simple apps, but as the feature set grows the drawbacks of deep inheritance become apparent.
Solution 2: Use a Helper Class
Create a separate helper class that encapsulates PostAction handling and add it as a property to the ViewModel:
class PostActionHandler(private val viewModel: BaseViewModel) {
fun handleAction(action: PostAction) = when (action) {
is PostAction.Clicked -> viewModel.navigate()
is PostAction.LikeClicked -> viewModel.showSnackBar("Liked")
is PostAction.ShareClicked -> { /* Implementation */ }
}
}In each screen ViewModel you instantiate the helper: class PostScreenViewModel : BaseViewModel() { val actionHandler = PostActionHandler(this) } @Composable fun PostScreen(viewModel: PostScreenViewModel) { PostItem(onAction = viewModel.actionHandler::handleAction) } @Composable fun PostItem(onAction: (PostAction) -> Unit) { /* UI */ } Advantages: no extra inheritance level. Drawbacks: the helper cannot be overridden (lack of flexibility) and callers must expose the helper property, violating the Law of Demeter. Solution 3: Use Kotlin Class Delegation Define an interface for the action handling and provide a default implementation class: interface PostActionHandler { suspend fun handleAction(action: PostAction) } class PostActionHandlerImpl : PostActionHandler { override suspend fun handleAction(action: PostAction) = when (action) { is PostAction.Clicked -> handlePostClick(action.id) is PostAction.LikeClicked -> handleLikeClick(action.id) is PostAction.ShareClicked -> handleShareClick(action.id) } private fun handlePostClick(id: String) { } private fun handleLikeClick(id: String) { } private fun handleShareClick(id: String) { } } class PostViewModel : BaseViewModel(), PostActionHandler by PostActionHandlerImpl() { @Composable fun PostScreen(viewModel: PostViewModel) { PostItem(onAction = viewModel::handleAction) } @Composable fun PostItem(onAction: (PostAction) -> Unit) { /* UI */ } } This solves the flexibility issue of the helper class, but class delegation cannot pass the this reference of the ViewModel, so the delegated implementation cannot access BaseViewModel functions or ViewModelScope. Solution 4: Use Kotlin Interface Default Functions (Kotlin 1.8+) Interface default functions allow us to reference the owning ViewModel directly. Add a viewModel property to the interface and implement the logic using it: interface PostActionHandler { val viewModel: BaseViewModel fun handleAction(action: PostAction) = viewModel.viewModelScope.launch { when (action) { is PostAction.Clicked -> handlePostClick(action.id) is PostAction.LikeClicked -> handleLikeClick(action.id) is PostAction.ShareClicked -> handleShareClick(action.id) } } suspend fun handlePostClick(id: String) { viewModel.navigate() } suspend fun handleLikeClick(id: String) { viewModel.showToast("Liked") } suspend fun handleShareClick(id: String) { /* Implementation */ } } class PostViewModel : BaseViewModel(), PostActionHandler { override val viewModel = this // Optionally override specific functions } @Composable fun PostScreen(viewModel: PostViewModel) { PostItem(onAction = viewModel::handleAction) } @Composable fun PostItem(onAction: (PostAction) -> Unit) { /* UI */ } The interface can also expose other dependencies such as a repository or use‑case, allowing the handler to launch coroutines and perform data operations while still having full access to ViewModel capabilities. Practical Example (Solution 4) Assume a home page that displays a mixed list of posts and news items. Define sealed actions for both, create corresponding handler interfaces, and let HomeViewModel implement them: sealed interface PostAction { /* Clicked, LikeClicked, ShareClicked */ } sealed interface NewsAction { /* Clicked, LikeClicked, BookmarkClicked */ } interface PostActionHandler { val viewModel: BaseViewModel val postRepo: PostRepository fun handleAction(action: PostAction) = viewModel.viewModelScope.launch { /* ... */ } // default implementations for each action } interface NewsActionHandler { val viewModel: BaseViewModel val newsRepo: NewsRepository fun handleAction(action: NewsAction) = viewModel.viewModelScope.launch { /* ... */ } } class HomeViewModel : BaseViewModel(), PostActionHandler, NewsActionHandler { override val viewModel = this override val postRepo = PostRepository() override val newsRepo = NewsRepository() // Rest of ViewModel code } @Composable fun HomeScreen(viewModel: HomeViewModel) { LazyColumn { items(viewModel.items) { when (it) { is News -> NewsCard(it, onAction = viewModel::handleAction) is Post -> PostCard(it, onAction = viewModel::handleAction) } } } } @Composable fun NewsCard(news: News, onAction: (NewsAction) -> Unit) { /* UI */ } @Composable fun PostCard(post: Post, onAction: (PostAction) -> Unit) { /* UI */ } This design lets each ViewModel inherit multiple handler interfaces, keeping responsibilities single while providing full access to ViewModel functions, repositories, and coroutine scopes. Summary The article explored four solutions for sharing user‑interaction logic in ViewModels: Class inheritance – simple but limited to single inheritance. Helper class – easy to use but lacks overridability. Class delegation – avoids extra inheritance but cannot pass this to access ViewModel features. Interface default functions – offers the best balance of flexibility, reusability, and access to ViewModel capabilities. A comparison table highlights key characteristics such as multi‑inheritance support, overridability, and VM accessibility, confirming that interface default functions are the most suitable approach for complex scenarios. Feature Class Inheritance Helper Class Class Delegation Interface Default Functions Multiple inheritance ❌ ✅ ✅ ✅ Overridable ✅ ❌ ✅ ✅ VM access ✅ ✅ ❌ ✅ Recommended scenario Simple pages with limited components Highly consistent component behavior across pages Business‑logic‑centric reuse with minimal UI interaction Complex scenarios requiring flexibility and multi‑inheritance Overall, the interface default‑function solution satisfies the single‑responsibility principle for each ActionHandler while allowing ViewModels to inherit multiple handlers, making it the most maintainable choice. Recommended Reading SOLID Design Principles in Android Practice Molecule – Let Compose Break Free from ViewModel Kotlin 1.5 New Feature: What Are Sealed Interfaces Good For?
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.