kmebin / ON-SOPT-ANDROID

27기 ON SOPT 안드로이드 파트 세미나 📚

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SOPT-27th-Android



📝 1주차 과제 (2020-10-16 / 2020-10-21)

💻 필수 과제 (2020-10-16)

  • 로그인 화면

login

  • 빈칸이 있는 경우 회원가입 화면

signup


💻 성장 과제 1 (2020-10-16)

  • 회원가입 완료 후 이전 로그인 화면으로 돌아오기

login2


💻 성장 과제 2 (2020-10-21)

  • 자동 로그인 화면

login3 login4


📝 코드 설명

💡 startActivityForResult

LoginActivity.kt

  • 회원가입 버튼을 클릭했을 때, LoginActivity에서 SignUpActivity로 이동합니다.

  • SignUpActivity가 종료되면서 데이터를 받아오기 위해 startActivityForResult()를 사용했습니다.

btn_sign_up.setOnClickListener {
    val intent = Intent(this, SignUpActivity::class.java)
    startActivityForResult(intent, 111)
}

SignUpActivity.kt

  • 회원가입 버튼을 클릭했을 때, EditTextView에 값이 하나라도 없을 경우 "빈칸이 있습니다."라는 ToastMessage를 띄웁니다.

  • 회원가입 버튼을 클릭했을 때, 모든 EditTextView에 값이 있는 경우 "회원가입이 완료되었습니다."라는 ToastMessage를 띄우고, Id와 Password 값을 넣어줍니다.

  • setResult()로 값을 설정해주고, finish()로 현재 액티비티를 종료한 후 LoginActivity로 돌아옵니다.

btn_sign_up.setOnClickListener {

    var inputName = et_name.text.toString()
    var inputId = et_id.text.toString()
    var inputPw = et_pw.text.toString()

    if (inputName.isEmpty() || inputId.isEmpty() || inputPw.isEmpty())
        Toast.makeText(this, "빈칸이 있습니다.", Toast.LENGTH_SHORT).show()
    else {
        Toast.makeText(this, "회원가입이 완료되었습니다.", Toast.LENGTH_SHORT).show()

        val intent = Intent()
        intent.putExtra("id", et_id.text.toString())
        intent.putExtra("password", et_pw.text.toString())
        setResult(Activity.RESULT_OK, intent)
        finish()
    }
}

LoginActivity.kt

  • LoginActivity로 돌아오면서 onActivityResult()가 실행됩니다.

  • 회원가입 화면에서 입력했던 아이디와 비밀번호를 받아와 로그인 화면에 입력되어 있게 합니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK && requestCode == 111) {
        var userId = data!!.getStringExtra("id")
        var userPw = data.getStringExtra("password")

        editText_id.setText(userId)
        editText_pw.setText(userPw)
    }
}

💡 SharedPreferences

LoginActivity.kt

  • SharedPreferences()Map 형태로 간단한 값을 저장할 수 있습니다.

  • SharedPreferences.Editor는 SharedPreferences 개체의 값을 수정하는데 사용되는 인터페이스입니다.

val sharedPref: SharedPreferences = getSharedPreferences("pref", Context.MODE_PRIVATE)
val sharedEdit = sharedPref.edit()

  • 로그인 버튼을 클릭했을 때, "반갑습니다."라는 ToastMessage를 띄우고 HomeActivity로 이동합니다.

  • Editor 객체에 putString()을 통해 Map 형태로 데이터를 저장합니다.

  • 반드시 apply() 또는 commit()을 해줍니다.

btn_login.setOnClickListener {
    Toast.makeText(this, "반갑습니다.", Toast.LENGTH_SHORT).show()

    sharedEdit.putString("Id", editText_id.text.toString())
    sharedEdit.putString("Password", editText_pw.text.toString())
    sharedEdit.apply()

    val intent = Intent(this, HomeActivity::class.java)
    startActivity(intent)
}

  • getString()을 통해 해당 Key 값을 가진 string을 가져옵니다. 존재하지 않는다면 default 값을 가져옵니다.
var idValue = sharedPref.getString("Id", "")
var pwValue = sharedPref.getString("Password", "")

editText_id.setText(idValue)
editText_pw.setText(pwValue)

  • SharedPreferences 안에 값이 저장되어 있다면, 자동 로그인 되었다는 ToastMessage를 띄우고 HomeActivity로 이동합니다.

  • 앱을 재시작했을 때 자동 로그인이 됩니다.

if (idValue.toString().isNotBlank() && pwValue.toString().isNotBlank()) {
    Toast.makeText(this, "${idValue.toString()}님 자동 로그인 되었습니다.", Toast.LENGTH_SHORT).show()

    val intent = Intent(this, HomeActivity::class.java)
    startActivity(intent)
}



📝 2주차 과제 (2020-10-29)

💻 필수 과제 (2020-10-29)

  • 로그인했을 때의 화면과 각 아이템을 클릭했을 때의 상세 화면

recyclerview detail


💻 성장 과제 1 (2020-10-29)

  • 격자 형태(GridLayout)로 바뀐 화면

gridLayout


💻 성장 과제 2 (2020-10-29)

  • 아이템 이동 및 삭제 화면

move remove


📝 코드 설명

💡 RecyclerView

ProfileData.kt

  • 아이템에 대한 데이터 객체를 만들기 위해 data class를 생성합니다.
data class ProfileData(
    val title : String,
    val subTitle : String,
    val contents : String
)

ProfileViewHolder.kt

  • Adapter에서 전달받은 데이터를 layout에 넣어줍니다. 이를 Bind한다고 표현합니다.

  • onBind()는 Adapter에서 호출되고, 실질적으로 데이터를 요소들에 넣어주는 메소드입니다.

class ProfileViewHolder (itemView : View) : RecyclerView.ViewHolder(itemView){

    private val title : TextView = itemView.findViewById(R.id.tv_title)
    private val subTitle : TextView = itemView.findViewById(R.id.tv_subtitle)

    fun onBind(data : ProfileData){
        title.text = data.title
        subTitle.text = data.subTitle
    }
}

ProfileAdapter.kt

  • Adapter에는 onCreateViewHolder(), getItemCount(), onBindViewHolder() 3가지 메소드를 반드시 오버라이드 해줘야 합니다.

  • setOnClickListener를 통해 아이템을 클릭하면 상세 화면으로 이동하게 했습니다.

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {

    val view = LayoutInflater.from(context).inflate(R.layout.profile_item_list, parent, false)

    return ProfileViewHolder(view).apply {
        // item을 클릭하면 상세 화면으로 이동
        itemView.setOnClickListener {
            val curPosition : Int = adapterPosition
            val profile : ProfileData = data.get(curPosition)
            val intent = Intent(context, ProfileDetailActivity::class.java)

            intent.putExtra("title", profile.title)
            intent.putExtra("subTitle", profile.subTitle)
            intent.putExtra("contents", profile.contents)
            context.startActivity(intent)
        }
    }
}

override fun getItemCount(): Int = data.size

override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
    holder.onBind(data[position])
}

💡 GridLayout

ProfileActivity.kt

  • 우선 RecyclerView의 배치 방향을 LinearLayoutManager로 설정하였습니다.

  • profileAdapter에 리스트로 보여줄 데이터들을 넣어줍니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_profile)

    profileAdapter = ProfileAdapter(this)

    rv.adapter = profileAdapter
    rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

    profileAdapter.data = mutableListOf(
        ProfileData("이름", "김희빈", "안녕하세요. 김희빈입니다."),
        ProfileData("나이", "23", "1998년 5월 31일생입니다."),
        ProfileData("파트", "안드로이드", "안드로이드 파트 YB입니다."),
        ProfileData("Github", "www.github.com/kmebin", "repository : SOPT-27th-Android"),
        ProfileData("Blog", "blowhui.tistory.com", "J가 되고 싶은 P의 개발 블로그"),
        ProfileData("Sopt", "www.sopt.org", "SHOUT OUR PASSION TOGETHER! 27기 ON SOPT")
    )

    profileAdapter.notifyDataSetChanged()
}

  • Option Menu를 사용해서 RecyclerView의 Layout을 변경할 수 있게 했습니다.
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

    val inflater = menuInflater
    inflater.inflate(R.menu.menu_layout, menu)

    return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    
    when(item.itemId) {
        R.id.linear -> {
            rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
            return true
        }
        R.id.grid -> {
            rv.layoutManager = GridLayoutManager(applicationContext, 2, LinearLayoutManager.VERTICAL, false)
            return true
        }
        else -> {
            return super.onOptionsItemSelected(item)
        }
    }
}

💡 ItemTouchHelper

ItemTouchHelper.kt

  • ItemTouchHelper 클래스를 사용하여 아이템을 drag&drop하고 swipe하는 것을 구현할 수 있습니다.

  • SimpleCallback() 에 drag와 swipe할 방향을 각각 설정해 줍니다.

val helper = ItemTouchHelper(object :
    ItemTouchHelper.SimpleCallback(
        ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END,
        ItemTouchHelper.START){

  • drag할 아이템의 위치와 drop할 아이템의 위치를 notifyItemMoved() 메소드에 전달하여 이동시킬 수 있습니다.
override fun onMove(
    recyclerView: RecyclerView,
    from: RecyclerView.ViewHolder,
    to: RecyclerView.ViewHolder
): Boolean {
    val fromPosition = from.adapterPosition
    val toPosition = to.adapterPosition
    adapter.notifyItemMoved(fromPosition, toPosition)
    return false
}

  • swipe를 통해 제거할 아이템의 위치를 notifyItemRemoved() 에 전달하여 삭제할 수 있습니다.
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
    adapter.data.removeAt(viewHolder.adapterPosition)
    adapter.notifyItemRemoved(viewHolder.adapterPosition)
}



📝 3주차 과제 (2020-11-06)

💻 필수 과제 (2020-11-06)

move

  • 전체 화면 : ViewPager + BottomNavigation

  • ViewPager : 프로필 화면(HomeFragment) - 리사이클러뷰 화면(ProfileFragment) - 비어있는 화면(SettingsFragment)

  • 프로필 화면(HomeFragment) : TabLayout + ChildFragment(HomeInfoFragment - HomeOtherFragment)


📝 코드 설명

💡 ViewPager

MainPagerAdapter.kt

  • ViewPager도 RecylerView와 같이 입력받은 데이터 리스트를 화면에 배치하기 위한 Adapter 구현이 필요합니다.

  • ViewPagerAdapter 역할을 하기 위해 FragmentStatePagerAdapter를 상속받습니다. 보여지는 화면 기준 양 옆의 프래그먼트를 제외한 나머지를 완전히 파괴하는 방식을 사용하기 때문에 메모리 누수 관리에 효과적입니다.

  • getItem()에 ViewPager의 각 position에서 보여줄 프래그먼트들을 지정합니다.

  • getCount()는 Adapter에서 만들 페이지 수를 반환합니다.

class MainPagerAdapter(fm: FragmentManager)
    : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    var fragments = listOf<Fragment>()

    override fun getItem(position: Int): Fragment = when(position){
        0 -> HomeFragment()
        1 -> ProfileFragment()
        2 -> SettingsFragment()
        else -> throw IllegalStateException("Unexpected position $position")
    }

    override fun getCount(): Int = 3
}

MainActivity.kt

  • onCreate()에서 ViewPager에 선언한 Adapter를 장착합니다.
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        vp_main.adapter = MainPagerAdapter(supportFragmentManager)
    }
    
    ...
}

💡 BottomNavigation

bottom_navi_menu.xml

  • 하단 탭의 메뉴 아이템을 생성할 xml 파일을 만들어 줍니다.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_profile"
        android:icon="@drawable/ic_baseline_home"
        android:title="Profile"/>
    <item
        android:id="@+id/menu_recycler"
        android:icon="@drawable/ic_baseline_list"
        android:title="List"/>
    <item
        android:id="@+id/menu_settings"
        android:icon="@drawable/ic_baseline_settings"
        android:title="Settings"/>
</menu>

bottom_navi_color.xml

  • 하단 탭의 메뉴 아이템의 아이콘 색상을 설정할 xml 파일을 만들어 줍니다.
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#FFC107" android:state_checked="true"/>
    <item android:color="#9E9E9E" android:state_checked="false"/>
</selector>

activity_main.xml

  • 하단 탭을 배치할 xml 파일에 BottomNavigationView를 추가합니다.

  • app:menu=에 생성한 메뉴 xml 파일을 넣어줍니다.

<com.google.android.material.bottomnavigation.BottomNavigationView
    ...
    app:itemIconTint="@color/bottom_navi_color"
    app:itemRippleColor="#F3EBAB"
    app:itemTextColor="@color/bottom_navi_color"
    app:menu="@menu/bottom_navi_menu"/>

MainActivity.kt

  • onStart()에서 addViewPagerListenersetBottomNavigationListener를 호출합니다.
class MainActivity : AppCompatActivity() {
    ...
    
    override fun onStart() {
        super.onStart()

        vp_main.addViewPagerListener(bottom_navi)
        bottom_navi.setBottomNavigationListener(vp_main)
    }
}

addViewPagerListener.kt

  • BottomNavigation을 ViewPager와 연동하기 위해서는 페이지 변경에 관한 리스너가 필요합니다.

  • onPageScrollStateChanged(), onPageScrolled() : 화면 전환을 감지하는 리스너입니다.

  • onPageSelected: ViewPager의 페이지 중 하나가 선택된 경우 그에 대응되는 하단 탭의 상태를 변경합니다.

fun ViewPager.addViewPagerListener(bottomNavigationView: BottomNavigationView) {
    this.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{

        override fun onPageScrollStateChanged(state: Int) {}

        override fun onPageScrolled(
            position: Int,
            positionOffset: Float,
            positionOffsetPixels: Int
        ) {}

        override fun onPageSelected(position: Int) {
            bottomNavigationView.menu.getItem(position).isChecked = true
        }
    })
}

setBottomNavigationListener.kt

  • BottomNavigation을 세팅하는 리스너입니다.

  • setOnNavigationItemSelectedListener : 각 탭을 클릭했을 때 이벤트를 처리하는 리스너입니다.

  • 각 탭을 선택했을 때 ViewPager의 해당 페이지로 화면이 전환됩니다.

fun BottomNavigationView.setBottomNavigationListener(viewPager: ViewPager) {
    this.setOnNavigationItemSelectedListener {
        when(it.itemId) {
            R.id.menu_profile -> viewPager.currentItem = 0
            R.id.menu_recycler -> viewPager.currentItem = 1
            R.id.menu_settings -> viewPager.currentItem = 2
        }
        true
    }
}

💡 HomeFragment + TabLayout

HomeFragment.kt

  • Fragment는 onCreateView()가 Activity에서의 onCreate() 역할을 합니다.
class HomeFragment : Fragment() {

    private lateinit var homeViewPagerAdapter: HomeViewPagerAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_home, container, false)

        return view
    }
    
    ...

}    

  • onViewCreated()에서는 onCreateView()에서 return해 준 View들을 가지고 다룰 수 있습니다.

  • setupWithViewPager()TabLayout과 ViewPager를 연동합니다.

  • 탭 아이템의 title을 설정하는 getTabAt()는 반드시 연동 후에 작성해야 합니다.

class HomeFragment : Fragment() {
    
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        homeViewPagerAdapter = HomeViewPagerAdapter(childFragmentManager)
        vp_home.adapter = homeViewPagerAdapter

        tl_home.setupWithViewPager(vp_home)
        tl_home.apply {
            getTabAt(0)?.text = "INFO"
            getTabAt(1)?.text = "OTHER"
        }
        super.onViewCreated(view, savedInstanceState)
    }
}



📝 6주차 과제 (2020-12-03)

💻 필수 과제 (2020-12-03)

  • 회원가입/로그인 완료 화면

signup_gif login_gif

  • POSTMAN 테스트

login_img

login_img


💻 성장 과제 1, 2 (2020-12-10)

  • 더미데이터/카카오 웹 검색

6th_gif


📝 코드 설명

💡 Retrofit2

SoptService.kt

  • 식별 URL을 Interface로 설계합니다.

  • Http method 종류가 POST 이면, @Body 를 통해 RequestBody를 설정하고, return 값으로는 Response 객체를 줍니다.

  • 서버에 데이터를 단순 요구할 때는 GET 방식을 사용하며, 쿼리 매개변수를 사용할 경우에는 @Query로 표현합니다.

interface SoptService {
    // 회원가입
    @Headers("Content-Type: application/json")
    @POST("/users/signup")
    fun postSignUp(
            @Body body: RequestSignUpData
    ) : Call<ResponseSignUpData>

    // 로그인
    @Headers("Content-Type: application/json")
    @POST("/users/signin")
    fun postLogin(
        @Body body : RequestLoginData
    ) : Call<ResponseLoginData>
    
    // 더미데이터
    @Headers("Content-Type: application/json")
    @GET("/api/users")
    fun getDummy(
        @Query("page") page : Int
    ) : Call<ResponseDummyData>

    // 카카오 웹 검색
    @Headers("Authorization: KakaoAK {RSET_API_KEY}")
    @GET("/v2/search/web")
    fun getWebSearch(
        @Query("query") web : String
    ):Call<ResponseKakaoData>
}

RequestSignUpData.kt

  • 회원가입 Request 객체를 생성합니다.
data class RequestSignUpData(
    val email : String,
    val password : String,
    val userName : String
)

RequestLoginData.kt

  • 로그인 Request 객체를 생성합니다.
data class RequestLoginData(
    val email : String,
    val password : String
)

ResponseSignUpData.kt

  • 회원가입 Response 객체를 생성합니다.
data class ResponseSignUpData(
    val status : Int,
    val success : Boolean,
    val message : String,
    val data : SignUpData
) {
    data class SignUpData(
        val email : String,
        val password : String,
        val userName : String
    )
}

ResponseLoginData.kt

  • 로그인 Response 객체를 생성합니다.
data class ResponseLoginData(
    val status : Int,
    val success : Boolean,
    val message : String,
    val data : LoginData,    
) {
    data class LoginData(
        val email : String,
        val password : String,
        val userName : String
    )
}

ResponseDummyData.kt

  • 더미데이터 Response 객체를 생성합니다.
data class ResponseDummyData(
    val page: Int,
    val per_page: Int,
    val total: Int,
    val total_pages: Int,
    val data: List<DummyData>,
    val support: Support
){
    data class DummyData(
        val id: Int,
        val email: String,
        val first_name: String,
        val last_name: String,
        val avatar: String
    )

    data class Support(
        val url: String,
        val text: String
    )
}

ResponseKakaoData.kt

  • 카카오 웹 검색 Response 객체를 생성합니다.
data class ResponseKakaoData(
	val meta: Meta,
	val documents: List<Document>
){
	data class Meta(
		val total_count: Int,
		val pageable_count: Int,
		val is_end: Boolean
	)

	data class Document(
		val datetime: String,
		val contents: String,
		val title: String,
		val url: String
	)
}

SoptServiceImpl.kt

  • Retrofit Interface의 실제 구현체는 하나만 생성하여 프로젝트 어디서나 사용할 수 있도록 싱글톤으로 만들어줍니다.
  • 싱글톤 객체로 사용하기 위해 object로 선언합니다.
  • 메인 서버 URL을 담을 변수들을 생성해 줍니다.
  • Retrofit 객체 retrofit을 생성해 줍니다.
  • Interface 객체를 넘겨 실제 구현체를 생성해 줍니다.
object SoptServiceImpl {
    private const val BASE_URL = "http://15.164.83.210:3000"
    private const val DUMMY_URL = "https://reqres.in"
    private const val KAKAO_URL = "https://dapi.kakao.com"

    private val baseRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val baseService : SoptService = baseRetrofit.create(SoptService::class.java)

    private val dummyRetrofit : Retrofit = Retrofit.Builder()
        .baseUrl(DUMMY_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val dummyService : SoptService = dummyRetrofit.create(SoptService::class.java)

    private val kakaoRetrofit : Retrofit = Retrofit.Builder()
            .baseUrl(KAKAO_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    val kakaoService : SoptService = kakaoRetrofit.create(SoptService::class.java)
}

SignUpActivity.kt

  • Call < Type > : 싱글톤 객체를 통해 비동기적으로 Call 객체를 받아옵니다.
  • Callback < Type > : Type 객체를 받아왔을 때 프로그래머의 행동입니다.
  • onFailure()에는 통신이 실패했을 때 호출되고, 통신이 성공했다면 onResponse()가 호출됩니다.
val call : Call<ResponseSignUpData> = SoptServiceImpl.service.postSignUp(
	RequestSignUpData(email = email, password = password, userName = userName)
)
call.enqueue(object : Callback<ResponseSignUpData>{
	override fun onFailure(call: Call<ResponseSignUpData>, t: Throwable) {
		Log.d("tag", t.localizedMessage)
	}

	override fun onResponse(
        call: Call<ResponseSignUpData>, response:Response<ResponseSignUpData>
    ) {
		response.takeIf { it.isSuccessful }
			?.body()
			?.let { it ->
                   
			} ?: showError(response.errorBody())
	}
})

About

27기 ON SOPT 안드로이드 파트 세미나 📚


Languages

Language:Kotlin 100.0%