Slide 1

Slide 1 text

Pluu kakaobank GDE for Android @pluulove must know to write better ௏٘ ಿ૕ 1% ৢܻӝ

Slide 2

Slide 2 text

2 TARGET ௏٬ җઁ / ഈস

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

KEY-POINT LOOK BACK

Slide 5

Slide 5 text

G O O D C O D E ? ! 5 Architecture

Slide 6

Slide 6 text

DESIGN 6 Good Design No Design दр ӝמ

Slide 7

Slide 7 text

7 ୊਺ ௏٘ܳ ࢸݺೡ ٸ যڌѱ द੘ೞաਃ? Language? Structure? DI?

Slide 8

Slide 8 text

Over 50% Clean Architecture ૕ޙ਷ ҟߧਤ۽ ഛ੢

Slide 9

Slide 9 text

Clean Architecture ৖ ࢎۈীѱ ࢸݺ ೧ࠁࣁਃ. नੑীѱ ࢸݺب ೧ࠁࣁਃ. ೨ब਷ ޖ঺ੌөਃ?

Slide 10

Slide 10 text

Clean Architecture By separating the so ft ware into layers, and conforming to The Dependency Rule, you will create a system that is intrinsically testable, with all the bene fi ts that implies. ࣗ೐౟ਝযܳ ҅கਵ۽ ܻ࠙ೞҊ ઙࣘࢿ ӏ஗ਸ ળࣻೣਵ۽ॄ ࠄ૕੸ਵ۽ పझ౟ оמೠ दझమਸ ٜ݅ ࣻ ੓׮ h tt ps://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html ޖ঺੉ ೨बੋо?

Slide 11

Slide 11 text

Clean Architecture SRP OCP LSP ISP DIP OOP ҙबࢎ ܻ࠙ Data Structure Architecture … Design Pa tt ern Refactoring … ӝ߈ ૑ध ݽ؛੄ ੉೧ ݶ੽ ޙઁ ߧਤ Domain DDD ୭੸ച

Slide 12

Slide 12 text

h tt ps://khalilstemmler.com/a rt icles/so ft ware-design-architecture/full-stack-so ft ware-design/ Software Design & Architecture Stack

Slide 13

Slide 13 text

13 Any? MVC MVVM MVP What’s best?

Slide 14

Slide 14 text

੗न݅੄ ӝળ੉ ೙ਃ • ؊ ݆਷ ӝמ? • উ੹? • ਬݺ? • ೧ࠁҊ रযࢲ? • ഑द, ࠺ૉפझ সޖࠁ׮ ਋ࢶदೞח ੉ਬח? 14 ޖ঺੉ ૓૞ ੉ਬੋоਃ?

Slide 15

Slide 15 text

Good Code? • ੍ӝ ए਍ ௏٘ • ઺ࠂ੉ হח ௏٘ • పझ౟о ए਍ ௏٘ • ӝఋ 15 ޖ঺ਸ ೨ब о஖۽ ل Ѫੋо?

Slide 16

Slide 16 text

Good Code with Refactoring ೙ࣻ ઑѤ < 16 ௏٘ ௬ܻ౭ ௿ܽ ௏٘ ೐۽ಕ࣊օ્ܻ ইఃఫ୛ ܻಂష݂ ٣੗ੋ ಁఢ ҃ઁ੸ ࠺ਊ

Slide 17

Slide 17 text

U S E I D I O M S 17

Slide 18

Slide 18 text

Are you need mutable? 18 fun test(list: MutableList): MutableList { val transformList = mutableListOf() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?

Slide 19

Slide 19 text

Are you need mutable? 19 fun test(list: List): MutableList { val transformList = mutableListOf() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?

Slide 20

Slide 20 text

Are you need mutable? 20 fun test(list: List): List { val transformList = mutableListOf() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?

Slide 21

Slide 21 text

List transform API 21 fun test(list: List): List { val transformList = mutableListOf() for (item in list) { transformList.add(item.toString()) } return transformList } fun test(list: List): List { return buildList { for (item in list) { add(item.toString()) } } } 3 fun test(list: List): List { return list.map { item - > item.toString() } } 2 fun test(list: List): List { val transformList = list.map { item -> item.toString() } return transformList } 1

Slide 22

Slide 22 text

• Code smell • Application-level • Class-level • Method-level • Link : h tt ps://bit.ly/3vCk6Fy S T A R T L I N E 22

Slide 23

Slide 23 text

23 ӝמ ୶о ܻಂష݂ য়ט੄ ؀࢚

Slide 24

Slide 24 text

24 SAMPLE ےؒ ࢤࢿӝ • ےؒਵ۽ ࢤࢿػ ఫझ౟/࢚࢝ਸ ಴दೞח ܻझ౟ • ߓ҃࢝ী ٮܲ ఫझ౟ ஸ۞ • ೦ݾ ࢶఖद, ೧׼ ೦ݾী ⭑ ಴द • CLEAR റীب ӝর • CLEAR ࢶఖद ֎౟ਕ௼ ۽Ӓ ੹࣠

Slide 25

Slide 25 text

25 REFACTORING + over engineering খਵ۽ ࣗѐೞח Ѫب ੿׹਷ ইתפ׮.

Slide 26

Slide 26 text

26 Step 1. Always run task

Slide 27

Slide 27 text

[2 time run Task] Executing tasks: [:app:assembleDebug] in project … > Task :app:preBuild UP-TO-DATE ... > Task :app:generateDebugBuildConfig ... > Task :app:writeDebugSigningConfigVersions UP-TO-DATE > Task :app:kaptGenerateStubsDebugKotlin > Task :app:kaptDebugKotlin > Task :app:compileDebugKotlin > Task :app:compileDebugJavaWithJavac > Task :app:mergeDebugJavaResource UP-TO-DATE > Task :app:dexBuilderDebug > Task :app:mergeProjectDexDebug > Task :app:packageDebug > Task :app:createDebugApkListingFileRedirect UP-TO-DATE > Task :app:assembleDebug BUILD SUCCESSFUL in 1s 37 actionable tasks: 8 executed, 29 up-to-date 27 android { ... defaultConfig { ... buildConfigField "String", "SAMPLE", '\"' + generateValue() + '"' } } static def generateValue() { return System.currentTimeMillis() } Step 1. Always run task

Slide 28

Slide 28 text

28 android { ... defaultConfig { ... // FIXED 1. Always run Gradle task buildConfigField "String", "SAMPLE", '\"123"' } } FIXED 1. Always run task

Slide 29

Slide 29 text

Step 2. Unnecessary, gradle options 1 2 3 4 1. min 21ীࢲ ࠛ೙ਃ 2. ೙ਃೠ ২࣌݅ ഝࢿച 3. desugarо ೙ਃೠ ҃਋ী ୶о 4. Optimize ࠽٘ ২࣌ ୶о

Slide 30

Slide 30 text

Step 3. move network in View 30 class MainActivity : AppCompatActivity() { private val logRepository by lazy { provideRepository() } private fun reset() { // Use OkHttp, Retrofit lifecycleScope.launch { logRepository.sendLog() } ... } }

Slide 31

Slide 31 text

FIXED 3. move network in View 31 class MainActivity : AppCompatActivity() { / / FIXED 3. use ViewModel class SearchViewModel : ViewModel() { private val logRepository by lazy { provideRepository() } // FIXED 3. move network fun sendLog() { // Use OkHttp, Retrofit viewModelScope.launch { logRepository.sendLog() } } } ӝࠄ੸ੋ ೧Ѿߨ

Slide 32

Slide 32 text

Step 4. Generate in View 32 class MainActivity : AppCompatActivity() { ... private fun generate() { val item = SampleItem( text = ('a' + (0 until 26).random()).toString(), bgColor = Color.rgb( (0 .. 255).random(), (0 .. 255).random(), (0 .. 255).random() ) ) sampleAdapter.addItem(item) } ... }

Slide 33

Slide 33 text

FIXED 4. Generate in View 33 class MainActivity : AppCompatActivity() { class SearchViewModel : ViewModel() { .. . // FIXED 4. move generate fun generate() { val item = SampleItem( text = / ** generate text */ , bgColor = / ** generate color */ ) ... } } ӝࠄ੸ੋ ೧Ѿߨ

Slide 34

Slide 34 text

Step 5. Add unit test 34 class SearchViewModelTest { private lateinit var viewModel: SearchViewModel @Before fun setup() { viewModel = SearchViewModel() } @Test fun generate() { // when 1 viewModel.generate() // then 1 assertTrue(viewModel.items.getOrAwaitValue().isNotEmpty()) // when 2 viewModel.reset() // then 2 assertTrue(viewModel.items.getOrAwaitValue().isEmpty()) } } • Unit Test۽ ViewModel పझ౟ • ࡅܲ ۽૒ ୓௼

Slide 35

Slide 35 text

Step 6. Import android framework 35 import android.graphics.Color class SearchViewModel : ViewModel() { ... }

Slide 36

Slide 36 text

interface SampleItemGenerator { fun generateAlphabet(): String fun generateColor(): Int // Optional fun generate(): SampleItem } import android.graphics.Color class SearchViewModel( // FIXED 6. provide generator private val generator: SampleItemGenerator ) : ViewModel() { .. . } FIXED 6. Import android framework 36 import android.graphics.Color

Slide 37

Slide 37 text

Step 7. Favorite in View 37 class MainActivity : AppCompatActivity() { private val preferences: SharedPreferences by lazy { getSharedPreferences("sample", Context.MODE_PRIVATE) } private fun setUpViews() { sampleAdapter = SampleAdapter( onFavorite = { preferences.edit { putString("KEY", it) } sampleAdapter.updateFavorite(it) sampleAdapter.notifyDataSetChanged() } ) sampleAdapter.updateFavorite( preferences.getString("KEY", null).orEmpty() ) } } View੄ ҙबࢎ৬ ׮ࣗ Ѣܻо ݢ Preference

Slide 38

Slide 38 text

FIXED 7. Favorite in View 38 data class SampleItem( val text: String, val bgColor: Int, // FIXED 7. add field favorite val isFavorite: Boolean = false ) class SearchViewModel( ... // FIXED 7. provide saver private val savingRepository: SavingRepository ) : ViewModel() { ... fun generate() { val item = generator.generate() // FIXED 7. use favorite val favoriteText = savingRepository.getFavorite() cachedList.add( item.copy(isFavorite = item.text == favoriteText) ) } // FIXED 7. update favorite fun updateFavorite(text: String) { savingRepository.saveFavorite(text) ... interface SavingRepository { fun saveFavorite(text: String) fun getFavorite(): String } class SavingRepositoryImpl( private val preferences: SharedPreferences ) : SavingRepository { override fun saveFavorite(text: String) { /** save text */ } override fun getFavorite(): String { return /** get text */ } }

Slide 39

Slide 39 text

class SampleAdapter( private val onFavorite: (String) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): SampleViewHolder { return SampleViewHolder.create(parent, onFavorite) } ... } Step 8. User events in RecyclerViews 39 class SampleViewHolder( private val binding: ItemSampleBinding, private val onFavorite: (String) -> Unit ) : RecyclerView.ViewHolder(binding.root) { init { binding.btnButton.setOnClickListener { // TODO onFavorite( /** Text */ ) } } ... } Optional Adapter੄ ࠂ੟بী ݏ୾ࢲ ૐо

Slide 40

Slide 40 text

FIXED 8. User events in RecyclerViews 40 Optional data class SampleItem( val text: String, val bgColor: Int, val isFavorite: Boolean = false, // FIXED 8. add field action val onFavorite: (String) -> Unit ) class SampleViewHolder( private val binding: ItemSampleBinding ) : RecyclerView.ViewHolder(binding.root) { private var item: SampleItem? = null init { binding.btnButton.setOnClickListener { item ?. let { safeItem -> // FIXED 8. use action safeItem.onFavorite(safeItem.text) } } class SearchViewModel( . .. ) { fun generate() { val item = SampleItem( text = . .. , bgColor = .. . , isFavorite = …, // FIXED 8. pending favorite function onFavorite = :: updateFavorite ) ... } / / FIXED 8. change visibility modifier private fun updateFavorite(text: String) { ... } } UI events ~ Handle user events h tt ps://developer.android.com/topic/architecture/ui-layer/ events#recyclerview-events

Slide 41

Slide 41 text

Step 9. Item manage for repository 41 class SearchViewModel( . .. ) : ViewModel() { private val cachedList = mutableListOf() fun generate() { val item = /** Generate item * / if (cachedList.none { it.text == item.text }) { val favoriteText = savingRepository.getFavorite() cachedList.add( /* * Mapping item * / ) } else { ... } } fun reset() { cachedList.clear() ... } } Optional ؘ੉ఠ ࠁҙਸ ViewModel ؀न ׮ܲҔী ݐӝҊ र׮ݶ? ୶റ, ؘ੉ఠ ੷੢ਸ ࢲߡ۽ ੉ҙೠ׮ݶ?

Slide 42

Slide 42 text

class SearchViewModel( private val itemRepository: ItemRepository, ... ) : ViewModel() { private val cachedList = mutableListOf() fun generate() { // FIXED 9. use item repository itemRepository.generate() .onSuccess { refresh() } } fun reset() { itemRepository.reset() ... } } FIXED 9. Item manage for repository 42 Optional - interface ItemRepository { val data: List fun generate(): Result fun reset() } // FIXED 9. use item repository class ItemRepositoryImpl : ItemRepository { private val cachedList = mutableListOf() override val data: List get() = cachedList override fun generate(): Result { val item: GenerateItem = /** Generate */ } override fun reset() { ... } }

Slide 43

Slide 43 text

Step 10. Add ViewModelProvider Factory 43 private val viewModel by lazy { SearchViewModel( itemRepository = /** Generate */ , savingRepository = /** Generate */ ) }

Slide 44

Slide 44 text

FIXED 10. Add ViewModelProvider Factory 44 private val viewModel by viewModels { / / FIXED 10. user ViewModelProvider Factory SearchViewModelFactory(this) } recommend : DI (Dagger, Hilt, Koin etc) private val viewModel by lazy { private val context: Context ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return SearchViewModel( ... ) as T } }

Slide 45

Slide 45 text

Step 11. Use Inline classes 45 Optional पઁ ֍ਸ ч : ARGB or RGB Int੄ Range : Int.MIN_VALUE ~ Int.MAX_VALUE h tt ps://developer.android.com/reference/android/graphics/Color data class SampleItem( val text: String, val bgColor: Int, val isFavorite: Boolean = false, val onFavorite: (String) -> Unit ) { fun isDarkBg(): Boolean { return /** true or false */ } }

Slide 46

Slide 46 text

data class SampleItem( val text: String, val bgColor: ColorValue, val isFavorite: Boolean = false, val onFavorite: (String) -> Unit ) FIXED 11. Use Inline classes 46 Optional data class SampleItem( tt ps://kotlinlang.org/docs/inline-classes.html @JvmInline value class ColorValue(@ColorInt val value: Int) { fun isDark(): Boolean = /* * true or false */ }

Slide 47

Slide 47 text

class ItemRepositoryImpl { ... ) : ItemRepository { private val cachedList = mutableListOf() override val data: List get() = cachedList override fun generate(): Result { val item: GenerateItem = /** Generate * / return if (cachedList.none { it.text == item.text }) { cachedList.add(item) Result.success(item) } else { Result.failure( / ** Error * / ) } } ... } Step 12. Migration wrapper data 47 Optional LOOPܳ جݶࢲ ୓௼ ୭ঈ੄ दաܻয়ח ݃૑݄ Elementө૑ ୓௼

Slide 48

Slide 48 text

FIXED 12. Migration wrapper data 48 Optional class ItemRepositoryImpl { . .. ) : ItemRepository { / / FIXED 13. modify container type private val cachedSet = TreeSet( TreeSet { p0, p1 -> p0.text.compareTo(p1.text) } ) override val data: List get() = cachedSet.toList() override fun generate(): Result { val item: GenerateItem = /** Generate */ return if (cachedSet.add(item)) { ... } else { ... } } . .. }

Slide 49

Slide 49 text

Step 13. Use Coroutine Flow 49 class SearchViewModel( ... ) : ViewModel() { ... fun generate() { itemRepository.generate() .onSuccess { refresh() } } fun reset() { itemRepository.reset() refresh() } private fun updateFavorite(text: String) { savingRepository.saveFavorite(text) refresh() } private fun refresh() { // refresh live event } } Optional ؘ੉ఠ ୶о/јनਸ Steram ؀਽

Slide 50

Slide 50 text

FIXED 13. Use stream 50 class SearchViewModel( . .. ) : ViewModel() { val items: LiveData> = savingRepository.favoriteTextFlow.combine( itemRepository.dataFlow ) { savingText, list -> // merge items } . .. } Optional ৘ઁ۽ח Coroutine Flow ࢎਊ ֢୹ೡ ఫझ౟ ߂ ૌѹ଺ӝ ױযо ߸҃غח ҃਋, Streamਸ ా೧ࢲ ୊ܻ - LiveData, Rx, Coroutine Flow

Slide 51

Slide 51 text

51 app/src/main/java/com/pluu/sample/codeforreadability ├── SampleApp.kt ├── data │ ├── ItemRepository.kt │ ├── LogResult.kt │ ├── SampleApi.kt │ ├── SampleRepository.kt │ └── SavingRepository.kt ├── model │ ├── ColorValue.kt │ ├── GenerateItem.kt │ ├── SampleItem.kt │ └── TriggerTreeSet.kt ├── presentation │ ├── MainActivity.kt │ ├── SampleAdapter.kt │ ├── SampleItemDecoration.kt │ ├── SampleViewHolder.kt │ ├── SearchViewModel.kt │ └── SearchViewModelFactory.kt ├── provider │ ├── RandomGenerator.kt │ └── RetrofitProvider.kt └── utils ├── DimensExtension.kt └── keyFlow.kt app/src/main/java/com/pluu/sample/codeforreadability ├── SampleApp.kt ├── data │ ├── LogResult.kt │ ├── SampleApi.kt │ └── SampleRepository.kt ├── model │ └── SampleItem.kt ├── presentation │ ├── MainActivity.kt │ ├── SampleAdapter.kt │ ├── SampleItemDecoration.kt │ └── SampleViewHolder.kt ├── provider │ └── RetrofitProvider.kt └── utils └── DimensExtension.kt Diff 655 additions and 116 deletions Before A ft er

Slide 52

Slide 52 text

ࣻҊೞ࣑णפ׮ ৈӝө૑о ӝࠄ ੼ࣻ җ੿ 52

Slide 53

Slide 53 text

Exclude git history Code Forma tt ing Lint No Internet Con fi guration changes Contrived complexity Side e ff ects Large class Too many parameters Long method Excessively long line of code F I N A L C H E C K 53 җઁ ઁ୹ ੹

Slide 54

Slide 54 text

Try 54 ܻಂష݂ ѼҊೣ оةࢿ SOLID ࠽٘ ѐࢶ 1%

Slide 55

Slide 55 text

Thank you. [email protected] http:/ /pluu.github.io pluulove pluulove