Slide 1

Slide 1 text

Say bye to Fragments with Conductor & Kotlin Miquel Beltran Android Dev @ nebenan.de @Miqubel

Slide 2

Slide 2 text

@Miqubel Slides & Code:

Slide 3

Slide 3 text

@Miqubel

Slide 4

Slide 4 text

@Miqubel

Slide 5

Slide 5 text

Activity

Slide 6

Slide 6 text

Activity Router Activity Router

Slide 7

Slide 7 text

Activity Router Controller 1 Controller 2 Controller 3 View

Slide 8

Slide 8 text

class MainActivity : AppCompatActivity() { private lateinit var router: Router override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) router = Conductor.attachRouter(this, controller_container, savedInstanceState) } @Miqubel

Slide 9

Slide 9 text

@Miqubel

Slide 10

Slide 10 text

class MainActivity : AppCompatActivity() { private lateinit var router: Router override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) router = Conductor.attachRouter(this, controller_container, savedInstanceState) if (!router.hasRootController()) { router.setRoot(RouterTransaction.with(MainController())) } } @Miqubel

Slide 11

Slide 11 text

Controller @Miqubel

Slide 12

Slide 12 text

class SimpleController : Controller() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { val view = inflater.inflate(R.layout.controller_simple, container, false) // Do something with the view return view } } @Miqubel

Slide 13

Slide 13 text

@Miqubel

Slide 14

Slide 14 text

router.pushController( RouterTransaction.with( OtherSimpleController() ) ) @Miqubel

Slide 15

Slide 15 text

override fun onBackPressed() { if (!router.handleBack()) { super.onBackPressed() } } router.popCurrentController() or @Miqubel

Slide 16

Slide 16 text

Transitions @Miqubel

Slide 17

Slide 17 text

@Miqubel

Slide 18

Slide 18 text

router.pushController( RouterTransaction.with(DummyController()) .popChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler()) ) router.pushController( RouterTransaction.with(DummyController()) .popChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler()) ) @Miqubel

Slide 19

Slide 19 text

class CustomChangeHandler : AnimatorChangeHandler() { override fun getAnimator( container: ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator { val animator = AnimatorSet() to?.let { animator.play( ObjectAnimator .ofFloat(to, View.SCALE_X, 0f, 1f) ) animator.play( ObjectAnimator .ofFloat(to, View.SCALE_Y, 0f, 1f) ) } return animator } }

Slide 20

Slide 20 text

@Miqubel

Slide 21

Slide 21 text

override fun getAnimator(…): Animator { val animator = AnimatorSet() to?.let { animator.play(…) // The "to" View has a child with id "image" animator.play( ObjectAnimator .ofFloat(to.image, View.ROTATION, -360f, 1f) ) } return animator } @Miqubel

Slide 22

Slide 22 text

@Miqubel

Slide 23

Slide 23 text

Lifecycle @Miqubel

Slide 24

Slide 24 text

New Controller instance @Miqubel

Slide 25

Slide 25 text

onCreateView Controller is on top Pushed to Router New Controller instance @Miqubel

Slide 26

Slide 26 text

onCreateView onDestroyView Controller is on top Pop from Router Config change. etc. New Controller instance @Miqubel

Slide 27

Slide 27 text

onCreateView onDestroyView Controller is on top Controller back on top New Controller instance @Miqubel

Slide 28

Slide 28 text

onCreateView onDestroyView Controller is on top New Controller instance Removed from Router @Miqubel ❌

Slide 29

Slide 29 text

Controller Class Properties @Miqubel

Slide 30

Slide 30 text

class MyController(val parameter: String) : Controller() { override fun onCreateView(…): View { val view = inflater.inflate(…) view.textView.text = "My favorite city is $parameter" return view } } ❌ DON’T! @Miqubel

Slide 31

Slide 31 text

class MyController(bundle: Bundle) : Controller(bundle) { constructor(parameter: String) : this(Bundle().apply { putString(EXTRA_PARAMETER, parameter) }) // Access arguments like class properties private val parameter by lazy { args.getString(EXTRA_PARAMETER) } //… view.textView.text = "My favorite city is $parameter" @Miqubel

Slide 32

Slide 32 text

Accessing Views @Miqubel

Slide 33

Slide 33 text

fun changeText(text: String) { // access to views with KAE view?.run { textView.text = text } } @Miqubel

Slide 34

Slide 34 text

class MyController : Controller() { // Warning! this will leak on onDestroyView lateinit var textView: TextView override fun onCreateView(…): View { val view = inflater.inflate(…) // Store a reference to the textView for later textView = view.textView return view } ❌ DON’T! @Miqubel

Slide 35

Slide 35 text

Conductor & Arch. Comp. @Miqubel

Slide 36

Slide 36 text

model = ViewModelProviders .of(activity as AppCompatActivity) .get(MyViewModel::class.java) model.getLiveData() .observe( activity as AppCompatActivity, Observer { // Use the LiveData values } ) ❌ DON’T! @Miqubel

Slide 37

Slide 37 text

abstract class BaseViewModelController : LifecycleController() { private val viewModelStore = ViewModelStore() fun viewModelProvider(factory): ViewModelProvider { return ViewModelProvider(viewModelStore, factory) } } @Miqubel Ref: https://github.com/bluelinelabs/Conductor/pull/405

Slide 38

Slide 38 text

class ViewModelController : BaseViewModelController() { model = viewModelProvider() .get(MyViewModel::class.java) model.getLiveData() .observe(this, Observer { //… }) @Miqubel

Slide 39

Slide 39 text

Dependency Injection (Dagger) @Miqubel

Slide 40

Slide 40 text

class MyDaggerController : Controller() { @Inject lateinit var value: String override fun onCreateView(…): View { val view = inflater.inflate(…) (activity!!.application as App).component.inject(this) return view } } @Miqubel

Slide 41

Slide 41 text

Testing Controllers @Miqubel

Slide 42

Slide 42 text

@RunWith(AndroidJUnit4::class) class TestableControllerEspressoTest { @get:Rule val activity = ActivityTestRule( TestActivity::class.java ) } @Miqubel

Slide 43

Slide 43 text

@RunWith(AndroidJUnit4::class) class TestableControllerEspressoTest { @get:Rule val activity = ActivityTestRule… val controller = TestableController() @Before fun setUp() { activity.runOnUiThread { activity.activity.router.setRoot( RouterTransaction.with(controller) ) } } } @Miqubel

Slide 44

Slide 44 text

@RunWith(AndroidJUnit4::class) class TestableControllerEspressoTest { //… @Test fun a_click_action() { // Call to Controller methods directly activity.runOnUiThread { controller.onClickDoAction() } // Check views onView(withId(R.id.textView)) .check(matches(withText(“Test is running"))) } } @Miqubel

Slide 45

Slide 45 text

Recap @Miqubel View based apps on Android

Slide 46

Slide 46 text

Recap @Miqubel View based apps on Android Manage your view stack

Slide 47

Slide 47 text

Recap @Miqubel View based apps on Android Manage your view stack Nice transition animations

Slide 48

Slide 48 text

Recap @Miqubel View based apps on Android Manage your view stack Nice transition animations Surviving configuration changes

Slide 49

Slide 49 text

Recap @Miqubel View based apps on Android Manage your view stack Nice transition animations Surviving configuration changes Compatible with pres. patterns

Slide 50

Slide 50 text

Recap @Miqubel View based apps on Android Manage your view stack Nice transition animations Surviving configuration changes Compatible with pres. patterns Easy to test with Espresso

Slide 51

Slide 51 text

Library: https://github.com/bluelinelabs/Conductor Talk sample code: https://github.com/miquelbeltran/conductor-talk- demo Ask Me Anything: @Miqubel ありがとう! /ˌɑɹiˈɡɑtoʊ/