Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scaling Productivity - How we have improved our...

Scaling Productivity - How we have improved our dev experience

Avatar for Gabriel Ittner

Gabriel Ittner

July 07, 2022
Tweet

More Decks by Gabriel Ittner

Other Decks in Programming

Transcript

  1. How do we measure Engineering Productivity? • You can't improve

    what you can't measure • Google: DORA • It depends! For Freeletics: • Reduce time to write testable and maintainable code • PR review time • Rework Rate / Number of bugs • Repetitive tasks • Time to onboard new joiners • Time to create a new mobile app release • Build times / CI cost
  2. Approach • Lean start up principles • Discovery • User

    interviews • Ship increments • Test things out in front of real users • Part time model: feature development + engineering productivity team member to feel real world pain
  3. The code base • 600 Gradle modules in a monorepo

    • Unidirectional data flows • Compose for anything new • Dagger + Anvil • AndroidX Navigation • Fragments because of legacy
  4. Repetitive tasks • each new module requires a lot of

    classes Time to onboard new • di ff erent structure in modules • hard to navigate our code base Observations and opportunities Time to onboard Repetitive tasks
  5. class ExampleStateMachine @Inject constructor( ) : FlowReduxStateMachine<ExampleState, ExampleAction>() { init

    { spec { } } } class ExampleNavigator @Inject constructor() : NavEventNavigator() { } @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, ) { }
  6. Learnings • Start with simple easy to built tools •

    Developers will give feedback and want more when using it • Example for tools • Formatting • IDE templates • Taking screenshots/gifs from a device • Shorthands for common commands or Gradle tasks
  7. Repetitive tasks • developer still write lot of code to

    glue classes together Rework rate • prone to simple bugs (i.e. lifecycle) Observations and opportunities Rework Rate Repetitive tasks
  8. @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, )

    class ExampleStateMachine : FlowReduxStateMachine<ExampleState, ExampleAction>() Set up code
  9. @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, )

    class ExampleStateMachine : FlowReduxStateMachine<ExampleState, ExampleAction>() Set up code
  10. @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, )

    class ExampleStateMachine : FlowReduxStateMachine<ExampleState, ExampleAction>() How do they communicate? Set up code
  11. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  12. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  13. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  14. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  15. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  16. @Composable fun ExampleUiScreen(…): Unit { val state = produceState<ExampleState?>(initialValue =

    null) { stateMachine.state.collect { value = it } } if (state.value != null) { ExampleUi(state.value) { action -> coroutineScope.launch { stateMachine.dispatch(action) } } } } Set up code
  17. Observations and opportunities Rework rate • Common Fragment issues like

    issues with lifecycle • Tying logic to system components Rework Rate
  18. public class ExampleUiFragment : Fragment() { public override fun onCreateView(

    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { return ComposeView(requireContext()).apply { setContent { ExampleUiScreen(...) } } } } Fragments
  19. public class ExampleUiFragment : Fragment() { public override fun onCreateView(

    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { return ComposeView(requireContext()).apply { setContent { ExampleUiScreen(...) } } } } Fragments
  20. public class ExampleUiFragment : Fragment() { public override fun onCreateView(

    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { return ComposeView(requireContext()).apply { setContent { ExampleUiScreen(...) } } } } Fragments
  21. 😕 Problems • Code that came from the template is

    hard to update afterwards • Parts of the template that are not meant to be modified will end up being modified anyways
  22. @ComposeFragment( scope = ExampleScope::class, parentScope = AppScope::class, stateMachine = ExampleStateMachine::class,

    ) @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, ) Template → Codegen
  23. @ComposeFragment( scope = ExampleScope::class, parentScope = AppScope::class, stateMachine = ExampleStateMachine::class,

    ) @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, ) Codegen
  24. @ComposeFragment( scope = ExampleScope::class, parentScope = AppScope::class, stateMachine = ExampleStateMachine::class,

    ) @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, ) Codegen
  25. @ComposeFragment( scope = ExampleScope::class, parentScope = AppScope::class, stateMachine = ExampleStateMachine::class,

    ) @Composable fun ExampleUi( state: ExampleState, sendAction: (ExampleAction) -> Unit, ) Codegen
  26. class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init {

    binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  27. class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init {

    binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  28. class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init {

    binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  29. class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init {

    binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  30. class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init {

    binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  31. @RendererFragment( scope = ExampleScope::class, parentScope = AppScope::class, stateMachine = ExampleStateMachine::class,

    ) class ExampleRenderer( binding: ExampleViewBinding ): ViewRenderer<ExampleState, ExampleAction>(binding) { init { binding.button.setOnClickListener { sendAction(ExampleAction.ButtonClicked) } } override fun renderToView(state: ExampleState) { } } Codegen for Views
  32. @ScopeTo(ExampleScope::class) @ContributesSubcomponent( scope = ExampleScope::class, parentScope = AppScope::class, ) interface

    ExampleComponent { val stateMachine: ExampleStateMachine @ContributesSubcompont.Factory interface Factory { fun create(): ExampleComponent } @ContributesTo(AppScope::class) interface ParentComponent { fun exampleComponentFactory(): Factory } }
  33. @ScopeTo(ExampleScope::class) @ContributesSubcomponent( scope = ExampleScope::class, parentScope = AppScope::class, ) interface

    ExampleComponent { val stateMachine: ExampleStateMachine @ContributesSubcompont.Factory interface Factory { fun create(): ExampleComponent } @ContributesTo(AppScope::class) interface ParentComponent { fun exampleComponentFactory(): Factory } }
  34. @ScopeTo(ExampleScope::class) @ContributesSubcomponent( scope = ExampleScope::class, parentScope = AppScope::class, ) interface

    ExampleComponent { val stateMachine: ExampleStateMachine @ContributesSubcompont.Factory interface Factory { fun create(): ExampleComponent } @ContributesTo(AppScope::class) interface ParentComponent { fun exampleComponentFactory(): Factory } } @ComposeFragment( scope = ExampleScope::class, parentScope = AppScope::class, )
  35. @ScopeTo(ExampleScope::class) @ContributesSubcomponent( scope = ExampleScope::class, parentScope = AppScope::class, ) interface

    ExampleComponent { val stateMachine: ExampleStateMachine @ContributesSubcompont.Factory interface Factory { fun create(): ExampleComponent } @ContributesTo(AppScope::class) interface ParentComponent { fun exampleComponentFactory(): Factory } }
  36. class ExampleViewModel( parentComponent: ExampleComponent.ParentComponent, ) : ViewModel() { val component:

    ExampleComponent = parentComponent.exampleComponentFactory().create() }
  37. class ExampleStateMachine @Inject constructor( ) : FlowReduxStateMachine<ExampleState, ExampleAction>() { init

    { spec { } } } class ExampleNavigator @Inject constructor() : NavEventNavigator() { } Surviving config changes
  38. @ScopeTo(ExampleScope::class) class ExampleStateMachine @Inject constructor( ) : FlowReduxStateMachine<ExampleState, ExampleAction>() {

    init { spec { } } } @ScopeTo(ExampleScope::class) class ExampleNavigator @Inject constructor() : NavEventNavigator() { } Surviving config changes
  39. @ScopeTo(ExampleScope::class) class ExampleStateMachine @Inject constructor( ) : FlowReduxStateMachine<ExampleState, ExampleAction>() {

    init { spec { } } } @ScopeTo(ExampleScope::class) class ExampleNavigator @Inject constructor() : NavEventNavigator() { } Surviving config changes
  40. @Module object ExampleModule { @Provides fun provideExampleApi(retrofit: Retrofit): ExampleApi =

    retrofit.create(ExampleApi::class) } interface ExampleRepository class RealExampleRepository @Inject constructor( api: ExampleApi ) : ExampleRepository Anvil
  41. @Module @ContributesTo(ExampleScope::class) object ExampleModule { @Provides fun provideExampleApi(retrofit: Retrofit): ExampleApi

    = retrofit.create(ExampleApi::class) } interface ExampleRepository class RealExampleRepository @Inject constructor( api: ExampleApi ) : ExampleRepository Anvil
  42. @Module @ContributesTo(ExampleScope::class) object ExampleModule { @Provides fun provideExampleApi(retrofit: Retrofit): ExampleApi

    = retrofit.create(ExampleApi::class) } interface ExampleRepository @ContributesBinding(ExampleScope::class, ExampleRepository::class) class RealExampleRepository @Inject constructor( api: ExampleApi ) : ExampleRepository Anvil
  43. @Module @ContributesTo(ExampleScope::class) object ExampleModule { @Provides fun provideExampleApi(retrofit: Retrofit): ExampleApi

    = retrofit.create(ExampleApi::class) } interface ExampleRepository @ContributesBinding(ExampleScope::class, ExampleRepository::class) class RealExampleRepository @Inject constructor( api: ExampleApi ) : ExampleRepository Anvil
  44. Learnings • Start with simple easy to built tools •

    Iterate after trying out and seeing what works well • Isolating system components and code generation make updates easy
  45. Observations and opportunities PR Review Time • Domain layer logic

    is hard to read Rework rate • async. code is hard (i.e. cancelation, race conditions, ... ) • testing code is hard • building reusable logic is hard to get right • fragile code, edge cases PR review time Rework Rate
  46. ...

  47. ...

  48. DSL

  49. class WorkoutStateMachine: FlowReduxStateMachine<WorkoutState, WorkoutAction>() { init { spec { inState<CountdownState>{

    onEnter{ state -> decreaseCountdownTimer(state) } } inState<RepetitionExercise>(){ on<ClickAction>(){ state -> moveToNextExercise(state) } } inState<RestingState>{ onEnter{ decreaseRestTimer() } on<SkipClickedAction>{ state -> moveToNextExercise(state) } } } } } github.com/freeletics/FlowRedux
  50. class WorkoutStateMachine: FlowReduxStateMachine<WorkoutState, WorkoutAction>() { init { spec { inState<CountdownState>{

    onEnter{ state -> decreaseCountdownTimer(state) } } inState<RepetitionExercise>(){ on<ClickAction>(){ state -> moveToNextExercise(state) } } inState<RestingState>{ onEnter{ decreaseRestTimer() } on<SkipClickedAction>{ state -> moveToNextExercise(state) } } } } } github.com/freeletics/FlowRedux
  51. class ExampleStateMachineTest { @Test fun `countdown decreases`() = runTest {

    val workout : Workout = ... val initialState = CountdownState(timeLeft = 3) val stateMachine = WorkoutStateMachine(workout, state) stateMachine.state.test { // Turbine assertEquals( CountdownState(3), awaitItem() ) assertEquals( CountdownState(2), awaitItem() ) assertEquals( CountdownState(1), awaitItem() ) assertEquals( RepetitionExerciseState(workout.exercises[0]), awaitItem() ) } } } github.com/cashapp/turbine
  52. spec { inState<CountdownState>{ onEnter{ getState, setState -> decreaseCountdown() } }

    suspend fun decrementCountdown(getState : GetState, setState : SetState){ val state : CountdownState = getState() delay(1_000) setState( state.copy(timeLeft = state.timeLeft - 1) ) }
  53. spec { inState<CountdownState>{ onEnter{ state : CountdownState -> decrementCountdown(state) }

    } suspend fun decrementCountdown( state: CountdownState ): ChangedState<WorkoutState> { delay(1_000) 
 return MutateState<CountdownState, WorkoutState> { this.copy(timeLeft = this.timeLeft - 1) } }
  54. spec { inState<CountdownState>{ onEnter{ state : CountdownState -> decrementCountdown(state) }

    } suspend fun decrementCountdown( state: State<CountdownState> ): ChangedState<WorkoutState> { delay(1_000) 
 return state.mutate { this.copy(timeLeft = this.timeLeft - 1) } }
  55. • Navigation is hard to scale Time to onboard •

    Hard to know how to navigate to another teams feature Observations and opportunities Time to onboard
  56. data class TweetFragmentArgs( val itemId: Long = -1L ) {

    fun toBundle(): Bundle { } companion object { @JvmStatic fun fromBundle(bundle: Bundle): TweetFragmentArgs { } } } safe-args
  57. data class TweetFragmentArgs( val itemId: Long = -1L ) {

    fun toBundle(): Bundle { } companion object { @JvmStatic fun fromBundle(bundle: Bundle): TweetFragmentArgs { } } } Boilerplate?
  58. Boilerplate? fun NavController.navigateTo(route: NavRoute) { val args = Bundle().putParcelable("route", route)

    // call actual navigate } fun <T : NavRoute> Fragment.requireRoute() { return requireArguments().getParcelable<T>("route") }