안드로이드1

timothy jeong·2021년 10월 25일
0

Android with Kotlin

목록 보기
1/69

Android

Activity 는 유저가 할 수 있는 한가지의 집중된 것을 의미한다.
전형적인 Activity는 UI 요소가 어떻게 스크린에 나타날지 정의한 layout 과 관계를 갖는다.

[프로젝트 구조]

Kotlin 프로젝트임에도 app/java 디렉터리 안에 main 이 있는 이유는, 이 규약을 지킴으로써 Java 로 작성된 프로젝트와도 호환될 수 있도록 하기 위함이다.

java(generated) 는 빌드한 결과가 들어있는 폴더로, 건드리지 않는게 좋다. Debug 할때나 뜯어본다.

Res 디렉터리는 리소스들을 보관하는 곳이다.
이때 말하는 리소스는 static content 로 이미지, 텍스트, 스크린 레이아웃, 스타일, 색상 hex 코드 등을 의미한다.

안드로이드 앱은 코틀린 코드와 리소스들이 분리되어 지도록 설계되는것을 권장한다.

하나의 activity 는 res/layout 에 위치한 ui layout file *.xml 파일 하나에 대응된다.

App/manifests 디렉터리에 있는 파일은 안드로이드 시스템이 코딩된 앱을 실행시키기 위해 필요한 자세한 정보들을 가지고 있다.
Activity 를 정의하는 부분도 갖고 있다.

또한 Permission 에 관한 정보도 갖고 있다.

[Examine MainActivity]

많은 프로그래밍 언어들이 main() 함수를 프로그래밍의 시작점으로 삼지만, 안드로이드는 프로그래밍에서는 그 역할을 AndroidManifest.xml 에서 지정된 Activity 가 맡게된다. 기본적으로는 MainActivity 가 그 역할을 맡도록 설정되어 있다.

각각의 액티비티들은 그에 대응되는 레이아웃 파일을 갖고있다. 액티비티와 레이아웃은 layout inflation 이라는 프로세스에 의해서 연결된다.

액티비티가 시작되면 xml 파일로 정의된 레이아웃 파일은 turn into (inflated into) Kotlin view object in memory. 레이아웃 파일이 코틀린 뷰 오브젝트로 변환되고 나면 , 액티비티는 이러한 오브젝트를 스크린에 그릴 수 있고 , 동적으로 조정할 수 있다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Bundle 이 친구는 java class 이다.

R class
프로젝트 뷰에서 /app/build/generated/source/r 디렉터리에 있다.
databinding 을 이용하면 안생기는것 같다.

MainActivity 는 AppCompatActivity 를 extends 하고 있다.
AppCompatActivity 는 Activity 의 서브클래스로 현대적인 안드로이드의 기능을 지원하는 동시에 이전 버전의 안드로이드와의 호환성을 지원하는 클래스이다.

MainActivity 는 Constructor 를 사용하지 않고, OnCreate 메서드만 정의하고 있다. OnCreate 메서드는 LifeCycle 메서드이다.

setContentView() 메서드가 액티비티와 연관시킬 레이아웃을 지정한다. 이 메서드는 R.layout.activity_main 를 통해서 레아이웃을 지정하고 있다. 사실 이 레퍼런스 값은 인티저 값을 의미한다.

R 클래스는 앱을 빌드할때 생성되는데, R 클래스는 Res 디렉터리에 있는 콘텐츠를 포함한 앱의 모든 에셋들을 포함하고 있다.

R.layout.activity_main 에서 참조되는 값은, res/layout/activiry_main.xml 파일이다. (리소스는 파일 확장자를 명시하지 않는다.) R 클래스를 이용하여 res 에 있는 콘텐츠, 이미지, 스트링 등에 접근할 수 있다.

[Examine and explore the app layout file]

모든 액티비티들은 하나의 연관된 레이아웃을 (res/layout) 을 갖는다. 레이아웃 파일은 XML 파일인데, 액티비티가 어떻게 보일지를 알려준다. 레이아웃 파일은 view 와 view 가 스크린에 나타날지를 정의하면서 액티비티가 어떻게 보일지 개발자에게 알려준다.

View 는 텍스트, 이미지, 버튼과 같이 View 클래스틀 extends 한 것들을 의미한다. View 는 다양한 것들이 있다.
*.xml 레이아웃 파일의 xml 코드를 직접 수정함으로써 레이아웃을 변경할 수 있다.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout   
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

</LinearLayout>

이렇게 변경했다고 해보자,
Root element 는 이다. LinearLayout 는 ViewGroup 중 하나인데, ViewGroup 은 다른 뷰를 담을 수 있는 콘테이너 역할과 스크린에서 뷰의 위치를 지정하는 역할을 수행한다. 또한 ViewGroup 은 다른 ViewGroup 들을 담을 수 있다.

레이아웃.xml 파일에 있는 모든 View 들은 위계질서를 갖도록 조직화 된다. 조직화 되는 방식은 ViewGroup 에 따라 다른데, 예시처럼 LinearLayout 라면, child view 를 선형적으로 조직화한다. 앱이 구동되면, 레이아웃 파이렝 있는 xml 의 위계질서는 객체들의 위계질서로 변환된다(inflated).

LinearLayout 이 android:layout_width="match_parent"
속성을 갖고 있는데, Root ViewGroup 이므로 이의 Parent 는 Screen 그 자체이다. 따라서 Screen 사이즈 만큼의 넓이를 갖는 다는 것을 의미한다.

android:layout_height="wrap_content" 는 ViewGroup 이 포함하고 있는 View 혹은 ViewGroup 의 높이만큼 height 를 지정한다는 것을 의미한다.

[View Deep Dive]

[referring resource]

Text 에 사용되는 String 을 하드코딩 하는대신 별도의 파일에서 관리하는 것이 추천된다. Res/values 디렉터리에 string.xml 파일이 있다. 이 파일 구조는 직관적인데.

<resources>
    <string name="app_name">DiceRoller</string>
    <string name="roll_label">Roll</string>
</resources>

딱 봐도 어떻게 써야하는지 명확하다.
이렇게 지정된 string 들은 다음과 같이 참조한다.
단축키는 Mac : option + enter 이다.

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/roll_label" />

[style and position views]

ViewGroup 에서 자식 view 들을 stack 하는 방법을 지정할 수 있다.

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

LinearLayout 에 android:orientation="vertical" 를 추가함으로써 하위 view 들이 수직적으로 쌓이도록 하였다.

하위 view 들은 layout gravity 속성을 통해 세부 위치를 지정해준다. layout_gravity 는 자신을 포함하고 있는 부모 위젯 레이아웃에서 옵션값에 따라 정렬하는 것을 의미한다.


<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    android:layout_gravity="center_horizontal"
    android:textColor="@color/teal_200"/>

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="@string/roll_label" />

[ Get a reference to the button in code]

Kotlin code 를 통해 버튼을 참조할때, findViewById() 메서드를 이용할 것이다. 따라서 버튼에 아이디를 만들어줘야한다.

<Button
    android:id="@+id/roll_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:text="@string/roll_label" />

메인액티비티에서는 findViewById(R.id.roll_button) 를 통해서 해당하는 위젯(view 에 대응되는 개념) 을 가져온다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button =findViewById(R.id.roll_button)       
    }
}

버튼이 클릭될 때마다 메시지가 뜨게 만들어보자

Toast 는 잠깐 뜨는 팝업 형식의 메시지이다.
Toast 를 구현하기 위해서는 Context, Message, duration to show 를 지정해야한다.
Context 는 현재 안드로이드 os 의 상태에 대해 의사소통하고, 정보를 얻도록 하는 객체이다. AppCompatActivityContext 의 서브 클래스이므로 this 로 메인 액티비티 클래스를 참조시키면 된다.

rollButton.setOnClickListener 에 이제 만들어진 Toast 를 참조시킨다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button = findViewById(R.id.roll_button)
        rollButton.setOnClickListener { rollDice() }
    }

    private fun rollDice () {
        Toast.makeText(this, "button clicked", Toast.LENGTH_SHORT).show()
    }
}

[Change State]

위젯에서 값을 변경함으로써 레이아웃.xml 의 값을 변경할 수 있다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val rollButton: Button = findViewById(R.id.roll_button)
        val resultText: TextView = findViewById(R.id.result_text)
        resultText.text = "Dice Rolled!"
        rollButton.setOnClickListener { rollDice() }
    }
    private fun rollDice () {
        Toast.makeText(this, "button clicked", Toast.LENGTH_SHORT).show()
    }
}

val resultText: TextView = findViewById(R.id.result_text) 를 통해서 텍스트 view 를 위젯화 시키고, resultText.text = "Dice Rolled!" 를 통해서 텍스트 내용을 변경했다.

[Image]

[Add Image Res]

Res/drawable 는 이미지 리소스를 넣는 공간이다.
이미지 파일은 vector 이미지로 표현되는 xml 파일 형식이다. Vector 이미지를 이용하면 해당 이미지를 다양한 사이즈로 구현하기 쉽게 한다. Bitmap 이미지 (PNG, GIF) 는 디바이스에 따라서 스케일을 다르게 설정해줘야하는 번거로움이 있다.

이미지를 어떻게 벡터화 시키지?

[Update layout to use Images]

Layout 파일을 아래와 같이 변경해줍니다.
기존 TextView 를 삭제하고, ImageView 를 더해줍니다.

<androidx.constraintlayout.utils.widget.ImageFilterView
    android:id="@+id/dice_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:src="@drawable/dice_1"
    />

이에 맞춰서 kotlin 코드도 바꿔줍니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        val rollButton: Button = findViewById(R.id.roll_button)
        rollButton.setOnClickListener { rollDice() }
    }

    private fun rollDice() {

        val drawableResource = when ((1..6).random()) {
            1 -> R.drawable.dice_1
            2 -> R.drawable.dice_2
            3 -> R.drawable.dice_3
            4 -> R.drawable.dice_4
            5 -> R.drawable.dice_5
            else -> R.drawable.dice_6
        }
        val diceImage: ImageView = findViewById(R.id.dice_image)
        diceImage.setImageResource(drawableResource)
    }
}

[Find Resources]

안드로이는 위계적으로 res 를 찾기 때문에(layout.xml 에서 ) 버튼을 누를 때마다 findById 를 하는 것은 효율적이지 못하다.

액티비티 단에서 변수로 View 를 정의하고 계속 가지고 있으면 해결될 문제인데, 한가지 걸리는게 있다. 레이블의 view 는 onCreate 메서드로 inflate 되기 전까지는 메모리영역으로 접근이 불가능하다. 즉, 변수화 할 수 없다는 것이다.

이러한 문제 때문에 변수가 선언됨과 동시에 초기화 되는 것이 이상적임에도 불구하고, nullable 로 선언하여 null로 초기화하는 방법이 있다. 하지만, 이렇게 되면, 해당 view 에 접근하려고 할 때마다 null 을 체크해줘야하는 번거로움이 생긴다. 이러한 단점을 보완하기 위해 lateinit 키워드를 사용할 수 있다.

Latent 은 코틀린 컴파일러에게 해당 변수는 사용되기 전에 초기화 될것임을 약속한다. 따라서 매번 null 체크를 해줄 필요 없이, non-nullable 변수로 취급할 수 있다.

class MainActivity : AppCompatActivity() {
    lateinit var diceImage : ImageView
    lateinit var rollButton: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        diceImage = findViewById(R.id.dice_image)
        rollButton = findViewById(R.id.roll_button)
        rollButton.setOnClickListener { rollDice() }
    }

    private fun rollDice() {

        val drawableResource = when ((1..6).random()) {
            1 -> R.drawable.dice_1
            2 -> R.drawable.dice_2
            3 -> R.drawable.dice_3
            4 -> R.drawable.dice_4
            5 -> R.drawable.dice_5
            else -> R.drawable.dice_6
        }
        diceImage.setImageResource(drawableResource)
    }
}

[use default image]

ImageView 를 쓰고 layout.xml android:src=“@drawable/empty_dice” 를 설정해서 디폴트 이미지를 설정할 수 있다.

Xml 속성에서 android 로 시작하는게 아니라 tools 로 시작하도록 할 수 있는데, 이 tools 가 의미하는 바는 디자인 에디터나 프리뷰에서만 해당 콘텐츠를 적용한다는 것이다. 앱을 컴파일 하고 나면 tools 로 지정된 속성은 사라진다.

[Understand API levels and compatibility]

안드로이드 코딩의 대단한 점은 스마트폰을 타겟으로 코딩을 했음에도 TV, Watch 등에 호환이 된다는 것이다. 물론 더 완벽한 호환을 위해서는 API 레벨과 호환성 문제를 이해해야한다. 이번 파트에서는 API 레벨과 호환성 문제를 이해하기 위한 개념을 전개할 것이다.

[API Level]

각각의 API 레벨은 서로 다른 안드로이드 os 버전에 상응한다. 그리고 프로젝트를 만듦과 동시에 minimum API levle 을 설정하는데, 이는 제작하는 앱이 지원하는 가장 오래된 os 버전을 의미한다.

build.gradle (Module: app) 파일은 앱을 개발하는데 필요한 의존성과 파라미터들을 빌드하는 스크립트이다. 많은 경우에 앱 모듈이 유일한 모듈일 수 있다. 하지만 앱이 점점 복잡해져서 여러 파트로 나눠야한다면, 그리고 혹은 Watch 에 까지 지원한다면 같은 프로젝트 내에서 2개 이상의 모듈을 관리할 필요성이 생긴다.

build.gradle (Module: app) 의 상단부분, android 를 분석해보자.

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.demo"
        minSdk 21
        targetSdk 31
        versionCode 1
        versionName "0.0.1"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

compileSdk 31 는 Gradle 이 앱을 컴파일 하기 위해 사용해야하는 API 레벨을 지정한 것이다. 프로젝트 어플리케이션이 지원하는 최신 버전의 API 를 의미한다.

defaultConfig 섹션에서 targetSdk 31 는 앱을 테스트한 가장 최근 API 버전을 의미한다. 대다수의 경우 compileSdk 31 와 동일하다.

minSdk 21 이 값이 지금까지 언급된 세가지 중 가장 중요한 값인데, 어플리케이션이 지원할 가장 오래된 API 버전을 의미한다. 이 버전을 너무 낮게 설정하면 Android OS 의 새로운 기능을 놓칠 수 있다. 반대로 너무 높게 설정하면 새로운 device 에서만 돌아가는 앱을 만들 수 있다.

[Compatibility]

호환성 문제는 많은 안드로이드 개발자들이 겪는 문제였으나, 2018년 구글이 Android JetPack 을 발표하면서 많은 부분이 개선되었다. JetPack 은 이전 API 버전의 많은 클래스, 함수를 포함하고 있다.

MainActivity 클래스는 Activity 자체가 아니라 AppCompatActivity 를 expands 하고 있는데, AppCompatActivityandroidx.appcompat.app 에서 import 되고 있고, androidx 는 Android Jetpack libraries 의 name space 이다. 즉 AppCompatActivity 를 expands 하므로써 MainActivity 의 구버전 호환성이 어느정도 확보되는 것이다.

[Add compatibility for vector drawables]

Vector Image 가 Bitmap 이미지에 비해 갖는 이점으로는 유연한 확장성, 적은 용량이 있다. Vector Image 는 사실은 xml 파일로 Vector Drawable 이라고도 불린다. 이러한 Vector Image 는 API 21 이후로 지원되는 것으로 그 이전 버전까지의 호환을 고려한다면 호환성을 위한 별도의 작업이 필요하다.

만약 별도의 작업 없이 컴파일, 빌드 한다면 어떻게 될까? 일단은 이미지가 제대로 나오기는 한다. 이는 Gradle Build 할때 API 21 이전 버전에는 Vector file 을 PNG file 로 변환하여 사용하기 때문이다. 그렇다면 별 문제 없는게 아닐까?

아니다. 추가적으로 생성되는 PNG file 들은 앱의 용량을 증가시키기 때문이다. 불필요하게 용량을 많이 잡아먹는 앱이 유저들에게 선호될리 없다.

defaultConfig 파트에 vectorDrawables.useSupportLibrary = true 를 추가하자.

그리고 layout.xml 에 새로운 namespace 를 추가하자, 기존의 android, tools 에 더해서 app 이라는 namespace 를 추가.
http://schemas.android.com/apk/res-auto 는 기존 안드로이드 프레임워크가 아닌 사용자 정의 코드 또는 라이브러리에서 가져온 속석을 사용하기 위한 네임스페이스이다.

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
>

그 뒤 ImageView 에 android:src 파트를 app:srcCompat 로 바꾸면 vector Image 를 PNG 로 따로 생성하지 않아도 이미지가 생성된다.

profile
개발자

0개의 댓글