Slide 1

Slide 1 text

Jetpack Compose Recipes #androidjetpack

Slide 2

Slide 2 text

01 What is Jetpack Compose? 02 Impreactive UI vs Declarative UI A quick recall to compose 03 XML to Compose Migration 04 State Handling Explore state handling in compose with a Spinner example 05 Reusable Component 06 Best practices for Compose 07 Questions & Answer Questions on anything “Android”

Slide 3

Slide 3 text

A Library? or a candy? Jetpack Compose

Slide 4

Slide 4 text

“Android’s modern toolkit for building native UI” developer.android.com A Documentation

Slide 5

Slide 5 text

Adobe Stock#243026154 Why Jetpack Compose? ● UI development process နဲ ့ Paradigm ကို ရိုး ှင်းေစတယ် ● Traditional Layout Inflation (သို့ ) Imperative UI ထက် ေရးရတဲ့ Code ပိုနည်း ● Kotlin သံုး ပီးေရးလို့ရ

Slide 6

Slide 6 text

01 Efficient UI Rendering 02 Interoperable with existing Widgets and Architecture Components 03 Easier State Management 04 Cuts down development time 05 Tooling Support by Android Studio 06 A rapidly growing Community Advantages

Slide 7

Slide 7 text

01 Interoperability comes with a price 02 Performance drops in Debug mode 03 Requires a Rebuild for preview Caveats

Slide 8

Slide 8 text

The apparent comparison Imperative vs Declarative UI

Slide 9

Slide 9 text

Adobe Stock#243026154 ● Line by line execution of UI logic ● Focuses on “HOW” of UI presentation ● ဘယ်ေန ့ မှ ေဆာင်မှာလဲ xD Imperative UI ● We describe, or declare, what UI should look like ● Focuses on “What” of UI Presentation Declarative UI

Slide 10

Slide 10 text

val linearLayout = findViewById(R.id.linearLayout) linearLayout.addView(createTextView("Text 1")) linearLayout.addView(createTextView("Text 2")) linearLayout.addView(createTextView("Text 3")) fun createTextView(text: String) : TextView = TextView().apply { this.text = text setColorResource(R.color.red) } Imperative UI

Slide 11

Slide 11 text

Imperative UI Column { Text(text = "Text 1") Text(text = "Text 2") Text(text = "Text 3") } Declarative UI

Slide 12

Slide 12 text

Is it time though? XML to Compose Migration

Slide 13

Slide 13 text

01 Add dependency 02 Migration strategies 03 App Architecture 04 Theme in Compose 05 Image Loading XML to Compose Migration

Slide 14

Slide 14 text

buildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1' } } plugins { id 'kotlin-android' } 1 . Add dependency Configure Kotlin

Slide 15

Slide 15 text

android { defaultConfig { ... } buildFeatures { // Enables Jetpack Compose for this module compose true } ... composeOptions { kotlin Compiler Extension Version '1.1.1' } } Configure Gradle

Slide 16

Slide 16 text

Migration Strategies New Compose Only Screen Composed and View mixed Migrate Whole Screen

Slide 17

Slide 17 text

class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... binding.composeView.setContent { Mdc Theme { Surface{ Text("Hello Compose") } } } } } MainActivity.kt

Slide 18

Slide 18 text

activity_main.xml

Slide 19

Slide 19 text

App Architecture @Composable fun MainContent ( viewModel: ViewModel ) Follow Activity / Fragment Lifecycle LiveData.observeAsState( ) Flow.collectAsState( ) Observable.subscribeAsState( )

Slide 20

Slide 20 text

@Composable fun MainScreen() { val viewModel: MainViewModel = viewModel() val data = viewModel.something.observeAsState() MainContent(viewModel) } LiveData

Slide 21

Slide 21 text

@Composable fun MainScreen() { val viewModel: MainViewModel = viewModel() val data = viewModel.something.collectAsState() MainContent(viewModel) } Flow

Slide 22

Slide 22 text

@Composable fun MainScreen() { val viewModel: MainViewModel = viewModel() val data = viewModel.something.subscribeAsState() MainContent(viewModel) } Observable

Slide 23

Slide 23 text

MaterialTheme Themes in Compose MaterialTheme( colors = …, typography = …, shapes = … ) { // app content } MDC Compose Theme Adapter goo.gle/mdc-compose-theme-adapter ● Use MDC theme as single source of truth ● Read the color, text appearance and shape appearance from your MDC theme -> Compose

Slide 24

Slide 24 text

MdcTheme

Slide 25

Slide 25 text

01 Color 02 Typography 03 Shape Themes in Compose https://developer.android.com/jetpack/compose/themes/material

Slide 26

Slide 26 text

private val Yellow200 = Color(0xffffeb46) // ... private val DarkColors = darkColors( primary = Yellow200, // ... ) private val LightColors = lightColors( primary = Yellow500, primaryVariant = Yellow400, // ... ) 1. Color

Slide 27

Slide 27 text

val Rubik = FontFamily( Font(R.font.rubik_regular), Font(R.font.rubik_medium, FontWeight.W500), Font(R.font.rubik_bold, FontWeight.Bold) ) val MyTypography = Typography( h1 = TextStyle( fontFamily = Rubik, fontWeight = FontWeight.W300, fontSize = 96.sp ), /*...*/) 2.Typography

Slide 28

Slide 28 text

val Shapes = Shapes( small = Rounded Corner Shape(percent = 50), medium = Rounded Corner Shape(0f), large = Cut Corner Shape( topStart = 16.dp, topEnd = 0.dp, bottomEnd = 0.dp, bottomStart = 16.dp ) ) MaterialTheme(shapes = Shapes, /*...*/) 3.Shape

Slide 29

Slide 29 text

Image Loading Api to load image from url CoilImage Api to load vector drawables & assets PNG image PainterResource

Slide 30

Slide 30 text

@Composable fun MainScreen() { Icon( painter = painterResource(id = R.drawable.ic_logo), contentDescription = null // decorative element ) } PaintersResource

Slide 31

Slide 31 text

@Composable fun MainScreen(item: ExploreItem) { CoilImage( data = item.imageUrl, contentScale = ContentScale.Crop // decorative element ) } CoilImage

Slide 32

Slide 32 text

Other async examples ● BackButton Handling ● Permission goo.gle/compose-samples

Slide 33

Slide 33 text

State Handling in Compose What is State and how to handle ui state in composition #composestate

Slide 34

Slide 34 text

Adobe Stock#243026154 အနီးစပ်ဆံုးကေတာ့ UI State What is State? ● Card ကို clickလိုက်ရင် ripple animation ေလးြဖစ်လာတာက Card ရဲရွှေ့ state တစ်ခုပါပဲ ● Event တစ်ခုကို change လိုက်ရင် ြဖစ်လာတဲ့အရာ ● Compose မှာလဲ သူ့ data ေတွကို ေြပာင်းလဲြပသတာကို compose state လို့ေခါ်

Slide 35

Slide 35 text

Jetpack compose ကေန composable ကို executes လုပ်လိုက်တဲ့အချ ိန်မှာ UI ကို built လုပ်သွားတာ Composition 01 Data ကို changes လုပ်လိုက်တဲ့အခါ composable ကို ြပန် re-running လုပ်ပီး composition ကို update လုပ်တာကို Recomposition 02 အေြခေနတစ်ခုမှာ composables ေတွကို recomposition အတွက် ြပန်ပီးtrackingလုပ်ေပးတာ Compose’s state tracking system 03 Key Points

Slide 36

Slide 36 text

Compose State APIs

Slide 37

Slide 37 text

Adobe Stock#243026154 State ● Read-only value ေတွပဲသိမ်းထားနိုင်တယ် ● Value ကို ေြပာင်းလိုက်မှ composition ကို ြပန်ပီးေတာ့ notify လုပ်ေပးတယ်

Slide 38

Slide 38 text

Adobe Stock#243026154 MutableState ● Value ေတွကို update ေပးဖို့ လုပ်ေပးတယ် Extension function of State ● Value ကို ေြပာင်းလိုက်ရင် အဲ့ value ကို recompositon ရဲရွှေ့ RecomposeScopes ကိုြပန်ပီးေတာ့ စီစဥ်တယ်

Slide 39

Slide 39 text

Imperative UI var selectedIndex by mutableStateOf(0) MutableState

Slide 40

Slide 40 text

Adobe Stock#243026154 No State State မပါဘဲနဲ ့ data ေတွကို ေြပာင်းမယ်ဆိုရင် State မပါတဲ့ object ကို ေဆာက်ေပးတယ်

Slide 41

Slide 41 text

@Composable fun NoState() { var clickCount = 0 Column { Button(onClick = { clickCount++ Log.d("TAG", "NoState: "+clickCount) }) { Text(text = "$clickCount times clicked") } } }

Slide 42

Slide 42 text

● Mutable သို့ မဟုတ် Immutable ြဖစ်တဲ့ object ေတွကို သိမ်းထားနိုင်တယ် ● Value သာ ေြပာင်းသွားမယ်ဆိုရင် သူနဲ ့ သက်ဆိုင်တဲ့ widget ကို update သွားလုပ်ေပးဖို့ recomposition ( refresh UI) ကို trigger ြပန်လုပ်ေပးတယ် remember

Slide 43

Slide 43 text

var selectedIndex by remember{ mutableStateOf(0) } Imperative UI Syntax @Composable fun RememberSample() { var clickCount by remember { mutableStateOf(0) } Column { Button(onClick = { clickCount++ }) { Text(text = "$clickCount times clicked") } } } Example

Slide 44

Slide 44 text

● Remember နဲ ့ တူတူေပမဲ့ သူက activity သို့ မဟုတ် process ကီး recreate ြပန်လုပ်ရင်ေတာင်မှ restore ြပန်လုပ်ေပးပါတယ် configuration ေြပာင်းလဲရင်လဲ survive ြဖစ်ပါတယ် rememberSaveable

Slide 45

Slide 45 text

Stateful & Stateless

Slide 46

Slide 46 text

Adobe Stock#243026154 Stateful Composable function ကို create လုပ်တဲ့အချ ိန်မှာ သူ့ရဲရွှေ့ state ကို အြပည့်အ၀ control လုပ်နိုင်တာဆိုလိုတယ် အားနည်းချက်ကေတာ့ reusable component လုပ်လို့မရတာရယ် ၊ testing အတွက်ခက်ခဲတာရယ်

Slide 47

Slide 47 text

@Composable fun MainScreen() { var isExpanded by remember { mutableStateOf(false) } Row { Text(modifier = Modifier.weight(1f)) IconButton( onClick = { isExpanded = !isExpanded }) { Image( imageVector = if (isExpanded) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp, contentDescription = "" ) } } }

Slide 48

Slide 48 text

Adobe Stock#243026154 Stateless Composable ကို create လုပ်တဲ့အချ ိန်မှာ သူ့ရဲရွှေ့ state ကို modify ြပန်မလုပ်နိုင်ဘူး သူက လက်ခံလို့ရပီး State ကို ပဲ hoist လုပ်ေပးနိုင်တယ်၊ အဲ့တာကို State hoisting လုပ်တယ်လို့ေခါ်တယ်

Slide 49

Slide 49 text

@Composable fun MainScreen(){ val name by remember { mutableStateOf(“”) } CustomTextField(name, onNameChanged = { name = it }) } @Composable fun CustomTextField(name: String,onNameChanged: (String)-> Unit){ Text(text = name ) OutlineTextField( value = name, onValueChanged = onNameChanged, label = { Text(text = name ) } ) }

Slide 50

Slide 50 text

UI Update Loop

Slide 51

Slide 51 text

Unidirectional flow with Spinner

Slide 52

Slide 52 text

@Composable fun DropdownDemo() { var expanded by remember { mutableStateOf(false) } val items = listOf("Apple", "Ball", "Cat", "Dog", "Elephant", "Florida") var selectedIndex by remember { mutableStateOf(0) } Box(...) { Text(items[selectedIndex]) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, ) { items.forEachIndexed { index, s -> DropdownMenuItem(onClick = { selectedIndex = index expanded = false }) { Text(text = s + disabledText) } } } } }

Slide 53

Slide 53 text

a.k.a custom Composables Reusable Components

Slide 54

Slide 54 text

State what?? State Hoisting

Slide 55

Slide 55 text

Composable တစ်ခု ရဲရွှေ့ state ေတွကို control လုပ်နိုင်တဲ့ privilege ကို upper level composable ကိုတစ်ဆင့်ေရရွှေ့ေပးတာ ကို ဆိုလိုတာပါ။ State Hoisting

Slide 56

Slide 56 text

@Composable fun BurgerItem() { Card { Row { Image(/**/) Column { Text("Tower Burger") NumberControl() } } } } State Hoisting

Slide 57

Slide 57 text

State Hoisting @Composable fun BurgerItem() { Card { Row { Image(/**/) Column { Text("Tower Burger") NumberControl() } } } }

Slide 58

Slide 58 text

State Hoisting @Composable fun NumberControl() { var number by remember { mutableStateOf(0) } Row { MinusButton( onClick = { number-- } ) Text(text = number.toString()) PlusButton( onClick = { number++ } ) } }

Slide 59

Slide 59 text

State Hoisting @Composable fun NumberControl( value: Int = 0, onTapAdd: () -> Unit, onTapMinus: () -> Unit ) { ... }

Slide 60

Slide 60 text

State Hoisting @Composable fun NumberControl( value: Int = 0, onTapAdd: () -> Unit, onTapMinus: () -> Unit ) { Row { MinusButton(onClick = onTapAdd) Text(text = value.toString()) PlusButton(onClick = onTapMinus) } }

Slide 61

Slide 61 text

State Hoisting @Composable fun BurgerCard() { ... Column { ... var count by remember { mutableStateOf(0) } NumberControl( value = count, onTapAdd = { count++ }, onTapMinus = { count++ }, ) } }

Slide 62

Slide 62 text

State Hoisting in Screen Composables

Slide 63

Slide 63 text

Android Studio’s preview feature works better with stateless composables Lorem ipsum Why do we need that?

Slide 64

Slide 64 text

@Composable fun MovieScreen( viewModel: MovieViewModel = hiltViewModel() ) { val movieList = viewModel.getMoviesFlow().collectAsState(emptyList()) LazyColumn(Modifier.fillMaxSize()) { items(items = movieList.value, key = { it.id }) { movie -> MovieListItem(movie) } } }

Slide 65

Slide 65 text

@Preview @Composable fun MovieScreen( viewModel: MovieViewModel = hiltViewModel() ) { val movieList = viewModel.getMoviesFlow().collectAsState(emptyList()) LazyColumn(Modifier.fillMaxSize()) { items(items = movieList.value, key = { it.id }) { movie -> MovieListItem(movie) } } }

Slide 66

Slide 66 text

How do we fix that?

Slide 67

Slide 67 text

We can use Stateful + Stateless Composables Stateless Composables

Slide 68

Slide 68 text

@Composable fun MovieScreen(viewModel: MovieViewModel) { val movieList by viewModel.getMoviesFlow().collectAsState(emptyList()) MovieScreenStateless(movieList) } @Preview @Composable fun MovieScreenStateless( movieList: List = dummyMovieList ) { ... }

Slide 69

Slide 69 text

@Composable fun MovieScreen(viewModel: MovieViewModel) { val movieList by viewModel.getMoviesFlow().collectAsState(emptyList()) MovieScreenStateless(movieList) } @Preview @Composable fun MovieScreenStateless( movieList: List = dummyMovieList ) { ... }

Slide 70

Slide 70 text

@Composable fun MovieScreen(viewModel: MovieViewModel) { val movieList by viewModel.getMoviesFlow().collectAsState(emptyList()) MovieScreenStateless(movieList) } @Preview @Composable fun MovieScreenStateless( movieList: List = dummyMovieList ) { ... }

Slide 71

Slide 71 text

What if there are multiple states?

Slide 72

Slide 72 text

@Composable fun MovieScreen(viewModel: MovieViewModel) { val movieList by viewModel.getMoviesFlow().collectAsState(emptyList()) MovieScreenStateless(movieList) } @Preview @Composable fun MovieScreenStateless( movieList: List = dummyMovieList, onTapItem: (Movie) -> Unit, onTapFavorite: (Movie) -> Unit, ... ) { ... }

Slide 73

Slide 73 text

We expose a ScreenState class Stateless Composables

Slide 74

Slide 74 text

// MovieScreenState.kt data class MovieScreenState( val movieList: State> = mutableStateOf(emptyList()), val onTapMovie: (Movie) -> Unit = {} )

Slide 75

Slide 75 text

// MovieScreenState.kt data class MovieScreenState( val movieList: State> = mutableStateOf(emptyList()), val onTapMovie: (Movie) -> Unit = {} ) { companion object { val previewState by lazy { MovieScreenState( movieList = mutableStateOf( listOf( Movie(1, "Fast And Furious"), Movie(2, "Hobbs & Shaw"), Movie(3, "Venom 2") ) ) ) } } }

Slide 76

Slide 76 text

// MovieScreen.kt @Composable fun MovieScreen(viewModel: MovieViewModel) { MovieScreenStateless( state = MovieScreenState( movieList = viewModel.getMoviesFlow().collectAsState(emptyList()), onTapMovie = viewModel::onTapMovie, onTapFavorite = viewModel::onTapFavorite ) ) } @Preview @Composable fun MovieScreenStateless( state: MovieScreenState = MovieScreenState.previewState ) { ... }

Slide 77

Slide 77 text

// MovieScreen.kt @Composable fun MovieScreen(viewModel: MovieViewModel) { MovieScreenStateless( state = MovieScreenState( movieList = viewModel.getMoviesFlow().collectAsState(emptyList()), onTapMovie = viewModel::onTapMovie, onTapFavorite = viewModel::onTapFavorite ) ) } @Preview @Composable fun MovieScreenStateless( state: MovieScreenState = MovieScreenState.previewState ) { ... }

Slide 78

Slide 78 text

// MovieScreen.kt @Composable fun MovieScreen(viewModel: MovieViewModel) { MovieScreenStateless( state = MovieScreenState( movieList = viewModel.getMoviesFlow().collectAsState(emptyList()), onTapMovie = viewModel::onTapMovie ) ) } @Preview @Composable fun MovieScreenStateless( state: MovieScreenState = MovieScreenState.previewState ) { ... }

Slide 79

Slide 79 text

Each Composable has different Responsibility Stateless Composables

Slide 80

Slide 80 text

State Classes Tips & Tricks

Slide 81

Slide 81 text

Expose factory methods for custom component states Stateless Composables

Slide 82

Slide 82 text

// CustomCalendar.kt @Composable fun CalendarScreen() { val calendarState = rememberCalendarState() CustomCalendar(state = calendarState) } @Preview @Composable fun CustomCalendar( state: CalendarState = CalendarState.previewState ) { ... }

Slide 83

Slide 83 text

// CustomCalendarState.kt @Composable fun rememberCalendarState(): CalendarState = remember { CustomCalendarState() } interface CalendarState { ... } private class CustomCalendarState() : CalendarState { ... }

Slide 84

Slide 84 text

// CustomCalendarState.kt @Composable fun rememberCalendarState( parameter1: String = DEFAULT_PARAM_1, parameter2: Int = DEFAULT_PARAM_2, callBack: () -> Unit = {} ): CalendarState = remember { CustomCalendarState(parameter1, parameter2, callBack) } private class CustomCalendarState( val parameter1: String, val parameter2: Int, val callBack: () -> Unit ) : CalendarState

Slide 85

Slide 85 text

Best practices for Compose #bestpracticecompose

Slide 86

Slide 86 text

01 Used name parameter in compose 02 Avoid using fixed dimensions 03 Reuse as much as possible 04 Use helper classes 05 Use Effect Handler Top 5 best practices for Compose 06 Avoid as much recomposition as possible 07 Use keys argument for LazyLists for better performance

Slide 87

Slide 87 text

Adobe Stock#243026154 Used name parameters in compose ● So much more readable ● ဘာေတွကို parameter အေနနဲ ့ ထားခဲ့တယ်ဆိုတာ သဲသဲကွဲကွဲသိနိုင်တယ် ● So much clean in code ● Should use more than one parameter

Slide 88

Slide 88 text

@Composable fun MainScreen() { Icon( painter = painterResource(id = R.drawable.ic_logo), contentDescription = null // decorative element ) } Example

Slide 89

Slide 89 text

// Correct way to use onClick Lambdas ListItem( data = item, onClick = { /* Handle onClick */ } ) // Be careful with Trailing Lambdas ListItem( data = item ) { /* Handle onClick */ } Example 2

Slide 90

Slide 90 text

Adobe Stock#243026154 Reused as much as possible ● Less code ● Should use at least twice ● More reliability

Slide 91

Slide 91 text

Adobe Stock#243026154 Use helper classes ● Developer should not use some long parameters eg.textfield need color , it need style , need value change state and so on.

Slide 92

Slide 92 text

@Composable fun MainScreen() { var number: State = mutableStateOf(0) Text( text = “Example”, style = mediumStyle() ) } } @Composable fun mediumStyle(@FontRes font: Int = R.font.opensans_regular): TextStyle { return TextStyle( color = DARK, fontFamily = FontFamily(Font(font)), fontSize = dpToSp(dimensionResource(R.dimen.text_size_medium)) ) }

Slide 93

Slide 93 text

Adobe Stock#243026154 Avoid using fixed dimensions ● Dimensions ကို အေသေပးခဲ့မယ်ဆိုရင် larger screen မတူတာပဲြဖစ်ြဖစ် resolution ကွာတဲ့ြဖစ်ြဖစ် responsive မြဖစ်ဘူး ● အဲ့လိုမြဖစ်ေအာင် weight ေတွ, row and column ေတွနဲ ့ box ေတွသံုးပီး composables ေတွကို align လုပ်လို့ရတယ်

Slide 94

Slide 94 text

Adobe Stock#243026154 Use effect handlers The concept we called side effect ● LaunchedEffect Run suspend functions in the scope of composable ● DisposableEffect Effect that require cleanup ● SideEffect Publish Compose state to non-compose code ● produceState Convert non-compose state into Compose state ● derivedStateof Convert one or multiple state objects into another state ● snapshotFlow Convert Compose’s state into Flows

Slide 95

Slide 95 text

Adobe Stock#243026154 Avoid as much recomposition as possible ● Recomposition က ေစျး ကီးပါတယ်။ Recompose လုပ်တိုင်း layout positioning ကို အစက ြပန်လုပ်ရတာမလို့ တတ်နိုင်သေလာက်ေ ှာင်သင့်ပါ တယ် ● mutableState ကို သံုးတဲ့အခါ မလိုအပ်တဲ့ recomposition ေတွြဖစ်တတ်လို့ သတိထားသံုးသင့်ပါတယ်။

Slide 96

Slide 96 text

Be careful with mutableState

Slide 97

Slide 97 text

var transactionAmount by remember { mutableStateOf(0) } transactionAmount += 100 for (x in 0..5) { transactionAmount *= x Text(transactionAmount.toString()) }

Slide 98

Slide 98 text

var transactionAmount by remember { mutableStateOf(0) } transactionAmount += 100 for (x in 0..5) { transactionAmount *= x Text(transactionAmount.toString()) }

Slide 99

Slide 99 text

... transactionAmount *= 1 Text(transactionAmount.toString()) transactionAmount *= 2 Text(transactionAmount.toString()) transactionAmount *= 3 Text(transactionAmount.toString()) transactionAmount *= 4 Text(transactionAmount.toString()) transactionAmount *= 5 Text(transactionAmount.toString()) ...

Slide 100

Slide 100 text

val transactionAmounts = listOf(100, 200, 300, 400, 500) for (amount in transactionAmounts) { Text(amount.toString()) }

Slide 101

Slide 101 text

Optimize Layouts for least Recomposition

Slide 102

Slide 102 text

Avoid Recomposition @Composable fun BurgerCard() { Image(/**/) Column { Text(/**/) var count by remember { mutableStateOf(0) } NumberControl( value = count, onTapAdd = { count++ }, onTapMinus = { count++ }, ) } }

Slide 103

Slide 103 text

For better Ahead-of-time compilations Baseline Profiles

Slide 104

Slide 104 text

Thank you! Android Engineer@ Codigo @developer_ptut Thaw Zin Toe Android Engineer @Codigo @harryluu_96 Naing Aung Luu / Harry Pe Tut thawzintoe-ptut

Slide 105

Slide 105 text

No content

Slide 106

Slide 106 text

Android

Slide 107

Slide 107 text

iOS

Slide 108

Slide 108 text

Net Core

Slide 109

Slide 109 text

Node.js

Slide 110

Slide 110 text

Laravel

Slide 111

Slide 111 text

Java

Slide 112

Slide 112 text

Frontend (React.js)

Slide 113

Slide 113 text

React - Native