Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ implement screens for sign up #85

Merged
merged 7 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package net.pengcook.android.presentation

import android.net.Uri
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.bumptech.glide.Glide
import net.pengcook.android.R

Expand All @@ -19,6 +26,19 @@ fun loadImage(
}
}

@BindingAdapter("app:imageUri")
fun loadImage(
view: ImageView,
uri: Uri?,
) {
if (uri != null) {
Glide
.with(view.context)
.load(uri)
.into(view)
}
}

@BindingAdapter("app:favoriteCount")
fun favoriteCountText(
view: TextView,
Expand Down Expand Up @@ -55,3 +75,49 @@ fun timeRequiredText(
val context = view.context
view.text = context.getString(R.string.time_format_required).format(time)
}

@BindingAdapter("bind:selectedValue")
fun bindSpinnerData(
spinner: Spinner,
newValue: String?,
) {
val adapter = spinner.adapter as? ArrayAdapter<String>
val position = adapter?.getPosition(newValue)
if (position != null && position != spinner.selectedItemPosition) {
spinner.setSelection(position, true)
}
}

@InverseBindingAdapter(attribute = "bind:selectedValue", event = "bind:selectedValueAttrChanged")
fun captureSelectedValue(spinner: Spinner): String {
return spinner.selectedItem as String
}

@BindingAdapter("bind:selectedValueAttrChanged")
fun setSpinnerListener(
spinner: Spinner,
listener: InverseBindingListener?,
) {
if (listener == null) {
spinner.onItemSelectedListener = null
return
}

val onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long,
) {
listener.onChange()
}

override fun onNothingSelected(parent: AdapterView<*>) {
// do nothing
}
}

spinner.onItemSelectedListener = onItemSelectedListener
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.pengcook.android.presentation.core.listener

interface AppbarActionEventListener {
fun onNavigateBack()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.pengcook.android.presentation.core.util

open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set

fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

fun peekContent(): T = content
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class OnboardingFragment : Fragment() {
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun handleGoogleSignInResult(task: Task<GoogleSignInAccount>) {
try {
val account = task.getResult(ApiException::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.pengcook.android.presentation.signup

interface BottomButtonClickListener {
fun onConfirm()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.pengcook.android.presentation.signup

sealed interface SignUpEvent {
class NavigateToMain(
val accessToken: String,
val refreshToken: String,
) : SignUpEvent

data object Error : SignUpEvent

data object NavigateBack : SignUpEvent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.pengcook.android.presentation.signup

import java.io.File

data class SignUpForm(
val profileImage: File? = null,
val username: String = INITIAL_VALUE,
val nickname: String = INITIAL_VALUE,
val yearOfBirthday: Int = INITIAL_YEAR,
val monthOfBirthday: Int = INITIAL_MONTH,
val dayOfBirthday: Int = INITIAL_DAY,
val country: String = INITIAL_COUNTRY,
) {
companion object {
private const val INITIAL_VALUE = ""
private const val INITIAL_YEAR = 2024
private const val INITIAL_MONTH = 1
private const val INITIAL_DAY = 1
private const val INITIAL_COUNTRY = ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package net.pengcook.android.presentation.signup

import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import net.pengcook.android.R
import net.pengcook.android.databinding.FragmentSignUpBinding

class SignUpFragment : Fragment() {
private var _binding: FragmentSignUpBinding? = null
private val binding: FragmentSignUpBinding
get() = _binding!!
private val viewModel: SignUpViewModel by viewModels()
private val launcher =
registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri ->
try {
viewModel.changeProfileImage(uri)
} catch (e: RuntimeException) {
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentSignUpBinding.inflate(inflater)
binding.viewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
setUpBirthDateSpinner()
setUpCountrySpinner()
setUpClickListener()
observeViewModel()
}

private fun setUpClickListener() {
binding.ivProfileImage.setOnClickListener {
launcher.launch("image/*")
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

binding nullable 처리에 이어 destroy때 null로 해주는거 좋네요. 명시적으로 destroy 되는 느낌이 있어 좋아보이는데, 궁금한점은 nullable 처리해뒀기 때문에 GC가 일정 시간 지나면 알아서 destroy 하지 않는지 궁금합니다. 제가 몰라서 혹시 아시면 알려주시면 감사하겠습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구글에서 해당 방식으로 fragment를 사용하도록 제시하는데, 가장 큰 이유는 fragment의 생명주기의 특성 상 메모리 누수가 발생할 수 있기 때문이라고 볼 수 있습니다.
https://www.reddit.com/r/android_devs/comments/orbdas/is_it_required_or_good_practice_to_set_the/
https://woowacourse.slack.com/archives/C06DZU72A79/p1714702344693229?thread_ts=1714700634.545369&cid=C06DZU72A79

}

private fun setUpCountrySpinner() {
val countryAdapter =
ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
List(100) { "country$it" },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 여기는 국가를 샘플로 담은 것 같은데, 향후 국가리스트를 서버에서 받을지, 인메모리로 저장해둘지 공유해주시면 좋을것 같아요 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 사안은 서버와 협의해봐야 할 것 같네요!

)
countryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.formCountry.spFormContent.spDefault.adapter = countryAdapter
}

private fun setUpBirthDateSpinner() {
val yearAdapter =
ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
List(100) { (it + 1920).toString() },
)
yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.formBirthDate.spFormContent1.spDefault.adapter = yearAdapter

val monthAdapter =
ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
List(12) { (it + 1).toString() },
)
monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.formBirthDate.spFormContent2.spDefault.adapter = monthAdapter

val dayAdapter =
ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_item,
List(31) { (it + 1).toString() },
)
dayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.formBirthDate.spFormContent3.spDefault.adapter = dayAdapter
}

private fun observeViewModel() {
viewModel.signUpEvent.observe(viewLifecycleOwner) { event ->
val signUpEvent = event.getContentIfNotHandled() ?: return@observe
when (signUpEvent) {
is SignUpEvent.NavigateToMain -> {
findNavController().navigate(R.id.action_signUpFragment_to_homeFragment)
}
is SignUpEvent.Error -> {
}
is SignUpEvent.NavigateBack -> {
findNavController().navigate(R.id.action_signUpFragment_to_onboardingFragment)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.pengcook.android.presentation.signup

import android.net.Uri

data class SignUpUiState(
val isLoading: Boolean = false,
val profileImage: Uri? = null,
val fulfilled: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package net.pengcook.android.presentation.signup

import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.pengcook.android.presentation.core.util.Event

class SignUpViewModel :
ViewModel(),
BottomButtonClickListener {
val usernameContent: MutableLiveData<String> = MutableLiveData()
val nicknameContent: MutableLiveData<String> = MutableLiveData()
val year: MutableLiveData<String> = MutableLiveData()
val month: MutableLiveData<String> = MutableLiveData()
val day: MutableLiveData<String> = MutableLiveData()
val country: MutableLiveData<String> = MutableLiveData()
private var _imageUri: MutableLiveData<Uri> = MutableLiveData()
val imageUri: LiveData<Uri>
get() = _imageUri

private var _signUpUiState: MutableLiveData<SignUpUiState> = MutableLiveData(SignUpUiState())
val signUpUiState: LiveData<SignUpUiState>
get() = _signUpUiState

private var _signUpEvent: MutableLiveData<Event<SignUpEvent>> = MutableLiveData()
val signUpEvent: LiveData<Event<SignUpEvent>>
get() = _signUpEvent

fun changeProfileImage(uri: Uri) {
_imageUri.value = uri
}

override fun onConfirm() {
_signUpUiState.value = signUpUiState.value?.copy(isLoading = true)
_signUpEvent.value = Event(SignUpEvent.NavigateToMain("", ""))
_signUpUiState.value = signUpUiState.value?.copy(isLoading = false)
}
}
7 changes: 7 additions & 0 deletions android/app/src/main/res/drawable/bg_radius.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
<corners android:radius="16dp"/>
<stroke android:color="@color/gray" android:width="1dp" />
</shape>
5 changes: 5 additions & 0 deletions android/app/src/main/res/drawable/ic_back_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>

</vector>
Loading
Loading