A 액티비티 → B 액티비티 이동

Intent 에 key value 1:1 형식으로 데이터를 담아 액티비티를 시작한다.

//A 액티비티에서의 호출
Intent intent = new Intent(activity, B.class);
        intent.putExtra( , 넘겨줄 내용);
        intent.putExtra( , 넘겨줄 내용);
        activity.startActivity(intent);
        ActivityAnimation.activityAnimateEnter(activity); //액티비티 전환시 애니메이션

//B 액티비티에서의 데이터 수신
getIntent().getIntExtra( , 0L)
getIntent().getBooleanExtra( , false)

개선할 점

  1. 기존 방식은 키 값을 B액티비티 외부에서도 알고 있어야 한다.
  2. 새로운 키 값의 정의가 필요하다.
  3. 키 값을 다른 키값으로 사용하지는 않았는지 확인이 필요하다.
  4. 같은 키값을 사용 했어도 동일한 키값에 대해 Boolean 으로 넘겨 줬는데 Long 으로 받는 경우 데이터를 제대로 전달 받지 못할 수 있다.

이런 실수를 방지 하기 위해 새로운 방식을 도입하려고 한다.

새로운 방식

A 액티비티 → B 액티비티 이동

공통으로 필요한 부분을 위해 BaseArguments Abstract Class 를 준비했다.

abstract class BaseArguments : Parcelable {

    fun startActivityEnter(activity: Activity, destinationClass: Class<*>) {
        Intent(activity, destinationClass).apply {
            putExtra(KEY_ARGUMENTS, this@BaseArguments)
        }.let {
            activity.startActivity(it)
            enterAnimation(activity)
        }
    }

    fun startActivityEnterForResult(activity: Activity, destinationClass: Class<*>, requestCode: Int) {
        Intent(activity, destinationClass).apply {
            putExtra(KEY_ARGUMENTS, this@BaseArguments)
        }.let {
            activity.startActivityForResult(it, requestCode)
            enterAnimation(activity)
        }
    }

    //reified는 자바 코드에서는 사용 못함
    inline fun <reified destinationActivity : Activity> startActivityEnter(activity: Activity) {
        Intent(activity, destinationActivity::class.java).apply {
            putExtra(KEY_ARGUMENTS, this@BaseArguments)
        }.let {
            activity.startActivity(it)
            enterAnimation(activity)
        }
    }

    //reified는 자바 코드에서는 사용 못함
    inline fun <reified destinationActivity : Activity> startActivityEnter(activity: Activity, requestCode: Int) {
        Intent(activity, destinationActivity::class.java).apply {
            putExtra(KEY_ARGUMENTS, this@BaseArguments)
        }.let {
            activity.startActivityForResult(it, requestCode)
            enterAnimation(activity)
        }
    }

    //enterAnimation 커스텀 사용 가능
    //진입시 다른 애니메이션 원하는 경우 오버라이드 해서 사용
    open fun enterAnimation(activity: Activity) {
        //진입 애니메이션 구현
    }

    companion object {
        const val KEY_ARGUMENTS = "KEY_ARGUMENTS"

        @JvmStatic
        fun createFromIntent(intent: Intent): BaseArguments {
            return intent.getParcelableExtra(KEY_ARGUMENTS)!!
        }
    }
}

코틀린 코드 사용 방법

class B {//B 액티비티 안에 Arguments 클래스를 생성
  private lateinit var arguments: Arguments

  @Parcelize
  class Arguments(
        val value: Boolean
  ) : BaseArguments()

  override fun onCreate(savedInstanceState: Bundle?) {
        arguments = BaseArguments.createFromIntent(intent) as Arguments
        arguments.value //넘겨준 데이터 접근
  }
}

A 액티비티에서의 호출
B.Arguments(true).startActivityEnter(this, B.class);

자바 코드 사용 방법

class B {
    public static class Arguments extends BaseArguments {
        private final long value;

        public Arguments(long value) {
            this.value = value;
        }

        protected Arguments(Parcel in) {
           this.value = in.readLong();
        }

    public static final Creator<Arguments> CREATOR = new Creator<Arguments>() {
        @Override
        public Arguments createFromParcel(Parcel source) {
            return new Arguments(source);
        }

        @Override
        public Arguments[] newArray(int size) {
            return new Arguments[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int i) {
        dest.writeLong(this.value);
    }

  }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        arguments = (Arguments) BaseArguments.createFromIntent(getIntent());
    }
}

A 액티비티에서의 호출
new B.Arguments(true).startActivityEnter(this, B.class);

개선된 점

  1. 외부에서 키 값을 알고 있지 않아도 된다.
  2. 새로운 키값에 대한 정의가 필요없다.
  3. 키 값을 다른 키값으로 사용하지는 않았는지 확인이 필요 없다.
  4. 데이터를 넘겨 받는 순간 Type을 알기 때문에 데이터 보장이 된다.

아쉬운점

  1. 액티비티 생성 시 1:1 로 Arguments 클래스를 생성해야한다.
  2. 코틀린에서는 Parcelize 어노테이션을 사용하면 편하지만 자바에서 Parcelable 을 사용시 선언이 번거롭다.

코틀린으로 함수형 프로그래밍 시작하기

2.1 프로퍼티 선언과 안전한 널 처리

val a: String = "a" // 읽기 전용 프로퍼티(값을 변경 할 수 없음)
var a: String ="a" //가변 프로퍼티 (값을 다른 값으로 변경 할 수 있다.)

타입 추론을 사용한 선언

val a ="a"
var b ="b"

a 가 String 타입을 코틀린 컴파일러가 추론하기 때문에 String 을 생략할 수 있다.

안전한 널처리

val a: String = null //컴파일 오류 발생!!
val a: String? = null

2.2 함수와 람다

함수를 선언 하는 방법 3가지

fun sum(a:Int, b: Int): Int {
    return a + b
}

fun sum(a:Int, b: Int): Int  = a + b 

fun sum(a:Int, b: Int) = a + b 

파라미터 디폴트 값 셋팅

fun sum(a:Int = 0, b: Int = 0) = a + b //sum() 호출 하면 return 0+0 과 같다.

익명 함수와 람다 표현식

fun calculate(x: Int, y: Int, calculate: (Int,Int)-> Int): Int {
    return calculate(x,y)
}

사용 calculate(1,2, {x,y->x+y})
return 1+2 
결과값 3

확장 함수

fun Int.sum(value: Int): Int {
    return this + value
}
이미 작성된 Int 클래스에 sum 함수를 생성   있다. 

2.3 제어 구문

if문

자바와 달리 if문이 구문으로 쓰이기도 하고 표현식으로 사용되기도 한다.

if 구문 사용

val isTrue : Boolean
if(x > y){
    isTrue = true
}else{
    isTrue = false
}

if 표현식

val isTrue = if(x>y){ true } else { false }
true , false 라는 값을 isTrue 변수에 반환

when문

자바의 switch와 비슷한 기능

when(value){
    1 ->{println("value==1")} //value 가 1인 경우
    2,3 ->{println("value 2 or value 3")} //value 가 2 또는 3인 경우
    else ->{println("else")} //그 외의 경우
}

when 표현식

val value = when {
    x == 1 -> 1
    x == 2 -> 2
    else -> 3 
}

for문

val list = listOf(1,2,3,4,5)

for(item in list){
    print(item) //12345 출력
}
for((index, item) in list.withIndex()){
    println("index: $index item: $item")
    // println("index: 0 item: 1")
    // println("index: 1 item: 2")
    // println("index: 2 item: 3")
    // println("index: 3 item: 4")
    // println("index: 4 item: 5")
} 

for(i in 1..5){
    print(i) 12345 출력
}
for(i until 1..5){
    print(i) 1234 출력
}
for(i in 6 downTo 0 step 2){
    print(i) // 6420 출력
}

2.4 인터페이스

인터페이스 특징

  1. 다중 상속이 가능하다
  2. 추상 함수를 가질 수 있다
  3. 함수의 본문을 구현할 수 있다
  4. 여러 인터페이스에서 같은 이름의 함수를 가질 수 있다
  5. 추상 프로퍼티를 가질 수 있다

1번, 2번 예제 (다중 상속이 가능하다, 추상 함수를 가질 수 있다)

interface A {
    printA() //추상 함수
}
interface B {
    printB() // 추상 함수
}
class Model : A,B { //A 와 B 다중 상속
    override fun printA(){
        //하고 싶은 구현
    }
    override fun printB(){
        //하고 싶은 구현
    }
}

3번 예제 (함수의 본문을 구현할 수 있다)

interface A {
    printA(){
        print("A")
    }
}

4번 예제 (여러 인터페이스에서 같은 이름의 함수를 가질 수 있다)

interface A {
    printTest(){
        print("A")
    }
}
interface B {
    printTest(){
        print("B")
    }
}
class Model : A,B{
    //Model 클래스의 printTest 사용 시 A 의 printTest 구현부 , B 의 printTest 구현부 둘다 호출
    override fun printTest(){
        super<A>.printTest()
        super<B>.printTest()
    }
}   
}

5번 예제 (추상 프로퍼티를 가질 수 있다)

interface A {
    val a : Int
}
class Model {
    override val a: Int = 1 //val ,var 둘다 가능하다.
}

함수형 프로그래밍 특징

  1. 불변성(immutable)
  2. 참조 투명성(referential transparency)
  3. 일급 함수(first-class-fuction)
  4. 게으른 평가(lazy evaluation)

불변성(immutable) 순수 함수에 대해 알아보자.

순수한 함수는 동일한 입력에 동일한 결과를 돌려 준다.

class Test {
    var z = 10

    @Test
    fun fucTest() {
        println("plus : " + plus(2, 3)) //5 출력
        println("plus2 : " + plus2(2, 3).toString()) //15출력
    }

    fun plus(x: Int, y: Int): Int {
        return x + y
    }

    fun plus2(x: Int, y: Int): Int {
        return x + y + z
    }
}

첫번째 plus 함수는 어떤 값이 와도 두개의 값을 더해서 값을 돌려줘서 순수 함수라고 볼수 있다.

두번째 plus2 함수는 z 값이 중간에 변경되면 동일 입력 값이라도 결과 값이 변경될 수 있기 때문에 순수한 함수라고 볼 수 없다.

참조 투명성에 대해 알아보자.

위의 plus 함수는 x,y 참조 되어 있기 때문에 참조 투명한 함수라고 볼 수 있고 plus2는 z가 추가적으로 참조 되어 있기 때문에 불투명하다.

plus2 를 투명하게 변경한다면 아래와 같이 변경 되어야 한다.

class Test {

    @Test
    fun fucTest() {
        println("plus2 : " + plus2(2, 3, 5).toString()) //10출력
    }

    fun plus2(x: Int, y: Int, z: Int): Int {
        return x + y + z
    }
}

일급 함수에 대해 알아보자.

일급 함수를 알아보기 전에 일급 객체라 뭔지 알고 가자.

일급 객체란?

  1. 객체를 함수의 매개변수로 넘길 수 있다.
  2. 갹체를 함수의 반환값으로 돌려 줄 수 있다.
  3. 객체를 변수나 자료구조에 담을 수 있다.

코틀린 최상위 개념인 Any는 일급 객체이다.

일급 함수란?

  1. 함수를 함수의 매개변수로 넘길 수 있다.
  2. 함수를 함수의 반환값으로 돌려 줄 수 있다.
  3. 함수를 변수나 자료구조에 담을 수 있다.
class Test {

    var funcList: List<(Int) -> String> = listOf { value -> value.toString() }//함수를 자료구조에 담음

    fun doSomthing(func: (Int) -> String) {//함수를 함수의 매개변수로 넘김
        //doSomething
    }

    fun doSomething(): (Int) -> String {//함수를 함수의 반환값으로 돌려줌
        return { value -> value.toString() }
    }
}

게으른 평가(lazy evaluation) 에 대해 알아보자

class Test {
    val lazyValue: String by lazy {
        println("시간 오래 걸리는 작업")
        "hi"
    }


    @Test
    fun test(){
        println(lazyValue)
        println(lazyValue)
    }
}

lazy 는 기본적으로 값이 필요한 시점에 평가되기 때문에 프로그래머가 평가 시점을 지정할 수 있다.

시간이 오래 걸리는 작업은 한번만 평가 되고 그다음 호출 되지 않는다.

값이 지속적으로 변경 되는 값인데 lazy를 착각해서 잘못사용하게 되는 경우 잘못된 값을 리턴 받을 수 있다.

함수형 프로그래밍의 기본적인 개념을 알아보는 시간을 가졌다.

11장까지 있으니 올해도 화이팅 ㅎ

gson 라이브러리는 json object 를 json string 으로 변환 시켜주거나 json string 을 json object 로 쉽게 변환 할 수 있게 만들어 주는 라이브러리이다.

라이브러리 추가는 app gradle dependency에 다음과 같이 추가해주면 사용 가능하다.

dependencies {
  implementation 'com.google.code.gson:gson:2.8.6'
}

우리는 작업 하다보면 특정 Object 내부 필드를 모두 로그로 보고 싶을 때가 있다.

그렇게 되면 object 의 toString 를 override 해서 모든 필드에 대해 다시 정의를 해줘야 모두 노출이 가능하다.

그리고 필요한 object 마다 toString 을 재정의 해서 번거롭게 사용 해야 한다.

class ToJsonStringTest {

    @Test
    fun toStringTest() {
        val order = Order(
                id = 1,
                goods = listOf(
                        Goods(id = 1, name = "나나", price = 3000L),
                        Goods(id = 2, name = "다다", price = 4000L)
                )
        )
        println(order.toString())
    }

    class Order(val id: Long, val goods: List<Goods>) {
        override fun toString(): String { //return 을 재정의 해줘야 함.
            return "id : $id , goods : $goods"
        }
    }

    class Goods(val id: Long, val name: String, val price: Long) {
        override fun toString(): String {//return 을 재정의 해줘야 함.
            return "id : $id , name : $name , price : $price"
        }
    }
}

아래는 order.toString 의 결과이다.

이부분을 gson 라이브러리를 사용해서 다음과 같이 해결 하였다.

fun Any.toJsonString(withNull: Boolean = false): String {
    return try {
        val builder = GsonBuilder().disableHtmlEscaping().setPrettyPrinting()
        if(withNull) builder.serializeNulls()
        builder.create().toJson(this)
    } catch (e: Exception) {
        e.printStackTrace()
        "Fail to print toJsonString()"
    }
}

class ToJsonStringTest {

    @Test
    fun toStringTest() {
        val order = Order(
                id = 1,
                goods = listOf(
                        Goods(id = 1, name = "나나", price = 3000L),
                        Goods(id = 2, name = "다다", price = 4000L)
                )
        )
        println(order.toJsonString())
    }

    class Order(val id: Long, val goods: List<Goods>)

    class Goods(val id: Long, val name: String, val price: Long)
}

아래는 order.toJsonString 의 결과이다.

toString 을 override 하지않고 Gson 을 사용해 Kotlin Extension 으로 만들어 둔 다음 사용하면 된다.

오늘은 LiveData 와 Flow State 차이점을 알아보자.

LiveData

  1. Value 가 Nullable 사용 시 널 체크 또는 liveData.value!! 사용해야함 (번거로움)
  2. setValue 와 postValue setValue 와 postValue 의 차이점을 알고 있어야 한다.

setValue 는 mainThread 에서 사용 해야한다.

postValue 는 background Thread , mainThread 모두 사용 가능

postValue 와 setValue 여러번 호출될 경우 생각한 대로 데이터 싱크가 맞지 않을 수 있음.

setValue 가 여러번 호출 되는 경우 모두 호출 되지만 postValue 는 연속으로 여러번 호출 되는 경우 최신 값이 호출됨.

StateFlow

  1. Value 가 NonNull 사용 시 널 체크를 할 필요 없이 사용
  2. Collect 사용 시 디폴트 값이 그대로 수집됨
  3. Value 값이 변경된 값만 수집 됨.
class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainActivityViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
        viewModel.apply {
            testLiveData.observe(this@MainActivity, Observer { //liveData 데이터 수집
                Log.e("kyh!!!", "testLiveData : $it")
            })
            lifecycleScope.launch {
                testStateFlow.collect { //testStateFlow 데이터 수집
                    Log.e("kyh!!!", "testStateFlow : $it")
                }
            }

        }
        tvpostValueLiveData.setOnClickListener {
            viewModel.postValueLiveData()
        }
        tvsetValueLiveData.setOnClickListener {
            viewModel.setValueLiveData()
        }
        tvFlowState.setOnClickListener {
            viewModel.setValueFlowState()
        }

    }
}

class MainActivityViewModel : ViewModel(){
    val testLiveData = MutableLiveData<Boolean>()
    val testStateFlow = MutableStateFlow<Boolean>(false) //false 값이 처음 불림

    fun setValueLiveData() {// setValue 로 호출 했기 때문에 모든 값이 수집 된다.
        testLiveData.value = false
        testLiveData.value = false
        testLiveData.value = true
        testLiveData.value = false
    }

    fun postValueLiveData() {// postValue 로 호출 했기 때문에 제일 마지막에 호출 한 false 값이 호출 된다.
        testLiveData.postValue(false)
        testLiveData.postValue(false)
        testLiveData.postValue(true)
        testLiveData.postValue(false)
    }

    fun setValueFlowState(){// stateFlow 는 변경된 최신값을 호출한다. 기존 값이 false 였기 때문에 처음 세번은 수집 되지 않고 맨 뒤 true, false 수집 된다.
        testStateFlow.value = false
        testStateFlow.value = false
        testStateFlow.value = false
        testStateFlow.value = true
        testStateFlow.value = false
    }
}

주의 !!

A->B 화면 이동을 위한 LiveData 를 뷰모델에서 만들어서 사용하고 있었는데 그냥 StateFlow 로 변경 하면 화면 열리자 마자 A->B 화면 이동이 되버림.