2주차 배달앱 강의 스터디 수업 내용

1주차 복습 내용

http://javaexpert.tistory.com/971?category=720102

2주차 클론 수업 내용

  1. Category 데이터 클래스 구현
  2. List<Category> 구현
  3. 리사이클러뷰를 그리드 레이아웃 매니저로 구현 및 데이터 연결
  4. GPS 기능 켜져있는지 체크
  5. 유저 위치 가져오기 위해 퍼미션 작업
  6. Presenter 함수 구현
  7. Address Object Data 클래스 구현
  8. 퍼미션 승인 후 트래킹 진행
  9. 좌표를 가져온 후 네이버 지오코딩 api 를 통해 자세한 동을 가져온다.
  10. Retrofit Api 구현
  11. converter-moshi 를 통해 JSON -> Address Class 로 변환
  12. RxJava Converter 구현
  13. RxJava Subscribe 를 통해 화면에 푸시
  14. 최종적으로 화면에 동이름을 뿌리도록 진행

Category 데이터 클래스 구현

  • resId 는 아이템에 들어갈 이미지 drawable 위치
  • background 는 아이템 뒤에 들어갈 백그라운드 이미지 ( drawable xml 이미지)
  • type 은 서브 페이지로 넘길때 필요한 파라미터
data class Category (
        val no: Int,
        val resId: Int,
        val background: Int = R.drawable.bdt_btn_white,
        val title: String = "",
        val type: String = ""
)

리사이클러뷰를 그리드 레이아웃 매니저로 구현

  • content_main.xml 에서 RecyclerView 를 구현
   <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/mainRecyclerView"
        android:clipToPadding="false"
        android:padding="@dimen/item_offset"
        />

List<Category> 구현해서 리스트에 데이터로 연결

  • 메인화면에 표시될 배열 미리 정의
    val items = arrayOf(
            Category(0, R.drawable.bdt_home_a_ctgr_all, title = "전체"),
            Category(1, R.drawable.bdt_home_a_ctgr_seasonal, R.drawable.bdt_btn_brown, title = "계절메뉴"),
            Category(2, R.drawable.bdt_home_a_ctgr_chicken, title = "치킨"),
            Category(3, R.drawable.bdt_home_a_ctgr_chinese, title = "중식"),
            Category(4, R.drawable.bdt_home_a_ctgr_pizza, title = "피자"),
            Category(5, R.drawable.bdt_home_a_ctgr_bossam, title = "족발,보쌈"),
            Category(6, R.drawable.bdt_home_a_ctgr_burger, title = "도시락"),
            Category(7, R.drawable.bdt_home_a_ctgr_japanese, title = "일식")
    )

리사이클러뷰를 그리드 레이아웃 매니저로 구현 및 데이터 연결

    ...
    private fun initRecyclerView() {
        mainRecyclerView.layoutManager = GridLayoutManager(getContext(), 2)
        mainRecyclerView.adapter = MainRecylerViewAdapter(items, getContext(), ::handleItem)
        val itemDecoration = ItemOffsetDecoration(getContext(), R.dimen.item_offset)
        mainRecyclerView.addItemDecoration(itemDecoration)
    }

GPS 기능 켜져있는지 체크

  • 기능이 꺼져있는 경우 강제로 GPS 설정화면으로 이동시킴
fun checkGPS() {
        val service = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val enabled = service
                .isProviderEnabled(LocationManager.GPS_PROVIDER)
        
        if (!enabled) {
            val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
            startActivity(intent)
        }
    }

유저 위치 가져오기 위한 퍼미션 체크

    private fun checkPermission() {

        if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            updateMyLocation()
        } else {
            ActivityCompat.requestPermissions(this,arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 1);
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (permissions.size == 1 &&
                permissions[0] == android.Manifest.permission.ACCESS_FINE_LOCATION &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                return
            }
        }else {
            updateMyLocation()
        }
    }

위치 가져온 후 내 위치 트래킹

  • 정상적으로 트래킹 후 Presenter로 한국 주소를 얻기 위해 좌표를 보냄
fun updateMyLocation() {
        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val criteria = Criteria()
        // 정확도
        criteria.setAccuracy(Criteria.NO_REQUIREMENT);
        // 전원 소비량
        criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
        // 고도, 높이 값을 얻어 올지를 결정
        criteria.setAltitudeRequired(false);
        // provider 기본 정보(방위, 방향)
        criteria.setBearingRequired(false);
        // 속도
        criteria.setSpeedRequired(false);
        // 위치 정보를 얻어 오는데 들어가는 금전적 비용
        criteria.setCostAllowed(true);

        provider = locationManager.getBestProvider(criteria, false);
        val location = locationManager.getLastKnownLocation(provider);
        if(location != null) {
            Log.d("KTH","${location.latitude},${location.longitude}")
            mPresenter.getAddr(lng = location.longitude, lat = location.latitude)
        }
    }

Presenter 구현

  • 좌표를 repository 에 다시 보내서 Observable<Address> 형태로 리턴 받음
  • compositeDisposable 는 메모리릭을 위해 구독된 disposable 을 detach 때 제거를 해준다.
override fun getAddr(lng: Double, lat: Double) {        
        val disposable = repository.convertAddr(lng, lat)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onNext = {                            
                            mView?.updateAddress(it.items[0].addressDetail.dongmyun)
                        },
                        onError = {
                            it.printStackTrace()
                        }
                )

        compositeDisposable.add(disposable)
    }

Adress 데이터 클래스 구현

  • 서버로 부터 결과값을 가져온 후 Address 데이터로 변환하기 위해 필요하다.
import com.squareup.moshi.Json

data class ResponseAddress(
        @field:Json(name = "result") val result : Address
)

data class Address (
        @field:Json(name = "total") val total : Int,
        @field:Json(name = "items") val items : List<AddressItem>
)

data class AddressItem (
        @field:Json(name = "address") val address : String,
        @field:Json(name = "addrdetail") val addressDetail : AddressDetail
)

data class AddressDetail (
        @field:Json(name = "country") val country : String,
        @field:Json(name = "sido") val sido : String,
        @field:Json(name = "sigugun") val sigugun : String,
        @field:Json(name = "dongmyun") val dongmyun : String,
        @field:Json(name = "rest") val rest : String
)

Source 클래스 구현

  • Repository 에서 구현될 모델 클래스들의 공통적인 함수 모음
interface Source {
    
    fun getConvertedAddr(lat : Double, lng : Double ) : Observable<Address>

}
  • FirebaseRepository by Source
class FirebaseRepository : Source{
    val firestoreApp by lazy {
        FirebaseFirestore.getInstance()
    }
    
    override fun getConvertedAddr(lat: Double, lng: Double) : Observable<Address> {
        TODO("not implemented") //To change body of created functions use File | Settings | File
    }
}
  • NetworkRepository by Source
object NetworkRepsitory : Source {
    
    override fun getConvertedAddr(lng: Double, lat: Double): Observable<Address> {
        //129.075090,35.179632
        return ApiManager.getAddressFromLatLng(lng, lat)
    }
}
  • RepositoryImpl 를 통해서 Presenter와 통신한다.
    • NetworkRepository 에서 주소를 변환하도록 지시
class RepositoryImpl {

    fun convertAddr(lng: Double, lat: Double): Observable<Address> = NetworkRepsitory.getConvertedAddr(lng, lat)
}

Retrofit API Manager

  • Retrofit Builder 구현
object BaseApiManager {
    fun initRetrofit(Server : String): Retrofit {
        val interceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        val client = OkHttpClient.Builder().apply {
            networkInterceptors().add(Interceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                        .method(original.method(), original.body())
                        .build()
                chain.proceed(request)
            })
            addInterceptor(interceptor)
        }

        return Retrofit.Builder().baseUrl(Server)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(BaseApiManager.createMoshiConverter())
                .client(client.build())
                .build()
    }

    private fun createMoshiConverter(): MoshiConverterFactory = MoshiConverterFactory.create()
  • 중요한 부분

    • RxJava2CallAdapterFactory 를 통해서 컨버팅 된 결과물을 Observable 를 스트림으로 바꿔준다.
    • BaseApiManager.createMoshiConverter() 을 통해 GSON 컨버팅을 진행한다.
            return Retrofit.Builder().baseUrl(Server)
                  .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                  .addConverterFactory(BaseApiManager.createMoshiConverter())
                  .client(client.build())
                  .build()

네이버 지코코딩을 통해 좌표 -> 주소로 변환

  • subscribeOn 은 RxJava 구독 시작시 쓰레드를 결정시킨다.
  • Network 를 사용시 io 쓰레드 이용해서 통신
  • 결과물은 Observable<Address> 으로 리턴된다.
object ApiManager {
    private const val BASE_SERVER: String = "https://openapi.naver.com/v1/map/"
    var retrofit: Retrofit

    init {        
        retrofit = BaseApiManager.initRetrofit(BASE_SERVER)
    }

    fun getAddressFromLatLng(lng: Double, lat: Double) : Observable<Address> {
        val addressService = retrofit.create(AddressService::class.java)
        
        return addressService.getAddress(query = "$lat,$lng")
                .map{ it.result } //ResponseAddress -> Address
                .subscribeOn(Schedulers.io())
    }
}

API 인터페이스를 통해서 통신 처리

  • ResponseAddress -> Address -> AddressItem -> AddressDetail 으로 오브젝트들을 변환시킨다.
interface AddressService {

    /*
    위도 : latitude
    경도 : longtitude
    "https://openapi.naver.com/v1/map/reversegeocode?encoding=utf-8&coordType=latlng&query=";
     */
    @Headers(
            "X-Naver-Client-Id:네이버API",
            "X-Naver-Client-Secret:네이버API"
    )
    @GET("reversegeocode?encoding=utf-8&coordType=latlng")
    fun getAddress(@Query("query") query : String) : Observable<ResponseAddress>
}

Presenter로 통해서 결과값을 UI 로 처리

  • observeOn(AndroidSchedulers.mainThread()) 을 통해서 해당 밑으로는 Android UI Thread 로 처리
  • mView 를 통해서 updateAddress 해줌으로써 데이터를 가져와서 UI 로 올려보내는 작업은 끝났다.
override fun getAddr(lng: Double, lat: Double) {        
        val disposable = repository.convertAddr(lng, lat)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onNext = {                            
                            mView?.updateAddress(it.items[0].addressDetail.dongmyun)
                        },
                        onError = {
                            it.printStackTrace()
                        }
                )

        compositeDisposable.add(disposable)
    }

RecyclerViewAdapter 이벤트 리스너 구현

  • Adapter에 리스너 붙이는 작업
  • Kotlin 확장함수를 통해서 ViewHolder 클래스를 이용해서 listen 함수 구현
fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
    itemView.setOnClickListener {
        event.invoke(adapterPosition, itemViewType)
    }
    return this
}
  • Adapter 부분
    • clickListener는 MainActivity에서 콜백 구현
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecylerViewAdapter.ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.main_recyclerview_item, parent, false)
        return ViewHolder(view).listen{ pos, type ->
            clickListener(items[pos])
        }
    }
  • MainActivity
    • MainActivity 에서 아이템 클릭시 리스트 화면으로 넘어가기 위해 필요
...
mainRecyclerView.adapter = MainRecylerViewAdapter(items, getContext(), ::handleItem)
...


fun handleItem(item: Category) {
    startActivity(
        Intent(this, ListActivity::class.java).apply {
            this.putExtra("item", item)
        }
    )
}

이상으로 2주차 부산 배달앱 클론앱 스터디 내용이었습니다.

다음주에는 3주차로 리스트 화면 상세 구현 및 등록 등을 진행할 예정입니다.

+ Recent posts