관련 키워드

Thread, UiThread, MainThread, Jank, ANR, Asynchronous, Synchronous, Blocking, Non-Blocking, Concurrency, Parallelism, Handler, Looper, MessageQueue, AsyncTask

 

 


왜 비동기가 필요한가?

 

안드로이드에서는 UI를 그리는 메인스레드가 매우 중요하다.

 

애플리케이션이 실행되고 메인액티비티가 메모리에 로드되면

메인스레드를 포함한 프로세스가 생성된다.

 

안드로이드는 초당 60프레임을 지원하고 한 프레임은 16ms안에 그려져야 한다.

16ms 안에 화면을 그리는 작업이 완료되지 않으면 쟁크가 발생한다.

(쟁크; jank : 프레임 누락으로 인해 끊겨보이는 현상)

5초 이상의 긴 작업을 메인스레드에서 실행한다면 ANR이 발생한다.

(ANR; Application Not Responding)

 

결국 안드로이드에서 UI스레드가 원활하게 돌아가기 위해서는 비동기가 필수로 사용된다.

 

 

 

 

 


그럼 비동기는 무엇인가?

 

동기(Synchronous)

작업을 수행하고 그 작업이 완료될 때까지 다른 작업을 하지못하고 기다리는 방식

 

예를 들어 편의점에서 혼자 일을 한다면 카운터 일 물건 정리나 청소 등의 일을 하는 동안 전혀할 수 없다.

즉, 메인스레드가 UI를 그려야만 하는데 무거운 작업(시간이 오래걸리는)을 한다면 그 동안 UI를 그릴 수 없어 화면이 멈춰있을 것이다.

 

 

비동기(Asynchronous)

어떤 작업을 수행하지만 완료와 상관없이 계속해서 작업을 할 수 있는것

 

편의점이 바빠 한 명의 직원을 더 고용했다. 한명은 계속해서 카운터 일을 보고 물건 정리나 청소 등의 일은 다른 직원에게 시키면 카운터가 비는 일이 없다.

메인 스레드는 계속해서 UI를 그리고, 무거운 작업은 추가적인 스레드에게 시키면 된다.

 

 

 

 

사실 위에서 설명한 동기는 Sync-Bloking이고, 비동기는 Async-NonBlocking이다.

Sync-NonBlocking, Async-Blocking은 거의 사용되지 않기 때문에 우선은 위에 설명만 알아도 된다.

하지만 더 자세히 알고 싶다면 아래 포스팅 참조

 

Sync, Async, Blocking, NonBlocking 추천 포스팅

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

 

Blocking-NonBlocking-Synchronous-Asynchronous

꽤 자주 접하는 용어다. 특히나 요즘들어 더 자주 접하게 되는데, 얼추 알고는 있고 알고 있는게 틀린 것도 아니지만, 막상 명확하게 구분해서 설명하라면 또 만만치가 않은.. 그래서 찾아보면 ��

homoefficio.github.io

 

 

 

 

 


그럼 어떻게 구현하지?

 

무거운 작업을 새로운 스레드에서 한다고 치자.

작업이 끝나고 그 결과를 가지고 UI에 보여주고 싶다.

UI관련 작업은 메인스레드에서만 해야하는데 어떻게 보여줄까?

 

이때 Handler가 필요로 하다.

 

Handler는 스레드의 작업을 관리하고 스레드간에 작업을 전달하기 위한 용도로 존재한다.

 

굉장히 오랜시간동안 Handler가 UI를 변경하기 위한 용도라고만 생각해왔다.

작업스레드에서 UI를 처리하기 위해 주로사용하다 보니 그런 고정관념이 생겼던 것 같다.

 

 

새로운 스레드에서 작업을 마치면 결과를 가지고 변경할 UI 작업을

Handler를 통해 메인스레드에 전달하면 된다.

 

 

 

 

 


Handler 예시

 

    private val handler = MainHandler(this)

    companion object {
        const val HANDLE_WORK_UPDATE_PROGRESS = 1
        const val HANDLE_WORK_DONE = 2

        class MainHandler(
            activity : MainActivity
        ) : Handler() {
            private val activity = WeakReference<MainActivity>(activity)
            override fun handleMessage(msg: Message) {
                when(msg.what) {
                    HANDLE_WORK_UPDATE_PROGRESS -> activity.get()?.updateProgress(msg.obj as Int)
                    HANDLE_WORK_DONE -> activity.get()?.showDoneMessage()
                }

            }
        }
    }

    private fun processAndUpdateUI() {
        Thread {
            for(i in 0..10) {
                Thread.sleep(1000L)

                handler.obtainMessage().apply {
                    what = HANDLE_WORK_UPDATE_PROGRESS
                    val progress = i*10
                    obj = progress
                }.let {
                    handler.sendMessage(it)
                }
            }
            handler.sendEmptyMessage(HANDLE_WORK_DONE)
        }.start()
    }

    fun showDoneMessage() {
        tv_main_state.text = "completed"
    }

    fun updateProgress(progress: Int) {
        tv_main_state.text = "progress $progress"
    }

 

processAndUpdateUI()에서 10초동안 작업이 진행하며

매 1초마다 진행정도를 전달하는 UI작업 (HANDLE_WORK_UPDATE_PROGRESS)

10초가 모두 지나고 완료를 표시하는 UI작업 (HANDLE_WORK_DONE)

두가지를 Handler를 통해 처리하는 간단한 예시이다.

 

Handler에 메세지 외에도 Runnable을 던질 수도 있다.

 

AsyncTask, runOnUiThread 역시 내부적으로는 Handler를 이용하고 있다.

 

 

 

 

 


Handler의 동작 원리

 

 

위에 그림에서 Handler, MessageQueue, Looper역할을 알아야 한다.

 

 

1. Handler를 통해 스레드의 외부 혹은 내부에서 작업을 받는다.

2. 처리해야할 작업을 MessageQueue에 enqueue한다.

3. Looper는 처리해야할 작업이 있는지 계속해서 확인하고 Handler에게 처리를 맡긴다.

4. Handler는 해당 작업에 맞는 처리를 한다.

 

 

메인스레드에서 위와 같이 동작한다고 보면된다.

 

우리가 새로 만드는 작업 스레드는 여기서 Looper가 없기 때문에 직접 만들고 loop해줘야 한다.

그렇기 때문에 Looper를 포함한 HandlerThread를 지원한다. 

 

 

추천 포스팅

https://academy.realm.io/kr/posts/android-thread-looper-handler/

 

안드로이드 백그라운드 잘 다루기 Thread, Looper, Handler

안드로이드 백그라운드 잘 다루기 Thread, Looper, Handler

academy.realm.io

 

 

 

 


실제 비동기 사용

 

위에 Handler 예시를 LiveData를 사용해서 바꿔보았다.

 

    private val stateText = MutableLiveData<String>()

    private fun observe() {
        stateText.observe(this, Observer {
            tv_main_state.text = it
        })
    }

    private fun initViews() {
        btn_main_update_ui.setOnClickListener { processAndUpdateUI() }
        stateText.value = "start progress"
    }

    private fun processAndUpdateUI() {
        Thread {
            for(i in 0..10) {
                Thread.sleep(1000L)
                stateText.postValue("progress ${i*10}%")
            }
            stateText.postValue("done")
        }.start()
    }

 

LiveData를 공부없이 써서 맞게 쓴것인지 모르겠지만..

대략 반으로 코드 라인이 줄었다.

 

실제 회사에서 Handler를 UI 변경을 위해 사용하는 일은 드물 것이라 생각한다.

비동기를 더 쉽게하기 위한 정말 많은 기술과 조합이 있기 때문이다.

 

대표적으로 사용하는 비동기 기술로 Coroutine과 Rx가 있다.

 

 

 

 

 

'Android > Common' 카테고리의 다른 글

수명주기; Lifecycle  (0) 2020.06.22
안드로이드 개발자 로드맵  (2) 2020.05.07
안드로이드 프로젝트 루틴  (0) 2020.02.20

 

 

안드로이드 개발자로서 잘하는 개발자가 되기 위해선 무엇이 필요할까?

체워도 체워도 언제나 고민이다.

 

우연히 찾게된 안드로이드 개발자 로드맵 PPT이다.

네이버 테크 콘서트에서 발표한 내용을 담은 PPT였다.

더 검색해보니 영상도 찾을 수 있었다.

 

영상을 보면서 나오는 내용들 대부분은 접하거나 공부한 내용이다.

하지만 부족한 부분도 있고 공부했다 해도 완벽하게 설명할 수 있는가 하면 아니다.

 

안드로이드 개발자로서 튼튼한 기본기를 가지고

이를 바탕으로 더 많은 것을 할 수 있는 개발자가 되기를 바란다.

 

발표한 내용을 바탕으로 포스팅을 정리해보려 한다.

 

 

PPT

https://www.slideshare.net/NaverEngineering/techcon-2019-mobile-android3

 

[TECHCON 2019: MOBILE - Android]3.안드로이드 개발자 로드맵

NAVER WEBTOON | Son App Tech | 안중환 안드로이드개발자로드맵

www.slideshare.net

 

영상

https://tv.naver.com/v/9329737/list/486582

 

TECH CONCERT: MOBILE 2019 - 안드로이드 개발자 로드맵

NAVER Engineering | 내용 네이버 안드로이드 개발자 3년차가 되는 과정에서 졸업생은 무엇을 준비하고, 어떤 걸 공부해야 하는지 키워드와 노하우를 공유합니다.

tv.naver.com

 

 


기본기

 

CS(Computer Science)

DB + OS + Network + Algorithm + Data Structures

 

개발자라면 너무 당연히 접해본 것들이다.

대학교에서 공부했고 취업을 하기 위해 공부했고

실무를 하면서도 계속 부족하다고 느끼는 기본 중의 기본이다.

 

 

T자형 인재

DB + OS + Network + Algorithm + Data Structures + Git

Framework + Programming Language

 

Git을 사용하지 않는 회사는 거의 없다라고 생각한다.

Git까지 공부하고 나면 어떤 언어로 어떤 것을 개발할지 확장해 공부한다.

 

 

 

 


키워드

 

안드로이드 개발자로 정했다.

그렇다면 관련 키워드는 어디에서 나올까?

 

GoogleIO : 매년 구글의 기술을 설명하는 컨퍼런스

Android Dev Summit : 개발에 관해 집중적으로 발표하는 컨퍼런스

 

위의 컨퍼런스들을 통해 새로운 키워드들이 등장한다.

뒤늦게 대세가 된 후 접하기 보단 더 빠르게 기술에 대해 인지할 수 있다.

 

 

Android Developers Youtube : 컨퍼런스 발표 동영상이 올라와 있다.

https://www.youtube.com/user/androiddevelopers/videos

 

Android Developers

Welcome to the official Android Developers YouTube channel. Get the latest Android news, best practices, live videos, demonstrations, tutorials, and more. Su...

www.youtube.com

 

위의 컨퍼런스 외에 한국에서 진행하는 컨퍼런스들도 있다.

드로이드 나이츠, 코틀린 나이트, GoogleIO in Seoul, if kakao, Naver Deview

 

항상 관심을 가지고 참석해보도록 하자.

 

 

 

 

그 외에 사이트로

 

Android Weekly : 메일을 통해 안드로이드 뉴스를 전달해준다.

https://androidweekly.net/

 

Android Weekly - Free weekly Android & Kotlin development newsletter

Android Weekly is a free newsletter that helps you to stay cutting-edge with your Android Development.

androidweekly.net

 

 

 

 


안드로이드

 

개발을 하다보면 구글링이 필수가 된다.

 

하지만 구글링으로 나오지 않을 수도 있고 잘못된 정보를 다루고 있을 수 도 있다.

그렇기 때문에 공식 문서부터 찾아보는 방법을 권한다.

 

 

새로운 API를 접할 때

1. Android Developer 검색

2. Android Developer Medium, Blog 검색

3. CodePath Android Cliffnotes 검색

4. 구글링

 

 

Android Developer : 최신 정보는 영어로 먼저 나오기 때문에 영어로 보길 권장한다.

 - Android Studio / User Guide

 - Jetpack

 - Kotlin

 - Docs / Guides, References, Samples

https://developer.android.com/

 

Android 개발자  |  Android Developers

Android 앱 개발자를 위한 공식 사이트입니다. Android SDK 도구 및 API 문서를 제공합니다.

developer.android.com

 

Android Developer Medium

https://medium.com/androiddevelopers

 

Android Developers – Medium

The official Android Developers publication on Medium.

medium.com

 

Android Developer Blog

https://android-developers.googleblog.com/

 

Android Developers Blog

 

android-developers.googleblog.com

 

Google Codelabs : 코드베이스로 익혀나가고 싶다면...

https://codelabs.developers.google.com/

 

Google Codelabs

Google Developers Codelabs provide a guided, tutorial, hands-on coding experience. Most codelabs will step you through the process of building a small application, or adding a new feature to an existing application. They cover a wide range of topics such a

codelabs.developers.google.com

 

CodePath Android Cliffnotes : 사용법, 꿀팁, 관련 오픈소스

https://guides.codepath.com/android

 

Home | CodePath Android Cliffnotes

CodePath Android Cliffnotes Welcome to the open-source CodePath Android Cliffnotes! Our goal is to become the central crowdsourced resource for complete and up-to-date practical Android developer guides for any topic. Just take me to the notes! We have And

guides.codepath.com

 

 

기본적인 내용

4대 컴포넌트 : Activities, Services, BroadcaseReceivers, ContentProviders

Jetpack : Fragment, ConstraintLayout, ViewPager, RecyclerView...

Inflater

View

 

Inflate, View

 - XML -> Inflate -> 화면을 객체 형태로 만듬

 - 비용이 있는 동작

 - Layout Inspector로 만든 화면 보기 

 - 화면은 모두 View로 되어있다.

 - 화면은 View의 트리구조형태이며 전위 순회하며 깊이우선탐색하여 처리해나아간다.

 - 렌더링 : measure > layout > draw

 - Fragment 역시 View : 즉 Fragment보다 View를 재활용 단위로 가져가는 것이 좋다.

   View를 사용하면 xml에 넣거나 동적생성하여 재활성이 좋아진다.

 

 

 

 


비동기

  안드로이드의 꽃!   

 

 

비동기관련 기술들

 - Thread

 - Executor

 - CompletableFuture(java8, sdk>=24)

 - AsyncTask

 - Loader

 - WorkManager

 - Rx

 - Coroutine

 

AndroidDeveloper의 비동기 관련 내용

https://developer.android.com/

 

Docs - Core topics - Activities - Processes and app lifecycle

 - foreground process

 - visible process

 - service process

 - cached process

Docs - Core topics - Background tasks - Sending operations to multiple threads - Communicate with the UI thread

 - UI Thread 의 정의와 통신하는 방법

 - Handler, Looper, MessageQueue

Docs - Besr Practice - Processes and Threads Overview

 - Thread 기본 사용법

 

 

Callback에 대한 이해

 - Runnable 왜 있을까?

 - subroutines, lamda, expressions, blocks, function pointers...

 

concurrency vs parallelism

 

synchronous vs asynchronous

 

blocking vs non-blocking

 

Java Concurrent

 

#synchronized #volatile #atomic #semaphore #ReentrantLock #LockFree #FalseSharing #ForkJoinPoll #Executor #BlockingQueue #CountDownLatch #Future ...

 

 

Thread, MessageQueue, Handler, Looper에 대한 이해

#Consumer-Producer Patter #MainThread #UiThread #MessageQueue #Handler #MainLooper #Looper #HandlerThread

https://academy.realm.io/kr/posts/android-thread-looper-handler/

 

안드로이드 백그라운드 잘 다루기 Thread, Looper, Handler

안드로이드 백그라운드 잘 다루기 Thread, Looper, Handler

academy.realm.io

 

 

위에 내용을 이해하고 Rx, Coroutine을 사용하자

 

 

 

정확한 타이밍에 실행이 보장되길 원한다면

  : Foreground Service

특정 조건을 걸고 실행이 보장되길 원한다면

  : JobScheduler, JobDispatcher, AlarmManager, BroadcastReceivers

 

 

WorkManager in Jetpack

 - 기존의 파편화된 백그라운드 처리를 하나로 모아서 실행 

 

 

 

 


성능 최적화

 

초당 60프레임 : 프레임당 16ms

한 프레임에 16ms보다 더 오래 걸리는 경우 프레임 드랍이 발생

 

프레임 드랍이 발생한다면

 - MainThread -> WorkerThread

 - UI 업데이트가 순차적으로 진행될 수 있도록 조절

 - CPU -> GPU

 - GPU에 안그려도 되는 부분을 알려준다

 

#ViewHierarchy #DoubleLayoutTaxation #Async #Overdraw #AnimationProperty #Cache #Clipping #ObjectPool #HardwareAcceleration #VSYNC

 

 

발표에서 보여주고자 하는 동영상이 어디있는지 모르겠지만

같은 내용을 다루는 듯 하다.

https://www.youtube.com/watch?time_continue=8&v=qk5F6Bxqhr4&feature=emb_title

 

 

 


안드로이드 프레임워크

 

우리가 사용하는 부분은 Application Framework까지이다.

 

NDK 개발시에는 필수로 더 로우레벨까지 공부해야 한다.

 

 

Android Open Source Project

 - Cofigure / RUNTIME : 컴파일러 관련 내용

#.class #.dex #Dalvik #ART #D8 #R8 #AOT #JIT

 - Develop / GRAPHICS : 화면 렌더링 관련 내용

#SurfaceView #TextureView #BufferQueue, #SurfaceFlinger, #Surface, #Canvas, #SurfaceHolder 

 - Develop / ARCHITECTURE : IPC 관련 내용

#HAL #Context #Parcel # BinderIPC #SystemServices #MediaServer #SystemServer

https://source.android.com/ 

 

Android 오픈소스 프로젝트  |  Android Open Source Project

Android는 세계를 하나로 묶어줍니다. 오픈소스 Android 운영체제를 사용하여 강력한 기기를 만드세요.

source.android.com

 

 


언어

 

Kotlin이 인기가 많다.

Kotlin을 하려면 Java도 알아야 한다.

함수형 프로그래밍을 사용하기에는 Java보다는 Kotlin을 사용하는 것이 났다.

 

 

프로그래밍 패러다임

 - 기능에 따라 프로그래밍 언어를 분류하는 방법. 언어는 여러 패러다임으로 분류 할 수 있다.

 - 프로그래밍 관점을 갖게 해 주고, 결정하는 역할

 - 서로 다른 프로그래밍 언어는 서로 다른 프로그래밍 패러다임을 지원

 - 새로운 것을 가능하게도 하지만 어떤 기법을 금지 함

 

#StructuredProgramming #Object-OrientedProgramming #FunctionalProgramming #ReactiveProgramming
#FunctionalReactiveProgramming #Modular Programming

 

 

 

Kotlin과 Java는 객체지향프로그래밍, 함수형 프로그래밍패러다임을 가지고 있다. 

 

 

객체지향프로그래밍

 

#OOP #클래스 #객체 #메시지 #추상화 #캡슐화 #상속 #구성 #다형성 #S.O.L.I.D

 

 

디자인 패턴

 

SourceMaking

https://sourcemaking.com/

 

Design Patterns and Refactoring

Design Patterns and Refactoring articles and guides. Design Patterns video tutorials for newbies. Simple descriptions and full source code examples in Java, C++, C#, PHP and Delphi.

sourcemaking.com

 

 

 

함수형프로그래밍

 - 클래스가 아닌 함수가 재활용 단위

 - 함수는 1급 객체

 

#람다 #클로저 #순수함수 #고차함수 #lazy #커링 #재귀 #메모이제이션 #모나드

 

 

 


Best Practice

 

클린 아키텍처

클린 코드

리팩토링

 

굉장히 유명한 책들이다.

하지만 많은 학습이 전제되지 않은 상태에서는 의미가 없을 수 있다.

 

그러니 우선 이것만 해보자!

 - 함수는 한가지 일만 한다.

 - SRP : 클래스는 하나의 책임만 가진다.

 - 상속 대신 구성

 

 

드로이드 나이츠에서 발표한 Clean Arhcitecture

(몇년전 발표. 첫 드로이드 나이츠로 기억하고 있다.)

https://academy.realm.io/kr/posts/clean-architecture-in-android/

 

안드로이드에 Clean Architecture 적용하기

안드로이드 앱을 만들면서 두 번의 대규모 업데이트를 배경으로 코드가 계속 바뀌었고, 이런 변화하는 코드에 잘 대응할 수 있는 Clean Architecture에 대해 고민한 경험에 대해 공유하고자 합니다.

academy.realm.io

 


TODO

키워드 수집

기록해라 : 블로그, Git과 같은 것들로 기록해라

 

테스트

 - 유닛테스트를 작성해보자

 - 좋은 구조로 가는 지름길이 되기도 한다.

 - CI

 

#TDD #BDD #Junit #Dummy #Stub #Spy #Fake #Mock #Mockito #PowerMock #Robolectric #CI #Jenkins #TestCoverage #Jacoco

 

 

 

위에 내용들을  완벽 하게 익혀다고 생각한다면

다양한 기술들을 공부해 스펙트럼을 넓혀보자!

 

 

 

 

 

'Android > Common' 카테고리의 다른 글

수명주기; Lifecycle  (0) 2020.06.22
비동기  (0) 2020.05.14
안드로이드 프로젝트 루틴  (0) 2020.02.20

 

개발포스팅은 언제나 유통기한을 조심하세요.

 

 

Github : https://github.com/junghun9102/AndroidTemplate

Branch : ui/custom_dialog

 

junghun9102/AndroidTemplate

Contribute to junghun9102/AndroidTemplate development by creating an account on GitHub.

github.com

 

Activity 외에 필수적으로 사용하게 되는 Toast, Dialog 등이 있다.

앱 디자인에 더 완벽하게 맞추고 싶을 수 있다.

 

커스텀해서 사용해보자!

 

 

 


CustomToast

 

 

 

 

view_toast.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_view_toast"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:padding="15dp"
        android:background="#7000"
        android:gravity="center"
        android:textColor="@android:color/white"/>

</LinearLayout>

 

원하는 토스트 레이아웃을 만든다

 

 

 

CustomToast.kt

object CustomToast {

    fun show(context: Context, stringResId: Int, duration: Int = Toast.LENGTH_SHORT) = show(context, context.getString(stringResId), duration)

    private fun show(context: Context, content: String, duration: Int = Toast.LENGTH_SHORT) {
        val view = LayoutInflater.from(context).inflate(R.layout.view_toast, null)
        view.tv_view_toast.text = content

        Toast(context).apply {
            setGravity(
                Gravity.FILL_HORIZONTAL or Gravity.BOTTOM,
                0,
                context.dimen(R.dimen.custom_toast_bottom_margin)
            )
            this.duration = duration
            setView(view)
        }.show()
    }

}

 

dimen에 bottomMargin을 정해두고 사용했다.

(혹시 모르시는 분이 있을까봐 context.dimen은 anko 라이브러리)

 

토스트 내용은 하드코딩하지 않는 한 resource 레퍼런스만

받아서 사용하기 때문에 String으로 받는 함수는 private으로 묶어 두었다.

 

 

 

 

ContextExtensions.kt

fun Context.showToast(resId: Int) = CustomToast.show(this, resId)

 

MainActivity.kt

    private fun initViews() {
        btn_main_toast.setOnClickListener {
            showToast(R.string.app_name)
        }
    }

 

사용하기 쉽도록 extension을 추가했다.

 

 

 


CustomDialog

 

 

 

 

원하는 레이아웃형태 만들기

view_dialog.xml

더보기
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_view_dialog_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textSize="20dp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tv_view_dialog_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="20dp"
        android:gravity="end">

        <TextView
            android:id="@+id/tv_view_dialog_negative"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:padding="5dp"
            android:foreground="?attr/selectableItemBackground"
            android:text="@string/common_cancel" />

        <TextView
            android:id="@+id/tv_view_dialog_positive"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:foreground="?attr/selectableItemBackground"
            android:text="@string/common_confirm" />

    </LinearLayout>
    
</LinearLayout>

 

 

 

 

CustomDialog.kt

class CustomDialog(
    context: Context,
    title: String,
    content: String? = null
) {

    private val view = LayoutInflater.from(context).inflate(R.layout.view_dialog, null)

    private val dialog = AlertDialog.Builder(context)
        .setView(view)
        .create()

    fun show() {
        dialog.show()
    }
    
    fun setPositiveButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setPositiveButton(view.context.getString(buttonTextResId), func)
    
    /*
        이하 코드생략
    */
    
}

 

전체코드 보기

더보기
class CustomDialog(
    context: Context,
    title: String,
    content: String? = null
) {

    constructor(context: Context, titleResId: Int, contentResId: Int? = null) : this(
        context,
        context.getString(titleResId),
        contentResId?.let { context.getString(it) }
    )

    private val view = LayoutInflater.from(context).inflate(R.layout.view_dialog, null)
    private val titleView = view.tv_view_dialog_title
    private val contentView = view.tv_view_dialog_content
    private val positiveView = view.tv_view_dialog_positive
    private val negativeView = view.tv_view_dialog_negative
    private val defaultClickFunc = { dialog.dismiss() }

    init {
        titleView.text = title
        content?.let {
            contentView.text = it
        } ?: kotlin.run {
            contentView.visibility = View.GONE
        }

        positiveView.setOnClickListener { defaultClickFunc.invoke() }
        negativeView.setOnClickListener { defaultClickFunc.invoke() }
    }

    private val dialog = AlertDialog.Builder(context)
        .setView(view)
        .create()

    fun show() {
        dialog.show()
    }

    fun setPositiveButton(buttonText: String, func: () -> Unit = defaultClickFunc): CustomDialog {
        setTextViewForButton(positiveView, buttonText, func)
        return this
    }

    fun setPositiveButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setPositiveButton(view.context.getString(buttonTextResId), func)

    fun setNegativeButton(buttonText: String, func: () -> Unit = defaultClickFunc): CustomDialog {
        setTextViewForButton(negativeView, buttonText, func)
        return this
    }

    fun setNegativeButton(buttonTextResId: Int, func: () -> Unit = defaultClickFunc) =
        setNegativeButton(view.context.getString(buttonTextResId), func)

    private fun setTextViewForButton(view: TextView, buttonText: String, func: () -> Unit) {
        view.run {
            text = buttonText
            setOnClickListener {
                func.invoke()
                dialog.dismiss()
            }
        }
    }

}

 

 

MainActivity.kt

    private fun initViews() {
        btn_main_alert_dialog.setOnClickListener {
            CustomDialog(this, R.string.dialog_delete_title, R.string.dialog_delete_content)
                .setPositiveButton(R.string.common_delete) {
                    showToast(R.string.message_delete_complete)
                }
                .show()
        }
    }

 

핵심은 인플레이팅한 커스텀 뷰를 AlertDialog에 setView하는 것이다.

 

기존의 AlertDialog를 대체할 Dialog를 만들었지만

전혀 다른 방식으로 간단한 선택창 또는 도움창 등을 보여주는데 사용할 수 있다.

 

 

 

 

 

'창고' 카테고리의 다른 글

Retrofit / OkHttp  (0) 2020.05.29
Activity간의 화면 전환  (0) 2020.03.11
internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09

 

WARNING

 

 

App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW intent filter. See issue explanation for more details.

inspection info:Adds URLs to get your app into the Google index; to get installs and traffic to your app from Google Search

 

issue id GoogleAppIndexingWarning

 

More info:

https://g.co/Appindexing/AndroidStudio

 

 

 


WHY

 

링크를 들어가 보면 앱 링크 추가에 관한 내용이 나온다.

 

앱 링크란 앱의 특정 컨텐츠로 사용자를 바로 안내하는 HTTP URL이다.

해당 기능을 통해 앱에 더 접근하기 쉽고 더 많은 유입을 이끌어 낼 수 있다.

 

다만 웹과 앱을 동시에 보유한 프로젝트에 한해 가능해 보인다.

 

여기서 구글 검색에서 해당 컨텐츠에 도달하기 위해 ACTION_VIEW 인텐트가 필요하다.

기본 설정으로 최소 하나 이상의 Activity에 ACTION_VIEW 인텐트 필터가 필요하다.

 

 

 


SOLUTION

 

 

<action android: name="android.intent.action.VIEW" />

위 코드를 한 개 이상의 Activity에 추가하면 warning을 해결할 수 있다.

 

 

 


ADVANCE

 

앱 링크에 대해 더 자세히 알고 싶다면...

 

https://developer.android.com/training/app-links

 

Android 앱 링크 처리하기  |  Android 개발자  |  Android Developers

기기에서 웹 링크를 따라가는 사용자는 종종 혼란스러운 선택에 직면합니다. 링크를 탭하면 시스템에서 사용자에게 링크를 처리할 앱을 지정해 줄 것을 요청하는 경우가 자주 있습니다. 예를 들어 은행에서 온 이메일에 포함된 URI를 클릭할 때 다음과 같은 대화상자가 열릴 수 있습니다.

developer.android.com

 

https://developer.android.com/training/app-links/deep-linking

 

앱 콘텐츠 딥 링크 만들기  |  Android 개발자  |  Android Developers

사용자가 링크에서 앱에 진입할 수 있도록 하려면 관련 활동의 인텐트 필터를 앱 manifest에 추가해야 합니다. 이러한 인텐트 필터는 모든 활동의 콘텐츠로 연결되는 딥 링크를 허용…

developer.android.com

 

https://developer.android.com/studio/write/app-link-indexing

 

Android 앱 링크 추가  |  Android 개발자  |  Android Developers

Android 앱 링크는 Android 앱의 특정 콘텐츠로 사용자를 바로 안내하는 HTTP URL입니다.

developer.android.com

 

 

 

 

 

overridePendingTransition(int enterAnim, int exitAnim)

Call immediately after one of the flavors of startActivity(android.content.Intent) or finish() to specify an explicit transition animation to perform next.

 

 

1. Activity가 실행, 종료 코드 직후 바로 호출하면된다.

2. 첫번째 파라미터로 보여질 화면의 애니메이션을 입력한다.

3. 두번째 파라미터로 종료되거나 가려질 화면의 애니메이션을 입력한다.

 

 

 

 


 

MainActivity의 왼쪽에서 LeftActivity가 들어온다.

 

 

MainActivity.kt

        btn_main_left.setOnClickListener {
            startActivity(Intent(this@MainActivity, LeftActivity::class.java))
            overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
        }

 

1. startActivity 후에 overridePendingTransition을 호출한다.

2. LeftActivity는 왼쪽에서 들어와야 하기 때문에 slide_in_left를 넘겨준다.

3. MainActivity는 오른쪽으로 나가야 하기 때문에 slide_out_right를 넘겨준다.

 

 

 

slide_in_left.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

slide_out_right.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

 

 

 

 


 

종료시에는 반대로

MainActivity가 오른쪽에서 들어오고 LeftActivity가 왼쪽으로 나가는 것이 자연스럽다.

 

 

LeftActivity.kt

    override fun finish() {
        super.finish()
        overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
    }

 

1. finish 후에 overridePendingTransition을 호출한다.

2. MainActivity는 오른쪽에서 들어와야 하기 때문에 slide_in_right를 넘겨준다.

3. LeftActivity는 왼쪽으로 나가야 하기 때문에 slide_out_left를 넘겨준다.

 

 

 

slide_in_right.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%p" android:toXDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

slide_out_left.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="-100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

 

 

 

 

 


 

위에 예시를 포함한

상황에 맞는 애니메이션을 사용하면 된다.

 

fromXDelta, toXDelta를 fromYDelta, toTDelta로 변경하면 위아래로 이동할 수 있고

android.R.anim.fade_in, android.R.anim.fade_out을 사용할 수도 있다.

 

 

 

 

 

'창고' 카테고리의 다른 글

Retrofit / OkHttp  (0) 2020.05.29
custom_dialog (+custom_toast)  (0) 2020.05.02
internal_storage  (0) 2020.02.26
permission  (0) 2020.02.26
DP(Device Independence Pixel)?  (0) 2019.06.09

 

 

 

DIP (Dependency Inversion Principle; 의존 역전 법칙)

프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.". DI(의존성 주입) 역시 이 원칙을 따르는 방법 중에 하나이다.

 

 

 


구현체에 의존

 

 

 

메인 화면과 메모 상세 화면에서 메모 데이터를 조작하고 싶다.

그래서 MainPresenter와 MemoDetailPresenter는 MemoLocalRepository를 가지고 있다.

 

로컬 데이터베이스(임베디드 데이터베이스)를 사용하여 앱을 만드려한다.

MemoLocalRepository는 *RoomDatabase에서 메모 데이터를 받아오도록 작성했다.

*RoomDatabase : Android 임베디드 데이터베이스

 

 

 

개발도중 서버를 사용하는 것으로 요구사항이 변경되었다.

 

이로 인해 MemoLocalRepository를 고쳐야했고 이를 사용중이던 MainPresenter와 MemoDetailPresenter 역시 수정이 필요했다. (의존성)

 

 

 

 


DIP 적용

 

 

위와 같은 일을 막기 위해 구조부터 변경했다.

MemoRepository를 인터페이스로 만들었다.

 

MemoLocalRepository는 MemoRepository 인터페이스상속받아 만들었고

MainPresenter와 MemoDetailPresenter는 MemoRepository 인터페이스로 가지고 있다.

 

사용하는 부분에서도 구현하는 부분에서도 추상화에만 의존하고 있다.

 

이로 인해 MainPresenter와 MemoDetailPresenter는

어디서 어떻게 받아오는지 상관없이 인터페이스 함수대로 데이터를 조작할 수 있다.

 

MemoLocalRepository 대신 MemoRemoteRepository로 변경하던 MemoDummyRepository로

변경하던 상관없이 MemoRepository에만 의존성을 가지고 있으면 된다.

 

 

 

 

 


DI

 

MemoPresenter는 MemoRepsotiroy 인터페이스에만 의존하면 된다.

그럼 MemoRepository는 어디서 어떻게 생성해주지?

 

어디선가 MemoLocalRespository를 생성해서 전달해준다면 

그 생성한 클래스는 결국 구현체에 의존하게 되는데 조삼모사가 아닌가?

 

그래서 DI가 존재한다.

 

전문적으로 생성해서 전달해주는 모듈이 있고

구현체를 몰라도 모듈에 설정한대로 외부에서 구현체를 생성해 전달해주어

사용하는 부분은 구현체에 의존성없이 사용할 수 있다.

 

class MainPresenter(
    private val memoRepository: MemoRepository
)

private val presenter: MainPresenter by inject()

 

위와 같은 코드 형태이다.

by inject()를 통해 외부에서 인스턴스를 만들어 준다.

 

어떤 구현체의 인스턴스인지 명시하지 않고 사용할 수 있어 의존성이 전혀 없다. 

 

DI 역시 DIP의 방법 중에 하나이고

DI를 통해 DIP를 더 잘 지킬 수 있다.

 

 

 

 

 


외부 포스팅

 

좋은 DIP 포스팅

https://medium.com/@lightsoo/dependency-inversion-principle-52a7bb6dd6be

 

Dependency Inversion Principle

본부장님의 클린코드 강의중 Function Structure를 공부하면서 DIP에 대한 설명을 듣는데 이해가 잘 되지않아서 다시 위의 내용을 번역 + DIP강의를 듣고 정리한 문서이다.

medium.com

 

 

 

'SoftwareDesign' 카테고리의 다른 글

ISP  (0) 2020.03.01
LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

ISP (Interface Segregation Principle; 인터페이스 분리 법칙)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

 

 

 

 


범용 인터페이스

 

캐셔가 필요하지 않은 기능들을 포함한 계산기를 사용하고 있다.

 

 

 


인터페이스 분리

 

 

인터페이스를 작은 단위로 분리함으로 캐셔에게만 맞는 계산기와

신입생에거 필요한 계산기를 인터페이스를 가지고 있을 수 있게 되었다.

 

인터페이스를 작은 단위로 만들게 되면 사용성이나 확장성이 좋아진다.

 

 

 


FIN

 

사실 인터페이스를 범용으로 쓸 일이 없었다.

인터페이스를 사용보다 크게 만들어 문제되는 일도 없었다.

 

다른 포스팅들을 더 읽어보니

'SRP와 같이 인터페이스가 하나의 책임을 가져야 한다'

설명하는 경우가 지배적이다.

 

interface SmartPhoneFunction {
    
    fun call()
    fun sms()
    fun showInternet()
    
}

class SmartPhone : SmartPhoneFunction

 

interface CallFunction {
    fun call()
}

interface SMSFunction {
    fun sms()
}

interface InternetFunction {
    fun showInternet()
}

class SmartPhone : CallFunction, SMSFunction, InternetFunction

 

와 같이 분리하는 예시를 많이 보여준다.

 

이 예시가 더 잘 드러나 보인다.

 

하지만 위에 계산기 예시 역시

NormalCalculator는 기본적인 계산기의 책임을 다하고

ScientificCalculator는 공학 계산기의 책임을 다하는

SRP와 같은 맥락의 예시이다.

 

 

결론은 '인터페이스를 하나의 책임을 갖도록 분리해라.' 가 가장 적합해보인다.

 

 

SOLID에 대해 포스팅하면서 책임이라는 단어가 정말 많이 나온다.

 

그 만큼 중요하지만 책임의 크기를 정하는 것에 대해서도 고민해야할 것이다.

 

무조건 작고 많이 분리한다고 좋은 것도 아니다.

오히려 너무 많은 인터페이스와 클래스가 복잡도를 높일 수가 있다.

 

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

DIP  (0) 2020.03.01
LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

LSP (Liskov Substitution Principle; 리스코브 치환 법칙)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

 

위키의 설명을 보고 이해하려다가 혼란에 빠졌었다.

 

쉽게 설명해서

부모 클래스로 형변환하여 사용해도 아무런 이상없이 사용하면 된다.

자식 클래스 부모 클래스의 만든 전제 내에서 확장하면된다.

 

 

 

 


위반 예시

 

동물은 먹는다. (O)

고양이는 물고기를 먹는다. (O)

고양이 동물이다. (O)

동물은 물고기를 먹는다. (O)

 

직사각형은 가로 세로가 같던 다르던 상관없다. (O)

정사각형은 가로 세로가 같아야 한다. (O)

정사각형 직사각형이다. (O)

직사각형은 가로 세로가 같아야 한다. (X)

 

자식이 부모를 모순되게 만들지 말라는 원칙이다.

 

다형성을 통해 다양한 자식 클래스들을 부모클래스로 통합해 처리하는 로직이 있을 때

자식 중에 로직의 기대를 벗어나게 오버라이딩했다면 문제가 발생할 수 있다.

 

 

 

 


FIN

 

사실 모순을 발생하는 예시가 적으며

발생하더라도 문제가 생기지 않을 확률이 높다.

 

그렇게 신경쓰고 있어야할 원칙은 아닌듯 싶다.

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

DIP  (0) 2020.03.01
ISP  (0) 2020.03.01
OCP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01

 

 

 

OCP (Open/Cloesed Priciple; 개방-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.

 

 

SRP의 연장선에 있는 원칙이라고 생각한다.

 

SRP에 맞게 하나의 책임을 가지고 있는 클래스를 확장하여 새로운 책임에 집중한 클래스를 만들 수 있다. 

그렇게 되면 해당 클래스는 자신의 책임에 집중하고 다른 책임을 변경할 필요가 없다.

 

만약 변경이 일어날 수 있는 코드가 있다면 추상화를 통해 구현을 상속받는 클래스에 맡겨야 한다.

추상화한 코드로 인해 상위 클래스는 수정할 필요가 없고 하위 클래스는 구현을 통해 확장할 수 있다.

 

 

 

 

 


OCP 예시

 

 

 

이미지를 포함하고 있는 메모를 작성하는 안드로이드 코드이다.

 

 

메모를 작성할 때, 메모를 수정할 때 모두 이미지를 받아올 수 있어야 하기 때문에

중복된 코드를 GetImageActivity로 캡슐화했다.

 

물론 이미지를 필요로 하는 모든 액티비티에서 재사용 가능하다.

 

 

1. 이미지를 Gallery 또는 Camera에서 받아온다. (getImageFromGallery, getImageFromCamera)

2. 받아온 이미지를 적합한 사이즈로 가공한다. (workAfterGetFromGallery, workAfteGetFromCamera)

3. 가공한 이미지를 onGetImageFile를 호출해 전달한다.

 

 

GetImageActivity를 상속받는 클래스들은 이미지를 받아오는 책임에 대해 건드릴 필요가 없다. (폐쇄)

상속받아 이미지를 받아 어떻게 자신의 책임을 다 할지만 생각하면 된다. (개방)

 

그렇기 때문에

 

GetImageActivity의 책임과 관련된 메소드들 최대한 폐쇄해두었다.

getImageFromGallery, getImageFromCamera : FINAL

workAfterGetImageFromGallery, workAfterGetImageFromCamera : FINAL, PRIVATE

 

추상 메소드 onGetImageFile을 구현해서 자신의 책임에 대해 확장하도록 했다.

 

 

 

 


FIN

 

내가 포스팅한 내용은 상속하여 확장하는 클래스의 입장에서의 OCP이다.

 

다른 사람들의 포스팅을 보니 정말 많은 설명들이 있었다.

 

인터페이스를 사용한 방법, 추상 메소드를 사용한 방법

상속에서의 OCP, 조립에서의 OCP

 

많은 OCP의 예가 있으리라 생각하고

그 모든 것들이 OCP라고 생각한다.

 

초기 원칙을 정할 때 정해진 케이스가 있었을지 몰라도..

 

 

결국 OCP의 핵심은

 

 

상속을 통한 확장, 인터페이스를 구현한 클래스의 증가, 기능 변경, 알고리즘 교체

같은 어떤 확장을 하더라도 기존의 코드들이 영향을 받지 않는다는 것이다.

 

 

 

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

ISP  (0) 2020.03.01
LSP  (0) 2020.03.01
SRP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19

 

 

 

SRP (Single Reponsibility Principle; 단일 책임 원칙)

 

한 클래스는 하나의 책임을 가져야 한다.

클래스가 변경되는 이유가 하나여야 한다.

 

 

 

 

 


SRP가 뭐지?

 

 

사진을 추가할 수 있는 메모 앱을 만드려고 한다.

메모를 작성하는 화면수정하는 화면이 필요하다.

 

거의 비슷한 동작을 할 것이다.

두 개의 클래스를 만든다면 분명 중복 코드가 생기게 된다.

 

그래서 천재적으로 하나의 MemoManipulateAcitivity를 만들었다.

 

 

 

짠! 아주 책임이 가득한 클래스가 완성되었다.

 

SRP고 뭐고 잘 동작한다.

나는 훌륭하게 일을 해냈다.

 


작성일자와 수정일자를 표시하는 요구사항이 추가되었다.

 

코드를 수정하고 메모를 작성해 보니 잘 표시한다. 출시!

 

출시하고 보니 수정할 때마다 작성일자가 오늘일자로 수정된다.

아 수정하는 일도 했었지...

 

한 클래스에 두가지의 책임이 공존해 실수가 나왔다.

 

 


프로필 화면에서 사진을 추가하는 요구사항이 추가되었다.

 

프로필 화면에서도 이미지를 받아와야 하는데 코드가 중복된다....

 

하나의 책임에 집중하지 못한 클래스는 재사용하기 힘들다.

 

 

 

 

 


 

하나의 책임에 집중해보자

 

 

클래스당 하나의 책임만 가지도록 만들어 보자

 

 

하나의 책임을 가지도록 클래스를 분리했다.

각 클래스는 하나의 책임에 집중한 적은 양의 코드만 남았다.

 

 

이미지를 받아오는 코드를 캡슐화 했다.

이를 통해 중복코드없이 프로필 설정화면에도 재사용할 수 있게 되었다.

 

메모 작성 화면과 수정 화면의 공통 조작 부분을 MemoManipulateActivity로 캡술화하고

두 클래스에 의존적인 메모를 만드는 부분은 추상 메소드 getMemo로 만들었다.

 

이로 인해 작성 화면과 수정 화면은 서로 불필요한 영향을 받을 일이 줄고 자신의 책임에 집중할 수 있다.

 

 

 

 

 


외부 포스팅

 

더 자세하게 깊게 알고 싶다면..

https://velog.io/@amobmocmo/Software-Design-SRP-Single-Responsibility-Principle-4pk2aklhqo

 

[Software Design] SRP (Single Responsibility Principle)

책임 로버트 C. 마틴은 책임을 변경하려는 이유라고 정의했다. 변화의 시기와 이유가 같다면 같은 책임 아래 있다고 보는 것이다. 반대로, 한 객체 내에서 변화의 시기, 이유가 다른 부분이 존재한다면 그 객체는 여러 책임을 가지고 있는 것이다. 그에 따라 이렇게 좀 더 알아보기 쉽게 정의할 수 있을 것 같다. > 책임은 객체에 의해 정의되는 응집도있는 ...

velog.io

 

 

 

 

'SoftwareDesign' 카테고리의 다른 글

LSP  (0) 2020.03.01
OCP  (0) 2020.03.01
SOLID 개요  (0) 2020.03.01
3. Observer  (0) 2020.01.19
2. Strategy  (0) 2020.01.07

+ Recent posts