728x90

https://m3.material.io/

 

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

m3.material.io

 

Material-UI (mui)는 웹 애플리케이션을 빌드하기 위한 리액트 기반의 UI 프레임워크이다. Google이 2014년 안드로이드 스마트폰에 적용하면서 알려졌으며, Material Design을 기반으로하였기에 시각적으로 훌륭하고 사용하기 쉽다. 때문에 개발자들도 스타일리쉬한 쉬운 사용자 인터페이스를 개발을 손쉽게 할 수 있다. 이러한 Material-UI는 컴포넌트 기반 아키텍처를 사용하여 재사용 가능하고 일관된 디자인 요소를 구축하도록 한다.

특징

  • CSS-in-JS
    다른 UI 라이브러리들과 마찬가지로, js에서 css를 관리하는 방식으로 스타일링한다. 이는 컴포넌트별로 스타일을 정의하고 적용하는데 유용하다.
  • 컴포넌트 기반
    mui는 다양한 UI 요소를 컴포넌트 형태로 제공하며, 이러한 컴포넌트를 조합하여 원하는 인터페이스를 구성할 수 있다.
  • 반응형 디자인
    mui 컴포넌트는 다양한 화면 크기와 장치에 맞게 반응한다. 모바일 기기부터 데스크톱까지 일관된 사용자 경험을 제공해준다.
  • 테마 커스터마이징
    테마를 사용하여 앱의 전반적인 스타일을 일관되게 제어하거나 수정할 수 있다. 색상, 글꼴 등을 커스터마이징하여 앱을 더 맞춤화할 수 있다.
  • 문서화와 예제
    mui는 잘 구성된 예제 문서와 ts전용 코드 또한 제공하기 때문에 ts react 개발자가 손쉽게 접근할 수 있다.
  •  

https://m3.material.io/components/cards/overview

https://m3.material.io/

에서 원하는 components 를 선택하고, Android Resources 를 찾아보면 된다. 

 

728x90

MVC vs MVVM

MVC

각 요소들의 역할

  • Model : 데이터를 저장 및 가공
  • View : Model이 갖고 있는 데이터를 시각화하여 표현
  • Controller : Model과 View 모두에게 관여하며, Model 통해 데이터를 제어하고, 데이터가 변경될 때마다 View를 업데이트

흐름

  1. 사용자의 액션은 Activity/Fragment가 받음
  2. Activity/Fragment는 이를 확인하고 필요한 경우 Model에 업데이트
  3. Model로 부터 전달받은 데이터를 통해 View를 업데이트

View를 나타내는 Activity/Fragment Controller의 역할까지 맡고 있음

장점

  • 굉장히 단순하고 직관적

단점

  • Controller 규모가 커진다
  • 가독성
  • 컨트롤러 하나가 여러 model?(데이터 소스)로 부터 의존성을 갖게 됨
  • 유지 보수가 어려워짐
  • UI 노출에 걸리는 시간 오래걸린다

MVVM

각 요소들의 역할

  • Model : 데이터를 관리
  • View : 화면에 데이터를 표시
  • ViewModel : Model 데이터를 요청하고 UI(View)를 위한 데이터를 가공하는 역할

흐름

  1. 사용자의 액션은 View가 받음
  2. 들어온 액션은 ViewModel로 전달되며, ViewModel은 Model 에게 데이터를 요청
  3. Model은 ViewModel에게 요청받은 데이터를 전달하고
  4. ViewModel은 응답받은 데이터를 가공하고 저장
  5. View는 ViewModel를 관찰 하고 변경된 데이터를 화면에 나타냄

(Model은 Network(Retrofit), Local DB(Room) 통신을 말함)

728x90

아키텍쳐 패턴의 종류

  • MVC, MVP, MVVM, MVI, …가 있음

아키텍쳐 가이드

 

UI Layer

UI Layer

  • UI의 역할은 화면에 애플리케이션 데이터를 표시하고 사용자와 상호작용
    • 상호작용 : 버튼 클릭, 네트워크 처리, 데이터의 변경, …
  • UI는 데이터 레이어에서 가져온 애플리케이션 상태를 시각적으로 나타냄

UI State

  • LiveData
  • StateFlow, SharedFlow

ViewModel

  • ViewModel는 비즈니스 로직을 캡슐화하고 UI 상태를 나타냄
  • ViewModel에서 Data Layer에 있는 데이터를 가져옴
  • ViewModel UI에 사용될 상태를 처리하고 UI Layer에 이벤트를 알리거나 상태를 업데이트함
  • UI Layer는 ViewModel에 이벤트를 전송함

ViewModelLifeCycle

  • ViewModel은 View의 라이프사이클이 종료(destory)될때 사라짐(cleared)

  • 앱의 데이터 읽기, 생성, 저장, 변경 방식을 결정하는 규칙으로 구성함

Domain Layer (optional)

  • 하나의 기능만을 위한 클래스
  • optional이기 때문에 해도되고, 안해도됨
  • ex) GetTodoDetailUseCase(), AddTodoContentUseCase()

 

요약

View

  • 보통 Activity, Fragment가 View 역할을 담당
  • 사용자의 Action을 받음 (텍스트 입력, 버튼 터치 등)
  • ViewModel의 데이터를 관찰해(Observe) UI를 갱신
  • 사용자의 Action을 감지하고 데이터 변화를 통해 UI 갱신 처리

ViewModel

  • 사용자의 Action, LifeCycle에 의해 View에서 요청한 데이터, 비지니스 로직을 처리함
  • Model에 요청한 데이터를 받음

Model (Data Layer)

  • ViewModel에서 요청한 데이터를 처리, 반환
  • Local DB(SQLite, Room), Network 통신(Retrofit)을 이용함

참고

https://developer.android.com/topic/architecture?hl=ko

 

앱 아키텍처 가이드  |  Android 개발자  |  Android Developers

앱 아키텍처 가이드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에는 고품질의 강력한 앱을 빌드하기 위한 권장사항 및 권장 아키텍처가 포함

developer.android.com

https://thdev.tech/android/2022/12/12/Android-Follow-MVVM-Intro/

 

안드로이드 MVVM 패턴 따라 하기 - 시작하기 전에 |

I’m an Android Developer.

thdev.tech

https://velog.io/@dddooo9/Android-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%EB%B0%A9%EB%B2%95

 

[Android] MVVM 패턴과 적용방식

[Android] MVVM 패턴과 적용방식

velog.io

 

728x90

카카오톡 검색 이미지

Api 연결 

 

dependencies

    implementation ("com.google.code.gson:gson:2.10.1")
    implementation ("com.squareup.retrofit2:retrofit:2.9.0")
    implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation ("com.squareup.okhttp3:okhttp:4.10.0")
    implementation ("com.squareup.okhttp3:logging-interceptor:4.10.0")

 

ImageSearchApi 

import com.example.imagesearch.data.ImageSearch
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query

private const val AUTH_HEADER = //API//
private const val SORT_DEFAULT = "accuracy"
private const val PAGE_NUMBER = 1
private const val API_MAX_RESULT = 80
interface ImageSearchApi {
    @GET("/v2/search/image")
    suspend fun getImage(
        @Header("Authorization") authorization: String = AUTH_HEADER,
        @Query("query") query: String,
        @Query("sort") sort: String = SORT_DEFAULT,
        @Query("page") page: Int = PAGE_NUMBER,
        @Query("size") size: Int = API_MAX_RESULT
    ): ImageSearch
}

 

RetrofitInstance

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    private const val BASE_URL = "https://dapi.kakao.com"
    private val retrofit by lazy {
        Retrofit
            .Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    val imageSearchApi: ImageSearchApi by lazy {
        retrofit.create(ImageSearchApi::class.java)
    }
}

DataDTO 

data class ImageSearch(
    val documents: MutableList<Document>,
    val meta: Meta
)

data class Document(
    val collection: String,
    @SerializedName("datetime")
    val datetime: String,
    @SerializedName("display_sitename")
    val displaySiteName: String,
    @SerializedName("doc_url")
    val docUrl: String,
    val height: Int,
    @SerializedName("image_url")
    val imageUrl: String,
    @SerializedName("thumbnail_url")
    val thumbnailUrl: String,
    val width: Int
)

data class Meta(
    @SerializedName("is_end")
    val isEnd: Boolean,
    @SerializedName("pageable_count")
    val pageableCount: Int,
    @SerializedName("total_count")
    val totalCount: Int
)

 

 

SearchImageFragment에서 사용 

package com.example.imagesearch.fragment

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import com.example.imagesearch.adapter.ImageAdapter
import com.example.imagesearch.data.Document
import com.example.imagesearch.databinding.FragmentSearchImageBinding
import com.example.imagesearch.listener.ImageClickListener
import com.example.imagesearch.manager.DocumentsManager
import com.example.imagesearch.retrofit.RetrofitInstance
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class SearchImageFragment : Fragment(), ImageClickListener {
    private var _binding: FragmentSearchImageBinding? = null
    private val binding get() = _binding!!
    private lateinit var imageAdapter: ImageAdapter
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentSearchImageBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        with(binding) {
            btnSearch.setOnClickListener {
                val query = etSearch.text.toString()
                if (query.isEmpty()) return@setOnClickListener
                saveData(query)//검색어 저장

                //Api 연결 시 IO 으로 연결 하고, UI갱신은 withContext로 Main에서 처리
                CoroutineScope(Dispatchers.IO).launch {
                    val responseData = RetrofitInstance.imageSearchApi.getImage(query = query)
                    DocumentsManager.addDocument(responseData.documents)
                    withContext(Dispatchers.Main) {
                        initRecyclerView()
                    }
                }

                downKeyBoard(requireContext(), etSearch)
            }
        }
    }

    private fun initRecyclerView() {
        binding.recyclerView.apply {
            imageAdapter = ImageAdapter(this@SearchImageFragment)
            adapter = imageAdapter
            layoutManager = GridLayoutManager(context, 2)
            //검색 결과 80개 표시
            val searchList = DocumentsManager.getDocument()
            val sublist = if (searchList.size > 80) searchList.subList(0, 80) else searchList
            imageAdapter.submitList(sublist)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }

    //이미지 클릭시 좋아요 표시
    override fun onClickImage(document: Document, position: Int) {
        DocumentsManager.toggleLike(document)
        imageAdapter.notifyItemChanged(position)
    }

    //키보드 내리기
    private fun downKeyBoard(context: Context, editText: EditText) {
        val inputMethodManager =
            context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        editText.clearFocus()
        inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
    }

    //검색어 저장
    private fun saveData(keyWord: String) {
        activity?.getSharedPreferences(SEARCH_WORD, Context.MODE_PRIVATE)?.edit {
            putString(KEYWORD, keyWord)
            apply()
        }
    }

    //저장된 검색어 가져오기
    private fun getData() {
        val saveKeyWord = activity?.getSharedPreferences(SEARCH_WORD, Context.MODE_PRIVATE)
        binding.etSearch.setText(saveKeyWord?.getString(KEYWORD, ""))
    }

    override fun onResume() {
        getData()
        initRecyclerView()
        super.onResume()
    }

    companion object {
        private const val SEARCH_WORD = "searchWord"
        private const val KEYWORD = "keyWord"
    }
}
728x90

1. Kakao Developers 접속 https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

 

2. 문서 확인 후 시작하기에서 API를 발급받는다. 

Kakao Developers 홈 → 시작하기 화면

 

 

3. 문서를 꼼꼼히 읽어본다.

이미지 검색하기API 의 파라미터 부분

 

※REST API키를 헤더에 담아 GET으로 요청

 

 

사용할 API를 찾아서 이해하기와 REST API 부분을 이해하고,  REST API 테스트 도구를 사용해서 직접 데이터를 받아 볼 수 있음 

 

728x90

gson - JSON 변환

Retrofit - HTTP Api 통신 

okhttp - 로그 확인용

spninner - 스피너 라이브러리   

라리브러리 추가

DustData 

import com.google.gson.annotations.SerializedName

data class Dust(val response: DustResponse)

data class DustResponse(
    @SerializedName("body")
    val dustBody: DustBody,
    @SerializedName("header")
    val dustHeader: DustHeader
)

data class DustBody(
    val totalCount: Int,
    @SerializedName("items")
    val dustItem: MutableList<DustItem>?,
    val pageNo: Int,
    val numOfRows: Int
)

data class DustHeader(
    val resultCode: String,
    val resultMsg: String
)

data class DustItem(
    val so2Grade: String,
    val coFlag: String?,
    val khaiValue: String,
    val so2Value: String,
    val coValue: String,
    val pm25Flag: String?,
    val pm10Flag: String?,
    val o3Grade: String,
    val pm10Value: String,
    val khaiGrade: String,
    val pm25Value: String,
    val sidoName: String,
    val no2Flag: String?,
    val no2Grade: String,
    val o3Flag: String?,
    val pm25Grade: String,
    val so2Flag: String?,
    val dataTime: String,
    val coGrade: String,
    val no2Value: String,
    val stationName: String,
    val pm10Grade: String,
    val o3Value: String
)

 

구조 

response 안에 body, header

body 안에 totalCount, items, pageNo, numOfRows

hearder 안에 resultCode, resultMsg 

 

변수와 해당 json 데이터 형식이 맞아야 하는데, @SerializedName("body") 어노테이션을 사용해서 연결시켜 줌 

 

NetWorkClient

package com.example.miseya.Retrofit

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object NetWorkClient {
    //https 로 변경
    private const val BASE_URL = "https://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"

    val dustNetWork :NetWorkInterface by lazy { dustRetrofit.create(NetWorkInterface::class.java) }

    private val dustRetrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }).build())//client 개발때 만 사용 배포시 삭제 -> 로그확인용
            .build()
    }
    
    방법 2
//    private fun createOkHttpClient(): OkHttpClient {
//        val interceptor = HttpLoggingInterceptor()
//
//        if (BuildConfig.DEBUG)
//            interceptor.level = HttpLoggingInterceptor.Level.BODY
//        else
//            interceptor.level = HttpLoggingInterceptor.Level.NONE
//
//        return OkHttpClient.Builder()
//            .connectTimeout(20, TimeUnit.SECONDS)
//            .readTimeout(20, TimeUnit.SECONDS)
//            .writeTimeout(20, TimeUnit.SECONDS)
//            .addNetworkInterceptor(interceptor)
//            .build()
//    }
//
//    private val dustRetrofit = Retrofit.Builder()
//        .baseUrl(DUST_BASE_URL)
//        .addConverterFactory(GsonConverterFactory.create())
//        .client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
//            level = HttpLoggingInterceptor.Level.BODY
//        }).build())
//        .build()
//
//    val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)

}

 

NetWorkInterface

package com.example.miseya.Retrofit

import com.example.miseya.data.Dust
import retrofit2.http.GET
import retrofit2.http.Query

private const val apiKey =
    "발급받은 키"
private const val SidoName = "서울"

interface NetWorkInterface {
    @GET("getCtprvnRltmMesureDnsty")
    suspend fun getDust(
        @Query("serviceKey") serviceKey: String = apiKey,
        @Query("returnType") returnType: String = "json",
        @Query("numOfRows") numOfRows: String = "50",
        @Query("pageNo") pageNo: String = "1",
        @Query("sidoName") sidoName: String = SidoName,
        @Query("ver") ver: String = "1.0",
    ): Dust
}

방법 2
//interface NetWorkInterface {
//    @GET("getCtprvnRltmMesureDnsty") //시도별 실시간 측정정보 조회 주소
//    suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
//}

 

MainActivity

import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.miseya.Retrofit.NetWorkClient
import com.example.miseya.data.DustItem
import com.example.miseya.databinding.ActivityMainBinding
import com.skydoves.powerspinner.IconSpinnerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    var items = mutableListOf<DustItem>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        with(binding) {
            spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
                //communicateNetWork(setUpDustParameter(text))
                communicateNetWork(text)
            }

            spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->

                var selectedItem = items.filter { f -> f.stationName == text }

                tvCityname.text = selectedItem[0].sidoName + "  " + selectedItem[0].stationName
                tvDate.text = selectedItem[0].dataTime
                tvP10value.text = selectedItem[0].pm10Value + " ㎍/㎥"

                when (getGrade(selectedItem[0].pm10Value)) {
                    1 -> {
                        mainBg.setBackgroundColor(Color.parseColor("#9ED2EC"))
                        ivFace.setImageResource(R.drawable.mise1)
                        tvP10grade.text = "좋음"
                    }

                    2 -> {
                        mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
                        ivFace.setImageResource(R.drawable.mise2)
                        tvP10grade.text = "보통"
                    }

                    3 -> {
                        mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
                        ivFace.setImageResource(R.drawable.mise3)
                        tvP10grade.text = "나쁨"
                    }

                    4 -> {
                        mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
                        ivFace.setImageResource(R.drawable.mise4)
                        tvP10grade.text = "매우나쁨"
                    }
                }
            }
        }

    }

    private fun communicateNetWork(sido: String) = CoroutineScope(Dispatchers.IO).launch {
        //GET 응답 데이터
        val resourceData = NetWorkClient.dustNetWork.getDust(sidoName = sido)
        items = resourceData.response.dustBody.dustItem!!

        //선택한 해당 시/도 의 측정 위치 stationName(문서참조)를 추가
        val goo = ArrayList<String>()
        items.forEach {
            goo.add(it.stationName)
        }
        withContext(Dispatchers.Main) {
            //Api 를 받아오는 작업을 코루틴에서 비동기로 처리 -> Dispatchers.IO
            //UI를 변경 하기 위해 withContext Dispatchers.Main으로 스위칭(UI작업은 메인스레드에서만)
            binding.spinnerViewGoo.setItems(goo)
            val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
        }

    }
방법 2 
//    private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
//        val responseData = NetWorkClient.dustNetWork.getDust(param)       
//
//        val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
//        items = responseData.response.dustBody.dustItem!!
//
//        val goo = ArrayList<String>()
//        items.forEach {
//            goo.add(it.stationName)
//        }
//
//        runOnUiThread {
//            binding.spinnerViewGoo.setItems(goo)
//        }
//
//    }

//    private fun setUpDustParameter(sido: String): HashMap<String, String> {
//        val authKey =
//            "발급받은 키"
//
//        return hashMapOf(
//            "serviceKey" to authKey,
//            "returnType" to "json",
//            "numOfRows" to "100",
//            "pageNo" to "1",
//            "sidoName" to sido,
//            "ver" to "1.0"
//        )
//    }

    fun getGrade(value: String): Int {
        val mValue = value.toInt()
        var grade = 1
        grade = if (mValue >= 0 && mValue <= 30) {
            1
        } else if (mValue >= 31 && mValue <= 80) {
            2
        } else if (mValue >= 81 && mValue <= 100) {
            3
        } else 4
        return grade
    }
}

 

주석 처리부분 -> @QueryMap, HashMap으로 응답 데이터를 받는 방법 

xml 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_bg"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#9ED2EC"
    tools:context=".MainActivity">


    <com.skydoves.powerspinner.PowerSpinnerView
        android:id="@+id/spinnerView_sido"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@color/md_green_200"
        android:foreground="?attr/selectableItemBackground"
        android:gravity="center"
        android:hint="도시 선택"
        android:padding="10dp"
        android:textColor="@color/white_93"
        android:textColorHint="@color/white_70"
        android:textSize="14.5sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/spinnerView_goo"
        app:spinner_arrow_gravity="end"
        app:spinner_arrow_tint="@color/yellow"
        app:spinner_divider_color="@color/white_70"
        app:spinner_divider_show="true"
        app:spinner_divider_size="0.4dp"
        app:spinner_item_array="@array/Sido"
        app:spinner_item_height="46dp"
        app:spinner_popup_animation="normal"
        app:spinner_popup_background="@color/background800"
        app:spinner_popup_elevation="14dp"
        tools:ignore="HardcodedText,UnusedAttribute" />

    <com.skydoves.powerspinner.PowerSpinnerView
        android:id="@+id/spinnerView_goo"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@color/md_green_100"
        android:foreground="?attr/selectableItemBackground"
        android:gravity="center"
        android:hint="지역 선택"
        android:padding="10dp"
        android:textColor="@color/white_93"
        android:textColorHint="@color/white_70"
        android:textSize="14.5sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/spinnerView_sido"
        app:layout_constraintTop_toTopOf="parent"
        app:spinner_arrow_gravity="end"
        app:spinner_arrow_tint="@color/yellow"
        app:spinner_divider_color="@color/white_70"
        app:spinner_divider_show="true"
        app:spinner_divider_size="0.4dp"
        app:spinner_item_height="46dp"
        app:spinner_popup_animation="normal"
        app:spinner_popup_background="@color/background800"
        app:spinner_popup_elevation="14dp"
        tools:ignore="HardcodedText,UnusedAttribute" />


    <ImageView
        android:id="@+id/iv_face"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/mise1" />

    <TextView
        android:id="@+id/tv_p10value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text=" - ㎍/㎥"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/iv_face" />

    <TextView
        android:id="@+id/tv_p10grade"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text=""
        android:textColor="#048578"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_p10value" />

    <TextView
        android:id="@+id/tv_cityname"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="50dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="도시를 선택해 주세요."
        android:textColor="#242323"
        android:textSize="36sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/iv_face"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="8dp"
        android:text=""
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_cityname" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

 

Manifest에 인터넷 권한 추가 

<uses-permission android:name="android.permission.INTERNET"/>

EnPoint(BaseUrl) 이 https 가 아닌경우 android:usesCleartextTraffic="true" 추가해줌

android:usesCleartextTraffic="true"
728x90

공공데이터 api를 사용하기 위한 준비과정

 

1. 사이트 접속 https://www.data.go.kr/index.do

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

2. 로그인 & 사용하려는 api 검색 

공공데이터 포털

3. 오픈API선택 & 활용신청

오픈 API 필터

4.서비스 유형 & 데이터 포맷(JSON or XML ...등) & End Point(BaseURL) & 상세설명 확인

5.ex) 요청주소 GET 방식으로 시도별 실시간 측정정보 조회,

서비스URL -> BaseURL,

요청변수 확인

 

6. 샘플데이터 확인

 

자료출처 - http://www.data.go.kr

728x90

매니페스트, xml, 종속성 추가 과정 

https://developers.google.com/maps/documentation/android-sdk/config?hl=ko

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mapwithmarker">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />

        <!--
             The API key for Google Maps-based APIs.
        -->
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />

        <activity
            android:name=".MapsMarkerActivity"
            android:label="@string/title_activity_maps"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

xml

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.mapwithmarker.MapsMarkerActivity" />

 

dependencies 에 라이브러리 추가 

implementation ("com.google.android.gms:play-services-maps:18.2.0")
implementation ("com.google.android.gms:play-services-location:21.1.0")

 

 

Activity의 onCreate() 메서드에서 레이아웃 파일을 콘텐츠 뷰로 설정합니다. FragmentManager.findFragmentById()를 호출하여 지도 프래그먼트의 핸들을 가져옵니다. 그런 다음 getMapAsync()를 사용하여 지도 콜백에 등록합니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Retrieve the content view that renders the map.
    setContentView(R.layout.activity_maps)

    // Get the SupportMapFragment and request notification when the map is ready to be used.
    val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as? SupportMapFragment
    mapFragment?.getMapAsync(this)
}

 

OnMapReadyCallback 인터페이스를 구현하고, GoogleMap 객체를 사용할 수 있을 때 지도를 설정하도록 onMapReady() 메서드를 재정의합니다.

class MapsMarkerActivity : AppCompatActivity(), OnMapReadyCallback {

    // ...

    override fun onMapReady(googleMap: GoogleMap) {
      val sydney = LatLng(-33.852, 151.211)
      googleMap.addMarker(
        MarkerOptions()
          .position(sydney)
          .title("Marker in Sydney")
      )
    }
}

 

 

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

/**
 * 특정 위치를 나타내는 마커(핀)와 함께 Google 지도를 표시하는 활동입니다.
 */
class MapsMarkerActivity : AppCompatActivity(), OnMapReadyCallback {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Retrieve the content view that renders the map.
        setContentView(R.layout.activity_maps)

        //SupportMapFragment를 가져오고 지도를 사용할 준비가 되면 알림을 요청합니다.
        val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as? SupportMapFragment
        mapFragment?.getMapAsync(this)
    }

    override fun onMapReady(googleMap: GoogleMap) {
      val sydney = LatLng(-33.852, 151.211)
      googleMap.addMarker(
        MarkerOptions()
          .position(sydney)
          .title("Marker in Sydney")
      )
      googleMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
    }
}
 
지도에서 사용자 이벤트 처리
  • GoogleMap.OnMapClickListener: 지도 클릭 이벤트
  • GoogleMap.OnMapLongClickListener: 지도 롱 클릭 이벤트
  • GoogleMap.OnMarkerClickListener: 마커 클릭 이벤트
  • GoogleMap.OnMarkerDragListener: 마커 드래그 이벤트
  • GoogleMap.OnInfoWindowClickListener: 정보 창 클릭 이벤트
  • GoogleMap.OnCameraIdleListener: 지도 화면 변경 이벤트
 
googleMap?.setOnMapClickListener { latLng ->
            Log.d("map_test", "click : ${latLng.latitude} , ${latLng.longitude}")
        }
        
googleMap?.setOnMapLongClickListener { latLng ->
            Log.d("map_test", "long click : ${latLng.latitude} , ${latLng.longitude}")
        }
        
googleMap?.setOnCameraIdleListener {
            val position = googleMap!!.cameraPosition
            val zoom = position.zoom
            val latitude = position.target.latitude
            val longitude = position.target.longitude
            Log.d("map_test", "User change : $zoom $latitude , $longitude")
        }
        
googleMap?.setOnMarkerClickListener { marker ->
            true
        }
        
googleMap?.setOnInfoWindowClickListener { marker ->
        }

 

 

참고 

https://developers.google.com/maps/documentation/android-sdk/map-with-marker?hl=ko#kotlin

 

마커가 포함된 지도 추가  |  Android용 Maps SDK  |  Google for Developers

의견 보내기 마커가 포함된 지도 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 튜토리얼에서는 Android 앱에 Google 지도를 추가하는 방법을 설명합니

developers.google.com

https://developers.google.com/maps/documentation/android-sdk/config?hl=ko

 

Android 스튜디오 프로젝트 설정  |  Android용 Maps SDK  |  Google for Developers

의견 보내기 Android 스튜디오 프로젝트 설정 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 빠른 시작에 자세히 설명되어 있는 Google 지도

developers.google.com

간단한 예제 

import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import com.example.googlemap.databinding.ActivityMainBinding
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

class MainActivity : AppCompatActivity(), OnMapReadyCallback {
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!
    lateinit var mGoogleMap: GoogleMap
    lateinit var fusedLocationClient: FusedLocationProviderClient //위치 서비스
    lateinit var locationCallback: LocationCallback //위치 정보 받기 위한 콜백
    lateinit var locationPermission: ActivityResultLauncher<Array<String>> //퍼미션
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        locationPermission =
            registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
                if (result.all { it.value }) {
                    (supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment)!!.getMapAsync(
                        this
                    )//getMapAsync -> onMapReady
                } else {//문제 발생시
                    Toast.makeText(this, "권한 승인이 필요합니다.", Toast.LENGTH_SHORT).show()
                }
            }
        //권한 요청
        locationPermission.launch(
            arrayOf(
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION,
            ),
        )

    }

    //지도 객체를 이용할 수 있는 상황이 될 때 /권한 허용된 상태라면 getMapAsync
    override fun onMapReady(p0: GoogleMap) {
        //onMapReady가 되면 마커 하나
        val seoul = LatLng(37.566610, 126.978403)
        mGoogleMap = p0
        mGoogleMap.mapType = GoogleMap.MAP_TYPE_NORMAL // default 노말 생략 가능
        mGoogleMap.apply {
            val markerOptions = MarkerOptions()
            markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
            markerOptions.position(seoul)
            markerOptions.title("서울시청")
            markerOptions.snippet("Tel:01-120")
            addMarker(markerOptions)
        }
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        updateLocation()//위치 업데이트
    }

    fun updateLocation() {
        val locationRequest = LocationRequest.create().apply {
            interval = 1000 //1초 마다 갱신  
            fastestInterval = 500
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }
        locationCallback = object : LocationCallback() {
            //1초에 한번씩 변경된 위치 정보가 onLocationResult 로 전달된다.
            override fun onLocationResult(locationResult: LocationResult) {
                locationResult?.let {
                    for (location in it.locations) {
                        Log.d("위치 정보", "위도 : {$location.")
                        setLastLocation(location)//계속 실시간으로 위치를 받아오고 있기 때문에 맵을 확대해도 다시 줄어든다.
                    }
                }
            }
        }

        //권한 처리
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                this, Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }

        fusedLocationClient.requestLocationUpdates(
            locationRequest, locationCallback,
            Looper.myLooper()!!
        )
    }

    fun setLastLocation(lastLocation: Location) {
        val LATLNG = LatLng(lastLocation.latitude, lastLocation.longitude)
        val markerOptions = MarkerOptions().position(LATLNG).title("나 여기 있어용~")
        val cameraPosition = CameraPosition.Builder().target(LATLNG).zoom(15.0f).build()

        mGoogleMap.addMarker(markerOptions)
        mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
    }
}
728x90

구글 지도를 사용하기 위해 API 를 받아 오는 과정 (API KEY 생성 과정)  

 

1. 구글 클라우드  콘솔이동 http://console.cloud.google.com/ (로그인 -> 카드등록)

콘솔 화면

 

2. Google Maps Platfrom 이동 

maps 입력 -> Google Maps Platform

 

3. API 키 생성 및 복사 

 

4.제한필요하면 제한추가

제한사항 설정

 

 

728x90

in 을 사용하다 보면 평소에 생각하지 않고 외운것 처럼 자연스럽게 사용 하다가도,가끔 헷갈릴 때가 있다. in 앞에 조건이 뒤 조건에 포함 되어있는건지 in 뒤에 조건이 앞에 조건에 포함되어 있는건지 그래서 기억하기 쉽게 정리를 해본다.

결론부터 얘기 하면, in 앞의 조건이 in (Range) 안에 있으면 true 아니면 false      

fun main() {
    var numList = listOf(1, 2, 3, 4, 5)
    var numList2 = listOf(1, 3, 5, 7, 9)
    
    if (3 in numList) {
        println("3은 numList 컬렉션에 포함되어 있습니다.")
    } else {
        println("3은 numList 컬렉션에 포함되어 있지 않습니다.")
    }
    //3은 numList 컬렉션에 포함되어 있습니다.
    
    val x = 5
    
    if (x in 1..10) {
        println("$x 는 1과 10 사이에 있습니다.")
    } else {
        println("$x 는 1과 10 사이에 없습니다.")
    }
    //5 는 1과 10 사이에 있습니다.
    
    println("filter -> 0과 6사이에 있는 numList2 ${numList2.filter { it in (0..6) }}")
    //filter -> 0과 6사이에 있는 numList2 [1, 3, 5]
   
    println("filter -> 0과 6사이에 없는 numList2 ${numList2.filter { it !in (0..6) }}")
 	//filter -> 0과 6사이에 없는 numList2 [7, 9]
   
    val string = "abcdefg"
    val string2 = "abc"
   
   
    if(string2 in string){
        println("$string2 는 $string 안에 포함 됩니다.")
    }else{
        println("$string2 는 $string 안에 포함 되지 않습니다.")
    }
    //abc 는 abcdefg 안에 포함 됩니다.
}

+ Recent posts