- 로그인 페이지 만들기
@file:Suppress("UnusedImport")
package com.example.a220402
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.example.a220402.databinding.ActivitySignInBinding
import kotlinx.android.synthetic.main.activity_sign_in.*
class SignInActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignInBinding
private lateinit var resultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult())
//registerForActivityResult : Activityresult에 대한 콜백 생성, Lancher 생성
{
if (it.resultCode == Activity.RESULT_OK) { //result_ok인 경우 수행
val id = it.data?.getStringExtra("id") ?: "" //?. 연산은 엘비스 연산자임. 왼쪽 피연산자 값이 null이 아니면 id 출력
val pw = it.data?.getStringExtra("pw") ?: ""
binding.etId.setText(id)
binding.etPw.setText(pw)
}
}
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater) //inflate는 xml의 뷰를 객체화해준다고 생각하자
setContentView(binding.root)
val intent = Intent(this, HomeActivity::class.java)
binding.btn.setOnClickListener() {
if (binding.etId.text.isNullOrBlank() || binding.etPw.text.isNullOrBlank()) {
Toast.makeText(this, "아이디/비밀번호를 확인해주세요", Toast.LENGTH_SHORT).show()
//isNullOrBlank 함수 사용, id와 pw 둘 중 하나만 비어있어도 Toast 출력
} else {
Toast.makeText(this, "로그인 성공", Toast.LENGTH_SHORT).show()
startActivity(intent)
//로그인 성공 시 홈 화면으로 이동
}
}
binding.btnSignup.setOnClickListener() {
val intent = Intent(this, SignUpActivity::class.java)
resultLauncher.launch(intent)
//signup 버튼을 누르면 SignUpActivity로 이동, intent 객체를 lancher에 실어 이동.
}
}
}
- if : 빈칸인 경우를 작성할 때, if문 내에서 "" 같은 내용을 쓸 수도 있지만, isNullOrBlank 같은 함수를 이용해보는 것도 좋은 방법인 듯!
- 회원가입 페이지 만들기
package com.example.a220402
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.a220402.databinding.ActivitySignUpBinding
import kotlinx.android.synthetic.main.activity_sign_in.*
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnFinshSignup.setOnClickListener {
if (binding.etName.text.isNullOrBlank() || binding.etId.text.isNullOrBlank() || binding.etPw.text.isNullOrBlank()) {
Toast.makeText(this, "입력되지 않은 정보가 있습니다", Toast.LENGTH_SHORT).show()
} else {
val intent = Intent(this@SignUpActivity, SignInActivity::class.java) //signinactivity에 대한 intent 객체 생성
intent.putExtra("id", et_id.text.toString()) //id에 et_id 데이터 담음
intent.putExtra("pw", et_pw.text.toString()) //마찬가지로 pw에 et_pw 담음
setResult(Activity.RESULT_OK, intent) //result_ok인 경우 SignInActivitiy로 intent 객체 보냄
finish()
}
}
}
}
- CallBack : 다른 함수의 인자로써 이용되는 함수 / 이벤트에 의해 호출되는 함수
- putextra : putextra를 통해 데이터 담아서 전달이 가능하다.
- RESULT_OK : setResult(Activity.RESULT_OK, intent)에서 결과 ok면 intent 객체 보낸다.
그렇기 때문에
<ScrollView
android:id="@+id/sv_profile"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
//(.. ImageView와 TextView들 .. )
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
이처럼 ScrollView 밑에 LinearLayout 하나 넣고, 그 밑에 TextView와 ImageView등을 넣어주면 된다.
중심을 가운데다 줄 수 있다는 의미.
?. 연산은 엘비스 연산자이다.
val a = 왼쪽?. : 오른쪽
val b = a?.length ?:0
왼쪽 피연산자 값이 null이 아니면 그 피연산자 값을 반환하고 null이면 오른쪽 피연산자의 결과값을 반환
ctl + alt + l
xml의 뷰를 객체화해준다고 생각하면 된다. 더 쉽게 말하면 실체화 시키는 것.
Activityresult에 대한 콜백 생성, Lancher 생성.
val intent = Intent(this, HomeActivity::class.java)
HomeActivity에 대한 intent 객체 생성
Login | Join |
---|---|
아이디, 비밀번호 중 하나라도 미입력 시 로그인 불가, 로그인 시 로그인 성공 토스트 출력, MY INFO 스크롤뷰 구현 | 회원가입 내용 중 하나라도 미입력시 회원가입 불가, 회원가입 아이디, 비밀번호가 로그인 시 유지 |
package com.example.a220402
data class FollowerData(
val image: Int,
val name : String,
val introduction : String
)
- dataclass : 리스트로 보여줄 데이터를 담는 클래스라고 생각하기
package com.example.a220402
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.a220402.databinding.ItemFollowerListBinding
class FollowerAdapter : RecyclerView.Adapter<FollowerAdapter.FollowerViewHolder>() {
val followerList = mutableListOf<FollowerData>()
class FollowerViewHolder(
private val binding: ItemFollowerListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(data: FollowerData) {
binding.ivProfile.setImageResource(data.image)
binding.tvName.text = data.name
binding.tvIntro.text = data.introduction
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerViewHolder {
val binding =
ItemFollowerListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return FollowerViewHolder(binding)
}
override fun onBindViewHolder(holder: FollowerViewHolder, position: Int) {
holder.onBind(followerList[position])
}
override fun getItemCount(): Int = followerList.size
}
-
class FollowerAdapter 안에 class FollowerViewHolder가 있는 NestedClass 구조!
-
onCreateViewHolder : parent에 들어온 뷰그룹을 받아서 해당 뷰그룹이 어떤 흐름에 생성되어야 할지 정보를 가지고 있고, 정보를 LayoutInflater에 넘겨준다. 이후 생성된 뷰 객체를 return한다!
-
onBindViewHolder : ViewHolder와 position의 데이터 결합
-
getItemCount : 전체 데이터 개수
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FollowerFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_Follower"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="8"
tools:listitem="@layout/item_follower_list"/>
</androidx.constraintlayout.widget.ConstraintLayout>
- itemcount = 8개로 지정
- listitem = item_follower_list.xml을 리스트에 띄움
package com.example.a220402
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.a220402.databinding.FragmentFollowerBinding
class FollowerFragment : Fragment() {
private lateinit var followerAdapter: FollowerAdapter
private var _binding: FragmentFollowerBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFollowerBinding.inflate(inflater, container, false)
initAdapter()
return binding.root
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun initAdapter() {
followerAdapter = FollowerAdapter()
binding.rvFollower.adapter = followerAdapter
followerAdapter.followerList.addAll(
listOf(
FollowerData(R.drawable.yr, "최유리", "안드로이드 YB 파트원 치코리타"),
FollowerData(R.drawable.yj, "최윤정", "안드로이드 YB 파트원 마자용"),
FollowerData(R.drawable.sb, "김수빈", "안드로이드 OB 파트원 라이츄"),
FollowerData(R.drawable.jw, "이준원", "안드로이드 YB 파트원 꼬지모"),
FollowerData(R.drawable.ym, "권용민", "안드로이드 OB 파트원 알통몬")
)
)
followerAdapter.notifyDataSetChanged()
}
}
- Adapter 초기화 후 Adapter와 RecyclerView 연동
- List로 보여줄 데이터를 Adapter에 넣고(listOf), Adapter에 전체 리스트의 데이터가 갱신되었다고 알려줌
-[x] fragment간 전환 (follower <-> repo)
package com.example.a220402
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.a220402.databinding.ActivityHomeBinding
class HomeActivity : AppCompatActivity() {
private var position = FOLLOWER_POSITION
private lateinit var binding: ActivityHomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
initTransactionEvent()
}
fun initTransactionEvent() {
val fragment1 = FollowerFragment()
val fragment2 = RepoFragment()
supportFragmentManager.beginTransaction().add(R.id.fragment_main, fragment1).commit()
binding.followerbtn.setOnClickListener {
if (position == REPO_POSITION) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_main, fragment1)
.commit()
position = FOLLOWER_POSITION
}
}
binding.repobtn.setOnClickListener {
if (position == FOLLOWER_POSITION) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_main, fragment2)
.commit()
position = REPO_POSITION
}
}
}
- add() : 프래그먼트를 추가, replace는 교체!
- beginTransaction() : 트랜잭션 추가 or 교체 or 삭제 생성
- commit() : 커밋을 꼭 해야 작업 수행!
- companion object : 상수 값 선언, 클래스에 하나만 존재
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<gradient android:angle="270" android:endColor="#009999" android:startColor="#bbffff" />
<corners android:radius="10dp" />
<stroke android:width="5dp" android:color="#bbffff" />
</shape>
</item>
</selector>
- stroke : 테두리 굵기 선정
- 버튼에서 android:background="@drawable/gradient1" 으로 불러옴
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle">
<corners
android:radius="10dp"/>
<stroke
android:width="2dp"
android:color="@color/purple_200" />
</shape>
</item>
</selector>
- item_follower_list에서 android:background="@drawable/round" 로 불러옴
<resources>
<string name="app_name">220402</string>
<string name="profile">이름 : 최유리\n나이 : 빠른.. 23살\nMBTI : ESFP\n</string>
</resources>
- TextView 안에 android:text="@string/profile" 로 불러옴
- 긴 내용을 따로 정리함으로써 코드가 더 간결해짐!
ViewHolder는 틀이라고 생각하면 이해하기 편하다..! 내용을 담는 그릇
Adapter는 ViewHolder를 생성하고 ItemLayout을 ViewHolder에 넘겨준다!
TextView나 button 테두리, 그라데이션 같은 경우 불러와서 사용할 수 있다!
dataclass에 val image: Int 로 이미지 변수를 추가하고,
R.drawable.이미지이름을 list에 추가,
FollowerViewHolder 클래스 안에 binding.ivProfile.setImageResource(data.image) 해주면
이미지도 리스트의 이름, 소개처럼 사람마다 변경할 수 있다!
오버라이딩 해주어야 하는 경우 alt + enter 누르면 오버라이딩이 자동으로 뜸! 오버라이딩 다 칠 필요 없어서 편하게 할 수 있다..!
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" 사용하면 바둑판처럼 배열 가능하다.
companion object{
const val FOLLOWER_POSITION = 1
const val REPO_POSITION = 2
}
}
Login - Follower List - Repository List |
---|
버튼 클릭시 전환, GridLayout 적용, 설명 길면 ...으로 표시되게 하기 |
- HomeActivity를 ProfileFragment로 바꾸기
- Font 적용
- bottomNavigation 적용
- TabLayout 적용
- Button에 Selector 활용하기
- 이미지 원형으로 표시
- ViewPager2 중첩 해결
-먼저 Profile, Home, Camera Fragment 3개 생성 -activity_main.xml에 ViewPager2를 배치하고 ViewPagerAdapter 생성
//TabViewPagerAdapter.kt
package com.example.a220402
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
class TabViewPagerAdapter(fragment: Fragment) :
FragmentStateAdapter(fragment) {
val fragments = mutableListOf<Fragment>()
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment {
return when (position) {
FOLLOWING_FRAGMENT -> TabFragment1()
FOLLOWER_FRAGMENT -> TabFragment2()
else -> throw IndexOutOfBoundsException()
}
}
companion object {
const val FOLLOWING_FRAGMENT = 0
const val FOLLOWER_FRAGMENT = 1
}
}
- FragmentStateAdapter 클래스 상속 받음.
FragmentStateAdapter는 RecyclerView.Adapter를 상속받는다!
-bottomNavigationView 하단 메뉴 생성
//menu_sample.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_profile"
android:icon="@drawable/ic_union"
android:title="프로필" />
<item
android:id="@+id/menu_home"
android:icon="@drawable/ic_home"
android:title="홈" />
<item
android:id="@+id/menu_camera"
android:icon="@drawable/ic_camera"
android:title="카메라" />
</menu>
- Drawable Resource File에서 이미지 불러와서 icon 지정
//selector_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#6424D5" android:state_pressed="true" /> //눌렀을 때
<item android:color="#6424D5" android:state_checked="true" />
<item android:color="#C9C9C9" android:state_checked="false" />
</selector>
- selector로 버튼 및 BottomNavigation 눌렸을 때, 눌려 있을 때 등 색상 지정
//MainActivity.kt
package com.example.a220402
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.widget.ViewPager2
import com.example.a220402.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var testViewAdapter: TestViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initAdapter()
initBottomNavi()
}
private fun initAdapter() {
val fragmentList = listOf(ProfileFragment(),HomeFragment(), CameraFragment())
testViewAdapter = TestViewAdapter(this)
testViewAdapter.fragments.addAll(fragmentList)
binding.vpMain.adapter = testViewAdapter
}
private fun initBottomNavi() {
binding.vpMain.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
binding.bnvMain.menu.getItem(position).isChecked = true
}
})
binding.bnvMain.setOnItemSelectedListener {
when (it.itemId) {
R.id.menu_profile -> {
binding.vpMain.currentItem = FIRST_FRAGMENT
return@setOnItemSelectedListener true
}
R.id.menu_home -> {
binding.vpMain.currentItem = SECOND_FRAGMENT
return@setOnItemSelectedListener true
}
else -> {
binding.vpMain.currentItem = THIRD_FRAGMENT
return@setOnItemSelectedListener true
}
}
}
}
companion object {
const val FIRST_FRAGMENT = 0
const val SECOND_FRAGMENT = 1
const val THIRD_FRAGMENT = 2
}
}
- 아래 MainActivity에서 initAdapter가 ViewPagerAdpater,
initBottomNavi가 ViewPager와 BottomNavigationView 연결하는 Adapter
//ProfileFragment.kt
package com.example.a220402
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.example.a220402.databinding.FragmentProfileBinding
class ProfileFragment : Fragment() {
private var position = FOLLOWER_POSITION
private var _binding: FragmentProfileBinding? = null //fragment로 바꿨기 때문에 _binding
private val binding get() = _binding ?: error("Binding이 초기화 되지 않았습니다")
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentProfileBinding.inflate(layoutInflater, container, false)
initTransactionEvent()
initImage() //꼭 return 전에 작성해줘야 한다
return binding.root
}
private fun initImage() {
Glide.with(this)
.load(R.drawable.uxri)
.circleCrop()
.into(binding.image)
} //이미지 원형으로 크롭
fun initTransactionEvent() {
val fragment1 = ProfileFollowerFragment()
val fragment2 = PfRepoAdapter()
childFragmentManager.beginTransaction()
.add(R.id.fragment_main, fragment1)
.commit()
binding.followerbtn.setOnClickListener {
if (position == REPO_POSITION) {
childFragmentManager.beginTransaction().replace(R.id.fragment_main, fragment1)
.commit()
position = FOLLOWER_POSITION
}
}
binding.repobtn.setOnClickListener {
if (position == FOLLOWER_POSITION) {
childFragmentManager.beginTransaction().replace(R.id.fragment_main, fragment2)
.commit()
position = REPO_POSITION
}
}
}
companion object {
const val FOLLOWER_POSITION = 1
const val REPO_POSITION = 2
}
}
- childFragmentManager를 사용하여 중첩 Fragment가 가능하게 한다.
parentFragmentManager를 사용해도 되지만, 만~약에 BottomNavigationView 중 하나가 사라진다면 예상치 못한 버그가 발생될 수 있으므로 안전하게 childFragmentManager를 사용하자. - Glide 사용하여 Profile에 있는 사진 원형으로 크롭
//noto_sans_kr.xml
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font
android:font="@font/noto_sans_kr_thin"
android:fontWeight="200" />
<font
android:font="@font/noto_sans_kr_light"
android:fontWeight="300" />
<font
android:font="@font/noto_sans_kr_regular"
android:fontWeight="400" />
<font
android:font="@font/noto_sans_kr_medium"
android:fontWeight="500" />
<font
android:font="@font/noto_sans_kr_bold"
android:fontWeight="700" />
<font
android:font="@font/noto_sans_kr_black"
android:fontWeight="900" />
</font-family>
- Drawable Resource File에서 font 폴더 생성
- 파일명을 noto_sans_kr_nn으로 변경 후 불러옴
- 각 폰트마다 fontWeight를 부여하여 사용할 수 있도록 xml 파일 생성
-fragment xml 파일에 TabLayout 추가하기
-HomeActivity에 TabFragmentAdapter와 initTabLayout 추가하기
//HomeActivity.kt
package com.example.a220402
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.a220402.databinding.FragmentHomeBinding
import com.google.android.material.tabs.TabLayoutMediator
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding ?: error("Binding이 초기화 되지 않았습니다")
private lateinit var sampleTabViewPagerAdapter: TabViewPagerAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentHomeBinding.inflate(layoutInflater, container, false)
initAdapter()
initTabLayout()
return binding.root
}
private fun initAdapter() {
val fragmentList = listOf(TabFragment1(), TabFragment2())
sampleTabViewPagerAdapter = TabViewPagerAdapter(this)
sampleTabViewPagerAdapter.fragments.addAll(fragmentList)
binding.homevp.adapter = sampleTabViewPagerAdapter
}
private fun initTabLayout() {
val tabLabel = listOf("팔로잉", "팔로워")
TabLayoutMediator(binding.hometl,binding.homevp) {tab, position ->
tab.text = tabLabel[position]
}.attach()
}
}
- initAdapter는 1에서 했던 내용과 동일
- initTabLayout에서 TabLayoutMediator 불러옴
- https://github.com/android/views-widgets-samples/blob/master/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt 에서 NestedScrollableHost.kt 파일 불러오기 (구글 깃허브)
//fragment_home.xml
<com.example.a220402.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hometl">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/homevp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/noto_sans_kr_regular"
android:includeFontPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hometl" />
</com.example.a220402.NestedScrollableHost>
- 레이아웃 xml 파일에서 태그를 이용해 적용하고자 하는 요소를 감싸준다.
- 이 때 해당 요소는 ViewPager2의 바로 아래에 위치한 유일한 자식이어야 한다!
- Drawable Resource File에서 폰트 폴더 생성하면 자꾸 없어졌었는데 로컬에서 안스 폴더 들어가서 찾았다.. 앞으로 파일이 안보이면 로컬에서 찾아보자..
...
initTransactionEvent()
initImage() //return 전에 작성해줘야 한다
return binding.root
}
- return 뒤에 무언가를 호출하면 호출이 안되니 꼭 return 앞에서 호출하자!
//rectancle_radius_5
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/selector_color"/>
<corners android:radius="5dp"/>
</shape>
- 버튼 background로 불러올 수 있는 xml 파일이다.
shape로 모양을 정하고, corners로 모서리 굴곡 정도를 정하고, solid로 색을 정할 수 있는데 나는 color에서 미리 만들어두었던 Button Selector를 불러와서 적용하였다.
//selector_color
<item android:color="#6424D5" android:state_pressed="true" />
<item android:color="#6424D5" android:state_checked="true" />
<item android:color="#6424D5" android:state_checked="false" />
- state_pressed는 버튼에서 사용하기 위한 내용이고(눌렀을 때), state_checked는 bottomNavi에서 사용하기 위한 내용이다(눌려 있는 상태). 이 내용을 한 xml 파일에 적용하고 각 xml 파일에서 background로 불러오면 된다.
//ProfileFragment
...
fun initTransactionEvent() {
val fragment1 = ProfileFollowerFragment()
val fragment2 = PfRepoAdapter()
childFragmentManager.beginTransaction()
.add(R.id.fragment_main, fragment1)
.commit()
...
- childFragmentManager를 사용하여 중첩 Fragment가 가능하게 한다.
parentFragmentManager를 사용해도 되지만, 만~약에 BottomNavigationView 중 하나가 사라진다면 예상치 못한 버그가 발생될 수 있으므로 안전하게 childFragmentManager를 사용하자.
<com.example.a220402.NestedScrollableHost
...
</com.example.a220402.NestedScrollableHost>
- xml에서 위 코드로 ViewPager2를 감싸주고, 구글에서 제공하는 NestedScrollableHost.kt 파일을 추가함으로써 쉽게 중첩 스크롤 문제를 해결할 수 있다.
...
android:fontFamily="@font/noto_sans_kr_medium"
android:includeFontPadding="false"
...
- font는 xml 파일에서 이런 식으로 불러오는데, 여기서 includeFontPadding을 false로 적용해주면 폰트를 적용했을 때 위아래로 적용된 패딩 값을 없앨 수 있다.
기기 크기가 모두 달라 디자인이 달라질 수 있다. 이 문제는 px가 아닌 dp로 크기를 지정하는데, svg와 9-patch로 dp로 크기적용이 가능하다. png는 그냥 삽입하면 크기 등이 달라질 수 있기 때문에 9-patch로 변경해주면 된다.
package com.example.a220402
data class RequestSignIn (
val email: String,
val password: String
)
- 변수명을 email로 했고 이는 postman의 키 값과 동일하니 Serialized 안 해줘도 된다.
package com.example.a220402
data class RequestSignUp (
val name: String,
val email: String,
val password: String
)
package com.example.a220402
data class ResponseSignIn(
val status: Int,
val message: String,
val data: Data
) {
data class Data(
val email: String,
val name: String
)
}
package com.example.a220402
data class ResponseSignUp(
val status: Int,
val message: String,
val data: Data
) {
data class Data(
val id: Int
)
}
package com.example.a220402
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface SoptService {
@POST("auth/signin")
fun postLogin(
@Body body: RequestSignIn
): Call<ResponseSignIn>
@POST("auth/signup")
fun postSignup(
@Body body: RequestSignUp
): Call<ResponseSignUp>
}
- 동기적, 비동기적으로 Type을 받아오는 객체
package com.example.a220402
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceCreator {
private const val BASE_URL = "http://13.124.62.236/"
private const val BASE_URL_GITHUB = "https://api.github.com/"
private val retrofit:Retrofit = Retrofit.Builder() //생성자 호출
.baseUrl(BASE_URL) //서버에 메인 URL 전달
.addConverterFactory(GsonConverterFactory.create()) //gson 컨버터 연동
.build() //Retrofit 객체 변환
private val githubRetrofit:Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL_GITHUB)
.addConverterFactory(GsonConverterFactory.create())
.build()
val soptService: SoptService = retrofit.create(SoptService::class.java)
val githubApiService: GithubApiService = githubRetrofit.create(GithubApiService::class.java)
//interface 객체를 create에 넘겨 실제 구현체 생성
}
- BASE_URL = "http://13.124.62.236/" : 메인 서버 도메인
...
import retrofit2.Call
import retrofit2.Response
import retrofit2.Callback
...
class SignInActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignInBinding
private lateinit var resultLauncher: ActivityResultLauncher<Intent>
private fun loginNetwork() {
val requestSignIn = RequestSignIn(
email = binding.etId.text.toString(),
password = binding.etPw.text.toString()
)
//서버에 요청을 보내기 위한 RequestData 생성
val call: Call<ResponseSignIn> = ServiceCreator.soptService.postLogin(requestSignIn)
//싱글톤 객체를 이용해 Retrofit이 만들어준 interface 구현체에 접근하여 Call 객체를 받아온다
call.enqueue(object : Callback<ResponseSignIn> {
override fun onResponse( //Callback 익명클래스 선언
call: Call<ResponseSignIn>,
response: Response<ResponseSignIn>
) {
if (response.isSuccessful) {
val data = response.body()?.data
Toast.makeText(
this@SignInActivity,
"${data?.email}님 반갑습니다!",
Toast.LENGTH_SHORT
).show()
startActivity(Intent(this@SignInActivity, MainActivity::class.java))
} else Toast.makeText(this@SignInActivity, "로그인에 실패하였습니다.", Toast.LENGTH_SHORT)
.show()
}
override fun onFailure(call: Call<ResponseSignIn>, t: Throwable) {
Log.e("NetworkTest", "error:$t") //오류처리 코드
}
})
}
...
binding.btn.setOnClickListener() {
loginNetwork() // 로그인 버튼 눌렀을 때 서버통신 이루어짐
...
- call.enqueue는 실제 서버통신을 비동기적으로 요청
- if문에서 val data는 null값 올 수 있으므로 nullable 타입
...
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class SignUpActivity : AppCompatActivity() {
private lateinit var binding: ActivitySignUpBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnFinshSignup.setOnClickListener {
if (binding.etName.text.isNullOrBlank() || binding.etId.text.isNullOrBlank() || binding.etPw.text.isNullOrBlank()) {
Toast.makeText(this, "입력되지 않은 정보가 있습니다", Toast.LENGTH_SHORT).show()
} else {
val intent = Intent(this@SignUpActivity, SignInActivity::class.java)
intent.putExtra("id", et_id.text.toString()) //id에 et_id 데이터 담음
intent.putExtra("pw", et_pw.text.toString()) //마찬가지로 pw에 et_pw 담음
setResult(Activity.RESULT_OK, intent) //result_ok인 경우 SignInActivitiy로 intent 객체 보냄
SignUpNetwork()
finish()
}
}
}
//함수.. oncreate 밑에 씁시다..
private fun SignUpNetwork() {
val requestSignUp = RequestSignUp(
name = binding.etName.text.toString(),
email = binding.etId.text.toString(),
password = binding.etPw.text.toString()
)
val call: Call<ResponseSignUp> = ServiceCreator.soptService.postSignup(requestSignUp)
call.enqueue(object : Callback<ResponseSignUp> {
override fun onResponse( //Callback 익명클래스 선언
call: Call<ResponseSignUp>,
response: Response<ResponseSignUp>
) {
if (response.isSuccessful) {
val data = response.body()?.data //null값 올 수 있으므로 nullable 타입
Toast.makeText(this@SignUpActivity, "${data?.id}님 반갑습니다!", Toast.LENGTH_SHORT).show()
startActivity(Intent(this@SignUpActivity, MainActivity::class.java))
} else Toast.makeText(this@SignUpActivity, "회원가입에 실패하였습니다.", Toast.LENGTH_SHORT).show()
}
override fun onFailure(call: Call<ResponseSignUp>, t: Throwable) {
Log.e("NetworkTest", "error:$t") //오류처리 코드
}
})
}
}
package com.example.a220402
data class ResponseUserInfo(
val login : String,
val avatar_url : String
)
package com.example.a220402
import retrofit2.Call
import retrofit2.http.GET
interface GithubApiService{
@GET("users/uxri")
fun getUserInfo(): Call<ResponseUserInfo>
@GET("users/uxri/followers")
fun getFollowingInfo(): Call<List<ResponseUserInfo>>
}
- GithubApi에서 받아오는 것
...
class FollowerViewHolder(
private val binding: ItemProfileFollowerListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(data: ResponseUserInfo) {
binding.follower = data
Glide.with(binding.ivProfile).load(data.avatar_url)
.circleCrop()
.into(binding.ivProfile)
}
}
...
- databinding 사용. 바인딩 이름, 설명 다 할 필요 없이 코드가 한 줄로 줄어들었음.
- Glide로 Github에서 받아온 avatar_url 사진 불러옴
ㄴ이거진짜속많이썩였다..이마짚...
...
class ProfileFollowerFragment : Fragment() {
private lateinit var followerAdapter: ProfileFollowerAdapter
private var _binding: FragmentFollowerBinding? = null
private val binding get() = _binding ?: error("바인딩이 초기화되지 않았습니다")
var responseData = mutableListOf<ResponseUserInfo>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFollowerBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUserInfoNetwork()
followerAdapter = ProfileFollowerAdapter()
binding.rvFollower.adapter = followerAdapter
}
private fun initUserInfoNetwork() {
val call: Call<List<ResponseUserInfo>> = ServiceCreator.githubApiService.getFollowingInfo()
call.enqueue(object : Callback<List<ResponseUserInfo>> {
override fun onResponse(
call: Call<List<ResponseUserInfo>>,
response: Response<List<ResponseUserInfo>>
) {
if (response.isSuccessful) {
val data = response.body()
data?.let {
followerAdapter.followerList = it.toMutableList()
followerAdapter.notifyDataSetChanged()
}
} else {
}
}
override fun onFailure(call: Call<List<ResponseUserInfo>>, t: Throwable) {
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
- onCreateView에서 함수 다 호출하지 말고 override fun onViewCreated에서 하자!
- followerAdapter.notifyDataSetChanged 제발 쓰자 이거 안쓰면 안뜬다고...
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="follower"
type="com.example.a220402.ResponseUserInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginVertical="10dp"
android:background="@drawable/round">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.25" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:fontFamily="@font/noto_sans_kr_bold"
android:includeFontPadding="false"
android:text="@{follower.login}"
android:textColor="@color/black"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/iv_profile"
app:layout_constraintTop_toTopOf="@+id/iv_profile"
app:layout_constraintBottom_toBottomOf="@id/iv_profile"
tools:text="최유리" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- databinding을 해주었습니다. follower안에 이미지랑 로그인이 들어가있습니다~
- ImageView에서 src를 지웠습니다... Glide 해줬기 때문에 src도 있으면 중복되니까! 둘다 있었을 때 src 때문에 이미지가 처음에만 뜨고 프래그먼트 넘기거나 하면 사진이 안 떴었다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ProfileFollowerFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_Follower"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="8"
tools:listitem="@layout/item_profile_follower_list" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- 얘두 마찬가지로 데이터바인딩 해줬습니다!
package com.example.a220402.response
data class ResponseWrapper<T>(
val status: Int,
val success: Boolean,
val message: String,
val data: T?
)
val call: Call<ResponseWrapper<ResponseSignIn>> = ServiceCreator.soptService.postLogin(requestSignIn)
call.enqueue(object : Callback<ResponseWrapper<ResponseSignIn>> {
override fun onResponse(
call: Call<ResponseWrapper<ResponseSignIn>>,
response: Response<ResponseWrapper<ResponseSignIn>>
) {
if (response.isSuccessful) {
val data = response.body()?.data
Toast.makeText(
this@SignInActivity,
"${data?.email}님 반갑습니다!",
Toast.LENGTH_SHORT
).show()
startActivity(Intent(this@SignInActivity, MainActivity::class.java))
} else Toast.makeText(this@SignInActivity, "로그인에 실패하였습니다.", Toast.LENGTH_SHORT)
.show()
}
override fun onFailure(call: Call<ResponseWrapper<ResponseSignIn>>, t: Throwable) {
Log.e("NetworkTest", "error:$t")
}
})
이렇게 WrapperClass를 사용하면 (SignUpActivity도 마찬가지)
data class ResponseSignIn(
val email: String,
val name: String
)
반복되는 내용을 줄일 수 있다!
로그인 | POSTMAN |
---|---|
회원가입 | POSTMAN |
Github | POSTMAN |
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_bias="0.5"
width와 height 값을 직접 입력하는 것보다 width 값을 0으로, height 값은 parent로 준 후에constraintHorizontal_bias에서 비율로 설정해주는 것이 좋습니다!
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_main"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repobtn"/>
버튼 내용 수정하다가 FragmentContainerView 날려먹고 코틀린 파일에서 container id 오류 뜬다고 몇십분 헤매다가... 갑자기 뭔가를 지워버린 것 같은게 생각나서.. 깃헙에서 빨리 데려오니 괜찮아졌습니다... 다음부턴 이 중요한걸 날려먹는 바보짓을 하지말자...
바보같은 나 도와준 천재 짱수빈 사랑해
import retrofit2.Call
import retrofit2.Response
import retrofit2.Callback
제발 까먹지 말고 추가합시다
" option enter " 로
- TabFragment1 이런 의미없는건 안돼ㅡㅡ
class FollowerViewHolder(
private val binding: ItemProfileFollowerListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(data: ResponseUserInfo) {
binding.follower = data
Glide.with(binding.ivProfile).load(data.avatar_url)
.circleCrop()
.into(binding.ivProfile)
- ImageView에서 src를 아예 지워줬습니다! 액티비티에서 Glide를 해줬기 때문에 src도 있으면 중복되니까! 둘다 있었을 때 src 때문에 이미지가 처음에만 뜨고 프래그먼트 넘기거나 하면 사진이 안 뜨는 일이 발생합니다..
저를 구제해주신 승현오빠에게 압도적 감사를 드립니다!!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUserInfoNetwork()
followerAdapter = ProfileFollowerAdapter()
binding.rvFollower.adapter = followerAdapter
}
- 함수를 onCreateView에서 다 호출하지 말고 이렇게 해줍시다~
private fun initUserInfoNetwork() {
val call: Call<List<ResponseUserInfo>> = ServiceCreator.githubApiService.getFollowingInfo()
call.enqueue(object : Callback<List<ResponseUserInfo>> {
override fun onResponse(
call: Call<List<ResponseUserInfo>>,
response: Response<List<ResponseUserInfo>>
) {
if (response.isSuccessful) {
val data = response.body()
data?.let {
followerAdapter.followerList = it.toMutableList()
followerAdapter.notifyDataSetChanged()
}
} else {
}
}
- 사실 이 부분이 완벽히 이해되지는 않았습니다.. ResponseUserInfo에서 데이터를 리스트로 받아온다는 것은 알았습니다!
- MutableList가 이해되지 않아서 더 공부해 볼 예정입니다.
followerAdapter.notifyDataSetChanged()
- 이 친구를 해주지 않으면 보이지 않습니다...
이거때문에 고생한 용민오빠에게 영광을(?) 돌립니댜..
binding.btnFinshSignup.setOnClickListener
이게 왜 맨 뒤로 가있었죠 최유리씨? 정신차리세요
- Local History로 되돌리기가 가능합니다
- command + F로 뭐 예를 들어 ResponseUserInfo가 있는 내용을 찾고싶다, 하면 바로 검색이 가능합니다.
import ...
object LoginSharedPreferences {
private const val STORAGE_KEY = "USER_AUTH"
private const val AUTO_LOGIN = "AUTO_LOGIN"
private lateinit var preferences: SharedPreferences
fun getSharedPreference(context: Context): SharedPreferences {
return context.getSharedPreferences(STORAGE_KEY, Context.MODE_PRIVATE)
}
fun getAutoLogin(context: Context): Boolean {
return getSharedPreference(context).getBoolean(AUTO_LOGIN, false)
}
fun setAutoLogin(context: Context, value: Boolean) {
getSharedPreference(context).edit()
.putBoolean(AUTO_LOGIN, value)
.apply()
}
fun setLogout(context: SettingActivity):Boolean{
getSharedPreference(context).edit()
.remove(AUTO_LOGIN)
.clear()
.apply()
return getSharedPreference(context).getBoolean(AUTO_LOGIN, false)
}
}
- 앱 전역에서 호출되기 때문에 Object 키워드로 싱글톤으로 만들어준다.
private const val STORAGE_KEY = "USER_AUTH"
: 키 값을 상수화- get()은 값을 읽어오는 것, set()은 값을 작성하는 것
- 자동로그인, 자동로그인 해제 내용 구현
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_checkbox_on" android:state_selected="true" />
<item android:drawable="@drawable/ic_checkbox_off" android:state_selected="false"/>
</selector>
...
private fun initClickEvent(){
binding.btnAutoLogin.setOnClickListener{
binding.btnAutoLogin.isSelected = !binding.btnAutoLogin.isSelected
LoginSharedPreferences.setAutoLogin(this, binding.btnAutoLogin.isSelected)
}
}
private fun isAutoLogin() {
if(LoginSharedPreferences.getAutoLogin(this)){
showToast("자동로그인 되었습니다")
startActivity(Intent(this@SignInActivity, MainActivity::class.java))
finish()
}
}
...
- 자동로그인 버튼 클릭 시 자동로그인 실행
- 자동로그인 성공 시 토스트 메시지 출력하고
MainActivity
로 이동
import...
class SettingActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingBinding.inflate(layoutInflater)
setContentView(binding.root)
setLogout()
}
private fun setLogout() {
binding.btnLogout.setOnClickListener {
LoginSharedPreferences.setLogout(this)
showToast("자동로그인 해제되었습니다")
val intent = Intent(this, SignInActivity::class.java)
startActivity(intent)
}
}
fun Context.showToast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
- 버튼 눌렀을 떄 자동로그인 해제 및 토스트 출력,
SignInActivity
로 이동
private fun clickEvent(){
binding.btnSetting.setOnClickListener {
val intent = Intent(context, SettingActivity::class.java)
startActivity(intent)
}
}
- setting 버튼 클릭 시 SettingActivity로 이동
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.OnboardingActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:elevation="3dp"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="온보딩"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fcv_onboarding"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/cl_title"
app:navGraph="@navigation/nav_onboarding"/>
</androidx.constraintlayout.widget.ConstraintLayout>
androidx.constraintlayout.widget.ConstraintLayout
에 온보딩 내용 고정FragmentContainerView
에NavHostFragment
지정android:name
= NavHost지정을 위한 NavHostFragment 클래스 설정app:defaultnavHost
= NavHostFragment가 백버튼 로직을 가로챌 수 있게 해주는 속성app:navGraph
= NavHostFragment를 NavigationGraph와 연결하는 속성
...
private fun onboarding(){
binding.btnNext.setOnClickListener {
findNavController().navigate(R.id.action_onboarding1Fragment_to_onboarding2Fragment)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
- next 버튼 누르면 다음 프래그먼트로 연결 (다른 프래그먼트도 같은 방식으로 진행)
package com.example.a220402.activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.a220402.R
import com.example.a220402.databinding.ActivityOnboardingBinding
import kotlinx.android.synthetic.main.fragment_onboarding1.view.*
class OnboardingActivity : AppCompatActivity() {
private lateinit var binding: ActivityOnboardingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
}
override fun finish() {
if (binding.fcvOnboarding.btn_next.isSelected)
super.finish()
}
}
- 마지막 next button이 클릭되면 끝나도록 설정했는데 확실히 맞는지는 모르겠다.. 좀 더 공부해봐야겠습니다!
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_onboarding"
app:startDestination="@id/onboarding1Fragment">
<fragment
android:id="@+id/onboarding1Fragment"
android:name="com.example.a220402.fragment.Onboarding1Fragment"
android:label="fragment_onboarding1"
tools:layout="@layout/fragment_onboarding1" >
<action
android:id="@+id/action_onboarding1Fragment_to_onboarding2Fragment"
app:destination="@id/onboarding2Fragment" />
</fragment>
<fragment
android:id="@+id/onboarding2Fragment"
android:name="com.example.a220402.fragment.Onboarding2Fragment"
android:label="fragment_onboarding2"
tools:layout="@layout/fragment_onboarding2" >
<action
android:id="@+id/action_onboarding2Fragment_to_onboarding3Fragment"
app:destination="@id/onboarding3Fragment" />
</fragment>
<fragment
android:id="@+id/onboarding3Fragment"
android:name="com.example.a220402.fragment.Onboarding3Fragment"
android:label="fragment_onboarding3"
tools:layout="@layout/fragment_onboarding3" >
<action
android:id="@+id/action_onboarding3Fragment_to_signInActivity"
app:destination="@id/signInActivity" />
</fragment>
<activity
android:id="@+id/signInActivity"
android:name="com.example.a220402.activity.SignInActivity"
android:label="activity_sign_in"
tools:layout="@layout/activity_sign_in" />
</navigation>
- 드래그 앤 드롭으로 연결했을 때 자동으로 생성되는 xml 파일!
자동로그인 | 자동로그인 해제 | 온보딩 |
---|---|---|
- 자동로그인 해제 구현 할 때, ProfileFragment에서 SettingActivity로 넘어가지 않았다.
private fun clickEvent(){
binding.btnSetting.setOnClickListener {
val intent = Intent(context, SettingActivity::class.java)
startActivity(intent)
}
}
- 위 코드와 같이 함수를 생성하고 호출해주었는데도 안 됐다. 그래서 액티비티 문제인가 싶어서 액티비티를 다른 액티비티로 바꾸어 해봤는데 그건 성공해서 SettingActivity 문제인 것을 깨닫고 이유를 찾아보았는데, 문제는 manifests에 있었다.
<activity
android:name=".activity.SettingActivity"
android:exported="true" />
manifests에 이 내용을 다시 추가해주고 빌드하니 성공했다. 다음에 새로 액티비티 만들면 꼭 manifests 확인하자..
fun getAutoLogin(context: Context): Boolean {
return getSharedPreference(context).getBoolean(AUTO_LOGIN, false)
}
fun setAutoLogin(context: Context, value: Boolean) {
getSharedPreference(context).edit()
.putBoolean(AUTO_LOGIN, value)
.apply()
}
- get()은 값을 읽어오는 것, set()은 값을 작성하는 것
- 앱 전역에서 호출되기 때문에 Object 키워드로 싱글톤으로 만들어줘야 한다.
- 어플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고 그 메모리에 인스턴스르 만들어 사용하는 디자인 패턴이다.
- 인스턴스가 1개만 생성되는 특징을 가진 이 패턴을 이용하면, 하나의 인스턴스를 메모리에 등록해서 여러 쓰레드가 동시에 해당 인스턴스를 공유하여 사용할 수 있게끔 할 수 있기 때문에 요청이 많은 곳에서 사용하면 효율이 높아진다.
findNavController().navigate(R.id.action_onboarding1Fragment_to_onboarding2Fragment)
- 프래그먼트 전환 로직 (NavController)
fun init(context: Context) {
preferences = context.getSharedPreferences(STORAGE_KEY, Context.MODE_PRIVATE)
}
- LoginSharedPreferences에서 init을 실행시키려면!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
LoginSharedPreferences.init(this)
}
- 처음 시작하는 뷰인 OnboardingActivity에서 init 호출을 해줘야한댜 !!