Slide 1

Slide 1 text

Building a production-ready Chat SDK with Jetpack Compose Filip Babić Jetpack Compose Lead @filbabic Márton Braun Android Dev Advocate @zsmb13 1

Slide 2

Slide 2 text

About Filip Lead Android Developer for Jetpack Compose at Stream GDE @ Kotlin & Android. Published author and conference speaker. Links: - @filbabic on Twitter - /in/filbabic/ on LinkedIn 2

Slide 3

Slide 3 text

About Márton Android Developer Advocate at Stream GDE @ Kotlin & Android. Published author and conference speaker. Find me: - @zsmb13 on Twitter - on zsmb.co - /in/zsmb13 on LinkedIn 3

Slide 4

Slide 4 text

Contents ● Why Jetpack Compose? ● Compose in production? ● Our component design system ● Common pitfalls & concerns ● Component readability ● Customization 4

Slide 5

Slide 5 text

Why Jetpack Compose? 5

Slide 6

Slide 6 text

Why Jetpack Compose? It’ll change Android development forever ● Faster and more efficient development ● Intuitive API design after the initial learning curve ● Declarative + components as functions = deep customization ● Language consistency (all Kotlin!) 6

Slide 7

Slide 7 text

Why Jetpack Compose for Stream? Meet users where they are ● Provide easy-to-use, “idiomatic” APIs ● Great match for our SDK’s customization needs ○ Theming ○ Slot APIs 7

Slide 8

Slide 8 text

Compose in production? 8

Slide 9

Slide 9 text

Compose in production? It’s been pretty stable for many months now - since the first betas. The core API is stable and secure, but there are some parts of the API that need more work. For the most part, you can avoid these experimental APIs in your day-to-day. 9

Slide 10

Slide 10 text

API to watch out for ● Accompanist libraries ● Some lazy APIs ● Sticky headers, Grids ● Some modifiers ● Some Material Components (e.g. clickable Card and Surface) 10

Slide 11

Slide 11 text

Using experimental APIs 11 zsmb.co/opt-in-annotations/

Slide 12

Slide 12 text

With all that, we can say that Compose is prod-ready and here to stay! :] 12

Slide 13

Slide 13 text

Component design system 13

Slide 14

Slide 14 text

Component design system We provide three component types ● Screen components: Out-of-the-box solutions with limited customization. ● Bound components: OoB solutions for a portion of the screen. More customizable. ● Stateless components: State-driven components offering full customization. 14

Slide 15

Slide 15 text

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ChatTheme { MessagesScreen( channelId = channelId, messageLimit = 30, onBackPressed = { finish() }, onHeaderActionClick = {} ) } } } Screen components 15

Slide 16

Slide 16 text

@Composable public fun MessagesScreen( channelId: String, messageLimit: Int = 30, showHeader: Boolean = true, onBackPressed: () -> Unit = {}, onHeaderActionClick: () -> Unit = {}, ) Screen components 16

Slide 17

Slide 17 text

@Composable public fun MessagesScreen( channelId: String, messageLimit: Int = 30, showHeader: Boolean = true, onBackPressed: () -> Unit = {}, onHeaderActionClick: () -> Unit = {}, ) Screen components 17

Slide 18

Slide 18 text

// Rest of your UI / setContent MessageList( modifier = Modifier .background(ChatTheme.colors.appBackground) .fillMaxSize(), viewModel = listViewModel, onThreadClick = { message -> // Handle clicks } ) Bound components 18

Slide 19

Slide 19 text

@Composable public fun MessageList( viewModel: MessageListViewModel, modifier: Modifier = Modifier, onThreadClick: (Message) -> Unit = { viewModel.openMessageThread(it) }, onLongItemClick: (Message) -> Unit = { viewModel.selectMessage(it) }, onMessagesStartReached: () -> Unit = { viewModel.loadMore() }, onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, itemContent: @Composable (MessageItem) -> Unit = { DefaultMessageContainer( messageItem = it, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, ) }, ... ) Bound components 19

Slide 20

Slide 20 text

@Composable public fun MessageList( viewModel: MessageListViewModel, modifier: Modifier = Modifier, onThreadClick: (Message) -> Unit = { viewModel.openMessageThread(it) }, onLongItemClick: (Message) -> Unit = { viewModel.selectMessage(it) }, onMessagesStartReached: () -> Unit = { viewModel.loadMore() }, onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, itemContent: @Composable (MessageItem) -> Unit = { DefaultMessageContainer( messageItem = it, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, ) }, ... ) Bound components 20

Slide 21

Slide 21 text

@Composable public fun MessageList( viewModel: MessageListViewModel, modifier: Modifier = Modifier, onThreadClick: (Message) -> Unit = { viewModel.openMessageThread(it) }, onLongItemClick: (Message) -> Unit = { viewModel.selectMessage(it) }, onMessagesStartReached: () -> Unit = { viewModel.loadMore() }, onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, itemContent: @Composable (MessageItem) -> Unit = { DefaultMessageContainer( messageItem = it, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, ) }, ... ) Bound components 21

Slide 22

Slide 22 text

22

Slide 23

Slide 23 text

@Composable public fun Messages( messagesState: MessagesState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (MessageItem) -> Unit, onScrollToBottom: () -> Unit, modifier: Modifier = Modifier, itemContent: @Composable (MessageItem) -> Unit, ) Stateless components 23

Slide 24

Slide 24 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components Loading & Pagination state 24

Slide 25

Slide 25 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components 25

Slide 26

Slide 26 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components Empty and Loaded states 26

Slide 27

Slide 27 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components 27

Slide 28

Slide 28 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components Overlay (selected message) Thread mode Unread message indicator 28

Slide 29

Slide 29 text

public data class MessagesState( val isLoading: Boolean = true, val isLoadingMore: Boolean = false, val endOfMessages: Boolean = false, val messageItems: List = emptyList(), val selectedMessage: Message? = null, val currentUser: User? = null, val newMessageState: NewMessageState? = null, val parentMessageId: String? = null, val unreadCount: Int = 0, ) Stateless components 29

Slide 30

Slide 30 text

Common Pitfalls 30

Slide 31

Slide 31 text

● Thinking imperatively - you can’t “update” the UI or set listeners. ● Hardcoding customization - using modifiers too much in internal code. ● Migrating everything - just like with Kotlin, you should migrate slowly. ● Lack of examples - there aren’t too many examples out there to guide you. Common pitfalls/issues in Compose Things you’ll probably run into 31

Slide 32

Slide 32 text

Common Concerns 32

Slide 33

Slide 33 text

● Dynamic sized components (Avatar) ● Message List optimizations (Optimistic updates) ● Deep customization (e.g. attachments, colors, typography…) Common Concerns Things we were worried about 33

Slide 34

Slide 34 text

Component Readability 34

Slide 35

Slide 35 text

Component Readability Avoid falling into the trap of component hell Most declarative UI frameworks suffer from the issue of deep component nesting. For readability, we suggest moving to a “flat structure” if possible for your high-level components, as well as logically decoupling components. 35

Slide 36

Slide 36 text

@Composable public fun MessagesScreen(...) { Box(modifier = Modifier.fillMaxSize()) { Scaffold( modifier = Modifier.fillMaxSize(), topBar = { if (showHeader) MessageListHeader(...) }, bottomBar = { MessageComposer() } ) { MessageList() } } } 36

Slide 37

Slide 37 text

@Composable public fun MessagesScreen(...) { Box(modifier = Modifier.fillMaxSize()) { Scaffold(...) { MessageList() } if (selectedMessage != null) SelectedMessageOverlay() if (isShowingAttachments) AttachmentsPicker() val deleteAction = messageActions.firstOrNull { it is Delete } if (deleteAction != null) SimpleDialog() } } 37

Slide 38

Slide 38 text

38

Slide 39

Slide 39 text

Box(modifier = Modifier.fillMaxSize()) { BottomDrawer(drawerContent = { AnimatedVisibility(visible = isShowingAttachments) { AttachmentsPicker(...) } }) { Scaffold(topBar = { ... }, bottomBar = { ... }) { MessageList(itemContent = { }) } } if (messageActions.any { it is Delete }){ } } 39

Slide 40

Slide 40 text

Customization mindset 40

Slide 41

Slide 41 text

Customization Mindset Give out-of-the-box solutions, allow customization ● Provide a modifier in components people will use - Jetpack Compose API Guidelines. ● Think about exposing content parameters where appropriate - Slot APIs. ● Think about theming and building a design system. ● Provide customization for state and action handlers. 41

Slide 42

Slide 42 text

State Action handlers UI & Slot APIs Component parameters @Composable public fun MessageComposer( viewModel: MessageComposerViewModel, onSendMessage: (Message) -> Unit = { viewModel.sendMessage(it) }, onAttachmentsClick: () -> Unit = {}, onValueChange: (String) -> Unit = { viewModel.setMessageInput(it) }, onAttachmentRemoved: (Attachment) -> Unit = { viewModel.removeSelectedAttachment(it) }, onCancelAction: () -> Unit = { viewModel.dismissMessageActions() }, modifier: Modifier = Modifier, integrations: @Composable RowScope.() -> Unit = { DefaultComposerIntegrations(onAttachmentsClick) }, label: @Composable () -> Unit = { DefaultComposerLabel() }, input: @Composable RowScope.() -> Unit = { MessageInput( modifier = Modifier .fillMaxWidth() .weight(1f), label = label, value = viewModel.input, attachments = viewModel.selectedAttachments, activeAction = viewModel.activeAction, onValueChange = onValueChange, onAttachmentRemoved = onAttachmentRemoved ) }, ) 42

Slide 43

Slide 43 text

State @Composable public fun MessageComposer( viewModel: MessageComposerViewModel, ) 43

Slide 44

Slide 44 text

@Composable public fun MessageComposer( value: String, attachments: List, activeAction: MessageAction?, ) State @Composable public fun MessageComposer( viewModel: MessageComposerViewModel, ) 44

Slide 45

Slide 45 text

Action handlers @Composable public fun MessageComposer( onSendMessage: (Message) -> Unit = { viewModel.sendMessage(it) }, onAttachmentsClick: () -> Unit = {}, onValueChange: (String) -> Unit = { viewModel.setMessageInput(it) }, onAttachmentRemoved: (Attachment) -> Unit = { viewModel.removeSelectedAttachment(it) }, onCancelAction: () -> Unit = { viewModel.dismissMessageActions() }, ) 45

Slide 46

Slide 46 text

Action handlers @Composable public fun MessageComposer( onSendMessage: (Message) -> Unit = { viewModel.sendMessage(it) }, onAttachmentsClick: () -> Unit = {}, onValueChange: (String) -> Unit = { viewModel.setMessageInput(it) }, onAttachmentRemoved: (Attachment) -> Unit = { viewModel.removeSelectedAttachment(it) }, onCancelAction: () -> Unit = { viewModel.dismissMessageActions() }, ) MessageComposer( viewModel = composerViewModel, onSendMessage = { message -> message.text = message.text.replace("XML", "Jetpack Compose") composerViewModel.sendMessage(message) } ) 46

Slide 47

Slide 47 text

Action handlers @Composable public fun MessageComposer( onSendMessage: (Message) -> Unit = { viewModel.sendMessage(it) }, onAttachmentsClick: () -> Unit = {}, onValueChange: (String) -> Unit = { viewModel.setMessageInput(it) }, onAttachmentRemoved: (Attachment) -> Unit = { viewModel.removeSelectedAttachment(it) }, onCancelAction: () -> Unit = { viewModel.dismissMessageActions() }, ) MessageComposer( viewModel = composerViewModel, onSendMessage = { message -> message.text = message.text.replace("XML", "Jetpack Compose") composerViewModel.sendMessage(message) } ) 47

Slide 48

Slide 48 text

@Composable public fun MessageComposer( integrations: @Composable RowScope.() -> Unit = { DefaultComposerIntegrations(onAttachmentsClick) }, label: @Composable () -> Unit = { DefaultComposerLabel() }, input: @Composable RowScope.() -> Unit = { MessageInput( modifier = Modifier.fillMaxWidth().weight(1f), label = label, value = viewModel.input, attachments = viewModel.selectedAttachments, activeAction = viewModel.activeAction, onValueChange = onValueChange, onAttachmentRemoved = onAttachmentRemoved ) }, ) Slot APIs 48

Slide 49

Slide 49 text

@Composable public fun MessageComposer( integrations: @Composable RowScope.() -> Unit = { DefaultComposerIntegrations(onAttachmentsClick) }, label: @Composable () -> Unit = { DefaultComposerLabel() }, input: @Composable RowScope.() -> Unit = { MessageInput( modifier = Modifier.fillMaxWidth().weight(1f), label = label, value = viewModel.input, attachments = viewModel.selectedAttachments, activeAction = viewModel.activeAction, onValueChange = onValueChange, onAttachmentRemoved = onAttachmentRemoved ) }, ) Slot APIs 49

Slide 50

Slide 50 text

@Composable public fun MessageComposer( integrations: @Composable RowScope.() -> Unit = { DefaultComposerIntegrations(onAttachmentsClick) }, label: @Composable () -> Unit = { DefaultComposerLabel() }, input: @Composable RowScope.() -> Unit = { MessageInput( modifier = Modifier.fillMaxWidth().weight(1f), label = label, value = viewModel.input, attachments = viewModel.selectedAttachments, activeAction = viewModel.activeAction, onValueChange = onValueChange, onAttachmentRemoved = onAttachmentRemoved ) }, ) Slot APIs 50

Slide 51

Slide 51 text

@Composable public fun MessageComposer( integrations: @Composable RowScope.() -> Unit = { DefaultComposerIntegrations(onAttachmentsClick) }, label: @Composable () -> Unit = { DefaultComposerLabel() }, input: @Composable RowScope.() -> Unit = { MessageInput( modifier = Modifier.fillMaxWidth().weight(1f), label = label, value = viewModel.input, attachments = viewModel.selectedAttachments, activeAction = viewModel.activeAction, onValueChange = onValueChange, onAttachmentRemoved = onAttachmentRemoved ) }, ) Slot APIs 51

Slide 52

Slide 52 text

@Composable public fun ChatTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors: StreamColors = if (isInDarkMode) StreamColors.defaultDarkColors() else StreamColors.defaultColors(), typography: StreamTypography = StreamTypography.default, shapes: StreamShapes = StreamShapes.default, attachmentFactories: List = StreamAttachmentFactories.defaultFactories, reactionTypes: Map = defaultReactionTypes, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalColors provides colors, LocalTypography provides typography, LocalShapes provides shapes, LocalAttachmentFactories provides attachmentFactories, LocalReactionTypes provides reactionTypes, ) { content() } } 52

Slide 53

Slide 53 text

@Composable public fun ChatTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors: StreamColors = if (isInDarkMode) StreamColors.defaultDarkColors() else StreamColors.defaultColors(), typography: StreamTypography = StreamTypography.default, shapes: StreamShapes = StreamShapes.default, attachmentFactories: List = StreamAttachmentFactories.defaultFactories, reactionTypes: Map = defaultReactionTypes, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalColors provides colors, LocalTypography provides typography, LocalShapes provides shapes, LocalAttachmentFactories provides attachmentFactories, LocalReactionTypes provides reactionTypes, ) { content() } } 53

Slide 54

Slide 54 text

@Composable public fun ChatTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors: StreamColors = if (isInDarkMode) StreamColors.defaultDarkColors() else StreamColors.defaultColors(), typography: StreamTypography = StreamTypography.default, shapes: StreamShapes = StreamShapes.default, attachmentFactories: List = StreamAttachmentFactories.defaultFactories, reactionTypes: Map = defaultReactionTypes, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalColors provides colors, LocalTypography provides typography, LocalShapes provides shapes, LocalAttachmentFactories provides attachmentFactories, LocalReactionTypes provides reactionTypes, ) { content() } } 54

Slide 55

Slide 55 text

public data class StreamColors( public val textHighEmphasis: Color, public val textLowEmphasis: Color, public val disabled: Color, public val borders: Color, public val inputBackground: Color, public val appBackground: Color, public val barsBackground: Color, public val linkBackground: Color, public val primaryAccent: Color, public val errorAccent: Color, public val infoAccent: Color, public val highlight: Color, ) 55

Slide 56

Slide 56 text

Chat SDK Resources ● Compose Chat SDK on GitHub 🌟 ● https://github.com/GetStream/stream-chat-android/ ● Compose Tutorial ● https://getstream.io/chat/compose/tutorial/ ● Compose SDK docs ● https://getstream.io/chat/docs/sdk/android/compose/overview/ ● Twitter ● @getstream_io 56

Slide 57

Slide 57 text

Jetpack Compose Resources ● Official Jetpack Compose API Guidelines ● https://github.com/androidx/androidx/blob/androidx-main/compose/doc s/compose-api-guidelines.md ● Google Compose pathway ● https://developer.android.com/courses/pathways/compose ● GDG Osijek YT ● www.youtube.com/channel/UCAXQuBMZ5B7f4thG5gO_3Rw 57

Slide 58

Slide 58 text

Questions? Filip Babić Jetpack Compose Lead @filbabic Márton Braun Android Dev Advocate @zsmb13 58