$30 off During Our Annual Pro Sale. View Details »

Essential concepts to know when learning Declar...

HyunWoo Lee
September 12, 2024

Essential concepts to know when learning Declarative UI

This is the speaker deck for Essential concepts to know when learning Declarative UI, presented at DroidKaigi2024 (Tokyo, Japan).

DroidKaigi2024 (Tokyo, Japan)에서 진행한 Essential concepts to know when learning Declarative UI의 Speaker Deck입니다.

HyunWoo Lee

September 12, 2024
Tweet

More Decks by HyunWoo Lee

Other Decks in Programming

Transcript

  1. ESSENTIAL CONCEPTS TO KNOW WHEN LEARNING DECLARATIVE UI HYUNWOO LEE

    ORGANIZER, KOTLIN USER GROUPS SEOUL, SOUTH KOREA ANDROID/REACT NATIVE DEVELOPER, VIVA REPUBLICA(TOSS) DROIDKAIGI 2024
  2. ESSENTIAL CONCEPTS TO KNOW WHEN LEARNING DECLARATIVE UI • WHAT

    IS “DECLARATIVE” UI? • HOW IT DIFFERS: IMPERATIVE UI VS DECLARATIVE UI • COMPONENTS OF DECLARATIVE UI PARADIGM • STATE MANAGEMENT: NOW AND THEN?
  3. / / Write the program ex) sums 1 to 10

    THINKING IN “DECLARATIVE” WAY fun main() { var sum = 0 for (i in 1 . . 10) { sum += i } println(“sum = $sum”) }
  4. / / Write the program ex) sums 1 to 10

    THINKING IN “DECLARATIVE” WAY fun main() { (1 . . 10).sum() println(“sum = $sum”) }
  5. COMPONENT - ANDROID FIRST OF ALL IS UI fun TimeTableItem(

    tags: List<String>, title: String, isLike: Boolean ) { Column() { Row() { Tags(tags = tags) Like(isLike = isLike) } SectionTitle(title = title) } }
  6. COMPONENT - IOS FIRST OF ALL IS UI struct TimeTableItem:

    View { var tags: [String] var title: String var isLike: Bool var body: some View { VStack { HStack { Tags(tags: tags) Like(isLike: isLike) } SectionTitle(title: title) } } }
  7. COMPONENT-REACT NATIVE FIRST OF ALL IS UI function TimeTableItem({ tags,

    title, isLike }: Props) { return ( <View> <View style= { { flexDirection: ‘row’ } } > <Tags tags={tags} / > <Like isLike={isLike} / > < / View> <SectionTitle title=title / > < / View> ) }
  8. SCREEN = SUM OF COMPONENTS FIRST OF ALL IS UI

    • Single Screen(Single Component) = Sum of Multiple Components • Function Component vs Class Component • Function Component: Using data from parameters or local variables • Jetpack Compose(Android), React(React Native) • Class Component: Using data from the constructor, member variables • SwiftUI, React(React Native, old fashioned)
  9. STATE AND RENDER HOW TO RENDER AS I WANT? •

    So far, we have dealt with components using prede fi ned-data(parameter, constructor etc) • Then what should be done if the user needs to enter data or change the screen by user interactions?
  10. STATE AND RENDER HOW TO RENDER AS I WANT? •

    “STATE” is the answer • Change of state triggers screen’s change -> re-render • Implementations of state and re-render in each libraries seem quite similar
  11. STATE - ANDROID STATE AND RE-RENDER fun TimeTableItem( tags: List<String>,

    title: String, isLike: Boolean ) { Column() { Row() { Tags(tags = tags) Like(isLike = isLike) } SectionTitle(title = title) } }
  12. STATE - ANDROID STATE AND RE-RENDER fun TimeTableItem( tags: List<String>,

    title: String ) { var isLike by remember { mutableStateOf(false) } Column() { Row() { Tags(tags = tags) Like( isLike = isLike, modifier = Modifier.clickcable { isLike = !isLike } ) } SectionTitle(title = title) } }
  13. STATE - ANDROID STATE AND RE-RENDER fun TimeTableItem( tags: List<String>,

    title: String ) { var isLike by remember { mutableStateOf(false) } Column() { Row() { Tags(tags = tags) Like( isLike = isLike, modifier = Modifier.clickcable { isLike = !isLike } ) } SectionTitle(title = title) } }
  14. STATE - ANDROID STATE AND RE-RENDER fun TimeTableItem( tags: List<String>,

    title: String ) { var (isLike, setIsLike) = remember { mutableStateOf(false) } Column() { Row() { Tags(tags = tags) Like( isLike = isLike, modifier = Modifier.clickcable { isLike = !isLike } ) } SectionTitle(title = title) } }
  15. STATE - REACT NATIVE STATE AND RE-RENDER function TimeTableItem({ tags,

    title }: Props) { const [isLike, setIsLike] = useState(false); return ( <View> <View style= { { flexDirection: ‘row’ } } > <Tags tags={tags} / > <Like isLike={isLike} onLikeChange={() = > setIsLike(!isLike)} / > < / View> <SectionTitle title=title / > < / View> ) }
  16. STATE - REACT NATIVE STATE AND RE-RENDER function TimeTableItem({ tags,

    title }: Props) { const [isLike, setIsLike] = useState(false); return ( <View> <View style= { { flexDirection: ‘row’ } } > <Tags tags={tags} / > <Like isLike={isLike} onLikeChange={() = > setIsLike(!isLike)} / > < / View> <SectionTitle title=title / > < / View> ) }
  17. STATE - IOS STATE AND RE-RENDER struct TimeTableItem: View {

    var tags: [String] var title: String @State var isLike: Bool var body: some View { VStack { HStack { Tags(tags: tags) Like(isLike: isLike) } SectionTitle(title: title) } } } struct Like: View { @Binding var isLike: Bool var body: some View { if isLike { Image(.icFavoriteFill) } else { Image(.icFavoriteOutline) } } }
  18. STATE AND RENDER STATE AND RE-RENDER • Re-render mechanisms are

    also similar • Responsibility for changing UI has shifted from the developer to the system.
  19. STATE AND RENDER STATE AND RE-RENDER • Re-render mechanisms have

    similarities, but there are also slight differences • Android: Smart Recomposition • React: Virtual DOM • SwiftUI: Update by id
  20. HANDLE NON-UI RELATED LOGIC IN UI CODE SIDE EFFECTS •

    How would you implement alert when like icon is displayed? • Although it is not directly related to rendering the UI, you must consider handling the operations necessary to prepare the data required for the UI. • Side Effect = Non-UI Rendering Related Logic, but ESSENTIAL Logics • HTTP Request, Permission Requests, Alarm Noti fi cation etc
  21. SIDE EFFECTS fun TimeTableItem( tags: List<String>, title: String ) {

    var isLike by remember { mutableStateOf(false) } Column() { Row() { Tags(tags = tags) Like( isLike = isLike, modifier = Modifier.clickcable { isLike = !isLike } ) } SectionTitle(title = title) } }
  22. SIDE EFFECTS fun TimeTableItem( tags: List<String>, title: String ) {

    var isLike by remember { mutableStateOf(false) } LaunchedEffect(isLike) { toast(if (isLike) “You stored this session!” else “You deleted this session”) } Column() { Row() { Tags(tags = tags) Like( isLike = isLike, modifier = Modifier.clickcable { isLike = !isLike } ) } SectionTitle(title = title) } }
  23. SIDE EFFECTS function TimeTableItem({ tags, title }: Props) { const

    [isLike, setIsLike] = useState(false); useEffect(() = > { / / Handle Side Effects alert(`${isLike ? ‘You stored this session’ : ‘You deleted this session’}`); }, [isLike]); return ( <View> <View style= { { flexDirection: ‘row’ } } > <Tags tags={tags} / > <Like isLike={isLike} onLikeChange={() = > setIsLike(!isLike)} / > < / View> <SectionTitle title=title / > < / View> ); }
  24. SIDE EFFECTS function TimeTableItem({ tags, title }: Props) { useEffect(

    () = > { / / Handle Side Effects }, [isLike] ); }
  25. SIDE EFFECTS function TimeTableItem({ tags, title }: Props) { useEffect(

    () = > { / / Handle Side Effects }, [isLike] ); } Denpendency Array • Empty Array: if screen mounts, callback executes once(actually not) • Non-empty Array: if element’s value change, callback executes • No-value(unde fi ned): Callback executes at every re-render
  26. SIDE EFFECTS function TimeTableItem({ tags, title }: Props) { useEffect(

    () = > { / / Handle Side Effects () = > { } }, [isLike] ); } Clean Up Function • Executes when the component unmounts on screen
  27. SIDE EFFECTS HANDLING SIDE EFFECTS IN ANDROID • LaunchedEffect •

    keys: similar as “dependency array” • but if you want to execute only once when component mounts, you need to put unchangeable value (Unit, true) • Side Effect: de fi ne your side effect (suspend function) LaunchedEffect(key1, key2) { / / Side Effect }
  28. SIDE EFFECTS HANDLING SIDE EFFECTS IN ANDROID • DisposableEffect •

    keys: same as LaunchedEffect • Side Effect: non-suspend function • onDispose: executes when component unmounts DisposableEffect(key1, key2) { / / Side Effect onDispose { } }
  29. SIDE EFFECTS HANDLING SIDE EFFECTS IN ANDROID • SideEffect •

    Executes every re-render SideEffect { / / Side Effect }
  30. SIDE EFFECTS HANDLING SIDE EFFECTS IN SWIFTUI VStack { Text(text).padding()

    Button("Change Text") { text = "Text changed \(count + 1) times" } .onAppear { } .onDisappear { } .onChange(of: text) { newValue in print("Text changed to: \(newValue)”) } }
  31. GOOD, THEN IS IT ENOUGH? • With this level of

    knowledge, you should be able to create components or screens with simple logic using declarative UI. • However, if the logic becomes even slightly more complex or the screen has multiple layers, the readability & developer experience can signi fi cantly decrease.
  32. SHARE THE STATE TO BOTTOM NODE • How to share

    the ui state or theme data(color, typography etc) to child components. • Using parameters (props drilling) • Local scoped data (like data teleportation)
  33. SHARE THE STATE TO BOTTOM NODE • Local Scoped Data

    in Jetpack Compose - CompositionLocal • staticCompositionLocalOf • Suppose CompositionLocal’s value does not change • If value changes entire screen will be recomposed • compositionLocalOf • If value changes, recompose components that read value
  34. SHARE THE STATE TO BOTTOM NODE • Local Scoped Data

    • In SwiftUI - EnvironmentalValue • React - Context
  35. SHARE THE STATE TO BOTTOM NODE / / Define shared

    data val LocalColor = staticCompositionLocalOf<Colors> { Error(“No Colors”) } LOCAL SCOPED DATA(COMPOSITIONLOCAL) - JETPACK COMPOSE / / Provide value to “LocalColor” (CompositionLocal) CompositionLocalProvider( LocalColor provides appColors() ) { / / The components within this block can use LocalColors }
  36. SHARE THE STATE TO BOTTOM NODE LOCAL SCOPED DATA(COMPOSITIONLOCAL) -

    JETPACK COMPOSE CompositionLocalProvider( LocalColor provides appColors() ) { KaigiRow() } fun KaigiRow() { / / Use LocalColors in here val colors = LocalColors.current Row(modifier = Modifier.background(color.background)) { } }
  37. SHARE THE STATE TO BOTTOM NODE LOCAL SCOPED DATA(ENVIRONMENTVALUES) -

    SWIFT / / Define key struct ThemeKey: EnvironmentKey { static let isDarkMode: Bool = false } / / Introduce new value to EnvironmentValues extension EnvironmentValues { var isDarkMode: Bool { get { self[ThemeKey.self] } set { self[ThemeKey.self] = newValue } } }
  38. SHARE THE STATE TO BOTTOM NODE LOCAL SCOPED DATA(ENVIRONMENTVALUES) -

    SWIFT / / Provide EnvironmentValues struct ContentView: View { var body: some View { VStack { Child() .environment(\.isDarkMode, true) } } } / / Use EnvironmentValues struct Child: View { @Environment(\.isDarkMode) var isDarkMode }
  39. SHARE THE STATE TO BOTTOM NODE LOCAL SCOPED DATA(CONTEXT) -

    REACT / / Define Context const ThemeContext = createContext() / / Define Context Provider const ThemeProvider = ({ children }) = > { const [theme, setTheme] = useState(‘light’) return ( <ThemeContext.Provider value= { { theme } } > {children} < / ThemeContext.Provider> ) }
  40. SHARE THE STATE TO BOTTOM NODE LOCAL SCOPED DATA(CONTEXT) -

    REACT / / Define component that use context function Component() { const { theme } = useContext() return <Text>{`Current Theme is ${theme}`} < / Text> } / / Wrap it with Provider function Component() { return <ThemeProvider><Component / > < / ThemeProvider> }
  41. MVI, TCA AND REDUX STATE MANAGEMENT WITH STORE • MVI,

    TCA is “hot trend” for developers using Compose, SwiftUI • Is it really “trendy” way? • Managing the UI State in a single store(or N store) has already been demonstrated in React’s Flux pattern(Redux)
  42. ASYNC(SERVER) STATE MANAGEMENT • SWR - stale-while-revalidate • Use existing

    cache before response arrived • SWR’s ef fi ciency 👍 - just manage your options SYNC MORE EASILY WITH SERVER STATE - SWR
  43. ASYNC(SERVER) STATE MANAGEMENT • Async state management - Data (from

    external source) management + SWR strategy • Actually screen state is closely related to server state(loading, success, etc) • Async State Management explains “we should manage also this state too!” • Example - Optimistic Update • Assume that the server response is successful and maintain the UI state accordingly, but roll it back if the request fails SYNC MORE EASILY WITH SERVER STATE - ASYNC STATE MANAGEMENT
  44. ASYNC(SERVER) STATE MANAGEMENT S-W-R(STALE-WHILE-REVALIDATE) & REACT QUERY • Most famous

    async state management library • There are many features • Caching • Updating stale data when it stales (using staleTime) • Invalidate cache forcefully • Provide loading state, error state to draw screen more easily using those function Example() { const {data, isLoading, isError, refetch} = useQuery(‘index’, fetchData, { staleTime: 5000 } ); if (isLoading) return <Text>Loading… < / Text> if (isError) return <View><ErrorIamge / > < / View> const invalidate = () = > { queryClient.invalidateQueries(‘index’) } return <View>{data}<View onClick={invalidate} >Invalidate < / View> < / View> }
  45. CONCLUSION • "Declarative UI is a concept that is not

    tied to any speci fi c library. • If you understand the essential concepts, you can work with other libraries as well and write code with a certain level of pro fi ciency. • If you're considering about how to effectively use declarative UI, I recommend looking into the methodology in React. • React uses many concepts like React Hooks and local state management, which I couldn't introduce(due to time constraints). • These approaches were developed by those who faced similar challenges years ahead of us as Android developers, so they can become best practices for the services you're currently managing.