스톱워치(초시계)
기능 소개
타이머 시작, 일시정지, 초기화, 랩타임 표시등의 기능을 갖는 초시계를 제작해 봅니다.
핵심이 되는 주제
timer
백그라운드 스레드
runOnUiThread
ScrollView
FloatingActionButton
실습
1) 빈 액티비티를 생성 (앱 이름 : StopWatch)
벡터 이미지를 사용할 것이므로 vectorDrawables.useSupportLibrary = true 설정
(모듈수준 bundle.gradle의 defaultConfig{} 에 작성하고, 'New Sync' 클릭해 주는 거 있지 않으셨죠?^^ )
2) 레이아웃 작업
① 두 개의 TextView (초, 백분의 1초 시간 표시용)를 배치합니다.
(id : 각각 secTextView, milliTextView)
참고) 뷰의 컨텍스트 메뉴에서 기준라인을 보이게 함으로써 두 뷰의 글자 하단을 정렬할 수 있어요.
② 세 개의 벡터이미지 준비해 주세요.
play arrow(시작), pause(일시정지), refresh(초기화)
③ 2개의 FloatingActionButton(FAB)을 배치해야 하는데요.
- FAB는 벡터 이미지로 깔끔한 버튼을 만들기에 적합한 이미지합성 버튼입니다.
- 구글 머티리얼 디자인에 자주 사용됩니다.
- 컴포넌트 팔레트 창에서 처음 이 컴포넌트를 추가하려고 하면 네트워크 상에서 다운로드 받도록 되어 있습니다.
(다운로드 받으세요)
- 다운로드가 끝났으면 FAB를 드래그앤드롭으로 가져다 놓고,
벡터 이미지(시작, 초기화 이미지)를 선택하고 id와 색상을 적당히 설정하세요.
총 2개의 버튼을 만들면 됩니다.
참고) 시작 버튼 id: playFab, 초기화 버튼 id: resetFab
④ 1개의 Button (랩 타임용)을 배치하세요 - id: labButton
⑤ 중앙에 ScrollView (랩 타임 표시용)를 배치해 주세요.
수직으로 차곡차곡 쌓이는 레이아웃을 LinearLayout이라고 합니다.
(컴포넌트 트리창을 보면 ScrollView 하위에 LinearLayout이 들어있을 거예요.
ScrollView 뷰는 자식 뷰룰 하나만 갖는 특수한 뷰이고,
LinearLayout 뷰는 여러 개의 자식 뷰를 갖습니다)
LinearLayout 뷰 안에 동적으로 타임랩 값들을 새로운 뷰 형태로 쌓이도록 만들것입니다.
컴포넌트 트리 창에서 LinearLayout 뷰를 선택하고 id를 입력해 주세요. ( 예: lapLayout )
3) 코딩
※ timer (타이머)
안드로이드의 두 가지 스레드 모드가 있습니다.
① 메인스레드 (일반적인 UI 가능)
② 워커스레드 (작업 시간이 오래 걸리고 화면에 표시되지 않음. 당연히 UI 조작을 불가)
타이머 코딩 형식
timer (period = 1000) { // 1000 = 1초
// 워커스레드 (UI조작 불가)
runOnUiThread {
// 메인스레드 (UI조작 가능)
}
}
- MainActivity.kt
package com.tistory.stopwatch
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import kotlin.concurrent.timer
class MainActivity : AppCompatActivity() {
private var time = 0
private var timerTask: Timer? = null // null을 허용
private var isRunning = false
private var lap = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
playFab.setOnClickListener {
isRunning = !isRunning
if (isRunning) { start() } else { pause() }
}
labButton.setOnClickListener {
recordLapTime()
}
resetFab.setOnClickListener {
reset()
}
}
private fun start() {
playFab.setImageResource(R.drawable.ic_pause_black_24dp) // 일시정지 이미지
timerTask = timer(period=10) { // 타이머 인터벌 10 ms
time++
val sec = time / 100
val milli = time % 100
runOnUiThread { // UI 조작이 가능한 블럭
secTextView.text = "$sec"
milliTextView.text = "$milli"
}
}
}
private fun pause() {
playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp) // 시작 이미지
timerTask?.cancel() // 실행중인 타이머 취소
}
private fun recordLapTime() {
if (!isRunning) return // 타이머가 실행 중이 아니라면 리턴
val lapTime = this.time
val textView = TextView(this) // 동적으로 TextView 생성
textView.text = "$lap LAB : ${lapTime / 100}. ${lapTime % 100}"
lapLayout.addView(textView, 0) // 0 : 맨 위쪽에 추가
lap++
}
private fun reset() {
timerTask?.cancel() // 실행중인 타이머 취소
// 모든 변수 초기화
time = 0
isRunning = false
playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp)
secTextView.text = "0"
milliTextView.text = "0"
// 모든 랩타임 기록 삭제
lapLayout.removeAllViews()
lap = 1
}
}
간단하지만 실행해보면 훌륭하게 동작하는 타이머가 완성됐습니다!
이번 프로그램은 비교적 간단했네요.
다음에는 자신만의 웹 브라우저를 제작해 봅시다~
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (8/16) - 앱 만들기 1 (비만도 계산기)
비만도 계산기
기능 소개
키와 몸무게를 입력하고 결과보기 버튼을 누르면 새 화면에서 비만도를 문자와 그림으로 표시합니다.
부수 기능으로 마지막으로 입력한 키와 몸무게 자료를 저장합니다.
핵심이 되는 주제
입력 및 출력 두 개 화면('액티비티')을 사용합니다.
두 화면 사이에 데이터 전달은 인텐트라는 화면 전환을 사용합니다.
SharedPreference로 자료를 저장합니다.
기타 : Anko 라이브러리 사용 방법 ( 라이브러리 의존성 추가)
벡터 이미지 사용 방법
실습
1) 빈 액티비티를 갖는 프로젝트 생성 (앱 이름 : BMICalculator)
2) 코드 작성을 쉽게 해주는 Anko 라이브러리 추가
참고: Anko 라이브러리의 구성
① Anko Commons (인텐트, 다이얼로그, 로그) <-- 우리가 사용할 모듈
② Anko Layouts (레이아웃)
③ Anko SQLite (SQLite)
④ Anko Coroutines (코루틴)
프로젝트 탐색기 창에서 모듈 수준의 그레이들 파일인 build.gradle 파일을 더블 클릭하여 편집.
(파일명 뒤에 Module 수준과 Project 수준의 구분이 표시되어 있음)
dependencies 항목에 다음의 Anko 라이브러리를 추가.
('라이브러리 의존성 추가'라고 하며 안드로이드 스튜디오에서 자동 다운로드 설치를 하게 됨)
implementation "org.jetbrains.anko:anko:$anko_version"
프로젝트 탐색기 창에서 프로젝트 수준의 그레이들 파일인 build.project 파일을 더블 클릭하여
Anko 라이브러리 버전을 변수로 지정.
buildscript {
ext.kotlin_version = '1.3.50'
ext.anko_version='0.10.5'
repositories {
google()
jcenter()
}
dependencies {
....
}
}
추가한 그레이들 파일들을 적용하기 위해 에디트창 탭 이름 바로 아래에 'Sync Now'를 클릭해서
프로젝트를 재 빌드.
3) 레이아웃 작업 1 - 키, 몸무게 입력 화면
① 'Plain Text' 뷰 배치
ID : heightEditText
input Type : (숫자만 입력할 수 있도록) number
hint : (입력 전에 표시할 문자열) 키(cm) --> 이 문자열이 보이도록 text 란의 Name을 삭제
② Plain Text 뷰도 되지만, 숫자만 입력할 거니까 이번에는 'Number' 뷰를 배치
ID : weightEditText
③ 'Button' 뷰 배치
ID : resultButton
4) 레이아웃 작업 2 - 결과 화면
에디터 창을 디자인이 아닌 텍스트 모드로 전환한 후, 결과를 출력할 새로운 액티비티 추가.
File > New > Activity > Empty Activity
(Activity Name : ResultActivity)
① activity_result.xml 창에 TextView 뷰 배치
ID : resultTextView
textAppearance : appCompat.Large
② 이미지 뷰 배치
참고) 안드로이드 스튜디오에서 제공하는 이미지 파일들
비트맵 이미지: PNG, JPG
벡터 이미지 : SVG, EPS
벡터 이미지를 사용해 보자. (에셋 스튜디오에서 생성할 수 있음)
프로젝트 창 > res 폴더 우클릭 > New > Vector Asset에서 벡터 이미지 생성
Clip Art: 에서 원하는 이미지 선택
같은 방법으로 비만도 정상, 비만, 저체중 세 가지 이미지를 추가.
activity_result.xml 화면에서 ImageView 뷰를 배치
project 항목에서 '정상'용 이미지를 선택하고 OK (예: 스마일 아이콘)
ImageView 속성 : 사이즈 조정 (예: 가로(layout_width), 세로(layout_height : 각각 100 dp),
색상(tint) 조정
※ 벡터 이미지는 vectorDrawable 리소스로 분류하는데,
백터드로어블은 Android 5.0부터 동작함.
우리는 4.4버전으로 생성했으므로 모듈 수준의 build.gradle 파일에
다음을 추가해 이를 지원케 해야 함. (Sync Now 잊지말고!)
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
5) 뒤로가기 기능 추가
결과 화면에서 다시 입력화면으로 돌아가기 위한, '뒤로가기' 기능을 상단에 추가해 보죠.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tistory.bmicalcurator">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ResultActivity" android:parentActivityName=".MainActivity"></activity>
</application>
</manifest>
앱을 실행해 보면, 결과화면 상단에 뒤로가기 링크가 표시되고, 터치하면 입력 화면으로 되돌아 갑니다.
6) 코딩
간단한 데이터와 함께 또 다른 화면(액티비티)를 구동해 주는 인텐트를 이용합니다.
<< 자료 입력 화면 MainActivity >>
-- MainActivity.kt (코틀린 일반 코딩)
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.content.Intent // 자동으로 임포트됨
import kotlinx.android.synthetic.main.activity_main.* // 레이아웃 정보가 자동으로 임포트되어 있음
// 이 덕택에 앞에서 추가했던 텍스트뷰나 버튼 사용이 가능한 것임
// 자동 임포트는 'kotlin-android-extensions' 플러그인에 의한 기능임
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadData()
resultButton.setOnClickListener {
saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())
val intt = Intent(this, ResultActivity::class.java)
startActivity(intt)
}
}
// SharedPreference로 자료 저장
private fun saveData(h: Int, w: Int) {
val pref = PreferenceManager.getDefaultSharedPreferences(this) // 프리퍼런스 객체 생성
val editor = pref.edit() // 에디터 객체 얻어오기
// 에디터 객체는 프리퍼런스 객체에 데이터를 넣어주는 역할을 함
editor.putInt("HEIGHT", h) // 키-값 쌍으로 자료 저장
.putInt("WEIGHT", w)
.apply() // 적용
}
// 자료 읽어 오기
private fun loadData() {
val pref = PreferenceManager.getDefaultSharedPreferences(this) // 프리퍼런스 객체 생성
val h = pref.getInt("HEIGHT", 0) // 0은 값이 없을 때의 디폴트 설정 값
val w = pref.getInt("WEIGHT", 0)
if (h !=0 && w!=0) {
heightEditText.setText(h.toString())
weightEditText.setText(w.toString())
}
}
}
-- MainActivity.kt (Anko 코딩)
우리는 Anko 라이브러리를 추가해서 빌드하고 있는데, Anko 라이브러리를 쓸 경우 위 버튼 리스너
를 다음과 같이 간결하게 적어도 됩니다. (단, 이 때 anko 라이브러리를 임포트해 줘야 함)
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.startActivity // 추가할 것
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
resultButton.setOnClickListener {
saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())
// Anko 코딩된 부분
startActivity<ResultActivity>(
"weight" to weightEditText.text.toString(),
"height" to heightEditText.text.toString()
)
}
}
...
}
<< 결과 화면 ResultActivity >>
-- ResultActivity.kt 의 onCreate()
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_result.*
class ResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_result)
val h = intent.getStringExtra("height").toInt() // 인텐트에서 자료 빼오기
val w = intent.getStringExtra("weight").toInt()
val bmi = w / Math.pow(h / 100.0, 2.0)
when {
bmi >= 35 -> resultTextView.text = "고도 비만"
bmi >= 30 -> resultTextView.text = "2단계 비만"
bmi >= 25 -> resultTextView.text = "1단계 비만"
bmi >= 23 -> resultTextView.text = "과체중"
bmi >= 18.5 -> resultTextView.text = "정상"
else -> resultTextView.text = "저체중"
}
when {
bmi >= 23 -> imageView.setImageResource(R.drawable.ic_sentiment_very_dissatisfied_black_24dp)
bmi >= 18.5 -> imageView.setImageResource(R.drawable.ic_sentiment_satisfied_black_24dp)
else -> imageView.setImageResource(R.drawable.ic_sentiment_dissatisfied_black_24dp)
}
Toast.makeText(this, "BMI : $bmi", Toast.LENGTH_SHORT).show() // Toast 메시지로 BMI 값 출력
// Anko로는 toast("BMI : $bmi")
}
실행 결과
Anko 라이브러리를 추가하며 실습해 봤던, 깃허브를 이용한 '라이브러리 의존성 추가'는 참 편리하고 효과적인 것 같습니다.
이 번 실습을 두 세 차례 차근 차근 따라해 보면, 흐름이 파악되실 거예요. 이렇게해서 첫 번째 안드로이드 앱을 만들어 봤습니다. 수고 하셨습니다~
'프로그래밍' 카테고리의 다른 글
[코틀린의 고급 문법 2]
고급 문법 마지막 시간입니다, 힘내자구요~~ 화이팅!
람다식
람다식이란 함수를 간결하게 표시한 형태입니다. 익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리합니다.
코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있습니다.
아래 세 가지 표현은 모두 같은 의미입니다. (코딩 간결화의 변천 과정이라고 생각해도 됨)
① 일반 함수
fun add(x: Int, y: Int) : Int { return x + y }
② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y
③ 람다식
var add = {x: Int, y: Int -> x + y } // { } 부분이 람다식임. '{ 인수들 -> 함수본문 }' 형태임
사용 예)
println(add(3+4)) // 결과: 7
SAM 변환
JAVA에서 메소드가 하나뿐인 인터페이스는 인터페이스를 구현하는 대신 함수로 구현할 수 있는데,
이것을 'SAM변환'이라고 합니다. SAM (Simple Abstract Method ; 간단한 추상메소드)
코틀린 함수는 매개변수로 한 개의 추상메소드를 받는 형태를 취할 수 있는데,
이 때는 자바에서 작성된 인터페이스인 경우에 한해,
자바의 SAM변환과 유사하게 함수로 구현된 매개변수를 전달할 수 있습니다.
예를 들어 버튼의 클릭 이벤트를 구현할 때, View.OnClickListener 인터페이스를 구현하는데,
(이 View.OnClickListener는 onClick() 이라는 추상메소드 한 개만을 가지고 있음)
이 인터페이스를 함수로, 더 나아가 익명메소드 형태로 구현해서 전달해 봅시다.
button.setOnClickListener(object : View.OnClickListener { //OnClickListener는 OnClick메소드 하나만 포함
override fun onClick(v: View?) { // 익명메소드 형태
// 클릭시 처리 코드
}
}
)
그리고 위 익명메소드는 람다식으로 표현할 수 있으므로,
button.setOnClickListener({ v: View? -> // OnClickListener가 람다식화된 형태
// 클릭시 처리 코드(람다 블럭)
})
코틀린에서 메소드 호출 시 제일 뒤의 매개변수가 람다식인 경우에는,
가독성을 위해 람다식을 ( ) 밖으로 빼서 작성할 수 있습니다.
button.setOnClickListener( ) { v: View? ->
// 클릭시 처리 코드(람다 블럭)
}
그리고 람다식이 어떤 메소드의 유일한 매개변수인 경우에는 메소드의 ( ) 를 생략할 수 있습니다. 게다가 컴파일러가 자료형을 추론할 수 있다면 자료형을 생략할 수 있는 것이므로, 최종적으로 다음과 같이 되겠네요.
button.setOnClickListener{ v ->
// 클릭시 처리 코드(람다 블럭)
}
※ 만약에 '클릭시 처리 코드'에서 v라는 매개변수를 사용하지 않는다면,
v를 _ 기호로 대치할 수도 있습니다.
button.setOnClickListener{ _ ->
// 클릭시 처리 코드(람다 블럭)
}
※ 또 만약에 그리고 람다식에서 매개변수가 하나 뿐인 경우라면,
매개변수를 아예 생략할 수도 있습니다.
button.setOnClickListener{
// 클릭시 처리코드(람다 블럭)
}
그리고 이 때 람다블럭내에서는 매개변수를 it로 접근할 수 있습니다.
예를들면,
button.setOnClickListener{
it.visiblity = view.GONE // 여기서 it는 View? 형 v를 의미함 ----- (K)
}
위에서 SAM변환을 이용한 익명메소드에서 람다식화, 그리고 코틀린의 편리한 생략 표현 기법들을 총동원해 코드를
간결화시켜봤고 코드들 모두 같은 의미입니다. 어쨌거나 가장 가독성이 뛰어난 형태는 (K)의 코드죠. 앞으로 종종 등장합니다.
주의해야 할 점은 코틀린에서의 SAM변환은 자바에서 작성한 인터페이스일 때에만 동작한다는 것~!.
확장 함수
코틀린에서는 .연산자를 이용해 이미 정의된 클래스에 메소드를 쉽게 추가할 수 있습니다.
(보통 Java나 C#에서는 final로 상속이 봉인되어 있어서 메소드를 추가하지 못하는 경우가 많은데 편리한 기능인 것 같네요)
(확장 함수 내부에서 해당 객체로의 접근은 this를 사용)
예) Int 클래스에 isEven()을 추가해 보자.
fun Int.isEven( ) = this % 2 == 0 // 'this % 2 == 0'의 결과인 부울 값이 함수의 결과 값이 됨
println(5.isEven()) // 결과: false
형 변환
val a = 10L
b = a.toInt()
c = a.toDouble()
d = a.toString()
e = Integer.parseInt(d) // 문자열 형을 숫자로 변환
※ 일반 클래스 간 형 변환 (as 키워드)
open class Human()
class Man: Human()
val man = Man()
val human = man as Human // Human 형으로 형 변환
형 체크
val st = "hello"
if (st is String) { println(st.toUpperCase() }
고차 함수
함수를 매개 변수로 전달하거나 함수 형으로 반환할 수 있습니다. 이렇게 사용되는 함수를 '고차 함수'하고 합니다.
fun add( x: Int, y: Int, callback: (sum: Int) -> Unit) { // 두 개의 Int 매개변수와 한 개의 익명함수 매개변수
// 매개 변수로 쓰인 익명함수는 한 개의 Int 매개변수를 받고 리턴 값은 없음
callback( x + y )
}
add(3,4, {println(it)}) // 결과: 7 함수를 { }로 감싸고, 이 함수 내부에서는 반환 값을 it로 접근한 예임
동반 객체
'팩토리 메소드'
코틀린에서는 클래스를 객체화 하는 것과는 별개로 메소드를 이용해 객체를 생성하는 코딩 패턴을 지원하는데, 이를 '팩토리 메소드'라고 합니다.
(나중에 다루게 될 프래그먼트 컴포넌트는 특수한 제약 때문에 팩토리 메소드로만 객체를 생성할 수 있음)
코틀린에서는 타 언어에서 정적인 메소드를 만들 때 사용하는 static 키워드 같은 게 없습니다. 그 대신 '동반 객체 (companion object)'라는 것을 통해 이를 구현합니다. (companio : 동반자, 동료, 친구)
다음은 newInstance() 정적 메소드를 사용해서 Fragment 객체를 생성하는 팩토리 패턴을 구현한 것입니다.
여기서 동반 객체 내부의 메소드는 Fragment 클래스와 아무 관계가 없는 정적인 존재입니다.
class Fragment {
companion object {
fun newInstance(): Fragment { // shs: 함수의 반환 형이 Fragment 형이라...
println("생성됨")
}
}
}
val fragment = Fragment.newInstance() // shs: 이거 정적 Fragment 안의 newInstance 멤버메소드를 액세스 하는 것과 비슷한데요...
<<< 코틀린 기본 라이브러리에서 유용한 함수들 >>>
let()
블럭에 자기 자신을 인수로 전달(it로 참조)하고 실행 결과를 반환합니다.
'안전한 호출' 즉, str이 null이 아닐때만 호출 되도록 ?.연산자를 이용하면 더 좋습니다.
val result = str?.let { // 결과는 Int형
Integer.parseInt(it)
}
// fun <T, R> T.let(block: (T) -> R): R
with()
객체를 매개변수로 받고 블럭에 리시버 객체형으로 전달(this로 참조)해 준 후, 실행 결과를 반환합니다.
단, '안전한 호출'이 불가능하기 때문에 반드시 str이 null아닐 때에만 호출해야 합니다.
with(str) {
println(toUpperCase()) // this.toUpperCase()에서 this.를 생략할 수도 있다.
}
// fun <T, R> with(receiver: T, block T.() -> R): R
apply()
블럭에 객체 자신이 리시버 객체형으로 전달되고 그 객체형으로 반환됩니다.
(주로 객체의 상태를 변경해서 반환할 때 사용함)
val result = car?.apply {
car.setColor(Color.BLACK)
car.setPrice(1000000)
}
// fun T.apply(block: T.() -> Unit): T
run()
run() 함수는 익명 함수처럼 쓰는 법과 객체에서 호출하는 법 모두를 제공합니다.
i) 익명 함수처럼 쓸 때
블럭의 결과를 반환합니다.
블럭 안에 선언된 변수들은 모두 임시로 잠깐 사용하는 변수들인데, 임시 변수를 많이 사용하는 복잡한 계산에 유용하겠네요.
val avg = run{
val kor = 90
val eng = 80
val math = 90
(kor + eng + math) / 3.0f
}
// fun run(block: () -> R): R
ii) 객체에서 호출 할 때
객체를 블럭의 리시버 객체로 전달하고 결과를 반환합니다.
안전한 호출이 가능하므로 with()보다 더 유용합니다~
str?.run {
println(toUpperCase())
}
// fun <T, R> T.run(block: T.() -> R): R
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (6/16) - 코틀린 고급 문법 1/2
[코틀린의 고급 문법 1]
특별한 상태 값 null
코틀린에서는, 모든 객체는 생성과 동시에 값을 갖도록 초기화 하는 것을 원칙으로 하며
null 사용을 허용하지 않습니다.
즉,
val a: String // 불허
val a: String = null // 불허
하지만, null을 꼭 사용하겠다면, ?를 사용해서 null을 허용하겠다고 명백히 해줘야 합니다.
val a: String? = null
주의) 위 변수 a 사용시 고려할 점
val b: String? = a // 허용 (a나 b나 동류이므로)
val c: String = a // Error (a는 부정확한 상태므로 불허)
val c: String = a!! // a값이 null이 아님을 보증한다는 의미로 !!를 붙여쓰면 허용
늦은 초기화
가끔 초기화를 일부러 늦춰야 하는 경우에 사용합니다. 코틀린에서는 두 개의 키워드로 이를 지원합니다.
(앱이 시작될 때 일부 변수들을 늦게 초기화함으로써 연산을 분산시켜 실행 속도를 빠르게 하기도 함)
lateinit
var 타입의 늦은 초기화 (int, long, float, double과 같은 프리미티브 자료형에는 사용 불가)
lateinit var a : String // 허용
by lazy
val 타입의 늦은 초기화 (프리미티브 자료형에도 사용 가능)
val a : String by lazy {
"Hello"
}
실습)
val str : String by lazy {
println("늦은 초기화!")
"Hello"
}
println(str) // 늦은 초기화!, Hello (str 첫 사용시에만 '늦은 초기화!'가 출력됨)
// (즉, lazy { } 블럭 내부 코드가 한 번만 실행됨)
println(str) // Hello
안전한 호출
.연산자 대신 ?. 연산자를 사용하면 변수 값이 null이 아닌 경우에만 메소드가 호출됩니다 (편리한 기능이네요!)
var nameUpperCase = if (str != null) str else null // 이런 문장을
var nameUpperCase = str?.toUpperCase // 로 간략하게 쓸 수 있다.
또 한 가지 편리한 기능이 있는데, 위에서 str이 null 일 때 nameUpperCase도 null 이 됩니다.
그런데 이 때 엘비스 연산자 ?: 를 사용하면 null 을 대체할 문자열을 대입할 수 있습니다.
var nameUpperCase = str?.toUpperCase ?: "초기화가 안됐어요" // str이 null인 경우 문자열이 대입됨
컬렉션
데이터 집합을 만드는 자료 구조를 말합니다. (예: 리스트, 맵, ...)
원소들의 내용을 수정할 수 없는 타입과 수정할 수 있는 가변형(mutable) 타입이 있습니다.
<리스트>
원소 변경 불가
val colors1: List = listOf("초록", "주황", "빨강", "파랑", "하양")
val colors1 = listOf("초록", "주황", "빨강", "파랑", "하양") // 물론 데이터 형을 생략할 수도 있습니다
원소 변경 가능
val colors2: MutableList = mustableListOf("초록", "주황", "빨강", "파랑", "하양")
val colors2 = mustableListOf("초록", "주황", "빨강", "파랑", "하양")
<colors2.add("검정") // 원소 추가
colors2.removeAt(4) // 원소 제거
color2[0] = "연두" // 원소 변경
<맵>
키-값 쌍의 데이터 집합 (중복불가)
원소 변경 불가
val colors3: Map<String, Int> = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
println(colors3["B"])
원소 키 값 변경 가능
val colors3: MutableMap<String, Int> = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
사용 예)
for ((k, v) in colors3) {
println("$k --- $v")
}
<집합>
원소 변경 불가
val cities: Set = setOf("Seoul", "Incheon", "CheongJu")
val cities = setOf("Seoul", "Incheon", "CheongJu")
원소 변경 가능
val cities: MutableSet = mutableSetOf("Seoul", "Incheon", "CheongJu")
val cities = mutableSetOf("Seoul", "Incheon", "CheongJu")
사용 예)
cities.add("Sokcho")
cities.remove("Incheon")
(계속...)
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (5/16) - 코틀린 기본 문법 2/2
[코틀린의 기본 문법 2]
class (클래스)
class Person { } // 빈 클래스 형태
class Person { // 생성자를 정의한 클래스 (*k1)
constructor(name: String) {
println(name)
}
}
희한하게 클래스를 아래 처럼도 정의하는군요...
class Person (var name: String) { } // (생성자를 갖는) 빈 클래스 (참고: var 생략 가능)
// 굳이 클래스명 옆에 이런 문법을 만들것 까지야...
// 메소드 오버로드는 없나보군요...
class Person (name: String) {
init { // init 블럭은 생성자와 함께 제일 먼저 실행됨 (위 *k1과 동일 결과)
println(name)
}
}
var person1 = Person("원빈") // 개체 생성
person1.name = "한효주" // 프로퍼티 쓰기
println(person1.name) // 프로퍼티 읽기
접근 제한자
public : (생략가능) 전체 공개
private : 현재 파일 내에서만 공개
internal : 같은 모듈 내에서만 공개 (모듈? 예를 들면, 한 프로젝트 내에 스마트폰용 모듈, 시계용 모듈, TV용 모듈,
태블릿용 모듈 등등 여러 모듈들을 제작하는 경우가 있음. 모듈은 여러 개의 파일 조각으로 이뤄져 있음)
protected : 부모로 부터 상속받은 클래스에서만 공개
클래스 상속
※ 코틀린에서는 기본적으로 상속을 금지합니다. (추상 클래스 제외)
그러나 꼭 사용하고자 한다면 open 키워드를 사용할 수 있습니다.
빈 클래스 상속
open class Animal { }
class Dog : Animal( ) { } // SHS: 반드시 상위 클래스의 생성자를 호출하는 형식임
생성자를 갖는 클래스의 상속
open class Animal(val name: String) { }
class Dog(name: String) : Animal(name) { }
중첩 클래스
안쪽 클래스는 바깥 클래스A와 거의 독립적입니다.
안쪽과 바깥쪽 변수명이 같아도 됩니다. 단지 선언된 위치가 클래스A 안쪽일 뿐입니다.
class A {
class B { // 중첩 클래스
}
}
var bb = A.B( ) // 클래스B의 위치를 명시하고 클래스B를 객체화 할 수 있음.
(중첩된) 내부 클래스
안쪽 클래스에 inner 키워드를 붙여주면 바깥 클래스 멤버에 접근할 수 있고,
이 때의 내부 클래스B는 클래스A의 멤버로 소속이 바뀝니다.
class A {
var a=10
inner class B { // (중첩된) 내부 클래스 - 멤버가 된 클래스
fun result( ) {
a = 20 // 바깥 클래스 멤버에 접근 가능
}
}
}
var bb = A( ).B( ) // 클래스B는 클래스A의 멤버이므로 객체화된 바깥 클래스를 통해서만 객체화가 가능
추상 클래스
미구현 메소드가 포함된 클래스를 의미하죠. 키워드: abstract
클래스명과 미구현 메소드명 앞 모두에 키워드를 작성해야 합니다.
미구현 메소드가 단 한 개라도 포함되어 있다면 추상 클래스 입니다.
추상 클래스는 반드시 상속을 목적으로 제작하는 클래스입니다.
반드시 상속한 후, 미구현 메소드를 구현해야 합니다.
이것은 반드시 지켜야 하는 개발 팀원들 간의 약속입니다.
abstract class A {
abstract fun func1( ) { }
fun func2( ) { }
}
class B: A( ) {
override fun func3( ) {
println("Hello")
}
}
인터페이스
추상 클래스와 거의 같지만, 일단 2가지 큰 차이점이 있습니다.
1. 추상 클래스는 하나만 상속 가능
인터페이스는 복수 개를 상속 받을 수 있음 ('다중 상속')
2. 추상 클래스 내부에는 추상 메소드만 선언 가능 (abstract 키워드 필수)
인터페이스 내부에서는 추상 메소드 + 일반 메소드 모두 사용가능 (abstract 키워드 불필요)
interface Runable {
fun run( ) // { } 코드가 없는 추상 메소드
fun walk( ) = println("걷는다") // 일반 메소드 포함 가능
}
class Human : Runable {
override fun run( ) { println("달린다") }
}
※ 상속과 인터페이스를 함께 사용해본 예)
class Animal { ... }
interface Runable { ... }
interface Eatable { ... }
class Dog : Animal( ), Runable( ), Eatable( ) { ... } // 클래스 상속과 인터페이스 다중 상속
(계속...)
'프로그래밍' 카테고리의 다른 글
[Windows 10에서 안드로이드 앱 개발] 코틀린(Kotlin)을 이용한 '누구나 스마트폰 앱 개발' (4/16) - 코틀린 기본 문법 1/2
모든 언어가 그렇듯이 코틀린 언어에도 문법이 있습니다. 여기에서는 코틀린의 기본 문법들을 익혀 봅시다.
프로그래밍 언어들은 단 한 가지 언어라도 입문했었다면 사실상 다른 언어들은 거져 먹는 식입니다.
코틀린 역시 특별히 새로운 문법은 없으니까 아래 글을 한 번 읽어 보는 것만으로도 파악이 끝나실 거예요.
문법을 설명하기 전에 먼저 스튜디오 안에 있는 편리한 도구 두 가지를 소개해 드릴게요.
[편리한 도구 - REPL]
REPL (Read-eval-print loop)은 코틀린 명령어(='함수'='메서드' 모두 같은 의미로 받아들여도 됨)를 한 줄씩 입력해서 실행해 볼 수 있는 쉘입니다. (쉘: 인간의 명령과 기계의 동작 사이에서 소통을 도와주는 통역자 같은 프로그램)
REPL을 실행해 보세요 --> 메인메뉴 Tools>Kotlin>Kotlin REPL 클릭하고, 아래 명령을 입력해 보세요.
println("안녕하세요, 코틀린")
명령을 실행하도록 Ctrl+엔터 를 눌러주세요.
println() 은 괄호 속의 문자열을 출력해 주는 메서드 입니다.
[편리한 도구 - 스크래치]
여러 줄의 명령어들을 한꺼번에 실행시켜 볼 수 있는, REPL 보다 좀 더 진보된 쉘입니다.
스크래치를 실행해 보세요 --> 메인메뉴 File>New>Scratch File>Kotlin 클릭하고, 아래 소스를 입력해 보세요.
var a=10
println (a * 10)
이들 명령을 실행하도록 창에서 '▶'아이콘을 눌러주세요.
var 는 변수를 선언하는 명령어이고 a에 10이 대입된 후, 이어지는 문장에서 a*10이 계산되어 100이라는 결과가 출력되는데 재밌는 것은 각 줄마다 처리된 결과가 각 줄 우측에 각각 표시된다는 거죠. 스레기통 아이콘을 누르면 출력 결과가 지워집니다.
참고) 만약 각 행에 밑줄이 그어지면서 런타임 스크립트 오류가 표시되면, Ctrl+Shift+Alt+s를 누르시고 Dependencies>app 에서 Path를 업데이트 시켜주세요. 업데이트 링크가 보일 거예요.
(안보이면 수직 스크롤바를 내려보세요)
[코틀린 기본 문법 1]
기본적인 문법을 간략하게 정리해 보겠습니다.
프로그래밍을 조금이라도 했던 분들은 그냥 이해되시겠지만, 처음 프로그래밍을 접하시는 분들은 지금부터 아래 나열된 각 키워드들에 대해서는 각자 검색등을 통해 지식을 습득하셔야 합니다. 이들에 대한 설명은 두꺼운 서적 한 권 정도 되니까 여기에 세세한 내용들을 적을 수는 없어요. ㅠㅜ
수치 자료형 : Int, Long, Short, Byte, Float, Double
문자 자료형 : Char, String
기타 : Unit (아무것도 없는 상태를 가리킴. 타 언어의 void에 해당)
리터럴 (데이터형을 표현하는 방법입니다)
1 : Int형
1L : Long형
1.0 : Double형
1.0f : Float형
var는 변수 (값 수정 가능), val은 상수 (값 수정 불가)
var a: Int = 10 // 변수 a를 Int형으로 선언 후 10을 대입
var b: Long = 10L // 변수 b를 Long형으로 선언 후 10을 대입
val pi: Float = 3.14f // 상수 pi를 Float형으로 선언 후 3.14를 대입 (값 수정 불가)
val str: String = "안녕" // 문자열 상수. 여러 줄의 문자열은 """로 감싸면 됨 (값 수정 불가)
val ch: Char = '굿' // 문자 상수. (값 수정 불가)
참고) 다른 언어에서 처럼 변수타입 없이 다음과 같이 써도 되는데, 이런 경우에는 타입이 자동으로 설정됨
var a = 10
val str = "안녕"
배열은 var, val 어느 키워드로 선언하든 모두 값 수정이 가능
val arr: Array<Int> = arrayOf(1,2,3,4,5) // Int형 1차원 배열
val arr = arrayOf(1,2,3,4,5) // 자료형을 생략해도 됨 (위와 동일)
arr[0] = 2 // 배열
++i // 증감연산자 사용 가능. i++ 라고 해도 됨.
== // 문자열 비교
=== // 개체 비교
println("입력하신 문자열은 $str 입니다") // " "내에서 문자열 변수 참조 (이건 마치 PHP 같네요)
<함수>
fun circleArea(r: Float) : Float { // Float형 인자 r을 받고 Float형 반환 값을 갖는 함수
return (r*r*pi)
}
fun circleArea(r: Float) : Pair<String, Float> { // 두 가지 형 데이터를 반환하는 함수
return Pair("원의 넓이 = ", r*r*pi)
}
fun circleArea(r: Float) : Unit { // 반환 값은 Unit형 즉, 없음.
println(r*r*pi)
}
fun circleArea(r: Float) { // 반환 값이 없을 때(=return문 없음) 반환타입을 아예 생략해도 됨
println(r*r*pi)
}
※ 식의 결과를 함수의 결과 값으로 대입
fun f( ) : Unit = println("Hi")
fun f( ) = println("Hi")
<조건문>
if ( a == 10) { println("맞아요") } // 조건문
else { println("틀려요") }
val max = if (a>b) a else b
when (a) { // switch 문을 코틀린에서 when 이라고 적음(이건 SQL 같은 모습이죠)
1 -> println("1 입니다")
2, 3 -> println("2 또는 3 입니다")
4..7 -> println("4~7 사이 값입니다") // ..연산자 사용도 가능하네요
!8..10 -> println("8~10 사이 값이 아닙니다")
else -> println("...")
}
val str = when (a % 2) {
0 -> "짝수"
1 -> "홀수"
}
fun isEven(n: Int) = when (n%2) { // when 문 결과를 함수의 반환 값으로도 사용 가능
0 -> "짝수" // 나잘난 프로그램 덕후나 쓸법한 문법...알아도 쓰지말자!
1 -> "홀수"
else -> "Exception Error" // else 분기식은 생략할 수 없음
}
println(isEven(3)) // 결과: "홀수"
val numbers : Array<Int> = arrayOf(1,2,3,4,10,11,-3,-2,-1,0)
for (i in numbers){ // 타 언어의 foreach문에 대응함
println(i) // 결과: 1;2;3;4;10;11;-3;-2;-1;0
}
for (i in 1..10) {.....} // 1,2,3,...,10
for (i in 1..10 step 2) {.....} // 1,3,5,7,9 step은 양수만 가능
for (i in 10 downTo -2 step 2) {.....} // 10,8,6,4,2,0,-2 10..-2라는 표현은 불가함
while (i < 10) {.....} // while 문
do {.....} while(i<10) // do while 문
몇 가지 문법을 살펴봤는데, 변수 선언 방법이나 클래스 상속 부분들이 C#과 같은 언어에 비해
덜 직관적인 것 같아 보이네요;; 뭐라고 표현해야 할까... 코틀린 문법이 마치 공돌이 덕후스러운
오버가 너무 많이 보인다고나 할까요^^;
제가 마소의 열성팬은 아니지만, 마소의 팬이 안될래야 안될 수가 없게 만드네요. ㅎㅎ
(계속...)