MVI with Jetpack Compose

MVI with Jetpack Compose

Slides for Mobilization IX

Ae7cc81ee2744719c66282681a219110?s=128

Luca Nicoletti

October 26, 2019
Tweet

Transcript

  1. luca_nicolett MVI with Jetpack Compose

  2. MVI luca_nicolett

  3. luca_nicolett MVI VIEW INTENT MODEL T USER Updates Manipulates Interacts

    Sees
  4. luca_nicolett MVI Core principles: • Unidirectional • Immutability • Reactiveness

    • Functional
  5. luca_nicolett MVI

  6. luca_nicolett MVI UI REDUCER BUSINESS 
 LOGIC Actions Events State

    UI 
 Listens STATE
  7. luca_nicolett MVI SOMETHING REDUCER ACTION
 RELAY Actions State
 Updates STATE


    RELAY TRANSFORMERS Actions States Events
  8. luca_nicolett MVI • Actions • Transformers • Reducers • States

  9. luca_nicolett MVI The middleware • Works as a glue •

    Binds actions to transformers • Transformations to reducers • Actions to reducers
  10. luca_nicolett MVI perform("rating selection") .on<ActionFeedbackAction.RatingSelected>() .withReducer(reducers::reduceRatingSelected)

  11. luca_nicolett MVI perform("load logged in patient data") .on( LifecycleAction.Created::class.java, ProfileScreenAction.RetryConnection::class.java,

    ProfileScreenAction.MonitorReady::class.java ) .transform { transformers.loadPatient(this) } .withReducer(reducers::reduceLoggedInPatient)
  12. luca_nicolett MVI perform("load logged in patient data") .on( LifecycleAction.Created::class.java, ProfileScreenAction.RetryConnection::class.java,

    ProfileScreenAction.MonitorReady::class.java ) .transform { transformers.loadPatient(this) } .withReducer(reducers::reduceLoggedInPatient)
  13. luca_nicolett MVI How does it help? • Decoupling • Testing

  14. luca_nicolett MVI

  15. luca_nicolett MVI

  16. luca_nicolett MVI Fragment Renderer Activity render()

  17. luca_nicolett MVI private fun render(state: HomeScreenRedesignState) { loading_container .show(state.loadingState ==

    LoadingState.Loading) unable_to_connect_error_container .show(state.loadingState == LoadingState.Error) /* ... */ }
  18. luca_nicolett MVI fun View.show(isVisible: Boolean = true) { this.visibility =

    if (isVisible) View.VISIBLE else View.GONE }
  19. luca_nicolett MVI private fun render(state: EditMoodState) { submitEditsButton?.isEnabled = state.hasAppliedChanges

    /* ... */ }
  20. luca_nicolett MVI private fun render(state: AddMoodState) { if (state.showUrgentHelpDialog) {

    showUrgentHelpDialog() return } /* ... */ }
  21. luca_nicolett MVI Coming soon

  22. Jetpack Compose luca_nicolett

  23. luca_nicolett Jetpack Compose Google I/O 2019 Android Dev Summit 2019

  24. luca_nicolett Jetpack Compose Google I/O 2019 Android Dev Summit 2019

  25. luca_nicolett Jetpack Compose • Declarative UI

  26. luca_nicolett Jetpack Compose Declarative -> describe what you would like

    Procedural -> describe how to achieve it
  27. luca_nicolett Android - “old” <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:layout_gravity="center_horizontal"> <TextView

    android:layout_width="match_parent" android:layout_height="wrap_content" android:text="First block" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Second block" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Third block" /> </LinearLayout>
  28. luca_nicolett Flutter Column( children: [ Text('First block'), Text('Second block'), Text('third

    block') ] )
  29. luca_nicolett Android - Jetpack Compose Column ( crossAxisAlignment = Center

    ) { Text("First block") Text("Second block") Text("Third block") }
  30. luca_nicolett Jetpack Compose • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible
  31. luca_nicolett Jetpack Compose @Composable
 @GenerateView fun Greetings(name: String) { /*

    ... */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  32. luca_nicolett Jetpack Compose @Composable
 @GenerateView fun Greetings(name: String) { /*

    ... */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  33. luca_nicolett Jetpack Compose @Composable
 @GenerateView fun Greetings(name: String) { /*

    ... */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  34. luca_nicolett Jetpack Compose • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible • Unbundled from the OS
  35. luca_nicolett Jetpack Compose buildFeatures { // Enables Jetpack Compose for

    this module compose true } // Set both the Java and Kotlin compilers to target Java 8. compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" }
  36. luca_nicolett Jetpack Compose buildscript { ... dependencies { classpath "org.android.tools.build:gradle:4.0.0-alpha01"

    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2" } }
  37. luca_nicolett Jetpack Compose dependencies { // Include the following Compose

    toolkit dependencies. implementation "androidx.ui:ui-framework:0.1.0-dev02" implementation "androidx.ui:ui-tooling:0.1.0-dev02" implementation "androidx.ui:ui-layout:0.1.0-dev02" implementation "androidx.ui:ui-material:0.1.0-dev02" }
  38. luca_nicolett Jetpack Compose dependencies { impl "androidx.compose:compose-runtime:0.1.0-dev02" }

  39. luca_nicolett Jetpack Compose

  40. luca_nicolett Jetpack Compose mkdir ~/bin PATH=~/bin:$PATH curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo

    chmod a+x ~/bin/repo
  41. luca_nicolett Jetpack Compose mkdir androidx-master-dev cd androidx-master-dev repo init -u

    https://android.googlesource.com/platform/ manifest -b androidx-master-dev repo sync -j8 -c
  42. luca_nicolett Jetpack Compose mkdir androidx-master-dev cd androidx-master-dev repo init -u

    https://android.googlesource.com/platform/ manifest -b androidx-master-dev repo sync -j8 -c Download the code (and grab a coffee while we pull down 6GB)
  43. luca_nicolett Jetpack Compose cd path/to/checkout/frameworks/support/ ./studiow

  44. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  45. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  46. luca_nicolett Jetpack Compose fun Activity.setContent(content: @Composable() () -> Unit) :

    CompositionContext? { /* ... */ }
  47. luca_nicolett Jetpack Compose fun Activity.setContent(content: @Composable() () -> Unit) :

    CompositionContext? { val craneView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this) .also { setContentView(it) } val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients( craneView, this, coroutineContext ) {
  48. luca_nicolett Jetpack Compose fun Activity.setContent(content: @Composable() () -> Unit) :

    CompositionContext? { val craneView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this) .also { setContentView(it) } val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients( craneView, this, coroutineContext ) {
  49. luca_nicolett .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this) .also { setContentView(it)

    } val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients( craneView, this, coroutineContext ) { content() } } } Jetpack Compose
  50. luca_nicolett Jetpack Compose @Composable fun Greeting(name: String) { Text (text

    = "Hello $name!") }
  51. luca_nicolett Jetpack Compose @Composable fun Greeting(name: String) { Text (text

    = "Hello $name!") } @Preview @Composable fun PreviewGreeting() { Greeting("Android") }
  52. luca_nicolett Jetpack Compose

  53. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { 


    /* ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) }
 /* ... */ }
  54. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { 


    /* ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) }
 /* ... */ }
  55. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { 


    /* ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) 
 // Paints the text onto the given canvas. }
 /* ... */ }
  56. luca_nicolett Jetpack Compose Google I/O 2019

  57. luca_nicolett Jetpack Compose

  58. luca_nicolett Jetpack Compose Android Dev Summit 2019

  59. luca_nicolett Jetpack Compose @Composable fun TextField(/* ... */) { //

    States val hasFocus = +state { false } val coords = +state<LayoutCoordinates?> { null } /* ... */ }
  60. luca_nicolett Jetpack Compose @CheckResult("+") /*inline*/ fun <T> state(/*crossinline*/ init: ()

    -> T) = memo { State(init()) }
  61. luca_nicolett Jetpack Compose /** * The State class is an

    @Model class meant to * wrap around a single value. * It is used in the `+state` and `+stateFor` effects. */ @Model class State<T> @PublishedApi internal constructor(value: T) : Framed { }
  62. luca_nicolett Jetpack Compose /** * [Model] can be applied to

    a class which represents your * application's data model, and will cause instances of the * class to become observable, such that a read of a property * of an instance of this class during the invocation of a * composable function will cause that component to be * "subscribed" to mutations of that instance. Composable * functions which directly or indirectly read properties * of the model class, the composables will be recomposed * whenever any properties of the the model are written to. */
  63. luca_nicolett Jetpack Compose @Model data class TaskModel( var isDone: Boolean,

    val description: String )
  64. luca_nicolett Jetpack Compose @Model data class TaskModel( var isDone: Boolean,

    val description: String ): BaseModel()
  65. luca_nicolett Jetpack Compose @Model data class TaskModel( var isDone: Boolean,

    val description: String ): BaseModel() TaskModel.kt: (14, 3): Model objects do not support inheritance
  66. luca_nicolett Jetpack Compose

  67. luca_nicolett Jetpack Compose @Composable fun DrawSeekBar(x: Float) { var paint

    = +memo { Paint() } Draw { canvas, parentSize -> /* ... */ canvas.drawRect(Rect(/* ... */), paint) canvas.drawRect(Rect(/* ... */), paint) canvas.drawCircle(/* ... */, paint) } }
  68. luca_nicolett Jetpack Compose

  69. luca_nicolett Jetpack Compose @Composable fun AlertDialog( onCloseRequest: () -> Unit,

    title: (@Composable() () -> Unit)? = null, text: (@Composable() () -> Unit), confirmButton: (@Composable() () -> Unit), dismissButton: (@Composable() () -> Unit)?, buttonLayout: AlertDialogButtonLayout ) { Dialog(onCloseRequest = onCloseRequest) { /* ... */ } }
  70. luca_nicolett Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit,

    children: @Composable() () -> Unit ) { val context = +ambient(ContextAmbient) val dialog = +memo { DialogWrapper(context, onCloseRequest) } }
  71. luca_nicolett Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit,

    children: @Composable() () -> Unit ) { +onActive { dialog.show() onDispose { dialog.dismiss() dialog.disposeComposition() } } }
  72. luca_nicolett Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit,

    children: @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }
  73. luca_nicolett Jetpack Compose

  74. luca_nicolett Jetpack Compose @Composable fun RippleRect() { val radius =

    withDensity(+ambientDensity()) { TargetRadius.toPx() } val toState = +state { ButtonStatus.Initial } val rippleTransDef = +memo { createTransDef(radius.value) } val onPress: (PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed
  75. luca_nicolett Jetpack Compose } val toState = +state { ButtonStatus.Initial

    } val rippleTransDef = +memo { createTransDef(radius.value) } val onPress: (PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed } val onRelease: () -> Unit = { toState.value = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) {
  76. luca_nicolett Jetpack Compose } val onRelease: () -> Unit =

    { toState.value = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) { Transition( definition = rippleTransDef, toState = toState.value ) { state -> RippleRectFromState(state = state) } } } }
  77. luca_nicolett @Composable fun RippleRectFromState(state: TransitionState) { // TODO: file bug

    for when "down" is not a // file level val, it's not memoized correctly val x = down.x val y = down.y val paint = Paint().apply { color = Color( alpha = getAlpha(), red = 0, Jetpack Compose
  78. luca_nicolett // file level val, it's not memoized correctly val

    x = down.x val y = down.y val paint = Paint().apply { color = Color( alpha = getAlpha(), red = 0, green = 235, blue = 224 ) } val radius = state[radius] Jetpack Compose
  79. luca_nicolett green = 235, blue = 224 ) } val

    radius = state[radius] Draw { canvas, _ -> canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose
  80. luca_nicolett canvas.drawCircle( Offset(x, y), radius, paint ) } } fun

    getAlpha() = state[ androidx.ui.animation.demos.alpha ] * 255).toInt() Jetpack Compose
  81. luca_nicolett Jetpack Compose @Composable fun Padding( padding: EdgeInsets, children: @Composable()

    () -> Unit ) { /* ... */ }
  82. luca_nicolett Jetpack Compose data class PaddingModifier( val left: IntPx =

    0.ipx, val top: IntPx = 0.ipx, val right: IntPx = 0.ipx, val bottom: IntPx = 0.ipx ) : LayoutModifier { /* ... */ }
  83. luca_nicolett Jetpack Compose /** * An immutable chain of [modifier

    elements][Modifier.Element] * for use with Composables. A Composable that has a `Modifier` * can be considered decorated or wrapped by that `Modifier`. * * Composables that accept a [Modifier] as a parameter to be * applied to the whole component represented by the composable * function should name the parameter `modifier` and assign the * parameter a default value of [Modifier.None] */
  84. luca_nicolett Jetpack Compose

  85. All together luca_nicolett

  86. luca_nicolett All together OOP approach Functional approach

  87. All together Object Oriented luca_nicolett

  88. luca_nicolett All together interface BaseViewState { @Composable fun buildUI() }

  89. luca_nicolett All together object InProgress : BaseViewState(true, null) { @Composable

    override fun buildUI() { Container(expanded = true) { CircularProgressIndicator() } } }
  90. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 viewModel

    = /* view model init */
 setContent {
 CustomTheme {
 render(viewModel.states())
 }
 }
 /* .. */
 }
  91. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 viewModel

    = /* view model init */
 setContent {
 CustomTheme {
 render(viewModel.states())
 }
 }
 /* .. */
 }
  92. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 viewModel

    = /* view model init */
 setContent {
 CustomTheme {
 render(viewModel.states())
 }
 }
 /* .. */
 }
  93. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 viewModel

    = /* view model init */
 setContent {
 CustomTheme {
 render(viewModel.states())
 }
 }
 /* .. */
 }
  94. luca_nicolett All together override fun render( observableState: Observable<BaseViewState> ) {

    val state = +observe(ViewState.Idle, observableState) state.buildUI() }
  95. luca_nicolett All together override fun render( observableState: Observable<BaseViewState> ) {

    val state = +observe(ViewState.Idle, observableState) state.buildUI() }
  96. luca_nicolett All together override fun render( observableState: Observable<BaseViewState> ) {

    val state = +observe(ViewState.Idle, observableState) state.buildUI() }
  97. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  98. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  99. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  100. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  101. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  102. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  103. luca_nicolett All together

  104. luca_nicolett All together /** * Disposes of a composition that

    was started using [setContent]. * This is a convenience method around [Compose.disposeComposition] */ fun Activity.disposeComposition() { val view = window .decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? ViewGroup ?: error("No root view found") Compose.disposeComposition(view, null) }
  105. luca_nicolett All together /** * Disposes any composition previously run

    with [container] * as the root. This will release any resources that have * been built around the composition, including all * [onDispose] callbacks that have been registered with * [CompositionLifecycleObserver] objects. */ @MainThread fun disposeComposition( container: ViewGroup, parent: CompositionReference? = null ) {
  106. luca_nicolett * [onDispose] callbacks that have been registered with *

    [CompositionLifecycleObserver] objects. */ @MainThread fun disposeComposition( container: ViewGroup, parent: CompositionReference? = null ) { // need to remove compositionContext from context map composeInto(container, parent) { } container.setTag(TAG_ROOT_COMPONENT, null) } All together
  107. luca_nicolett All together fun <T> observe(initialState: T, data: Observable<T>) =

    effectOf<T> { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } } return result.value }
  108. All together Functional luca_nicolett

  109. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent

    { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  110. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent

    { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  111. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent

    { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  112. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent

    { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  113. luca_nicolett All together fun processIntents(intents: Observable<PostListIntent>): PostListViewState { intents.subscribe(intentsObserver) return

    modelState()
 } }
  114. luca_nicolett All together fun modelState() = +effectOf<PostListViewState> { val result

    = +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value
  115. luca_nicolett All together fun modelState() = +effectOf<PostListViewState> { val result

    = +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value
  116. luca_nicolett fun modelState() = +effectOf<PostListViewState> { val result = +state

    { PostListViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value } All together
  117. luca_nicolett val result = +state { PostListViewState.Idle } var disposable:

    Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value } All together
  118. luca_nicolett +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value

    = it } } +onDispose { disposable?.dispose() } result.value } All together
  119. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent

    { PostListScreen( processIntents(intents()) ) } }
  120. luca_nicolett All together @Composable fun PostListScreen( state: PostListViewState ) {

    when (state) { PostListViewState.InProgress -> { Container(expanded = true) { CircularProgressIndicator() } } /* ... */ } }
  121. luca_nicolett All together PostListScreen(processIntents(intents()))

  122. luca_nicolett All together

  123. luca_nicolett All together PostListScreen( processIntents( intents() ) )

  124. luca_nicolett References MVI http://hannesdorfmann.com/android/model-view-intent https://www.novatec-gmbh.de/en/blog/mvi-in-android/ https://cycle.js.org/model-view-intent.html Jetpack Compose https://www.youtube.com/watch?v=VsStyq4Lzxo https://medium.com/q42-engineering/android-jetpack-compose-895b7fd04bf4

    https://blog.karumi.com/android-jetpack-compose-review https://courses.csail.mit.edu/6.831/2006/lectures/L9.pdf http://intelligiblebabble.com/compose-from-first-principles/ http://intelligiblebabble.com/content-on-declarative-ui/ https://www.youtube.com/watch? v=Q9MtlmmN4Q0&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=60 https://www.youtube.com/watch? v=SPsdRXcgqjI&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=8&t=0s https://www.youtube.com/watch? v=XPMrnR1_Biw&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=13&t=0s
  125. luca_nicolett Thank you!