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

코드 품질 1% 올리기

코드 품질 1% 올리기

"[안드로이드 컨퍼런스] 코로나시대의 안드로이드 개발자들"에서 발표한 "코드 품질 1% 올리기" 발표자료입니다

pluulove (노현석)

May 14, 2022
Tweet

More Decks by pluulove (노현석)

Other Decks in Programming

Transcript

  1. 3

  2. G O O D C O D E ? !

    5 Architecture
  3. 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 ޖ঺੉ ೨बੋо?
  4. Clean Architecture SRP OCP LSP ISP DIP OOP ҙबࢎ ܻ࠙

    Data Structure Architecture … Design Pa tt ern Refactoring … ӝ߈ ૑ध ݽ؛੄ ੉೧ ݶ੽ ޙઁ ߧਤ Domain DDD ୭੸ച
  5. ੗न݅੄ ӝળ੉ ೙ਃ • ؊ ݆਷ ӝמ? • উ੹? •

    ਬݺ? • ೧ࠁҊ रযࢲ? • ഑द, ࠺ૉפझ সޖࠁ׮ ਋ࢶदೞח ੉ਬח? 14 ޖ঺੉ ૓૞ ੉ਬੋоਃ?
  6. Good Code? • ੍ӝ ए਍ ௏٘ • ઺ࠂ੉ হח ௏٘

    • పझ౟о ए਍ ௏٘ • ӝఋ 15 ޖ঺ਸ ೨ब о஖۽ ل Ѫੋо?
  7. Good Code with Refactoring ೙ࣻ ઑѤ < 16 ௏٘ ௬ܻ౭

    ௿ܽ ௏٘ ೐۽ಕ࣊օ્ܻ ইఃఫ୛ ܻಂష݂ ٣੗ੋ ಁఢ ҃ઁ੸ ࠺ਊ
  8. Are you need mutable? 18 fun test(list: MutableList<Int>): MutableList<String> {

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

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

    val transformList = mutableListOf<String>() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?
  11. List transform API 21 fun test(list: List<Int>): List<String> { val

    transformList = mutableListOf<String>() for (item in list) { transformList.add(item.toString()) } return transformList } fun test(list: List<Int>): List<String> { return buildList { for (item in list) { add(item.toString()) } } } 3 fun test(list: List<Int>): List<String> { return list.map { item - > item.toString() } } 2 fun test(list: List<Int>): List<String> { val transformList = list.map { item -> item.toString() } return transformList } 1
  12. • 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
  13. 24 SAMPLE ےؒ ࢤࢿӝ • ےؒਵ۽ ࢤࢿػ ఫझ౟/࢚࢝ਸ ಴दೞח ܻझ౟

    • ߓ҃࢝ী ٮܲ ఫझ౟ ஸ۞ • ೦ݾ ࢶఖद, ೧׼ ೦ݾী ⭑ ಴द • CLEAR റীب ӝর • CLEAR ࢶఖद ֎౟ਕ௼ ۽Ӓ ੹࣠
  14. [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
  15. 28 android { ... defaultConfig { ... // FIXED 1.

    Always run Gradle task buildConfigField "String", "SAMPLE", '\"123"' } } FIXED 1. Always run task
  16. Step 2. Unnecessary, gradle options 1 2 3 4 1.

    min 21ীࢲ ࠛ೙ਃ 2. ೙ਃೠ ২࣌݅ ഝࢿച 3. desugarо ೙ਃೠ ҃਋ী ୶о 4. Optimize ࠽٘ ২࣌ ୶о
  17. 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() } ... } }
  18. 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() } } } ӝࠄ੸ੋ ೧Ѿߨ
  19. 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) } ... }
  20. 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 */ ) ... } } ӝࠄ੸ੋ ೧Ѿߨ
  21. 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 పझ౟ • ࡅܲ ۽૒ ୓௼
  22. 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
  23. 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
  24. 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 */ } }
  25. class SampleAdapter( private val onFavorite: (String) -> Unit ) :

    RecyclerView.Adapter<SampleViewHolder>() { 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੄ ࠂ੟بী ݏ୾ࢲ ૐо
  26. 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
  27. Step 9. Item manage for repository 41 class SearchViewModel( .

    .. ) : ViewModel() { private val cachedList = mutableListOf<SampleItem>() 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 ؀न ׮ܲҔী ݐӝҊ र׮ݶ? ୶റ, ؘ੉ఠ ੷੢ਸ ࢲߡ۽ ੉ҙೠ׮ݶ?
  28. class SearchViewModel( private val itemRepository: ItemRepository, ... ) : ViewModel()

    { private val cachedList = mutableListOf<SampleItem>() 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<GenerateItem> fun generate(): Result<GenerateItem> fun reset() } // FIXED 9. use item repository class ItemRepositoryImpl : ItemRepository { private val cachedList = mutableListOf() override val data: List<GenerateItem> get() = cachedList override fun generate(): Result<GenerateItem> { val item: GenerateItem = /** Generate */ } override fun reset() { ... } }
  29. Step 10. Add ViewModelProvider Factory 43 private val viewModel by

    lazy { SearchViewModel( itemRepository = /** Generate */ , savingRepository = /** Generate */ ) }
  30. 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 <T : ViewModel> create(modelClass: Class<T>): T { return SearchViewModel( ... ) as T } }
  31. 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 */ } }
  32. 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 */ }
  33. class ItemRepositoryImpl { ... ) : ItemRepository { private val

    cachedList = mutableListOf<GenerateItem>() override val data: List<GenerateItem> get() = cachedList override fun generate(): Result<GenerateItem> { 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ө૑ ୓௼
  34. FIXED 12. Migration wrapper data 48 Optional class ItemRepositoryImpl {

    . .. ) : ItemRepository { / / FIXED 13. modify container type private val cachedSet = TreeSet<GenerateItem>( TreeSet { p0, p1 -> p0.text.compareTo(p1.text) } ) override val data: List<GenerateItem> get() = cachedSet.toList() override fun generate(): Result<GenerateItem> { val item: GenerateItem = /** Generate */ return if (cachedSet.add(item)) { ... } else { ... } } . .. }
  35. 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 ؀਽
  36. FIXED 13. Use stream 50 class SearchViewModel( . .. )

    : ViewModel() { val items: LiveData<List<SampleItem >> = savingRepository.favoriteTextFlow.combine( itemRepository.dataFlow ) { savingText, list -> // merge items } . .. } Optional ৘ઁ۽ח Coroutine Flow ࢎਊ ֢୹ೡ ఫझ౟ ߂ ૌѹ଺ӝ ױযо ߸҃غח ҃਋, Streamਸ ా೧ࢲ ୊ܻ - LiveData, Rx, Coroutine Flow
  37. 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
  38. 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 җઁ ઁ୹ ੹