- Constraint/LinearLayout
- MVVM Pattern
- ViewModel
- LiveData
- Data Binding
- SharedPreference
- RecyclerView
- ViewPager2
- EncryptedSharedPreference
- Coroutine
- Retrofit2
2020/10/10 (토)
메인 Activity에서 다른 Activity를 띄우고 그 Activity에서 입력한 값을 Activity를 종료하고 메인 액티비티로 돌아올 때 사용하고 싶으면
- Main에서 startActvityForResult 호출
val intent = Intent(applicationContext, SignUpActivity::class.java)
startActivityForResult(intent,SIGN_UP_CODE)
- 다른 액티비티에서 intent에 필요한 데이터를 넣음
intent.putExtra("id", et_id.text.toString())
intent.putExtra("password", et_password.text.toString())
- requestCode로 정상종료가 되었는 지 판단하고 조건에 따라서 data 변수를 통해 intent에 저장했던 데이터 사용
if(requestCode == SIGN_UP_CODE) {
loginId = data!!.getStringExtra("id")
loginPassword = data.getStringExtra("password")
}
- SharedPreference를 싱글턴 객체(object 객체)로 만듦 (LoginPreference)
- Application을 상속받은 클래스에서 LoginPreference 초기화
- 이미 싱글턴 객체에 필요한 변수에 대해서 getter/setter를 설정했으므로 싱글턴 객체의 멤버변수를 바꿔주는 것으로 SharedPreference 내의 value 변경 가능
var myIsLogin: Boolean
get() = prefs.getBoolean(isLogin.first, isLogin.second)
set(value) = prefs.edit{
it.putBoolean(isLogin.first, value)
}
var myId:String
get() = prefs.getString(mail.first, mail.second)?:""
set(value) = prefs.edit {
it.putString(mail.first, value)
}
var myEtPassword:String
get() = prefs.getString(password.first, password.second)?:""
set(value) = prefs.edit() {
it.putString(password.first, value)
}
2020/10/30 (금)
- ViewModel과 View를 분리하여 데이터를 처리하는 클래스(ViewModel)/화면에서 일어나는 이벤트, 화면전환과 같은 일을 처리하는 클래스(Activity)로 분리하였다
- View 클래스에서 ViewModel의 데이터에 직접 참조하지 않고 ViewModel의 데이터가 변경되면 이벤트가 발생시키는 LiveData를 활용하였다. MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Data Binding
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.mainViewModel = activityViewModel
binding.lifecycleOwner = this
// Observe Data
activityViewModel.isRegisterClicked.observe(this, {
registerButtonClickListener(it)
})
activityViewModel.isSignIn.observe(this, {
signInButtonClickListener(it)
})
}
data class ProjectData
data class ProjectData (
val img_portfolio : Int,
val txt_title : String,
val txt_explain : String
)
class ProjectViewHolder
class ProjectViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val img_portfolio = itemView.findViewById<ImageView>(R.id.img_project)
val txt_title = itemView.findViewById<TextView>(R.id.txt_title)
val txt_explain = itemView.findViewById<TextView>(R.id.txt_explain)
fun onBindViewHolder(projectData: ProjectData) {
img_portfolio.setImageResource(projectData.img_portfolio)
txt_title.text = projectData.txt_title
txt_explain.text = projectData.txt_explain
}
}
class ProjectAdapter
class ProjectAdapter(private val context : Context) : RecyclerView.Adapter<ProjectViewHolder>() {
var datas = mutableListOf<ProjectData>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProjectViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.item_project, parent, false)
return ProjectViewHolder(view)
}
override fun onBindViewHolder(holder: ProjectViewHolder, position: Int) {
holder.onBindViewHolder(datas[position])
}
override fun getItemCount(): Int = datas.size
}
ItemTouchHelperCallback
class ItemTouchHelperCallback(private val context : Context,
adapter: ProjectAdapter) : ItemTouchHelper.Callback() {
private val mAdapter = adapter
// 길게 누를 시 아이템의 이동 가능 여부 결정
override fun isLongPressDragEnabled(): Boolean {
return true
}
// 아이템 스와이프 가능 여부 결정
override fun isItemViewSwipeEnabled(): Boolean {
return true
}
// 이동/스와이프 방향의 결정
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
if(recyclerView.layoutManager == GridLayoutManager(context, 3)) {
val dragFlags =
ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
val swipeFlags = 0
return makeMovementFlags(dragFlags, swipeFlags)
} else {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return makeMovementFlags(dragFlags, swipeFlags)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if(viewHolder.itemViewType != target.itemViewType)
return false
mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
// 스와이프할 때 아이템 삭제 가능
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
mAdapter.onItemDismiss(viewHolder.adapterPosition)
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val alpha = ALPHA_FULL - Math.abs(dX) / viewHolder.itemView.width
viewHolder.itemView.apply {
setAlpha(alpha)
translationX = dX
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
}
class ProjectAdapter
아이템들이 이동한 것, 삭제된 결과를 Adapter 내의 List에 반영
override fun onItemMove(from: Int, to: Int): Boolean {
if(from < to) {
for(i in from until to) {
Collections.swap(datas, i, i+1)
}
} else {
for(i in from downTo (to+1)) {
Collections.swap(datas, i, i-1)
}
}
notifyItemMoved(from, to)
return true
}
override fun onItemDismiss(position: Int) {
datas.removeAt(position)
notifyItemRemoved(position)
}
2020/11/1 (일)
- Activity에서 Fragment로 뷰 변경
- Fragment로 뷰가 변함에 따라 ViewModel, DataBinding 방식도 변경
SearchFragment.kt
class SearchFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentSearchBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_search, container, false)
return binding.root
}
}
ProjectFragment.kt
class ProjectFragment : Fragment(), OnStartDragListener {
private val welcomeViewModel : WelcomeViewModel by activityViewModels()
private lateinit var projectBinding: FragmentProjectBinding
private lateinit var mAdapter: ProjectAdapter
private lateinit var touchHelper: ItemTouchHelper
var data = mutableListOf<ProjectData>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
projectBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_project, container, false)
projectBinding.welcomeViewModel = welcomeViewModel
projectBinding.lifecycleOwner = viewLifecycleOwner
return projectBinding.root
}
}
- 지난번에 미완성한 2주차 성장과제 완료/3주차 필수과제 완료
ViewPager2(ViewPager 대신 ViewPager2로 구현)
- FragmentStateAdapter를 상속받는 MainViewPagerAdapter를 만들고 이를 ViewPager2에 부착
MainViewPagerAdapter.kt
class MainViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
// 전체 페이지 수
override fun getItemCount(): Int = 3
// 해당 위치에 맞는 Fragment 생성
override fun createFragment(position: Int): Fragment {
return when(position) {
0 -> ProjectFragment()
1 -> SearchFragment()
else -> ProfileFragment()
}
}
}
WelcomeActivity.kt
class WelcomeActivity : AppCompatActivity() {
private val welcomeViewModel: WelcomeViewModel by viewModels()
private lateinit var binding: ActivityWelcomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWelcomeBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.welcomeViewModel = welcomeViewModel
binding.lifecycleOwner = this
val mainViewPagerAdapter = MainViewPagerAdapter(this)
binding.vpFragmentSlider.apply {
adapter = mainViewPagerAdapter
registerOnPageChangeCallback(PageChangeCallBack())
}
}
}
BottomNavigation
- SetOnNavigationItemSelectedListener와 PagerChangeCallback을 활용하여 Bottom Nav의 탭을 누르면 ViewPager2의 페이지가 뜰 수 있게
class WelcomeActivity : AppCompatActivity() {
private val welcomeViewModel: WelcomeViewModel by viewModels()
private lateinit var binding: ActivityWelcomeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWelcomeBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.welcomeViewModel = welcomeViewModel
binding.lifecycleOwner = this
val mainViewPagerAdapter = MainViewPagerAdapter(this)
binding.vpFragmentSlider.apply {
adapter = mainViewPagerAdapter
registerOnPageChangeCallback(PageChangeCallBack())
}
binding.welcomeBottomnav.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.main_project -> binding.vpFragmentSlider.currentItem = 0
R.id.main_search -> binding.vpFragmentSlider.currentItem = 1
R.id.main_proile -> binding.vpFragmentSlider.currentItem = 2
}
true
}
}
// ViewPager2의 PagerChangeCallback(바텀 네비의 선택된 탭에 맞춰서 페이징함)
private inner class PageChangeCallBack : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
binding.welcomeBottomnav.selectedItemId = when (position) {
0 -> R.id.main_project
1 -> R.id.main_search
2 -> R.id.main_proile
else -> throw IllegalAccessException("No Such Position")
}
}
}
}
Tab Layout
- 검색 기능 구현을 위해 검색화면 구현, 아래 다른 검색 기능을 두기 위해 TabLayout 설치, addOnTabSelectedListener를 활용하여 ViewPager에 걸린 Fragment를 페이징
SearchFragment.kt
class SearchFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentSearchBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_search, container, false)
val mainViewPagerAdapter = SearchViewPagerAdapter(childFragmentManager)
binding.vpSearchSlider.adapter = mainViewPagerAdapter
binding.tabSearch.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
binding.vpSearchSlider.setCurrentItem(tab?.position!!)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
return binding.root
}
}
Repository Pattern(MVVM Architecture) & Coroutine
- SignUpRepository
class SignUpRepository(
private val retrofitService: RetrofitService
) {
suspend fun signUp(requestSignUp: RequestSignUp) = retrofitService.postSignUpUser(requestSignUp)
}
- MainViewModel에서 validation 과정
suspend fun validateUserData() : Boolean {
val validationData = viewModelScope.async {
repository.signIn(RequestSignIn(
email = editId.get()!!,
password = editPassword.get()!!
))
}.await()
Log.d("log user data", "${validationData}")
return validationData.success
}
- MainActivity
private fun loginProcess() {
CoroutineScope(Dispatchers.Main).launch {
if (activityViewModel.validateUserData()) {
activityViewModel.setAutoLoginInfo()
"로그인 성공".toast()
transferActivity(WelcomeActivity::class.java)
finish()
} else {
"회원정보가 잘못되었습니다 다시 하세요".toast()
}
}
}
코루틴을 사용함으로써, 비동기 처리를 자동으로 해줘 enqueue와 같은 BoilerPlate 코드를 줄일 수 있음 viewModelScope, CoroutineScope로 원하는 Dispatcher에서 함수를 실행시키면서 Main 스레드를 블로킹시키지 않고 결과값을 받아낼때까지 지연시킬 수 있음