Kotlin TabLayout에 CustomView 적용하는 예제

|

item_selected_tab.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="horizontal">

  <TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="10dp"
    android:layout_gravity="center"
    android:maxWidth="@dimen/tab_max_width"
    android:minWidth="20dp"
    android:singleLine="true"
    android:textColor="@color/textColorPrimary"
    android:textSize="16sp"/>

  <Button
    android:id="@+id/remove_button"
    android:layout_width="24dp"
    android:layout_height="24dp"
    android:layout_gravity="center"
    android:background="@drawable/btn_remove"/>
  
</LinearLayout>


MainActivity.kt

class MainActivity : AppCompatActivity(), TabLayout.OnTabSelectedListener {

    private val tabLayout: TabLayout by lazy {
        findViewById<TabLayout>(R.id.tabs)
    }

    private val adapter: CategoryPagerAdapter by lazy {
        CategoryPagerAdapter(supportFragmentManager)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar = findViewById<Toolbar>(R.id.toolbar)

        setSupportActionBar(toolbar)

        viewpager.adapter = adapter
        tabLayout.setupWithViewPager(viewpager)
        tabLayout.addOnTabSelectedListener(this)

        add_tab.setOnClickListener {
            showEditCategoryNameDialog(null)
        }
    }

    override fun onTabReselected(tab: TabLayout.Tab?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onTabUnselected(tab: TabLayout.Tab?) {
        tab?.customView = null
    }

    override fun onTabSelected(tab: TabLayout.Tab?) {
        val view = LayoutInflater.from(applicationContext).inflate(R.layout.item_selected_tab, null)
        todoInsertFragment.currentTabPosition = tab?.position?.toLong() ?: 0

        val tabInfo = adapter.getTabInfo(tab?.position ?: 0)
        view.title.text = tabInfo?.name

        view.title.setOnLongClickListener(object : View.OnLongClickListener {
            override fun onLongClick(v: View?): Boolean {
                showEditCategoryNameDialog(tabInfo)
                return true
            }

        })

        view.remove_button.setOnClickListener {
            if (tabInfo != null) showDeleteCategoryDialog(tabInfo)
        }

        tab?.customView = view
    }

    private fun showEditCategoryNameDialog(tabInfo: TabInfo?) {
        val dialogLayout = LayoutInflater.from(applicationContext).inflate(R.layout.dialog_edit_name, null)
        val editText = dialogLayout.findViewById<EditText>(R.id.editText)
        editText.setText(tabInfo?.name ?: "")

        AlertDialog.Builder(this)
                .setTitle("이름을 입력하세요.")
                .setView(dialogLayout)
                .setPositiveButton("OK") { _, _ ->
                    if (tabInfo != null) {
                        ModelManager.instance.updateTabInfo(tabInfo.id, editText.text.toString())
                    } else {
                        ModelManager.instance.addTabInfo(editText.text.toString())
                        selectLastTab()
                    }
                }
                .show()
    }

    private fun showDeleteCategoryDialog(tabInfo: TabInfo) {
        AlertDialog.Builder(this)
                .setTitle("Category 삭제")
                .setMessage("${tabInfo.name} 항목을 정말 삭제하시겠습니까?")
                .setPositiveButton("확인") { _, _ ->
                    ModelManager.instance.deleteTabInfo(tabInfo.id)
                    selectNextTab(tabLayout.selectedTabPosition)
                }
                .show()
    }
}

lateinit 및 lazy()

|

lateinit

lateinit는 다음과 같은 특징이 있습니다.

  • var 변수에만 사용 가능
  • Non-null 데이터 타입에만 가능
  • primitive type에는 사용할 수 없음
  • 클래스 생성자에서 사용 못함
  • 로컬 변수로 사용 못함

그럼, 어떤 경우에 사용해야 할까요? 코틀린은 NULL 체크에 대한 검사가 엄격하기 때문에

class Sample {
    private var name: String
}

와 같은 코드는 Property must be initialized or be abstract 오류가 발생합니다.

실제로 초기화 때 값을 지정해주고 코드를 작성하는 것이 더욱 좋지만, 그렇지 못한 경우가 많이 발생합니다. 그럴 때는 아래와 같이 lateinit 키워드를 변수 앞에 적어주면 변수 초기화를 나중에 할 수 있게 됩니다.

class Sample {
    private lateinit var name: String
}


lazy

lazy() 함수는 lateinint와 비슷한 역할을 하는데 다음과 같은 특징이 있습니다.

  • val 변수에만 사용 가능
  • primitive type에도 사용 가능
  • Non-null, Nullable 둘 다 사용 가능
  • 클래스 생성자에서 사용 못함
  • 로컬 변수로 사용 가능
private val lazyExample: String by lazy {
    println("lazyExample - init()")

    "[lazy] lazyExample is initialized."
}

fun test() {
    println("Start...")

    println("1st : $lazyExample")
    println("2nd : $lazyExample")
    println("3rd : $lazyExample")
}

실행 결과는 다음과 같습니다.

Start...
lazyExample - init()
1st : [lazy] lazyExample is initialized.
2nd : [lazy] lazyExample is initialized.
3rd : [lazy] lazyExample is initialized.

위 결과와 같이, 처음 호출될 시점에 초기화를 한 번 하며 그 이후로는 그 결과값만 사용하는 것을 확인할 수 있습니다.

람다(Lambda)

|

람다 함수는 다음과 같은 형태로 사용할 수 있습니다.

(매개 변수) -> { TODO("...) }

실제 코드로 예를 들면 다음과 같습니다.

button.setOnClickListener((v) -> {
    TODO("onClicked !!")
})

Kotlin Material 스타일 TabLayout 예제

|

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#F48FB1</color>
  <color name="colorPrimaryDark">#F48FB1</color>
  <color name="textColorPrimary">#EC407A</color>
  <color name="windowBackground">#FFFFFF</color>
  <color name="navigationBarColor">#000000</color>
  <color name="colorAccent">#c8e8ff</color>
</resources>


dimens.xml

<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="tab_max_width">264dp</dimen>
  <dimen name="tab_padding_bottom">16dp</dimen>
  <dimen name="tab_label">14sp</dimen>
  <dimen name="custom_tab_layout_height">72dp</dimen>
</resources>


styles.xml

<resources>
  <!-- Base application theme. -->
  <style name="AppTheme" parent="RanMaterialTheme.Base">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>

  <style name="RanMaterialTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="windowNoTitle">true</item>
    <item name="windowActionBar">false</item>
  </style>
</resources>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:layout_scrollFlags="scroll|enterAlways"
      app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

      <Button
        android:id="@+id/add_tab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_gravity="end"
        android:text="+"/>

      <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toStartOf="@id/add_tab"
        app:tabGravity="fill"
        app:tabMode="scrollable"/>

    </RelativeLayout>

  </android.support.design.widget.AppBarLayout>
  
  <android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</android.support.design.widget.CoordinatorLayout>


MainActivity.kt

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v7.widget.Toolbar
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val toolbar = findViewById<Toolbar>(R.id.toolbar)

        setSupportActionBar(toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        val list = ArrayList<String>()
        list.add("Year")
        list.add("Month")
        list.add("Week")
        list.add("Day")

        val adapter = TodoListPagerAdapter(supportFragmentManager)
        adapter.setList(list)
        viewpager.adapter = adapter

        val tabLayout = findViewById<TabLayout>(R.id.tabs)
        tabLayout.setupWithViewPager(viewpager)

        add_tab.setOnClickListener {
            list.add("Added")
            adapter.setList(list)
        }
    }
}

타입 체크 및 캐스팅, 비교 연산

|

is와 as

코틀린에서는 타입 체크할 때 is 키워드를 사용하며, Java에서의 instanceof와 동일한 역할을 합니다. 그리고 타입 캐스팅시에는 as 키워드를 사용합니다.

fun setLayoutParam(view: View) {
    if (view is LinearLayout) {
        var param = view.layoutParams as LinearLayout.LayoutParams
        param.gravity = Gravitiy.CENTER
        view.layoutParams = param
    } else if (view is RelativeLayout) {
        var param = view.layoutParams as RelativeLayout.LayoutParams
        param.addRule(RelativeLayout.ALIGN_PARENT_CENTER)
        view.layoutParams = param
    }
}


NullPoint Exception 방지

코틀린에서는 ? 기호를 이용해서 해당 변수가 null 값을 가질 수 있음을 알려주며, 기본적으로는 non null 상태입니다.

var str:String? = "hello"
str = null

그리고 변수를 사용할 때도 변수뒤에 ? 기호를 붙여서 Nullpoint Exception 처리를 할 수 있습니다.

fun test() {
    var str: String? = "hello"
    str = null
    var length: Int? = str?.length
}

위의 예제에서 만약 str 변수가 null이 되면 str?.lengthnull을 리턴하기 때문에 length 변수가 null이 됩니다. 즉, 위험할 수 있는 코드이기 때문에 아래와 같이 표현할 수 있습니다.

fun test() {
    var str: String? = "hello"
    str = null
    var length: Int = str?.length ?: 0
}

위와 같이 코드를 작성하면 str?.lengthnull일 경우 숫자 0 으로 리턴하기 때문에 length 변수는 항상 값을 가질 수 있습니다.

그리고 !! 기호를 사용하게 되면 명시적으로 변수에 절대 null을 참조할 수 없다는 것을 지정할 수 있습니다.

fun test() {
    var str: String? = "hello"
    var length: Int = str!!.length
}


== 와 ===

=====는 비교 연산자로 ==는 Java에서 사용하던 ==와 동일한 역할을 합니다. 그리고 내부적으로 NULL 체크를 하기 때문에 좀 더 간략하게 코드를 작성할 수 있습니다. 다만, a == b에서 ab 모두 null인 경우 true가 되기 때문에 주의할 필요는 있습니다.

===의 경우는 두 변수가 정말 똑같은 주소값을 갖고 있는지 판단하며 Java에서의 equals()와 동일합니다.