Layout

레이아웃은 앱에서 사용자 인터페이스를 위한 구조(예: 액티비티)를 정의합니다.

레이아웃의 모든 요소는 ViewViewGroup 객체의 계층을 사용하여 빌드됩니다. 

일반적으로 View는 사용자가 보고 상호작용할 수 있는 것을 그립니다. 

View Group은 그림 1과 같이 View와 ViewGroup 객체의 레이아웃 구조를 정의하는 투명 컨테이너입니다. 

 

그림 1. 레이아웃을 정의하는 뷰 계층.

일반적으로 View 객체는 '위젯' 이라고 하고 여러 하위 클래스 중 하나가 될 수 있습니다.

(예 : Button 또는 TextView)

 

ViewGroup 객체는 대개 '레이아웃' 이라고 부르고 다양한 레이아웃 구조를 제공하는 여러 유형 중 하나일 수 있습니다. (예: LinearLayout 또는 ConstraintLayout)

 

레이아웃을 정의하는 방법

  • UI 요소를 XML로 선언.

  • 런타임에 레이아웃 요소 인스턴스화.

XML에서 UI를 선언하면 동작을 제어하는 코드와 앱을 표현하는 것을 분리할 수 있습니다.

또한 XML 파일을 사용하면 다양한 화면 크기와 방향에 여러 가지 레이아웃을 쉽게 제공할 수 있습니다.

 

Android 프레임워크는 위 방법 중 하나 또는 두 가지 모두를 사용하여 앱의 UI를 빌드하는 유연한 수단을 제공합니다.

예를 들어 앱의 기본 레이아웃을 XML에서 선언한 다음, 런타임에서 레이아웃을 수정할 수 있습니다.

XML 쓰기

Android의 XML 어휘를 사용하면 UI 레이아웃과 그 안에 들어있는 화면 요소를 HTML에서 웹 페이지를 디자인할 떄와 같은 방식으로 신속하게 디자인할 수 있습니다. 

 

각 레이아웃 파일에는 반드시 딱 하나의 루트 요소만 있어야 하며, 이는 View 또는 ViewGroup 객체여야 합니다.

루트 요소를 정의한 후에 더 많은 레이아웃 객체 또는 위젯을 하위 요소로 추가하여 계층적으로 레이아웃을 정의하는 뷰 계층을 빌드할 수 있습니다. 

 

레이아웃을 XML로 선언하고 나면 그 파일을 Android 프로젝트의 res/layout/ 폴더 내에 .xml 확장자로 저장하여 적절하게 컴파일되도록 합니다.

 

레이아웃 리소스

레이아웃 리소스는 Activity의 UI 또는 UI 구성요소의 아키텍처를 정의합니다.

 

파일 위치

  • res/layout/filename.xml

  • 파일 이름이 리소스 ID로 사용됩니다.

컴파일된 리소스 데이터 유형

  • View 리소스를 가리키는 리소스 포인터입니다.

리소스 참조

  • 자바 : R.layout.filename

  • XML : @[package:]layout/filename

더보기

요소

<ViewGoup>

다른 View 요소의 컨테이너. 다양한 ViewGroup 객체가 있으며 각 객체를 사용하여 여러 방식으로 하위 요소의 레이아웃을 지정할 수 있습니다. VIewGroup 객체이는 LinearLayout, RelativeLayout, FrameLayout 등이 있습니다.

 

ViewGroup의 모든 파생 객체가 중첩된 View를 허용한다고 가정해서는 안됩니다. 일부 ViewGroup은 AdapterVIew 클래스를 구현한 것이며 이 클래스는 Adapter 하위에서만 하위 요소를 걸정합니다.

 

속성

android:id

  • 리소스 ID. 요소의 고유한 리소스 이름이며 이를 사용하여 애플리케이션에서 ViewGroup의 참조를 가져올 수 있습니다.

android:layout_height   및   android:layout_width

  • 크기 또는 키워드. 필수사항. 크기 값(또는 크기 리소스) 또는 키워드("match_parent" 또는 "wrap_content")

더 많은 속성이 ViewGroup 기본 클래스에서 지원되며 사용가능한 모든 속성의 참조는 ViewGroup 클래스의 대응하는 문서를 확인하세요.

 

<View>

일반적으로 '위젯'이라고 하는 개별 UI 구성요소입니다. VIew 객체에는 TextVIew, Button, CheckBox 등이 있습니다.

 

속성

android:id

  • 리소스 ID. 요소의 고유한 리소스 이름이며 이를 사용하여 애플리케이션에서 ViewGroup의 참조를 가져올 수 있습니다.

android:layout_height   및   android:layout_width

  • 크기 또는 키워드. 필수사항. 크기 값(또는 크기 리소스) 또는 키워드("match_parent" 또는 "wrap_content")

더 많은 속성이 View 기본 클래스에서 지원되며 사용가능한 모든 속성의 참조는 대응하는 참조 문서를 확인하세요.

 

<requestFocus>

View 객체를 나타내는 모든 요소는 빈 요소를 포함할 수 있고 이 빈 요소는 화면에서 상위 요소에 초기 포커스를 줍니다. 이러한 요소는 파일당 하나만 포함할 수 있습니다.

 

<include>

레이아웃에 레이아웃 파일을 포함합니다.

속성

layout

  • 레이아웃 리소스. 필수사항. 레이아웃 리소스를 가리키는 참조입니다.

android:id

  • 리소스 ID. 요소의 고유한 리소스 이름이며 이를 사용하여 애플리케이션에서 ViewGroup의 참조를 가져올 수 있습니다.

android:layout_height   및  android:layout_width

  • 크기 또는 키워드. 필수사항.  크기 값(또는 크기 리소스) 또는 키워드("match_parent" 또는 "wrap_content"

<include> 내에 포함된 레이아웃에서 루트요소에 의해 지원되는 다른 레이아웃 속성을 포함할 수 있고 그러한 속성은 루트 요소에 정의된 속성을 재정의합니다.

주의 : <include> 태그를 사용하여 레이아웃 속성을 재정의하면 다른 레이아웃 속성을 적용하기 위해 andorid:layout_height, andorid:layout_width를 모두 재정의해야 합니다.

 

레이아웃을 포함하는 또 다른 방법은 ViewStub을 사용하는 것입니다. ViewStub은 가벼운 View로서, 명시적으로 펼칠 때까지 레이아웃 공간을 소비하지 않으며 이 시점에 android:layout 속성에 의해 재정의된 레이아웃 파일을 포함합니다. ViewStub을 사용하는 방법에 관한 자세한 내용은 주문형 뷰 로드를 참조합니다.

 

<merge>

레이아웃 계층 구조에 그려지지 않은 대체 루트 요소입니다. 루트 요소로 이 요소를 사용하면 이 레이아웃이 이미 적절한 상위 뷰를 포함하여 <merge> 요소의 하위 요소를 포함하는 레이아웃에 배치되는 경우에 유용합니다.

<include>를 사용하여 이 레이아웃을 다른 레이아웃 파일에 포함하고 이 레이아웃이 다른 ViewGroup 컨테이너가 필요하지 않을 때에 특히 유용합니다. 레이아웃 병합에 관한 자세한 내용은 <include/>로 레이아웃 재사용을 참조하세요.

 

android:id값 

ID 값의 경우 일반적으로 "@+id/name" 구문 형식을 사용해야 합니다. 더하기 기호(+)는 새 리소스 ID를 나타내며 리소스 정수가 아직 존재하지 않는다면 aapt 도구는 R.java 클래스에 새 리소스 정수를 만듭니다.

 

android:layout_height 및 android:layout_width의 값

높이 및 너비 값은 Android에서 지원하는 모든 크기 단위(px, dp, sp, pt, in, mm) 또는 다음의 키워드를 사용하여 표현할 수 있습니다.

  • match_parent : 상위 요소의 크기와 일치하도록 크기를 설정합니다. fill_parent 지원을 중단하기 위해 API 레벨 8에서 추가 되었습니다.

  • wrap_content

XML 리소스 로드

앱을 컴파일하는 경우, 각 XML 레이아웃 파일이 View 리소스 안에 컴파일 됩니다.

Activity.onCreate() 콜백 구현에서 앱 코드로부터 레이아웃 리소스를 로드해야 합니다.

setContentView() 를 호출하고 R.layout.layout_file_name의 형태로 레이아웃 리소스에 대한 참조로 전달합니다.

예를 들어 XML 레이아웃이 main_layout.xml로 저장된다면 다음과 같이 액티비티에 대해 로드합니다.

fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_layout)
}

 액티비티 내의 onCreate() 콜백 메서드는 액티비티가 시작될 때 Android 프레임워크에 의해 호출됩니다.

 

특성

모든 View와 ViewGroup 객체는 고유한 여러가지 XML 특성을 지원합니다.

어떤 특성은 View 객체에만 적용되지만(예: TextView는 textSize 특성 지원), 이 특성은 이 클래스를 확장할 수 있는 View 객체로부터 상속받은 것입니다. 어떤 특성은 루트 View 클래스에서 상속되기 때문에(예: id 특성) 모든 View 객체에 공통적으로 적용됩니다.

 

그리고 나머지 특성은 "레이아웃 매개변수"로 간주됩니다. 이들은 View 객체의 특정한 레이아웃 방향을 설명하는 것으로, 이는 해당 객체의 상위 ViewGroup 객체에서 정의된 바에 따릅니다.

 

ID

View 객체는 트리 내에서 뷰를 고유하게 식별할 수 있는 ID가 연결될 수 있습니다. 

앱을 컴파일할 때 이 ID는 정수로 참조되지만, 일반적으로 레이아웃 XML 파일의 id 특성에서 문자열로 할당됩니다.

이는 모든 View 객체에 공통적인 XML 특성이며(View 클래스에서 정의), 매우 자주 사용하게 될 것입니다.

 

문자열 시작 부분에 있는 앳 기호(@)는 XML 파서가 ID 문자열의 나머지를 파싱하고 확장하여 ID 리소스로 식별해야 한다는 것을 나타냅니다. 더하기 기호(+)는 이것이 새 리소스 이름이며, 이것을 반드시 생성하여 리소스에 추가해야 한다는 것을 뜻합니다. 

 

Android 리소스를 참조할 때에는 더하기 기호는 필요하지 않지만 android 패키지 네임스페이스를 다음과 같이 반드시 추가해야 합니다.

android:id="@android:id/empty"

android 패키지 네임스페이스가 들어가면 이제 로컬 리소스 클래스에서가 아니라 android.R 리소스 클래스에서 ID를 참조하게 됩니다.  

 레이아웃 매개변수

layout_something  이라는 XML 레이아웃 특성이 뷰가 상주하는 ViewGoup에 대해 적절한 뷰의 레이아웃 매개변수를 정의합니다. 

 

모든 ViewGroup 클래스가 중첩된 클래스를 하나씩 구현하며 이것이 ViewGroup.LayoutParams 를 확장합니다.

이 하위 클래스에는 각 하위 뷰의 크기와 위치를 뷰 그룹에 적절한 방식으로 정의하는 속성 유형이 들어있습니다.

그림 2에서 볼 수 있듯이, 상위 뷰 그룹이 각 하위 뷰의 레이아웃 매개변수를 정의합니다.

 

그림2. 각 뷰와 연관된 레이아웃 매개변수가 있는 뷰 계층을 시각화한 것.

모든 LayoutParams 하위 클래스에는 설정 값에 대한 각기 자신만의 구문이 있다는 점을 참고하세요.

각 하위 요소는 자신의 상위에 적합한 LayoutParams를 정의해야 합니다. 다만 이것은 자신의 하위에 대해 각기 다른 LayoutParams도 정의할 수 있습니다.

 

모든 ViewGroup에는 너비와 높이(layout_width, layout_height)가 포함되며, 각 뷰는 이들을 반드시 정의해야 합니다. 선택사항으로 여백과 테두리도 포함하는 LayoutParams도 많습니다.

 

너비와 높이는 정확한 치수로 지정할 수 있습니다. 다만 이것은 자주 하지 않는 것이 좋습니다.

그보다는 다음과 같은 상수 중 하나를 사용하여 너비 또는 높이를 설정하는 경우가 더 많습니다.

  • wrap_content는 콘텐츠에 필요한 치수대로 자동으로 크기를 조정하도록 뷰에 지시합니다.

  • match_parent는 상위 뷰 그룹이 허용하는 한 최대한으로 커지도록 뷰에 지시합니다.

 일반적으로 픽셀과 같이 절대적인 단위를 사용하여 레이아웃 너비와 높이를 지정하는 것은 권장하지 않습니다.

그 대신, 밀도 독립적인 픽셀 단위(dp), wrap_content 또는 match_parent와 같이 상대적인 측정치를 사용하는 것이 더 낫습니다. 이렇게하면 앱이 다양한 기기 화면 크기에 걸쳐서도 적절하게 표시되도록 보장하는데 도움이 되기 때문입니다.

 

레이아웃 위치

뷰의 모양은 직사각형입니다. 뷰에는 위치가 있으며, 이는 한 쌍의 왼쪽 및 상단좌표, 그리고 두 개의 치수가 너비와 높이를 나타내는 형식으로 표현됩니다. 위치와 치수의 단위는 픽셀입니다.

 

뷰의 위치를 검색할 수 있습니다. getLeft() 및 getTop() 메서드를 호출하면 됩니다.

getLeft()는 뷰를 나타내는 직사각형의 왼쪽, 즉 X좌표를 반환합니다.

getTop()은 뷰를 나타내는 직사각형의 상단, 즉 Y좌표를 반환합니다.

이들 메서드는 둘 다 해당 뷰의 상위에 상대적인 뷰의 위치를 반환합니다.

예를들어 getLeft() 가 20을 반환하는 경우 이는 해당 뷰가 그 뷰의 바로 상위의 왼쪽 가장자리에서 오른쪽으로 20픽셀 떨어진 곳에 있다는 뜻입니다.

 

그 외에도 불필요한 계산을 피하기 위해 여러가지 편의 메서드가 제공됩니다. 

getRight() 및 getBottom()을 들 수 있습니다. 이 메서드는 해당 뷰를 나타내는 직사각형의 오른쪽과 하단가장자리의 좌표를 반환합니다.

 

예를 들어 getRight()를 호출하는 것은 getLeft() + getWidth() 계산과 비슷합니다.

크기, 패딩 및 여백

뷰의 크기는 너비와 높이로 표현됩니다. 사실 하나의 뷰는 두 쌍의 너비 및 높이 값을 소유합니다.

 

첫 번째 쌍을 측정된 너비 및 측정된 높이라고 합니다. 이들 치수는 뷰가 상위 내에서 얼마나 커지고자 하는지를 정의합니다.

측정된 치수를 가져오려면 getMeasuredWidth() 및 getMeasuredHeidght() 를 호출합니다. 

 

두 번째 쌍은 단순히 너비 및 높이라고 일컬으며, 때로는 그리기 너비(drawing width) 및 그리기 높이(drawing height)로 부를 때도 있습니다. 이러한 치수는 그리는 시간과 배치 후에 뷰가 화면에 표시되는 실제 크기를 정의합니다. 이들 값은 측정된 너비및 높이와 달라도 되지만 꼭 달라야 하는 것은 아닙니다. 너비와 높이를 가져오려면 getWidth() 및 getHeight()을 호출하면 됩니다.

 

뷰의 치수를 측정하기 위해 뷰는 자신의 패딩을 감안합니다. 패딩은 뷰의 왼쪽, 상단, 오른쪽 및 하단 부분에 대해 픽셀로 표시됩니다. 패딩은 정해진 픽셀 수를 사용하여 뷰의 콘텐츠를 오프셋하는 데 쓰일 수도 있습니다. 

예를 들어 왼쪽 패딩을 2로 설정하면 해당 뷰의 콘텐츠를 왼쪽 가장자리에서 오른쪽으로 2픽셀 밀어냅니다.

패딩을 설정할 때는 setPadding(int, int, int, int) 메서드를 사용하면 되고,

이를 쿼리하려면 getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()을 사용하면 됩니다.

 

뷰는 패딩을 정의할 수 있지만, 여백(margin)에 대한 지원은 전혀 제공하지 않습니다. 다만 뷰 그룹이 그와 같은 지원을 제공합니다.

일반 레이아웃

ViewGroup 클래스의 각 하위 클래스는 각기 고유한 방식으로 자신 안에 중첩된 뷰를 표시합니다.

아래는 Android 플랫폼에서 기본 제공되는, 보다 보편적인 레이아웃 유형을 몇 가지 나타낸 것입니다.

 하나 이상의 레이아웃을 또 다른 레이아웃에 중첩하여 UI 디자인을 이룰 수도 있지만, 레이아웃 계층을 가능한 한 얕게 유지하도록 애써야 합니다. 중첩된 레이아웃이 적을수록 레이아웃이 더욱 빠르게 그려집니다.
(가로로 넓은 뷰 계층이 깊은 뷰 계층보다 낫습니다)

LinaearLayout

여러 하위 요소를 하나의 가로 또는 세로 방향 행으로 정리하는 레이아웃.

이 레이아웃은 창의 길이가 화면 길이를 웃도는 경우 스크롤바를 생성합니다.

RelativeLayout

여러 하위 객체의 위치를 서로 상대적으로 나타내거나, 상위와 상대적으로 나타낼 수 있도록 해줍니다.

WebView

웹 페이지를 표시합니다.

 

SUMMARY

레이아웃은 앱에서 사용자 인터페이스를 위한 구조를 정의합니다.

레이아웃의 모든 요소는 View 와 ViewGroup 객체의 계층을 사용하여 빌드됩니다.

 

레이아웃을 XML로 선언할 시 동작을 제어하는 코드와 앱을 표현하는 것을 분리할 수 있습니다. 또한 XML 파일을 사용하면 다양한 화면 크기와 방향에 여러 가지 레이아웃을 쉽게 제공할 수 있습니다. 

 

View 객체는 트리 내에서 뷰를 고유하게 식별할 수 있는 ID가 연결될 수 있으며, 앱을 컴파일할 때 이 ID는 정수로 참조되지만, 일반적으로 레이아웃 XML 파일의 id 특성에서 문자열로 할당됩니다.

 

View 의 모양은 직사각형입니다. 뷰에는 위치가 있으며, 이는 한 쌍의 왼쪽 및 상단좌표, 그리고 두 개의 치수가 너비와 높이를 나타내는 형식으로 표현됩니다. 위치와 치수의 단위는 픽셀입니다.

 

Reference

레이아웃 : developer.android.com/guide/topics/ui/declaring-layout?hl=ko

레이아웃 리소스 : developer.android.com/guide/topics/resources/layout-resource?hl=ko

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

MotionLayout  (0) 2020.12.02
ConstraintLaytout  (0) 2020.11.02

네트워크 상태체크하기

enum class NETWORK_STATUS {
    TYPE_WIFI,
    TYPE_MOBILE,
    TYPE_NOT_CONNECTED
}

class NetworkStatus {
    companion object {

        fun getConnectivityStatus(context: Context): NETWORK_STATUS {
            val manager: ConnectivityManager? =
                context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

            if (manager != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    val networkCapabilities = manager.getNetworkCapabilities(manager.activeNetwork)

                    if (networkCapabilities != null) {
                        if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                            return NETWORK_STATUS.TYPE_WIFI
                        } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                            return NETWORK_STATUS.TYPE_MOBILE
                        }
                    }
                } else {
                    val networkInfo = manager.activeNetworkInfo

                    if (networkInfo != null) {
                        val type = networkInfo.type

                        if(type == ConnectivityManager.TYPE_WIFI) {
                            return NETWORK_STATUS.TYPE_WIFI
                        } else if (type == ConnectivityManager.TYPE_MOBILE) {
                            return NETWORK_STATUS.TYPE_MOBILE
                        }
                    }
                }
            }

            return NETWORK_STATUS.TYPE_NOT_CONNECTED
        }
    }
}

 

EditText에 입력을 할 때 핸드폰의 키패드가 올라오게 됩니다. 이 때, 뷰의 하단에 있는 UI가 키패드가 올라옴에 따라 어떻게 보이길 원하나요? 보이는게 좋은가요? 아니면 가려지길 바라나요? 이번 글에선 이부분을 다뤄보겠습니다.

 

먼저 아래와 같은 뷰가 있습니다.

 

 

 

이 때 EditText에 입력을 하는데 RecyclerView가 키패드와 같이 따라 올라와 뷰가 이상해집니다.

이 문제는 어렵지 않게 해결할 수 있습니다. 

 

AndroidManifest.xml 파일에서는 키패드가 나타날 때 UI를 어떻게 처리할지 정할 수 있습니다.

 

AndroidManifest.xml 파일로 이동하여 해당 액티비티에 아래와 같은 속성을 적용해줍니다.

android:windowSoftInputMode="adjustPan"

이 속성은 키패드가 올라올 때 UI를 덮도록 설정해 줍니다.

android:windowSoftInputMode="adjustNothing"

이 속성도 UI와 관계없이 키패드가 올라오도록 해줍니다.

 

 

만약 UI가 잘리지 않고 그대로 보여지고 싶다면 아래와 같이 속성을 적용해주면 됩니다.

android:windowSoftInputMode="adjustResize"

 

'Android' 카테고리의 다른 글

@JvmOverloads  (0) 2020.12.15
@JvmOverloads  (0) 2020.12.15
[Android] 테스트  (0) 2020.04.11
[Android] Gradle  (0) 2020.04.10
[Android] ContentProvider, Service, BroadcastReceiver  (0) 2020.04.03

테스트는 왜 필요할까요? 다음의 몇가지를 들 수 있습니다.

  • 필요한 조건을 만족하는지 확인
  • 오류 발견
  • 앱의 사용성을 확인

자동테스트 & 수동테스트

수동테스트는 코딩 작업이 끝난 상태에서 apk를 만들고, 체크리스트로 작성한 테스트 케이스를 차례대로 실행하는 형태로 이뤄지는 경우가 많습니다.

 

수동 테스트의 특성

  • 최종 사용자와 완전히 똑같이 동작을 확인할 수 있다.
  • 사용성이나 쾌적함 등 정성적인 면에서 평가할 수 있다.

자동테스트는 '단위 테스트' , 'UI 테스트' 로 불리는 테스트 코드를 만들어 테스트 코드를 실행함으로써 앱이 바르게 동작하는지 확인합니다.

 

자동테스트의 특성

  • 반복 실행이 가능하다.
  • 테스트 실행 상황을 시각화할 수 있다.
  • 클래스와 메서드 등 사용자 조작으로 확인하기 어려운 단위의 동작을 확인할 수 있다.

이런 특성들이 있어 자동 테스트로 동작에 대체로 이상이 없다는 것을 확인한 다음, 수동 테스트를 통해 공개 전 최종 확인 및 사용자 경험을 최적화하는 식으로 구분해서 생각합니다. 

 

단위 테스트

클래스나 메서드처럼 아주 작은 단위를 검증하기 위해 실행되는 테스트입니다. 소스코드 자체의 타당성이나 품질을 확인할 수 있으며, 가장 작은 단위로 검증이나 실행이 이뤄지므로 실행 시간도 아주 짧습니다. 그러므로 일반적으로 통신이나 IO 처리와 같은 외부 리소스와의 통신을 무효로 하고, 단말이나 환경에 의존하지 않는 상태로 테스트할 필요가 있습니다.

UI 테스트

실제 동작 환경을 중요시하므로 일반적으로는 실제 기기나 에물레이터 상에서 실행됩니다. 

이름 그대로 단말의 표시를 바탕으로 확인해 갑니다. 또한 통신이나 IO 처리 등 외부 리소스를 이용해 테스트하는 경우도 있습니다.

 

JUnit

자바에서 사실상의 표준이 되는 단위 테스트 프레임워크이며, 버전 3과 버전 4의 큰 차이 중 하나는 테스트 코드에 어노테이션이 이용되는 점을 들 수 있습니다.

 

 

Mockito를 이용한 객체의 목화

단위 테스트에서는 IO 처리 등과 같은 외부의 영향을 받지 않게 테스트를 작성해야 합니다.

 

예를 들어, HTTP 통신을 하는 경우를 생각해 봅시다.
테스트할 때 HTTP 통신을 실제로 하는 테스트를 작성해 버리면 단말 설정이나 서버 사오항에 따라 테스트 결과가 달라집니다.  그런 상황에서는 테스트에 실패해도 코드에 문제가 있는지, 그렇지 않으면 설정 등 주변 환경에 문제가 있는지 판단할 수 없기에 신뢰할 수 있는 테스트라고 할 수 없습니다.

 

이런 사태를 피하고자 테스트할 때는 의존하는 처리를 위장할 필요가 생깁니다.

HTTP 통신의 예에서 위장이란 어떤 테스트에서는 성공 응답을 반드시 반환하고, 반대로 다른 테스트에서는 반드시 실패 응답을 반환하는게 되겠지요. 추상적으로 말하면 위장이란 '실제 처리는 하지 않고 원하는 값을 반환하거나 처리를 실행시키는 것' 이라고 할 수 있습니다. 위장한 객체를 잘 사용하면 언제 어떤 때라도 동작이 항상 일정한 테스트를 작성할 수 있게 됩니다.

 

이런 위장을 간편하게 구현할 때 사용하는 것이 '목 객체' 입니다. 

(목 라이브러리를 사용하지 않아도 테스트할 때 클래스를 상속하는 방식 등으로 위장을 실현할 수 있지만 구현할 게 너무 많아지므로 목 라이브러리를 사용하는 경우가 많습니다.)

'Android' 카테고리의 다른 글

@JvmOverloads  (0) 2020.12.15
[Android] 키패드에 따른 화면변경 설정하기  (0) 2020.04.20
[Android] Gradle  (0) 2020.04.10
[Android] ContentProvider, Service, BroadcastReceiver  (0) 2020.04.03
[Android] Activity, View, Layout  (0) 2020.04.03

MVP

MVP(Model View Presenter)는 사용자 인터페이스를 구축할 때 이용하는 설계 기법이다.

Model

데이터와 비즈니스 로직이 들어있고, UI에 관한 로직은 가지지 않는다.

데이터베이스나 API 접근에 관한 처리는 여기에 들어간다.

Presenter

모델과 뷰 사이에서 서로 통신한다.

뷰에서 발생한 이벤트가 프레젠터에 알려지면 프레젠터는 그 이벤트에 대응하는 처리.

뷰와 모델 사이에는 항상 프레젠터가 들어간다. 모델이나 뷰의 실체인 인스턴스를 프레젠터로부터 직접 참조하게하지 않고, 인터페이스 등을 이용해 접근할 수 있게 한다.

이렇게 하면 테스트 시에 목 객체(Mock Object)로 대체할 수 있어 테스트하기가 쉬워진다.

MVP 설계의 장점

Model, View, Presenter로 역할을 명확히 나누므로 어느 처리 내용이 어디에 있는지 명확해지고 코드의 관리 효율이 높아진다. MVP패턴으로 설계하면 필연적으로 역할을 나눠야 하기에 액티비티에 구현을 채워넣을 수 없게 된다. 

결과적으로 처리를 나눌 수 있어 액티비티를 작게 만들 수 있다. 또한 뷰와 모델 사이에 프레젠터가 들어가므로 뷰와 모델의 의존관계가 없어진다.

MVP 설계의 단점

프레젠터는 인터페이스를 통해 뷰와 모델에 접근하므로 그것들의 위치를 인터페이스로서 정의할 필요가 있다.

이 부분이 길어지기 쉽다. 또한 모델에서 가져온 데이터를 뷰에 표시하는 것을 개발자가 직접 구현해야 한다.

안드로이드에는 기본적으로 MVP 패턴을 지원하는 프레임워크가 없어서 어떻게 UI 로직을 프레젠터로 분리하는가 하는 설계상의 난도가 높다는 것도 단점으로 들 수 있다.


MVVM

Android Gradle Plugin을 통해 데이터 바인딩(Databinding)이 지원된다.

Databinding은 사용자 인터페이스와 데이터를 연결하는(바인딩하는) 메커니즘이다.

데이터 바인딩을 활용한 설계 기법으로 MVVM(Model View ViewModel)이 있다.

 

모델에는 MVP의 모델처럼 데이터와 비즈니스 로직이 들어간다.

 

뷰는 데이터를 표시한다. MVP와 달리 ViewModel이 모델에서 가져온 데이터를 반영해서 표시한다.

ViewModel 가진 값이 데이터 바인딩으로 자동적으로 뷰에 반영되므로 뷰 부분에서 반영하는 구현을 할 필요가 없어진다. 하지만 안드로이드에서는 애니메이션이나 액티비티 전환 등 ViewModel에서 구현하기 어려운 항목이 있다. 그런 부분은 뷰에서 구현할 필요가 있다.

 

기본적으로 ViewModel은 뷰의 상태와 UI에 관한 로직을 구현하고, 데이터 바인딩을 통해 ViewModel의 상태가 뷰에 반영된다. 또한 뷰 클릭 등의 이벤트를 ViewModel이 받고 모델과 데이터를 주고받아 데이터 바인딩으로 뷰의 상태를 갱신한다.

MVVM 설계의 장점

MVP 패턴처럼 역할을 분리할 수 있으므로 액티비티를 작게 만들 수 있다.

데이터 바인딩으로 MVP일 때 기술하는 모델에서 가져온 데이터를 뷰에 반영하는 로직도 작성할 필요가 없으므로 액티비티의 코드를 많이 줄일 수 있다. 프레젠터와 마찬가지로 뷰에 의존하는 코드가 없어 테스트하기 쉽다.

MVVM 설계의 단점

바인딩에 대한 처리는 자동으로 생성되므로 데이터 바인딩 처리는 블랙박스화돼 있다. 자동으로 생성된 코드는 일반적으로 가독성이 낮고 디버그하기 어렵다.

black은 컴포넌트 안을 보거나 변경할 수 없으므로 고안된 대로 재사용이 가능하다.
고안된 대로 쓸 수 있기 때문에 일관된 프로그래밍이 가능하다.

 

Preference

안드로이드 개발 레벨업 교과서

ContentProvider

  • ContentProvider는 앱 사이에서 각종 데이터를 공유할 수 있게 해주는 컴포넌트이다.
  • 안드로이드 표준 시스템에서는 연락처인 Contacts나 이미지나 동영상 등의 데이터를 보관하는 MediaStore등이 ContentProvider로 공개돼 있다. 
  • 데이터를 검색, 추가, 갱신, 삭제할 수 있으며, 주로 SQLite 등의 관계형 데이터베이스 이용을 염두에 두고 설계됐습니다.
  • ContentProvider가 제공하는 데이터는 ContentResolver를 통해 접근하도록 설계돼있고, ContentProvider자신에 대한 참조는 필요없다.
Cursor는 데이터에 접근하는 포인터
ContentProvider에서 데이터를 가져오는 흐름
1. ContentResolver 구하기
2. Cursor 구하기
3. Cursor에서 데이터 가져오기
4. Cursor해제

BroadcastReceiver

  • 어떤 이벤트가 발생한 사실을 앱에 알리고 싶을 때 BroadcastReceiver에 통지한다.
  • 단말기 전원이 들어왔거나 디스크 용량 부족 등 시스템의 이벤트를 앱에 알리거나, 앱간의 연계를 위해 이벤트를 알리고 싶을 때 이용한다.
  • 브로드캐스트 Intent를 받았을 때의 처리를 onReceive에서 구현한다
    어느 브로드캐스트 Intent를 받을지는 IntentFilter로 정의한다.

LocalBroadcastReceiver

브로드캐스트는 다른 앱에 송신하는 것이 가능하지만 경우에 따라서는 다른 앱에 알릴 필요없이 앱내에서 완결시키고 싶을 때 LocalBroadcast로 다른 앱에 알리지 않고 끝낼 수 있다. LocalBroadcast를 수신하려면 LocalBroadcastReceiver를 이용한다.

 

Broadcast를 수신해 처리할 때 주의할 점

안드로이드는 전력 소비를 줄이고자 사용자가 화면을 끄면 슬립 상태로 들어간다.

브로드캐스트를 수신해서 뭔가 시간이 오래 걸리는 처리를 하는 중에 슬립되는 경우가 있다.

일반적으로 화면이 꺼지고 CPU가 동작하지 않는 상태를 슬립 상태라고 한다.

이런 슬립 상태에서도 브로드캐스트를 받을 수 있지만 받은 후에 곧 바로 슬립하므로 시간이 걸리는 처리는 취소되어 계속할 수 없다.

 

처리를 계속하려면 CPU를 깨울 필요가 있다. 이런 동작을 안드로이드 시스템 세계에서는 WakeLock을 얻는다고 한다. Wake '깨운다', Lock '열쇠로 잠근다' 는 의미인데, 잠들 수 없는 방에 CPU를 넣고 열쇠로 잠가두는 것을 상상하면 쉽다. 반대로 처리가 끝나면 WakeLock을 해제한다.

 

주의할 점은 WakeLock 해제를 잊으면 CPU가 슬립할 수 없어 계속 동작하고 전력을 낭비하게 된다. 이는 앱의 신뢰성과도 연관되니 주의해야한다. 

 

다만 다행히 지원라이브러리에 WakefulBroadcastReceiver라는 클래스가 있어 이 클래스를 이용하면 처리 중에만 WakeLock을 얻고, 처리가 끝나면 해제하는 일련의 흐름을 실행해준다.
WakefulBroadcastReceiver로 얻은 WakeLock은 60초로 타임아웃이 설정돼 있다. 60초가 넘으면 슬립 상태로 전환되니 주의할 필요가 있다.


Service

  • 백그라운드 처리를 위해 준비된 컴포넌트이다.
  • UI없이 백그라운드로 처리할 수 있다.

Service의 3종류

  1. Context.startService()를 호출해 시작되는 서비스
  2. Context.bindService()를 호출해서 Service에 바인드하는 종류
  3. AIDL (Android Interface Definition Language: 안드로이드 인터페이스 정의 언어)을 이용하는 서비스
AIDL로 앱끼리 연결할 때는 각각 Service로 실행되는 스레드가 다르다는 점에 주의할 필요가 있다.
단순히 앱을 연계하고 싶을 때는 startActivityForResult()를 이용하는 액티비티 연계로 대신할 수 없는지 검토해 보는 것이 좋다.

Service의 수명주기와 콜백

onCreate Service가 생성된 뒤에 콜백된다.
onStartCommand Service가 시작된 뒤에 콜백된다.
onBind

Context.bindService()를 통해 이 Service가 바인드되는 경우에 호출된다.

또한 바인드 후, 서비스에 접속할 때는 ServiceCOnnection.onServiceConnected가 콜백된다.

onRebind

이 Service가 언바인드된 다음 다시 접속할 때 콜백된다.

onUnbind

이 Service가 언바인드될 때 콜백된다. 

onDestroy

Service가 페기되기 직전에 콜백된다.

Service가 폐기되는 타이밍

바인드된 경우는 바인드한 모든 클라이언트로부터 언바인드됐을 때 폐기된다.

 

Service가 바인드되지 않은 채 startService로 시작된 경우에는 명시적으로 Service.stopSelf()로 Service 자신이 스스로 종료하거나 다른 컴포넌트에서 Context.stopService()를 호출해 Service를 종료했을 때 폐기된다.

 

마지막으로, Service가 바인드되고 startService로 시작된 경우는 모든 클라이언트로부터 언바인드되고 또한 명시적으로 Service.stopSelf()로 Service자신이 스스로 종료하거나 다른 컴포넌트에서 Context.stopService()를 호출해 Service를 종료했을 때 폐기된다.

 

Activity와 Fragment의 수명주기에 의존하지 않고 백그라운드에서 처리하고 싶은 경우 일반적으로 IntentService가 최적의 선택이 된다.

'Android' 카테고리의 다른 글

[Android] 테스트  (0) 2020.04.11
[Android] Gradle  (0) 2020.04.10
[Android] Activity, View, Layout  (0) 2020.04.03
[안드로이드] 갤러리 앱으로 연결 시키기  (0) 2020.03.08
[안드로이드] 갤러리 구현하기  (0) 2020.02.21

Activity : 액티비티라는 단어의 의미는 '활동'

사용자가 어떤 활동을 할 때 실행되는 애플리케이션의 컴포넌트를 가리킨다. 액티비티에는 윈도우가 있고, 그 윈도우에 텍스트나 이미지를 표시해 사용자 조작에 반응할 수 있다. UI가 없는 액티비티도 있지만 기본적으로 한 액티비티가 한 화면을 표시한다.

 

AppCompatActivity를 상속하는 Activity를 만들면 Meterial Design의 가이드라인에 따른 AppCompat 라이브러리를 제대로 활용할 수 있다.

AppCompatActivity를 상속할 수 없을 때는 AppCompatDelegate를 이용할 수 있다.

 

Activity의 수명주기

onCreate() 생성 초기화 처리와 뷰 생성(setContentView 호출) 등
onStart() 비표시 통신이나 센서 처리를 시작
onResume() 최전면 표시(맨 앞쪽) 필요한 애니메이션 실행 등의 화면 갱신 처리
onPause() 일부 표시(일시정지) 상태 애니메이션 등 화면 갱신 처리를 정지 또는 일시정지할 때 필요 없는 리소스를 해제하거나 필요한 데이터를 영속화
onStop() 비표시 통신이나 센서 처리를 정지
onDestroy() 폐기 필요 없는 리소스를 해제, 액티비티 참조는 모두 정리
※ Android N부터 멀티윈도우가 도입됐습니다.
   멀티윈도우를 지원하는 경우 애니메이션 실행 등 화면 갱신 처리의 정지는 onStop()에서 한다.

시스템 메모리가 모자랄 경우 시스템은 onStop, onDestroy를 콜백하지 않고 액티비티를 강제 종료시켜 메모리를 확보할 때가 있다. 이런 경우 데이터를 영속적으로 보존하려면 액티비티가 일시정지 상태로 전환되는 onPause에서 이를 처리할 필요가 있다.

 

onCreate와 onDestroy / onStart와 onStop / onResume과 onPause 를 쌍으로 준비와 뒷정리 또는 시작과 종료(취소)하는 조합을 생각하면 어떤 시점에 어떤 작업을 처리할지 상상하기 쉬워진다.

 

onCreate에서 뷰를 만들면 onDestroy에서 해제한다.

뷰는 액티비티가 폐기된 다음, 가비지 콜렉션(GC: Garbage Collection)에 의해 자동으로 메모리에서 해제된다.

 

onStart에서 위치 정보를 취득했다면 onStop에서 취득을 정지하는 식이다.

 

onDestroy에서 액티비티가 폐기되면 GC가 메모리 영역에서 해제한다. 

단, 액티비티의 인스턴스가 다른 클래스에서 참조되고 있을 때는 폐기된 후에도 메모리에 남아 결국 메모리 누수가 발생한다.

 

Activity의 백스택

새로운 액티비티가 시작되면 실행중이던 Activity는 백스택에 들어간다. 또한 시작한 Activity는 태스크라는 그룹에 속한다. 안드로이드 OS의 버전에 따라서도 미묘하게 동작이 달라 다 이해하기는 어려우므로 3가지만 알아두자.

  • 같은 앱에서 시작된 액티비티는 같은 백스택에 쌓인다.
  • taskAffinity의 속성에 따라 소속되는 태스크가 달라진다.
  • launchMode에 따라 Activity 생성의 여부, 새로운 태스크에 속하는 등 Activity의 시작이 달라진다.

백스택에 쌓인 액티비티는 '뒤로가기' 키 등으로 액티비티를 종료하면 차례로 꺼내진다.

 

taskAffinity는 '태스크 친화성' 이라는 의미지만, 대체로 '태스크 이름'으로 바꿔 읽는것이 이해하기 쉽다.

taskAffinity를 지정하지 않은 경우는 자기 앱의 패키지 이름이 태스크 이름이 딘다.

taskAffinity를 설정하지 않으면 그 앱의 taskAffinity는 모두 같아진다.

 

launchMode는 4가지가 있다.  자주 사용하는 것은 standard, singleTop, singleTask의 3가지이다.

standard는 매번 액티비티의 인스턴스를 새로 생성한다.

singleTop은 같은 액티비티가 최상위에서 실행 중이면 액티비티를 생성하지 않고, 그 대신 최상위 인스턴스의 onNewIntent()를 호출한다.

singleTask는 1개의 태스크에 인스턴스가 존재한다. 이미 같은 액티비티가 실행 중이면 액티비티를 생성하지 않는다.


View, Layout

View란 UI를 구성하는 바탕이 되는 컴포넌트로서 네모난 그리기 영역을 가진다.

Layout은 뷰를 어떤 위치에 어떤 크기로 표시할지 결정하는 것입니다.

dialog.show() 시 발생한 에러

android.view.WindowManager$BadTokenException: Unable to add window — token null is not for an application”

val builder: AlertDialog.Builder = AlertDialog.Builder(applicationContext)
builder.setMessage("message")
	.setTitle("title")
	.setNegativeButton(R.string.cancel, DialogInterface.OnClickListener{ dialog, id ->
		...
	})
    .setPositiveButton(R.string.ok, DialogInterface.OnClickListener{ dialog, id ->
	    ...
    })

val dialog:AlertDialog = builder.create()
dialog.show()

1행에서 AlertDialog.Builder(applicationContext)를 AlertDialog.Builder(this@MainActivity)로 수정하여 해결할 수 있습니다.

Android 6.0 이상에서는 앱에 권한이 필요함을 선언하려면 해당 권한을 Manifest에 나열하고 런타임에 사용자가 권한을 승인하도록 해야합니다.  Android 지원 라이브러리를 사용하여 권한을 확인하고 요청하는 방법을 알아보겠습니다.

 

Android 프레임워크에서는 Android 6.0(API level 23)과 유사한 메서드를 제공하지만 지원라이브러리르 사용하여 더욱 쉽게 이전 Android 버전과 호환성을 지원할 수 있습니다.

 

manifest에 권한 추가

Android에서는 버전에 상관없이 앱에 권한이 필요함을 선언하려면 manifest에 <uses-permission> 요소를 최상위 <manifest>의 하위요소로 지정합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hyun.sqlite">

    <uses-permission android:name="android.permission.CAMERA"/>

    <application
    	...
    </application>

</manifest>

권한 선언 후 해당 권한의 민감한 정도에 따라 시스템 동작은 달라집니다. 어떤 권한은 '정상'권한으로 간주되어 시스템이 설치 시에 곧바로 부여하지만, 어떤 권한은 '위험'권한으로 간주되어 사용자가 명시적으로 엑세스 권한을 부여해야 합니다. 권한에 관한 자세한 정보는 보호수준에서 확인할 수 있습니다.

 

권한 확인

앱에 위험 권한이 필요한 경우 해당 권한이 요구되는 작업을 할 때마다 권한이 있는지 확인해야합니다.

Android 6.0(API level 23)부터는 앱이 더 낮은 API 레벨을 타겟팅하더라도 사용자가 언제든 앱의 권한을 취소할 수 있습니다. 따라서 어제 카메라를 사용했다고 해서 오늘도 권한이 있다고 가정할 수는 없습니다.

 

권한이 있는지 확인하려면 ContextCompat.checkSelfPermission()메서드를 호출합니다.

// 권한 체크하기
if(ContextCompat.checkSelfPermission(this@WelcomeActivity, Manifest.permission.CAMERA) ==
   PackageManager.PERMISSION_GRANTED) {
	// 권한이 있을 때 카메라 실행하기
	Toast.makeText(this, "카메라 허가 가능, 프리뷰 시작", Toast.LENGTH_SHORT).show()
	startCamera()
} else {
	// 권한이 없을 때 권한 요청하기
	requestCameraPermission()
}

앱에 권한이 있는 경우 이 메서드는 PERMISSION_GRANTED를 반환하고, 앱은 작업을 계속 진행할 수 있습니다. 앱에 권한이 없는 경우 이 메서드는 PERMISSION_DENIED를 반환하고, 앱은 사용자에게 명시적으로 권한을 요청해야합니다.

 

권한 요청

앱이 checkSelfPermission()에서 PERMISSION_DENIED를 수신하면 사용자에게 해당 권한을 요청하는 메시지를 표시해야합니다. Android는 requestPermissions()과 같이 권한을 요청하는데 사용할 수 있는 여러 메서드를 제공합니다. 이러한 메서드를 호출하면 표준 Android 대화상자가 나타나며, 이 상자는 맞춤설정할 수 없습니다.

 

사용자에게 표시되는 방식은 기기 Android버전과 애플리케이션의 대상 버전에 따라 다릅니다. 자세한 내용은 권한 개요에 설명됩니다.

 

앱에 권한이 필요한 이유 설명

사용자가 사진앱을 실행하는 경우 사용자에게 카메라 사용 권한을 요청해도 놀라지 않을 것입니다. 그러나 사용자 위치나 연락처에 접근하려고 하면 사용자가 이유를 이해하지 못할 수도 있습니다. 앱에서 권한을 요청하기 전에 먼저 사용자에게 이유를 설명하는 것을 고려해야합니다. 명심할 점은, 설명이 사용자에게 부담이 되어서는 안됩니다. 너무 많은 설명을 제공할 경우 사용자가 짜증을 느끼고 앱을 제거할 수도 있습니다.

 

권한 설명에서 사용할 수 있는 한 가지 방법은 사용자가 해당 권한 요청을 이미 거절한 경우에만 설명을 제공하는 것입니다. Android에서는 이를 위해 유틸리티 메서드 shouldShowRequestPermissionRationable()를 제공합니다. 이 메서드는 사용자가 전에 해당 요청을 거부한 경우에는 true를 반환하고, 사용자가 해당 권한을 거부했으며 '다시 묻지 않음' 옵션을 선택했거나 기기 정책에서 해당 권한을 금지하는 경우에는 false를 반환합니다.

 

사용자가 권한이 요구되는 기능을 계속 사용하려고 시도하면서도 권한 요청을 계속 거절한다면 아마도 이 사용자는 해당 기능을 제공하기 위해 앱에 권한이 필요한 이유를 모를 수도 있습니다. 이런 상황에서는 설명을 하는 것이 좋을 수 있습니다. 권한을 요청할 때 좋은 사용자 환경을 만드는 방법에 관한 권장사항을 확인할 수 있습니다.

 

여러분에게 필요한 권한 요청

앱에 필요한 권한이 아직 없는 경우 앱은 requestPermissions() 메서드 중 하나를 호출하여 적절한 권한을 요청해야 합니다. 앱은 원하는 권한 및 이 권한 요청을 식별하기 위해 지정된 정수 요청 코드를 전달합니다. 이 메서드는 비동기식으로 작동합니다. 즉각적으로 반환되며, 사용자가 메시지에 응답하면 시스템은 그 결과를 가지고 앱의 콜백 메서드를 호출하여 앱이 requestPermissions()에 전달한 것과 동일한 요청 코드를 전달합니다.

 

아래의 예는 카메라 권한을 요청하는 코드입니다.

시나리오

  • 카메라 접근 권한이 있는지 확인

  • 권한이 없으면 권한이 필요한 이유를 표시해야하는지 확인

  • 권한 요청

private fun requestCameraPermission() {

	if (ActivityCompat.shouldShowRequestPermissionRationale(this@WelcomeActivity, Manifest.permission.CAMERA)) {
        Toast.makeText(this@WelcomeActivity, "카메라 권한이 요구됩니다", Toast.LENGTH_SHORT).show()
        ActivityCompat.requestPermissions(this@WelcomeActivity, arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CAMERA)
    } else {
        // 다시 묻지 않음을 누르고 거부하고 요청했을 때
        Toast.makeText(this@WelcomeActivity, "카메라 허가를 받을 수 없습니다.", Toast.LENGTH_SHORT).show()
        ActivityCompat.requestPermissions(this@WelcomeActivity, arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CAMERA)
    }
}

참고 : requestPermissions()을 호출하면 시스템은 표준 대화상자를 사용자에게 표시합니다. 이 대화상자를 구성하거나 변경할 수 없습니다. 앱에 권한이 필요한 이유에 나오는 것처럼, 사용자에게 정보나 설명을 제공해야하는 겨우 requestPermissions()를 호출하기 전에 제공하는 것이 좋습니다.

권한 요청 응답 처리

사용자가 앱 권한 요청에 응답하면 시스템은 앱의 onRequestPermissionResult() 메서드를 호출하여 사용자 응답을 전달합니다. 권한이 부여되었는지 확인하려면 앱은 해당 메서드를 재정의해야 합니다. 이 콜백에는 requestPermissions()에 전달한 것과 동일한 요청 코드가 전달됩니다.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {

    if(requestCode == PERMISSION_REQUEST_CAMERA) {
        if(grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "카메라의 허가를 받았다", Toast.LENGTH_SHORT).show()
            startCamera()
        }
    } else {
        Toast.makeText(this, "카메라 요청이 거부되었다", Toast.LENGTH_SHORT).show()
    }
}

시스템이 표시하는 대화상자에서는 앱이 액세스해야 하는 권한 그룹을 설명합니다. 특정 권한은 나열하지 않습니다. 

 

예를 들어 READ_CONTACTS 권한을 요청하는 경우 기기의 연락처에 액세스해야 한다는 메시지만 시스템 대화상자에 나타납니다. 사용자는 각 권한 그룹에 관해 한번만 권한을 부여해야 합니다.

 

앱이 해당 그룹에 있는 다른 권한(앱 manifest에 나열된 다른 권한)을 요청하는 경우 시스템이 자동으로 권한을 부여합니다. 여러분이 권한을 요청하면 시스템은 사용자가 시스템 대화상자를 통해 명시적으로 요청을 승인했을 때와 동일한 방식으로 onRequestPermissionsResult() 콜백 메서드를 호출하고 PERMISSION_GRANTED를 전달합니다.

 

참고 : 사용자가 이미 동일한 그룹에 있는 다른 권한을 부여한 경우라도 앱이 필요한 모든 권한을 명시적으로 요청해야 합니다. 향후 Android 리리스에서는 권한 그룹화도 변경될 수 있습니다. 코드에서는 특정 권한이 동일한 그룹에 있다고 가정하거나 없다고 가정해서는 안됩니다.

 

예를 들어 manifest에 READ_CONTACTS 및 WRITE_CONTACTS를 둘 다 나열한다고 가정합니다. READ_CONTACTS를 요청하여 사용자가 권한을 부여한 후 WRITE_CONTACTS를 요청하면 시스템이 사용자와 상호작용 없이 곧바로 해당 권한을 부여합니다.

 

사용자가 권한 요청을 거부하는 경우 앱은 적절한 작업을 수행해야 합니다. 예를 들어 해당 권한이 필요한 작업을 사용자가 요청하면, 수행할 수 없는 이유를 설명하는 대화상자를 표시할 수 있습니다.

 

시스템에서 사용자에게 권한을 부여하도록 요청하면 사용자는 해당 권한을 다시 요청하지 말도록 시스템에 지시할 수 있습니다. 이 경우 앱이 해당 권한을 다시 요청하기 위해 requestPermissions()를 사용할 때 마다 시스템은 즉시 해당 요청을 거부합니다. 시스템은 사용자가 명시적으로 요청을 다시 거부했을 때와 동일한 방식으로 onRequestPermissionResult() 콜백 메서드를 호출하고 PERMISSION_DENIED를 전달합니다.

 

앱이 해당 권한을 가지지 못하도록 기기 정책에서 금지하는 경우에도 이 메서드는 false를 반환합니다. 즉, requestPermissions()를 호출하는 경우 사용자와의 직접적 상호작용이 발생했다고 가정할 수 없습니다.

 

Reference

 

Android Runtime PermissionBasicSample을 참고하여 만들었습니다. 

package com.hyun.sqlite

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_welcome.*

const val PERMISSION_REQUEST_CAMERA = 0

class WelcomeActivity : AppCompatActivity() {

    val TAG: String = ".WelcomeActivityAKDJKA"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_welcome)

        btn_camera.setOnClickListener {
            showCameraPreview()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<out String>, grantResults: IntArray
    ) {
        Log.d(TAG, "onRequestPermissionsResult")

        if (requestCode == PERMISSION_REQUEST_CAMERA) {
            if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "카메라의 허가를 받았다. 프리뷰 시작", Toast.LENGTH_SHORT).show()
                startCamera()
            }
        } else {
            Toast.makeText(this, "카메라 요청이 거부되었다", Toast.LENGTH_SHORT).show()
        }
    }

    // 1
    fun showCameraPreview() {
        Log.d(TAG, "showCameraPreview")

        // 권한 체크하기
        if (ContextCompat.checkSelfPermission(this@WelcomeActivity, Manifest.permission.CAMERA) ==
            PackageManager.PERMISSION_GRANTED
        ) {
            // 권한이 있을 때 카메라 실행하기
            Toast.makeText(this, "카메라 허가 가능, 프리뷰 시작", Toast.LENGTH_SHORT).show()
            startCamera()
        } else {
            // 권한이 없을 때 권한 요청하기
            requestCameraPermission()
        }
    }


    private fun requestCameraPermission() {
        Log.d(TAG, "requestCameraPermission()")

        if (ActivityCompat.shouldShowRequestPermissionRationale(
                this@WelcomeActivity,
                Manifest.permission.CAMERA
            )
        ) {
            Toast.makeText(this@WelcomeActivity, "카메라 권한이 요구됩니다", Toast.LENGTH_SHORT).show()
            ActivityCompat.requestPermissions(
                this@WelcomeActivity,
                arrayOf(Manifest.permission.CAMERA),
                PERMISSION_REQUEST_CAMERA
            )
        } else {
            // 다시 묻지 않음을 누르고 거부하고 요청했을 때
            Toast.makeText(this@WelcomeActivity, "카메라 허가를 받을 수 없습니다.", Toast.LENGTH_SHORT).show()
            ActivityCompat.requestPermissions(
                this@WelcomeActivity,
                arrayOf(Manifest.permission.CAMERA),
                PERMISSION_REQUEST_CAMERA
            )
        }
    }

    private fun startCamera() {
        Log.d(TAG, "startCamera()")
        Intent(this@WelcomeActivity, CameraPreviewActivity::class.java).let {
            startActivity(it)
        }
    }
}

'Android' 카테고리의 다른 글

[Kotlin] 안드로이드 권한 - 1  (0) 2019.09.30
[Design] Material Design  (0) 2019.08.18
[Kotlin] Room Library  (0) 2019.08.17
Jetpack, AndroidX  (0) 2019.07.30
Singleton, MVC, MVP, MVVM 한 눈에 보기  (0) 2019.07.18

+ Recent posts