kotlin 코틀린데이터클래스예제

코틀린데이터클래스예제

DataClass.kt
package my.demo

/**
 * 코틀린의 최상위 클래스인 `Any`
 */

/**
 * 데이터 클래스란!?
 *
 * 클래스가 메서드 없이 프로퍼티만을 가질 때 사용할 수 있는 클래스이다.
 *
 * 코틀린 개발자들은 데이터 클래스의 모든 프로퍼티를 읽기 전용(`val` 타입)으로 만들어
 * 불변 클래스(`Immutable Class`)로 사용할 것은 권한다. HashMap 등의 컨테이너에 데이터 클래스를 사용할 경우 `불변성`이 필수 요소이기
 * 때문이다. = copy()
 */

data class Sample(val name: String, val postalCode: Int)

/**
 * `data`를 붙이는 이유는, 코틀린에서 데이터 클래스에게 제공하는 혜택들이 있기 때문이다.
 * 바로 **필수 메서드 자동 생성** 기능이다.
 *
 * - toString()
 *  - > 인스턴스를 문자열로 변환해서 반환하는 역할. 코틀린에서는 클래스명과 해시코드를 조합해서 만들어진 문자열을 반환
 * - equals()
 *  - > 자바에서는 `==` 연산 대신 `equals()` 메서드를 호출하여 두 객체의 값이 동일한지 확인하도록 한다.
 *  - > 하지만 코틀린에서는 `==` 연산과 `equals()` 연산이 같다.
 * - hashCode()
 * - copy()
 *  - > copy() 메서드는 객체를 복사하면서 일부 프로퍼티의 값을 변경할 수 있도록 해준다. 원본 객체는 복사된 객체의 수정 여부에 전혀 영향을 받지 않는다.
 */

class Client(val name: String, val posatalCode: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is my.demo.Client) return false

        /**
         * `is` 연산자는 특별한 기능
         *
         * 참조 객체가 연산에 사용된 타입과 일치할 경우 연산에 사용된 참조 객체를 해당 타입으로 자동 변환 (Smart Cast)
         */
        return name == other.name && posatalCode == other.posatalCode
    }

    /**
     * JVM 언어에는 `equals()`가 true를 반환하는 두 객체는 반드시 hashCode()를 반환해야 한다 라는 제약이 있다
     * 따라서 equals()를 오버라이드 할 경우 반드시 hashCode() 메서드도 함께 오버라이드 해야한다.
     *
     * data class는 모든걸 자동으로 완성해준다.
     */
    override fun hashCode(): Int = name.hashCode() * 31 + posatalCode
}

data class Client2(val name: String, val postalCode: Int)

fun main(argv: Array<String>) : Unit {
    val client = Client("Monguse", 1234)
    val client2 = Client("Monguse", 1234)
    println(client == client2)
    println(client.equals(client2))

    /**
     * HashSet 객체에서 값이 Client 객체를 찾지 못한다
     * HashSetOf(); 클래스는 각 객체의 해시코드를(Hash Code)를 우선적으로 검사하기 때문이다.
     */
    val clientSet: HashSet<Client> = hashSetOf<Client>(Client("Monguse", 1234))
    println(clientSet.contains(Client("Monguse", 1234)))

    /**
     * copy() 예제
     */
    val client3 = Client2("Monguse", 1234)
    val client4 = client3.copy(postalCode = 1236)
    println(client3)
    println(client4)
}

kotlin 코틀린인터페이스와클래스그리고접근제어자예시

코틀린인터페이스와클래스그리고접근제어자예시

ClassInterface.kt
package my.demo

interface User {
    val nickname: String
}

class PrivateUser2(override val nickname: String): User

class PrivateUser(override val nickname: String): User

class SubscribingUser(val email: String): User {
    override val nickname: String = getId()

    fun getId(): String = email.substringBefore('@')
}

class SubscribingUser2(val email: String): User {
    override val nickname: String = getId()
    fun getId(): String = email.substringBefore("@")
}

class Rectangle {
    var height: Int = 0
    get() = field
    set(value) {
        field = value * 10
    }

    var width: Int = 0
    get() = field
    set(value) {
        field = value
    }
}

class Rectangle2(var height: Int, var width: Int) {
    val isSquare: Boolean

    init {
        isSquare = if (height == width) true else false
    }
}

class Rectangle3(var height: Int, var width: Int) {
    // height와 width가 var 타입임으로 위에서와 다르게 변경될 수 있음
    // 커스텀 접근자 라는 방식을 제공
    val isSquare: Boolean
    get() = height == width
}

/**
 * 접근자의 가기성 변경자
 * http://blog.naver.com/PostView.nhn?blogId=yuyyulee&logNo=221216709294&parentCategoryNo=&categoryNo=22&viewDate=&isShowPopularPosts=true&from=search
 */
class LengthCounter {
    // getter는 상관없지만 setter는 접근되면 안되어야 하기 때문에
    var counter: Int = 0
    private set

    fun addWord(word: String) {
        counter += word.length
    }
}

fun main() {
    val rect: Rectangle = Rectangle()
    rect.height = 10
    rect.width = 10
    println("Rect, ${rect.height}, ${rect.width}")

    println(Rectangle2(10, 10).isSquare)

    val counter: LengthCounter = LengthCounter()
    counter.addWord("Hello, World")
    println(counter.counter)
}

kotlin ActionQueue

ActionQueue.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

typealias Action = () -> Unit

private const val TAG = "ActionQueue"

@ExperimentalCoroutinesApi
@Suppress("MemberVisibilityCanBePrivate")
/** A buffered [Action] queue that collects processable [Action] lambda tasks for a processor (eg. a coroutine).
 * @param scope The scope which will be used to enqueue the [Action]s (eg. Activity scope). Fallback default is [GlobalScope].
 * @param onClosed A callback to be invoked once the queue is closed. */
class ActionQueue(
	private val scope: CoroutineScope = GlobalScope,
	private val onClosed: Action? = null
) {
	private var channel: Channel<Action> = createChannel()
	private var currentJob: Job? = null

	val hasQueued
		get() = !channel.isEmpty
			&& !channel.isClosedForSend
			&& !channel.isClosedForReceive

	/** Sends the [Action] to the queue for processing.
	 * @return true if enqueued the [Action] (because the channel was open). */
	@Synchronized
	fun add(action: Action): Boolean {
		val channelIsOpen = !channel.isClosedForSend
		runBlocking {
			currentJob = scope.launch {
				if (channelIsOpen) {
					channel.send(action)
				}
			}
			currentJob?.join()
			currentJob = null
		}
		return channelIsOpen
	}

	/** @return enqueued [Action] if there is any. Returns null if there is no [Action] to process or the queue was closed. */
	fun get(): Action? = channel.poll()

	/** @return enqueued [Action] if there is any, else empty [Action]. */
	fun safeGet(): Action = get() ?: {}

	/** Closes the queue, cancelling any ongoing [add] operation and removing all pending [Action]s. */
	fun close() {
		currentJob?.cancel()
		channel.cancel()
	}

	/** Opens the queue. This invokes [close], removing all pending [Action]s in the queue. */
	fun open() {
		close()
		channel = createChannel()
	}

	private fun createChannel() = Channel<Action>(Channel.UNLIMITED).also { newChannel ->
		onClosed?.let {
			newChannel.invokeOnClose { it() }
		}
	}
}
ActionQueueTest.kt
import kotlinx.coroutines.*
import <...>.ActionQueue
import org.junit.Test

class ActionQueueTest {
  @ExperimentalCoroutinesApi
  @Test
	fun main() {
		println("Setup")
		val queue = ActionQueue(onClosed = { println("onClosed was called") }).also {
			it.add { println("A") }
			it.add { println("B") } // ignored by [early close | reopen]
			it.add { println("C") } // ignored by [early close | reopen]
		}

		runBlocking {
			GlobalScope.launch {
				println("Start")
//				while (queue.hasQueued) {
				if (queue.hasQueued) {
					queue.safeGet()()
//					queue.close()
				}

				queue.apply {
					open()
					add { println("D") }
					add { println("E") }
					add { println("F") }
				}

				while (queue.hasQueued) {
					queue.safeGet()()
				}

				queue.close()
				println("Done")
			}.join()
			println("Exit")
		}
	}
}

kotlin 数据源工厂

数据源工厂

DictionaryAllWordsDataSourceFactory.kt
class DictionaryAllWordsDataSourceFactory(
    private val repository: Repository,
    private val networkState: MutableLiveData<Event<NetworkMessage>>
) :
    DataSource.Factory<Int, Meaning>() {
    override fun create(): DataSource<Int, Meaning> {
        return DictionaryAllWordsDataSource(repository, networkState)
    }
}

kotlin 字典所有单词数据源

字典所有单词数据源

DictionaryAllWordsDataSource.kt
class DictionaryAllWordsDataSource(//...) :
    PositionalDataSource<Meaning>() {
    //...
    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Meaning>) {
        //...
              val startPage = (params.startPosition / PAGE_SIZE) + 1
              if (totalPages >= startPage) {
                  val words = getWordsFromPage(startPage, startPage)
                  if (words.isEmpty()) throw NoDataException()
                  callback.onResult(words)
              }
        //...
    }

    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Meaning>) {
        //...
              val total = getTotalPages()
              val endPage = min((params.requestedLoadSize / PAGE_SIZE), 3)
              val startPage = 1
              val words = getWordsFromPage(startPage, endPage)
              if (words.isEmpty()) throw NoDataException()
              callback.onResult(words, 0, (total.end * PAGE_SIZE))
              totalPages = total.end
        //...
    }

    fun getWordsFromPage(startPage: Int, endPage: Int): MutableList<Meaning> {
        val words = mutableListOf<Meaning>()
        for (index in startPage..endPage) {
            val dictionary = getWordsFromPage(index)
            dictionary.words.forEach { words.add(it) }
        }
        //...
        return words
    }

    private fun getWordsFromPage(pageNo: Int): Dictionary {
        //...
        return repository.getPageInDictionary(pageNo)
    }

    private fun getTotalPages(): Page {
        //...
        return repository.getNumberOfPagesInDictionary()
    }
}

kotlin 查看模型主要活动

查看模型主要活动

MainActivityViewModel.kt
class MainActivityViewModel(//...) :
    ViewModel() {
    //...
    val words: MediatorLiveData<PagedList<Meaning>> = MediatorLiveData()
    //...
    
    private fun addAllWordsAndRemoveSearch() {
        removeSearchObserver()
        if (!mapOfLiveData.containsKey(ALL)) {
            mapOfLiveData[ALL] = LivePagedListBuilder(dataSourceFactory, config).build()
            words.addSource(mapOfLiveData[ALL]!!) {
                words.value = it
            }
        }
    }
    
    private fun addSearchAndRemoveAllWords() {
        removeAllWordsObserver()
        if (!mapOfLiveData.containsKey(SEARCH)) {
            mapOfLiveData[SEARCH] = LivePagedListBuilder(searchDataSourceFactory, config).build()
            words.addSource(mapOfLiveData[SEARCH]!!) {
                words.value = it
            }
        }
    }
    //...
}

kotlin 单词列表片段

单词列表片段

WordListFragment.kt
class WordListFragment : Fragment() {
    //...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mainActivityViewModel.words.observe(this, Observer {
            meaningAdapter.submitList(it)
        })
        //...
    }
    //...
}

kotlin 意义适配器

意义适配器

MeaningAdapter.kt
class MeaningAdapter : PagedListAdapter<Meaning, RecyclerView.ViewHolder>(REPO_COMPARATOR) {
    //..
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): RecyclerView.ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.fragment_meaning, parent, false)
        return DictionaryViewHolder(view)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val dictionaryItem = getItem(position)
        if (dictionaryItem != null) {
            //Perform binding of data to view here
        }
    }

    inner class DictionaryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        //..
    }

    companion object {
        private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Meaning>() {
            override fun areItemsTheSame(oldItem: Meaning, newItem: Meaning): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Meaning, newItem: Meaning): Boolean =
                oldItem == newItem
        }
    }
}

kotlin 字典API

字典API

DictionaryApi.kt
interface DictionaryApi {
    @GET("/api/v1/dictionary/pages")
    fun getNumberOfPagesInDictionary(): Call<Page>

    @GET("/api/v1/dictionary/page/{pageNo}")
    fun getWordsInDictionaryPage(@Path("pageNo") pageNo: Int): Call<Dictionary>

    @GET("/api/v1/dictionary")
    fun searchForWordsInDictionary(@Query("pageNo") pageNo: Int, @Query("word") word: String): Call<SearchResult>
}

kotlin 字典模块 - DI

字典模块 - DI

DictionaryModule.kt
private const val BASE_URL = "https://dictionary-sample.azurewebsites.net/"
const val PAGE_SIZE = 20

val dictionaryModule = module {
    single { createApiClient() }
    single { DictionaryRepository(get()) as Repository }
    single { PagedList.Config.Builder().setPageSize(PAGE_SIZE).setEnablePlaceholders(false).build() }
    single { MutableLiveData<Event<NetworkState>>() }
    factory { DictionaryAllWordsDataSourceFactory(get(), get()) }
    factory { DictionarySearchWordsDataSourceFactory(get(), get()) }
    viewModel { MainActivityViewModel(get(), get(), get(), get()) }
}


private fun createApiClient(): DictionaryApi {
    val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
    return retrofit.create(DictionaryApi::class.java)
}