💡 빈 칸 안내
if (name.isEmpty() || id.isEmpty() || password.isEmpty()) {
toast("빈칸없이 모두 작성해주세요:)")
}
- isEmpty : 빈 칸이 있는지 확인.
💡 비밀번호 가리기
<EditText
...
android:hint="비밀번호를 입력하세요:)"
android:inputType="textPassword" />
- hint : 미리보기 글씨 제공
- inputType : 입력 내용 가리기
💡 화면 이동 및 자동 입력
// SignUpActivity
val intent = Intent()
intent.putExtra("Id", id)
intent.putExtra("Pw", password)
setResult(Activity.RESULT_OK,intent)
finish()
// SignInActivity
private val REQUEST_CODE_LOGIN_ACTIVITY =1000
...
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode==REQUEST_CODE_LOGIN_ACTIVITY){
if(resultCode== Activity.RESULT_OK){
val id=data!!.getStringExtra("Id")
val pw=data!!.getStringExtra("Pw")
edt_sign_in_id.setText(id)
edt_sign_in_password.setText(pw)
}
}
}
- requestCode : activity 구별
- onActivityResult : 호출 Activity로부터 결과를 받기 위해 사용
- putExtra, getStringExtra : 값 전달
- setText : 내용 자동 입력
💡 자동 로그인
// SignInPreference
private const val ID_KEY = "id"
fun getId(context: Context): String{
val sharedPreferences = context.getSharedPreferences(ID_KEY, Context.MODE_PRIVATE)
return sharedPreferences.getString(ID_KEY,"")?:""
}
fun setId(context: Context, id: String){
val sharedPreferences = context.getSharedPreferences(ID_KEY,Context.MODE_PRIVATE)
sharedPreferences
.edit()
.putString(ID_KEY,id)
.apply()
}
// SignInActivity
private fun autoSignIn() {
val id = SignInPreference.getId(this)
if (id.isNotEmpty()) {
val intent = Intent(this, HomeActivity::class.java)
toast("자동로그인 되었습니다:)")
startActivity(intent)
}
}
// ToastExt
fun Context.toast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
- sharedPreferences : id, pw를 기억
- toast : 토스트 메시지 안내( 확장함수로 이용 )
💡 RecyclerView
data class PortfolioListData(
val image: Int,
val title: String,
val subTitle: String,
val date: String,
val contents: String
)
// rv_item_portfolio.xml
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="item"
type="com.example.mh_onsopt_assignment.vo.PortfolioListData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<de.hdodenhof.circleimageview.CircleImageView
setImage="@{item.image}"
class PortfolioAdapter(private val context: Context) : RecyclerView.Adapter<PortfolioViewHolder>() {
var data = mutableListOf<PortfolioListData>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PortfolioViewHolder {
val binding:RvItemPortfolioBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.rv_item_portfolio,
parent,
false
)
return PortfolioViewHolder(binding)
}
override fun onBindViewHolder(holder: PortfolioViewHolder, position: Int) {
data[position].let {
holder.bind(it)
}
...
}
override fun getItemCount(): Int = data.size
}
class PortfolioViewHolder(val binding: RvItemPortfolioBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(item: PortfolioListData){
binding.item = item
}
}
// PortfolioFragment
private fun initRvList(){
portfolioAdapter = PortfolioAdapter(requireContext())
binding.rvPortfolio.adapter = portfolioAdapter
portfolioAdapter.data = dummyPortfolioList.getPortfolioList()
portfolioAdapter.notifyDataSetChanged()
}
- data class : RecyclerView Item 내용 정의
- Adapter : 데이터를 ViewHolder에 전달
- ViewHolder : 데이터를 뷰에 뿌려줌
- Fragment에서 RecyclerView와 Adapter를 연결.
- 데이터 입력(dummyPortfolioList) 후, 갱신(notifyDataSetChanged) 알리기
💡 GridLayout
<androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
...
tools:listitem="@layout/rv_item_portfolio_2" />
- layoutManager : 레이아웃 설정(GridLayout이므로 spanCount까지 설정해줍니다.)
- tools:listitem을 사용하여 적용된 프리뷰를 편리하게 바로 확인
💡 ViewPager 및 BottomNavigation
class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT){
var fragments = listOf<Fragment>()
override fun getItem(position: Int): Fragment {
return fragments[position]
}
override fun getCount(): Int {
return fragments.size
}
}
private lateinit var viewpagerAdapter : ViewPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
viewpagerAdapter = ViewPagerAdapter(supportFragmentManager)
viewpagerAdapter.fragments = listOf(
ProfileFragment(),
PortfolioFragment(),
SettingFragment()
)
vp_home.adapter = viewpagerAdapter
vp_home.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {}
override fun onPageSelected(position: Int) {
bottom_navi.menu.getItem(position).isChecked = true
}
})
bottom_navi.setOnNavigationItemSelectedListener {
var index by Delegates.notNull<Int>()
when(it.itemId){
R.id.menu_profile -> index = 0
R.id.menu_portfolio -> index = 1
R.id.menu_setting -> index = 2
}
vp_home.currentItem = index
true
}
...
- ViewPagerAdapter : Fragment 연결
- addOnPageChangeListener : 화면전환을 감지
- setOnNavigationItemSelectedListener: 각 탭 클릭 시, 이벤트 처리
💡 TabLayout
rootView.tl_profile.setupWithViewPager(rootView.vp_profile)
rootView.tl_profile.apply {
getTabAt(0)?.text = "첫 번째"
getTabAt(1)?.text = "두 번째"
}
- 위와 동일하게 ViewPager 구현 후, TabLayout과 연결
- getTabAt(index)?.text : 인텍스와 일치하는 탭 아이템의 title 입력
💡 빈 칸 안내
// Response
data class ResponseSignData(
val data: Data,
val message: String,
val status: Int,
val success: Boolean
) {
data class Data(
val email: String,
val password: String,
val userName: String
)
}
// Request
data class RequestSignupData(
val email : String,
val password : String,
val userName : String
)
interface SoptService {
@Headers("Content-Type: application/json")
@POST("users/signup")
fun postSignUp(
@Body body : RequestSignupData
) : Call<ResponseSignData>
...
object SoptServiceImpl {
private const val BASE_URL = "http://15.164.83.210:3000/"
private val retrofit : Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val service : SoptService = retrofit.create(SoptService::class.java)
}
private fun postSignUp(email:String, password:String, name:String){
val call : Call<ResponseSignData> = SoptServiceImpl.service.postSignUp(
RequestSignupData(email = email,password = password, userName = name)
)
call.enqueue(object : Callback<ResponseSignData> {
override fun onFailure(call: Call<ResponseSignData>, t: Throwable) {
// 통신 실패 로직
Log.d("명","실패")
}
override fun onResponse(
call: Call<ResponseSignData>,
response: Response<ResponseSignData>
) {
response.takeIf { it.isSuccessful}
?.body()
?.let { it ->
val intent = Intent()
intent.putExtra("Id", email)
intent.putExtra("Pw", password)
setResult(Activity.RESULT_OK,intent)
finish()
} ?: showError(response.errorBody())
}
})
}
private fun showError(error : ResponseBody?){
val e = error ?: return
val ob = JSONObject(e.string())
toast(ob.getString("message"))
}
- data class : 통신을 위한 객체 정의
- SoptService : 식별 URL을 interface로 설계
- SoptServiceImpl : base_url이 있는 실제 구현체 생성
- showError : error message를 Toast message로 보여줌(ex: 이미 존재하는 이메일 입니다.)
- SoptServiceImpl를 이용하여 Call 객체를 받아 온 후, enqueue를 호출하여 통신을 비동기적으로 요청. CallBack 익명클래스를 선언하여 통신이 실패/성공한 경우를 나눠 구현.