728x90

확장함수를 사용하면 기존 클래스에 쉽게 메소드를 추가할 수 있다. 

 

  • 설명
    • 코틀린에서는 자바와 달리 외부에서 클래스의 메소드를 추가할 수 있다.
    • 과도하게 사용하면 코드의 가독성을 해칠 수 있지만 장점도 존재한다.
    • 원하는 메소드가 있지만 내가 설계한 클래스가 아닐때 외부에서 메소드를 관리한다.
    • 내 목적을 위해 외부에서 관리하기 때문에 원본 클래스의 일관성을 유지할 수 있다.
  • 주의사항
    • 확장함수는 public 멤버에만 접근할 수 있고 private, protected는 접근할 수 없다.
    • private 또는 protected 멤버에 접근하려면 클래스 내부의 멤버함수 형태가 적합하다.
    • 클래스의 멤버함수처럼 상속할 수 없다.
    • 즉, 하위 클래스에서 확장함수를 재정의(오버라이드)할 수 없다.

 

  • 클래스를 변경하지 못하는 상황에서 확장함수로 메소드를 추가해서 사용할 수 있다.
    • X개발자가 클래스를 만들어서 전달해줬는데 나는 다른 기능도 추가되었으면 함
    • A개발자도 본인이 사용할 메소드가 추가적으로 필요하다고 함
    • B개발자도 본인이 사용할 메소드가 추가적으로 필요하다고 함
    • X개발자는 나름대로 확장성을 고려해서 클래스를 만들었는데.. 모든 요구를 들어주다가는 고려한 내용들을 지키지 못할것같음
    • 이때, 확장함수를 이용해서 필요한 기능들을 본인들이 추가해서 사용함

 

예시로, 이름 나이만 몸무게만 출력하는 displayInfo 메소드가 있는데 추가로 키 까지 조회 하고 싶다.

fun main() {
    fun People.getHeight() = println("키: $height") // 확장함수
    val people = People("김나박",20,70.5,180.2)
    people.disPlayInfo()     /*
                                이름: 김나박
                                나이: 20
                                몸무게: 70.5
                                            */
    people.getHeight() //키: 180.2
}
class People(val name: String, val age: Int, val weight : Double, val height : Double){
    fun disPlayInfo(){
        println("이름: $name")
        println("나이: $age")
        println("몸무게: $weight")
    }
}
728x90

SharedPreferences란?

안드로이드 앱 개발을 진행하다 보면, 앱의 데이터들을 저장하여 관리해야 할 상황이 존재한다. 데이터의 양이 많거나 중요 데이터의 경우 서버나 DB에 저장해야겠지만, 간단한 설정 값이나 문자열 같은 데이터를 저장하기 위해 DB를 사용하기는 부담스럽기 때문에 SharedPreferences를 사용하는 것이 적합하다.

SharedPreferences의 특징

  • 보통 초기 설정값이나 자동 로그인 여부 등 간단한 값을 저장하기 위해 사용
  • Application에 파일 형태로 데이터를 저장한다.
  • Application이 삭제되기 전까지 저장한 데이터가 보존된다.
  • Key-value 방식

 

MODE의 종류

 

  • MODE_PRIVATE : 생성한 Application에서만 사용 가능하다.
  • MODE_WORLD_READABLE : 외부 App에서 사용 가능, But 읽기만 가능
  • MODE_WORLD_WRITEABLE : 외부 App에서 사용 가능, 읽기/쓰기 가능

대부분은 MODE_PRIVATE를 많이 사용한다.

 

사용 예시

        binding.btnSave.setOnClickListener { //데이터 저장 버튼 눌렸을 때
            val sharedPreferences = getSharedPreferences("파일이름", Context.MODE_PRIVATE)
            val editor = sharedPreferences.edit()
            editor.putString("key",binding.editText.text.toString()) //"key" ,"value" 형태 값을 가져올 때 "key" 가 맞아야함
            editor.apply() //or commit() // apply() or commit()을 해줘야 적용됨
            Toast.makeText(this, "데이터 저장 완료", Toast.LENGTH_SHORT).show()
        }
        val sharedPreferences = getSharedPreferences("EditText", Context.MODE_PRIVATE)
        binding.editText.setText(sharedPreferences.getString("key", "디폴트 값"))
        binding.btnDelete.setOnClickListener {//데이터 삭제 버튼 눌렸을 때
            val sharedPreferences = getSharedPreferences("EditText", Context.MODE_PRIVATE)
            val editor = sharedPreferences.edit()
            editor.clear()//데이터 삭제
            editor.apply()//적용
            Toast.makeText(this, "데이터 삭제", Toast.LENGTH_SHORT).show()
            binding.editText.setText("")
        }

    }

    override fun onResume() {
        getSharedPreferences("파일이름", Context.MODE_PRIVATE).run {
            binding.editText.setText(getString("key", "디폴트 값"))
        }
        super.onResume()
    }

사용 예시

데이터를 저장할 때, commit() 보다는 apply() 가 권장된다.

(commit()은 동기 처리라서 상황에 따라 안좋은 선택이 될수 있음)

 

람다식으로도 사용 가능하다. (람다식 edit{} 은 디폴트로 apply() 가 적용됨)

        binding.btnSave.setOnClickListener {
            getSharedPreferences("파일이름", Context.MODE_PRIVATE).edit {
                //putString("key","value")
                putString("EditText", binding.editText.text.toString())
            }
            Toast.makeText(this, "데이터 저장", Toast.LENGTH_SHORT).show()
        }
        binding.btnDelete.setOnClickListener {
            getSharedPreferences("파일이름", Context.MODE_PRIVATE).edit {
                clear()
            }
            binding.editText.setText("")
            Toast.makeText(this, "데이터 삭제", Toast.LENGTH_SHORT).show()
        }

    }

    override fun onResume() {
        getSharedPreferences("파일이름", Context.MODE_PRIVATE).run {
            binding.editText.setText(getString("EditText", "디폴트 값"))
        }
        super.onResume()
    }
728x90

 

 

 

 

 

 

사용자에게 권한을 요청하는 방법 

manifest에 퍼미션 등록 ex) 접근 권한을 등록 해준다.(READ_MEDIA_IMAGES)

 

checkPermission 함수

private fun checkPermission() {
        when {
            ActivityCompat.checkSelfPermission(          //권한이 허용 되었는지 체크
                this,
                Manifest.permission.READ_MEDIA_AUDIO
            ) == PackageManager.PERMISSION_GRANTED -> {
                loadImage()
            }

            ActivityCompat.shouldShowRequestPermissionRationale(	//권한이 취소되었을 때, 
                this,
                Manifest.permission.READ_MEDIA_AUDIO
            ) -> {
                showRequestRationalDialog()			//권한취소시 교육용 다이얼로그 
            }

            else -> requestReadMediaImages()			//두 개의 상황이 아닐 때, 권한 요청 
        }
    }

 

권한을 요청하고 허용을 한 상태이면 다음 동작을 하기 위해 onRequestPermissionsResult()를 오버라이딩 해준다. 

 

원형

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

 

권한 허용시 다음 동작을 위한 코드 (권한이 허용되면, loadImage()를 부른다.)

	//요청된 권한 결과 처리
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        val readImagePermissionGranted =
            requestCode == REQUEST_READ_MEDIA_IMAGES_CODE && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
        if (readImagePermissionGranted) { //request코드 와 권한이 허용된 상태면 loadImages()
            loadImage()
        } else {
            if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.READ_MEDIA_IMAGES)){
                showRequestRationalDialog()  //권한이 거부되면 교육용 다이얼로그
            }
            else{
                showSettingDialog()     //다시 취소시 세팅다이얼로그
            }
        }
    }

 

추가로, 교육용 다이얼로그에서도 거부가 되면 showSettingDialog() 가 뜨는데 setPositiveButton 시 디테일 세팅 으로 이동하는 코드 

private fun navigateToSettings() {   //권한이 취소된 상태에서 다이얼로그에서 확인 눌렀을 때 세팅화면으로 이동
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", packageName, null)
        }
        startActivity(intent)
    }

 

전체 코드 

class SearchActivity : AppCompatActivity() {
    //request code
    companion object {
        private const val REQUEST_READ_MEDIA_IMAGES_CODE = 100
    }

    //
    private val loadImageLauncher =
        registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uriList ->
            changeImage(uriList)
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_search)
        val toolBar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolBar)
        val loadImageButton = findViewById<Button>(R.id.btn_LoadImage)
        toolBar.apply {
            title = "SearchActivity"
            setSupportActionBar(this)
        }
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        loadImageButton.setOnClickListener {
            checkPermission()
        }

    }



    //선택한 uriList 를 image로 세팅
    private fun changeImage(uriList: List<Uri>) {
        val image = findViewById<ImageView>(R.id.iv_Image)
        image.setImageURI(uriList[0])
    }
    //갤러리 접근
    private fun loadImage() {
        loadImageLauncher.launch("image/*")
    }

    //권한 요청
    private fun requestReadMediaImages() {
        ActivityCompat.requestPermissions(
            this, arrayOf(Manifest.permission.READ_MEDIA_IMAGES),
            REQUEST_READ_MEDIA_IMAGES_CODE
        )
    }
    //권한 확인
    private fun checkPermission() {
        when {
            ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_MEDIA_AUDIO
            ) == PackageManager.PERMISSION_GRANTED -> {
                loadImage()
            }

            ActivityCompat.shouldShowRequestPermissionRationale(
                this,
                Manifest.permission.READ_MEDIA_AUDIO
            ) -> {
                showRequestRationalDialog()
            }

            else -> requestReadMediaImages()
        }
    }


    private fun showRequestRationalDialog() {
        AlertDialog.Builder(this).apply {
            setMessage("사진을 가져오려면 권한이 필요합니다.")
            setNegativeButton("취소") { dialogInterface, _ ->
                dialogInterface.cancel()
            }
            setPositiveButton("확인") { _, _ -> requestReadMediaImages() }
            show()
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                finish()
                true
            }

            else -> super.onOptionsItemSelected(item)
        }
    }


    //요청된 권한 결과 처리
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        val readImagePermissionGranted =
            requestCode == REQUEST_READ_MEDIA_IMAGES_CODE && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
        if (readImagePermissionGranted) { //request코드 와 권한이 허용된 상태면 loadImages()
            loadImage()
        } else {
            if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.READ_MEDIA_IMAGES)){
                showRequestRationalDialog()  //권한이 거부되면 교육용 다이얼로그
            }
            else{
                showSettingDialog()     //다시 취소시 세팅다이얼로그
            }
        }
    }

    private fun showSettingDialog() {//셋팅 다이얼로그
        AlertDialog.Builder(this).apply {
            setMessage("권한이 취소된 상태입니다. 권한 허용 하러가기")
            setNegativeButton("취소") { dialogInterface, _ -> dialogInterface.cancel() }
            setPositiveButton("확인") { _, _ -> navigateToSettings() }
            show()
        }
    }

    private fun navigateToSettings() {   //권한이 취소된 상태에서 다이얼로그에서 확인 눌렀을 때 세팅화면으로 이동
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", packageName, null)
        }
        startActivity(intent)
    }
}
728x90

EditText를 이용해서 로그인 기능 구현하기 

먼저 필드의 유효성 검사를 하기 위해서 addTextChangedListenerTextWatcher를 사용한다.

 

원형 

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")
            }

        })

onTextChagned 에서 변화가 있을때마다 호출해줌

 

validation () 

private fun validation(editText: EditText) {  //각 필드 유효성 체크
        editText.addTextChangedListener(object : TextWatcher {
            val tv_passwordDescription = findViewById<TextView>(R.id.tv_DescriptionPassword)
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {

            }

            //텍스트 변화 있을 때 마다 실행
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

                if (editText.text.length == 0 || editText.text.isNullOrEmpty() || isEmpty(editText)
                ) {
                    when (editText.id) {
                        R.id.et_Id -> editText.error = "아이디를 입력하세요"
                        R.id.et_Email -> editText.error = "이메일을 입력하세요"
                        R.id.et_Password -> {
                            editText.error = "비밀번호를 입력하세요"
                            tv_passwordDescription.isVisible = true
                        }

                        R.id.et_ConfirmPassword -> editText.error = "비밀번호를 확인해주세요."
                        else -> ""
                    }
                } else {
                    if (editText.id == R.id.et_Password) {
                        tv_passwordDescription.isVisible = false
                        if (check(editText) && editText.text.length >= 10) { //비밀번호가 유효하고 10자 이상인지

                        } else {
                            editText.error = "10자리 이상, 특수문자, 숫자포함"
                        }

                    }
                    if (editText.id == R.id.et_ConfirmPassword) {
                        if (isEqualPassword()) {                  //password와confirmPassword가 같은지 체크

                        } else {
                            editText.error = "비밀번호가 같지 않음"
                        }
                    }
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }

        })
    }

예시 EditText의 길이가 0 이거나 공백이면 error

setOnFocusChangeListener 를 이용해서 포커스가 아웃 되었을 때 역시 비어있으면 에러 처리

 

원형 

EditText.setOnFocusChangeListener { v, hasFocus ->
            if(hasFocus){

            }else{ //포커스 아웃시 처리

            }
        }

포커스가 되어있을 때 / 아닐 때 

 

focusOut()

private fun focusOut(editText: EditText) {//EditText가 비어 있을때 포커스 아웃처리
        editText.setOnFocusChangeListener { v, hasFocus ->
            if (hasFocus) {
            } else {
                editText.error = if (editText.text.length == 0 || editText.text.isNullOrEmpty()
                ) {
                    when (editText.id) {
                        R.id.et_Id -> "아이디를 입력하세요"
                        R.id.et_Email -> "이메일을 입력하세요"
                        R.id.et_Password -> "비밀번호를 입력하세요"
                        R.id.et_ConfirmPassword -> "비밀번호를 확인해주세요"
                        else -> ""
                    }
                } else null
            }
        }
    }

 

 

정규식을 이용해서 특수문자,숫자 포함 비밀번호 만들기

비밀번호가 특수문자 와 숫자를 포함하는지 체크 하는 check()

private fun check(editText: EditText): Boolean { //특수문자 숫자 정규식
        val pwd = """^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^+\-=])(?=\S+$).*$"""
        val pattern = Pattern.compile(pwd)
        return pattern.matcher(editText.text).matches()
    }

matches() 리턴 값 -> bollean  

 

비밀번호가 같지 않으면 가입 처리가 불가능

 

비밀번호 check에서 걸러짐
입력 값이 비어있을 때
유효성 검사 완료 가입승인

 

 

SignUpActivity 전체 코드

class SignUpActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sign_up)
        val signUpButton = findViewById<Button>(R.id.btn_SignUp)
        val id = findViewById<EditText>(R.id.et_Id)
        val password = findViewById<EditText>(R.id.et_Password)
        val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
        val email = findViewById<EditText>(R.id.et_Email)
        val spinner = findViewById<Spinner>(R.id.spinner)
        val emailForm = findViewById<EditText>(R.id.et_EmailForm)
        createSpinner(spinner, emailForm)
        validation(id)
        validation(email)
        validation(password)
        validation(confirmPassword)
        focusOut(id)
        focusOut(email)
        focusOut(password)
        focusOut(confirmPassword)

        signUpButton.setOnClickListener {
            if (id.text.isNullOrEmpty() || confirmPassword.text.isNullOrEmpty() || password.text.isNullOrEmpty() || email.text.isNullOrEmpty() || emailForm.text.isNullOrEmpty()) {
                Toast.makeText(this, "입력값이 비어있습니다.", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            } else {//모든 필드가 입력된 상태
                if (isEqualPassword() && check(password)) { //유효성 검사 후 승인
                    Intent().apply {
                        putExtra("updatedId", id.text.toString())
                        putExtra("updatedPassword", password.text.toString())
                        setResult(RESULT_OK, this)
                    }
                    saveData()
                    finish()
                } else if (check(password)) { //password 와 confirmPassword가 같을 때 처리
                    Toast.makeText(this, "비밀번호가 일치하지 않습니다.", Toast.LENGTH_SHORT).show()
                } else { //둘 다 아닐 때
                    Toast.makeText(this, "비밀번호는 10자리 이상, 특수문자, 숫자포함 입니다.", Toast.LENGTH_SHORT)
                        .show()
                }

            }
        }


    }

    private fun saveData() {   //입력 값 저장
        val id = findViewById<EditText>(R.id.et_Id)
        val password = findViewById<EditText>(R.id.et_Password)
        val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
        val email = findViewById<EditText>(R.id.et_Email)
        val emailForm = findViewById<EditText>(R.id.et_EmailForm)
        getSharedPreferences(INFO, Context.MODE_PRIVATE)?.edit {
            putString(ID, id.text.toString())
            putString(EMAIL, email.text.toString())
            putString(EMAIL_FORM, emailForm.text.toString())
            putString(PASSWORD, password.text.toString())
            putString(CONFIRM_PASSWORD, confirmPassword.text.toString())
            apply()
        }
    }

    private fun getSaveData() {  //저장한 값 불러오기
        val id = findViewById<EditText>(R.id.et_Id)
        val password = findViewById<EditText>(R.id.et_Password)
        val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
        val email = findViewById<EditText>(R.id.et_Email)
        val emailForm = findViewById<EditText>(R.id.et_EmailForm)
        getSharedPreferences(INFO, Context.MODE_PRIVATE).run {
            id.setText(getString(ID, ""))
            email.setText(getString(EMAIL, ""))
            password.setText(getString(PASSWORD, ""))
            confirmPassword.setText(getString(CONFIRM_PASSWORD, ""))
            emailForm.setText(getString(EMAIL_FORM, ""))
        }

    }

    override fun onResume() {  //onResume 시 값 불러옴
        getSaveData()
        super.onResume()
    }

    override fun onDestroy() {  //onDestroy 시 값 저장
        saveData()
        super.onDestroy()
    }

    private fun focusOut(editText: EditText) {//EditText가 비어 있을때 포커스 아웃처리
        editText.setOnFocusChangeListener { v, hasFocus ->
            if (hasFocus) {
            } else {
                editText.error = if (editText.text.length == 0 || editText.text.isNullOrEmpty()
                ) {
                    when (editText.id) {
                        R.id.et_Id -> "아이디를 입력하세요"
                        R.id.et_Email -> "이메일을 입력하세요"
                        R.id.et_Password -> "비밀번호를 입력하세요"
                        R.id.et_ConfirmPassword -> "비밀번호를 확인해주세요"
                        else -> ""
                    }
                } else null
            }
        }
    }

    private fun validation(editText: EditText) {  //각 필드 유효성 체크
        editText.addTextChangedListener(object : TextWatcher {
            val tv_passwordDescription = findViewById<TextView>(R.id.tv_DescriptionPassword)
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {

            }

            //텍스트 변화 있을 때 마다 실행
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {

                if (editText.text.length == 0 || editText.text.isNullOrEmpty() || isEmpty(editText)
                ) {
                    when (editText.id) {
                        R.id.et_Id -> editText.error = "아이디를 입력하세요"
                        R.id.et_Email -> editText.error = "이메일을 입력하세요"
                        R.id.et_Password -> {
                            editText.error = "비밀번호를 입력하세요"
                            tv_passwordDescription.isVisible = true
                        }

                        R.id.et_ConfirmPassword -> editText.error = "비밀번호를 확인해주세요."
                        else -> ""
                    }
                } else {
                    if (editText.id == R.id.et_Password) {
                        tv_passwordDescription.isVisible = false
                        if (check(editText) && editText.text.length >= 10) { //비밀번호가 유효하고 10자 이상인지

                        } else {
                            editText.error = "10자리 이상, 특수문자, 숫자포함"
                        }

                    }
                    if (editText.id == R.id.et_ConfirmPassword) {
                        if (isEqualPassword()) {                  //password와confirmPassword가 같은지 체크

                        } else {
                            editText.error = "비밀번호가 같지 않음"
                        }
                    }
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }

        })
    }

    private fun isEmpty(editText: EditText): Boolean {          //공백 문자 처리
        val empty = editText.text.toString()
        empty.replace(" ", "")
        return empty.contains(" ")
    }

    private fun createSpinner(spinner: Spinner, emailForm: EditText) {   //스피너 생성 및 리스너
        spinner.apply {
            adapter = ArrayAdapter.createFromResource(//스피너 생성
                baseContext,
                R.array.email,
                android.R.layout.simple_list_item_1
            )
            onItemSelectedListener = object : OnItemSelectedListener { //리스너
                override fun onItemSelected(
                    parent: AdapterView<*>?,
                    view: View?,
                    position: Int,
                    id: Long
                ) {
                    if (spinner.selectedItemPosition == 3) {    //드롭메뉴 직접입력 선택시 직접입력하게 비움
                        emailForm.apply {
                            setText("")
                            setHint("직접입력")
                        }
                    } else emailForm.setText(spinner.selectedItem.toString()) // 드롭메뉴 아이템선택시 EditText 입력
                }

                override fun onNothingSelected(parent: AdapterView<*>?) {
                }
            }
        }
    }

    private fun check(editText: EditText): Boolean { //특수문자 숫자 정규식
        val pwd = """^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^+\-=])(?=\S+$).*$"""
        val pattern = Pattern.compile(pwd)
        return pattern.matcher(editText.text).matches()
    }


    private fun isEqualPassword(): Boolean {   //password 와 confirmPassword가 같은지 체크
        val password = findViewById<EditText>(R.id.et_Password)
        val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
        return password.text.toString() == confirmPassword.text.toString()
    }
}
728x90

Acitivty 의 생명주기를 알아야 하는 이유?

- 앱의 완성도와 안정성을 높이기 위해 반드시 알아야 함

 

다른 앱으로 전환 시, 비정상 종료 되는 문제
사용자가 앱을 사용하지 않는데, 시스템 리소스가 소비되는 문제
사용자가 앱을 나갔다가 돌아왔을 때, 진행상태가 저장되지 않는 문제
화면이 가로 ↔ 세로 전환 될 때, 비정상 종료되거나, 진행상태가 저장되지 않는 문제 등이 있을 때, 해결하기 위해 인지 하고 있어야 함 

 

콜백


onCreate()

  • 필수적으로 구현해야함
  • Activity 의 생명주기 중 한 번만 발생해야하는 로직을 실행
    • 멤버 변수 정의
    • UI 구성 (setContentView, xml 레이아웃 파일 정의)
  • saveInstanceState 매개 변수 수신 → Activity 이전 저장 상태가 포함된 Bundle 객체

 

onStart()

  • Activity 가 사용자에게 표시
  • 앱은 Activity 를 포그라운드로 보내 상호작용할 수 있도록 준비

 

onResume()

  • Activity 가 포그라운드에 표시되어, 사용자와 상호 작용 할 수 있는 상태
  • 앱에서 포커스가 떠날 때 까지 onResume 상태에 머무름

 

onPause()

  • 사용자가 활동을 떠나는 첫 번째 신호
  • 매우 짧음
  • 활동이 포그라운드에 있지 않지만, 잠시 후 다시 시작할 작업을 일시 중지 하거나 조정
  • ex) 반투명 Activity 가 띄워져 포커스는 없지만 화면에 보이는 상태
  • 이 상태를 통해서, 실행중이지 않을 때 필요하지 않은 리소스를 해지할 수 있음
  • 이 상태에서, 데이터를 저장하거나, 네트워크 호출, DB 의 IO 작업을 하면 안됨
    • 매우 짧은 시간이라 메서드가 끝나기 전에 Activity 가 종료될 수 있음

onStop()

  • Activity 가 사용자에게 더 이상 표시 되지 않는 상태
  • CPU 를 비교적 많이 소모하는 종료 작업을 실행해야함
    • DB 저장
  • Activity 가 중단되면, Android OS 에서 리소스 관리를 위해, 해당 Activity 가 포함된 프로세스를 소멸시킬 수 있음

 

onDestroy()

  • Activity 가 완전히 종료되기 전에 실행
  • 호출되는 케이스
    • finish 호출 되어 Activity 가 종료될 때
    • configurationChange (ex 기기 회전, 멀티 윈도우) 로 인해, 시스템이 Activity 를 일시적으로 소멸 시킬 때

출처 : https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko

 

728x90

Intent 란 ?

  • 인텐트(Intent)는 일종의 메시지 객체이다.
  • 이것을 사용해 다른 앱 구성요소(액티비티, 서비스, 브로드 캐스트리시버)로 작업을 요청 할수 있다.

Intent 예

Intent의 유형 

1). 명시적 인텐트(Explicit Intent)

명시적 인텐트는 특정한 컴포넌트를 직접적으로 호출할 때 사용되는 인텐트다. 이 방법으로, 개발자는 인텐트 객체에 시작하고자 하는 구성 요소의 이름을 명확하게 설정하고 startActivity() 또는 startService() 메소드를 통해 해당 컴포넌트를 실행시킨다.

  • 활용 예시:
    • 이 방식은 주로 앱 내부에서 다른 액티비티나 서비스를 시작할 때 사용된다. 예를 들어, 메인 액티비티에서 사용자의 입력을 받아 세부 정보를 표시하는 새로운 액티비티를 띄울 때 명시적 인텐트가 활용된다.

2). 암시적 인텐트(Implicit Intent)

암시적 인텐트는 특정한 컴포넌트를 명시하지 않고, 수행하고자 하는 일반적인 작업을 인텐트 객체에 설정하여 startActivity() 메소드에 넘긴다. 이 때, 안드로이드 시스템은 이 인텐트를 처리할 수 있는 모든 애플리케이션을 검색하여 적합한 인텐트 필터를 가진 컴포넌트를 찾아 실행시킨다.

  • 인텐트 필터:
    • 인텐트 필터는 특정 인텐트에 반응하는 액티비티, 서비스 또는 브로드캐스트 리시버의 능력을 정의한다. 즉, 어떤 인텐트를 수신할 준비가 되어 있는지를 나타내는 설정이다.
  • 일반적 사용법:
    • 암시적 인텐트는 주로 다른 앱의 컴포넌트를 실행시키는 데 사용된다. 예를 들어, 사용자가 '지도 보기'를 요청할 때, 해당 작업을 처리할 수 있는 모든 앱 중 사용자가 선택할 수 있도록 안드로이드 시스템이 목록을 제공한다.
     

암시적 인텐트 예

3). 인텐트(Intent) 객체 분석하기

인텐트는 안드로이드 앱의 구성 요소들 사이에서 실행을 위임하거나 정보를 전달하는 역할을 한다. 다음은 인텐트의 주요 구성 요소와 그에 대한 실제 사용 예 이다.

1). 컴포넌트 이름 (Component Name)

  • 타겟 컴포넌트의 이름을 명시하여, 인텐트가 전달될 정확한 대상을 지정한다.
  • 예: **new Intent(context, TargetActivity.class)**에서 **TargetActivity.class**가 컴포넌트 이름이다.
  • 컴포넌트 이름이 없으면, 인텐트는 암시적으로 처리되고, 시스템이 적절한 대상을 찾는다.

2). 액션 (Action)

  • 인텐트가 수행해야 할 일반적인 작업을 정의한다.
  • 예: **Intent.ACTION_VIEW**는 사용자에게 데이터를 보여줄 때 사용된다.
  • 예: **Intent.ACTION_DIAL**은 전화 다이얼을 열기 위해 사용된다.

3). 데이터 (Data)

  • 작업을 수행하는 데 필요한 데이터의 URI를 지정합니다.
  • 예: **Uri.parse("tel:12345")**는 전화 앱에 전화번호 데이터를 제공한다.
  • 데이터는 보통 Uri 객체로 액션과 결합하여 사용된다.

4). 카테고리 (Category)

  • 인텐트의 유형을 더 구체적으로 지정하여, 어떤 컴포넌트가 처리할 수 있는지 알려준다.
  • 예: **Intent.CATEGORY_HOME**은 홈 화면 애플리케이션을 시작할 때 사용된다.

5). 엑스트라 (Extras)

  • 복잡한 데이터를 인텐트에 전달하기 위한 키-값 쌍의 추가 정보다.
  • 예: **intent.putExtra("extra_key", "value")**에서 **"extra_key"**는 전달할 데이터의 키이며, **"value"**는 실제 값 이다.
  • 엑스트라를 통해 기본 타입부터 Serializable 객체까지 다양한 데이터를 전달할 수 있다.

4). 명시적인텐트로 다른 액티비티 시작하기

명시적 인텐트(Explicit Intent)는 특정한 액티비티를 시작할 때 사용되며, 안드로이드에서는 이를 통해 현재 액티비티에서 다른 액티비티로 전환할 수 있다.

// 현재 액티비티에서 AnotherActivity를 시작하기 위한 명시적 인텐트 생성
val intent = Intent(this, AnotherActivity::class.java)

// 인텐트에 데이터 추가 (옵션)
intent.putExtra("key", "value")

// AnotherActivity 시작
startActivity(intent)
  1. 인텐트 생성: Intent 클래스의 인스턴스를 생성한다. 첫 번째 매개변수로는 현재 컨텍스트(this)를, 두 번째 매개변수로는 시작하고자 하는 액티비티의 Class 객체를 전달한다. 위 예제에서 **AnotherActivity::class.java**는 시작하고자 하는 대상 액티비티를 나타낸다.
  2. 데이터 전달 (선택 사항): putExtra 메소드를 사용하여 인텐트에 추가 데이터를 삽입할 수 있다. 이는 키-값 쌍으로 이루어져 있으며, 시작될 액티비티에서 이 데이터를 사용할 수 있다.
  3. 액티비티 시작: startActivity 메소드를 호출하여 인텐트를 사용한다. 이 호출이 실행되면 안드로이드 시스템은 명시된 인텐트에 따라 **AnotherActivity**를 시작한다.

명시적 인텐트는 주로 앱 내부의 액티비티 전환에 사용되며, 암시적 인텐트와 달리 시스템이 액티비티를 추론할 필요 없이 개발자가 직접 지정한다.

728x90

안드로이드 4대 컴포넌트 (Activity, Service, Broadcast Receiver, Content Provider)중 하나 

  • 설명: 액티비티는 사용자가 직접 상호작용하는 화면을 말한다. 예를 들어 이메일 앱에서는 이메일 목록을 보여주는 화면, 이메일 작성 화면 등이 각각 독립된 액티비티다.
  • 역할: 사용자 인터페이스(UI)를 담당하며, 사용자의 입력에 반응하는 이벤트 기반 프로그래밍을 가능하게 한다.

예시

액티비티(Activity)는 안드로이드 애플리케이션의 핵심 구성 요소로서 사용자가 직접 상호 작용하는 창(window)을 의미한다. 각 액티비티는 독립적인 화면으로서, 사용자에게 다양한 작업을 수행할 수 있는 인터페이스를 제공한다.

  • 상호 작용의 예:
    • 액티비티는 사용자가 전화를 거는 화면, 사진을 찍는 인터페이스, 이메일을 보내거나 지도를 확인하는 등의 다양한 기능을 수행할 때 마다 등장한다.
  • UI 구성:
    • 각 액티비티는 하나의 창을 가지며, 이 창에는 '뷰(View)'라 불리는 객체들을 통해 사용자 인터페이스가 구성된다. 뷰 객체들은 버튼, 텍스트 필드, 이미지 등 사용자가 볼 수 있는 모든 요소들을 포함한다.
  • 애플리케이션의 시작:
    • 모든 안드로이드 애플리케이션은 최소 한 개 이상의 액티비티를 포함하고 있으며, 앱이 시작될 때 시스템은 지정된 '메인' 액티비티를 실행하여 사용자에게 첫 화면을 보여준다.

액티비티는 사용자가 애플리케이션과 상호 작용하는 방식을 정의하는 중요한 요소이며, 안드로이드에서의 사용자 경험을 결정짓는 중추적인 역할을 담당한다.

 

모든 Activity컴포넌트는 Android Manifest파일에 등록되어야 함

 

Android Manifest 역할

  • AndroidManifest.xml 파일은 애플리케이션의 기본 구조를 정의하고, 시스템이 앱을 어떻게 실행해야 하는지를 알려주는 중요한 지시사항을 담당한다.
  • 패키지 이름 설정:
    • 앱의 고유 식별자로서, 애플리케이션의 패키지 이름을 설정한다. 이 이름은 Google Play와 같은 앱 스토어에서 앱을 구별하는 데 사용된다.
  • 구성 요소 선언:
    • 액티비티, 서비스, 브로드캐스트 리시버, 콘텐트 프로바이더와 같은 애플리케이션 구성요소들의 존재를 시스템에 알린다.
  • 권한 설정:
    • 앱이 사용자의 기기에서 특정 작업을 수행하기 위해 필요한 권한을 명시한다. 또한, 다른 앱이 이 애플리케이션과 상호작용하기 위해 필요한 권한도 설정할 수 있다.

 

 

728x90

뷰(View)란 무엇일까?

  • 안드로이드 앱의 UI를 구성하는 기본 단위는 뷰(View)이다.

뷰(View)의 구성

  1. 위젯(Widget)
    • View의 서브 클래스로서, 앱 화면을 구성하는 시각적인 모양을 지닌 UI요소
    • 예) 버튼,메뉴,리스트 등
  2. 레이아웃(Layout)
    • ViewGroup의 서브 클래스로서, 다른 뷰(위젯 혹은 레이아웃)를 포함하면서 이들을 정렬하는 기능을 지닌 UI요소

예시

 

위젯(Wdiget)는 과연 무엇일까?

  • View의 서브 클래스 중에서 화면에 보이는 것들을 말한다.
  • 대표적인 위젯 TextView, EditText, Button 등

예시

View란?

  • View클래스는 모든 UI 컴포넌트들의 부모 클래스
  • View클래스의 속성은 모든 UI컴포넌트들에서 공통적으로 사용 할 수 있다.
728x90

Object

 

object 키워드를 써서 특별한 단일 인스턴스를 만들 수 있다.  싱글톤(singleton) 패턴 인스턴스를 의미한다.

  • 싱글톤을 쉽게 만들 수 있는 키워드  
  • 여러 스레드에서 동시에 사용하려고 하더라도 딱 하나의 인스턴스만 생성된다.
  • 생성자 사용불가
  • 프로퍼티, 메서드, 초기화 블록은 사용 가능 
  • 클래스를 정의함과 동시에 객체를 생성한다. 

Object 식

 

object 식을 활용해서 속성들을 담고 있는 오브젝트를 쉽게 만들 수 있으며, 이렇게 간단히 할 때는 클래스를 선언할 필요 없이 오브젝트를 하나 만들어서 그 안에 멤버들을 선언해 두고 접근해 쓸 수 있다.

fun main() {
    rentPrice(10, 2, 1)
}

fun rentPrice(standardDays: Int, festivityDays: Int, specialDays: Int){

    val dayRates = object {
        var standard: Int = 30 * standardDays
        var festivity: Int = 50 * festivityDays
        var special: Int = 100 * specialDays
    }

    val total = dayRates.standard + dayRates.festivity + dayRates.special

    print("Total price: $$total") //Total price: $500 //함수호출 해당오브젝트 생성
}

 

 

Object 단일 인스턴스

 

object 선언을 할 수도 있다. 식의 형태가 아니라면, 변수에 값을 대입하는 용도로 쓸 수 없다. 별도의 인스턴스를 만들지 않고, 해당 오브젝트의 멤버에 곧바로 접근할 수 있다.

fun main() {
    println(Counter.count)    /*최초 초기화       생성 시점에 최초 초기화
                                0*/          
    Counter.countUp()           
    Counter.countUp()
    println(Counter.count)     //2 
}

object Counter{
    init {
        println("최초 초기화")
    }
    var count = 0
    fun countUp(){
        count++
    }
}

 

싱글톤으로 생성하면 객체를 한 번만 생성해서 계속 사용하기 때문에 초기화는 한번만 진행 됨 (값 공유)

(만약 class로 만들었다면 각각 다른 객체가 생성됨)

다른 클래스나 인터페이스도 상속가능하다.

Object는 안드로이드에서 에러코드를  정의할 때 많이 사용 

 

 

Companion Objects (동반 객체)

클래스 정의 안쪽에 선언하는 동반 오브젝트(객체)를 만들 수 있다. 문법적으로 자바의 정적(static) 메서드와 비슷하게 클래스 이름을 통해서 해당 오브젝트 멤버에 접근할 수 있다.

  • Java의 static 과 동일한 역할
  • 클래스 내에 하나만 생성할 수 있음
fun main() {
    Book.NAME       //인스턴스를 만들지 않아도 변수나 상수에 접근가능
    Book.create()  //인스턴스를 아직 만들지 않아도 클래스 메소드처럼 사용가능
}

class Book{
    companion object{
        const val NAME = "name"
        fun create() = Book()
    }
}

자바의 static 과 동일하기 때문에 인스턴스를 만들지 않아도 메소드나 프로퍼티에 바로 접근이 가능하다.  

728x90

코틀린은 클래스를 설계할 때 안정성을 위해 반드시 변수의 값을 초기화할것을 권장함

 

초기화 지연 ?

변수를 선언할 때 값을 지정하지 않고, 나중에 지정할 수 있는 방법이다. 

 

목적 -> 메모리를 효율적으로 사용하기 위해서, null safe 한 value를 사용하기 위해서 

 

lateinit , var

  • 변수 타입을 지정해줘야함 
  • 원시타입(primitive type)은 사용할 수 없음 (Int, Long, Double, Boolean, Char, Short, Byte, Float)
  • 선언 후, 나중에 초기화 해줘도 됨
class Test{
    lateinit var text : String    //primitive 타입 사용불가 
    fun lateInit (){
            if(::text.isInitialized){ // :: 으로 참조 
                println("초기화 됨")
            }else{
                println("초기화 전")
            }
    }
}
fun main() {
    val test = Test()
    test.lateInit()                     //초기화 전
    test.text = "초기화 시점"  
    test.lateInit()                    // 초기화 됨
    println("text = ${test.text}")  //text = 초기화 시점 
}

 

lazy , val

  • val(상수) 이기 때문에 값을 다시 변경할 수 없다. 
  • 호출 시점에 by lazy {...} 에 정의해둔 블록 부분의 초기화를 진행한다. 
  • 클래스 생성자에서 사용불가 
  • 원시 타입(primitive type) 사용 가능 
  • 지역 변수에서도 사용 가능 
class ByLazy{
    init {
        println("init Block") // 최초 초기화 선언알림
    }
    val lazy : String by lazy { "Lazy Test" }
    fun flow(){
        println("초기화 진행 전")             //초기화 되기 전 
        println("$lazy : 최초 초기화")       //최초 초기화 시점
        println("$lazy : 최초 초기화 후")     //초기화된 값 사용(불변)
    }
}
fun main(){
    val lazyInstance = ByLazy()  //init Block 
    lazyInstance.flow() /*
                             init Block 
                             초기화 진행 전
                             Lazy Test : 최초 초기화
                             Lazy Test : 최초 초기화 후                
                        */
}

 

lazy 는 선언과 동시에 초기화를 해줘야 하고, 호출시점에 초기화가 이루어진다.

  Primitive Type null-able local variable
lateinit X X X
lazy O O O

+ Recent posts