Compose 무작정 맛보기 [6. 설정화면 및 마무리]

ricky_0_k·2022년 7월 21일
0

Compose 맛보기

목록 보기
7/7

서론

정말 오랜만의 포스팅이다. 유구무언이다..

포스팅을 써야지 써야지는 마음 먹었는데...
정신적으로 지쳤어서 쉬고 싶다는 생각이 컸고,
이것 저것 일들이 겹쳐져 미루다가 지금까지 오게 되었다.

마음의 여유가 이제 약간은 생겼고,
개인적으로 했던 약속이나마 지켜야겠다는 생각이 들어
해당 Compose 맛보기 경험의 마무리를 지어보려 한다.

화면에 대한 소개

몇 개월만에 글을 적다보니, 내가 어떤 목차로 글을 작성했는지도 잊어버렸다...

1. 설정화면

UI 적으로는 비교적 간단하다.
각 설정 아이템 뷰가 있고 이는 리스트로 구현되어 있다.

다만 이전에 만들었던 화면들과 연결되는 요소가 많다.
(ex. 기존 점수 설정, 졸업 학점 설정 등)
위와 같이 경우에 따라 띄워지는 토스트 메시지도 다르기에
Intent 를 통해 화면 이동 및 결과 받기 작업을 해주어야 했다.

그런데 Compose 에서는 startActivityForResult, onActivityResult 를 사용하기 애매한 구조로 되어 있던 기억이었다.
마침 해당 기능이 deprecated 되어 있기도 해서 ActivityResultContracts 를 활용해 구현하였다.

설정 화면

그럼 바로 이야기를 진행해보겠다.

설정 화면 전체 View

fun SettingView(vm: SettingViewModel? = null) {
	// 1
    val context = LocalContext.current
    val activity = LocalContext.current as SettingActivity
    
    // 2
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) {
        when (it.resultCode) {
            Activity.RESULT_CANCELED -> {
                Toast.makeText(context, "작성 혹은 선택을 취소하였습니다.", Toast.LENGTH_SHORT).show()
            }
            Const.RESULT_INIT_OK -> {
                val messageText = StringBuilder()
                val score = it.data?.getIntExtra(Const.EXTRA_RESULT_INIT_SCORE, 0)
                messageText.append(
                    if (score == 45) R.string.toast_setting_to_4_5_warning.getString(context)
                    else R.string.toast_setting_change_success.getString(context)
                )
                vm?.sendToast(messageText)
                MyApplication.sCalculate()
            }
            Const.RESULT_GRADUATION_OK -> {
                vm?.sendToast(R.string.toast_setting_change_success.getString(context))
                MyApplication.sCalculate()
            }
        }
    }

    val titles = stringArrayResource(id = R.array.tv_setting_titles)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = colorResource(id = R.color.white))
    ) {
        Toolbar(
            navigationIcon = { vm?.let { BackButton(it) } },
            titleRes = R.string.tv_setting_title
        )

        Divider(
            modifier = Modifier
                .height(10.dp)
                .background(color = colorResource(id = R.color.grayBrightColor))
        )

	    // 3
        val scoreClickAction: () -> Unit = {
            launcher.launch(Intent(activity, InitActivity::class.java))
        }
        
        val graduateClickAction: () -> Unit = {
            launcher.launch(Intent(activity, GraduationActivity::class.java))
        }

        val versionClickAction: () -> Unit = {
            val str = "market://details?id=${context.packageName}"
            val intent = Intent()
            intent.action = Intent.ACTION_VIEW
            intent.data = Uri.parse(str)
            launcher.launch(intent)
        }

        val messageClickAction: () -> Unit = {
            val emailIntent = Intent(Intent.ACTION_SEND).apply {
                putExtra(Intent.EXTRA_EMAIL, arrayOf("..."))
                ...
            }
            try {
                launcher.launch(Intent.createChooser(emailIntent, "메일로 문의하기"))
            } catch (ex: android.content.ActivityNotFoundException) {
                vm?.sendToast("There are no email clients installed.")
            }
        }

        LazyColumn(modifier = Modifier.fillMaxWidth()) {
            items(count = titles.size) { index ->
                SettingItemView(
                    index,
                    titles[index],
                    when {
                        vm == null -> null
                        index == 0 -> scoreClickAction
                        index == 1 -> graduateClickAction
                        index == 2 -> versionClickAction
                        index == 3 -> messageClickAction
                        else -> null
                    }
                )
            }
        }
    }
}
  1. 지난 포스트에서도 언급했던 context 가져오는 방식이다.
    지난번에 이야기했다시피 application 차원의 context 를 가져오려면
    LocalContext.current.applicationContext 를 통해 가져와야 한다.

  2. 방금 언급했다시피 startActivityForResult()와 onActivityResult() 가 deprecated 로 되어 있다.
    이에 대해 구글은 Activity Result API 사용을 적극 권장하고 있다. 그래서 사용해보았다.

    공식 문서 를 읽다보니 "메모리 부족으로 인해 process와 Activity 가 소멸 되는 케이스" 때문에
    해당 API 사용을 적극 권장하는 듯한 생각이 들었다.

    다른 화면을 호출하여 돌아온 후 처리를 작성하는 방식은 여러가지가 있다.
    그중 눈에 보였던 것 2개 방식을 이야기하려 한다.

    1. registerForActivityResult 를 활용한 1:1 대응 방식

      // 호출 방법
      getContent.launch("image/*")
      
      ...
      
      // 결과 처리
      val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
        // Handle the returned Uri
      }

      getContent 를 통해 하나의 화면 이동에 대해, 하나의 결과 처리만 해주는 것을 확인할 수 있다.

    2. StartActivityForResult 를 활용한 1:다 대응 방식

      // 호출 방법
      getContent.launch("image/*")
      
      ...
        
      // 결과 처리
      val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
        if (result.resultCode == Activity.RESULT_OK) {
          val intent = result.data
          // Handle the Intent
        } else if (....) {
          ....
        }
      }

      startForResult 를 통해 하나의 화면 이동에 대해, 조건문으로 여러개의 결과 처리 를 해주는 것을 확인할 수 있다.

    나는 설정화면에서 1:다 대응 방식을 통해 다른 화면 호출 및 결과 처리를 해주었다.
    rememberLauncherForActivityResult 를 통해 Contract 를 등록하고 결과 처리를 해주는 건 동일하다.
    자세한 내용은 공식 문서 에서 확인이 가능하다.

  3. 각 설정 아이템 뷰에서 사용할 이벤트 명세이다. (____Action)
    2번의 연장선으로 launch(Intent) 를 통해 화면 이동을 하는 것을 확인할 수 있다.

설정 화면 아이템 View

정말 별거 없다. (사실은 내가 복습용으로 넣은 내용이라 카더라)

@Composable
fun SettingItemView(index: Int, title: String, action: (() -> Unit)?) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(
                enabled = true,
                interactionSource = remember { MutableInteractionSource() },
                indication = rememberRipple(bounded = true),
                onClick = { action?.let { it() } }
            )
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 32.dp, end = 32.dp, top = 16.dp, bottom = 16.dp)
        ) {
            Text(
                modifier = Modifier.align(Alignment.CenterStart),
                text = title,
                fontSize = 15.sp,
                color = colorResource(id = R.color.defaultTextColor)
            )
            if (index == 2)
                Text(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    text = "v${BuildConfig.VERSION_NAME}",
                    fontSize = 15.sp,
                    color = colorResource(id = R.color.defaultTextColor)
                )
            else
                Image(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    painter = painterResource(id = R.drawable.setting_next),
                    contentDescription = "setting_next"
                )
        }

        Divider(
            modifier = Modifier
                .height(1.dp)
                .background(color = colorResource(id = R.color.statisticTabColor))
        )
    }
}

Modifier, remember, clickable 을 활용한 단출한 코드이다.

이후로 내가 한 작업들

난 설정화면 작업 이후로 아래 작업들을 수행했다.

  1. 패키지 구조 일괄 정리
  2. Application 에 있었던 비즈니스 로직 Repository 로 옮김
  3. 일부 비효율적인 로직 리펙터링
  4. DB 로직 정리
  5. 자잘한 버그 수정 및 기능 개선 (ex. x학년 x학기 개행 안되는 내용, 꺾은선 그래프 커스텀뷰로 만들기 등)
  6. Clean Architecture 개념에 최대한 입각하여 프로젝트 멀티 모듈화
  7. Room 테이블 정리 및 마이그레이션 처리
  8. 오버 엔지니어링 (백그라운드에서 종료될 시 shimmer view 보여주기 등) 및 주석 정리

한 10일 정도 걸린 것 같고, 그 이후로 바로 런칭을 했다.
원래는 위 내용들도 싸그리 포스팅 주제로 다루려고 했으나.....
보여주기 위험한 부분들도 보이고, 시리즈의 끝이 보이지 않을듯하여 이렇게 언급만 하고 마무리를 지으려 한다.

마무리

이전 포스트에서 살짝 언급했었지만,
나는 해당 앱을 주제로 새로운 기능 개선유저의 이야기를 반영하는 모든 과정 하나하나를
포스트로 작성하려 했었다. 일종의 로그형 포스트랄까?

실제로 첫 주제로 잡고 있던 것도 있었다.
멀티 모듈에 맞춘 CD 로직 을 작성중이었고,
그에 따른 versionCode 업로드 로직도 다 만들어놓은 상태였기 때문이다.

하지만 개인의 사정이 생겨 유지보수만 개인적으로 진행하고,
포스팅할 주제가 생긴다면 그 주제를 가지고 포스트를 남기려 한다.

나중에 이야기하겠지만, 나의 2022년은 강화가 아닌 도전의 느낌이 강해졌다.
이러면서 2022년 계획도 완전히 바뀌게 되었다.

빚쟁이에게 빚을 진 느낌으로 로그형 포스트를 만들기보다는,
내가 개발하면서 마주한 빛 을 주제로 포스팅을 하는 게 더 유의미할거란 생각이 들어
이 주제는 잠시 개인적인 공간에 두고 마무리 지으려 한다.

시리즈 마무리는 지었지만
약속을 완벽하게 지키지 못한 나 자신에게
먼저 사과를 건네며 이 글을 마무리한다.

참고

  1. https://developer.android.com/jetpack/compose/libraries?hl=ko#activity_result
  2. https://developer.android.com/training/basics/intents/result?hl=ko
profile
valuable 을 추구하려 노력하는 개발자

0개의 댓글