Giter Site home page Giter Site logo

booster-android's Introduction

๐Ÿš€We are BOOSTER ANDROID๐Ÿš€

BOOSTER - ๋น ๋ฅด๊ฒŒ ์ถœ๋ ฅํ•˜๋Š” ํŽธ๋ฆฌํ•จ

SOPT 26๊ธฐ Appjam '๋ถ€์Šคํ„ฐ'

Faster / Easier / Together

ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ 2020.06 ~ ์ง„ํ–‰์ค‘



๐Ÿš€ Project Purpose

  • ๋น ๋ฅด๊ฒŒ ์ถœ๋ ฅํ•˜๋Š” ํŽธ๋ฆฌํ•จ

  • ๋Œ€ํ•™์ƒ์„ ์œ„ํ•œ ๋น ๋ฅด๊ณ  ๊ฐ„ํŽธํ•œ ์ธ์‡„ ์„œ๋น„์Šค

  • ์‚ฌ์ „ ์ธ์‡„ ์ฃผ๋ฌธ ์„œ๋น„์Šค

๐Ÿ”ง Tools

  • Android Studio

  • Zeplin

  • Postman

๐Ÿ“Œ Code Convention

  • ๋ณ€์ˆ˜๋ช…์€ ๊ธฐ๋ณธ์ ์œผ๋กœ camelCase๋กœ ์ž‘์„ฑ

  • ID NAMING : ๋ทฐ์ด๋ฆ„_์œ„์ ฏ์ค„์ธ๋ง_๊ธฐ๋Šฅ์ด๋ฆ„

  • ์ปค๋ฐ‹ํ•˜๊ธฐ ์ „์— reformat code๋ฅผ ์‹คํ–‰์‹œ์ผœ์„œ ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•ด์ค€๋‹ค.

๐ŸŒž Github Branching

  • ๊ฐœ์ธ Branch๋ฅผ ์ด๋ฆ„์œผ๋กœ ๋งŒ๋“  ๋’ค ๊ฐœ๋ฐœํ•œ๋‹ค.

  • ๊ฐœ์ธ Branch์—์„œ develop branch๋กœ PR์„ ๋ณด๋‚ธ๋‹ค.

  • ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์™„๋ฒฝํ•˜๋ฉด์„œ, ๋ชจ๋“  ํŒ€์›์ด ๋™์˜ํ•  ๋•Œ Master ๋ธŒ๋žœ์น˜๋กœ PR์„ ๋ณด๋‚ธ๋‹ค.

๐Ÿ›  Technology Stack

  • Minimum SDK version 24

  • Language : Kotlin

  • Retrofit : REST API Library

  • Gson : Json Data process Library

  • Glide : Image Process Library


โš™๏ธ Project Structure

  • application,bindingadapter,data,listener,ui,util๋กœ ๋Œ€๋ถ„๋ฅ˜
  • ํŒจํ‚ค์ง€ ๋‚ด๋ถ€์— ์„ธ๋ถ€ ํŒจํ‚ค์ง€๋กœ ๋‚˜๋ˆ  ์ •๋ฆฌ

1์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 11 29 113

๐Ÿ”‘ Dependency

//์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
implementation "androidx.appcompat:appcompat:1.1.0"
//LiveData๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"

// CardView Library
implementation 'androidx.cardview:cardview:1.0.0'

//Lottie Library
implementation 'com.airbnb.android:lottie:3.4.1'

// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin extended & experimental features
implementation "org.koin:koin-core-ext:$koin_version"
// Koin for Unit tests
testImplementation "org.koin:koin-test:$koin_version"
// Koin for Java developers
implementation "org.koin:koin-java:$koin_version"
    
// Koin for Android
implementation "org.koin:koin-android:$koin_version"
// Koin Android Scope features
implementation "org.koin:koin-android-scope:$koin_version"
// Koin Android ViewModel features
implementation "org.koin:koin-android-viewmodel:$koin_version"
// Koin Android Experimental features
implementation "org.koin:koin-android-ext:$koin_version"

// Koin AndroidX Scope features
implementation "org.koin:koin-androidx-scope:$koin_version"
// Koin AndroidX ViewModel features
implementation "org.koin:koin-androidx-viewmodel:$koin_version"
// Koin AndroidX Experimental features
implementation "org.koin:koin-androidx-ext:$koin_version"

//ํŒŒ์ผํ”ฝ์ปค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
implementation 'com.droidninja:filepicker:2.2.4'

//Material Components
implementation 'com.google.android.material:material:1.3.0-alpha01'
//TedPermission ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
implementation 'gun0912.ted:tedpermission:2.2.3'
//coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4"
//Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01"

//Naver map
implementation "com.naver.maps:map-sdk:3.8.0"

//coordinator layout
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"

//pdfium
implementation 'com.github.barteksc:pdfium-android:1.9.0'

๐Ÿ–• ์ฃผ์š”๊ธฐ๋Šฅ

0. ConstraintLayout ์‚ฌ์šฉํ•˜๊ธฐ

  • ๋Œ€๋ถ€๋ถ„์˜ ๋ ˆ์ด์•„์›ƒ์„ ConstraintLayout์œผ๋กœ ๊ตฌ์„ฑ
  • chain ๊ณผ match_parent ๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ๋ทฐ ๊ตฌ์„ฑ

์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23

item_order_condition.xml

<androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/item_order_prodress_cl_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="26dp"
            android:layout_marginEnd="26dp"
            android:layout_marginTop="24dp"
            app:layout_constraintTop_toBottomOf="@id/item_order_progress_tv_list"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

            <View
                android:layout_width="match_parent"
                android:layout_height="3dp"
                android:background="@drawable/bg_progress_receipt"
                setGradation="@{conditionRes.status}"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"/>

            <ImageView
                android:id="@+id/item_order_condition_iv_cicle_1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                changeCircleF="@{conditionRes.status}"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

            ...

        </androidx.constraintlayout.widget.ConstraintLayout>
  • Constraint Chain์„ ์ด์šฉํ•ด ๊ฐ€์šด๋ฐ ์ •๋ ฌ๋กœ ๋ฐฐ์น˜

์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23

activity_store_file_option.kt

<androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/option4-1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/option4">

            <LinearLayout
                android:id="@+id/linearcut1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/linearcut2"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent">
                
                <ImageView
                    android:id="@+id/order_option_btn_cut_1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:srcCompat="@drawable/sel_order_option_btn_cut_1" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="4dp"
                    android:fontFamily="@font/noto_sans_kr_regular"
                    android:text="1๊ฐœ"
                    android:textColor="#7d7d7d"
                    android:textSize="12sp" />
            </LinearLayout>

            <LinearLayout
                android:id="@+id/linearcut2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/linearcut3"
                app:layout_constraintStart_toEndOf="@id/linearcut1"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.0">

                <ImageView
                    android:id="@+id/order_option_btn_cut_2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:srcCompat="@drawable/sel_order_option_btn_cut_2" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="4dp"
                    android:fontFamily="@font/noto_sans_kr_regular"
                    android:text="2๊ฐœ"
                    android:textColor="#7d7d7d"
                    android:textSize="12sp" />
            </LinearLayout>

            <LinearLayout
                android:id="@+id/linearcut3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/linearcut4"
                app:layout_constraintStart_toEndOf="@id/linearcut2"
                app:layout_constraintTop_toTopOf="parent">

                <ImageView
                    android:id="@+id/order_option_btn_cut_3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    app:srcCompat="@drawable/sel_order_option_btn_cut_3" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="4dp"
                    android:fontFamily="@font/noto_sans_kr_regular"
                    android:text="3๊ฐœ"
                    android:textColor="#7d7d7d"
                    android:textSize="12sp" />
            </LinearLayout>

            <LinearLayout
                android:id="@+id/linearcut4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/linearcut3"
                app:layout_constraintTop_toTopOf="parent">

                <ImageView
                    android:id="@+id/order_option_btn_cut_4"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    app:srcCompat="@drawable/sel_order_option_btn_cut_4" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="4dp"
                    android:fontFamily="@font/noto_sans_kr_regular"
                    android:text="4๊ฐœ"
                    android:textColor="#7d7d7d"
                    android:textSize="12sp" />
            </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

1. ํ™•์žฅํ•จ์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐ

  • kotlin collection์—์„œ ์ œ๊ณตํ•˜๋Š” ํ™•์žฅํ•จ์ˆ˜ ์‚ฌ์šฉ
  • split() ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด uri์—์„œ ํŒŒ์ผ๋ช…์„ ๋ถ„๋ฆฌํ•œ๋‹ค

BoosterUtil.kt

fun getFileName(uri: Uri?): String? {
        if (uri == null) {
            return ""
        }
        val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
        cursor?.moveToNext()
        val path = cursor?.getString(cursor.getColumnIndex("_data"))
        cursor?.close()
        val filePath = path?.split("/")

        return filePath?.get(filePath.size - 1)
}
  • ๊ธฐ์กด ํด๋ž˜์Šค์— custom ํ•จ์ˆ˜๋ฅผ ํ™•์žฅํ•˜์—ฌ ์‚ฌ์šฉ
  • BindingAdapter์—์„œTextView์™€ ImageView ๋“ฑ์˜ view ์š”์†Œ์— ํ™•์žฅํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉ

BindingAdapter.kt

@BindingAdapter("setCancelVisible")
fun TextView.setCancelVisible(status : Int) {
    if (status!=1){
        visibility = GONE
    }
}

@BindingAdapter("setFavStar")
fun ImageView.setFavStar(status : Int) {
    if (status==0){
        setImageResource(R.drawable.store_detail_ic_star_inactive)
    }else{
        setImageResource(R.drawable.store_detail_ic_star_active)
    }
}

2. ์ค‘๋ณต ํด๋ฆญ ๋ฐฉ์ง€

๐Ÿ”ฅ issue

  • ์•กํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ด๋™ํ•˜๋Š” ๋ฒ„ํŠผ ํด๋ฆญ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์—ฐ์†์œผ๋กœ ๋น ๋ฅด๊ฒŒ ํ•  ๋•Œ ๋˜‘๊ฐ™์€ ์•กํ‹ฐ๋น„ํ‹ฐ ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ๊ณ„์†ํ•ด์„œ ์Œ“์ธ๋‹ค.

๐Ÿ“’ solution

  • ktx(kotlin-extension)์„ ํ™œ์šฉํ•˜์—ฌ ์ค‘๋ณต ํด๋ฆญ ๋ฐฉ์ง€ ๊ตฌํ˜„
class OnlyOneClickListener(
    private val clickListener: View.OnClickListener,
    private val interval: Long = 300
) :
    View.OnClickListener {

    private var clickable = true

    override fun onClick(view: View?) {
        if (clickable) {
            clickable = false
            view?.run {
                postDelayed({
                    clickable = true
                }, interval)
                clickListener.onClick(view)
            }
        } else {
            Log.e(TAG, "waiting for a while")
        }
    }
}

fun View.onlyOneClickListener(action: (v: View) -> Unit) {
    val listener = View.OnClickListener { action(it) }
    setOnClickListener(OnlyOneClickListener(listener))
}
์ด์ „ ์ฝ”๋“œ
act_main_btn_store.setOnClickListener {
            val intent = Intent(this@MainActivity, StoreListActivity::class.java)
            startActivity(intent)
        }
๋ฐ”๋€ ์ฝ”๋“œ
act_main_btn_store.onlyOneClickListener {
            val intent = Intent(this@MainActivity, StoreListActivity::class.java)
            startActivity(intent)
        }

๐Ÿ—ž result

  • ์—ฐ๋‹ฌ์•„ ํ„ฐ์น˜์‹œ ๋ถˆํ•„์š”ํ•œ clickEvent๊ฐ€ ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.

3. Scroll Animation

์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23

๐Ÿ”ฅ issue

  • ๋ทฐ ์Šคํฌ๋กค์‹œ ํƒ€์ดํ‹€ ๋ ˆ์ด์•„์›ƒ์ด ์ƒ๋‹จ์— ๊ณ ์ •๋œ์ฑ„๋กœ RecyclerView๊ฐ€ ์Šคํฌ๋กค ๋˜์•ผํ•œ๋‹ค.

๐Ÿ“’ solution

  • CollapsingToolbarLayout๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํƒ€์ดํ‹€ ์ƒ๋‹จ ๊ณ ์ •
  • addOnOffsetChangedListener ์•ˆ์—์„œ ๋ทฐ์˜ alpha ๊ฐ’์„ ์กฐ์ ˆํ•˜์—ฌ toolbar fade out ํšจ๊ณผ ๊ตฌํ˜„

frag_store_list.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".ui.storeList.StoreListFragment">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/frag_store_list_appBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            android:theme="@style/AppTheme.AppBarOverlay">

            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:id="@+id/frag_store_list_toolBar_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:fitsSystemWindows="true"
                app:layout_scrollFlags="scroll|exitUntilCollapsed"
                app:toolbarId="@+id/frag_store_list_toolBar">

                <androidx.appcompat.widget.Toolbar
                    android:id="@+id/frag_store_list_toolBar"
                    android:layout_width="match_parent"
                    android:layout_height="97dp"
                    android:background="@color/white"
                    app:layout_collapseMode="pin"
                    app:popupTheme="@style/AppTheme.PopupOverlay">

                    <ImageView
                        android:id="@+id/frag_store_list_iv_map"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="end|top"
                        android:layout_marginTop="4dp"
                        android:layout_marginEnd="2dp"
                        android:layout_marginBottom="4dp"
                        android:src="@drawable/store_detail_ic_map_blue"
                        app:layout_collapseMode="parallax" />

                </androidx.appcompat.widget.Toolbar>

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:id="@+id/frag_store_list_cl_title"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="80dp"
                    android:background="@android:color/transparent"
                    app:layout_collapseMode="pin">

                    <TextView
                        android:id="@+id/frag_store_list_tv_title"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="16dp"
                        android:layout_marginTop="9dp"
                        android:fontFamily="@font/noto_sans_kr_bold"
                        android:text="๋งค์žฅ"
                        android:textColor="@color/black"
                        android:textSize="26sp"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    ...

                </androidx.constraintlayout.widget.ConstraintLayout>

            </com.google.android.material.appbar.CollapsingToolbarLayout>

        </com.google.android.material.appbar.AppBarLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/frag_store_list_rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

        </androidx.recyclerview.widget.RecyclerView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

StoreListFragment.kt

frag_store_list_appBar.addOnOffsetChangedListener(OnOffsetChangedListener { frag_store_list_appBar, verticalOffset ->
            if (frag_store_list_appBar.totalScrollRange == 0 || verticalOffset == 0) {
                frag_store_list_iv_map.alpha = 1f
                return@OnOffsetChangedListener
            }
            val ratio = verticalOffset.toFloat() / frag_store_list_appBar.totalScrollRange.toFloat()
            frag_store_list_iv_map.alpha = 1f- abs(ratio)
    })

๐Ÿ—ž result

  • ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋”ํ•˜๋‹ˆ ์ข€ ๋” ์ƒ๊ธฐ์žˆ๋Š” ๋ทฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜๋‹ˆ ๋””์ž์ด๋„ˆ๊ฐ€ ์š”๊ตฌํ•˜๋Š” ์ •ํ™•ํ•œ ๋ทฐ(๊ทธ๋ฆผ์ž ๋“ฑ)์„ ๋งŒ๋“œ๋Š” ๋ฐ์—๋Š” ์•ฝ๊ฐ„์˜ ์–ด๋ ค์›€์ด ์žˆ์—ˆ๋‹ค.

4. ํ™”๋ฉด์„ ์•„๋ž˜๋กœ ๋‹น๊ฒจ์„œ List Refresh ํ•˜๊ธฐ

์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23

๐Ÿ”ฅ issue

  • ์ฃผ๋ฌธ ํ˜„ํ™ฉ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด List๋ฅผ ๋‹น๊ฒจ์„œ ์ƒˆ๋กœ๊ณ ์นจํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค

๐Ÿ“’ solution

  • ์›ํ•˜๋Š” List Layout์„ SwipeRefreshLayout์œผ๋กœ ๊ฐ์‹ผ๋‹ค
  • SwipeRefreshLayout์— setOnRefreshListener๋ฅผ ์ถ”๊ฐ€ํ•ด ํ†ต์‹  ํ•จ์ˆ˜๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค

fragment_store_list.kt

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
            android:id="@+id/frag_store_list_srl"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/frag_store_list_rv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/white"
                android:clipToPadding="false"
                android:paddingTop="8dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

StoreListFragment.kt

private fun refresh(){
        frag_store_list_srl.apply{
            setOnRefreshListener {
                viewModel.getStoreList(univIdx)
                this@apply.isRefreshing = false
            }
        }
    }

๐Ÿ—ž result

  • ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•  ๋•Œ ๋ ˆ์ด์•„์›ƒ์„ ๋‹น๊ฒจ List๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค

5. ๋กœ์ปฌ ๋””๋ฐ”์ด์Šค์—์„œ ํŒŒ์ผ ๊ฐ€์ ธ์˜ค๊ธฐ

์Šคํฌ๋ฆฐ์ƒท 2020-07-17 ์˜คํ›„ 6 26 23

๐Ÿ”ฅ issue

  • ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์ปค์Šคํ…€ ์ €์žฅ์†Œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.

๐Ÿ“’ solution

  • DroidNinja ์˜ Android-FilePicker(https://github.com/DroidNinja/Android-FilePicker) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ปค์Šคํ…€ ํŒŒ์ผ ์ €์žฅ์†Œ ๊ตฌํ˜„
 private fun fileAdd() {
        val builder: AlertDialog.Builder =
            AlertDialog.Builder(this, R.style.MyAlertDialogStyle2)
        builder.setTitle("์ถ”๊ฐ€ํ•  ํŒŒ์ผ์˜ ์ข…๋ฅ˜๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”")
        builder.setPositiveButton("์ด๋ฏธ์ง€") { dialogInterface: DialogInterface, i: Int ->
            FilePickerBuilder.instance
                .setMaxCount(1)
                .setActivityTheme(R.style.LibAppTheme) //optional
                .setActivityTitle("์ด๋ฏธ์ง€ ์„ ํƒ")
                .pickPhoto(this, REQUEST_CODE_PHOTO);
        }
        builder.setNegativeButton("๋ฌธ์„œ") { dialogInterface: DialogInterface, i: Int ->
            FilePickerBuilder.instance
                .setMaxCount(1)
                .setActivityTheme(R.style.LibAppTheme) //optional
                .setActivityTitle("๋ฌธ์„œ ์„ ํƒ")
                .pickFile(this, REQUEST_CODE_DOC);
        }
        builder.show()

    }

๐Ÿ—ž result

  • File Picker Open Source๋ฅผ ๋ถ„์„ํ•ด์„œ ๋ถ€์Šคํ„ฐ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊นŒ๋‹ค๋กœ์› ๋‹ค. ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๊ฒŒ ํ…Œ๋งˆ ๋ฐ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜์—ฌ ์„ฑ๊ณต์ ์œผ๋กœ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

6. form-data ๋กœ ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ(image,pdf ๋“ฑ) ์ „์†กํ•˜๊ธฐ

๐Ÿ”ฅ issue

  • form-data ๋กœ pdf, image ํŒŒ์ผ์„ ์„œ๋ฒ„์— ์ „์†กํ•ด์•ผ ํ•œ๋‹ค.

๐Ÿ“’ solution

  • ๊ฒฝ๋กœ๋ฅผ ํ†ตํ•ด File ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ์ค€ ๋‹ค์Œ RequestBody -> Multipart.Part ์ˆœ์œผ๋กœ ๋ณ€ํ™˜ํ•œ ๋‹ค์Œ ํ†ต์‹ ์„ ์ง„ํ–‰ํ•œ๋‹ค.

BoosterService.kt

    @Multipart
    @POST("/orders/{order_idx}/file")
    suspend fun postUploadFile(
        @Header("token") token: String,
        @Path("order_idx") orderIdx: Int,
        @Part file: MultipartBody.Part?,
        @Part thumbnail: MultipartBody.Part?
    ): ApiWrapper<com.example.booster.data.datasource.model.File>

FileStorageViewModel.kt

var requestBody: RequestBody? = null
        var requestBody2: RequestBody? = null

        when (file?.file_extension) {
            ".png" -> {
                requestBody = RequestBody.create(
                    MediaType.parse("image/png"), imageFile
                )
                requestBody2 = RequestBody.create(
                    MediaType.parse("image/png"), imageFile
                )
            }
            ".pdf" -> {
                requestBody = RequestBody.create(
                    MediaType.parse("application/pdf"), docFile
                )
                requestBody2 = RequestBody.create(
                    MediaType.parse("image/png"), thumbnailFile
                )
            }
            ".docx" -> requestBody = RequestBody.create(
                MediaType.parse("multipart/form-data"), docFile
            )
            ".jpeg", ".jpg" -> {
                requestBody = RequestBody.create(
                    MediaType.parse("image/jpeg"), imageFile
                )
                requestBody2 = RequestBody.create(
                    MediaType.parse("image/jpeg"), imageFile
                )
            }
        }
        Log.e(
            "pdfcheck",
            "check: " + requestBody + " " + file?.file_extension + " " + file?.file_name
        )
        val multipartBody =
            MultipartBody.Part.createFormData("file", file?.file_name, requestBody)

        val multipartBody2 =
            MultipartBody.Part.createFormData("thumbnail", "png", requestBody2)
            

๐Ÿ—ž result

  • MediaType ๋ณ€ํ™˜ ๋ฌธ๊ตฌ๊ฐ€ ํ‹€๋ฆฌ๊ณ , ๋ถˆ ํ•„์š”ํ•œ ํ—ค๋”๋ฅผ ๋„ฃ์–ด์„œ ์ฒ˜์Œ์—” ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๋งŽ์ด ๊ฒช์—ˆ์ง€๋งŒ, ๊ฒฐ๊ตญ ํ•ด๋‚ด์„œ ๋˜ ํ•œ ๋ฒˆ์˜ ์„ฑ์žฅ์„ ์ด๋ฃฉํ–ˆ๋‹ค.

7. pdf ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ ๋ฐ thumbnail ์ถ”์ถœ

๐Ÿ”ฅ issue

  • pdf๋ฅผ ์ €์žฅ์†Œ๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์™€์„œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ณ  ์ฒซ ํŽ˜์ด์ง€(์ธ๋„ค์ผ)๋ฅผ ์ด๋ฏธ์ง€๋กœ ์ถ”์ถœํ•œ๋‹ค.

๐Ÿ“’ solution

  • PdfRenderer ๋ฅผ ์ด์šฉํ•ด์„œ pdf ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ ์ œ๊ณต
val fileDescriptor: ParcelFileDescriptor?
        fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)

        //min. API Level 21
        val pdfRenderer: PdfRenderer?
        pdfRenderer = PdfRenderer(fileDescriptor)
        val pageCount: Int = pdfRenderer.pageCount
        Log.e(
            "pagecount",
            "check: " + pageCount.toString() + " " + pdfviewer_act_main_total_page.text
        )
        pdfviewer_act_main_total_page.text = pageCount.toString()
        Toast.makeText(this, "pageCount = $pageCount", Toast.LENGTH_LONG).show()

        val parentlayout = LinearLayout(this)
        parentlayout.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
        LinearLayout.LayoutParams.WRAP_CONTENT)
        parentlayout.orientation = LinearLayout.HORIZONTAL

        if (pageCount != 1) {
            pdfviewer_act_main_hs.removeView(pdfviewer_act_main_ll)
            pdfviewer_act_main_hs.addView(parentlayout)
        }

        for (i in 0 until pageCount) {
            pdfviewer_act_main_cur_page.text = (i + 1).toString()
            val imageView = ImageView(this)
            imageView.layoutParams = LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
            ) // value is in pixels
            val rendererPage = pdfRenderer.openPage(i)
            val rendererPageWidth: Int = rendererPage.width
            val rendererPageHeight: Int = rendererPage.height
            val bitmap =
                Bitmap.createBitmap(rendererPageWidth, rendererPageHeight, Bitmap.Config.ARGB_8888)
            rendererPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
            imageView.setImageBitmap(bitmap)

            if (pageCount == 1) {
                pdfviewer_act_main_ll.addView(imageView)
            }else {
                parentlayout.addView(imageView)
            }

            rendererPage!!.close()
        }

        pdfRenderer.close()
        fileDescriptor.close()
  • PdfRenderer๋ฅผ ์ด์šฉํ•ด์„œ ์ธ๋„ค์ผ ์ด๋ฏธ์ง€(bitmap) ์ถ”์ถœ
object PDFThumbnailUtils {
    fun convertPDFtoBitmap(context: Context, uri: Uri, pageNumber: Int): Bitmap? {
        val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val pdfRenderer = parcelFileDescriptor?.let { PdfRenderer(it) }
            val currentPage = pdfRenderer?.openPage(pageNumber)
            val bitmap = Bitmap.createBitmap(currentPage?.width!!, currentPage?.height!!, Bitmap.Config.ARGB_8888)
            currentPage?.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
            // Here, we render the page onto the Bitmap.
            return bitmap
        } else {
            TODO("VERSION.SDK_INT < LOLLIPOP")
        }
    }
}
  • ์‹ค์ œ ๋ฆฌ์‚ฌ์ดํด๋Ÿฌ๋ทฐ ๋ฐ์ดํ„ฐ์— ์ ์šฉ
inner class ViewHolder(itemView: View) :
        RecyclerView.ViewHolder(itemView) {
        fun bind(file: File) {
            //Log.e("uri", file.uri.path.toString())

            Log.e("file", file.file_name + " " + file.file_extension)
            if (file.file_extension == ".png" || file.file_extension == ".jpeg" || file.file_extension == ".jpg") {
                Glide.with(itemView.context).load(file.file_path).into(itemView.iv_file)
            } else {
                val uri = file.file_uri
                if (uri != null) {
                    val bitmap =
                        PDFThumbnailUtils.convertPDFtoBitmap(
                            itemView.context,
                            uri,
                            PAGE_NUMBER
                        )
                    if (bitmap != null) {
                        itemView.iv_file.setImageBitmap(bitmap)
                        //Log.e("context check: ", " " + itemView.context + " " + itemView.context.javaClass.name)
                    }

๐Ÿ—ž result

  • pdf๋‚˜ ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ œ๊ณต ํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, hwp,ppt ๋“ฑ ์˜คํ”ผ์Šค ๊ธฐ๋ฐ˜ ๋ฌธ์„œ๋“ค์€ ์ œ๊ณตํ•˜๊ธฐ์— ๊นŒ๋‹ค๋กœ์› ๋‹ค. ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

8. ์ถ”์ถœํ•œ ์ธ๋„ค์ผ์„ Multipart๋กœ ์„œ๋ฒ„๋กœ ์ „์†ก

๐Ÿ”ฅ issue

  • bitmap ํ˜•ํƒœ์˜ ์ธ๋„ค์ผ์„ Multipart๋ฅผ ์ด์šฉํ•˜์—ฌ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ฏธ์ง€๋ฅผ ํŒŒ์ผํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ „์†กํ•œ๋‹ค.

๐Ÿ“’ solution

  • bitmap์„ pngํ˜•ํƒœ ํŒŒ์ผ๋กœ ๋ณ€ํ™˜
private fun bitmapToFile(bitmap:Bitmap): java.io.File? {
        // Get the context wrapper
        val wrapper = ContextWrapper(applicationContext)

        // Initialize a new file instance to save bitmap object
        var file = wrapper.getDir("Images",Context.MODE_PRIVATE)
        file = java.io.File(file,"${UUID.randomUUID()}.png")

        try{
            // Compress the bitmap and save in jpg format
            val stream:OutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.PNG,100,stream)
            stream.flush()
            stream.close()
        }catch (e: IOException){
            e.printStackTrace()
        }

        // Return the saved bitmap uri
        return file
    }

๐Ÿ—ž result

  • Local Storage์—์„œ ๊ฐ€์ ธ์˜จ bitmap ํŒŒ์ผ์ด ์•„๋‹ˆ๋ผ filepath๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ์œ„ํ•˜์—ฌ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ•˜์˜€๊ณ  ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜์˜€๋‹ค.

9. ๋„ค์ด๋ฒ„์ง€๋„ API ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ”ฅ issue

  • ๋งค์žฅ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ง€๋„ ์œ„์˜ ๋งˆ์ปค๋กœ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๊ณ , ๋งˆ์ปค๋ฅผ ํ†ตํ•ด ์ƒ์„ธ์ •๋ณด ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜์—ฌ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์ž‡๋‹ค.

๐Ÿ“’ solution

  • ๋„ค์ด๋ฒ„์ง€๋„ API๋ฅผ ์ด์šฉํ•ด์„œ ๋งค์žฅ์˜ ์œ„๋„, ๊ฒฝ๋„ ์ •๋ณด๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ›์•„์™€์„œ ๋งˆ์ปค๋กœ ํ‘œ์‹œํ•œ๋‹ค.

StoreListFragment.kt

markers.clear()
            for(i in 0 .. it.size-1){
                markers.add(
                    MarkerData(
                        latitude = it[i].store_x_location,
                        longitude = it[i].store_y_location,
                        name = it[i].store_name,
                        idx = it[i].store_idx
                    )
                )
            }

MapActvity.kt

@UiThread
    override fun onMapReady(nMap: NaverMap) {

        val uiSettings = nMap.uiSettings
        uiSettings.isZoomControlEnabled = true
        uiSettings.isLocationButtonEnabled = true

        nMap.locationSource
        nMap.locationTrackingMode
        uiSettings.isScaleBarEnabled = false

        if (university == "์ˆญ์‹ค๋Œ€ํ•™๊ต"){
            cameraUpdate = CameraUpdate.scrollTo(LatLng(37.496575, 126.957427))
        }... //์„ ํƒํ•œ ํ•™๊ต ๋ณ„๋กœ focus ๋งž์ถ”๊ธฐ
        act_map_txt_univ.text = university

        nMap.moveCamera(cameraUpdate)
        draw(nMap)
    }
    //์‹ค์ œ ์ง€๋„๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์‹œ์ž‘ํ•œ๋‹ค. 
    fun draw(nMap: NaverMap){
        for(i in 0 until markers.size){
            repeat(1000) {
                array.plusAssign(Marker().apply {
                    position = LatLng(markers[i].latitude!!.toDouble(), markers[i].longitude!!.toDouble())
                    icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker)
                    tag = markers[i].name
                    width = Marker.SIZE_AUTO
                    height = Marker.SIZE_AUTO
                })
            }
        }
        //๋งˆ์ปค ํด๋ฆญ์‹œ tag ๋„์šฐ๊ธฐ
        val infoWindow = InfoWindow()
        infoWindow.adapter = object : InfoWindow.DefaultTextAdapter(this) {
            override fun getText(infoWindow: InfoWindow): CharSequence {
                return infoWindow.marker?.tag as CharSequence? ?:""
            }
        }
        //๋ฐ›์•„์˜จ ๋งค์žฅ ๋ฆฌ์ŠคํŠธ๋ณ„๋กœ ๋งˆ์ปค๋ฅผ ๋„์›Œ์ค€๋‹ค. ๋งˆ์ปค ํด๋ฆญ์‹œ ๋งˆ์ปค ์ด๋ฏธ์ง€ ๋ฐ”๊ฟˆ + tag๋„์šฐ๊ธฐ + tagํด๋ฆญ์‹œ ์ƒ์„ธํŽ˜์ด์ง€๋กœ ์ด๋™
        array.forEach { marker ->
            marker.map = nMap
            marker.setOnClickListener {
                for ( i in 0 until array.size){
                    array[i].icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker)
                }
                marker.icon = OverlayImage.fromResource(R.drawable.store_map_ic_marker_click)
                val cameraUpdate = CameraUpdate.scrollTo(marker.position)
                nMap.moveCamera(cameraUpdate)

                infoWindow.open(marker)
                infoWindow.setOnClickListener {
                    val intent = Intent(this, StoreDetailActivity::class.java)
                    for(i in 0 .. array.size){
                        if(markers[i].name == marker.tag){
                            val idx = markers[i].idx
                            intent.putExtra("storeIdx", idx)
                            break
                        }
                    }
                    startActivity(intent)
                    false
                }
                false
            }
        }
    }

๐Ÿ—ž result

  • ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋งค์žฅ์„ ์ง€๋„์— ๋งˆ์ปค๋กœ ๋„์›Œ์คŒ์œผ๋กœ์จ ์‚ฌ์šฉ์ž๊ฐ€ ์ง๊ด€์ ์œผ๋กœ ๋งค์žฅ์˜ ์œ„์น˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.

10. SharedPreferences๋ฅผ ์ด์šฉํ•ด์„œ ์‚ฌ์šฉ์ž ํ† ํฐ ๊ด€๋ฆฌํ•˜๊ธฐ

๐Ÿ”ฅ issue

  • ์•ฑ์„ ์žฌ๊ตฌ๋™ํ•˜์—ฌ๋„ ์‚ฌ์šฉ์ž์˜ ํ™œ๋™๊ธฐ๋ก์„ ๋‹ค์‹œ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ์‚ฌ์šฉ์ž๋ณ„ ํ† ํฐ์„ ์ €์žฅํ•œ๋‹ค. ํ•ด๋‹น ํ† ํฐ์œผ๋กœ ํ†ต์‹ ์„ ํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ณ„ ํ™œ๋™์„ ์‹๋ณ„ํ•œ๋‹ค.

๐Ÿ“’ solution

  • SharedPreferences๋ฅผ ์ด์šฉํ•˜์—ฌ ๋กœ๊ทธ์ธ์‹œ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ํ† ํฐ์„ ์ €์žฅํ•˜์—ฌ, ์ „์—ญ์—์„œ ํ•ด๋‹น ํ† ํฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋˜ํ•œ Interceptor๋กœ ๋งŒ๋“ค์–ด๋‘ฌ ํ†ต์‹ ํ•  ๋•Œ๋งˆ๋‹ค ํ•„์š”ํ•œ token๊ฐ’์„ ๋”ฐ๋กœ ๋„ฃ์–ด์ฃผ์ง€ ์•Š๊ณ  ๋ฏธ๋ฆฌ sharedpreferences์— ์ €์žฅํ•ด๋‘” token๊ฐ’์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

UserManager.kt

object UserManager {
    private lateinit var pref: SharedPreferences
    fun init(context: Context) {
        pref = context.getSharedPreferences("Booster", Context.MODE_PRIVATE)
    }

    var token: String?
        get() = pref.getString("token", null)
        set(value) = pref.edit {
            it.putString("token", value)
        }
}

CookiesInterceptor

class CookiesInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request =
            chain.request().newBuilder().header("Content-Type", "application/json")
                .header("token", UserManager.token?:"")
                .build()
        return chain.proceed(request)
    }
}
  • ๋กœ๊ทธ์ธ ํ†ต์‹ ์‹œ ์„œ๋ฒ„๋กœ ํ† ํฐ ๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค.

LoginActivity.kt

intent.putExtra("token", response.body()!!.data.accessToken)

BottomTabActivity.kt

if(intent.hasExtra("token")){
            bottom_vp.currentItem = 0
            token = intent.getStringExtra("token")
            UserManager.token = token

        }

๐Ÿ—ž result

  • ์ดˆ๊ธฐ์— ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ณ  test๋ฅผ ํ•  ๋•Œ ๋™์ผํ•œ ํ† ํฐ์„ ์‚ฌ์šฉํ•ด์„œ ์ฃผ๋ฌธํ˜„ํ™ฉ ๋ฐ ์ƒ์„ธ๋‚ด์—ญ ์ •๋ณด๊ฐ€ ๊ต‰์žฅํžˆ ๋งŽ์•„์„œ ๋ณด๊ธฐ ํž˜๋“ค์—ˆ๋Š”๋ฐ, ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋‹ˆ ๊ธฐ๋Šฅ testํ•˜๊ธฐ ํŽธํ•ด์กŒ๊ณ , ์‚ฌ์šฉ์ž๋ณ„ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์•˜๊ณ , ์ดํ›„ ์ด ๊ธฐ๋Šฅ์„ ๋” ๋ฐฐ์›Œ๋ณด๊ณ  ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค.

11. setOnKeyListener ์ด์šฉํ•ด์„œ focus

๐Ÿ”ฅ issue

  • ์—”ํ„ฐ๋กœ EditText๋ฅผ ๋‚˜์˜ฌ ๋•Œ focus๋ฅผ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค.

๐Ÿ“’ solution

  • setOnFocuseChangeListener์™€ setOnKeyListener ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ focus๋ฅผ ์„ค์ • ๋ฐ ํ•ด์ œํ•œ๋‹ค.

JoinActivity.kt

join_edt_pw_chk.setOnFocusChangeListener { v, hasFocus ->
   join_edt_pw_chk.isSelected = hasFocus
}

join_edt_pw_chk.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_ENTER) {
                v.clearFocus()
                val keyboard: InputMethodManager =
                    getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                keyboard.hideSoftInputFromWindow(join_edt_pw_chk.windowToken, 0)
                return@OnKeyListener true
            }
            false
        })

๐Ÿ—ž result

  • KeyEvent๋ฅผ ์ด์šฉํ•˜์—ฌ ์›ํ•˜๋Š” ์ž…๋ ฅ์— ๋”ฐ๋ผ focus๋ฅผ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋‹ค

12. addTextChangedListener ์ด์šฉํ•ด์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์‹ค์‹œ๊ฐ„ ์ฒดํฌ

๐Ÿ”ฅ issue

  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ์ž…๋ ฅ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฒดํฌํ•˜์—ฌ TextView์˜ visibility๋ฅผ ๋ฐ”๊พผ๋‹ค.

๐Ÿ“’ solution

  • addTextChangedListener๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž…๋ ฅ์˜ ๋ณ€ํ™”๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฒดํฌํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ํ•˜์˜€๋‹ค.

JoinActivity.kt

join_edt_pw_chk.addTextChangedListener {

                if (join_edt_pw.text.toString() == join_edt_pw_chk.text.toString()) {
                    join_tv_pw_check_fail.visibility = View.INVISIBLE
                    pwChk = true
                } else {
                    join_tv_pw_check_fail.visibility = View.VISIBLE
                }

            }

๐Ÿ—ž result

  • EditText์˜ text๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋น„๊ตํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ์ ˆํ•œ ๊ฒฝ๊ณ ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ–ˆ๋‹ค.

putExtra๋ฅผ ์ด์šฉํ•ด์„œ Fragment์™€ Activity ๊ฐ„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ

๐Ÿ”ฅ issue

  • ๋‹จ์ˆœํ•œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด ๋ถˆํ•„์š”ํ•˜๊ฒŒ api ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜์–ด ์ฝ”๋“œ๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ธธ์–ด์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.

๐Ÿ“’ solution

  • putExtra์™€ getStringExtra๋ฅผ ์ด์šฉํ•˜์—ฌ Fragment์™€ Activity๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ํ•˜์˜€๋‹ค.

MypageFragment.kt

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mypage_tv_goto_edit.setOnClickListener {

            val intent = Intent(context, EditProfileActivity::class.java)
            intent.putExtra("id", mypage_tv_id.text.toString())
            intent.putExtra("univ", univIdx.toString())
            intent.putExtra("name", mypage_tv_name.text.toString())
            startActivity(intent)
        }

        mypage_tv_goto_myengine.setOnClickListener {

            val intent = Intent(context, MyengineActivity::class.java)
            startActivity(intent)
        }
    }

EditProfileActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_edit_profile)
        
        var extraId = intent.getStringExtra("id")
        var extraUnivIdx = intent.getStringExtra("univ")
        var extraName = intent.getStringExtra("name")
}

๐Ÿ—ž result

  • ๊ฐ„๋‹จํžˆ data๋ฅผ View๊ฐ„์— ์ „๋‹ฌํ•˜๋ฉฐ ๋ถˆํ•„์š”ํ•œ ํ†ต์‹ ์„ ์ค„์˜€๋‹ค.

๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘งโ€๐Ÿ‘ง Developer

booster-android's People

Contributors

ghkdua1829 avatar jihyeonmon avatar jineeee avatar roksui avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.