본문 바로가기
Android App/Project(Kotlin)

[App] 비밀 다이어리

by AppJinny 2023. 1. 15.

*비밀 다이어리

-비밀번호가 설정되어 있는 다이어리

-비밀번호는 변경 가능함

 

*구조

-앱 실행

-0~9 범위를 가지고 있는 3개 <NumberPicker/> 비밀번호 선택

--초기 비밀번호 기본 값은 000

-OPEN 버튼 클릭

--저장되어있는 비밀번호 일치 시 : 텍스트를 쓸 수 있는 액티비티로 이동, 넘버피커 초기화(000)

--저장되어있는 비밀번호 불일치 시 : 알림창 생성(AlertDialog)

-비밀번호 변경 버튼 클릭(OPEN버튼 하단 작은 버튼)

--저장되어있는 비밀번호 일치 시 : 알림창 생성(Toast), 버튼의 색상이 빨강으로 변경

--저장되어있는 비밀번호 불일치 시 : 알림창 생성(AlertDialog)

-변경할 비밀번호 설정 후 다시 변경버튼 클릭

--알림창 생성(AlertDialog), 버튼의 색상이 원래 색상으로 변경, 넘버피커 초기화(000)

-어플을 종료해도 비밀번호와 다이어리 내용은 내부 저장소에 저장되며, 다시 어플 실행 시 데이터는 계속 존재함

 

 

-MainActivity.kt

/**  비밀 다이어리 만들기  **/

//1
//레이아웃 설정 : activity_main.xml
//넘버피커, 앱컴팻 버튼, 커스텀 폰트 적용

class MainActivity : AppCompatActivity() {

    //2
    //레이아웃 넘버피커를 가지고 있는 변수 선언하고
    //최대, 최소값 설정 .apply{}
    //뷰바인딩 사용없이 작성하기...
    val numPicker1: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numPicker1)
            .apply {
                minValue = 0
                maxValue = 9
            }
    }

    val numPicker2: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numPicker2)
            .apply {
                minValue = 0
                maxValue = 9
            }
    }

    val numPicker3: NumberPicker by lazy {
        findViewById<NumberPicker>(R.id.numPicker3)
            .apply {
                minValue = 0
                maxValue = 9
            }
    }

    //3
    //오픈버튼과 패스워드 변경버튼 선언
    val btnOpen: AppCompatButton by lazy {
        findViewById<AppCompatButton>(R.id.btnOpen)
    }

    val btnChangePW: AppCompatButton by lazy {
        findViewById<AppCompatButton>(R.id.btnChangePW)
    }

    //6-1
    //비밀번호 변경 시 다른 위젯이 동작하지 못 하도록
    //전역변수 생성
    var changePWMode = false

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

        //4
        //넘버피커 초기화하여 메모리에 할당
        numPicker1
        numPicker2
        numPicker3

        //5
        //오픈버튼 동작 정의
        //버튼을 눌렀을 때 저장되어있는 패스워드 값을 가져와 넘버피커 1,2,3의 숫자들과 비교
        //SharedPreference를 사용하여 기기에 저장되어있는 패스워드를 가져오기
        btnOpen.setOnClickListener {

            //6-1
            //비밀번호 변경 시 다른 위젯이 동작하지 못 하도록 메시지 띄우고
            //onCreate()에서 나가는 것인지 클릭리스너 람다에서 나가는 것인지 리턴 함수를 통해 명시
            //다시 리스너로 돌아가기...
            if (changePWMode){
                Toast.makeText(this, "비밀번호 변경 중입니다...", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            //5-1
            //SharedPreference를 사용해 password라는 파일에 저장되어 있는 값 접근하기
            //모드는 공유하지 않고 이 앱에서만 사용할 수 있는 Context.MODE_PRIVATE 모드 정의
            val passwordPreferences = getSharedPreferences("password", Context.MODE_PRIVATE)

            //5-2
            //현재 넘버피커에 설정되어있는 값 받아오기
            //문자열 타입으로 값 가져오기
            //val passwordFromUser = numPicker1.value.toString() + numPicker2.value.toString() + numPicker2.value.toString()
            val passwordFromUser = "${numPicker1.value}${numPicker2.value}${numPicker3.value}"

            //5-3
            //현재 넘버피커에 설정된 값(passwordFromUser)과
            //SharedPreference를 사용해 특정 파일에 접근한 값(passwordPreferences)을 비교
            //get메서드 입력값의 파라미터는 기본값을 지정할 수 있음
            //기본값(defaultValue)를 지정하면 해당 키의 데이터가 없을 때 지정한 기본값 반환
            //앱 처음 실행하면 데이터가 없으므로 000이 비밀번호의 기본값으로 설정된다고 보면 됨...
            if (passwordPreferences.getString("password", "000").equals(passwordFromUser)){

                //5-4
                //만약 저장되어있는 패스워드가 현재 패스워드와 같을 때
                //패스워드 성공
                //다음 페이지인 다이어리 페이지로 이동
                //인텐트를 통해 넘기기...
                startActivity(Intent(this,DiaryActivity::class.java))

                //넘버피커 값 초기화
                numPicker1.value = 0
                numPicker2.value = 0
                numPicker3.value = 0

            }else{
                //5-5
                //패스워드 실패
                //실패 다이얼로그 생성
                showErrorDialog()
            }
        }//btnOpen

        //6
        //비밀번호 변경버튼 정의
        //비밀번호 변경할 때 다른 동작을 하지 못 하도록 예외처리하기...
        //false를 가지고 있는 전역변수를 생성하여 사용함
        btnChangePW.setOnClickListener {

            //6-2
            //SharedPreference를 사용해 password라는 파일에 저장되어 있는 값 접근하는 변수생성
            val passwordPreferences = getSharedPreferences("password", Context.MODE_PRIVATE)
            //6-2-1
            //현재 피커에 설정한 번호를 변수생성하여 문자열로 저장
            val passwordFromUser = "${numPicker1.value}${numPicker2.value}${numPicker3.value}"

            //6-3
            //만약 비밀번호 변경모드라면 현재 넘버피커에 설정한 번호를
            //SharedPreference를 사용해 password라는 파일을 읽어와 .edit하여 값 변경
            if (changePWMode){

                //6-3-1
                //파일에 저장되어 있는 값을 읽어와 변경하기
                //자바에서는 .edit()함수를 이용해 .putString()형식으로 변경했으나
                //코틀린 에서는 edit함수가 이미 정의 되어 있어 쉽게 사용할 수 있음...
                //정의되어있는 edit함수를 람다 형식으로 사용하기...
                passwordPreferences.edit{

                    //6-3-2
                    //저장파일에 문자열로 저장된 번호 덮어쓰기
                    putString("password", passwordFromUser)
                    //6-3-3
                    //저장하는 값이 큰 경우에
                    //모든 작업이 끝난 후 저장하고 UI스레드를 사용하는 commit()사용하면 화면이 멈출 수 있으므로
                    //apply()를 하여 비동기적으로 바로 처리함
                    //현재 이 앱은 저장하는 값이 크지 않으므로 commit()처리를 하였음...
                    commit()
                }

                //6-3-4
                //파일의 비밀번호 변경 후 비밀번호 변경모드 다시 false로 하고
                //빨강색으로 활성화 되어있던 변경 버튼의 색상을 되돌려 놓기
                changePWMode = false
                btnChangePW.setBackgroundColor(getColor(R.color.custom_black))
                AlertDialog.Builder(this)
                    .setMessage("비밀번호가 변경되었습니다.")
                    .setNegativeButton("확인"){_,_ -> }
                    .create().show()

                //넘버피커 값 초기화
                numPicker1.value = 0
                numPicker2.value = 0
                numPicker3.value = 0

                //6-3-1 ***
                //위 passwordPreferences.edit{}코드를 더 편리하게 사용하는 방법
                //commit()의 실행 명령을 할 때... 꼭 명령을 해줘야 수정이 되는데
                //작성하다보면 이 부분을 놓치는 경우가 있음... 그래서
                //코틀린 에서는 실행 명령어를 까먹어도 이미 적용되어 있는
                //명령어를 사용할 수 있게 메서드를 정의해 두었음...
//                passwordPreferences.edit(true){
//                    val passwordFromUser = "${numPicker1.value}${numPicker2.value}${numPicker3.value}"
//                    putString("password", passwordFromUser)
//                }

            }else{
                //6-4
                //만약 비밀번호 변경모드가 활성화 되어 있지 않다면
                //활성화 될 때(::) 비밀번호가 저장되어있는 비밀번호와 맞는지 체크하여 활성화 하기...

                //6-4-1
                //저장되어있는 값과 현재 값 비교하여 실행
                if (passwordPreferences.getString("password", "000").equals(passwordFromUser)){

                    //6-4-2
                    //비밀번호가 일치 한다면 비밀번호 모드 true로 변경하고
                    //패스워드 변경모드가 활성화 되었음을 알려주는 토스트 생성
                    changePWMode = true
                    Toast.makeText(this, "변경할 비밀번호를 입력하세요.", Toast.LENGTH_SHORT).show()

                    //6-4-3
                    //변경버튼이 활성화되었으므로 버튼 백그라운드 색상을 빨간색으로 변경
                    btnChangePW.setBackgroundColor(Color.RED)

                }else{
                    //6-4-4
                    //비밀번호가 일치하지 않다면
                    //실패 다이얼로그 생성
                    showErrorDialog()
                }

            }

        }//btnChangePW

    }//onCreate

    //7
    //비밀번호가 일치하지 않을 때 생성되는 다이얼로그
    //오픈버튼과 비밀번호 변경 버튼을 눌렀을 때
    //저장되어있는 번호와 현재 넘버피커에 설정한 번호를 비교하여
    //일치하지 않았을 경우 같은 메시지를 보여줌
    //중복되는 코드를 최소화 하기 위해 메서드를 생성하여
    //각 버튼에 적용!
    fun showErrorDialog(){

        //7-1
        //Builder()를 하여 값을 세팅할 수 있음
        //실패창의 제목과 메시지 설정
        //닫기(PositiveButton)버튼 설정,
        //포지티브버튼은 두 개의 파라미터를 받기 때문에
        //람다로 두개의 파라미터와 내용 생략하기... (코드 Cmd + 클릭하여 상세보기 가능)
        //항상 .create().show()를 하여 다이얼로그 생성하기...
        AlertDialog.Builder(this)
            .setTitle("⚠️ Error")
            .setMessage("비밀번호가 일치하지 않습니다.")
            .setPositiveButton("확인"){ _, _ ->}
            .create().show()

    }//showErrorDialog()

}//MainActivity

 

-DiaryActivity.kt

//1
//액티비티 생성 후 매니페스트에 등록필수

class DiaryActivity : AppCompatActivity() {

    //4-3
    //메인 스레드에 연결되어 있는 핸들러 생성
    val handler = Handler(Looper.getMainLooper())

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

        //2
        //다이어리 액티비티의 뷰가 실행되었을 때
        //내부 저장소에서 저장되어있는 내용을 가져와 화면 에디트텍스트에 보여주기

        //SharedPreferences사용하여 내부 저장소 파일에 데이터 저장...
        //앞에서 파일이름을 password로 하여 같이 저장해도 상관 없지만
        //다이어리 내용을 가지고 있어야 할 파일명과 맞지 않이 헷갈릴 수 있기때문에
        //새 파일명을 지정해 데이터 저장하여 사용할 것임...

        //2-1
        //레이아웃의 에디트텍스트 연결
        val etContent = findViewById<EditText>(R.id.etContent)

        //2-2
        //getSharedPreferences를 하여 컨텐츠파일의 내용 가져와 변수에 저장
        val contentPreferences = getSharedPreferences("content", Context.MODE_PRIVATE)

        //2-3
        //에디트텍스트에 가져온 내용을 문자열로 저장하기
        //getString(파일명, 기본값)
        etContent.setText(contentPreferences.getString("content", ""))

        //4
        //다이어리 텍스트가 한 글자씩 변경될 때마다
        //내용을 저장을 위해 계속 실행되기 때문에 실행 낭비가 됨
        //효율성을 높이기 위해 글 작성 중 지정 시간동안 멈출 때마다 저장되도록
        //백그라운드에서 작동하는 스레드 기능 이용하기

        //4-1
        //백그라운드 스레드에 넣을 때 Runnable 인터페이스 사용***
        val runnable = Runnable {
            //4-2
            //저장소에 내용을 비동기(apply)로 저장하기
            contentPreferences.edit {
                putString("content", etContent.text.toString())
            }
            //4-3
            //새로 생성한 백그라운드 스레드는 UI에 접근할 수 없기 때문에
            //Handler를 사용하여 메인스레드와 연결하여 UI에 접근하여 수정할 수 있음
            //전역변수로 핸들러 생성...
            //로그로 텍스트 저장이 잘 되는지 확인
            Log.d("DiaryActivity", "TextSaved ${etContent.text.toString()}")

        }//runnable

        //3
        //다이어리 컨텐츠의 텍스트 내용이 변경될 때마다
        //변경된 내용을 저장하기 위해 호출하는 기능 사용
        //.addTextChangedListener{}
        etContent.addTextChangedListener {
            //5
            //백그라운드 스레드에서 일어나는 기능 구현하고
            //비동기로 저장한 내용을 핸들러를 이용해 메인과 백그라운드 연결 후
            //Hadler기능 중 postDelay기능을 사용하여
            //지정된 시간 후에 백그라운드 스레드의 기능이 구현되도록 설정

            //0.5초 이내에 텍스트 체인지가 계속 일어나고 있다면
            //runnable의 동작 실행이 되지 않도록 .removeCallbacks()
            //만약 0.5초 이상 텍스트가 변경되지 않았다면
            //핸들러를 이용해 runnable의 동작이 수행되면서 내부저장소에 저장됨

            //로그로 텍스트 변경을 잘 읽을 수 있는지 확인
            Log.d("DiaryActivity", "TextChanged :: $it")

            //5-1
            //이전에 백그라운드스레드(runnable)이 진행되고 있다면
            //지우고 다시 실행되도록...
            handler.removeCallbacks(runnable)
            //5-2
            //0.5초(500밀리초) 후에 핸들러를 이용하여 백그라운드 스레드(변수 runnable)기능 동작
            handler.postDelayed(runnable, 500)
        }

    }//onCreate
}//DiaryActivity

 

-결과

 

 

*사용코드 다시보기...

-NumberPicker 사용

--넘버피커 가로로 붙어있게 설정 : app:layout_constraintHorizontal_chainStyle="packed" 

-CustomFont 사용

--res - New - Android Resource Directory - name : font - OK

-Theme 툴바 삭제(NoActionBar)

--material테마 사용중... 새로운 테마 생성하여 AndroidManifest.xml에 적용

-AppCompatButton 사용

-android-ktx(Kotlin Android Extension)의 SharedPreference 사용

--SharedPreferences : 내부 저장소 이용(권한 설정 필요없음), 간단한 코드로 사용가능함

--참고 : https://heeyjinny.tistory.com/84

--참고사항 중 원래 SharedPreferences의 edit()함수 사용할 때 putString()을하여 변경했지만

--코틀린에서는 android-ktx기능을 이용해 미리 정의되어 있는 edit함수를 쉽게 사용할 수 있음...

-AlertDialog 사용 : .Builder로 타이틀, 메시지, 네거티브버튼 설정, .create().show()필수

-백그라운드 스레드 Runnable인터페이스로 생성

-Handler 사용

--Handler생성 : val handler = Handler(Looper.getMainLooper())

--Handler기능 사용 : .removecallBacks(), .postDelayed()