정말 오랜만의 포스팅이다. 유구무언이다..
포스팅을 써야지 써야지는 마음 먹었는데...
정신적으로 지쳤어서 쉬고 싶다는 생각이 컸고,
이것 저것 일들이 겹쳐져 미루다가 지금까지 오게 되었다.
마음의 여유가 이제 약간은 생겼고,
개인적으로 했던 약속이나마 지켜야겠다는 생각이 들어
해당 Compose 맛보기 경험의 마무리를 지어보려 한다.
몇 개월만에 글을 적다보니, 내가 어떤 목차로 글을 작성했는지도 잊어버렸다...
UI 적으로는 비교적 간단하다.
각 설정 아이템 뷰가 있고 이는 리스트로 구현되어 있다.
다만 이전에 만들었던 화면들과 연결되는 요소가 많다.
(ex. 기존 점수 설정, 졸업 학점 설정 등)
위와 같이 경우에 따라 띄워지는 토스트 메시지도 다르기에
Intent 를 통해 화면 이동 및 결과 받기 작업을 해주어야 했다.
그런데 Compose 에서는 startActivityForResult, onActivityResult 를 사용하기 애매한 구조로 되어 있던 기억이었다.
마침 해당 기능이 deprecated 되어 있기도 해서 ActivityResultContracts 를 활용해 구현하였다.
그럼 바로 이야기를 진행해보겠다.
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
}
)
}
}
}
}
지난 포스트에서도 언급했던 context 가져오는 방식이다.
지난번에 이야기했다시피 application 차원의 context 를 가져오려면
LocalContext.current.applicationContext 를 통해 가져와야 한다.
방금 언급했다시피 startActivityForResult()와 onActivityResult() 가 deprecated 로 되어 있다.
이에 대해 구글은 Activity Result API 사용을 적극 권장하고 있다. 그래서 사용해보았다.
공식 문서 를 읽다보니 "메모리 부족
으로 인해 process와 Activity 가 소멸
되는 케이스" 때문에
해당 API 사용을 적극 권장하는 듯한 생각이 들었다.
다른 화면을 호출
하여 돌아온 후 처리
를 작성하는 방식은 여러가지가 있다.
그중 눈에 보였던 것 2개 방식
을 이야기하려 한다.
registerForActivityResult 를 활용한 1:1 대응
방식
// 호출 방법
getContent.launch("image/*")
...
// 결과 처리
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
getContent 를 통해 하나의 화면 이동
에 대해, 하나의 결과 처리
만 해주는 것을 확인할 수 있다.
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 를 등록하고 결과 처리를 해주는 건 동일하다.
자세한 내용은 공식 문서 에서 확인이 가능하다.
각 설정 아이템 뷰에서 사용할 이벤트 명세이다. (____Action
)
2번의 연장선으로 launch(Intent)
를 통해 화면 이동을 하는 것을 확인할 수 있다.
정말 별거 없다. (사실은 내가 복습용으로 넣은 내용이라 카더라)
@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 을 활용한 단출한 코드이다.
난 설정화면 작업 이후로 아래 작업들을 수행했다.
한 10일 정도 걸린 것 같고, 그 이후로 바로 런칭을 했다.
원래는 위 내용들도 싸그리 포스팅 주제로 다루려고 했으나.....
보여주기 위험한 부분들도 보이고, 시리즈의 끝이 보이지 않을듯하여 이렇게 언급만 하고 마무리를 지으려 한다.
이전 포스트에서 살짝 언급했었지만,
나는 해당 앱을 주제로 새로운 기능 개선
및 유저의 이야기를 반영하는
모든 과정 하나하나를
포스트로 작성하려 했었다. 일종의 로그형 포스트랄까?
실제로 첫 주제로 잡고 있던 것도 있었다.
멀티 모듈에 맞춘 CD 로직
을 작성중이었고,
그에 따른 versionCode 업로드 로직도 다 만들어놓은 상태였기 때문이다.
하지만 개인의 사정이 생겨 유지보수
만 개인적으로 진행하고,
포스팅할 주제가 생긴다면 그 주제를 가지고 포스트를 남기려 한다.
나중에 이야기하겠지만, 나의 2022년은 강화가 아닌 도전
의 느낌이 강해졌다.
이러면서 2022년 계획도 완전히 바뀌게 되었다.
빚쟁이에게 빚을 진 느낌으로 로그형 포스트를 만들기보다는,
내가 개발하면서 마주한 빛
을 주제로 포스팅을 하는 게 더 유의미할거란 생각이 들어
이 주제는 잠시 개인적인 공간에 두고 마무리 지으려 한다.
시리즈 마무리는 지었지만
약속을 완벽하게 지키지 못한 나 자신에게
먼저 사과를 건네며 이 글을 마무리한다.