>데이터 레이어 구조
@데이터 종류
1. 캐시 데이터
1)카테고리 데이터 - 채널, 비디오
-CategoryModel을 nextPageToken과 CategoryResponse로 나누어 저장.
=>새 데이터가 들어오면 토큰은 replace하고 response는 추가하는 방식
2) 로그인 데이터
-로그인시 로그인 유지를 체크하면 로그인 데이터(아이디, 비밀번호)를 로컬에 저장함
-직접 로그아웃시 로그인 데이터를 삭제함
-스플래시 화면이 떠있는동안 로그인 데이터를 가져와서 자동로그인 구현
2. 리모트 데이터
1)파이어베이스
<1>유저데이터
-회원가입시 중복아이디 체크
-유저 데이터 변경시(이름이나 설명), 혹은 새로 회원가입시 수정 및 추가
-회원탈퇴시 유저 데이터 삭제
-로그인시 유저 데이터 제공
<2>유저 좋아요 데이터
-좋아요시 추가
-좋아요 취소시 삭제
-회원가입시 리스트 생성
-회원탈퇴시 리스트 삭제
-좋아요 리스트 제공
2)YoutubeAPI
<1>인기 동영상 받아오기
<2> 검색 동영상 받아오기
<3> 카테고리별 동영상 받아오기
<4> 채널아이디로 채널 받아오기
@데이터 모델
1. 카테고리채널
- CategoryChannelModel
-nextPageToken: 다음 페이지 검색을 위한 토큰
-CategoryChannelResponse: 각 채널별 정보가 담긴 데이터 모델
-API에서 사용2. 카테고리비디오
- CategoryVideoModel
-nextPageToken: 다음 페이지 검색을 위한 토큰
-CategoryVideoResponse: 각 비디오별 정보가 담긴 데이터 모델
-API에서 사용3. 검색 결과 비디오
- SearchResultModel
-nextPageToken: 다음 페이지 검색을 위한 토큰
-SearchResponse: 검색결과로 나온 각 비디오의 정보가 담긴 데이터 모델
-API에서 사용4. 비디오
- VideoModel
- 상세페이지에 비디오 정보를 띄우기 위해 검색과 카테고리 비디오의 공통 필드를 뽑아 변환한 데이터 모델
- Convert를 위한 메소드 작성되어 있음
5. 좋아요 리스트
-LikeList
-VideoModel이 리스트로 담겨있는 클래스
-파이어베이스에 사용6. 유저 데이터
- User
-마이 페이지에 나타낼 정보와 로그인 정보가 담긴 유저 데이터 모델
-파이어베이스에서 사용
@사용 방식
1. 카테고리 데이터
1)카테고리 채널과 카테고리 비디오는 sharedpreferences를 체크해서 비어있다면 새로 데이터를 API로 받아옴
2)받아온 데이터는 UI에 뿌려주고 비동기로 sharedpreference에 저장
3)새로고침시 sharedpreferences는 모든 카테고리에 대하여 전부 clear()
2. 인기 동영상, 검색결과 동영상, 카테고리 동영상
-받아온 데이터를 VideoModel로 변환
-좋아요로 데이터 추가 및 삭제시 파이어베이스에서 추가 및 삭제
3. 유저데이터
-처음 로그인시 파이어 베이스에서 받아오고 이후에는 intent로 전달
@에러 처리 방식
1. API와 SharedPreference: Result를 이용해서 에러 핸들링
2. firebase: 콜백함수를 이용
>스플래시
-theme에서 새로 스플래시 테마를 만들어서 처음 액티비티인 startActivity에 적용
implementation("androidx.core:core-splashscreen:1.0.1")
-dependencies설정
<style name="Theme.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/black</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_app_logo</item>
<item name="windowSplashScreenAnimationDuration">2000</item>
<item name="postSplashScreenTheme">@style/Theme.YouTubeProject</item>
</style>
-테마 생성
<activity
android:theme="@style/Theme.Splash"
android:name=".presentation.ui.StartActivity"
android:exported="true">
-AndroidManifest에서 테마 지정
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_start)
supportActionBar?.hide()
-액티비티에서 메소드 적용
>로그인
@HiltViewModel
class LoginViewModel @Inject constructor(private val checkLoginUseCase: CheckLoginUseCase, private val cacheLoginDataUseCase: CacheLoginDataUseCase, private val getCacheLoginDataUseCase: GetCacheLoginDataUseCase):ViewModel(){
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Init)
val uiState = _uiState.asStateFlow()
fun loginCheck(id:String, password:String, isCache:Boolean){
checkLoginUseCase(id,password){ user ->
if(user!=null){
var result=true
if(isCache){
result = cacheLoginDataUseCase(id,password)?:false
}
if(result){
_uiState.value = UiState.Success(user)
}else{
_uiState.value = UiState.Failure("cachefail")
}
}else{
_uiState.value = UiState.Failure("fail")
}
}
}
fun autoLogin(){
val result = getCacheLoginDataUseCase()
if(result!=null){
result.let {
val id = it.first
val password = it.second
if(id!=null&&password!=null){
loginCheck(id,password,true)
}else{
_uiState.value = UiState.Failure("failAutoLogin")
}
}
}else{
_uiState.value = UiState.Failure("failAutoLogin")
}
}
}
-자동로그인과 그냥 로그인 메소드를 만들어서 경우에 따라 UiState를 보내준다.
@AndroidEntryPoint
class StartActivity : AppCompatActivity() {
private val viewmodel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_start)
supportActionBar?.hide()
lifecycleScope.launch {
viewmodel.uiState.collectLatest {
Log.d("로그",it.toString())
if(it is UiState.Init){
viewmodel.autoLogin()
}
else if(it is UiState.Success){
Toast.makeText(this@StartActivity,"로그인 성공",Toast.LENGTH_SHORT).show()
val intent = Intent(this@StartActivity, MainActivity::class.java)
intent.putExtra("userData",it.data)
startActivity(intent)
finish()
}else {
supportFragmentManager.beginTransaction().replace(R.id.main, LoginFragment.newInstance()).commit()
}
}
}
}
}
-startActivity에서 자동로그인시도 후 Uistate를 collect하면서 체크후 실패시 로그인 프래그먼트를 띄움
-이후 로그인 성공시 여기서 처리
@AndroidEntryPoint
class LoginFragment : Fragment() {
private var _binding : FragmentLoginBinding? = null
private val binding get() = _binding!!
private val viewmodel:LoginViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLoginBinding.inflate(inflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
fun initView() = with(binding){
viewLifecycleOwner.lifecycleScope.launch {
viewmodel.uiState.collectLatest {
when(it){
is UiState.Failure -> {
if(it.e=="fail"){
Toast.makeText(requireContext(),"아이디나 비밀번호를 다시 입력해주세요.",Toast.LENGTH_SHORT).show()
}else if(it.e=="cachefail"){
Toast.makeText(requireContext(),"로그인 정보 저장에 실패하였습니다. 다시 시도해주세요.",Toast.LENGTH_SHORT).show()
}
}
is UiState.Init -> null
is UiState.Loading -> null
is UiState.Success -> null //액티비티에서 처리
}
}
}
loginBtnSignup.setOnClickListener {
parentFragmentManager.beginTransaction().replace(R.id.main, SignUpFragment.newInstance()).addToBackStack(null).commit()
}
loginBtnLogin.setOnClickListener {
val id = loginEtId.text.toString()
val password = loginEtPassword.text.toString()
val isCache = loginCbAutologin.isChecked
viewmodel.loginCheck(id,password,isCache)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
@JvmStatic
fun newInstance() = LoginFragment()
}
}
-로그인 시도시 데이터를 뷰모델로 보내주는 역할을 하며 Uistate를 collect하며 실패시 토스트메시지를 띄운다.
>회원가입
@HiltViewModel
class SignUpViewModel @Inject constructor(
private val checkSignUpUseCase: CheckSignUpUseCase,
private val registerOrModifyUserDataUseCase: RegisterOrModifyUserDataUseCase,
private val uploadProfileUseCase: UploadProfileUseCase,
private val createLikeListUseCase: CreateLikeListUseCase
):ViewModel() {
private val _uiState = MutableStateFlow<UiState<Unit>>(UiState.Init)
val uiState = _uiState.asStateFlow()
fun signUp(id:String, password:String, name:String, intro:String, profile:Bitmap?){
checkSignUpUseCase(id,password,name){ notify,isEnable ->
if(isEnable){
createLikeListUseCase(id){ likelist->
if(likelist){
var user = User(null,name, id, password, intro)
if(profile!=null){
uploadProfileUseCase(profile,id){ url ->
if(url==null){
_uiState.value = UiState.Failure("N"+notify)
}else{
registerOrModifyUserDataUseCase(user.copy(profile = url)){
if(it){
_uiState.value = UiState.Success(Unit)
}else{
_uiState.value = UiState.Failure("F"+notify)
}
}
}
}
}else{
registerOrModifyUserDataUseCase(user){
if(it){
_uiState.value = UiState.Success(Unit)
}else{
_uiState.value = UiState.Failure("F"+notify)
}
}
}
}else{
_uiState.value = UiState.Failure("Lfail to make likelist")
}
}
}else{
_uiState.value = UiState.Failure("N"+notify)
}
}
}
}
-회원가입시도시 아이디와 패스워드와 이름을 먼저 체크 하고 이후 좋아요 리스트 생성 성공 여부 이후 프로필이미지 업로드후 마지막으로 유저 데이터를 등록하며 마무리된다.
-경우의 따라 uistate를 보내줌.
@AndroidEntryPoint
class SignUpFragment : Fragment() {
private var _binding : FragmentSignUpBinding? = null
private val binding get() = _binding!!
val viewmodel:SignUpViewModel by viewModels()
private var profile: Bitmap? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentSignUpBinding.inflate(inflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}
fun initView() = with(binding){
viewLifecycleOwner.lifecycleScope.launch {
viewmodel.uiState.collectLatest {
when(it){
is UiState.Init -> null
is UiState.Failure -> {
if(it.e[0]!='N')
Toast.makeText(requireActivity(), "오류로 인해 등록에 실패하였습니다. 다시시도 해주세요.",Toast.LENGTH_SHORT).show()
signupTvNotify.visibility=View.VISIBLE
signupTvNotify.text = it.e.substring(1)
}
is UiState.Success -> {
Toast.makeText(requireActivity(),"회원가입 성공",Toast.LENGTH_SHORT).show()
parentFragmentManager.beginTransaction().replace(R.id.main,
LoginFragment.newInstance()
).commit()
}
is UiState.Loading -> null
}
}
}
signupBtnSignup.setOnClickListener {
val id=signupEtId.text.toString()
val password = signupEtPassword.text.toString()
val name = signupEtName.text.toString()
val intro = signupEtIntro.text.toString()
viewmodel.signUp(id, password, name, intro,profile)
}
val phohtoPicker =registerForActivityResult(ActivityResultContracts.PickVisualMedia()){ uri ->
if(uri !=null){
Toast.makeText(requireActivity(), "길게 클릭시 프로필 이미지가 초기화됩니다.", Toast.LENGTH_SHORT).show()
val result = runCatching {
BitmapFactory.decodeStream(requireActivity().contentResolver.openInputStream(uri))
}
profile = result.getOrNull()
profile?.let {
signupIvProfile.setImageBitmap(it)
}
}else{
profile = null
signupIvProfile.setImageResource(R.drawable.ic_person)
}
}
signupIvProfile.setOnClickListener {
phohtoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
signupIvProfile.setOnLongClickListener {
profile = null
signupIvProfile.setImageResource(R.drawable.ic_person)
return@setOnLongClickListener true
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
@JvmStatic
fun newInstance() = SignUpFragment()
}
}