국가 수도 맞추기 앱 만들기 #01


 첫번째 시간에는 간단하게 레이아웃을 구성해보도록 하겠습니다. 그리고 지금은 당장 쓰이진 않지만 앱의 핵심 데이터인 국가, 수도 정보도 추가하도록 하겠습니다. 이번 시간 최종 목표는 아래와 같은 구성을 가지는 것입니다.



 기본 구성은 다음과 같이 질문, 국가, 4개의 선택지, 이전, 다음 버튼으로 구성됩니다.

2개의 텍스트뷰와 6개의 버튼으로 이루어진 간단한 레이아웃입니다.





activity_main.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:id="@+id/activity_quiz_all"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center"

    android:orientation="vertical"

    tools:context="kotlinapp.circus.com.kotlinapplication.MainActivity">


    <TextView

        android:id="@+id/question_text"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_gravity="center_horizontal"

        android:text="Question"

        android:textSize="@dimen/qustion_size"/>


    <TextView

        android:id="@+id/question_country_text"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_gravity="center_horizontal"

        android:paddingBottom="20dp"

        android:text="Country"

        android:textSize="@dimen/qustion_size"/>



    <LinearLayout

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:orientation="vertical">


        <LinearLayout

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:orientation="horizontal">


            <Button

                android:id="@+id/answer_one"

                android:layout_width="150dp"

                android:layout_height="wrap_content"

                android:textSize="17dp"/>


            <Button

                android:id="@+id/answer_two"

                android:layout_width="150dp"

                android:layout_height="wrap_content"

                android:textSize="17dp"/>

        </LinearLayout>


        <LinearLayout

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:orientation="horizontal">


            <Button

                android:id="@+id/answer_three"

                android:layout_width="150dp"

                android:layout_height="wrap_content"

                android:textSize="17dp"/>


            <Button

                android:id="@+id/answer_four"

                android:layout_width="150dp"

                android:layout_height="wrap_content"

                android:textSize="17dp"/>

        </LinearLayout>

    </LinearLayout>


    <LinearLayout

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:orientation="horizontal"

        android:paddingTop="20dp">


        <Button

            android:id="@+id/prev_button"

            android:layout_width="wrap_content"

            android:layout_height="50dp"

            android:layout_gravity="left"

            android:layout_marginRight="30dp"

            android:drawableLeft="@drawable/arrow_left"

            android:text="@string/prev_button"

            android:textSize="20dp"/>


        <Button

            android:id="@+id/next_button"

            android:layout_width="wrap_content"

            android:layout_height="50dp"

            android:layout_gravity="right"

            android:drawableRight="@drawable/arrow_right"

            android:text="@string/next_button"

            android:textSize="20dp"/>

    </LinearLayout>

</LinearLayout>



 위와 같이 레이아웃 생성 후 텍스트뷰에 질문과 국가 정보를 출력할 수 있도록 연결해보겠습니다.

먼저 텍스트뷰의 id를 코드에서 바로 사용하기 위해서 activity_main의 모든 위젯들을 import 해줍니다.


MainActivity.kt

 package kotlinapp.circus.com.kotlinapplication


import android.support.v7.app.AppCompatActivity

import android.os.Bundle

import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity() {


    private var mCurrentIndex: Int = 0


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)


        showQuestion()

    }


    private fun showQuestion() {

        var countryList = CountryList()

        question_text.setText("${mCurrentIndex + 1}" + ". " + resources.getString(R.string.question_title))

        question_country_text.setText("[" + resources.getString(countryList.mQuestions[mCurrentIndex].country) + "]")

    }

}


 그 후 텍스트뷰의 id를 곧바로 사용하여 setText를 통해 표시할 텍스트를 지정해줍니다.

 만약 showQuestion() 메서드를 자바로 구현했다면 텍스트뷰에 대한 변수를 선언해주고 해당 변수를 레이아웃의 위젯과 연결해주는 과정이 필요합니다. 


 위젯의 id를 바로 사용할 수 있는 코틀린에서는 이러한 과정이 없어 매우 편리합니다.


public class MainActivity extends AppCompatActivity {


    private TextView mQuestionTextView;

    private TextView mQuestionCountryTextView;

        ...

        ...


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        ...

        ...


        mQuestionTextView = (TextView) findViewById(R.id.question_text);

        mQuestionCountryTextView = (TextView) findViewById(R.id.question_country_text);


        showQuestion();

}

    private void showQuestion() {

        mQuestionTextView.setText(mCurrentIndex + 1 + ". " + getResources().getString(R.string.question_title));

        mQuestionCountryTextView.setText("[" + getResources().getString(mQuestions[mCurrentIndex].getCountry()) + "]");

    }

        ...

        ...

}


 그리고 국가와 수도 정보를 가진 Question 클래스를 살펴보면 코틀린의 간결함과 편리함을 다시 한 번 느낄 수 있습니다.


Question.java

public class Question {

    private int mCountry;

    private int mCapital;


    public Question(int country, int capital) {

        mCountry = country;

        mCapital = capital;

    }


    public int getCountry() {

        return mCountry;

    }


    public int getCapital() {

        return mCapital;

    }

}

 어려운 내용들은 아니지만 국가, 수도 정보를 위한 변수와 생성자, getter가 존재하여 코드가 많아 보입니다.


Question.kt

 class Question(val country: Int, val capital: Int)

 primary constructor(주생성자)에 필요한 변수가 존재하며 getter, setter는 생략가능한 코틀린의 특성으로 인해 아주 간결한 모습입니다.


CountryList 클래스는 국가, 수도 정보를 가지는 Question 객체를 담고 있는 array를 포함하고 있습니다.


여기까지의 코드는 GitHub에 올려두었습니다. 해당 repository에 지속적으로 commit 하여 앱을 완성해 나가도록 해보겠습니다.


https://github.com/MyStoryG/CapitalQuiz


추상 클래스 (Abstract Class)

 클래스와 일부 멤버가 abstract로 선언되면 추상 클래스입니다. abstract 멤버는 아시다시피 클래스 내에서 아무런 구현이 없습니다.

     abstract class Student {

        abstract fun learnKotlin() // 최소 하나 이상의 멤버가 abstract

        fun learnJava() {}

    }

 

 또한 non-abstract open 멤버를 abstract 멤버로 재정의(override) 할 수 있습니다. 

[Remind] 상속을 하려면 꼭 open을 선언해주어야 합니다. 왜냐하면 아무 선언이 없으면 코틀린 클래스는 기본적으로 Java의 final 클래스와 같기 때문입니다.


     open class Person {

        open fun needToSleep() {}

    }


    abstract class Student : Person() {

        override abstract fun needToSleep()

        abstract fun learnKotlin()

        fun learnJava() {}

    }


인터페이스 (Interface)

 코틀린의 인터페이스는 Java8과 매우 유사합니다. abstract method 뿐 만 아니라 method의 구현도 가능합니다.

 [Tip] 인터페이스라고 하면 모든 method가 abstract method인 것이라고 알고 있을 수도 있는데 Java8에서 default 키워드로 method 구현이 가능해졌습니다.

Java

public interface Person {

 void needToSleep(); // interface 내에서는 abstract로 유추

 default String getName() {

   return "Hong";

  }


코틀린

     interface Person {

        fun needToSleep()

        fun getName():String = "Hong"

    }


 interface에서 구현한 method도 override가 가능합니다.

    interface Person {

        fun needToSleep()

        fun getName():String = "Hong"

    }


    class Student : Person {

        override fun needToSleep() {


        }

        fun learnKotlin() {


        }

        override fun getName(): String {

            return "Tom"

        }

    } 


 또한 다음과 같이 하나 이상의 interface를 구현할 수 있습니다.

     interface Person {

        fun needToSleep()

        fun getName():String = "Hong"

    }


    interface Adult {

        fun needToHoliday()

        fun getName():String = "Choi"

    }


    class Student : Person, Adult {

        override fun needToHoliday() {

            

        }

        override fun needToSleep() {


        }

        fun learnKotlin() {


        }

        override fun getName(): String {

            return "Tom"

        }

    }





상속 (Inheritance)


 코틀린에서는 super class (이하 부모클래스) 선언하지 않은 클래스는 default super로 Any라는 부모클래스를 가집니다.

 class Computer // 암시적으로 Any를 상속받음


표준 라이브러리에도 잘 설명되어 있습니다.

 Any (출처)

open class Any

The root of the Kotlin class hierarchy. Every Kotlin class has Any as a superclass.

 이 설명만 보면 마치 Java의 java.lang.Object와 같아 보입니다. 하지만 Any는 Object와는 다르게 equals(), hashCode(), toString() 외 다른 멤버는 가지지 않습니다. Object는 clone(), notify() 등 다른 멤버가 더 있습니다. (Object 참고)


상속받는 방법 (부모클래스를 명시적으로 선언하기)


 상속을 받기 위해서는 아래와 같이 부모클래스를 명시적으로 선언해주면 됩니다.

 open class Person(var name: String)

 class Student(name: String) : Person(name) 


 그렇다면 다음과 같이 student 객체를 만들어서 name을 출력하면 어떤식으로 동작을 할까요?

 val student = Student("Kim")

 println("student name = ${student.name}") 

 앞서 공부한 내용을 떠올려보면 Student의 주생성자를 호출할 것입니다. 그 후 부모클래스에 있는 property인 name을 초기화하기 위해서 Person의 주생성자 호출될 것입니다. 마치 부생성자가 this로 주생성자를 호출하여 property를 초기화 하듯이 동작합니다.


 그런데 한가지 신경쓰이는 open이라는 키워드가 있습니다. Java 클래스는 final을 명시했을 때 상속이 불가능하며 특별히 명시하지 않을 때는 final 클래스가 아닙니다. 코틀린 클래스는 open을 명시해야 상속이 가능하며 명시하지 않을 경우 Java의 final 클래스와 같습니다.

 즉, 다음과 같이 사용합니다.

Java에서는 상속 불가능 -> final 키워드

코틀린에서는 상속 가능 -> open 키워드


 Java 코드로 생각해보면 다음과 같습니다. 이렇게 보니 좀 더 이해하기 쉽습니다.

 public class Person { // final 클래스가 아님

 String name;

  public Person(String name) {

   this.name = name;

  }

 }


 public class Student extends Person {

  public Student(String name) {

   super(name);

  }

 }


 다음의 코틀린 코드들을 Java 코드로 변환해보면 코틀린 클래스에 대해서 좀 더 익숙해지는데 도움이 될 것 같습니다.

class ExampleUnitTest {

    @Test

    fun kotlin() {

        val student = Student()

        println("student name = ${student.name} student age = ${student.age}")

    }


    open class Person(var name: String = "Jane")

    class Student(name: String = "kim", var age: Int = 26) : Person(name)

 student name = kim student age = 26 


 class ExampleUnitTest {

    @Test

    fun kotlin() {

        val student = Student()

        println("student name = ${student.name} / student last_name = ${student.last_name} / student age = ${student.age}")

    }


    open class Person(var name: String = "Jane")

    class Student(var last_name: String = "Smith", var age: Int = 26) : Person(last_name)

}

 student name = Smith / student last_name = Smith / student age = 26 


+ Recent posts