위의 사진과 같이 BuildConfig 를 썼으나 인식하지 못하는 에러 발생.

 

1. import가 잘 되었는가?

import com.example.miseya.BuildConfig

 

BuildConfig 를 import 방법

import 패키지명.BuildConfig

저의 패키지명 그대로 쓰시면 안됩니다! 본인 패키지명 입력해야 합니다

 

 

이랬는데도 아래 사진과 같이 안되는 현상...

 

 

2. [이게 찐]모듈 수준 build.gradle 파일 buildFeatures에 buildConfig = true 추가 후 Sync

 

 

이 후 Build -> Clean Project -> Rebuild Project

 

+ 근데 해보니 아래 사진과 같이 필요없는 Unused import directive 표시가 뜬다.

아무래도 기존에 import해서 사용하는 방식에서 gradle 파일에서 설정해주는 형식으로 바뀐 듯 하다.

 

 

앞으로는 gradle 파일에서 viewBinding을 설정해줄 때 buildConfig도 같이 설정해줘야겠다.

 

1. build.gradle 파일에서 Splash Screen 라이브러리 추가 설정

 

2. res/values/themes/themes.xml 파일에서 Splash Screen Theme 설정

 

<!-- splash screen Theme -->
<style name="AppTheme.Splash" parent="Theme.SplashScreen">
    <item name="android:windowSplashScreenBackground">@color/black</item>
    <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_logo</item>
    <item name="android:windowSplashScreenAnimationDuration">1000</item>
    <item name="postSplashScreenTheme">@style/Theme.HotPlaceContactApp</item>
</style>

 

하나 하나 살펴보자면,

  • windowSplashScreenBackground : Splash Screen의 배경을 설정한다. 색상값, 이미지 등을 사용할 수 있다.
  • windowSplashScreenAnimatedIcon : Splash Screen 에서 사용할 아이콘을 설정한다. 
  • windowSplashScreenAnimationDuration : Splash Screen 을 보여줄 시간을 설정한다. 최대 1000ms
  • postSplashScreenTheme : Splash Screen을 보여주고 나서 다음에 나올 화면의 테마를 설정해준다.

여기서 다음에 나올 화면의 테마는 위에 기본으로 설정되어있는 앱 기본 테마로 해주면 된다.

 

3. AndroidManifest.xml 파일 설정

 

themes.xml 에서 설정한 splash style name으로 바꿔준다.

 

4. MainActivity 에서 Splash Screen 설정

 

중요한건 setContentView에 레이아웃을 넘겨주기 전에 보여줘야하므로 setContentView 전에 써준다.끝!

 

이러면 스플래쉬 액티비티를 따로 생성하지 않고 이용가능하므로 간편해졌다.

처음 배울 때는 Scroll View와 LinearLayout을 이용하여 Data List를 보여주었다.

나쁘지 않다 생각했으나 아무것도 모를 때이기에 그렇게 생각했다ㅋㅋ

 

여기서 한 단계 더 배워 Adapter View를 배워서 ListView와 GridView를 통해 아이템들을 보여주었다.

그래 이거지,, 동적으로 추가도 가능해지며 좀 더 그럴 듯 해보였다.

그 전에 스크롤 뷰에서 아이템의 위치 값을 이용하기 위해 intent로 쌩쇼를 한게 부끄러울 정도다.

하지만 이런 ListView와 GridView도 문제점을 가지고 있었다.

Data가 100개, 1000개, 10000개라면?? 혹은 Data가 동적으로 계속 변경된다면?

 

앱은 과부하되어 실행되지 않을 수 있겠다...

 

왜냐하면 기존의 ListView나 GridView는 밑으로 스크롤 할 때마다 위에 있던 아이템은 삭제되고, 맨 아래의 아이템은 생성 되길 반복하기 때문이다. 한 마디로 비효율적으로 일을 하는 형태이다.

 

이런 단점을 보완하기 위해 나온 이름하야 리사이클러 뷰!!

RecyclerView란?

리사이클러 뷰는 현재 현업에서 리스트 형태의 Data를 표시하는데 무조건 쓰이는 가장 최신의 위젯이다.

아래 그림과 같이 리스트를 스크롤 할 때, 위에 있던 아이템은 재활용 돼서 아래로 이동하여 재사용 한다.

많은 아이템을 효율적으로 관리하고 보여주는 역할을 한다.

 

Recycler View를 사용할 땐 3가지가 필요하다.

 

1. LayoutManager

  • LayoutManager는 RecyclerView 내부의 아이템들이 어떻게 배치될지를 결정한다.
  • 기본 제공되는 **LayoutManager**로는 LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager 등이 있다.

recyclerView.layoutManager = LinearLayoutManager(this) // 수직 리스트를 위한 LinearLayoutManager
// 또는
recyclerView.layoutManager = GridLayoutManager(this, 2) // 2열 그리드를 위한 GridLayoutManager

 

2. RecyclerView.Adapter

  • RecyclerView에 표시될 데이터와 해당 데이터를 보여줄 ViewHolder를 연결한다.
  • Adapter는 데이터셋의 변경 사항을 RecyclerView에 알리고, 데이터를 기반으로 뷰를 생성합니다.
class MyAdapter(private val dataList: MutableList<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView = view.findViewById(R.id.textView)
        val thumbnailView: ImageView = view.findViewById(R.id.thumbnail)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        return MyViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        Log.d("RecycleView", "onBindViewHolder :$position")
        holder.textView.text = dataList[position]
        holder.thumbnailView.setImageUri(dataList[position].thubnailUrl)
    }

    override fun getItemCount() = dataList.size

    // 데이터 추가
    fun addItem(data: String) {
        dataList.add(data)
        notifyItemInserted(dataList.size - 1)
    }

    // 데이터 삭제
    fun removeItem(position: Int) {
        if (position < dataList.size) {
            dataList.removeAt(position)
            notifyItemRemoved(position)
        }
    }
}

3. ViewHolder

  • RecyclerView의 개별 아이템 뷰를 위한 객체이다.
  • 아이템 뷰의 모든 서브 뷰를 담고 있어 재사용과 성능 최적화에 도움을 준다.
inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val thumbnailView: ImageView = view.findViewById(R.id.thumbnailView)
        val titleView: TextView = view.findViewById(R.id.titleView)
        // 여기에 필요한 다른 뷰를 추가할 수 있다.
    }

프로젝트를 할 때 마다 기본 버튼을 쓰니 튜터님들께서 디자인쪽으로 신경을 좀 쓰는게 좋을 것 같다는 피드백을 주셨다.

 

색이나 폰트 크기 등에 조금만 신경을 써도 생각보다 미적으로 보기가 편하긴 하다.

 

그래서 버튼에 조금이나마 디자인적인 요소를 넣을 수 있는 방법과 색상 변경이 안될 때 대처 방법을 소개한다.

 

간단하다.

 

 

이런 심플한 버튼을 하나 만든다.

 

그리고 drawable에서 drawable Resource File을 하나 만들어준다. root element에는 shape을 넣어주면 된다.

 

 

그리고 안에 내용물들을 채워주면 되는데 각각 태그들의 설명을 하자면.

 

solid 에서는 color를 통해 배경색을 지정해준다.

 

corners 에서는 radius를 통해 모서리부분에 대한 라운드 처리를 해준다.

 

size에서는 width와 height을 통해 사이즈를 지정해준다. (동일한 버튼이 여러개라면 이곳에서 지정해주는게 관리포인트를 줄일 수 있다.)

 

stroke에서는 width로 테두리 너비를 지정하고 color로 테두리 색을 지정해준다.

 

다 코드를 쳐주고 돌아와서 

android:background="@drawable/chat_background_pumpkin"

 

백그라운드에 넣어주면 버튼 디자인이 입혀지게된다.

 

그.런.데 여기서 안되는 나같은 사람도 있을 수 있을 것이다.

 

다 꾸며놨는데 디자인이 적용이 안되는 상황이 발생해버렸다.

 

하지만 해결방법은 너무나 간단했다.

 

<Button
    android:id="@+id/detail_item_chat"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="15dp"
    android:background="@drawable/chat_background_pumpkin"
    android:text="채팅하기"
    android:textSize="17sp"
    android:textStyle="bold"
    android:textColor="@color/white"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

 

기존에 이렇게 되어잇던 버튼 태그를..

 

<android.widget.Button

 

이런식으로 바꿔주면 적용이 된다.

 

회원 가입 기능을 구현하면서 사용자 입력값에 대한 유효성 검사를 해야할 때가 생겼다.

 

이름과 아이디 비밀번호 등을 입력 받으면서 이름에는 숫자, 특수문자가 들어가면 안되고 아이디는 이메일 형식으로 받아야 하고 비밀번호는 영문 숫자 특수문자 3가지 조합이 들어가야 한다. 

 

이런 유효성 검사 같이 Text 실시간 처리를 할 때 유용한 클래스가 있다.

 

바로 addTextChangedListener 와 TextWatcher이다.

 

addTextChangedListener 는 EditText 위젯에 TextWatcher를 추가하는 메소드이며 사실상 TextWatcher가 이벤트를 감지하고 처리하는데 쓰이는 주인공이다.

 

적용방법은 아래와 같다.

EditText.addTextChangedListener( object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
                TODO("Not yet implemented")
            }

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                TODO("Not yet implemented")
            }

            override fun afterTextChanged(s: Editable?) {
                TODO("Not yet implemented")
            }

        })

 

TextWatcher 는 세 가지 메서드를 정의하는 인터페이스이다.

차례대로 설명하겠다.

 

beforeTextChanged 는 텍스트가 변경되기 전에 호출되어 변경 전 처리해야할 작업을 넣어주면 된다.

매개변수를 살펴보면 s는 변경 전 텍스트를 가지고 있고 start는 변경이 시작되는 인덱스 before은 변경 전에 제거되는 문자의 수, count는 변경으로 인해 추가되는 문자의 수를 감지한다.

 

이를 통해 특정 문자의 입력을 제한하거나 수정 전에 기존 텍스트를 저장할 때 유용하게 쓸 수 있다. 예를 들어 사용자가 금지된 단어를 입력했을 때 변경을 취소하고 그 전의 텍스트로 복원시키기가 가능하다.

 

onTextChanged 는 텍스트가 변경될 때 호출되어 입력 중에 특정 패턴을 검사하거나 실시간으로 입력 내용을 반영하고자 할 때 유용하다.

 

afterTextChanged는 텍스트가 변경된 후에 호출되어 변경이 완료된 후 처리해야하는 작업이 있을 때 유용하다.

 

간단하게 로그를 찍어 살펴보자.

 

 

숫자를 빠르게 1234를 입력했다. 한 글자를 입력할 때마다 3개의 메소드가 차례대로 호출된다.

주의할 점은 before은 텍스트 변경 전 시점에 호출되기에 on과 after와 다른 값을 가지고 있다는 것이다.

 

다음은 굳이 3개의 메소드를 다 오버라이드해서 사용안할 분들은 위한 팁이다.

EditText.addTextChangedListener(@SuppressLint("RestrictedApi")
        object : TextWatcherAdapter() {
            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
                
        })

 

TextWatcherAdapter를 통해서 원하는 메소드만 오버라이드해서 사용 가능하다. 

대신 @SuppressLint("RestrictedApi") 어노테이션을 통해 오류메시지를 강제적으로 무시해주어야 한다.

@SuppressLint("RestrictedApi") 어노테이션은 Android Studio에서 Lint 경고를 무시하도록 하는 역할을 한다.

Lint는 코드에서 잠재적인 문제를 찾아내고 경고 메시지를 통해 문제점을 알려주는 녀석이다.

 

즉, 해당 코드 라인에서 발생하는 RestrictedApi 경고를 무시하라는 의미로 해당 코드에 대한 경고를 의도적으로 무시하고자 할 때 쓰는 방법이다.

 

이 방법말고 doBeforeTextChaged, doOnTextChaged, doAfterTextChaged 도 있다.

 

유효성 검사를 한 방법에 대해서 조금 남겨본다.

 

pwdEditText.addTextChangedListener(@SuppressLint("RestrictedApi")
        object : TextWatcherAdapter() {
            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
                pwdFlag = isValidPwd()
                signUpBtn.isEnabled = (nameFlag && ageFlag && mbtiFlag && idFlag && pwdFlag && pwdCheckFlag)
            }
        })
        
        
 fun isValidPwd() : Boolean {
        val pwdEditText = findViewById<EditText>(R.id.pwdTextInputEditText)
        val pwd = pwdEditText.text.toString()
        val pwdPattern = Regex("^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[\$@\$!%*#?&.])[A-Za-z[0-9]\$@\$!%*#?&.]{8,16}\$")
        return if (pwdEditText.text.isEmpty()) {
            pwdEditText.error = "비밀번호를 입력해주세요."
            false
        } else if (!pwd.matches(pwdPattern)){
            pwdEditText.error = "영문, 숫자, 특수문자를 모두 포함하여 주세요."
            false
        } else {
            pwdEditText.error = null
            true
        }
    }

 

정규표현식을 통해 실시간으로 입력받는 값을 처리해주고 있다. 영문 숫자 특수문자를 모두 포함하여야 에러텍스트가 null이되면서 통과하는 형식이다.

프래그먼트란?

안드로이드 앱의 사용자 인터페이스(UI) 일부를 나타내는 모듈화된 컴포넌트이다. 라고 하면 확 와닿지 않는다.

내 개인적인 이해를 가지고 예시를 들어 설명을 하자면, 퍼즐과 비슷한 것 같다.

 

액티비티가 한 폭의 그림이 담긴 큰 틀이라고 한다면, 프래그먼트는 그 그림을 이루는 퍼즐 조각같은거다.

 

우선 프래그먼트라는 개념이 나오게 된 배경을 알아보자.

 

예전에는 핸드폰이 나왔을 때 대부분 비슷비슷한 화면이었다. 근데 지금을 보자면 핸드폰 자체도 화면 크기가 굉장히 다양하다. 갤럭시 노트와 아이폰 미니만 비교해봐도 꽤나 큰 차이다. 그런데 폴드니,, 플립이니,, 뭐니 제각각 개성을 갖춘 녀석들이 등장하기도 하고 태블릿, 스마트TV, 스마트워치 등이 등장하면서 다양한 화면 크기에 대응해야하는 시기가 와버린 것이다. 

 

다양한 화면 크기에 대응하기 위해서는 화면을 동적으로 조절하고 유연하게 대응해야 할 필요가 있다. 이와 더불어 개발 측면에서도 기능을 모듈화시키고 각각 따로 떼어내는 추세이기에 그에 따라 화면도 여러개로 나누어 조합하여서 다양한 레이아웃을 생성할 수 있도록 하는 모듈화와 재사용성을 높일 필요가 있었다. 이러한 요구사항에 부합하도록 도입된 것이 프래그먼트이다.

 

요새 앱 환경도 웹의 SPA(Single Page Application) 같이 메인 액티비티 1~2개에 여러 개의 프래그먼트를 사용해서 화면을 구현하는 편이라고 한다.

 

프래그먼트 특징

1. Jetpack Library Navigation / Viewpager 와 같은 아주 유용한 라이브러리들이 프래그먼트로 사용하도록 설계되어서 함께 사용하면 아주 편리하게 화면을 구성할 수 있다.

 

2. 액티비티 내에서 여러 프래그먼트가 동작할 때, 각 프래그먼트 간에는 직접적인 통신이 가능하다. 이를 통해 모듈 간의 상호작용이 가능하다.

 

3. 화면을 여러개의 프래그먼트로 나눠서 다양한 레이아웃을 생성 가능하고 여기 저기 쓸 수 있으니 재사용성에도 기여한다. (화면이 작은 디바이스면 프래그먼트 하나 딱 보여주고 화면이 크면 프래그먼트 여러 개 띄어서 보여준다.) + 동적인 UI 업데이트

 

4. 액티비티는 안드로이드 시스템에서 직접 관리하지만 프래그먼트는 프래그먼트 매니저가 간접적으로 관리한다.

이를 통해 메모리 리소스가 상대적으로 덜 소모된다고 한다.

 

5. 액티비티안에 종속되지만 자체적인 생명주기를 가지고 있다.

 

프래그먼트의 생명주기를 왜 이해해야하는가?

이 정도 대충 읽으면 프래그먼트가 대세구만! 하는 느낌이 온다. 그럼 이제 잘 사용하고 싶어진다. 프래그먼트를 잘 사용하려면 프래그먼트의 생명주기를 잘 이해해야한다.

 

또한, 메모리 누수를 방지하기 위해서 프래그먼트의 각 단계별 상황에서 어떤 적절한 액션을 취해서 대처해야할지 알아야 하기에 잘 이해해놓아야 한다.

 

메모리 누수(Memory Leak) 란 한정된 자원을 가진 프로그램이 할당한 메모리를 제대로 해제하지 않아 발생하는 현상이다.

즉, 일꾼들에게 작업공간을 주고서 작업이 다 끝났으면 다시 다른 일꾼에게 그 공간을 잘 할당해주어야 하는데 노는 공간이 많이 발생하면서 계속 누적되고 시스템 자원이 낭비되고 결국 성능에 악영향을 끼치는 현상이다.

 

우리가 객체를 참조하고 더 이상 쓰지 않거나 필요하지 않은 시점에선 적절하게 메모리를 해제시켜야 성능과 안정성을 높일 수 있다. 그런 시점을 알려면? 생명주기 이해 해야한다!

 

위 그림은 안드로이드 공식 문서에서 제공한 그림이다. 보면 프래그먼트의 자체의 생명주기(왼쪽)와 프래그먼트 뷰의 생명주기를 구분해서 표현한다. 이 둘 주기의 차이점을 잘 파악해두면 메모리 누수나 런타임 오류를 방지할 수 있다.

 

우선 액티비티 생명주기와의 콜백함수 차이점을 보면 onCreateView() onCreatedView(), onViewStateRestored() 와 onSaveInstanceState(), onDestroyView()가 있다.

 

차이점만 인지하고 이제 생명주기를 알아보자.

 

onAttach() & onCreate()

(+내 메모리에 잘 넣기 위해서 조금 각색해보았다. 정확한 표현이 아닐 수 있기에 (?) 표시해두었다.)

Fragment의 압축파일(?) 같은 녀석이 프래그먼트 매니저를 통해서 호스트 액티비티에 attach되고, onAttach() 작업이 완료되면 onCreate()에서 프래그먼트 자체가 압축해제(?) 되어 생성된다.

 

생명주기 그림을 보면 알겠지만 아직 프래그먼트 뷰 생성 전이므로 뷰와 관련된 작업을 이 메소드에서 진행하는건 적절하지 않다.

 

한줄 요약

onAttach() -> 프래그먼트가 호스트 액티비티에 attach 된다.

onCreate() -> attach된 프래그먼트 자체가 생성된다.

 

onCreateView & onViewCreated()

onCreateView에서 프래그먼트 뷰가 초기화되고 완료됨과 동시에 onViewCreated()를 호출하여 완전히 생성된 뷰 객체를 반환한다. onCreateView에서 레이아웃을 inflate 하기에 뷰 객체들을 참조할 수 있지만, 안정적으로 뷰 객체의 생성이 보장이 된 onViewCreated에서 참조하는 것이 안전하다. (+ 여기에서 findViewById, 뷰 바인딩, LiveData 옵저빙, 각종 어댑더 세팅하면 된다.)

 

이제 슬슬 생명주기를 왜 알아야하는지 감이 오기 시작하죠?? (어디서 어떤 처리를 해야하는지 알겠죠?)

 

onCreateView() - > 프래그먼트 뷰 초기화 + 뷰 객체 반환

onViewCreated() -> 뷰의 생성 완료를 보장

onViewStateRestored() 

프래그먼트의 상태를 복원할 때 호출된다. 프래그먼트의 뷰 계층 구조와 연관된 상태를 복원하는데 사용된다.

많은 블로그에서 이렇게 설명한다. 사실 이렇게 들으면 초보입장에서는 알아듣기 힘든 것 같다.

 

우선, 이 콜백메소드는 일반적으로 프래그먼트가 소멸되고 다시 생성될 때 호출된다.

쉽게 이야기하자면, 우리가 앱을 보다가 잠시 떠났다가 들어왔을 때 다 초기화되어있을 수도 있지만 요즘 앱들은 내가 나갔다가 와도 내가 보던 그 화면 위치, 내가 작성하던 텍스트 등 포커싱 아웃 전 내 UI 현재 상태를 유지하고 있다.

 

그 상태들을 복원시킬 때 쓰이는게 이 메소드다. 

 

onViewStateRestored()  - > 소멸된 프래그먼트 상태 복원

 

onStart()

프래그먼트가 사용자에게 보여질 수 있을 때 호출되며 이 때부터 사용자에게 프래그먼트 뷰가 보이게 된다. 이 시점부터 프래그먼트는 자식 프래그먼트매니저를 통해 프래그먼트의 추가, 교체, 제거 등의 작업으로 관리되어진다.

 

onResume()

액티비티와 마찬가지로 onResume() 부터 사용자와 상호작용할 수 있는 상태이다. 이 때 상호작용에 필요한 작업을 수행하면 된다. 

 

onPause()

프래그먼트가 일시 중지될 때 호출된다.

onPause()에서는 사용자와의 상호작용이 중단되는 시점에서 필요한 작업을 수행한다.

 

onStop()

프래그먼트가 사용자에게 더 이상 표시되지 않을 때 호출된다.

onStop()에서는 UI 업데이트 및 리소스 정리와 같은 작업을 수행한다.

 

onSaveInstanceState()

프래그먼트가 소멸되기 전에 호출되며, 프래그먼트의 현재 상태를 저장하는 데 사용됩니다. 주로 화면 회전이나 다른 구성 변경 시에 발생하는 프로세스의 재시작으로부터 데이터를 보존하고 복원하는 데 활용됩니다. 

 

onDestroyView()

프래그먼트의 뷰 계층 구조가 소멸될 때 호출됩니다.

onDestroyView()에서는 UI와 관련된 자원을 해제하는 작업을 수행합니다.

 

onDestroy()

프래그먼트가 소멸될 때 호출됩니다.

onDestroy()에서는 필요한 자원을 해제하고 정리하는 작업을 수행합니다.

 

onDetach()

프래그먼트가 액티비티에서 분리될 때 호출됩니다.

onDetach()에서는 프래그먼트가 속한 액티비티에 대한 참조를 해제하는 작업을 수행합니다.

+ Recent posts