국가 수도 맞추기 앱 만들기 #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"

        }

    }





Getters and Setters


 코틀린에서 property를 선언할 때 생략없는 full syntax는 다음과 같습니다.

 var <propertyName>[: <PropertyType>] [= <property_initializer>]

    [<getter>]

    [<setter>]


 여기서 [ ]로 표시된 property_initializer, getter, setter은 선택사항입니다. 생략할 수 있다는 말입니다.


앞서 property를 공부할 때 다음과 같은 예제를 보았습니다.

  class Person {

  var id: Int = 0

  var name: String = "Suzuki"

 }


 getter, setter은 전혀 보이지 않습니다. 그런데 initializer는 사용했습니다. initializer을 생략을 하려면 어떻게 할 수 있을까요?

일단 initializer를 지워보면 Error:(22, 9) Property must be initialized or be abstract가 발생합니다.


 

 따라서 abstract로 선언하는 방법이 있고 위와 같이 constructor의 파라미터로 옮길 수 있습니다.

    abstract class Person {

        abstract var id: Int

        var name: String = "Suzuki"

    } 

or

    class Person(var id: Int) {

        var name: String = "Suzuki"

    } 


 property type도 생략할 수 있습니다. 단, initializer로 부터 type을 유추할 수 있을 때만 가능합니다.

     class Person() {

        var id = 0 // var id: Int = 0

        var name = "Suzuki" // var name: String = "Suzuki"

    }


 getter, setter는 생략되었는데 문제가 없는지 살펴봅시다. 위의 Person 클래스를 Java 코드로 Decompile 하면 다음과 같습니다.

 Decomplie 하는 방법은 Tools > Kotlin > Show Kotlin Bytecode 선택 후 Kotlin Bytecode 창에서 Decompile을 누르시면 됩니다.


    public static final class Person {

      private int id;

      @NotNull

      private String name = "Suzuki";


      public final int getId() {

         return this.id;

      }


      public final void setId(int var1) {

         this.id = var1;

      }


      @NotNull

      public final String getName() {

         return this.name;

      }


      public final void setName(@NotNull String var1) {

         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");

         this.name = var1;

      }

   }

 

코틀린 코드에서는 선언한 적이 없는 getter와 setter가 존재합니다. 이것으로 미루어 볼 때 코틀린에서는 기본적으로 getter와 setter를 선언할 필요가 없습니다. 

 property 사용 방법을 떠올려보면 getter와 setter를 사용하지 않고 변수명으로 직접 get/set을 하였습니다.

 val student = Person()

 student.id = 10 // 직접 set을 하고 있음

 println("student.name ${student.id}") // 직접 get 하고 있음

이것이 가능한 이유가 Java 코드로 Decompile 했을 때 getter/setter가 존재하기 때문이라는 것을 알 수 있습니다.


 getter와 setter을 생략하지 않고 선언하는 방법도 있습니다.

    class Person() {

        var id = 0

        get() =  100

        var name = "Suzuki"

        set(value) {

            field = "Kim"

        }

    }


 위 Decomplie 결과물을 참고하여 getter/setter를 추가했을 때의 코드를 Java 코드로 직접 변환해봅시다.

    public static final class Person {

      private int id;

      @NotNull

      private String name = "Suzuki";


      public final int getId() {

         return 100;

      }


      public final void setId(int var1) {

         this.id = var1;

      }


      @NotNull

      public final String getName() {

         return this.name;

      }


      public final void setName(@NotNull String value) {

         Intrinsics.checkParameterIsNotNull(value, "value");

         this.name = "Kim";

      }

   }


 이제 id의 getter는 100만 리턴해주고 name의 setter는 Kim으로만 assign을 합니다.

그럼 아무리 id의 값을 reassign 하더라도 student.id는 100을 리턴하게 됩니다. name은 아무리 reassign하더라도 Kim으로만 assign됩니다. 확인해볼까요?

class ExampleUnitTest {

    @Test

    fun kotlin() {

        var student = Person()

        student.name = ""

        student.id = 200

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


    }


    class Person() {

        var id = 0

        get() =  100

        var name = "Suzuki"

        set(value) {

            field = "Kim"

        }

    }

}

 student.id 100 student.name Kim 


+ Recent posts