코틀린-안드로이드

56일차)알고리즘 문제(마법의 엘리베이터), 해시셋 원소추가 시간복잡도, 특강(livedata, sharedflow, stateflow, Coroutine EventBus, Repository Pattern), 스크럼(ListAdapter에서 notifyDataSetChanged)

songyooho 2024. 7. 19. 21:01

>알고리즘 문제

1. 문제

마법의 세계에 사는 민수는 아주 높은 탑에 살고 있습니다. 탑이 너무 높아서 걸어 다니기 힘든 민수는 마법의 엘리베이터를 만들었습니다. 마법의 엘리베이터의 버튼은 특별합니다. 마법의 엘리베이터에는 -1, +1, -10, +10, -100, +100 등과 같이 절댓값이 10c (c ≥ 0 인 정수) 형태인 정수들이 적힌 버튼이 있습니다. 마법의 엘리베이터의 버튼을 누르면 현재 층 수에 버튼에 적혀 있는 값을 더한 층으로 이동하게 됩니다. 단, 엘리베이터가 위치해 있는 층과 버튼의 값을 더한 결과가 0보다 작으면 엘리베이터는 움직이지 않습니다. 민수의 세계에서는 0층이 가장 아래층이며 엘리베이터는 현재 민수가 있는 층에 있습니다.

마법의 엘리베이터를 움직이기 위해서 버튼 한 번당 마법의 돌 한 개를 사용하게 됩니다.예를 들어, 16층에 있는 민수가 0층으로 가려면 -1이 적힌 버튼을 6번, -10이 적힌 버튼을 1번 눌러 마법의 돌 7개를 소모하여 0층으로 갈 수 있습니다. 하지만, +1이 적힌 버튼을 4번, -10이 적힌 버튼 2번을 누르면 마법의 돌 6개를 소모하여 0층으로 갈 수 있습니다.

마법의 돌을 아끼기 위해 민수는 항상 최소한의 버튼을 눌러서 이동하려고 합니다. 민수가 어떤 층에서 엘리베이터를 타고 0층으로 내려가는데 필요한 마법의 돌의 최소 개수를 알고 싶습니다. 민수와 마법의 엘리베이터가 있는 층을 나타내는 정수 storey 가 주어졌을 때, 0층으로 가기 위해 필요한 마법의 돌의 최소값을 return 하도록 solution 함수를 완성하세요.


제한사항

  • 1 ≤ storey ≤ 100,000,000

 

2. 솔루션

class Solution {
    fun solution(storey: Int): Int {
        var answer: Int = 0
        var tmp=storey
        while(tmp>0){
            val cur=tmp%10
            val next=tmp%100 - cur
            if((cur==5&&next>=50)||cur>5){
                answer+=10-cur
                tmp+=10
            } 
            else answer+=cur
            tmp/=10
        }
        return answer
    }
}

-현재 체크하는 자릿수의 값이 5미만이면 내려가는게 이득

-5이면 앞자릿수를 체크해서 앞자릿수가 5이상이면 위로 올라가서 나중에 한번에 내려오는 게 이득 

=>그때문에 위로 올라가는 버튼(10-cur) 횟수만큼 answer을 올리고 층수를 앞자릿수에 1을 더해줌 

-5보다 크면 위와 동일한 방식 

 

 

>해시셋 원소추가시 동작 원리

1. 기본 크기: 원소 16개 ,부하계수 0.75

2. 재해시: 크기에 부하계수를 곱한값을 넘는 원소가 들어오게 되면 재해시가 일어남

=>현재 크기의 두배 크기의 해시 테이블로 옮겨감

3. 시간복잡도: 보통 O(1) 최악의 경우 O(n)

4. 결론: 해시셋의 경우 크기를 알고있는것이 아닌이상 과도하게 큰 초기크기를 설정해주지 않는 것이 좋다. 

=>메모리를 과도하게 차지해서 처리속도가 느려짐

 

 

>특강

1. Coroutine EventBus

(1)데이터 전달

1)LiveData

-Data의 변경을 관찰할 수 있는 Data Holder로 안드로이드 생명주기에 따라 업데이트를 진행

=>활성상태(STARTED or RESUMED)일때만 

-데이터 변화시 Observer객체에 변화를 알려주고 Observer의 onChanged()실행

-사용 방법

<1>ViewModel생성

class NameViewModel : ViewModel() {

    // String 타입의 LiveData 생성
    private val _currentName: MutableLiveData<String> = MutableLiveData("홍길동")
    val currentName: LiveData<String> = _currentName
    
    fun updateString(str:String){
    	_currentName.value = str
    }

    // Rest of the ViewModel...

-라이브 데이터는 mutable하지만 받아오는것은  immutable한 이유: Thead-safe하기 위해 방어적 복사 이용

-해당 값을 업데이트 해주기 위한 함수를 따로 만들음

@방어적 복사: 외부에서 참조에 의해 변경을 일으키지 못하도록 불변 객체로 복사해서 해당 객체로만 접근 가능하도록함.

@해당 View Model은 OnCreate()에서 사용해야함

[1]OnCreate()이외의 곳에서 생성하게 되면 onResume()시 pause나 stop에 의해 inactive됐다가 다시 active상태로 돌아올때 LiveData에 대한 코드가 중복호출될 수 있음. =>생명주기에 대한 관리가 필요없다는 라이브데이터의 이점을 잃음

[2]STARTED되자마자  최신의 값을 수신하기 위해선 OnCreate()에서 만들어야 함

 

<2>Observer 설정

-라이브 데이터는 데이터 변경시 활성화된 Observer에게만 업데이트를 제공 

-혹은 비활성화상태의 Observer가 활성화될시 업데이트 수신

-만약 비활성화와 활성화를 반복시 마지막으로 활성화된 이후 값이 변경된 경우에만 업데이트 수신

class MainActivity : AppCompatActivity() {
	
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding=AcitivtyMainBinding.inflate(layoutInflater) 
        setContentView(binding.root)
	    // ViewModel 객체 생성
        val myViewModel: MyViewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        // Obserber 객체 생성
        // 인터페이스에 선언된 함수가 하나이기에 SAM에 해당하여 람다로 표현하면 깔끔합니다.
        val nameObserver = object : Observer<String> {
            override fun onChanged(t: String?) {
                binding.nameView.text = t
            }
        }

        // 위의 코드와 같은 코드
        val nameObserver2 = Observer<String> {
            binding.nameView.text = it
        }

        // LifecycleOwner(이 코드에서는 액티비티에 해당)와 Observer객체를 넣어 LiveData 관찰
        myViewModel.currentName.observe(this, nameObserver)
        
        // Observer + observe 코드를 한꺼번에 선언하는 코드
        myViewModel.currentName.observe(this) {
        	binding.nameView.text = it
        }
        
        //스트링을 업데이트해주는 코드
        binding.btn.setOnClickListener{
        	myViewModel.updateString("aaa")
        }
    }
}

-Observer를 상속받는 오브젝트를 생성후  onChanged를 override해 데이터 변화시 동작을 설정할 수 있다.

-이후 라이브데이터에 observe(context, Observer)를 설정해준다

=>context는 생명주기 감지를 위함

-사용 불가능한 경우: 액티비티가 있는 UI Layer가 아닌 Domain Layer와 상호작용을 하는 경우

=>Activity가 없으므로 라이프사이클에 따른 업데이트가 불가능해져 LiveData사용이 불가능해진다.

=>이 경우 사용하는 것이 Flow

 

@생명주기

@ViewModel()

[1]특징

-ViewModelStoreOwner는 Activity에서 생성되면 액티비티가 완전히 종료될때까지, Fragment에서 생성되면 Fragment가 분리될때까지 메모리에 남아있는다.

=>ViewModel은 ViewModelStoreOwner가 사라질때까지 메모리에 남아있음

=>ViewModelStoreOwner에 의해 ViewModel이 소멸될때 onCleared()호출

-단 라이프사이클은 별개로 타므로 Configuration 변경으로 onCleared()가 호출되지 않는다.

-위와같은 특징에 보통 OnCreate에서 Viewmodel인스턴스를 생성한다.

@Configuration 변경(예를 들어 화면회전)으로 액티비티가 재시작 되더라도 ViewModel은 메모리상에 남아있는다. 

=>Configuration변경과 무관하게 유지되는 NonConfigurationInstances객체는 따로 관리

@여기선 Configuration발생 ViewModelStoreOwner가 ViewModelStore을 가지고 있는다

[2]구현시 권장사항

-ViewModel은 UI구현에 대해 상세를 몰라야 한다. 

-Context나 Resources같은 생명주기에 연관된 API의 참조를 가지고 있으면 안된다. =>UI보다 생명주기가 길기 때문에 메모리 누수 발생 가능.

-Activity나 Fragment는 ViewModel을 가지고 있을때 다른 클래스나 함수로 ViewModel을 넘겨주면 안됨

=>하위 컴포넌트가 ViewModel로직과 데이터에 접근하는 일이 발생하므

[3]ViewModel 요청 프로세스

-1. ViewModelProvider로 ViewModel 인스턴스 요청

-2. ViewModelPRovider는 내부에서 ViewModelStoreOwner를 참조해 ViewModelStores를 가저옴

-3. ViewModelStore에게 이미 생성된 ViewModel인스턴스 요청

-4. 만약 ViewModelStore에 적합한 ViewModel인스턴스가 없으면 Factory로 ViewModel인스턴스 생성

-5. 생성한 ViewModel 인스턴스를 ViewModelStore에 저장하고 만들어진 ViewModel인스턴스 반환

-6. 이미 생성된 ViewModel에 대한 인스턴스 요청 들어올시 1~3과정 반복

 

[4]구현

-0. gradle

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'  // 필수
implementation 'androidx.activity:activity-ktx:1.6.1'              
implementation 'androidx.fragment:fragment-ktx:1.6.1'

-1. 서브 클래스 정의

class MainViewModel : ViewModel() {
    ...
}

-2. Activity나 Fragment에서 OnCreate()에 인스턴스 생성

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel:MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //ViewModel 인스턴스 생성
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    }
}

=>여기서 this는 ViewModelSotreOwner의 Scope. 즉, Owner를 식별하기 위함임.

=>Owner가 다르면 같은 viewModel을 생성하더라도 다른 객체로 인지된다.

 

@Fragment간의 ViewModel공유

-ViewModel을 프래그먼트들의 공통 부모 액티비티를 Context로 받아서 생성하도록 하면Owner가 동일하므로 각각 프래그먼트에서 생성된 viewModel은 동일 객체를 참조하게된다.

class SharedViewModel : ViewModel(){
    val count = MutableLiveData<Int>().apply { value = 0 }
}
class MasterFragment : Fragment(){
    private lateinit var viewModel : SharedViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activity?.run {
            viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                .get(SharedViewModel::class.java)
        }
    }
}
class DetailFragment : Fragment(){
    private lateinit var viewModel : SharedViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activity?.run {
            viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                .get(SharedViewModel::class.java)
        }
    }
}

 

[5]초기화 방식

-1. ViewModelProvider사용: lateinit을 이용해서 onCreate()에서 생성한다.

class MainActivity : AppCompatActivity() {

    private lateinit var mainViewModel : MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        ...
    }
}

-Owner를 인자로 명시할 수 있다.

 

-2. by viewModels() 사용: 내부에서 지연초기화 처리가 되어있으므로 lateinit var을 사용 안 해도 됨

class MainActivity : AppCompatActivity() {

    private val mainViewModel by viewModels<MainViewModel>()
    
}

 

-3. by activityViewModels() 사용: Fragment에서 사용하며 Activity를 Owner로 해 ViewModel을 가져오기에 Fragment들과 Activity간의 ViewModel을 공유할 수 있다.

class FirstFragment : Fragment() {

    private val mainViewModel by activityViewModels<MainViewModel>()

}

 

@by 키워드

-1. 역할: 위임 패턴을 구현하는데 사용

-2. 클래스 위임

interface Inter{
	fun op()
}

class A():Inter{
	override fun op(){
    	println("operation")
    }
}

class Del1(inter:Inter): Inter{
	override fun op(){
    	inter.op()
    }
}

class Del2(inter:Inter): Inter by inter {}

-위와 같은 방식으로 위임 패턴을 구현할 수 있다. 

 

-3. 프로퍼티 위임

class Example {
    var p: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

-Accessor, 즉 get()과 set()을 위임하는것. 위와같이 get과 set을 override시키면 해당 함수를 위임받는다.

-thisRef는 프로퍼티를 소유한 객체, 즉 여기선 Example을 가리킨다.

-property:KProperty<*>는 프로퍼티의 메타데이터를 담고있는 객체로 프로퍼티의 이름이나 타입등의 정보를 얻을 수있음.

 

 

 

 

2)StateFlow

-상태를 표현하기 위한 Flow

-상태를 표현하기에 초깃값이 필요하고 하나의 값만 업데이트 가능

private val _stateFlow = MutableStateFlow("Hello World")
val stateFlow = _stateFlow.asStateFlow()

fun triggerStateFlow() {
    _stateFlow.value = "StateFlow"
}

 

3)SharedFlow

-이벤트 형태로 값을 전달

-emit()시에만 collector에게 값이 전달됨

@Thread-safe하며 외부동기화없이 코루틴에서 안전하게 호출가능

private val _sharedFlow = MutableSharedFlow<String>()
val sharedFlow = _sharedFlow.asSharedFlow()

fun triggerSharedFlow() {
    viewModelScope.launch {
        _sharedFlow.emit("SharedFlow")
    }
}

 

 

4)stateflow와 sharedflow의 동작 차이

lifecycleScope.launchWhenStarted {
    viewModel.~Flow.collectLatest { // ~에 state나 share 가 들어감
        Snackbar.make(
            binding.root,
            it,
            Snackbar.LENGTH_LONG
        ).show()
    }
}

-state: 초기 collect작동시 값을 받아옴. 그 외엔 값이 변하는 경우에만 collect작동

=>여기선 시작할때 스낵바가 뜨고 동일값을 주면 스낵바가 안뜸. 화면전환시 onCreate가 다시 시작되므로 스낵바가뜸

-shared:  emit() 시에만 collect가 작동함.

 

@StateFlow는 livedata와 유사하고 SharedFlow는 broadcastchannel과 유사(sharedFlow는 broadcastchannel을 대체하기 위해 설계, broadcastchannel은 deprecated)

 

@Flow 

1)Flow란: 비동기적 데이터 스트림 =>연속적으로 데이터를 보냄. 즉, Stream으로 연결되어 지속적인 데이터 교환가능

2)Cold Flow:

-하나의 플로우당 하나의 구독자가 연결됨.

=>새로운 구독자가 생기면 새로운 플로우를 만들어줘야 함

-collect는 생산자 코드(flow업데이트 하는 코드)를 트리거함

=>즉, collect시에만 동작: collect하는 구독자가 없으면 동작하지 않음

-flow블록을 사용한 단순한 Flow형태: flow{ ~ }

3)Hot Flow

-하나의 플로우로 여러 구독자가 공유

-SharedFlow와 StateFlow가 여기 속함

-collect여부와 상관없이 항상 동작함.

=>즉, 구독자와 독립적으로 존재하고 동작함

-생산자가 데이터를 emit(방출)하면 구독자가 해당 data를 collect(수집)한다.

 

 

(2)Coroutine EventBus

class EventBus {
    private val _events = MutableSharedFlow<Event>() // private mutable shared flow
    val events = _events.asSharedFlow() // publicly exposed as read-only shared flow

    suspend fun produceEvent(event: Event) {
        _events.emit(event) // suspends until all subscribers receive it
    }
}

-SharedFlow를 사용

-emit하여 Event를 여러 구독자에게 전송한다.

 

 

 

2. Repository Pattern

1)Singleton Pattern

companion object {
    private var INSTANCE : DataSource? = null
    fun getDataSource() : DataSource {
        return synchronized(DataSource::class) {
            val newInstance = INSTANCE ?: DataSource()
            INSTANCE = newInstance
            newInstance
        }
    }
}

-synchronized(자원): 해당 자원에 한번에 한 쓰레드만 접근할 수 있도록 함. 이후 블록 내 연산 결과를 반환

-인스턴스가 있는지 체크후 있으면 이미 생성된 인스턴스를 반환하고 없으면 생성 후 반환

 

2)Repository Pattern

<1> Repository Pattern이란?

-데이터의 출처(network, local(cache), data(room, SharedPreference, DataStore), file등)에 관계없이 동일 인터페이스로 데이터에 접근할 수 있도록 하는 패턴

<2>목적

-Data Layer를 캡슐화 시키는 것

<3>방법

-데이터가 있는 여러 저장소를 추상화 시켜 중앙집중 처리방식으로 구현

-Repository와 DataSource 사이의 의존성을 없앤다.

=>dataSource변경으로 repository에 영향이 없음

<3>이점

[1]Presentation Layer에서 Data Layer에 직접 접근을 안하므로 새로운 Data추가가 쉬움

[2]Presentation Layer에서 reopsitory에 데이터만 요청하면 되므로 일관된 인터페이스로 데이터 요청가능

 

<4>구조

 

 

@명명법

-1. Api response로 받은 데이터는 ~Response

-2. 로컬은 ~Entity

-3. 원격으로 받은것은  RemoteDataSource

-4. 캐시 데이터는 CacheDataSource

-5. Repository는 ~Repository

-6. 인터페이스에 대한 구현체는 인터페이스이름+Imple

 

@ ' :: ' 참조연산자

<1>함수 참조: 기존 함수를 변수에 저장하거나 다른 함수의 인자로 전달

fun greet(name: String) {
    println("Hello, $name!")
}

fun main() {
    val greetFunction = ::greet // 함수 참조
    greetFunction("Alice") // Hello, Alice!

    val names = listOf("Alice", "Bob", "Charlie")
    names.forEach(::greet) // 각 이름에 대해 greet 함수를 호출
}

 

<2>프로퍼티 참조: 클래스의 프로퍼티를 참조할때 사용

=>이때 생성된것이 아닌 클래스의 자체에 참조하는 것이므로 사용할때 어떤 인스턴스에 대한 프로퍼티를 가져올지 get으로 클래스 객체를 전달해 주어야한다.

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 30)
    val nameProperty = Person::name
    val ageProperty = Person::age

    println(nameProperty.get(person)) // Alice
    println(ageProperty.get(person)) // 30
}

 

<3>생성자 참조: 변수를 클래스 생성자 처럼 사용할 수 있게됨

class Person(val name: String, val age: Int)

fun main() {
    val createPerson = ::Person // 생성자 참조
    val person = createPerson("Alice", 30)

    println(person.name) // Alice
    println(person.age) // 30
}

 

@생성자 참조 예시

class Person(val name: String, val age: Int)

fun createPerson(factory: (String, Int) -> Person): Person {
    return factory("Alice", 30)
}

fun main() {
    val person = createPerson(::Person) // 생성자 참조를 사용하여 Person 객체 생성
    println(person.name) // Alice
    println(person.age) // 30
}

-생성자가 (String, Int)  -> Person형태의 함수이므로 createPerson에 생성자 참조로 값을 넣을 수 있음

-고차함수에서 위와 같이 사용 가능

 

@ ::class.java 

-대응하는 자바 클래스를 얻기 위함

-Java 리플렉션 API를 사용하거나 JAVA라이브러리의 메소드가 클래스 객체를 필요로 하는 경우에 주로 사

@Java리플렉션 API: 클래스, 인터페이스 ,필드 , 메소드등의 정보를 동적으로 검사 및 조작가능하게 하는 기능. 

 

@Enum class

<1>사용방법 

-기본: Class명.인스턴스명 으로 초기화 가능하며 name과 ordinal로 이름과 순서를 알 수 있다. 

enum class Fruit{
    GRAPE,
    APPLE,
    ORANGE,
    MANGE
}

fun main{
    val fruit = Fruit.GRAPE
    fruit.name // "GRAPE"
    fruit.ordinal // 0
}

 

 

-생성자 정의: 생성자에서 정의된 부분이 해당 인스턴스의 프로퍼티처럼 들어간다. 

enum class Fruit(val price:Int,val tag:String){
    GRAPE(3000,"grape"),
    APPLE(2000,"apple"),
    ORANGE(4000,"orange"),
    MANGO(10000,"mango")
}

fun main(){
	val fruit = Fruit.GRAPE
	fruit.price // 3000
	fruit.tag // "grape"
}

 

 

 

-일반클래스처럼 사용: 초기화에 사용된 인스턴스를 이용하여 함수나 프로퍼티를 사용할 수 있다. 단 이때 마지막 인스턴스에는 ;를 붙여줘야 한다.

enum class Fruit(val price:Int,val tag:String){
    GRAPE(3000,"grape"),
    APPLE(2000,"apple"),
    ORANGE(4000,"orange"),
    MANGO(10000,"mango");
    
    fun printName():String = "fruit name is $name"
    val tenPiecePrice
    	get() = price*10
}

fun main(){
	val fruit = Fruit.GRAPE
	fruit.printName() // "fruit name is GRAPE"
	fruit.tenPiecePrice // 30000
}

 

-추상메소드나 인터페이스 사용: 이때 각 인스턴스에대해 메소드를 오버라이드 해줘야 한다.

@단 이때 인터페이스는 상속되나 일반클래스, 혹은 다른  enum class로부터 상속은 불가능하다.

//추상 메소드 구현
enum class Fruit(val price:Int,val tag:String){
	GRAPE(3000,"grape"){
    	override fun printName():String = "fruit name is grape"
        },
    APPLE(2000,"apple"){
    	override fun printName():String = "fruit name is apple"
        },
    ORANGE(4000,"orange"){
    	override fun printName():String = "fruit name is orange"
        },
    MANGO(10000,"mango"){
    	override fun printName():String = "fruit name is mango"
        };
    
    abstract fun printName():String
}

//인터페이스 상속
interface Printable(){
	fun printName():String
}

enum class Fruit(val price:Int,val tag:String):Printable{
	GRAPE(3000,"grape"){
    	override fun printName():String = "fruit name is grape"
        },
    APPLE(2000,"apple"){
    	override fun printName():String = "fruit name is apple"
        },
    ORANGE(4000,"orange"){
    	override fun printName():String = "fruit name is orange"
        },
    MANGO(10000,"mango"){
    	override fun printName():String = "fruit name is mango"
        };
}

 

-이외의 메소드들

[1]valueOf(value:String): value에 해당하는 이름을 가진 인스턴스를 반환

[2]values(): 해당 Enum클래스에 포함된 값들을 Array로 반환(반환값은 각 인스턴스의 name)

 

<2>사용예시

[1]when()문: when에서 enumClass의 인스턴스를 받을때 모든 enumclass 인스턴스에 대해 적으면 else를 사용 안해도 됨.

val value:Fruit = Fruit.GRAPE
when(value){
    Fruit.GRAPE-> {...}
    Fruit.APPLE-> {...}
    Fruit.MANGO-> {...}
    Fruit.ORANGE-> {...}
}

=>보통 UI유지보수를 위해 State패턴을 이용

=>enum class가 객체상태를 정의하는데 사용

enum class UiState(){
	Success,
    Fail,
    Loading
}

fun handleState(state:UiState){
	when(state){
    	Success-> { ...}
        Fail -> { ...}
        Loading -> { ...}
    }
}

 

 

>스크럼

1. ListAdapter에서는 notifyDataSetChanged 대신 submitList()

=>라이브 데이터로 연결해서 데이터 변경시마다 submitList()로 새로 넣어