Skip to content

Om๐Ÿ‘€Roid - ์˜ค๋Š˜์€ ๋ฌด์Šจ Android?!

Notifications You must be signed in to change notification settings

TeamOmoolen/TeamOmoolen-Android

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation


์˜ค๋Š˜์€ ๋ฌด์Šจ ๋ Œ์ฆˆ? - Om๐Ÿ‘€len

์ฝ˜ํƒํŠธ๋ Œ์ฆˆ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๋งž์ถค ๋ Œ์ฆˆ ์ถ”์ฒœ ๋ฐ ์˜คํ”„๋ผ์ธ ํ”ฝ์—… ์˜ˆ์•ฝ ์„œ๋น„์Šค

๊ตญ๋‚ด ๋ชจ๋“  ๋ Œ์ฆˆ ์ •๋ณด, ๋ฆฌ๋ทฐ ๋ถ€ํ„ฐ ์˜๋ฃŒ ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์˜คํ”„๋ผ์ธ ํ”ฝ์—… ์˜ˆ์•ฝ ๊นŒ์ง€! "์˜ค๋Š˜ ๋ฌด์Šจ ๋ Œ์ฆˆ๋ผ์ง€?" ๊ณ ๋ฏผ๋  ๋•, ์˜ค๋ฌด๋ Œ!

SOPT 28th APPJAM

ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2021.06.26 ~ 2021.07.17

๐Ÿ“„ IA

๐Ÿ” Main Function

Splash ์นด์นด์˜คํ†ก ๋กœ๊ทธ์ธ
์˜จ๋ณด๋”ฉ1,2 ์˜จ๋ณด๋”ฉ3 ์˜จ๋ณด๋”ฉ4
ํ™ˆ ๋ฐœ๊ฒฌ ์ œํ’ˆ ์ƒ์„ธ
์ตœ๊ทผ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ๊ฒ€์ƒ‰



๐Ÿ’ฌ ๊ธฐ๋Šฅ ์ƒ์„ธ

1. Kakaotalk Login

  • ์นด์นด์˜คํ†ก์„ ์ด์šฉํ•˜์—ฌ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ํ•ฉ๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ Kakaotalk Login

    ๐Ÿงพ LoginViewModel.kt

    1. ๋‹จ๋ง ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
    fun newKakao(context:Context){
      if (AuthApiClient.instance.hasToken()) { //๋กœ๊ทธ์ธ์ด ๋œ ์ƒํƒœ์ธ์ง€ ํ™•์ธ
          UserApiClient.instance.accessTokenInfo { tokenInfo, error -> //์„œ๋ฒ„์— ์œ ํšจํ•œ accessํ† ํฐ์ด ์žˆ๋Š”์ง€ ๊ฐ€์ ธ์˜ด
              //ํ˜„์žฌ ์œ ํšจํ•œ accessํ† ํฐ์ด ์—†์Œ
              //accessํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ ๊ฒƒ์ด๋ผ๋ฉด sdk๋‚ด๋ถ€์—์„œ accesstoken์„ ๊ฐฑ์‹ ํ•œ๋‹ค.
              if (error != null) {
                  if (error is KakaoSdkError && error.isInvalidTokenError()) {
                      //accessํ† ํฐ ๊ฐฑ์‹ ๊นŒ์ง€ ์‹คํŒจํ•œ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— refreshํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์Œ, ๋กœ๊ทธ์ธ ํ•„์š”
                      newKakaoLogin(context)
                  }
                  else {
                      //๊ธฐํƒ€ ์—๋Ÿฌ
                  }
              }
              else{
                  //ํ† ํฐ ์œ ํšจ์„ฑ ์ฒดํฌ ์„ฑ๊ณต(ํ•„์š” ์‹œ sdk๋‚ด๋ถ€์—์„œ ํ† ํฐ ๊ฐฑ์‹ ๋จ)
                  newKakaoLogin(context)
              }
          }
      }
      else {
          //๋‹จ๋ง์— ํ† ํฐ์ด ์—†์œผ๋‹ˆ ๋กœ๊ทธ์ธ ํ•„์š”
          newKakaoLogin(context)
      }
    }
    
    1. ์นด์นด์˜คํ†ก ์„ค์น˜ ์—ฌ๋ถ€ ํ™•์ธ ํ›„ ๋กœ๊ทธ์ธ ์ฐฝ์œผ๋กœ ์ด๋™
    fun newKakaoLogin(context: Context){
          val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
              if (error != null) {
                  when {
                  }
              }
              else if (token != null) {
                  //Toast.makeText(context, "๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜์˜€์Šต๋‹ˆ๋‹ค.", Toast.LENGTH_SHORT).show()
                  getKakaoInfo()
              }
          }
    
          if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
              Log.e(LOGINVIEWMODEL, "์นด์นด์˜คํ†ก์œผ๋กœ")
              UserApiClient.instance.loginWithKakaoTalk(context, callback = callback)
          } else {
              Log.e(LOGINVIEWMODEL, "ํ™ˆํŽ˜์ด์ง€๋กœ")
              UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
          }
    
      }    
    1. ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ
       fun getKakaoInfo(){
        // ์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ (๊ธฐ๋ณธ)
        UserApiClient.instance.me { user, error ->
            if (error != null) {
                Log.e("LOGINVIEWMODEL", "์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ ์‹คํŒจ / "+error.toString(), error)
            }
            else if (user != null) {
                Log.i("LOGINVIEWMODEL_RESULT", "์‚ฌ์šฉ์ž ์ •๋ณด ์š”์ฒญ ์„ฑ๊ณต" +
                        "\nํšŒ์›๋ฒˆํ˜ธ: ${user.id}" +
                        "\n๋‹‰๋„ค์ž„: ${user.kakaoAccount?.profile?.nickname}" +
                        "\nํ”„๋กœํ•„์‚ฌ์ง„: ${user.kakaoAccount?.profile?.thumbnailImageUrl}")
                kakaoUser.name = user.kakaoAccount?.profile?.nickname.toString()
                kakaoUser.oauthKey = user.id.toString()
                Log.d("LOGINVIEWMODEL_RESULT","${kakaoUser.oauthKey} + ${kakaoUser.name}")
                //์„œ๋ฒ„์— ์š”์ฒญ
                postLogin()
            }
        }
    }
    
    1. ์„œ๋ฒ„์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ „์†ก ๋ฐ ์ž๋™ ๋กœ๊ทธ์ธ ์œ„ํ•œ sharedpreference ์„ค์ •์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. (splashํ™”๋ฉด์—์„œ ๋กœ๊ทธ์ธ ๋‚ด์—ญ์ด ์กด์žฌํ•˜๋ฉด ๋ฐ”๋กœ homeActivity๋กœ, ์•„๋‹ˆ๋ฉด loginActivity๋กœ intent)
    fun postLogin(){
      Log.d("LOGIN","post${kakaoUser.oauthKey} + ${kakaoUser.name}")
      val requestLoginData = RequestLoginData(oauthKey = kakaoUser.oauthKey, name = kakaoUser.name) //์ „์†กํ•  ๋ฐ์ดํ„ฐ
      val call: Call<ResponseLoginData> = UserClient.getApi.postLogin(requestLoginData)
      call.enqueue(object : Callback<ResponseLoginData> {
          override fun onResponse(
              call: Call<ResponseLoginData>,
              response: Response<ResponseLoginData>
          ){
              //token๊ฐ’ ์ €์žฅ
              SharedPreferenceToken.putSettingItem(getApplication<Application>().applicationContext,"USER_TOKEN",response.body()?.accessToken.toString())
              isNew.value = response.body()?.isNewUser
          }
          override fun onFailure(call: Call<ResponseLoginData>, t: Throwable) {
              Log.d("NetworkTest","error:$t")
          }
      })
    }

2. Onboarding

  • ์‚ฌ์šฉ์ž ๋งž์ถค ํ๋ ˆ์ด์…˜์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ์˜จ๋ณด๋”ฉ ๊ณผ์ •์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ Onboarding

    ๐Ÿงพ OnboardDatabase.kt

    • onboardData๋ผ๋Š” ๊ฐ์ฒด๋ฅผ singletone์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ 4๊ฐœ์˜ fragment์—์„œ ํ•œ ๊ฐ์ฒด๋ฅผ ๊ณต์œ ํ•˜๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
      (๊ฐ ํ™”๋ฉด์—์„œ ์–ป์€ ์ •๋ณด๋“ค์„ ํ•œ ๊ฐ์ฒด์— ๋„ฃ์–ด ์„œ๋ฒ„ ์ „๋‹ฌ)
    class OnboardDatabase {
    //์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด ์ƒ์„ฑ
    companion object{
        lateinit var onboardData:OnboardData
    }
    fun getOnboardData():OnboardData{
        return onboardData
    } 
    //...
    }

    โ—พ ๊ฐ fragment์—์„œ recyclerView๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฒ„ํŠผ์„ ๊ตฌ์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    • RecyclerView SingleChoice.
      : ๊ฐ recyclerView์˜ adapter์— single choice๋ฅผ ์œ„ํ•œ Interface๋ฅผ ์ •์˜ํ•œ ํ›„ fragment์—์„œ ํ•ด๋‹นํ•˜๋Š” setOnClickListener๋ฅผ ๋‹ฌ์•„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    ๐Ÿงพ AgeAdapter.kt

    class AgeAdapter : RecyclerView.Adapter<AgeAdapter.MyViewHolder>() {
    val ageList = mutableListOf<AgeInfo>()
    
    override fun onCreateViewHolder(
      parent: ViewGroup,
      viewType: Int
    ): MyViewHolder {
      val binding = ItemOnboardTextBinding.inflate(
          LayoutInflater.from(parent.context),
          parent,
          false
      )
      return MyViewHolder(binding)
    }
    
    override fun getItemCount(): Int = ageList.size
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
      holder.onBind(ageList[position])
      holder.itemView.setOnClickListener {
          itemClickListener.onClick(it, position)
      }
    }
    // (2) ๋ฆฌ์Šค๋„ˆ ์ธํ„ฐํŽ˜์ด์Šค
    interface OnItemClickListener {
      fun onClick(v: View, position: Int)
    }
    // (3) ์™ธ๋ถ€์—์„œ ํด๋ฆญ ์‹œ ์ด๋ฒคํŠธ ์„ค์ •
    fun setItemClickListener(onItemClickListener: OnItemClickListener) {
      this.itemClickListener = onItemClickListener
    }
    // (4) setItemClickListener๋กœ ์„ค์ •ํ•œ ํ•จ์ˆ˜ ์‹คํ–‰
    private lateinit var itemClickListener : OnItemClickListener
    
    class MyViewHolder(
      private val binding: ItemOnboardTextBinding
    ) : RecyclerView.ViewHolder(binding.root) {
      fun onBind(ageInfo: AgeInfo) {
          binding.tvText.text = ageInfo.age
      }
    }
    }

    ๐Ÿงพ OneOnboardFragment.kt

      private fun singleChoice() {
      binding.rvGender.addOnItemTouchListener(object :
          RecyclerView.OnItemTouchListener {
          override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
              TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
          }
          override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
              if (e.action == MotionEvent.ACTION_MOVE) { }
              else viewModel.genderSingleChoice(rv,e)
              return false
          }
          override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
              TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
          }
      })

3. Home

  • ์‚ฌ์šฉ์ž ๋งž์ถค ํ๋ ˆ์ด์…˜, ์ด๋ฒคํŠธ, ์ƒ์‹ ๋“ฑ์„ ๊ฐ„๋žตํžˆ ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ Home ํ™”๋ฉด

    • ์ €์žฅ๋˜์–ด์žˆ๋Š” ์‚ฌ์šฉ์ž์˜ ํ† ํฐ์„ ์ด์šฉํ•ด viewModel์—์„œ ์„œ๋ฒ„ ํ†ต์‹ , ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ •๋ณด์— ๋Œ€ํ•œ ๋งž์ถค ์ •๋ณด๋ฅผ ๋‹ด์€ ๋ Œ์ฆˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ๊ฐ RecommendationBySeason, RecommendationBySituation, RecommendationByUser, Giudes, DeadlineEvent, LastestEvent, NewLens๋ผ๋Š” ๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋กœ ๋ฐ›์•„, ์ด๋ฅผ RecyclerView๋กœ ๊ตฌ์„ฑํ•ด ๋ณด์—ฌ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

    • ์ด๋•Œ viewModel์—์„œ ํ†ต์‹ ํ•ด ๋ฐ›์€ ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ์šฐ, fragment์—์„œ observe๋ฅผ ํ†ตํ•ด ๊ด€์ฐฐํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€, ๋ฐ์ดํ„ฐ์— ๋ณ€ํ™”๊ฐ€ ์ƒ๊ธธ ๊ฒฝ์šฐ ์ด๋ฅผ ์•Œ๋ ค์ฃผ์–ด ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    • ์œ„์˜ ๋ Œ์ฆˆ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋“ค ์ค‘์—์„œ otherColors๋ผ๋Š” ์ƒ‰๊น” ๋ฐฐ์—ด์„ ๋ฐ›๋Š” ๊ฐ์ฒด์˜ ๊ฒฝ์šฐ, ์ค‘์ฒฉ recyclerView๋กœ ํ‘œํ˜„. _ ์ด๋Š” ์™ธ๋ถ€ RecyclerView_ Adapter์˜ ViewHolder์—์„œ bind ์‹œ ๋‚ด๋ถ€ RecyclerView์˜ Adapter๋ฅผ ์„ค์ •ํ•จ์œผ๋กœ์„œ ๊ตฌํ˜„.

    ๐Ÿงพ CuratingListAdapter.kt

    class CuratingListAdapter:RecyclerView.Adapter<CuratingListAdapter.CuratingViewHolder>() {
    
        private var curateList = emptyList<RecommendationByUser>()
    
        class CuratingViewHolder(
            private val binding : ItemOneCuratingBinding
        ): RecyclerView.ViewHolder(binding.root){
            fun bind(curatingInfo: RecommendationByUser){
                binding.curatingInfo = curatingInfo
    
                //์ž์‹ RecyclerView Adapter ์„ค์ •
                val listForColor = LensColorListAdapter()
                listForColor.setColoring(curatingInfo.otherColorList as List<String>)
                binding.rvOneCuratingColor.adapter = listForColor
                //์‹ ์ œํ’ˆ _ recycler view์˜ ๊ฒฝ์šฐ๋Š” ์—ฌ๊ธฐ์„œ ํด๋ฆญ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
    
            }
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CuratingViewHolder {
            val binding = ItemOneCuratingBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
    
            return CuratingViewHolder(binding)
        }
    
        override fun onBindViewHolder(holder: CuratingViewHolder, position: Int) {
            holder.bind(curateList[position])
            holder.itemView.setOnClickListener {
                itemClickListener.onClick(it, position)
            }
        }
    
        override fun getItemCount(): Int = curateList.size
    
        fun setCurating(curateList : List<RecommendationByUser>){
            this.curateList = curateList
            notifyDataSetChanged()
        }
    
        // (2) ๋ฆฌ์Šค๋„ˆ ์ธํ„ฐํŽ˜์ด์Šค
        interface OnItemClickListener {
            fun onClick(v: View, position: Int)
        }
        // (3) ์™ธ๋ถ€์—์„œ ํด๋ฆญ ์‹œ ์ด๋ฒคํŠธ ์„ค์ •
        fun setItemClickListener(onItemClickListener: OnItemClickListener) {
            this.itemClickListener = onItemClickListener
        }
        // (4) setItemClickListener๋กœ ์„ค์ •ํ•œ ํ•จ์ˆ˜ ์‹คํ–‰
        private lateinit var itemClickListener : OnItemClickListener
    
    }
    
    • OneHomeFragment์—์„œ ๊ฐ ์š”์†Œ ํด๋ฆญ ์‹œ ...

    • RecommendationBySeason, RecommendationBySituation, RecommendationByUser ์˜ ๊ฒฝ์šฐ, RecyclerView์˜ item ํด๋ฆญ ์‹œ ํ•ด๋‹น ๋ Œ์ฆˆ์˜ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™. _ ๋ Œ์ฆˆ์˜ ์ƒํ’ˆ id๋ฅผ ๋„˜๊ฒจ์คŒ.

    • ๊ฐ RecyclerView ์œ„์— ์žˆ๋Š” '๋”๋ณด๊ธฐ>' ํด๋ฆญ ์‹œ ๋ฐœ๊ฒฌ์˜ ๊ด€๋ จ ํƒญ์œผ๋กœ ์ด๋™. _ ๊ณ„์ ˆ ๊ด€๋ จ ์•„์ดํ…œ ์ถ”์ฒœ์˜ ๋”๋ณด๊ธฐ๋ฅผ ํด๋ฆญ ์‹œ ๋ฐœ๊ฒฌ ํƒญ์˜ 4๋ฒˆ์งธ ํƒญ์ธ ๊ณ„์ ˆ ํƒญ์œผ๋กœ ์ด๋™.

    • ์ƒ๋‹จ์˜ ๊ฒ€์ƒ‰๋ฐ” ํด๋ฆญ ์‹œ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€๋กœ ์ด๋™.

    ๐Ÿงพ OneHomeFragment.kt

    class OneHomeFragment : Fragment() {
        private val handler: Handler = Handler(Looper.getMainLooper())
        private var _binding: FragmentHomeOneBinding? = null
        private val binding get() = _binding ?: error("View๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด binding์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
    
        private val oneHomeViewModel: OneHomeViewModel by activityViewModels()
        private lateinit var situLayoutManager : RecyclerView.LayoutManager
        private lateinit var seasonLayoutManager : RecyclerView.LayoutManager
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            _binding = FragmentHomeOneBinding.inflate(inflater, container, false)
            binding.lifecycleOwner = viewLifecycleOwner
    
            initLayout()
            setClickListener()
    
            oneHomeViewModel.getHome()
    
            setCuratingAdapter()
            setCuratingObserve()
    
            setRecommend1Adapter()
            setRecommend1Observe()
    
            setRecommend2Adapter()
            setRecommend2Observe()
    
            setEventAdapter()
            setEventObserve()
            setEventIndicator()
    
            setAdAdapter()
            setAdObserve()
            setAdIndicator()
    
            setTipAdapter()
            setTipObserve()
    
            setNewAdapter()
            setNewObserve()
    
    
            return binding.root
        }
    
         override fun onStart() {
            super.onStart()
            oneHomeViewModel.situation.observe(viewLifecycleOwner) {
                if(oneHomeViewModel.situation.value.equals("์ผ์ƒ")) {
                    binding.tvHomeRecommend.text = oneHomeViewModel.situation.value + "์—์„œ ๋ผ์ง€ ์ข‹์€ ๋ Œ์ฆˆ"
                }
                else {
                    binding.tvHomeRecommend.text  = oneHomeViewModel.situation.value + "ํ• ๋•Œ ๋ผ์ง€ ์ข‹์€ ๋ Œ์ฆˆ"
                }
            }
            oneHomeViewModel.userName.observe(viewLifecycleOwner) {
                binding.tvHomeCurating.text = oneHomeViewModel.userName.value + "๋‹˜ ์ด ๋ Œ์ฆˆ ์–ด๋– ์„ธ์š”?"
            }
    
        }
        
        //RecyclerView ์•„์ดํ…œ ์‚ฌ์ด ๋งˆ์ง„ ์ง€์ • ๊ด€๋ จ ์ฝ”๋“œ ์ƒ๋žต...
        
        private fun setCuratingAdapter(){
            val curatingListAdapter = CuratingListAdapter()
            curatingListAdapter.setItemClickListener(object: CuratingListAdapter.OnItemClickListener{
                override fun onClick(v: View, position: Int) {
                    val rbu :RecommendationByUser = oneHomeViewModel.recommendationByUserList.get(position)
                    val intent = Intent(requireContext(), DetailActivity::class.java)
                    intent.putExtra("itemId", rbu.id)
                    startActivity(intent)
                }
            })
    
            binding.rvHomeCurating.adapter = curatingListAdapter
        }
        private fun setCuratingObserve(){
            oneHomeViewModel.recommendationByUserList.observe(viewLifecycleOwner){
                curatingList -> with(binding.rvHomeCurating.adapter as CuratingListAdapter){
                    setCurating(curatingList)
                }
            }
        }
    
        private fun setEventAdapter(){
            binding.vpHomeEvent.adapter = EventViewPagerAdapter()
        }
    
        private fun setEventObserve(){
            oneHomeViewModel.deadlineEventList.observe(viewLifecycleOwner){ eventList ->
                with(binding.vpHomeEvent.adapter as EventViewPagerAdapter){
                    setEvent(eventList)
                }
            }
        }
        private fun setEventIndicator() {
            TabLayoutMediator(binding.tabHomeEvent, binding.vpHomeEvent) { tab, position -> }.attach()
        }
        
        //์ด ์™ธ 5๊ฐœ์˜ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด์— ๋Œ€ํ•œ RecyclerView์˜ Adapter์™€ Observe ์ฝ”๋“œ ์ƒ๋žต. 
    
        private fun setClickListener(){
    
            binding.tvOneSearch.setOnClickListener {
                val intent = Intent(context, SearchActivity::class.java)
                startActivity(intent)
            }
    
            binding.clHomeCuratingMore.setOnClickListener{
    
                activity?.supportFragmentManager
                    ?.beginTransaction()
                    ?.replace(R.id.nav_host_home, TwoHomeFragment()
                        .apply {
                            arguments = Bundle().apply {
                                putInt("setIdx", 1)
                            }
                        }, "home->foryou")
                    ?.commit()
    
                (activity as HomeActivity).setBottomChecked(1)
            }
    
            binding.clHomeRecommendMore.setOnClickListener{
    
                activity?.supportFragmentManager
                    ?.beginTransaction()
                    ?.replace(R.id.nav_host_home, TwoHomeFragment().apply {
                        arguments = Bundle().apply {
                            putInt("setIdx", 2)
                        }
                    },"home->situ")
                    ?.commit()
    
                (activity as HomeActivity).setBottomChecked(1)
            }
    
    
            binding.clHomeSeasonMore.setOnClickListener{
    
                activity?.supportFragmentManager
                    ?.beginTransaction()
                    ?.replace(R.id.nav_host_home, TwoHomeFragment().apply {
                        arguments = Bundle().apply {
                            putInt("setIdx", 4)
                        }
                    }, "home->saeson")
                    ?.commit();
    
                (activity as HomeActivity).setBottomChecked(1)
    
            }
    
            binding.clHomeNewMore.setOnClickListener {
    
                activity?.supportFragmentManager
                    ?.beginTransaction()
                    ?.replace(R.id.nav_host_home, TwoHomeFragment().apply {
                        arguments = Bundle().apply {
                            putInt("setIdx", 3)
                        }
                    }, "home->saeson")
                    ?.commit();
    
                (activity as HomeActivity).setBottomChecked(1)
    
            }
    
    
        }
    
    }

4. ๋ฐœ๊ฒฌ

  • ์‚ฌ์šฉ์ž ๋งž์ถค ํ๋ ˆ์ด์…˜์„ ํ•œ๋ˆˆ์— ๋ชจ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ ๊ธฐ๋ณธ์ ์ธ ๊ตฌํ˜„ ๋ฐฉ์‹์€ Home์—์„œ RecyclerView๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•œ ๊ฒƒ๊ณผ ํฌ๊ฒŒ ์ฐจ์ด๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ _ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ๊ฒฝ์šฐ, ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ ํƒญ์—์„œ ํ…Œ๋งˆ์— ๋งž๊ฒŒ RecyclerView๋ฅผ ์ด์šฉํ•˜์—ฌ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ƒ‰๊น”์„ ์ค‘์ฒฉ recyclerView๋ฅผ ์ด์šฉํ•˜์˜€๊ณ , ๊ฐ ์•„์ดํ…œ์„ ํด๋ฆญ ์‹œ ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰๋ฐ”๋ฅผ ํด๋ฆญ ์‹œ ๊ฒ€์ƒ‰ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

    • ์ฐจ์ด์  : ๋ฐœ๊ฒฌ fragment ์œ„์— ๋‹ค์‹œ 4๊ฐœ์˜ fragment๋ฅผ tabLayout๊ณผ viewPager2๋ฅผ ์ด์šฉํ•œ ํƒญ์ด ์˜ฌ๋ผ๊ฐ€์ง. ์ด๋ฅผ ํ†ตํ•ด ๋ฐœ๊ฒฌ ํƒญ์—์„œ๋Š” ๋‹ค์‹œ ์ƒ์„ธ 4๊ฐœ์˜ ํƒญ์ด ๋ณด์—ฌ์ง€๋ฉฐ, ์ด๋ฅผ ์Šค์™€์ดํ”„๋ฅผ ํ†ตํ•ด ์ด๋™ํ•  ์ˆ˜ ์žˆ์Œ.

    • ๊ฐ ์ƒ์„ธ ํƒญ์€ For you, ๊ณ„์ ˆ, ์ƒํ™ฉ, ์‹ ์ œํ’ˆ ์ •๋ณด๋ฅผ onBoarding ๊ณผ์ •์—์„œ ์ž…๋ ฅํ•œ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๋˜ํ•œ, ๊ฐ ํƒญ์—๋Š” ํŠน์ • ์•„์ด์ฝ˜ ํด๋ฆญ ์‹œ ํ•ด๋‹น ํƒญ์˜ ์ •๋ณด๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ๋‹ค์ด์–ผ๋กœ๊ทธ์™€, ์ •๋ ฌ ๊ด€๋ จ ๋‹ค์ด์–ผ๋กœ๊ทธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. _ ์ •๋ ฌ ํด๋ฆญ ์‹œ ๊ฐ€๊ฒฉ์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ๋จ. (viewmodel์—์„œ recyclerview์— ์ ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ์„œ๋ฒ„ํ†ต์‹  ๋ฐ›๊ณ , ์ด๋ฅผ observe๊ฐ€ ๊ด€์ฐฐํ•˜๋‹ค ์ ์šฉ)

    • ๋ฐœ๊ฒฌ ํƒญ์˜ ๋กœ๊ทธ ํด๋ฆญ ์‹œ ํ™ˆ์œผ๋กœ ์ด๋™.

      โœ” ๊ตฌํ˜„ ์ฝ”๋“œ

      โ—พ

      ๐Ÿงพ TwoHomeFragment.kt

      class TwoHomeFragment : Fragment() {
          private var _binding: FragmentHomeTwoBinding? = null
          private val binding get() = _binding ?: error("View๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด binding์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
      
          private val homeViewModel: TwoHomeViewModel by viewModels() //์œ„์ž„์ดˆ๊ธฐํ™”
          private lateinit var mContext: Context
      
          private  var idx : Int? = null
          override fun onCreateView(
              inflater: LayoutInflater,
              container: ViewGroup?,
              savedInstanceState: Bundle?
          ): View? {
              _binding = FragmentHomeTwoBinding.inflate(inflater, container, false)
              binding.lifecycleOwner = viewLifecycleOwner
              mContext = requireContext()
              setClickListener()
      
      

โ€‹

          homeViewModel.getSuggestData()
  
          return binding.root
      }

โ€‹
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) }

      //ViewPager2์™€ tabLayout Init
      override fun onActivityCreated(savedInstanceState: Bundle?) {
          super.onActivityCreated(savedInstanceState)
  
          val pagerAdapter = PagerFragmentStateAdapter(requireActivity())
          pagerAdapter.addFragment(TwoHomeForYouFragment())
          pagerAdapter.addFragment(TwoHomeSituFragment())
          pagerAdapter.addFragment(TwoHomeNewFragment())
          pagerAdapter.addFragment(TwoHomeSeasonFragment())
  
          binding.vpHomeTwo.adapter = pagerAdapter
  
          binding.vpHomeTwo.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
              override fun onPageSelected(position: Int) {
                  super.onPageSelected(position)
              }
          })
  
          TabLayoutMediator(binding.findTabLayout, binding.vpHomeTwo) { tab, position ->
                  when (position) {
                       0 -> {
                        homeViewModel.tabItem2.observe(viewLifecycleOwner) {
                            tab.text = homeViewModel.tabItem1
                        }
                    }
                    1 -> {
                        homeViewModel.tabItem2.observe(viewLifecycleOwner){
                            tab.text = homeViewModel.tabItem2.value
                        }
                    }
                    2 -> {
                        homeViewModel.tabItem2.observe(viewLifecycleOwner) {
                            tab.text = homeViewModel.tabItem3
                        }
                    }
                    3 -> {
                        homeViewModel.tabItem4.observe(viewLifecycleOwner){
                            tab.text = homeViewModel.tabItem4.value
                        }
                    }
          }.attach()

โ€‹
idx = arguments?.getInt("setIdx") if(idx != null) { val tabLayout = binding.findTabLayout val tab = tabLayout.getTabAt(idx!! - 1) tab!!.select()

               binding.vpHomeTwo.setCurrentItem(idx!! - 1, false)
          }
      }
  
      private fun setClickListener() {
  
          binding.tvTwoSearch.setOnClickListener {
              val intent = Intent(context, SearchActivity::class.java)
              startActivity(intent)
          }
          
          binding.ivTwoLogo.setOnClickListener{
              activity?.supportFragmentManager
                  ?.beginTransaction()
                  ?.replace(
                      R.id.nav_host_home, OneHomeFragment(), "home->foryou")
                  ?.commit()
  
              (activity as HomeActivity).setBottomChecked(0)
          }
      }
  
  }
  ```
โ€‹    ๐Ÿงพ PagerFragmentAdapter.kt _ viewPagerํ•  fragment๋ฅผ ์ง€์ •.

```kotlin
class PagerFragmentStateAdapter(fragmentActivity: FragmentActivity): FragmentStateAdapter(fragmentActivity) {

    var fragments : ArrayList<Fragment> = ArrayList()

    override fun getItemCount(): Int {
        return fragments.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragments[position]
    }

    fun addFragment(fragment: Fragment) {
        fragments.add(fragment)
        notifyItemInserted(fragments.size-1)
    }

    fun removeFragment() {
        fragments.removeLast()
        notifyItemRemoved(fragments.size)
    }

}
```

๐Ÿงพ TwoHomeForYouFragment.kt

```kotlin
class TwoHomeForYouFragment : Fragment() {

    companion object {
        fun newInstance() = TwoHomeForYouFragment()
    }

    private var _binding: FragmentHomeTwoForyouBinding? = null
    private val binding get() = _binding ?: error("View๋ฅผ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•ด binding์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")

    private val viewModel: TwoHomeViewModel by activityViewModels()
    private val fragmentViewModel: TwoHomeForYouViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentHomeTwoForyouBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner

        //๋ฐ์ดํ„ฐ setting
        setForYouAdapter()
        setForYouObserve()

โ€‹
//์ •๋ ฌ ํด๋ฆญ ์‹œ binding.ivForYouSort.setOnClickListener{ val findSortPriceFragment = FindSortPriceFragment()

            findSortPriceFragment.setButtonClickListener(object: FindSortPriceFragment.OnButtonClickListener {
                override fun onLowPriceClicked() {
                    //์—ฌ๊ธฐ์„œ ์ •๋ ฌ
                    Log.d("click", "low price")
                    viewModel.getForyou(1,"price","asc")
                }

                override fun onHighPriceClicked() {
                    // ์—ฌ๊ธฐ์„œ ์ •๋ ฌ
                    Log.d("click", "high price")
                     viewModel.getForyou(1,"price","desc")
                }
            })
            findSortPriceFragment.show(childFragmentManager, "CustomDialog")
        }

        binding.ivFindQuestion1.setOnClickListener{
            val findQuestionFragment = FindQuestionFragment(1)
            findQuestionFragment.show(childFragmentManager, "CustomDialog2")

        }

        return binding.root
    }
    //... ์•„๋ž˜๋Š” ํ™ˆ์˜ OneHomeFragment.kt์™€ ์œ ์‚ฌ.
}
```

<br>

</div>
</details>    

5. ์ƒํ’ˆ ์ƒ์„ธ

  • ์ƒํ’ˆ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ ViewPager2

    - ์ด๋ฏธ์ง€ ์Šค์™€์ดํ”„ ์ „ํ™˜์„ ์œ„ํ•ด ViewPager2๋ฅผ ์‚ฌ์šฉ
    

    โ—พ DotsIndicator

    - TabLayout์˜ Indicator custom
    

    โœ” ๊ตฌํ˜„ ์ฝ”๋“œ

    โ—พ ViewPager2 - ์ด๋ฏธ์ง€ ์Šค์™€์ดํ”„ ์ „ํ™˜์„ ์œ„ํ•ด ViewPager2๋ฅผ ์‚ฌ์šฉ

    ๐Ÿงพ UserClient.kt

    data class KakaoUser(
          var oauthKey: String,
          var name: String
      )
    

    โ—พ DotsIndicator - TabLayout์˜ Indicator custom

    ๐Ÿงพ UserClient.kt

    data class KakaoUser(
          var oauthKey: String,
          var name: String
      )
    

6. ๊ฒ€์ƒ‰ ์ƒ์„ธ

  • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ Kakaotalk Login

    โœ” ๊ตฌํ˜„ ์ฝ”๋“œ

    โ—พ Login

    ๐Ÿงพ UserClient.kt

    data class KakaoUser(
          var oauthKey: String,
          var name: String
      )
    

7. ๊ฒ€์ƒ‰ (ํ‚ค์›Œ๋“œ/์ตœ๊ทผ, ํ•„ํ„ฐ)

  • ํ‚ค์›Œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ธŒ๋žœ๋“œ, ์ปฌ๋Ÿฌ, ์ง๊ฒฝ, ์ฃผ๊ธฐ๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•„ํ„ฐ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    โœจShow Detailsโœจ

    โœ” ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

    โ—พ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์˜ ๊ฒฝ์šฐ ๋ถ€๋ชจ activity์—์„œ ์ž…๋ ฅ๋ฐ›์€ ํ‚ค์›Œ๋“œ๋ฅผ ์ž์‹ fragment์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    ๋”ฐ๋ผ์„œ fragment๋“ค์—์„œ activity์˜ viewModel์„ ๊ณต์œ ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ทฐ๋ชจ๋ธ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    ๐Ÿงพ OneSearchFragment.kt

    private val viewModel: SearchViewModel by activityViewModels()

    โ—พ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ์ถ”๊ฐ€๋ฅผ ์œ„ํ•ด sharedPreference๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    (ํ˜„ ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ mutableList๋ฅผ sharedPreference์— ๋„ฃ๋Š” ์˜ค๋ฅ˜๋ฅผ ๋ฒ”ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๊ณ ์ณ์ ธ์•ผ ํ•  ์ฝ”๋“œ ํŒจํ„ด์ž…๋‹ˆ๋‹ค.)
    ๐Ÿงพ SharedPreferences.kt

        object SharedPreferences {
    
      fun setStringArrayPref(context: Context, key: String, values: MutableList<RecentInfo>) {
          val prefs = context.getSharedPreferences("setting",Context.MODE_PRIVATE)
          val editor = prefs.edit()
          val a = JSONArray()
          for (i in 0 until values.size) {
              a.put(values[i].name)
          }
          if (values.isNotEmpty()) {
              editor.putString(key, a.toString())
          } else {
              editor.putString(key, null)
          }
          editor.apply()
      }
    
      fun getStringArrayPref(context: Context, key: String): ArrayList<String>? {
          val prefs = context.getSharedPreferences("setting",Context.MODE_PRIVATE)
          val json = prefs.getString(key, null)
          val urls = ArrayList<String>()
          if (json != null) {
              try {
                  val a = JSONArray(json)
                  for (i in 0 until a.length()) {
                      val url = a.optString(i)
                      urls.add(url)
                  }
              } catch (e: JSONException) {
                  e.printStackTrace()
              }
          }
          return urls
      }
    
    
      }

    ๐Ÿงพ SearchViewModel.kt

    fun updateRecent(context:Context, recentSearch: MutableList<RecentInfo>, recentAdapter: RecentAdapter) {
      //sharedPreference
      SharedPreferences.setStringArrayPref(context,"RECENT_KEY",recentSearch)
    
      recentAdapter.recentList.clear()
      recentAdapter.recentList.addAll(recentSearch)
      recentAdapter.notifyDataSetChanged()
    }

    โ—พ ํ•„ํ„ฐ ๊ฒ€์ƒ‰์˜ ๊ฒฝ์šฐ๋„ ์˜จ๋ณด๋”ฉ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ •๋ณด๋ฅผ ํ•œ ๊ฐ์ฒด์— ๋ชจ์œผ๊ธฐ ์œ„ํ•ด SearchDatabase๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๊ด€๋ฆฌ ํ›„ ์„œ๋ฒ„๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ๐Ÿงพ SearchDatabase.kt

       class SearchDatabase {
      //์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด ์ƒ์„ฑ
      companion object{
          lateinit var searchData:SearchData
      }
    


๐Ÿ‘‹ Specification

Architecture MVVM
Jetpack Components DataBinding, LiveData, ViewModel, Lifecycle
Login Kakaotalk Login
Network OkHttp, Retrofit2
Fragment Management Navigation
Strategy Git Flow
Other Tool Notion, Slack
Continuous Integration Slack - Git auto notification



๐Ÿ“ฆ Package Structure

๐Ÿ“ฆomoolen
    โ””โ”€om๐Ÿ‘€roid
        โ”œโ”€๐Ÿ“‚detail
        โ”‚  โ”œโ”€๐Ÿ“‚detailApi
        โ”‚  โ”œโ”€๐Ÿ“‚popular
        โ”‚  โ””โ”€๐Ÿ“‚recommend
        โ”œโ”€๐Ÿ“‚home
        โ”‚  โ”œโ”€๐Ÿ“‚fragments
        โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚five
        โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚four
        โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚one
        โ”‚  โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚curating
        โ”‚  โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚event
        โ”‚  โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚networkApi
        โ”‚  โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚newItem
        โ”‚  โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚recommend
        โ”‚  โ”‚  โ”‚  โ””โ”€๐Ÿ“‚tip
        โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚three
        โ”‚  โ”‚  โ””โ”€๐Ÿ“‚two
        โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚api
        โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚foryou
        โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚newItem
        โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚season
        โ”‚  โ”‚      โ””โ”€๐Ÿ“‚situation
        โ”‚  โ””โ”€๐Ÿ“‚homeApi
        โ”œโ”€๐Ÿ“‚login_signup
        โ”‚  โ””โ”€๐Ÿ“‚login
        โ”‚      โ””โ”€๐Ÿ“‚loginApi
        โ”œโ”€๐Ÿ“‚onboarding
        โ”‚  โ”œโ”€๐Ÿ“‚api
        โ”‚  โ””โ”€๐Ÿ“‚fragments
        โ”‚      โ”œโ”€๐Ÿ“‚four
        โ”‚      โ”‚  โ”œโ”€๐Ÿ“‚brand
        โ”‚      โ”‚  โ””โ”€๐Ÿ“‚when
        โ”‚      โ”œโ”€๐Ÿ“‚one
        โ”‚      โ”‚  โ””โ”€๐Ÿ“‚recycle
        โ”‚      โ”‚      โ”œโ”€๐Ÿ“‚age
        โ”‚      โ”‚      โ””โ”€๐Ÿ“‚gender
        โ”‚      โ”œโ”€๐Ÿ“‚three
        โ”‚      โ”‚  โ””โ”€๐Ÿ“‚recycle
        โ”‚      โ”‚      โ”œโ”€๐Ÿ“‚effect
        โ”‚      โ”‚      โ””โ”€๐Ÿ“‚period
        โ”‚      โ””โ”€๐Ÿ“‚two
        โ”‚          โ””โ”€๐Ÿ“‚recycle
        โ”‚              โ”œโ”€๐Ÿ“‚color
        โ”‚              โ””โ”€๐Ÿ“‚what
        โ”œโ”€๐Ÿ“‚search
        โ”‚  โ”œโ”€๐Ÿ“‚data
        โ”‚  โ”œโ”€๐Ÿ“‚fragment
        โ”‚  โ”‚  โ”œโ”€๐Ÿ“‚one
        โ”‚  โ”‚  โ”‚  โ””โ”€๐Ÿ“‚recycle
        โ”‚  โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚popular
        โ”‚  โ”‚  โ”‚      โ””โ”€๐Ÿ“‚recent
        โ”‚  โ”‚  โ””โ”€๐Ÿ“‚two
        โ”‚  โ”‚      โ”œโ”€๐Ÿ“‚filterSearchApi
        โ”‚  โ”‚      โ””โ”€๐Ÿ“‚recycle
        โ”‚  โ”‚          โ”œโ”€๐Ÿ“‚brand
        โ”‚  โ”‚          โ”œโ”€๐Ÿ“‚color
        โ”‚  โ”‚          โ”œโ”€๐Ÿ“‚diameter
        โ”‚  โ”‚          โ””โ”€๐Ÿ“‚period
        โ”‚  โ””โ”€๐Ÿ“‚search_result
        โ”œโ”€๐Ÿ“‚splash
        โ””โ”€๐Ÿ“‚util
            โ”œโ”€๐Ÿ“‚api
            โ””โ”€๐Ÿ“‚firebase



๐Ÿ™†๐Ÿปโ€โ™€๏ธ Who we are?! ๐Ÿ™†๐Ÿปโ€โ™€๏ธ


์œ ์ง€์› ์ด์œ ์ • ์ฐจ์ง€์ˆ˜
์Šคํ”Œ๋ž˜์‹œ/์นด์นด์˜คํ†ก ๋กœ๊ทธ์ธ, ์˜จ๋ณด๋”ฉ, ๊ฒ€์ƒ‰(ํ‚ค์›Œ๋“œ,ํ•„ํ„ฐ) ํ™ˆ, ๋ฐœ๊ฒฌ ์ƒํ’ˆ ์ƒ์„ธ, ๊ฒ€์ƒ‰ ์ƒ์„ธ

About

Om๐Ÿ‘€Roid - ์˜ค๋Š˜์€ ๋ฌด์Šจ Android?!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages