Skip to main content
Kafka’s UI is built entirely with Jetpack Compose using Material3 design and MaterialYou dynamic colors. The UI layer follows modern Android best practices with declarative UI, unidirectional data flow, and reactive state management.

UI Layer Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Composable UI                             │
│   @Composable functions render UI based on state            │
└────────────────────────┬────────────────────────────────────┘
                         │ collectAsStateWithLifecycle()

┌─────────────────────────────────────────────────────────────┐
│                    ViewModel                                 │
│   StateFlow<ViewState> - Immutable UI state                 │
│   User action handlers (public functions)                   │
└────────────────────────┬────────────────────────────────────┘
                         │ invoke()

┌─────────────────────────────────────────────────────────────┐
│              Interactors & Observers                         │
│   Business logic and data access                            │
└─────────────────────────────────────────────────────────────┘

Composable Screens

Screen Structure Pattern

All feature screens follow a consistent pattern:
@Composable
@Inject  // kotlin-inject for factory
fun Homepage(viewModelFactory: () -> HomepageViewModel) {
    // 1. Get ViewModel
    val viewModel = viewModel { viewModelFactory() }
    
    // 2. Collect state
    val viewState by viewModel.state.collectAsStateWithLifecycle()
    
    // 3. Scaffold with top bar
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = { HomeTopBar(openProfile = viewModel::openProfile) }
    ) { padding ->
        ProvideScaffoldPadding(padding = padding) {
            Box(modifier = Modifier.fillMaxSize()) {
                // 4. Content based on state
                AnimatedVisibilityFade(visible = viewState.homepage.collection.isNotEmpty()) {
                    HomepageFeedItems(
                        homepage = viewState.homepage,
                        openItemDetail = viewModel::openItemDetail,
                        removeRecentItem = viewModel::removeRecentItem,
                        goToSearch = viewModel::openSearch
                    )
                }

                // 5. Loading state
                InfiniteProgressBar(
                    show = viewState.isFullScreenLoading,
                    modifier = Modifier.align(Alignment.Center)
                )

                // 6. Error state
                FullScreenMessage(
                    uiMessage = viewState.message,
                    show = viewState.isFullScreenError,
                    onRetry = viewModel::retry,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

Key UI Patterns

Factory Injection

Use @Inject on composable with ViewModel factory parameter

State Collection

collectAsStateWithLifecycle() for lifecycle-aware collection

Scaffold Pattern

Consistent use of Scaffold with top bar and FAB

State-Based UI

Render different UI based on ViewState properties

State Management

ViewState Pattern

Immutable data classes representing complete UI state:
data class HomepageViewState(
    val homepage: Homepage = Homepage(),
    val user: User? = null,
    val isLoading: Boolean = false,
    val appShareIndex: Int = 6,
    val message: UiMessage? = null,
) {
    // Derived/computed properties
    val isFullScreenLoading = isLoading && homepage.collection.isEmpty()
    val isFullScreenError = message != null && homepage.collection.isEmpty()
    val hasData = homepage.collection.isNotEmpty()
}

Creating StateFlow in ViewModel

val state: StateFlow<ViewState> = combine(
    observeHomepage.flow,
    observeUser.flow,
    loadingCounter.observable,
    uiMessageManager.message,
) { homepage, user, isLoading, message ->
    ViewState(
        homepage = homepage,
        user = user,
        isLoading = isLoading,
        message = message
    )
}.stateInDefault(
    scope = viewModelScope,
    initialValue = ViewState()
)

Reusable UI Components

Kafka has a rich library of reusable components in the ui:components module.

Item Components

// Full item card with cover, title, creator
@Composable
fun Item(
    item: Item,
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {}
) {
    Row(
        modifier = modifier
            .clickable(onClick = onClick)
            .fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(Dimens.Spacing12)
    ) {
        CoverImage(
            data = item.coverImage,
            modifier = Modifier.size(80.dp)
        )
        
        Column(modifier = Modifier.weight(1f)) {
            Text(
                text = item.title ?: "",
                style = MaterialTheme.typography.titleMedium,
                maxLines = 2
            )
            
            item.creator?.name?.let { creator ->
                Text(
                    text = creator,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        }
    }
}

// Row item (smaller, horizontal)
@Composable
fun RowItem(
    item: Item,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.width(140.dp),
        verticalArrangement = Arrangement.spacedBy(Dimens.Spacing08)
    ) {
        CoverImage(
            data = item.coverImage,
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(0.7f)
        )
        
        Text(
            text = item.title ?: "",
            style = MaterialTheme.typography.bodyMedium,
            maxLines = 2
        )
    }
}

// Small item (for grids)
@Composable  
fun ItemSmall(
    item: Item,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.spacedBy(Dimens.Spacing08)
    ) {
        CoverImage(
            data = item.coverImage,
            modifier = Modifier.size(48.dp)
        )
        
        Text(
            text = item.title ?: "",
            style = MaterialTheme.typography.bodySmall,
            maxLines = 1,
            modifier = Modifier.weight(1f)
        )
    }
}

Common UI Components

@Composable
fun MessageBox(
    text: String,
    modifier: Modifier = Modifier,
    leadingIcon: ImageVector? = null,
    trailingIcon: ImageVector? = null,
    onClick: (() -> Unit)? = null
) {
    Surface(
        modifier = modifier
            .fillMaxWidth()
            .then(onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier),
        shape = MaterialTheme.shapes.medium,
        color = MaterialTheme.colorScheme.secondaryContainer
    ) {
        Row(
            modifier = Modifier.padding(Dimens.Spacing16),
            horizontalArrangement = Arrangement.spacedBy(Dimens.Spacing12),
            verticalAlignment = Alignment.CenterVertically
        ) {
            leadingIcon?.let {
                Icon(imageVector = it, contentDescription = null)
            }
            
            Text(
                text = text,
                style = MaterialTheme.typography.bodyMedium,
                modifier = Modifier.weight(1f)
            )
            
            trailingIcon?.let {
                Icon(imageVector = it, contentDescription = null)
            }
        }
    }
}

Material3 & MaterialYou

Kafka uses Material3 with dynamic colors (MaterialYou).
@Composable
fun KafkaTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}
Type-safe navigation using Compose Navigation.
sealed class Screen(val route: String) {
    object Homepage : Screen("homepage")
    object Search : Screen("search?query={query}&filter={filter}") {
        fun createRoute(query: String = "", filter: String = "") =
            "search?query=$query&filter=$filter"
    }
    data class ItemDetail(val itemId: String) : Screen("item/$itemId")
    object Profile : Screen("profile")
    object Library : Screen("library")
}

Lists and LazyLayouts

Efficient list rendering with LazyColumn and LazyRow.
@Composable
fun HomepageFeedItems(
    homepage: Homepage,
    openItemDetail: (String, String) -> Unit
) {
    LazyColumn(
        modifier = Modifier.testTag("homepage_feed_items"),
        contentPadding = scaffoldPadding()
    ) {
        homepage.collection.forEachIndexed { index, collection ->
            when (collection) {
                is HomepageCollection.RecentItems -> {
                    item(key = "recent", contentType = "recent") {
                        RecentItems(
                            readingList = homepage.continueReadingItems,
                            openItemDetail = openItemDetail
                        )
                    }
                }
                
                is HomepageCollection.Row -> {
                    item(key = collection.key, contentType = "row") {
                        RowItems(
                            items = collection.items,
                            openItemDetail = { openItemDetail(it, "row") }
                        )
                    }
                }
                
                is HomepageCollection.Column -> {
                    columnItems(collection) { openItemDetail(it, "column") }
                }
            }
        }
    }
}

Best Practices

Lifecycle Awareness

Use collectAsStateWithLifecycle() instead of collectAsState()

Immutable State

ViewState should be immutable data classes

Stateless Composables

Keep composables stateless, hoist state to ViewModel

Keys in Lists

Always provide keys for items() in lazy layouts

Reusable Components

Extract common UI patterns to ui:components

Loading States

Show placeholders instead of empty screens

Build docs developers (and LLMs) love