Material-UI (mui)는 웹 애플리케이션을 빌드하기 위한 리액트 기반의 UI 프레임워크이다. Google이 2014년 안드로이드 스마트폰에 적용하면서 알려졌으며, Material Design을 기반으로하였기에 시각적으로 훌륭하고 사용하기 쉽다. 때문에 개발자들도 스타일리쉬한 쉬운 사용자 인터페이스를 개발을 손쉽게 할 수 있다. 이러한 Material-UI는 컴포넌트 기반 아키텍처를 사용하여 재사용 가능하고 일관된 디자인 요소를 구축하도록 한다.
특징
CSS-in-JS 다른 UI 라이브러리들과 마찬가지로, js에서 css를 관리하는 방식으로 스타일링한다. 이는 컴포넌트별로 스타일을 정의하고 적용하는데 유용하다.
컴포넌트 기반 mui는 다양한 UI 요소를 컴포넌트 형태로 제공하며, 이러한 컴포넌트를 조합하여 원하는 인터페이스를 구성할 수 있다.
반응형 디자인 mui 컴포넌트는 다양한 화면 크기와 장치에 맞게 반응한다. 모바일 기기부터 데스크톱까지 일관된 사용자 경험을 제공해준다.
테마 커스터마이징 테마를 사용하여 앱의 전반적인 스타일을 일관되게 제어하거나 수정할 수 있다. 색상, 글꼴 등을 커스터마이징하여 앱을 더 맞춤화할 수 있다.
문서화와 예제 mui는 잘 구성된 예제 문서와 ts전용 코드 또한 제공하기 때문에 ts react 개발자가 손쉽게 접근할 수 있다.
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"
}
}
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)
}
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))
}
}
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 안에 포함 됩니다.
}