자바와 다르게 코틀린은 소스 파일의 위치와 이름을 정하는 데에 제한이 없음
자바와 달리
원시타입(primitive types)과 참조타입(wrapper type)을 구분하지 않음
서로 다른 타입의 값을 자동으로 변환 해주지 않기 때문에,
명시적으로 값 변환을 해주어야 함


A is Type
타입A as 타입B


== 연산자를 사용하고,=== 연산자를 사용한다.== 연산자를 쓸 수 있다.fun 함수이름(매개변수 리스트) = expression인 경우,void 타입인 경우도 생략 가능)

val sum = { x: Int, y: Int -> x + y }
fun lambdaTest(a : (Int) -> Int) : Int { return a(10) }
lambdaTest({x -> x + 10 }) // 인자 타입(Int) 생략 가능
lambdaTest({ it + 10 }) // 람다에 인자(매개변수)가 1개 뿐이므로 인자리스트(x ->) 생략
lambdaTest{it + 10} // 람다가 함수의 유일한 인자이면, 함수의 괄호 생략
val array = arrayOf(MyClass(10, "class1"), MyClass(20, "class2"), MyClass(30, "class3"))
println(array.filter({ c : MyClass -> c.a < 15 })
// 람다가 함수 인자의 마지막으로 사용되면, 괄호 밖으로 뺄 수 있음
array.filter() { c : MyClass -> c.a < 15 }
// 람다가 함수의 유일한 인자이면, 함수의 괄호를 생략할 수 있음
array.filter { c : MyClass -> c.a < 15 }
// 인자 타입 생략 가능(컴파일러가 이미 array는 MyClass 타입인 것을 알기 때문)
array.filter { c -> c.a < 15 })
// 디폴트 매개변수 이름으로 it을 사용할 수 있음
array.filter { it.a < 15 } // 일반적으로 가장 많이 사용되는 형태
switch 문과 비슷하지만 케이스 마다 타입이 달라도 되며,expression으로 사용할 수 있음fun test (arg : Any) {
when(arg) {
10 -> println("10")
in 0..9 -> println("0 ≤ x ≤ 9")
is String -> println("Hello, $arg")
!in 0..100 -> println("x < 0 and x > 100")
else -> {
println("unknown")
}
}
}
null이 가능한 타입과 불가능한 타입 구분?를 붙이면 nullable 타입import java.util.NullPointerException
fun testNull(arg: String?) {
println(arg?.uppercase())
println(arg?.uppercase() ?: "-")
// "?:" 엘비스 연산자로 왼쪽 수식의 값이 null이라면 "-"을 리턴하라는 의미
}
try/catch문을 expression을 사용할 수 있음override를 반드시 써야함open 키워드 사용final: 오버라이드 금지, 기본적으로 final임open: 오버라이드 가능, 명시적으로 open을 붙여야 오버라이드 가능abstrack: 반드시 오버라이드 해야 함, abstract class 내에서만 사용 가능override: 오버라이드 가능public: 기본 모듈internal 같은 모듈protected: 상속 받은 클래스에서만 접근 가능private: 같은 클래스에서만 접근 가능inner 키워드 사용
sealed class Expr {
// Num과 Sum을 하나의 타입으로 묶어주는 부모 역할을 함과 동시에 상속 범위 제한을 함
// 만약 sealed 클래스가 아니라면 나중에 새로운 클래스가 추가되었을 때,
// eval에서 else를 강제로 써야함
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int =
when(e) {
is Expr.Num -> e.value
is Expr.Sum -> eval(e.right) + eval(e.left)
// Expr의 타입은 Num과 Sum 2개밖에 없기 때문에 else가 필요하지 않음
}
fun main() {
val r = eval(Expr.Sum(Expr.Num(10), Expr.Num(10)))
println(r)
}
default argument와 같은 기능을 함default argument가 존재하지 않아,secondary constructor를 사용함

equals() hashCode() toString() copy이 생성됨클래스 정의 없이 바로 객체를 생성하는 방법
1) 싱글톤 만들기
2) companion object 만들기
3) anonymous obeject를 만들 때 사용
Companion Object는 이 객체를 포함하는 클래스의 private 멤버에 접근 가능


unaryPlus, unaryMinus, not, inc, decequals, compareTo
// 상속
open class Animal {
fun eat() = println("eating")
}
class Dog : Animal() // Animal을 상속
val d = Dog()
d.eat() // eating ← Animal의 메소드를 그대로 물려받음
// 위임
interface Printable {
fun print()
}
class Printer : Printable {
override fun print() = println("printing")
}
// Printer 객체에게 Printable 구현을 위임
class Office(p: Printer) : Printable by p
val office = Office(Printer())
office.print() // printing ← Printer가 대신 처리
// 위임이 필요한 이유
interface Printable {
fun print()
}
interface Scannable {
fun scan()
}
class Printer : Printable {
override fun print() = println("printing")
}
class Scanner : Scannable {
override fun scan() = println("scanning")
}
// 상속은 하나만 가능하지만, 위임은 여러 개 조합 가능
class Office(p: Printer, s: Scanner) : Printable by p, Scannable by s
자료 처리를 수학적 함수의 계산으로 다루고
상태 변경(changing state)과 가변 데이터(mutabledata)를 쓰지 않는 프로그래밍 패러다임
명령형 프로그래밍(imperative programming)에서는 상태를 바꾸는 것을 강조하는 것과는 달리, 함수형 프로그래밍은 함수의 응용을 강조
1930년대에 계산가능성, 결정문제, 함수 정의, 함수 응용과 재귀를 연구하기 위해 개발된 형식체계인 람다 대수(lamda-calculus)에 근간을 두고 있다.
명령형 프로그래밍의 함수
1) 프로그래밍의 상태 값을 바꾸는 부수 효과가 생길 수 있음
2) 참조 투명성이 없고, 같은 코드라도 실행되는 프로그램의 상태에 따라
다른 결과값이 나옴
함수형 프로그래밍의 함수(수학적 함수)
1) 함수의 출력 값은 함수에 입력된 인수에만 의존
2) 인수 x에 같은 값을 넣고 함수 f를 호출하면 항상 f(x)라는 결과가 나옴
(참조 투명성)
3) 부수 효과를 제거하면 프로그램 동작 이해와 예측이 쉬움
{x -> x \* x}(1..10).map {it \* it}코틀린은 순수한 함수형 언어가 아님. 함수형 언어의 요소 뿐 아니라
명령형 언어, 객체 지향 언어 패러다임을 모두 가지고 있음
{x: Int, y: Int -> x + y}data class Student(val name: String, val age: Int)
fun main() {
val data = listOf(Student("Jun", 21), Student("James", 25),
Student("Tom", 21), Student("Jane", 23), Student("John", 23))
println(data.filter {it.age >= 22})
// [Student(name=James, age=25), Student(name=Jane, age=23),
// Student(name=John, age=23)]
println(data.map {it.age - 20})
// [1, 5, 1, 3, 3]
println(data.filter {it.age >= 22}.map(Student::name)
// [James, Jane, John]
println(data.groupBy {it.age})
// {21=[Student(name=Jun, age=21), Student(name=Tom, age=21)],
// 25=[Student(name=James, age=25)],
// 23=[Student(name=Jane, age=23), Student(name=John, age=23)]}
val words = arrayOf("hello", "hi", "hot", "apple",
"orange", "access", "order", "about")
println(words.groupBy {it.first()})
// {h=[hello, hi, hot], a=[apple, access, about], o=[orange, order]}
}
fun main() {
val nums = arrayOf(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5)
println(nums.all {it is Int}) // true
println(nums.all {it > 0}) // false
println(nums.any {it > 0}) // true
println(nums.count {it > 0}) // 5
println(nums.find {it > 0}) // 1
}
flatMap에 주어진 람다는 Iterable 객체를 리턴하고,Iterable을 모두 연결하여 하나의 List로 만듦public inline fun <T, R>
Iterable<T>.flatMap(transform: (T) -> Iterable<R>) : List<R>
fun main() {
val list = listOf("abc", "cde", "efg")
println(list.flatMap {it.toList()} // [a, b, c, c, d, e, e, f, g]
println(list.flatMap {it.toList()}.toSet()) // [a, b, c, d, e, f, g]
class Classes(val name: String, val students: List<String>)
val classes = listOf(Classes("Cprog", listOf("james", "john", "greg")),
Classes("OS", listOf("james", "john", "jane", "tom")),
Classes("Net", listOf("john", "jane", "alex", "sam")) )
println(classes.flatMap { it.students }.toSet().sorted())
// [alex, greg, james, jane, john, sam, tom]
}
filter, map 등의 함수를 부를 때, 바로 다른 리스트를 생성하게 됨
이러한 함수를 매우 큰 데이터에 대해 여러번 이어서 사용하면 매우 느려질 것
이런 경우 Sequence를 사용하면
리스트 생성을 최대한 늦추게 하는 방법임
실제로 리스트 생성 작업을 최대한 늦추기 때문에 lazy 연산이라고 보통 부름
Sequence를 사용하여 연산을 끝낸 후에,
다시 toList()로 Collection을 바꾸어 사용함
fun interface doInterface { // SAM
fun doIt()
}
fun doSomething(di: doInterface) = di.doIt()
fun main() {
doSomething(object : doInterface { // java-like
override fun doIt() { println("Java-like way") }
})
}
doSomething( doInterface { println("SAM") } )
doSomething { println("SAM 1") }
/*
doSomething {
override doIt() {
println("SAM1")
}
}
과 같음
추상 메소드가 1개이기 때문에 생략이 가능*/
val doi = { println("SAM 2") }
doSomething(doi)
let, run, with, apply, alsoit 또는 this로 지칭fun main() {
val str = with (StringBuilder()) { // this로 객체 지정
this.append("Hello, ")
append("This ") // this 생략
append("is an example of ")
append("lambda with.")
.toString()
} // with의 리턴값은 마지막 expression
println(str)
val str2 = StringBuilder().apply {
this.append("Hello, ")
append("This ") // this 생략
append("is an example of ")
append("lambda apply.")
}.toString() // apply는 StringBuilder를 리턴함
println(str2)
}
fun main() {
val r1 = "hello".let {
println("$it")
it.length
}
val v2 = "hello".run {
println("$this")
length
}
val v3 = "hello".also {
println("$it")
}.length
println("$r1, $r2, $r3") // 5, 5, 5
}
for문을 이용함Iterable을 이용하여 해당 클래스가 순회 가능함을 명시data class Three(var x: Int, var y: Int, var z: Int) : Iterable<Int> {
operator fun get(idx: Int) : Int {
return when(idx) {
0 -> x
1 -> y
2 -> z
else ->
throw IndexOutOfBoundsException("Invalid index $idx")
}
}
operator fun set(idx: Int, value: Int) {
when(idx) {
0 -> x = value
1 -> y = value
2 -> z = value
else ->
throw IndexOutOfBoundsException("Invalid index $idx")
}
}
operator fun contains(value: Int) = (x == value || y == value || z == value)
inner class MyIterator : Iterator<Int> {
var curIdx = 0
override fun next(): Int {
val ret = this@Three[curIdx]
curIdx++
return ret
}
override fun hasNext(): Boolean {
return curIdx <= 2
}
}
override fun iterator(): Iterator<Int> = MyIterator()
}
fun main() {
val three = Three(1, 2, 3)
println("${three[0]}, ${three[1]}, ${three[2]}")
println(3 in three)
for (i in three) {
println(i)
}
}
vararg를 매개 변수 이름 앞에 사용spread 연산자: *을 배열 앞에 붙여서 가변 인자로 넘겨줄 수 있음val list = listOf(1, 2, 3, 4)
fun listOf<T>(vararg values: T): List<T> { } // 가변 인자 선언
val args = arrayOf("1", "2", "3", "4")
val list2 = listOf("0", *args) // spread 연산자
println(list2) // 0, 1, 2, 3, 4
infix fun Int.add(other: Int): Int {
return this + other
}
println(3.add(5)) // 일반 호출
println(3 add 5) // infix 호출
data class Result(val result: Int, val status: String)
fun calcSomething(): Result {
// computations
return Result(404, "Not Found")
}
fun main() {
val (num, str) = 1 to "one"
println("$num, $str") // 1, one
val collection = mapOf(1 to "one", 2 to "two")
for ((a, b) in collection) {
println("$a, $b")
}
val (result, status) = calcSomething()
println("$result, $status") // 404, Not Found
}
모바일은 기기마다 화면 크기와 비율이 제각각
유연한 레이아웃(Flexible Layout)
화면 크기에 따라 너비나 높이가 유동적으로 변하는 구조
안전 영역(Safe Area)
노치 디자인이나 하단 바에 UI가 가려지지 않도록 보호하는 영역
적응형 UI(Adaptive UI)
기기 유형에 따라 레이아웃 구조 자체가 바뀌는 설계
디자인 가이드라인이 없어도 지켜야 할 최소한의 규칙
시각적 계층 구조
중요한 정보는 크고 진하게 배치하여 사용자의 시선을 유도
피드백(Affordance)
버튼을 눌렀을 때 색이 변하거나 진동이 오는 등, 동작이 수행됨을 알려야 함
일관성(Consistency)
앱 전체에서 확인 버튼의 위치나 뒤로가기 동작이 일관되어야 함
접근성(Accessibility)
저시력자나 색약 사용자를 위한 충분한 대비와 텍스트 크기 고려
이미지의 경우 content Description을 제공하여 시각장애인 고려
명령형(Imperative)→선언형(Declarative)으로
모바일 UI 프레임워크 패러다임 변화
ViewGroup은 View이지만 다른 View를 포함할 수 있는 ViewViewGroup에 속함기존 안드로이드 View(명령형 UI)에서는 컴포넌트를 find해서
해당 컴포넌트의 데이터를 수정
Compose UI는 상태(State)의 함수이며, UI는 상태에 따라 변경됨
로컬 상태와 전역 상태
로컬 상태: 특정 화면 내에서만 쓰이는 임시 데이터
예) 입력창의 텍스트
전역 상태: 앱 전체에서 공유되는 데이터
예) 로그인 유저 정보
상태가 변경되면 해당 상태를 이용하는 컴포저블 함수가 다시 실행
(Recomposition)
컴포저블을 재사용 가능하고 테스트하기 쉽게 만들기 위해
상태를 위쪽(Caller)으로 올리는 패턴
가능한 한 pure function으로 만들어서 관리
방법: 상태 변수 대신 value와 onValueChange 이벤트 콜백을 파라미터로 전달
장점: Stateless 컴포저블을 만들어 UI 로직과 비즈니스 로직을 분리
데이터는 위에서 아래로 흐르고, 이벤트는 아래에서 위로 전달되는 구조
이를 통해 데이터 무결성을 유지
상태를 가능한 한 위쪽에서 관리하여 유지보수가 용이
remember를 사용하지 않으면 함수가 다시 시작될 때 마다 변수가 초기화rememberSaveable을 사용@Preview(locale="ko")
와 같이 미리보기에서 언어 변경하여 확인 가능
앱 구성 요소, 사용자와 상호작용할 수 있는 화면 제공
안드로이드 앱 구성 요소
액티비티, 서비스, 브로드캐스트 리시버, 컨텐트 프로바이더
앱의 시작은 보통 액티비티에서
앱에는 2개 이상의 액티비티가 포함될 수 있음
Compose UI에서는 한 액티비티에서 컴포저블을 이용해서
여러 화면을 구성하고, 화면 전환을 하는 것이 일반적임
액티비티는 ComponentActivity를 상속하여 만듦
Compose UI인 경우 ComponentActivity를 상속
Android View 시스템에서는 Activity나 AppCompatActivity를 상속
API 호환성을 높이기 위해 AppCompatActivity 사용 권장
onCreate → onStart → onResume
→ 액티비티 활성화 →
onPause → onStop → onDestroy
1) MainActivity의 onPause()
2) SecondActivity의 onCreate → onStart() → onResume()
3) MainActivity의 onStop()
1) SecondActivity의 onPause()
2) MainActivity의 onRestart() → onStart() → onResume()
3) SecondActivity의 onStart() → onDestroy()
onPause() → onStop() → onDestroy()
→ onCreate() → onStart() → onResume()
startActivity(Intent(this, SecondActivity::class.java))
시작하려는 액티비티(SecondActivity.class)를 지정하고 Intent 생성
이렇게 명시적으로 액티비티 클래스를 지정하는 것을 명시적 인텐트라고 함
startActivity()에 인텐트 객체를 인자로 해서 호출
val context = LocalContext.current
val intent = Intent(context, SecondaActivity::class.java)
context.startActivity(intent)
LocalContext: 현재 사용 가능한 context를 알려줌SavedStateHandle을 사용하면 시스템이 보존해둔class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private val COUNT_KEY = "count"
// SavedStateHandle에서 값을 가져오거나 기본값 설정
// StateFlow로 변환하여 Compose에서 관찰 가능하게 만듦
val countStateFlow: StateFlow<Int> = savedStateHandle.getStateFlow(COUNT_KEY, 0)
fun increaseCount() {
val currentCount = savedStateHandle.get<Int>(COUNT_KEY) ?: 0
savedStateHandle[COUNT_KEY] = currentCount + 1
}
}